first commit

This commit is contained in:
Your Name
2026-09-08 11:40:15 +08:00
commit a5353f7eb5
9568 changed files with 1646214 additions and 0 deletions
+455
View File
@@ -0,0 +1,455 @@
<template>
<view class="card-container">
<!-- 顶部标题栏 -->
<!-- 就诊卡列表 -->
<view class="card-list">
<view
v-for="card in cardList"
:key="card.id"
class="card-item"
@click="viewCardDetail(card)"
>
<!-- 卡片头部 -->
<view class="card-header">
<view class="card-id">
<text class="label">诊单编号</text>
<text class="value">{{ card.id }}</text>
</view>
<view :class="['status-badge', getStatusClass(card.status)]">
{{ card.status_desc || getStatusText(card.status) }}
</view>
</view>
<!-- 患者信息 -->
<view class="card-info">
<view class="info-row">
<text class="info-label">患者姓名</text>
<text class="info-value">{{ card.patient_name || '-' }}</text>
</view>
<view class="info-row">
<text class="info-label">性别年龄</text>
<text class="info-value">{{ card.gender_desc || '-' }} / {{ card.age }}</text>
</view>
<view class="info-row">
<text class="info-label">诊断类型</text>
<text class="info-value">{{ getDiagnosisTypeName(card.diagnosis_type) }}</text>
</view>
<view class="info-row">
<text class="info-label">证型</text>
<text class="info-value">{{ getSyndromeTypeName(card.syndrome_type) }}</text>
</view>
</view>
<!-- 时间信息 -->
<view class="card-footer">
<view class="time-info">
<text class="time-label">诊断日期</text>
<text class="time-value">{{ card.diagnosis_date_text || formatDate(card.diagnosis_date) }}</text>
</view>
<view class="time-info">
<text class="time-label">创建时间</text>
<text class="time-value">{{ card.create_time || '-' }}</text>
</view>
</view>
<!-- 快捷入口 -->
<view class="card-actions">
<view class="card-action-btn primary" @click.stop="viewDailyRecord(card)">
<text class="card-action-icon">📈</text>
<text class="card-action-text">日常记录</text>
</view>
<view class="card-action-btn" @click.stop="viewCardDetail(card)">
<text class="card-action-icon">📝</text>
<text class="card-action-text">查看 / 编辑</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="!loading && cardList.length === 0" class="empty-state">
<uni-icons type="wallet" size="80" color="#1890ff"></uni-icons>
<text class="empty-text">暂无就诊卡</text>
<view class="add-card-btn" @click="createCard">新建就诊卡</view>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading-state">
<text class="loading-text">加载中...</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, getCurrentInstance } from 'vue'
import { onLaunch, onShow, onHide, onError ,onShareAppMessage} from '@dcloudio/uni-app'
const { proxy } = getCurrentInstance()
// 数据
const cardList = ref([])
const loading = ref(false)
// 页面加载时获取数据
onMounted(() => {
loadCardList()
})
onShow(() => {
loadCardList()
})
// 加载就诊卡列表
const loadCardList = async () => {
loading.value = true
try {
// 检查token是否存在
const token = uni.getStorageSync('token')
if (!token) {
uni.showToast({ title: '请先登录', icon: 'none' })
// setTimeout(() => {
// uni.redirectTo({ url: '/pages/login/login' })
// }, 1500)
return
}
// 获取当前用户的patient_id
const userData = uni.getStorageSync('userData')
const patientId = userData?.diagnosis?.patient_id
if (!patientId) {
//uni.showToast({ title: '患者信息不存在', icon: 'none' })
return
}
// 调用后端接口获取就诊卡列表
const res = await proxy.apiUrl({
url: '/api/tcm/getCardList',
method: 'GET',
data: {
patient_id: patientId
}
})
if (res.code === 1) {
cardList.value = res.data || []
} else {
uni.showToast({ title: res.msg || '获取失败', icon: 'none' })
}
} catch (err) {
console.error('加载就诊卡列表失败:', err)
//uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
// 查看卡片详情
const viewCardDetail = (card) => {
uni.navigateTo({
url: `/pages/Card/edit_card?id=${card.id}`
})
}
// 查看日常记录(血糖血压 / 饮食 / 运动 / 跟踪备注 + 波浪图)
const viewDailyRecord = (card) => {
const url = `/tongji/pages/index?diagnosis_id=${card.id}` +
`&patient_id=${card.patient_id || ''}` +
`&patient_name=${encodeURIComponent(card.patient_name || '')}` +
`&age=${card.age || ''}` +
`&gender=${card.gender || ''}`
uni.navigateTo({ url })
}
// 新建就诊卡(patient_id 创建后自动生成,仅需登录)
const createCard = () => {
const token = uni.getStorageSync('token')
if (!token) {
uni.showToast({ title: '请先登录', icon: 'none' })
return
}
uni.navigateTo({
url: '/pages/Card/edit_card?add=1'
})
}
// 获取状态文本
const getStatusText = (status) => {
return status === 1 ? '启用' : '禁用'
}
// 获取状态样式类
const getStatusClass = (status) => {
return status === 1 ? 'status-confirmed' : 'status-pending'
}
// 诊断类型字典
const diagnosisTypeMap = {
'first_visit': '初诊',
'follow_up': '复诊',
'consultation': '会诊'
}
// 证型字典
const syndromeTypeMap = {
'qi_deficiency': '气虚',
'blood_deficiency': '血虚',
'yin_deficiency': '阴虚',
'yang_deficiency': '阳虚',
'qi_stagnation': '气滞',
'blood_stasis': '血瘀',
'phlegm_dampness': '痰湿',
'damp_heat': '湿热',
'cold_dampness': '寒湿',
'wind_cold': '风寒',
'wind_heat': '风热'
}
// 获取诊断类型名称
const getDiagnosisTypeName = (type) => {
return diagnosisTypeMap[type] || type || '-'
}
// 获取证型名称
const getSyndromeTypeName = (type) => {
return syndromeTypeMap[type] || type || '-'
}
// 格式化日期(时间戳转日期)
const formatDate = (timestamp) => {
if (!timestamp) return '-'
const date = new Date(timestamp * 1000)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
</script>
<style lang="less" scoped>
/* 适老化设计:50-80岁 - 大字号、高对比、大触控区 */
.card-container {
min-height: 100vh;
background: linear-gradient(180deg, #e8f0fa 0%, #ffffff 100%);
padding-bottom: 56rpx;
}
.header {
background: #ffffff;
padding: 48rpx 40rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
.header-title {
font-size: 48rpx;
font-weight: bold;
color: #1890ff;
}
}
.card-list {
padding: 40rpx;
}
.card-item {
background: #ffffff;
border-radius: 28rpx;
padding: 44rpx;
margin-bottom: 36rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s;
min-height: 200rpx;
&:active {
transform: scale(0.98);
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
padding-bottom: 28rpx;
border-bottom: 4rpx solid #e8eaed;
}
.card-id {
.label {
font-size: 34rpx;
color: #555;
}
.value {
font-size: 40rpx;
font-weight: bold;
color: #1a1a1a;
}
}
.status-badge {
padding: 12rpx 28rpx;
border-radius: 28rpx;
font-size: 32rpx;
font-weight: 500;
&.status-confirmed {
background: #f6ffed;
color: #204e2b;
border: 4rpx solid #204e2b;
}
&.status-pending {
background: #fff7e6;
color: #fa8c16;
border: 4rpx solid #ffd591;
}
}
.card-info {
margin-bottom: 28rpx;
}
.info-row {
display: flex;
align-items: center;
margin-bottom: 24rpx;
.info-label {
font-size: 34rpx;
color: #555;
min-width: 200rpx;
}
.info-value {
font-size: 36rpx;
color: #1a1a1a;
font-weight: 500;
}
}
.card-footer {
padding-top: 28rpx;
border-top: 4rpx solid #e8eaed;
}
.card-actions {
display: flex;
gap: 20rpx;
margin-top: 28rpx;
padding-top: 28rpx;
border-top: 4rpx solid #e8eaed;
}
.card-action-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
padding: 24rpx 12rpx;
background: #f6faff;
border-radius: 20rpx;
border: 2rpx solid #d6e6fb;
min-height: 80rpx;
transition: all 0.2s;
&:active {
transform: scale(0.98);
opacity: 0.85;
}
&.primary {
background: linear-gradient(135deg, #1890ff, #0ea5a4);
border-color: transparent;
.card-action-icon,
.card-action-text {
color: #ffffff;
}
}
}
.card-action-icon {
font-size: 36rpx;
color: #1890ff;
}
.card-action-text {
font-size: 30rpx;
color: #1890ff;
font-weight: 500;
}
.time-info {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.time-label {
font-size: 30rpx;
color: #555;
min-width: 180rpx;
}
.time-value {
font-size: 32rpx;
color: #555;
}
}
.view-count {
display: flex;
align-items: center;
margin-top: 12rpx;
.count-icon {
font-size: 34rpx;
margin-right: 12rpx;
}
.count-text {
font-size: 30rpx;
color: #555;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 160rpx 0;
.empty-icon {
font-size: 140rpx;
margin-bottom: 40rpx;
}
.empty-text {
font-size: 40rpx;
color: #555;
margin-bottom: 56rpx;
}
}
.add-card-btn {
padding: 36rpx 88rpx;
background: #1890ff;
color: #ffffff;
font-size: 40rpx;
font-weight: 500;
border-radius: 56rpx;
min-height: 100rpx;
}
.loading-state {
display: flex;
justify-content: center;
padding: 120rpx 0;
.loading-text {
font-size: 36rpx;
color: #555;
}
}
</style>
+287
View File
@@ -0,0 +1,287 @@
<template>
<view class="card-container">
<!-- 顶部标题栏 -->
<view class="header">
<text class="header-title">{{ data.title }}</text>
</view>
<!-- 就诊卡列表 -->
<view class="card-list" v-html="data.content">
</view>
</view>
</template>
<script setup>
import { ref, onMounted, getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
// 数据
const data = ref({})
const loading = ref(false)
const type=ref('')
// 页面加载时获取数据
onMounted(() => {
loadCardList()
})
// 加载就诊卡列表
const loadCardList = async () => {
loading.value = true
try {
// 检查token是否存在
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
type.value = currentPage.options.type
// 调用后端接口获取就诊卡列表
const res = await proxy.apiUrl({
url: '/api/index/policy',
method: 'GET',
data: {
type: type.value
}
})
data.value = res.data
uni.setNavigationBarTitle({
title: res.data.title
});
} catch (err) {
console.error('加载就诊卡列表失败:', err)
//uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
// 查看卡片详情
const viewCardDetail = (card) => {
uni.navigateTo({
url: `/pages/Card/edit_card?id=${card.id}`
})
}
// 获取状态文本
const getStatusText = (status) => {
return status === 1 ? '启用' : '禁用'
}
// 获取状态样式类
const getStatusClass = (status) => {
return status === 1 ? 'status-confirmed' : 'status-pending'
}
// 诊断类型字典
const diagnosisTypeMap = {
'first_visit': '初诊',
'follow_up': '复诊',
'consultation': '会诊'
}
// 证型字典
const syndromeTypeMap = {
'qi_deficiency': '气虚',
'blood_deficiency': '血虚',
'yin_deficiency': '阴虚',
'yang_deficiency': '阳虚',
'qi_stagnation': '气滞',
'blood_stasis': '血瘀',
'phlegm_dampness': '痰湿',
'damp_heat': '湿热',
'cold_dampness': '寒湿',
'wind_cold': '风寒',
'wind_heat': '风热'
}
// 获取诊断类型名称
const getDiagnosisTypeName = (type) => {
return diagnosisTypeMap[type] || type || '-'
}
// 获取证型名称
const getSyndromeTypeName = (type) => {
return syndromeTypeMap[type] || type || '-'
}
// 格式化日期(时间戳转日期)
const formatDate = (timestamp) => {
if (!timestamp) return '-'
const date = new Date(timestamp * 1000)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
</script>
<style lang="less" scoped>
.card-container {
min-height: 100vh;
background: linear-gradient(180deg, #f0f7ff 0%, #ffffff 100%);
padding-bottom: 40rpx;
}
.header {
background: #ffffff;
padding: 40rpx 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
.header-title {
font-size: 40rpx;
font-weight: bold;
color: #1890ff;
}
}
.card-list {
padding: 30rpx;
}
.card-item {
background: #ffffff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s;
&:active {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.card-id {
.label {
font-size: 28rpx;
color: #666666;
}
.value {
font-size: 32rpx;
font-weight: bold;
color: #333333;
}
}
.status-badge {
padding: 8rpx 20rpx;
border-radius: 20rpx;
font-size: 24rpx;
font-weight: 500;
&.status-confirmed {
background: #f6ffed;
color: #52c41a;
border: 2rpx solid #b7eb8f;
}
&.status-pending {
background: #fff7e6;
color: #fa8c16;
border: 2rpx solid #ffd591;
}
}
.card-info {
margin-bottom: 20rpx;
}
.info-row {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.info-label {
font-size: 28rpx;
color: #666666;
min-width: 160rpx;
}
.info-value {
font-size: 30rpx;
color: #333333;
font-weight: 500;
}
}
.card-footer {
padding-top: 20rpx;
border-top: 2rpx solid #f0f0f0;
}
.time-info {
display: flex;
align-items: center;
margin-bottom: 12rpx;
.time-label {
font-size: 24rpx;
color: #999999;
min-width: 140rpx;
}
.time-value {
font-size: 26rpx;
color: #666666;
}
}
.view-count {
display: flex;
align-items: center;
margin-top: 8rpx;
.count-icon {
font-size: 28rpx;
margin-right: 8rpx;
}
.count-text {
font-size: 24rpx;
color: #999999;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty-icon {
font-size: 120rpx;
margin-bottom: 30rpx;
}
.empty-text {
font-size: 32rpx;
color: #999999;
}
}
.loading-state {
display: flex;
justify-content: center;
padding: 80rpx 0;
.loading-text {
font-size: 28rpx;
color: #999999;
}
}
</style>
File diff suppressed because it is too large Load Diff
+695
View File
@@ -0,0 +1,695 @@
<template>
<view class="container">
<!-- 搜索栏 -->
<!-- <view class="search-section">
<view class="search-bar">
<text class="search-icon">🔍</text>
<input class="search-input" placeholder="搜索药材、医生或症状..." type="text" />
<text class="search-filter"></text>
</view>
</view> -->
<!-- 平台资质 -->
<view class="qualification-section">
<view class="section-header">
<text class="section-title">平台资质</text>
<!-- <text class="more-link" @click="navigateToQualifications">查看详情</text> -->
</view>
<view class="qualification-grid">
<view class="qual-col-left">
<view
class="qualification-card large"
@click="navigateToQualification(qualifications[0])"
>
<view class="qual-card-inner qual-primary">
<view class="qual-icon"><image src="/static/wjw.png" style="width: 100rpx; margin-left: -5px;" mode="widthFix"></image></view>
<text class="qual-name text-white">卫健委认证机构</text>
<text class="qual-desc text-white-sub">互联网诊疗资质备案</text>
</view>
</view>
</view>
<view class="qual-col-right">
<view
class="qualification-card small"
@click="navigateToQualification(qualifications[1])"
>
<view class="qual-card-inner qual-beige">
<view class="qual-icon-wrap">
<view class="qual-icon"><image src="/static/yy.png" style="width: 100rpx; margin-left: -10px;" mode="widthFix"></image></view>
</view>
<view>
<text class="qual-name">互联网医院</text>
<text class="qual-desc">持证合规运营</text>
</view>
</view>
</view>
<view
class="qualification-card small"
@click="navigateToAllDoctors"
>
<view class="qual-card-inner qual-grey">
<view class="qual-icon-wrap">
<view class="qual-icon"><image src="/static/ys.png" style="width: 90rpx; margin-left: -5px;" mode="widthFix"></image></view>
</view>
<view>
<text class="qual-name">执业医师认证</text>
<text class="qual-desc">持证医师在线问诊</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 名医推荐 -->
<view class="doctors-section">
<view class="section-header">
<text class="section-title">名医推荐</text>
<text class="more-link" @click="navigateToAllDoctors">查看全部</text>
</view>
<view class="doctors-list">
<view
v-for="doctor in doctors"
:key="doctor.id"
class="doctor-card"
@click="selectDoctor(doctor)"
>
<image :src="doctor.avatar || defaultAvatar" class="doctor-photo" mode="aspectFill"></image>
<view class="doctor-info">
<view class="doctor-top">
<text class="doctor-name">{{ doctor.name }}</text>
<text class="doctor-rating"> {{ doctor.rating || '4.9' }}</text>
</view>
<text class="doctor-specialty">{{ doctor.specialty || doctor.title || '中医' }}</text>
<view class="doctor-price-row">
<text class="doctor-price">{{ doctor.price || '' }}</text>
<text class="doctor-unit" v-if="doctor.title">{{doctor.title}}</text>
</view>
<view class="doctor-actions">
<view class="appointment-btn" @click.stop="selectDoctor(doctor)">预约</view>
</view>
</view>
</view>
</view>
</view>
<!-- 健康百科 -->
<view class="encyclopedia-section">
<view class="section-header">
<text class="section-title">健康百科</text>
<text class="more-link" @click="navigateToEncyclopedia">查看全部</text>
</view>
<scroll-view class="encyclopedia-scroll no-scrollbar" scroll-x :show-scrollbar="false">
<view
v-for="(article, index) in articles"
:key="article.id"
class="article-card"
:class="{ 'article-card-secondary': index % 2 === 1 }"
@click="navigateToArticle(article)"
>
<view class="article-image-wrap">
<image :src="article.cover" class="article-image" mode="aspectFill"></image>
<view class="article-gradient" :class="{ 'gradient-secondary': index % 2 === 1 }"></view>
<view class="article-tag" :class="{ 'tag-secondary': index % 2 === 1 }">{{ article.tag }}</view>
</view>
<text class="article-title" :class="{ 'title-secondary': index % 2 === 1 }">{{ article.title }}</text>
<text class="article-desc">{{ article.desc }}</text>
</view>
</scroll-view>
</view>
<TabBarDock :active="0" />
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getCurrentInstance } from 'vue'
import { onShareAppMessage } from '@dcloudio/uni-app'
import TabBarDock from '@/components/app-tab-bar/tab-bar-dock.vue'
const { proxy } = getCurrentInstance()
const doctors = ref([])
const loading = ref(false)
const defaultAvatar = '/static/user/user.png'
// 平台资质
const qualifications = ref([
{ id: 1, name: '卫健委认证机构', desc: '互联网诊疗资质备案', icon: '✓', dark: true, size: 'large' },
{ id: 2, name: '互联网医院', desc: '持证合规运营', icon: '', dark: false, size: 'small' },
{ id: 3, name: '执业医师认证', desc: '持证医师在线问诊', icon: '★', dark: false, size: 'small' }
])
// 健康百科
const articles = ref([])
const fetchArticles = async () => {
try {
const response = await proxy.apiUrl({
url: '/api/article/lists',
method: 'GET',
data: { page_no: 1, page_size: 10 }
})
if (response && response.code === 1) {
articles.value = (response.data.lists || []).map(item => ({
...item,
cover: item.image,
tag: item.cid || ''
}))
}
} catch (error) {
console.error('获取文章列表失败:', error)
}
}
const selectDoctor = (doctor) => {
uni.navigateTo({
url: `/doctor/pages/doctor/doctor?id=${doctor.id}`
})
}
const navigateToQualifications = () => {
uni.showToast({ title: '资质详情', icon: 'none' })
}
const navigateToQualification = (item) => {
// 根据类型打开不同的协议页面
uni.navigateTo({
url: '/pages/Card/contact?type=health'
})
}
const navigateToAllDoctors = () => {
uni.navigateTo({
url: '/doctor/pages/doctor/list/list'
})
}
const navigateToEncyclopedia = () => {
uni.showToast({ title: '健康百科', icon: 'none' })
}
const navigateToArticle = (article) => {
uni.navigateTo({ url: `/doctor/pages/article/article?id=${article.id}` })
}
const fetchDoctors = async () => {
loading.value = true
try {
const response = await proxy.apiUrl({
url: '/api/doctor/lists',
method: 'GET',
data: {
page_no: 1,
page_size: 10
}
})
if (response && response.code === 1) {
const data = response.data
const doctorList = data.lists || []
doctors.value = doctorList
} else {
console.warn('获取医生列表失败:', response?.msg)
}
} catch (error) {
console.error('请求异常:', error)
} finally {
loading.value = false
}
}
const userinfo = async (code) => {
try {
// 通过 proxy 调用全局的 apiUrl
const res = await proxy.apiUrl({
url: '/api/user/info',
method: 'POST',
}, false) // 不显示加载中
uni.setStorageSync('userData', res.data)
} catch (err) {
}
}
onMounted(() => {
fetchDoctors()
fetchArticles()
userinfo()
})
</script>
<style scoped lang="scss">
/* TCM Care 设计系统 - 源自 code.html */
$background: #faf9f5;
$primary: #204e2b;
$primary-container: #386641;
$on-primary: #ffffff;
$on-primary-container: #afe2b3;
$secondary-container: #fdc39a;
$on-secondary-container: #794e2e;
$surface-container-lowest: #ffffff;
$surface-container-low: #f4f4f0;
$surface-container-high: #e9e8e4;
$surface-container-highest: #e3e2df;
$on-surface: #1b1c1a;
$on-surface-variant: #414941;
$tertiary-container: #4b6500;
$tertiary: #384c00;
$outline: #727970;
.container {
background: $background;
min-height: 100vh;
padding: 24rpx 32rpx;
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
.font-headline {
font-family: 'Noto Serif', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', serif;
}
// 搜索栏 - surface-container-low
.search-section {
margin-bottom: 32rpx;
}
.search-bar {
display: flex;
align-items: center;
background: $surface-container-low;
border-radius: 24rpx;
padding: 24rpx 32rpx;
}
.search-icon {
font-size: 36rpx;
color: $outline;
margin-right: 24rpx;
}
.search-input {
flex: 1;
background: transparent;
font-size: 28rpx;
color: $on-surface;
}
.search-input::placeholder {
color: #414941;
opacity: 0.5;
}
.search-filter {
font-size: 36rpx;
color: $outline;
}
// 通用区块
.section-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 32rpx;
}
.section-title {
font-size: 48rpx;
font-weight: bold;
color: $on-surface;
font-family: 'Noto Serif', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', serif;
}
.more-link {
font-size: 28rpx;
color: $primary;
font-weight: 500;
}
// 平台资质 - grid 布局
.qualification-section {
margin-bottom: 64rpx;
}
.qualification-grid {
display: flex;
gap: 32rpx;
}
.qual-col-left {
flex: 1;
}
.qual-col-right {
flex: 1;
display: flex;
flex-direction: column;
gap: 32rpx;
}
.qualification-card {
&.large {
height: 100%;
min-height: 320rpx;
}
&.small {
flex: 1;
min-height: 160rpx;
}
}
.qual-card-inner {
border-radius: 24rpx;
padding: 40rpx;
height: 100%;
min-height: 160rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
box-sizing: border-box;
position: relative;
overflow: hidden;
}
/* 卫健委认证 - primary-container */
.qual-primary {
background: $primary-container;
color: $on-primary;
}
.qual-primary .qual-icon .icon-text,
.qual-primary .qual-name {
color: $on-primary;
}
.qual-primary .qual-desc {
color: $on-primary-container;
font-size: 24rpx;
}
/* 互联网医院 - secondary-container */
.qual-beige {
background: $secondary-container;
color: $on-secondary-container;
flex-direction: row;
align-items: center;
gap: 24rpx;
padding: 32rpx;
}
.qual-beige .qual-icon .icon-text {
font-size: 40rpx;
color: $on-secondary-container;
}
.qual-beige .qual-name {
color: $on-secondary-container;
margin-bottom: 4rpx;
display: block;
}
.qual-beige .qual-desc {
color: $on-secondary-container;
opacity: 0.8;
font-size: 20rpx;
display: block;
}
/* 执业医师认证 - surface-container-high */
.qual-grey {
background: $surface-container-high;
color: $on-surface;
flex-direction: row;
align-items: center;
gap: 24rpx;
padding: 32rpx;
}
.qual-grey .qual-icon .icon-text {
font-size: 40rpx;
color: $primary;
}
.qual-grey .qual-name {
color: $on-surface;
display: block;
}
.qual-grey .qual-desc {
color: $on-surface-variant;
font-size: 20rpx;
display: block;
}
.qual-icon {
margin-bottom: 16rpx;
}
.qual-beige .qual-icon,
.qual-grey .qual-icon {
margin-bottom: 0;
}
.qual-icon .icon-text {
font-size: 56rpx;
color: $on-primary;
}
.qual-name {
font-size: 36rpx;
font-weight: 600;
color: $on-surface;
margin-bottom: 8rpx;
font-family: 'Noto Serif', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', serif;
&.text-white {
color: $on-primary;
}
}
.qual-desc {
font-size: 26rpx;
color: $on-surface-variant;
&.text-white-sub {
color: $on-primary-container;
}
}
// 名医推荐
.doctors-section {
margin-bottom: 64rpx;
}
.doctors-list {
display: flex;
flex-direction: column;
gap: 32rpx;
}
.doctor-card {
background: $surface-container-lowest;
border-radius: 24rpx;
padding: 40rpx;
display: flex;
align-items: center;
gap: 32rpx;
}
.doctor-photo {
width: 160rpx;
height: 230rpx;
border-radius: 16rpx;
flex-shrink: 0;
background: $surface-container-highest;
}
.doctor-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 192rpx;
}
.doctor-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8rpx;
}
.doctor-name {
font-size: 36rpx;
font-weight: bold;
color: $on-surface;
font-family: 'Noto Serif', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', serif;
}
.doctor-rating {
font-size: 24rpx;
color: #805533;
font-weight: bold;
}
.doctor-specialty {
font-size: 28rpx;
color: $on-surface-variant;
margin-bottom: 16rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.doctor-price {
font-size: 28rpx;
color: $primary;
font-weight: bold;
}
.doctor-unit {
font-size: 24rpx;
font-weight: normal;
color: $on-surface-variant;
}
.doctor-actions {
display: flex;
justify-content: flex-end;
position: relative;
right: 0;
bottom: 0;
}
.appointment-btn {
background: $primary;
color: $on-primary;
font-size: 24rpx;
font-weight: bold;
padding: 16rpx 32rpx;
border-radius: 9999rpx;
}
// 健康百科 - 横向滚动
.encyclopedia-section {
margin-bottom: 64rpx;
}
.encyclopedia-scroll {
white-space: nowrap;
margin: 0 -32rpx;
padding: 0 32rpx 32rpx;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.article-card {
display: inline-block;
width: 560rpx;
margin-right: 40rpx;
background: rgba(75, 101, 0, 0.06);
border-radius: 24rpx;
overflow: hidden;
vertical-align: top;
}
.article-card-secondary {
background: rgba(253, 195, 154, 0.1);
}
.article-card:last-child {
margin-right: 0;
}
.article-image-wrap {
position: relative;
width: 100%;
height: 256rpx;
overflow: hidden;
}
.article-image {
width: 100%;
height: 100%;
background: $tertiary-container;
opacity: 0.8;
}
.article-gradient {
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
background: linear-gradient(to top, $tertiary-container, transparent);
}
.article-gradient.gradient-secondary {
background: linear-gradient(to top, $secondary-container, transparent);
}
.article-tag {
position: absolute;
bottom: 16rpx;
left: 24rpx;
background: $tertiary;
color: $on-primary;
font-size: 20rpx;
padding: 8rpx 16rpx;
border-radius: 9999rpx;
text-transform: uppercase;
font-weight: bold;
letter-spacing: 0.05em;
z-index: 1;
}
.article-tag.tag-secondary {
background: #805533;
color: #ffffff;
}
.article-title {
font-size: 28rpx;
font-weight: bold;
color: #394d00;
padding: 32rpx 32rpx 8rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
font-family: 'Noto Serif', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', serif;
}
.article-title.title-secondary {
color: $on-secondary-container;
}
.article-desc {
font-size: 24rpx;
color: $on-surface-variant;
padding: 0 32rpx 32rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
</style>
+576
View File
@@ -0,0 +1,576 @@
<template>
<view class="container">
<!-- 等待视频通话页面 - 老年人友好版 -->
<view class="waiting-container">
<view class="waiting-content">
<!-- 大号医生图标动画 -->
<view class="waiting-animation">
<view class="pulse-ring"></view>
<view class="pulse-ring delay-1"></view>
<view class="pulse-ring delay-2"></view>
<view class="doctor-icon">
<text class="icon-text">👨</text>
</view>
</view>
<!-- 超大标题 -->
<view class="waiting-title">正在等待医生</view>
<view class="waiting-subtitle">视频通话即将开始</view>
<!-- 简化的状态显示 -->
<!-- <view class="status-card">
<view class="status-icon"></view>
<view class="status-main-text">系统已准备好</view>
<view class="status-sub-text">请耐心等待医生接入</view>
</view> -->
<!-- 大字号提示卡片 -->
<view class="tips-card">
<view class="tips-header">
<text class="tips-icon">💡</text>
<text class="tips-header-text">温馨提示</text>
</view>
<view class="tips-list">
<view class="tips-item">
<text class="tips-number">1</text>
<text class="tips-text">请保持手机摄像头清洁</text>
</view>
<view class="tips-item">
<text class="tips-number">2</text>
<text class="tips-text">选择光线明亮的位置</text>
</view>
<view class="tips-item">
<text class="tips-number">3</text>
<text class="tips-text">保持环境安静</text>
</view>
</view>
</view>
<!-- 大号帮助按钮 -->
<view class="help-button">
<text class="help-text">需要帮助点击这里</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref,onMounted,getCurrentInstance} from "vue";
// 2. 获取组件实例,通过 proxy 访问全局属性
const { proxy } = getCurrentInstance()
import { CallManager } from "@/TUICallKit/src/TUICallService/serve/callManager";
import { onLaunch, onShow, onHide, onError ,onShareAppMessage} from '@dcloudio/uni-app'
let userID = ref("4");
let avatarUrl = ref(""); // 声明为响应式变量
uni.CallManager = new CallManager();
onLaunch(() => {
uni.login({
provider: 'weixin',
success: function (loginRes) {
wxcode(loginRes.code)
// 获取用户信息
}
});
console.log('---------进入onMounted')
})
onShareAppMessage((res) =>{
// res.from 可判断分享触发方式:button(按钮触发)、menu(右上角菜单触发)
const shareSource = res.from;
// 自定义分享内容
return {
title: '视频问诊通话邀请', // 分享标题(必填)
path: '/pages/index/video', // 分享路径(必填,以 / 开头,可带参数)
imageUrl: '/static/share-img.png', // 分享图片(可选,建议尺寸 5:4,支持本地/网络图片)
desc: '自定义分享描述', // 小程序分享描述(仅在某些场景显示)
success() {
// 分享成功回调
uni.showToast({ title: '分享成功', icon: 'success' });
},
fail(err) {
// 分享失败回调
console.log('分享失败:', err);
uni.showToast({ title: '分享失败', icon: 'none' });
}
}
});
const wxcode = async (code) => {
return
try {
// 通过 proxy 调用全局的 apiUrl
const res = await proxy.apiUrl({
url: '/api/login/mnpLogin',
method: 'POST',
data: {
code: code
},
}, false) // 不显示加载中
loginHandler(res.data.diagnosis.patient_id)
} catch (err) {
uni.showToast({ title: '请求失败', icon: 'none' })
console.error(err)
}
}
const loginHandler = async (patient_id) => {
// 从后端获取签名
//const signatureData = await getSignatureFromServer(patient_id);
const res = await proxy.apiUrl({
url: '/api/tcm/getPatientSignature',
method: 'GET',
data: {
patient_id: patient_id ||patient
},
}, false)
const { userId, userSig, sdkAppId: SDKAppID } = res.data;
console.log('获取签名成功:',userSig);
getApp().globalData.userID = userId;
getApp().globalData.userSig = userSig;
getApp().globalData.SDKAppID = SDKAppID;
await uni.CallManager.init({
sdkAppID: SDKAppID, // 替换为用户自己的 sdkAppID
userID: userId, // 替换为用户自己的 userID
userSig: userSig, // 替换为用户自己的 userSig
globalCallPagePath: "TUICallKit/src/Components/TUICallKit", // 替换为步骤一里注册的全局监听页面
});
// uni.navigateTo({
// url: "./index",
// });
};
</script>
<style scoped>
.container {
width: 100vw;
height: 100vh;
background-color: #f4f5f9;
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
}
.counter-warp {
position: absolute;
top: 0;
right: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.background-image {
width: 100%;
}
.header-content {
display: flex;
width: 100vw;
padding: 50px 20px 10px;
box-sizing: border-box;
top: 100rpx;
background-color: #000;
align-items: center;
}
.icon-box {
width: 56px;
height: 56px;
}
.text-header {
height: 72rpx;
font-size: 48rpx;
line-height: 72rpx;
color: #ffffff;
margin: 40px auto;
}
.text-content {
height: 36rpx;
font-size: 24rpx;
line-height: 36rpx;
color: #ffffff;
}
.box {
width: 80%;
height: 50vh;
position: relative;
background: #ffffff;
border-radius: 4px;
border-radius: 4px;
display: flex;
flex-direction: column;
justify-content: left;
padding: 30px 20px;
}
.input-box {
flex: 1;
display: flex;
font-family: PingFangSC-Regular;
font-size: 14px;
color: rgba(0, 0, 0, 0.8);
letter-spacing: 0;
}
.login {
display: flex;
box-sizing: border-box;
margin-top: 15px;
width: 100%;
}
.login button {
background: rgba(0, 110, 255, 1);
border-radius: 30px;
font-size: 16px;
color: #ffffff;
letter-spacing: 0;
/* text-align: center; */
font-weight: 500;
}
.loginBtn {
margin-top: 64px;
background-color: white;
border-radius: 24px;
border-radius: 24px;
/* display: flex;
justify-content: center; */
width: 100% !important;
font-family: PingFangSC-Regular;
font-size: 16px;
color: #ffffff;
letter-spacing: 0;
}
.list-item {
display: flex;
flex-direction: column;
font-family: PingFangSC-Medium;
font-size: 14px;
color: #333333;
border-bottom: 1px solid #eef0f3;
}
.input-container {
width: 90%;
margin: 50px auto 0;
display: flex;
flex-direction: column;
font-family: PingFangSC-Medium;
font-size: 14px;
color: #333333;
border-bottom: 1px solid #eef0f3;
}
/* .input-box {
height: 20px;
padding: 5px;
width: 100%;
border: 1px solid #999999;;
} */
.list-item .list-item-label {
font-weight: 500;
padding: 10px 0;
}
.guide-box {
width: 100vw;
box-sizing: border-box;
padding: 16px;
display: flex;
flex-direction: column;
}
.single-box {
flex: 1;
border-radius: 10px;
background-color: #ffffff;
margin-bottom: 16px;
display: flex;
align-items: center;
}
.icon {
display: block;
width: 180px;
height: 144px;
}
.single-content {
padding: 36px 30px 36px 20px;
color: #333333;
}
.label {
display: block;
font-size: 18px;
color: #333333;
letter-spacing: 0;
font-weight: 500;
}
.desc {
display: block;
font-size: 14px;
color: #333333;
letter-spacing: 0;
font-weight: 500;
}
.logo-box {
position: absolute;
width: 100vw;
bottom: 36rpx;
text-align: center;
}
/* ========== 老年人友好等待界面样式 ========== */
.waiting-container {
width: 100vw;
min-height: 100vh;
background: linear-gradient(180deg, #f0f7ff 0%, #ffffff 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
}
.waiting-content {
width: 100%;
max-width: 700rpx;
display: flex;
flex-direction: column;
align-items: center;
}
/* 动画区域 - 更大更明显 */
.waiting-animation {
position: relative;
width: 280rpx;
height: 280rpx;
margin-bottom: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.pulse-ring {
position: absolute;
width: 100%;
height: 100%;
border: 6rpx solid #1890ff;
border-radius: 50%;
animation: pulse 2s ease-out infinite;
opacity: 0;
}
.pulse-ring.delay-1 {
animation-delay: 0.6s;
}
.pulse-ring.delay-2 {
animation-delay: 1.2s;
}
@keyframes pulse {
0% {
transform: scale(0.5);
opacity: 1;
}
100% {
transform: scale(1.3);
opacity: 0;
}
}
.doctor-icon {
width: 180rpx;
height: 180rpx;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(24, 144, 255, 0.3);
z-index: 1;
}
.icon-text {
font-size: 100rpx;
line-height: 1;
}
/* 超大标题 - 老年人易读 */
.waiting-title {
font-size: 56rpx;
font-weight: bold;
color: #1890ff;
margin-bottom: 20rpx;
text-align: center;
line-height: 1.4;
}
.waiting-subtitle {
font-size: 40rpx;
color: #333333;
margin-bottom: 60rpx;
text-align: center;
line-height: 1.5;
}
/* 状态卡片 - 简洁明了 */
.status-card {
width: 100%;
background: #ffffff;
border-radius: 24rpx;
padding: 50rpx 40rpx;
margin-bottom: 40rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
align-items: center;
border: 3rpx solid #52c41a;
}
.status-icon {
width: 80rpx;
height: 80rpx;
background: #52c41a;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 50rpx;
color: #ffffff;
margin-bottom: 30rpx;
font-weight: bold;
}
.status-main-text {
font-size: 44rpx;
font-weight: bold;
color: #52c41a;
margin-bottom: 20rpx;
text-align: center;
}
.status-sub-text {
font-size: 36rpx;
color: #666666;
text-align: center;
line-height: 1.6;
}
/* 提示卡片 - 大字号清晰 */
.tips-card {
width: 100%;
background: #fffbe6;
border-radius: 24rpx;
padding: 40rpx;
margin-bottom: 40rpx;
border: 3rpx solid #fadb14;
}
.tips-header {
display: flex;
align-items: center;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #ffd666;
}
.tips-icon {
font-size: 48rpx;
margin-right: 16rpx;
}
.tips-header-text {
font-size: 40rpx;
font-weight: bold;
color: #d48806;
}
.tips-list {
display: flex;
flex-direction: column;
gap: 30rpx;
}
.tips-item {
display: flex;
align-items: center;
padding: 24rpx;
background: #ffffff;
border-radius: 16rpx;
}
.tips-number {
width: 60rpx;
height: 60rpx;
background: #faad14;
color: #ffffff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
font-weight: bold;
margin-right: 24rpx;
flex-shrink: 0;
}
.tips-text {
font-size: 36rpx;
color: #333333;
line-height: 1.6;
font-weight: 500;
}
/* 帮助按钮 - 大号易点击 */
.help-button {
width: 100%;
height: 100rpx;
background: #ffffff;
border: 3rpx solid #1890ff;
border-radius: 50rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.15);
margin-top: 20rpx;
}
.help-text {
font-size: 38rpx;
color: #1890ff;
font-weight: bold;
}
</style>
+342
View File
@@ -0,0 +1,342 @@
<template>
<view class="container">
<!-- 等待视频通话页面 - 老年人友好版 -->
<view class="waiting-container">
<view class="waiting-content">
<!-- 大号医生图标动画 -->
<view class="waiting-animation">
<view class="pulse-ring"></view>
<view class="pulse-ring delay-1"></view>
<view class="pulse-ring delay-2"></view>
<view class="doctor-icon">
<text class="icon-text">👨</text>
</view>
</view>
<!-- 超大标题 -->
<view class="waiting-title">正在等待医生</view>
<view class="waiting-subtitle">视频通话即将开始</view>
<!-- 大字号提示卡片 -->
<view class="tips-card">
<view class="tips-header">
<text class="tips-icon">💡</text>
<text class="tips-header-text">温馨提示</text>
</view>
<view class="tips-list">
<view class="tips-item">
<text class="tips-number">1</text>
<text class="tips-text">请保持手机摄像头清洁</text>
</view>
<view class="tips-item">
<text class="tips-number">2</text>
<text class="tips-text">选择光线明亮的位置</text>
</view>
<view class="tips-item">
<text class="tips-number">3</text>
<text class="tips-text">保持环境安静</text>
</view>
</view>
</view>
<!-- 大号帮助按钮 -->
<view class="help-button" @click="url">
<text class="help-text">需要帮助点击这里</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, getCurrentInstance,onMounted } from "vue";
import { onShow, onShareAppMessage } from '@dcloudio/uni-app'
import { CallManager } from "@/TUICallKit/src/TUICallService/serve/callManager";
const { proxy } = getCurrentInstance()
let userId = ref("");
let userSig= ref("");
let SDKAppID=ref("");
uni.CallManager = new CallManager();
const doctorId = ref('')
const doctorName = ref('')
onShow(() => {
uni.login({
provider: 'weixin',
success: function (loginRes) {
console.log('---------进入onShow',loginRes.code)
wxcode(loginRes.code)
}
});
})
onMounted(() => {
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
// const options = current.options || current.$page?.options || {}
const options = proxy.$parsePageParams(currentPage.options|| currentPage.$page?.options || {});
doctorId.value = options.id || options.doctorId || ''
console.log('---------进入onMounted',options,doctorId.value)
})
onShareAppMessage((res) =>{
return {
title: '视频问诊通话邀请',
path: '/pages/index/video',
imageUrl: '',
desc: '视频问诊通话邀请',
success() {
uni.showToast({ title: '分享成功', icon: 'success' });
},
fail(err) {
console.log('分享失败:', err);
uni.showToast({ title: '分享失败', icon: 'none' });
}
}
});
const wxcode = async (code) => {
try {
const res = await proxy.apiUrl({
url: '/api/login/mnpLogin',
method: 'POST',
data: { code: code },
}, false)
console.log('---------------',res.data)
uni.setStorageSync('token', res.data.token)
uni.setStorageSync('userData', res.data)
loginHandler(res.data.diagnosis.patient_id)
} catch (err) {
uni.showToast({ title: '请求失败', icon: 'none' })
console.error(err)
}
}
const url=()=>{
const doctor=doctorId.value?'doctor_'+doctorId.value:'doctor_'+uni.getStorageSync('userData').diagnosis.diagnosis_id
// 跳转到聊天页面
uni.navigateTo({
url: `/TUIKit/pages/chat/chat?userID=${encodeURIComponent(userId)}&userSig=${encodeURIComponent(userSig)}&SDKAppID=${SDKAppID}&targetUserID=${doctor}`
});
}
const loginHandler = async (patient_id) => {
console.log('获取签名开始:',patient_id);
const res = await proxy.apiUrl({
url: '/api/tcm/getPatientSignature',
method: 'GET',
data: { patient_id: patient_id },
}, false)
const { userId, userSig, sdkAppId: SDKAppID } = res.data;
console.log('获取签名成功:',userSig);
getApp().globalData.userID = userId;
getApp().globalData.userSig = userSig;
getApp().globalData.SDKAppID = SDKAppID;
getApp().globalData.isIMInitialized = false; // 标记IM未初始化
// 初始化视频通话
await uni.CallManager.init({
sdkAppID: SDKAppID,
userID: userId,
userSig: userSig,
globalCallPagePath: "TUICallKit/src/Components/TUICallKit",
});
console.log('初始化完成,跳转到聊天页面');
};
</script>
<style scoped>
.container {
width: 100vw;
height: 100vh;
background-color: #f4f5f9;
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
}
/* ========== 老年人友好等待界面样式 ========== */
.waiting-container {
width: 100vw;
min-height: 100vh;
background: linear-gradient(180deg, #f0f7ff 0%, #ffffff 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
}
.waiting-content {
width: 100%;
max-width: 700rpx;
display: flex;
flex-direction: column;
align-items: center;
}
/* 动画区域 - 更大更明显 */
.waiting-animation {
position: relative;
width: 280rpx;
height: 280rpx;
margin-bottom: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.pulse-ring {
position: absolute;
width: 100%;
height: 100%;
border: 6rpx solid #1890ff;
border-radius: 50%;
animation: pulse 2s ease-out infinite;
opacity: 0;
}
.pulse-ring.delay-1 {
animation-delay: 0.6s;
}
.pulse-ring.delay-2 {
animation-delay: 1.2s;
}
@keyframes pulse {
0% {
transform: scale(0.5);
opacity: 1;
}
100% {
transform: scale(1.3);
opacity: 0;
}
}
.doctor-icon {
width: 180rpx;
height: 180rpx;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(24, 144, 255, 0.3);
z-index: 1;
}
.icon-text {
font-size: 100rpx;
line-height: 1;
}
/* 超大标题 - 老年人易读 */
.waiting-title {
font-size: 56rpx;
font-weight: bold;
color: #1890ff;
margin-bottom: 20rpx;
text-align: center;
line-height: 1.4;
}
.waiting-subtitle {
font-size: 40rpx;
color: #333333;
margin-bottom: 60rpx;
text-align: center;
line-height: 1.5;
}
/* 提示卡片 - 大字号清晰 */
.tips-card {
width: 100%;
background: #fffbe6;
border-radius: 24rpx;
padding: 40rpx;
margin-bottom: 40rpx;
border: 3rpx solid #fadb14;
}
.tips-header {
display: flex;
align-items: center;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #ffd666;
}
.tips-icon {
font-size: 48rpx;
margin-right: 16rpx;
}
.tips-header-text {
font-size: 40rpx;
font-weight: bold;
color: #d48806;
}
.tips-list {
display: flex;
flex-direction: column;
gap: 30rpx;
}
.tips-item {
display: flex;
align-items: center;
padding: 24rpx;
background: #ffffff;
border-radius: 16rpx;
}
.tips-number {
width: 60rpx;
height: 60rpx;
background: #faad14;
color: #ffffff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
font-weight: bold;
margin-right: 24rpx;
flex-shrink: 0;
}
.tips-text {
font-size: 36rpx;
color: #333333;
line-height: 1.6;
font-weight: 500;
}
/* 帮助按钮 - 大号易点击 */
.help-button {
width: 100%;
height: 100rpx;
background: #ffffff;
border: 3rpx solid #1890ff;
border-radius: 50rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.15);
margin-top: 20rpx;
}
.help-text {
font-size: 38rpx;
color: #1890ff;
font-weight: bold;
}
</style>
File diff suppressed because it is too large Load Diff
+512
View File
@@ -0,0 +1,512 @@
<template>
<view class="container">
<!-- 加载中 -->
<view v-if="loading" class="loading-wrapper">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 支付成功 -->
<view v-else-if="paySuccess" class="success-container">
<view class="success-content">
<view class="success-icon-wrapper">
<view class="success-icon-circle">
<text class="success-icon-check"></text>
</view>
</view>
<view class="success-title">支付成功</view>
<view class="success-amount">¥ {{ orderInfo.amount }}</view>
<view class="success-message">订单号{{ orderInfo.order_no }}</view>
</view>
</view>
<!-- 订单不存在 / 错误 -->
<view v-else-if="errorMsg" class="error-wrapper">
<view class="error-icon">!</view>
<text class="error-text">{{ errorMsg }}</text>
<button class="retry-btn" @click="loadOrderDetail">重试</button>
</view>
<!-- 订单详情 & 付款 -->
<view v-else-if="orderInfo" class="pay-wrapper">
<!-- 商户信息 -->
<view class="merchant-info">
<text class="merchant-title">付款给甄养堂互联网医院</text>
</view>
<!-- 金额区域 -->
<view class="amount-section">
<text class="amount-label">付款金额</text>
<view class="amount-row">
<text class="amount-symbol">¥</text>
<text class="amount-value">{{ formatAmount(orderInfo.amount) }}</text>
</view>
</view>
<view class="divider"></view>
<!-- 订单信息 -->
<view class="order-info-section">
<view class="info-row">
<text class="info-label">订单号</text>
<text class="info-value">{{ orderInfo.order_no }}</text>
</view>
<view class="info-row">
<text class="info-label">订单类型</text>
<text class="info-value">{{ orderInfo.order_type_desc }}</text>
</view>
<view class="info-row" v-if="orderInfo.remark">
<text class="info-label">备注</text>
<text class="info-value">{{ orderInfo.remark }}</text>
</view>
</view>
<view class="divider"></view>
<!-- 订单状态 -->
<view v-if="orderInfo.status == 2" class="paid-notice">
<view class="paid-icon"></view>
<text class="paid-text">该订单已支付</text>
</view>
<!-- 付款按钮 -->
<view v-else class="pay-btn-wrapper">
<button
class="pay-btn"
:disabled="paying"
:loading="paying"
@click="handlePay"
>
{{ paying ? '支付中...' : '付款' }}
</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
const loading = ref(true)
const paying = ref(false)
const paySuccess = ref(false)
const errorMsg = ref('')
const orderInfo = ref(null)
const orderNo = ref('')
onMounted(() => {
loadOrderDetail()
})
const loadOrderDetail = async () => {
loading.value = true
errorMsg.value = ''
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const params = proxy.$parsePageParams(currentPage.options || {})
orderNo.value = params.order_no || ''
if (!orderNo.value) {
loading.value = false
errorMsg.value = '缺少订单号'
return
}
try {
const res = await proxy.apiUrl({
url: '/api/tcm/getOrderByNo',
method: 'GET',
data: { order_no: orderNo.value }
}, false)
if (res.code === 1) {
orderInfo.value = res.data
} else {
errorMsg.value = res.msg || '订单加载失败'
}
} catch (err) {
errorMsg.value = '网络请求失败'
console.error('加载订单失败:', err)
} finally {
loading.value = false
}
}
const formatAmount = (amount) => {
const num = parseFloat(amount)
if (isNaN(num)) return '0.00'
return num.toFixed(2)
}
const ensureLogin = () => {
return new Promise((resolve, reject) => {
const token = uni.getStorageSync('token')
if (token) {
resolve(token)
return
}
uni.login({
provider: 'weixin',
success: async (loginRes) => {
try {
const res = await proxy.apiUrl({
url: '/api/login/mnpLogin',
method: 'POST',
data: { code: loginRes.code }
}, false)
if (res.code === 1 && res.data && res.data.token) {
uni.setStorageSync('token', res.data.token)
uni.setStorageSync('userData', res.data)
resolve(res.data.token)
} else {
reject(new Error(res.msg || '登录失败'))
}
} catch (e) {
reject(e)
}
},
fail: (err) => {
reject(err)
}
})
})
}
const handlePay = async () => {
if (paying.value || !orderInfo.value) return
if (orderInfo.value.status == 2) {
uni.showToast({ title: '订单已支付', icon: 'none' })
return
}
paying.value = true
try {
await ensureLogin()
const res = await proxy.apiUrl({
url: '/api/pay/prepay',
method: 'POST',
data: {
from: 'order',
order_id: orderInfo.value.id,
pay_way: 2,
redirect: '/pages/order/order'
}
}, false)
if (res.code !== 1 || !res.data) {
uni.showToast({ title: res.msg || '发起支付失败', icon: 'none' })
paying.value = false
return
}
const payParams = res.data
wx.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType || 'RSA',
paySign: payParams.paySign,
success: () => {
paySuccess.value = true
paying.value = false
},
fail: (err) => {
paying.value = false
if (err.errMsg && err.errMsg.includes('cancel')) {
uni.showToast({ title: '已取消支付', icon: 'none' })
} else {
uni.showToast({ title: '支付失败,请重试', icon: 'none' })
}
console.error('支付失败:', err)
}
})
} catch (err) {
paying.value = false
uni.showToast({ title: '支付异常,请重试', icon: 'none' })
console.error('支付异常:', err)
}
}
</script>
<style lang="less">
.container {
min-height: 100vh;
background-color: #f5f5f5;
}
.loading-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 6rpx solid #e0e0e0;
border-top-color: #1989fa;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
}
/* 错误页 */
.error-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
padding: 40rpx;
}
.error-icon {
width: 120rpx;
height: 120rpx;
line-height: 120rpx;
text-align: center;
font-size: 70rpx;
font-weight: bold;
color: #fff;
background-color: #ff4d4f;
border-radius: 50%;
margin-bottom: 30rpx;
}
.error-text {
font-size: 32rpx;
color: #666;
margin-bottom: 40rpx;
}
.retry-btn {
width: 320rpx;
height: 88rpx;
line-height: 88rpx;
text-align: center;
background-color: #1989fa;
color: #fff;
font-size: 32rpx;
border-radius: 44rpx;
border: none;
}
/* 付款主体 */
.pay-wrapper {
padding: 40rpx;
}
.merchant-info {
padding: 30rpx 0;
}
.merchant-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
/* 金额区域 */
.amount-section {
padding: 40rpx 0 50rpx;
}
.amount-label {
font-size: 28rpx;
color: #888;
margin-bottom: 16rpx;
display: block;
}
.amount-row {
display: flex;
align-items: baseline;
margin-top: 16rpx;
}
.amount-symbol {
font-size: 44rpx;
font-weight: bold;
color: #333;
margin-right: 8rpx;
}
.amount-value {
font-size: 80rpx;
font-weight: bold;
color: #333;
line-height: 1;
}
.divider {
height: 1rpx;
background-color: #e8e8e8;
margin: 10rpx 0 30rpx;
}
/* 订单信息 */
.order-info-section {
padding: 10rpx 0 20rpx;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24rpx;
}
.info-label {
font-size: 28rpx;
color: #888;
flex-shrink: 0;
margin-right: 20rpx;
}
.info-value {
font-size: 28rpx;
color: #333;
text-align: right;
word-break: break-all;
}
/* 已支付提示 */
.paid-notice {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 0;
}
.paid-icon {
width: 48rpx;
height: 48rpx;
line-height: 48rpx;
text-align: center;
font-size: 28rpx;
color: #fff;
background-color: #52c41a;
border-radius: 50%;
margin-right: 16rpx;
}
.paid-text {
font-size: 32rpx;
color: #52c41a;
font-weight: 600;
}
/* 付款按钮 */
.pay-btn-wrapper {
padding: 60rpx 40rpx 0;
}
.pay-btn {
width: 100%;
height: 100rpx;
line-height: 100rpx;
text-align: center;
background-color: #1989fa;
color: #fff;
font-size: 36rpx;
font-weight: bold;
border-radius: 16rpx;
border: none;
letter-spacing: 4rpx;
&[disabled] {
background-color: #a0cfff;
}
}
/* 支付成功页 */
.success-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1989fa 0%, #4da6ff 100%);
padding: 40rpx;
}
.success-content {
width: 100%;
max-width: 620rpx;
background: #fff;
border-radius: 32rpx;
padding: 90rpx 50rpx 70rpx;
text-align: center;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.15);
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-40rpx); }
to { opacity: 1; transform: translateY(0); }
}
.success-icon-wrapper {
margin-bottom: 48rpx;
}
.success-icon-circle {
width: 180rpx;
height: 180rpx;
margin: 0 auto;
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 12rpx 40rpx rgba(82, 196, 26, 0.35);
animation: scaleIn 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes scaleIn {
0% { transform: scale(0); opacity: 0; }
50% { transform: scale(1.1); }
100% { transform: scale(1); opacity: 1; }
}
.success-icon-check {
font-size: 110rpx;
color: #fff;
font-weight: bold;
line-height: 1;
}
.success-title {
font-size: 52rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.success-amount {
font-size: 56rpx;
font-weight: bold;
color: #1989fa;
margin-bottom: 30rpx;
}
.success-message {
font-size: 28rpx;
color: #999;
}
</style>
+426
View File
@@ -0,0 +1,426 @@
<template>
<view class="mine-page">
<!-- 顶部用户信息卡片 -->
<div class="user-card" @click="navigateTo('/pages/user/userinfo')">
<div class="user-header">
<image class="avatar" :src="userInfo.avatar" mode="aspectFill" />
<div class="user-info">
<text class="nickname">{{ userInfo.nickname || '未登录' }}</text>
<!-- <text class="vip-tag">VIP会员</text> -->
</div>
<!-- <div class="qr-code" @click.stop="showQRCode">
<uni-icons type="scan" size="20" color="#333"></uni-icons>
</div> -->
</div>
<div class="stat-row">
<div class="stat-item">
<text class="value">{{ userInfo.balance || 0 }}</text>
<text class="label">钱包()</text>
</div>
<div class="stat-item">
<text class="value">{{ userInfo.points || 0 }}</text>
<text class="label">积分</text>
</div>
<div class="stat-item">
<text class="value">{{ userInfo.coupons || 0 }}</text>
<text class="label">优惠券</text>
</div>
</div>
</div>
<!-- 订单卡片 -->
<!-- <div class="order-card">
<div class="card-header">
<text class="title">我的订单</text>
<div class="more" @click="navigateTo('/pages/order/list')">
<text>查看全部</text>
<uni-icons type="right" size="14" color="#999"></uni-icons>
</div>
</div>
<div class="order-list">
<div
class="order-item"
v-for="(item, index) in orderTypes"
:key="index"
@click="navigateTo(item.path)"
>
<div class="icon-wrapper">
<image :src="item.image" mode="aspectFit" class="order-icon" />
<text v-if="item.count" class="count-badge">{{ item.count }}</text>
</div>
<text class="order-name">{{ item.name }}</text>
</div>
</div>
</div> -->
<!-- 快捷功能区 -->
<!-- <div class="quick-actions">
<div
class="action-item"
v-for="(item, index) in allServices.slice(0, 4)"
:key="index"
@click="navigateTo(item.path)"
>
<div class="action-icon" :class="item.class">
<uni-icons :type="item.icon" size="24" color="#fff"></uni-icons>
</div>
<text class="action-name">{{ item.name }}</text>
</div>
</div> -->
<!-- 服务菜单列表适老化显示全部大触控区 -->
<div class="service-list">
<div
class="service-item"
v-for="(item, index) in allServices"
:key="index"
@click="navigateTo(item.path)"
>
<div class="left">
<div class="service-icon" :class="item.class">
<uni-icons :type="item.icon" size="28" color="#fff"></uni-icons>
</div>
<text class="service-name">{{ item.name }}</text>
</div>
<div class="right">
<text v-if="item.desc" class="desc">{{ item.desc }}</text>
<uni-icons type="right" size="20" color="#666"></uni-icons>
</div>
</div>
</div>
<!-- DEV 开发悬浮入口生产环境自动隐藏 -->
<dev-training-entry :bottom="360" />
<TabBarDock :active="2" />
</view>
</template>
<script>
import { onShow, onShareAppMessage } from '@dcloudio/uni-app'
import DevTrainingEntry from '@/components/dev-training-entry/index.vue'
import TabBarDock from '@/components/app-tab-bar/tab-bar-dock.vue'
export default {
name: 'profileB',
components: { DevTrainingEntry, TabBarDock },
data() {
return {
userInfo: {
avatar: '',
nickname: '',
userId: '',
balance: '0',
points: '0',
coupons: '5'
},
orderTypes: [
{
name: '待付款',
image: '/static/images/my/payment.png',
path: '/pages/order/list?type=1',
count: 2
},
{
name: '待发货',
image: '/static/images/my/delivery.png',
path: '/pages/order/list?type=2',
count: 1
},
{
name: '待收货',
image: '/static/images/my/shipping.png',
path: '/pages/order/list?type=3',
count: 0
},
{
name: '待评价',
image: '/static/images/my/review.png',
path: '/pages/order/list?type=4',
count: 3
},
{
name: '退换/售后',
image: '/static/images/my/after-sale.png',
path: '/pages/order/list?type=5',
count: 0
}
],
allServices: [
{ name: '就诊卡', icon: 'folder-add', path: '/pages/Card/Card', class: 'bg-cyan' },
{ name: '设置', icon: 'gear', path: '/pages/user/userinfo', class: 'bg-gray' }// ,
// { name: '客服中心', icon: 'headphones', path: '/pages/service/index', class: 'bg-orange', desc: '在线客服' }
]
}
},
onShow() {
this.userInfo=uni.getStorageSync('userData')
},
methods: {
navigateTo(path) {
uni.navigateTo({ url: path })
},
showQRCode() {
// 显示二维码
}
}
}
</script>
<style lang="scss" scoped>
/* 适老化设计:50-80岁老人 - 大字号、高对比、大触控区 */
.mine-page {
min-height: 100vh;
background: #e8eaed;
padding: 40rpx;
padding-bottom: calc(40rpx + 200rpx + env(safe-area-inset-bottom));
}
.user-card {
background: #fff;
border-radius: 28rpx;
padding: 48rpx;
margin-bottom: 40rpx;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
.user-header {
display: flex;
align-items: center;
margin-bottom: 48rpx;
.avatar {
width: 160rpx;
height: 160rpx;
border-radius: 80rpx;
margin-right: 36rpx;
border: 4rpx solid #e0e0e0;
}
.user-info {
flex: 1;
.nickname {
font-size: 48rpx;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 16rpx;
display: block;
letter-spacing: 1rpx;
}
.vip-tag {
background: linear-gradient(90deg, #FFD700 0%, #FFA500 100%);
color: #fff;
font-size: 28rpx;
padding: 6rpx 20rpx;
border-radius: 24rpx;
display: inline-block;
}
}
.qr-code {
width: 88rpx;
height: 88rpx;
border-radius: 44rpx;
background: #f5f6fa;
display: flex;
align-items: center;
justify-content: center;
}
}
.stat-row {
display: flex;
padding-top: 40rpx;
border-top: 4rpx solid #e8eaed;
.stat-item {
flex: 1;
text-align: center;
min-height: 120rpx;
.value {
font-size: 52rpx;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 12rpx;
display: block;
}
.label {
font-size: 32rpx;
color: #555;
font-weight: 500;
}
}
}
}
.order-card {
background: #fff;
border-radius: 24rpx;
padding: 40rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40rpx;
.title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.more {
display: flex;
align-items: center;
text {
font-size: 26rpx;
color: #999;
margin-right: 8rpx;
}
}
}
.order-list {
display: flex;
justify-content: space-between;
.order-item {
flex: 1;
text-align: center;
.icon-wrapper {
width: 100rpx;
height: 100rpx;
margin: 0 auto 0;
position: relative;
display: flex;
align-items: center;
justify-content: center;
.count-badge {
position: absolute;
top: -10rpx;
right: -10rpx;
background: #ff4d4f;
color: #fff;
font-size: 20rpx;
min-width: 32rpx;
height: 32rpx;
line-height: 32rpx;
text-align: center;
border-radius: 16rpx;
padding: 0 8rpx;
}
.order-icon {
width: 56rpx;
height: 56rpx;
}
}
.order-name {
font-size: 26rpx;
color: #666;
}
}
}
}
.quick-actions {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 30rpx;
margin-bottom: 30rpx;
.action-item {
background: #fff;
border-radius: 24rpx;
padding: 30rpx 20rpx;
text-align: center;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
.action-icon {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
margin: 0 auto 16rpx;
display: flex;
align-items: center;
justify-content: center;
}
.action-name {
font-size: 26rpx;
color: #666;
}
}
}
.service-list {
background: #fff;
border-radius: 28rpx;
padding: 20rpx 40rpx;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
.service-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 44rpx 0;
border-bottom: 4rpx solid #e8eaed;
min-height: 120rpx;
&:last-child {
border-bottom: none;
}
.left {
display: flex;
align-items: center;
.service-icon {
width: 88rpx;
height: 88rpx;
border-radius: 44rpx;
margin-right: 32rpx;
display: flex;
align-items: center;
justify-content: center;
}
.service-name {
font-size: 38rpx;
color: #1a1a1a;
font-weight: 500;
}
}
.right {
display: flex;
align-items: center;
.desc {
font-size: 30rpx;
color: #555;
margin-right: 20rpx;
}
}
}
}
// 背景色
.bg-red { background: #ff4d4f; }
.bg-blue { background: #1890ff; }
.bg-green { background: #52c41a; }
.bg-purple { background: #722ed1; }
.bg-orange { background: #fa8c16; }
.bg-cyan { background: #13c2c2; }
.bg-pink { background: #eb2f96; }
.bg-gray { background: #666666; }
</style>
+495
View File
@@ -0,0 +1,495 @@
<template>
<view class="userinfo-container">
<!-- 顶部标题栏 -->
<!-- 加载状态 -->
<view v-if="loading" class="loading-state">
<text>加载中...</text>
</view>
<!-- 编辑表单 -->
<view v-else class="form-wrapper">
<!-- 头像 -->
<view class="form-section">
<view class="form-group">
<text class="form-label">头像</text>
<view class="avatar-container">
<image
:src="formData.avatar"
class="avatar-image"
mode="aspectFill"
/>
<view class="avatar-upload" @click="uploadAvatar">
<uni-icons type="upload-filled" size="24" color="#FFA500" class="upload-icon"></uni-icons>
<text class="upload-text">更换头像</text>
</view>
</view>
</view>
</view>
<!-- 基本信息 -->
<view class="form-section">
<text class="section-title">基本信息</text>
<view class="form-group">
<text class="form-label">昵称</text>
<input
v-model="formData.nickname"
class="form-input"
placeholder="请输入昵称"
maxlength="50"
/>
</view>
<view class="form-group">
<text class="form-label">性别</text>
<view class="sex-group">
<view
class="sex-btn"
:class="{ active: formData.sex === 1}"
@click="formData.sex = 1"
>
</view>
<view
class="sex-btn"
:class="{ active: formData.sex === 2}"
@click="formData.sex = 2"
>
</view>
</view>
</view>
<view class="form-group">
<text class="form-label">年龄</text>
<input
v-model.number="formData.age"
class="form-input"
type="number"
placeholder="请输入年龄"
min="0"
max="150"
/>
</view>
</view>
<!-- 操作按钮 -->
<view class="button-group">
<button class="btn btn-submit" @click="submitForm" :disabled="submitting">
{{ submitting ? '保存中...' : '保存修改' }}
</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
const instance = getCurrentInstance();
// 2. 从实例中获取全局 $url
const $url = instance.appContext.config.globalProperties.$url;
// 数据
const loading = ref(false)
const submitting = ref(false)
// 表单数据
const formData = ref({
avatar: '',
patient_name: '',
phone: '',
nickname: '',
sex: 1,
age: ''
})
// 页面加载
onMounted(() => {
loadUserInfo()
})
// 加载用户信息
const loadUserInfo = async () => {
loading.value = true
try {
const res = await proxy.apiUrl({
url: '/api/user/info',
method: 'GET',
data: {}
})
if (res.code === 1) {
const userInfo = res.data
formData.value = {
avatar: userInfo.avatar || '',
patient_name: userInfo.patient_name || userInfo.nickname || '',
phone: userInfo.phone || '',
nickname: userInfo.nickname || '',
sex: userInfo.sex =='男'?1:2,
age: userInfo.age || ''
}
} else {
uni.showToast({ title: res.msg || '加载失败', icon: 'none' })
}
} catch (err) {
console.error('加载用户信息失败:', err)
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
// 上传头像
const uploadAvatar = () => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
const tempFilePath = res.tempFilePaths[0]
try {
uni.showLoading({ title: '上传中...' })
// 获取token
const token = uni.getStorageSync('token')
// 调用后端上传接口
const uploadRes = await new Promise((resolve, reject) => {
uni.uploadFile({
url: $url+'/api/upload/image',
filePath: tempFilePath,
name: 'file',
header: {
token: token,
'content-type': 'multipart/form-data'
},
success: (res) => {
const data = JSON.parse(res.data)
resolve(data)
},
fail: (err) => {
reject(err)
}
})
})
uni.hideLoading()
if (uploadRes.code === 1) {
// 上传成功,更新头像
formData.value.avatar = uploadRes.data.uri || uploadRes.data
uni.showToast({ title: '头像上传成功', icon: 'success' })
} else {
uni.showToast({ title: uploadRes.msg || '上传失败', icon: 'none' })
}
} catch (err) {
uni.hideLoading()
console.error('上传头像失败:', err)
uni.showToast({ title: '上传失败', icon: 'none' })
}
},
fail: (err) => {
console.error('选择图片失败:', err)
uni.showToast({ title: '选择图片失败', icon: 'none' })
}
})
}
// 验证手机号
const validatePhone = (phone) => {
if (!phone) return true // 可选字段
return /^1[3-9]\d{9}$/.test(phone)
}
// 提交表单
const submitForm = async () => {
// 验证必填字段
if (!formData.value.patient_name) {
uni.showToast({ title: '姓名不能为空', icon: 'none' })
return
}
// 验证手机号格式
if (formData.value.phone && !validatePhone(formData.value.phone)) {
uni.showToast({ title: '手机号格式不正确', icon: 'none' })
return
}
submitting.value = true
try {
// 调用后端API更新用户信息
const res = await proxy.apiUrl({
url: '/api/user/setInfo',
method: 'POST',
data: {
avatar: formData.value.avatar,
phone: formData.value.phone,
nickname: formData.value.nickname,
sex: formData.value.sex,
age: formData.value.age,
patient_name: formData.value.patient_name
}
})
if (res.code === 1) {
// 更新本地存储
const userData = uni.getStorageSync('userData')
const updatedData = {
...userData,
avatar: formData.value.avatar,
phone: formData.value.phone,
nickname: formData.value.nickname,
sex: formData.value.sex,
age: formData.value.age,
patient_name: formData.value.patient_name,
diagnosis: {
...userData.diagnosis,
patient_name: formData.value.patient_name
}
}
uni.setStorageSync('userData', updatedData)
uni.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
uni.showToast({ title: res.msg || '保存失败', icon: 'none' })
}
} catch (err) {
console.error('保存失败:', err)
uni.showToast({ title: '保存失败', icon: 'none' })
} finally {
submitting.value = false
}
}
// 返回上一页
const goBack = () => {
uni.navigateBack()
}
</script>
<style scoped>
/* 适老化设计:50-80岁 - 大字号、高对比、大触控区 */
.userinfo-container {
min-height: 100vh;
background: #e8eaed;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
background: #ffffff;
padding: 24rpx 40rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.header-back {
font-size: 48rpx;
color: #FFA500;
width: 88rpx;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
}
.header-title {
font-size: 44rpx;
font-weight: bold;
color: #1a1a1a;
flex: 1;
text-align: center;
}
.header-placeholder {
width: 88rpx;
}
.loading-state {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-size: 36rpx;
color: #555;
}
.form-wrapper {
padding: 40rpx;
}
.form-section {
background: #ffffff;
border-radius: 24rpx;
padding: 40rpx;
margin-bottom: 36rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
}
.section-title {
font-size: 40rpx;
font-weight: bold;
color: #FFA500;
margin-bottom: 32rpx;
display: block;
padding-bottom: 24rpx;
border-bottom: 4rpx solid #f0f0f0;
}
.form-group {
margin-bottom: 36rpx;
}
.form-group:last-child {
margin-bottom: 0;
}
.form-label {
display: block;
font-size: 36rpx;
color: #1a1a1a;
font-weight: 500;
margin-bottom: 20rpx;
line-height: 1.6;
}
.form-input {
width: 100%;
padding: 28rpx;
border: 4rpx solid #e0e0e0;
border-radius: 16rpx;
font-size: 36rpx;
line-height: 1.6;
color: #1a1a1a;
background: #ffffff;
box-sizing: border-box;
display: block;
height: auto;
min-height: 96rpx;
}
.form-input::placeholder {
color: #999;
font-size: 34rpx;
}
.avatar-container {
display: flex;
align-items: center;
gap: 32rpx;
}
.avatar-image {
width: 160rpx;
height: 160rpx;
border-radius: 80rpx;
background: #f0f0f0;
border: 4rpx solid #e0e0e0;
}
.avatar-upload {
flex: 1;
padding: 36rpx;
border: 4rpx dashed #FFA500;
border-radius: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16rpx;
min-height: 120rpx;
}
.upload-icon {
font-size: 56rpx;
}
.upload-text {
font-size: 34rpx;
color: #FFA500;
font-weight: 500;
}
.sex-group {
display: flex;
gap: 28rpx;
}
.sex-btn {
flex: 1;
padding: 32rpx;
border: 4rpx solid #e0e0e0;
border-radius: 16rpx;
text-align: center;
font-size: 38rpx;
line-height: 1.5;
color: #555;
background: #ffffff;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
min-height: 96rpx;
}
.sex-btn.active {
border-color: #FFA500;
background: #fff7e6;
color: #FFA500;
font-weight: bold;
}
.button-group {
display: flex;
gap: 28rpx;
margin-top: 56rpx;
margin-bottom: 56rpx;
}
.btn {
flex: 1;
padding: 36rpx;
border-radius: 20rpx;
font-size: 40rpx;
font-weight: bold;
border: none;
cursor: pointer;
transition: all 0.3s;
min-height: 100rpx;
}
.btn:active {
transform: scale(0.98);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-cancel {
background: #f0f0f0;
color: #555;
}
.btn-submit {
background: #FFA500;
color: #ffffff;
}
.btn-submit:active:not(:disabled) {
background: #e69500;
}
</style>