wxapp使用submodule

This commit is contained in:
lik
2026-05-27 15:10:30 +08:00
parent 43436f9a37
commit afc0e4d2e2
957 changed files with 14404 additions and 1 deletions

235
pages/ai/aichat/aichat.js Normal file
View File

@@ -0,0 +1,235 @@
const API = require('../../../utils/api.js')
Page({
data: {
messages: [],
inputValue: '',
inputFocus: false,
isTyping: false,
scrollToMessage: '',
quickQuestions: [
'怎么加入暖橙团队?',
'陪诊服务的流程是什么?',
'如何预约陪诊服务?',
'陪诊服务收费标准?'
]
},
messageIdCounter: 0,
onLoad() {
this.loadChatHistory()
this.initSocketListeners()
},
onUnload() {
this.removeSocketListeners()
},
onShow() {
this.setData({ inputFocus: true })
},
initSocketListeners() {
const app = getApp()
const socket = app.globalData.chatSocket
if (!socket) return
this._onSocketMessage = (data) => {
if (data && data.type === 'ai') {
this.handleAIReply(data)
}
}
this._onSocketError = (err) => {
console.error('[AIChat] WebSocket error', err)
this.handleAIError('网络连接失败,请检查网络设置')
}
socket.onMessage(this._onSocketMessage)
socket.onError(this._onSocketError)
},
removeSocketListeners() {
const app = getApp()
const socket = app.globalData.chatSocket
if (!socket) return
if (this._onSocketMessage) {
socket.onMessage(null)
}
if (this._onSocketError) {
socket.onError(null)
}
},
handleAIReply(data) {
let messages = null
if (this.data.messages.length > 0) {
let lastMessage = this.data.messages[this.data.messages.length - 1]
if (lastMessage.type === 'ai' && lastMessage._serverId === data.id) {
lastMessage.content += data.content || ''
messages = this.data.messages
}
}
if (!messages) {
const aiResponse = {
id: `msg_${++this.messageIdCounter}`,
_serverId: data.id,
type: 'ai',
contentType: 'text',
content: data.content || '',
time: this.formatTime(new Date())
}
if (data.sessionId) {
wx.setStorageSync('ai_session_id', data.sessionId)
}
messages = [...this.data.messages, aiResponse]
}
this.setData({
messages: messages,
isTyping: false
})
this.saveChatHistory(messages)
this.scrollToBottom()
},
loadChatHistory() {
try {
const history = wx.getStorageSync('ai_chat_history')
if (history && history.length > 0) {
this.setData({ messages: history })
this.scrollToBottom()
}
} catch (e) {
console.log('加载聊天记录失败', e)
}
},
saveChatHistory(messages) {
try {
wx.setStorageSync('ai_chat_history', messages)
} catch (e) {
console.log('保存聊天记录失败', e)
}
},
onInputChange(e) {
this.setData({ inputValue: e.detail.value })
},
onQuickQuestionTap(e) {
const question = e.currentTarget.dataset.question
this.setData({ inputValue: question })
this.sendMessage()
},
sendMessage() {
const content = this.data.inputValue.trim()
if (!content) return
const userMessage = {
id: `msg_${++this.messageIdCounter}`,
type: 'user',
contentType: 'text',
content: content,
time: this.formatTime(new Date())
}
const messages = [...this.data.messages, userMessage]
this.setData({
messages: messages,
inputValue: '',
isTyping: true
})
this.saveChatHistory(messages)
this.scrollToBottom()
this.sendToAI(content)
},
sendToAI(content, type = 'chat') {
const app = getApp()
const user = app.globalData.user
const socket = app.globalData.chatSocket
if (!socket.isConnected) {
socket.connect()
}
if (socket) {
socket.send({
type: type,
content: content,
userId: user ? user._id : '',
appId: app.globalData.appId
})
} else {
// 处理连接失败的情况
this.handleAIError('网络连接失败,请检查网络设置')
}
},
handleAIError(errorMessage) {
const errorResponse = {
id: `msg_${++this.messageIdCounter}`,
type: 'ai',
contentType: 'text',
content: errorMessage,
time: this.formatTime(new Date())
}
const messages = [...this.data.messages, errorResponse]
this.setData({
messages: messages,
isTyping: false
})
this.saveChatHistory(messages)
this.scrollToBottom()
},
scrollToBottom() {
setTimeout(() => {
const lastMessage = this.data.messages[this.data.messages.length - 1]
if (lastMessage) {
this.setData({
scrollToMessage: `msg-${lastMessage.id}`
})
}
}, 100)
},
previewImage(e) {
const src = e.currentTarget.dataset.src
wx.previewImage({
urls: [src],
current: src
})
},
formatTime(date) {
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${hours}:${minutes}`
},
clearChat() {
wx.showModal({
title: '确认清空',
content: '确定要清空所有聊天记录吗?',
success: (res) => {
if (res.confirm) {
this.sendToAI('clear', 'clear')
this.setData({ messages: [] })
wx.removeStorageSync('ai_chat_history')
wx.removeStorageSync('ai_session_id')
}
}
})
}
})

View File

@@ -0,0 +1,6 @@
{
"navigationBarTitleText": "AI陪诊助手",
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon"
}
}

123
pages/ai/aichat/aichat.wxml Normal file
View File

@@ -0,0 +1,123 @@
<view class="chat-container">
<!-- 消息列表区域 -->
<scroll-view
class="message-list {{messages.length > 0 ? 'has-messages' : ''}}"
scroll-y
scroll-into-view="{{scrollToMessage}}"
enhanced
show-scrollbar
>
<!-- 欢迎消息 -->
<view class="welcome-area" wx:if="{{messages.length === 0}}">
<view class="welcome-icon">
<t-icon name="chat" size="80rpx" color="#FF9B33" />
</view>
<text class="welcome-title">AI陪诊助手</text>
<text class="welcome-desc">我是您的智能陪诊助手,可以为您解答陪诊相关问题</text>
<view class="quick-questions">
<view
class="quick-item"
wx:for="{{quickQuestions}}"
wx:key="index"
bindtap="onQuickQuestionTap"
data-question="{{item}}"
>
<text>{{item}}</text>
</view>
</view>
</view>
<!-- 消息气泡 -->
<view
class="message-item {{item.type}}"
wx:for="{{messages}}"
wx:key="id"
id="msg-{{item.id}}"
>
<!-- AI消息头像在左内容在右 -->
<block wx:if="{{item.type === 'ai'}}">
<image class="avatar ai-avatar" src="/images/home-w.png" mode="aspectFill" />
<view class="message-content">
<view class="message-bubble">
<text class="message-text" wx:if="{{item.contentType === 'text'}}">{{item.content}}</text>
<image
wx:if="{{item.contentType === 'image'}}"
class="message-image"
src="{{item.content}}"
mode="widthFix"
bindtap="previewImage"
data-src="{{item.content}}"
/>
</view>
<text class="message-time">{{item.time}}</text>
</view>
</block>
<!-- 用户消息:内容在左,头像在右 -->
<block wx:if="{{item.type === 'user'}}">
<view class="message-content">
<view class="message-bubble">
<text class="message-text" wx:if="{{item.contentType === 'text'}}">{{item.content}}</text>
<image
wx:if="{{item.contentType === 'image'}}"
class="message-image"
src="{{item.content}}"
mode="widthFix"
bindtap="previewImage"
data-src="{{item.content}}"
/>
</view>
<text class="message-time">{{item.time}}</text>
</view>
<view class="avatar user-avatar">
<t-icon name="user-1" size="40rpx" color="#FFFFFF" />
</view>
</block>
</view>
<!-- AI正在输入提示 -->
<view class="message-item ai" wx:if="{{isTyping}}">
<image class="avatar ai-avatar" src="/images/home-active2.png" mode="aspectFill" />
<view class="message-content">
<view class="message-bubble typing-bubble">
<view class="typing-indicator">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
</view>
</view>
</view>
<!-- 底部留白,确保最新消息可见 -->
<view style="height: 40rpx;"></view>
</scroll-view>
<!-- 清空按钮 -->
<view class="clear-btn" wx:if="{{messages.length > 0}}" bindtap="clearChat">
<t-icon name="delete" size="28rpx" color="#999999" />
<text class="clear-text">清空聊天</text>
</view>
<!-- 输入区域 -->
<view class="input-area">
<view class="input-wrapper">
<input
class="message-input"
type="text"
value="{{inputValue}}"
placeholder="请输入您的问题..."
placeholder-class="input-placeholder"
confirm-type="send"
bindinput="onInputChange"
bindconfirm="sendMessage"
focus="{{inputFocus}}"
/>
<view class="input-actions">
<view class="action-btn send-btn {{inputValue ? 'active' : ''}}" bindtap="sendMessage">
<t-icon name="send" size="40rpx" color="{{inputValue ? '#FFFFFF' : '#CCCCCC'}}" />
</view>
</view>
</view>
</view>
</view>

278
pages/ai/aichat/aichat.wxss Normal file
View File

@@ -0,0 +1,278 @@
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #F5F5F5;
position: relative;
}
/* 清空按钮 */
.clear-btn {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6rpx;
padding: 12rpx 24rpx;
background-color: #FFFFFF;
border-top: 1rpx solid #EEEEEE;
}
.clear-btn:active {
opacity: 0.7;
}
.clear-text {
font-size: 22rpx;
color: #999999;
}
/* 消息列表区域 */
.message-list {
flex: 1;
padding: 20rpx 0;
}
.message-list.has-messages {
padding-top: 20rpx;
}
/* 欢迎区域 */
.welcome-area {
display: flex;
flex-direction: column;
align-items: center;
padding: 80rpx 40rpx;
text-align: center;
}
.welcome-icon {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
background: linear-gradient(135deg, #FF9B33 0%, #FF7A33 100%);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 155, 51, 0.3);
}
.welcome-title {
font-size: 40rpx;
font-weight: 600;
color: #333333;
margin-bottom: 16rpx;
}
.welcome-desc {
font-size: 28rpx;
color: #999999;
margin-bottom: 60rpx;
line-height: 1.6;
}
.quick-questions {
width: 100%;
display: flex;
flex-direction: column;
gap: 20rpx;
}
.quick-item {
background-color: #FFFFFF;
padding: 28rpx 32rpx;
border-radius: 16rpx;
font-size: 28rpx;
color: #333333;
text-align: left;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;
}
.quick-item:active {
transform: scale(0.98);
background-color: #FFF8F0;
}
/* 消息项 */
.message-item {
display: flex;
padding: 16rpx 24rpx;
align-items: flex-start;
}
.message-item.ai {
justify-content: flex-start;
}
.message-item.user {
justify-content: flex-end;
}
/* 头像 */
.avatar {
width: 72rpx;
height: 72rpx;
border-radius: 40%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.ai-avatar {
background: linear-gradient(135deg, #FF9B33 0%, #FF7A33 100%);
margin-right: 16rpx;
padding: 16rpx;
width: 46rpx;
height: 46rpx;
}
.user-avatar {
background: linear-gradient(135deg, #4CAF50 0%, #45A049 100%);
margin-left: 16rpx;
}
/* 消息内容 */
.message-content {
display: flex;
flex-direction: column;
max-width: 70%;
}
.message-item.user .message-content {
align-items: flex-end;
}
.message-item.ai .message-content {
align-items: flex-start;
}
/* 消息气泡 */
.message-bubble {
padding: 20rpx 24rpx;
border-radius: 16rpx;
word-wrap: break-word;
word-break: break-all;
}
.message-item.ai .message-bubble {
background-color: #FFFFFF;
border-top-left-radius: 4rpx;
}
.message-item.user .message-bubble {
background: linear-gradient(135deg, #FF9B33 0%, #FF7A33 100%);
border-top-right-radius: 4rpx;
}
.message-text {
font-size: 28rpx;
line-height: 1.6;
}
.message-item.ai .message-text {
color: #333333;
}
.message-item.user .message-text {
color: #FFFFFF;
}
.message-image {
max-width: 400rpx;
border-radius: 8rpx;
}
.message-time {
font-size: 20rpx;
color: #999999;
margin-top: 8rpx;
}
/* 正在输入指示器 */
.typing-bubble {
padding: 24rpx 32rpx;
}
.typing-indicator {
display: flex;
align-items: center;
gap: 12rpx;
}
.dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background-color: #CCCCCC;
animation: typing 1.4s infinite ease-in-out both;
}
.dot:nth-child(1) {
animation-delay: -0.32s;
}
.dot:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes typing {
0%, 80%, 100% {
transform: scale(0.6);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
/* 输入区域 */
.input-area {
background-color: #FFFFFF;
padding: 20rpx 24rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #EEEEEE;
}
.input-wrapper {
display: flex;
align-items: center;
gap: 16rpx;
}
.message-input {
flex: 1;
height: 72rpx;
background-color: #F5F5F5;
border-radius: 16rpx;
padding: 0 28rpx;
font-size: 28rpx;
color: #333333;
}
.input-placeholder {
color: #AAAAAA;
}
.input-actions {
display: flex;
align-items: center;
gap: 16rpx;
}
.action-btn {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #F5F5F5;
transition: all 0.2s ease;
}
.send-btn.active {
background: linear-gradient(135deg, #FF9B33 0%, #FF7A33 100%);
}

View File

@@ -0,0 +1,151 @@
const API = require('../../utils/api.js')
const STATUS_MAP = {
pending: { text: '待确认', color: '#F59E0B', bg: '#FEF3C7' },
confirmed: { text: '已确认', color: '#3B82F6', bg: '#DBEAFE' },
in_progress: { text: '进行中', color: '#8B5CF6', bg: '#EDE9FE' },
completed: { text: '已完成', color: '#10B981', bg: '#D1FAE5' },
cancelled: { text: '已取消', color: '#6B7280', bg: '#F3F4F6' }
}
const TYPE_MAP = {
outpatient: '门诊陪诊',
inpatient: '住院陪诊',
examination: '检查陪诊',
surgery: '手术陪诊',
medication: '取药陪诊',
other: '其他'
}
Page({
data: {
records: [],
loading: false,
hasMore: true,
page: 1,
pageSize: 10,
statusFilter: '',
statusTabs: [
{ value: '', label: '全部' },
{ value: 'pending', label: '待确认' },
{ value: 'confirmed', label: '已确认' },
{ value: 'in_progress', label: '进行中' },
{ value: 'completed', label: '已完成' }
]
},
onLoad() {
this.loadRecords(true)
},
onShow() {
this.loadRecords(true)
},
onPullDownRefresh() {
this.loadRecords(true).finally(() => {
wx.stopPullDownRefresh()
})
},
onReachBottom() {
if (this.data.hasMore && !this.data.loading) {
this.loadRecords(false)
}
},
loadRecords(reset = false) {
const app = getApp()
const user = app.globalData.user
if (!user || !user._id) {
wx.showToast({ title: '请先登录', icon: 'none' })
return Promise.resolve()
}
const page = reset ? 1 : this.data.page + 1
this.setData({ loading: true })
const params = {
userId: user._id,
page,
pageSize: this.data.pageSize
}
if (this.data.statusFilter) {
params.status = this.data.statusFilter
}
return API.escort.getMyRecords(params)
.then((res) => {
if (res.code === 0) {
const list = res.data.records || []
const formatted = list.map(item => this.formatRecord(item))
this.setData({
records: reset ? formatted : this.data.records.concat(formatted),
page,
hasMore: list.length >= this.data.pageSize
})
} else {
wx.showToast({ title: res.msg || '加载失败', icon: 'none' })
}
})
.catch(() => {
wx.showToast({ title: '网络请求失败', icon: 'none' })
})
.finally(() => {
this.setData({ loading: false })
})
},
formatRecord(item) {
const statusInfo = STATUS_MAP[item.escort?.status] || STATUS_MAP.pending
const typeText = TYPE_MAP[item.escort?.type] || '陪诊服务'
const fee = item.payment?.totalFee || 0
const appointmentTime = item.schedule?.date
? this.formatDateTime(item.schedule.date) + item.schedule.startTime
: '--'
return {
_id: item._id,
patientName: item.patient?.name || '--',
patientMobile: item.patient?.mobile || '--',
hospital: item.hospital?.name || '--',
department: item.hospital?.department || '--',
typeText,
status: item.escort?.status,
statusText: statusInfo.text,
statusColor: statusInfo.color,
statusBg: statusInfo.bg,
appointmentTime,
fee: fee.toFixed(2),
escortNote: item.notes?.escortNote || ''
}
},
formatDateTime(dateStr) {
const d = new Date(dateStr)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${year}-${month}-${day} `
},
onTabChange(e) {
const status = e.currentTarget.dataset.value
this.setData({ statusFilter: status })
this.loadRecords(true)
},
onTapRecord(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({
url: `/pages/mine/escort_record_detail/escort_record_detail?id=${id}`
})
},
makePhoneCall() {
wx.makePhoneCall({
phoneNumber: '18618162956'
})
}
})

View File

@@ -0,0 +1,6 @@
{
"navigationBarTitleText": "陪诊记录",
"usingComponents": {},
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark"
}

View File

@@ -0,0 +1,80 @@
<view class="container">
<!-- 状态筛选标签 -->
<view class="status-tabs">
<view
class="tab-item {{statusFilter === item.value ? 'active' : ''}}"
wx:for="{{statusTabs}}"
wx:key="value"
data-value="{{item.value}}"
bindtap="onTabChange"
>
{{item.label}}
</view>
</view>
<!-- 记录列表 -->
<view class="record-list" wx:if="{{records.length > 0}}">
<view
class="record-card"
wx:for="{{records}}"
wx:key="_id"
data-id="{{item._id}}"
bindtap="onTapRecord"
>
<!-- 卡片头部 -->
<view class="card-header">
<view class="header-left">
<text class="record-type">{{item.typeText}}</text>
<text class="record-time">{{item.appointmentTime}}</text>
</view>
<view class="status-badge" style="color: {{item.statusColor}}; background: {{item.statusBg}};">
{{item.statusText}}
</view>
</view>
<!-- 卡片内容 -->
<view class="card-body">
<view class="info-row">
<text class="info-label">就诊医院</text>
<text class="info-value">{{item.hospital}}</text>
</view>
<view class="info-row">
<text class="info-label">就诊科室</text>
<text class="info-value">{{item.department}}</text>
</view>
<view class="info-row">
<text class="info-label">就诊人</text>
<text class="info-value">{{item.patientName}} {{item.patientMobile}}</text>
</view>
</view>
<!-- 卡片底部 -->
<view class="card-footer">
<view class="fee-wrap">
<text class="fee-label">费用:</text>
<text class="fee-value">¥{{item.fee}}</text>
</view>
<view class="arrow">
<t-icon name="chevron-right" size="32rpx" color="#9CA3AF" />
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" wx:if="{{!loading && records.length === 0}}">
<t-icon name="file-unknown" size="120rpx" color="#D1D5DB" />
<text class="empty-text">暂无陪诊记录</text>
<text class="empty-subtext">您还没有预约陪诊服务</text>
</view>
<!-- 加载中 -->
<view class="loading-wrap" wx:if="{{loading}}">
<t-loading theme="circular" size="40rpx" text="加载中..." />
</view>
<!-- 到底提示 -->
<view class="no-more" wx:if="{{!loading && !hasMore && records.length > 0}}">
<text>—— 已经到底了 ——</text>
</view>
</view>

View File

@@ -0,0 +1,183 @@
page {
background-color: #FAF6F1;
}
.container {
min-height: 100vh;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
}
/* 状态筛选标签 */
.status-tabs {
display: flex;
align-items: center;
gap: 16rpx;
padding: 24rpx 32rpx;
background: #FFFFFF;
position: sticky;
top: 0;
z-index: 10;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
overflow-x: auto;
white-space: nowrap;
}
.tab-item {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 12rpx 28rpx;
font-size: 26rpx;
color: #6B7280;
background: #F3F4F6;
border-radius: 32rpx;
transition: all 0.2s;
}
.tab-item.active {
color: #FFFFFF;
background: linear-gradient(135deg, #FF9B33 0%, #FF8500 100%);
font-weight: 600;
}
/* 记录列表 */
.record-list {
padding: 20rpx 24rpx;
display: flex;
flex-direction: column;
gap: 20rpx;
}
.record-card {
background: #FFFFFF;
border-radius: 20rpx;
padding: 28rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
}
/* 卡片头部 */
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #F3F4F6;
}
.header-left {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.record-type {
font-size: 30rpx;
font-weight: 600;
color: #1F2937;
}
.record-time {
font-size: 24rpx;
color: #9CA3AF;
}
.status-badge {
font-size: 24rpx;
font-weight: 600;
padding: 8rpx 20rpx;
border-radius: 8rpx;
}
/* 卡片内容 */
.card-body {
display: flex;
flex-direction: column;
gap: 16rpx;
margin-bottom: 20rpx;
}
.info-row {
display: flex;
align-items: center;
}
.info-label {
font-size: 26rpx;
color: #9CA3AF;
width: 140rpx;
flex-shrink: 0;
}
.info-value {
font-size: 26rpx;
color: #374151;
flex: 1;
}
/* 卡片底部 */
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 20rpx;
border-top: 1rpx solid #F3F4F6;
}
.fee-wrap {
display: flex;
align-items: baseline;
}
.fee-label {
font-size: 26rpx;
color: #6B7280;
}
.fee-value {
font-size: 32rpx;
color: #FF9B33;
font-weight: 700;
}
.arrow {
display: flex;
align-items: center;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 200rpx;
}
.empty-text {
font-size: 30rpx;
color: #9CA3AF;
margin-top: 24rpx;
}
.empty-subtext {
font-size: 26rpx;
color: #D1D5DB;
margin-top: 12rpx;
}
/* 加载中 */
.loading-wrap {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
}
/* 到底提示 */
.no-more {
text-align: center;
padding: 32rpx;
font-size: 24rpx;
color: #D1D5DB;
}

View File

@@ -0,0 +1,66 @@
// pages/healthrecord/healthrecord.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})

View File

@@ -0,0 +1,3 @@
{
"usingComponents": {}
}

View File

@@ -0,0 +1,2 @@
<!--pages/healthrecord/healthrecord.wxml-->
<text>pages/healthrecord/healthrecord.wxml</text>

View File

@@ -0,0 +1 @@
/* pages/healthrecord/healthrecord.wxss */

43
pages/index/index.js Normal file
View File

@@ -0,0 +1,43 @@
Page({
data: {
contact: {
icon: 'logo-wechat-stroke',
phone: '18618162956'
},
services: {},
},
async onLoad() {
const app = getApp()
const services = await app.globalData.servicesReady
this.setData({
services: services.sort((a, b) => parseFloat(a.price) - parseFloat(b.price))
});
},
makePhoneCall() {
const phoneNumber = this.data.contact.phone;
wx.makePhoneCall({
phoneNumber: phoneNumber,
success: () => {
console.log('拨打电话成功');
},
fail: () => {
console.log('拨打电话失败');
}
});
},
goToDetail(e) {
const id = e.currentTarget.dataset.id;
wx.navigateTo({
url: `/pages/itemdetail/itemdetail?id=${id}`
});
},
goToAIChat() {
wx.switchTab({
url: '/pages/ai/aichat/aichat'
});
}
});

6
pages/index/index.json Normal file
View File

@@ -0,0 +1,6 @@
{
"navigationBarTitleText": "暖橙陪诊",
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon"
}
}

73
pages/index/index.wxml Normal file
View File

@@ -0,0 +1,73 @@
<view class="container">
<view class="header-section">
<image class="header-bg" src="/images/background.jpg" mode="aspectFill"></image>
<view class="user-info">
<image class="avatar" src="/images/home-active2.png" mode="aspectFill"></image>
<view class="welcome-text">
<text class="user-name">专业陪诊</text>
<text class="welcome-subtitle">让就医更简单,让客户更安心</text>
</view>
</view>
</view>
<view class="services-section">
<view class="service-item">
<view class="service-icon-wrap blue">
<image class="service-icon" src="/images/home-w.png" mode="aspectFill"></image>
</view>
<text class="service-name">陪诊</text>
</view>
<view class="service-item" bindtap="goToAIChat">
<view class="service-icon-wrap blue">
<image class="service-icon" src="/images/zixun-w.png" mode="aspectFill"></image>
</view>
<text class="service-name">AI客服</text>
</view>
<view class="service-item" bindtap="goToAIChat">
<view class="service-icon-wrap blue">
<image class="service-icon" src="/images/team-w.png" mode="aspectFill"></image>
</view>
<text class="service-name">商务合作</text>
</view>
<view class="service-item" bindtap="makePhoneCall">
<view class="service-icon-wrap blue">
<image class="service-icon" src="/images/call-w.png" mode="aspectFill"></image>
</view>
<text class="service-name">电话联系</text>
</view>
</view>
<view class="articles-section">
<view class="section-header">
<view class="section-title-wrap">
<t-icon name="view-list" class="section-icon" />
<text class="section-title">陪诊项目</text>
</view>
<text class="section-more"></text>
</view>
<view class="article-list">
<view class="article-item" wx:for="{{ services }}" wx:key="id" bindtap="goToDetail" data-id="{{ item.id }}">
<image class="article-image" src="{{ item.image }}" mode="aspectFill"></image>
<view class="article-content">
<text class="article-title">{{ item.title }}</text>
<view class="article-meta">
<text class="article-date">{{ item.subtitle }}</text>
<text class="article-time">{{ item.readTime }}</text>
</view>
</view>
<text class="article-price">¥{{ item.price }}</text>
</view>
</view>
<view class="contact-section">
<view class="contact-box">
<t-icon name="{{ contact.icon }}" class="contact-icon" />
<text class="contact-phone">18618162956</text>
<button class="contact-btn" bindtap="makePhoneCall">打电话 </button>
</view>
</view>
</view>
</view>

268
pages/index/index.wxss Normal file
View File

@@ -0,0 +1,268 @@
page {
background-color: #FAF6F1;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.container {
min-height: 100vh;
}
.header-section {
position: relative;
width: 100%;
height: 360rpx;
overflow: hidden;
padding-top: 10rpx;
}
.header-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.header-blur-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.doctor-image {
position: absolute;
right: -60rpx;
bottom: 0;
width: 380rpx;
height: 420rpx;
}
.user-info {
position: relative;
z-index: 10;
padding: 100rpx 40rpx 40rpx;
display: flex;
align-items: center;
}
.avatar {
width: 80rpx;
height: 80rpx;
}
.welcome-text {
margin-left: 24rpx;
}
.welcome-title {
display: block;
font-size: 28rpx;
color: #FFFFFF;
opacity: 0.9;
margin-bottom: 8rpx;
}
.user-name {
display: block;
font-size: 40rpx;
font-weight: 700;
color: #FFFFFF;
margin-bottom: 8rpx;
}
.welcome-subtitle {
display: block;
font-size: 26rpx;
color: #FFFFFF;
opacity: 0.8;
}
.contact-section {
padding: 0 32rpx;
margin-top: 40rpx;
position: relative;
z-index: 20;
}
.contact-box {
background: #FFFFFF;
border-radius: 10rpx;
padding: 34rpx 32rpx;
display: flex;
align-items: center;
}
.contact-icon {
font-size: 38rpx;
margin-right: 16rpx;
}
.contact-text {
font-size: 30rpx;
color: #9CA3AF;
margin-right: 16rpx;
}
.contact-phone {
font-size: 28rpx;
color: #1F2937;
font-weight: 600;
}
.contact-btn {
background: linear-gradient(135deg, #FF9B33 0%, #FF8500 100%);
color: #FFFFFF;
font-size: 28rpx;
font-weight: 600;
padding: 26rpx 6rpx;
border-radius: 20rpx;
margin-left: auto;
}
.services-section {
padding: 36rpx 32rpx;
display: flex;
justify-content: space-around;
background: #FFFFFF;
margin: 30rpx 0rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
}
.service-item {
display: flex;
flex-direction: column;
align-items: center;
}
.service-icon-wrap {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
}
.service-icon-wrap.blue {
background-color: #FF9B33;
}
.service-icon {
width: 50rpx;
height: 50rpx;
color: #FF9B33;
}
.service-name {
font-size: 28rpx;
color: #374151;
font-weight: 600;
}
.articles-section {
padding: 10rpx 0rpx;
}
.section-header {
display: flex;
align-items: center;
margin-bottom: 20rpx;
padding-left: 20rpx;
}
.section-icon {
font-size: 36rpx;
margin-right: 12rpx;
}
.section-title-wrap {
display: flex;
align-items: center;
}
.section-more {
margin-left: auto;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #1F2937;
}
.section-more {
font-size: 26rpx;
color: #FF9B33;
font-weight: 500;
}
.article-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.article-item {
background: #FFFFFF;
border-radius: 0rpx;
padding: 24rpx;
display: flex;
align-items: center;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
position: relative;
}
.article-image {
width: 160rpx;
height: 160rpx;
border-radius: 10rpx;
flex-shrink: 0;
}
.article-content {
flex: 1;
margin-left: 20rpx;
padding-right: 16rpx;
align-self: flex-start;
}
.article-title {
display: block;
font-size: 30rpx;
font-weight: 600;
color: #1F2937;
line-height: 1.4;
margin-bottom: 22rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.article-meta {
display: flex;
align-items: center;
gap: 16rpx;
}
.article-date {
font-size: 26rpx;
color: #6c6c6c;
}
.article-time {
font-size: 22rpx;
color: #9CA3AF;
}
.article-price {
font-size: 32rpx;
font-weight: 700;
color: #FF9B33;
align-self: flex-start;
margin-left: auto;
flex-shrink: 0;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
{
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon"
},
"navigationBarTitleText": "服务详情"
}

View File

@@ -0,0 +1,179 @@
<view class="container">
<!-- 服务图片 -->
<view class="service-image-wrap">
<image class="service-image" src="{{service.image}}" mode="aspectFill"></image>
</view>
<!-- 服务基本信息 -->
<view class="info-card">
<view class="service-header">
<view class="service-title-wrap">
<text class="service-title">{{service.title}}</text>
<view class="service-tag" wx:if="{{service.tag}}">{{service.tag}}</view>
</view>
<view class="service-price-wrap">
<text class="price-symbol">¥</text>
<text class="service-price">{{service.price}}</text>
<text class="price-unit">/次</text>
</view>
</view>
<view class="service-subtitle">{{service.subtitle}}</view>
</view>
<!-- 服务详情 -->
<view class="detail-card">
<view class="card-title">
<t-icon name="info-circle" class="card-icon" size="36rpx" />
<text>服务详情</text>
</view>
<view class="detail-content">
<text class="detail-text">{{service.description}}</text>
</view>
</view>
<!-- 服务流程 -->
<view class="detail-card">
<view class="card-title">
<t-icon name="list" class="card-icon" size="36rpx" />
<text>服务流程</text>
</view>
<view class="flow-list">
<view class="flow-item" wx:for="{{service.flow}}" wx:key="index">
<view class="flow-number">{{index + 1}}</view>
<view class="flow-content">
<text class="flow-title">{{item.title}}</text>
<text class="flow-desc">{{item.desc}}</text>
</view>
</view>
</view>
</view>
<!-- 注意事项 -->
<view class="detail-card">
<view class="card-title">
<t-icon name="error-circle" class="card-icon" size="36rpx" />
<text>注意事项</text>
</view>
<view class="notice-list">
<view class="notice-item" wx:for="{{service.notices}}" wx:key="index">
<view class="notice-dot"></view>
<text class="notice-text">{{item}}</text>
</view>
</view>
</view>
<!-- 底部占位,避免被下单栏遮挡 -->
<view class="bottom-placeholder"></view>
<!-- 底部下单栏 -->
<view class="bottom-bar">
<view class="price-info">
<text class="total-label">合计:</text>
<text class="total-price">¥{{totalPrice}}</text>
</view>
<button class="order-btn" bindtap="openOrderPopup">立即下单</button>
</view>
<!-- 下单弹窗遮罩 -->
<view class="popup-mask {{showOrderPopup ? 'show' : ''}}" bindtap="closeOrderPopup"></view>
<!-- 下单弹窗 -->
<view class="popup-content {{showOrderPopup ? 'show' : ''}}">
<view class="popup-header">
<text class="popup-title">确认订单</text>
<view class="popup-close" bindtap="closeOrderPopup">
<t-icon name="close" size="40rpx" />
</view>
</view>
<scroll-view class="popup-body" scroll-y>
<!-- 服务信息 -->
<view class="order-service-info">
<image class="order-service-image" src="{{service.image}}" mode="aspectFill"></image>
<view class="order-service-text">
<text class="order-service-title">{{service.title}}</text>
<text class="order-service-price">¥{{service.price}}/次</text>
</view>
</view>
<!-- 就诊人姓名 -->
<view class="order-row">
<text class="order-label">就诊人姓名</text>
<input class="order-input" value="{{patientName}}" placeholder="请输入就诊人姓名" placeholder-class="input-placeholder" bindinput="onPatientNameChange" />
</view>
<!-- 就诊人电话 -->
<view class="order-row">
<text class="order-label">联系电话</text>
<input class="order-input" value="{{patientPhone}}" placeholder="请输入联系电话" placeholder-class="input-placeholder" type="number" bindinput="onPatientPhoneChange" />
</view>
<!-- 就诊省份 -->
<view class="order-row">
<text class="order-label">就诊省份</text>
<picker mode="selector" range="{{provinces}}" value="{{provinceIndex}}" bindchange="onProvinceChange">
<view class="picker-value {{province ? '' : 'placeholder'}}">
{{province || '请选择省份'}}
</view>
</picker>
</view>
<!-- 就诊医院 -->
<view class="order-row">
<text class="order-label">就诊医院</text>
<input class="order-input" value="{{hospital}}" placeholder="请输入就诊医院" placeholder-class="input-placeholder" bindinput="onHospitalChange" />
</view>
<!-- 就诊科室 -->
<view class="order-row">
<text class="order-label">就诊科室</text>
<input class="order-input" value="{{department}}" placeholder="请输入就诊科室" placeholder-class="input-placeholder" bindinput="onDepartmentChange" />
</view>
<!-- 陪诊师性别要求 -->
<view class="order-row">
<text class="order-label">陪诊师性别</text>
<picker mode="selector" range="{{genderOptions}}" value="{{genderIndex}}" bindchange="onGenderChange">
<view class="picker-value {{gender ? '' : 'placeholder'}}">
{{gender || '请选择性别要求'}}
</view>
</picker>
</view>
<!-- 预约日期 -->
<view class="order-row">
<text class="order-label">预约日期</text>
<picker mode="date" value="{{appointmentDate}}" start="{{today}}" bindchange="onDateChange">
<view class="picker-value {{appointmentDate ? '' : 'placeholder'}}">
{{appointmentDate || '请选择预约日期'}}
</view>
</picker>
</view>
<!-- 预约时间 -->
<view class="order-row">
<text class="order-label">预约时间</text>
<picker mode="time" value="{{appointmentTime}}" start="08:00" end="18:00" bindchange="onTimeChange">
<view class="picker-value {{appointmentTime ? '' : 'placeholder'}}">
{{appointmentTime || '请选择预约时间'}}
</view>
</picker>
</view>
<!-- 备注 -->
<view class="order-row column">
<text class="order-label">备注说明</text>
<textarea class="order-textarea" value="{{remark}}" placeholder="请输入其他需求或备注信息" maxlength="200" placeholder-class="input-placeholder" bindinput="onRemarkChange" />
<text class="textarea-count">{{remark.length}}/200</text>
</view>
</scroll-view>
<view class="popup-footer">
<view class="popup-price">
<text class="popup-price-label">合计:</text>
<text class="popup-price-value">¥{{totalPrice}}</text>
</view>
<button class="popup-submit-btn" bindtap="submitOrder">提交订单</button>
</view>
</view>
</view>

View File

@@ -0,0 +1,495 @@
page {
background-color: #FAF6F1;
}
.container {
min-height: 100vh;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
/* 服务图片 */
.service-image-wrap {
width: 100%;
height: 420rpx;
overflow: hidden;
}
.service-image {
width: 100%;
height: 100%;
}
/* 信息卡片 */
.info-card {
background: #FFFFFF;
margin: -40rpx 24rpx 24rpx;
border-radius: 20rpx;
padding: 32rpx;
position: relative;
z-index: 10;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
}
.service-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16rpx;
}
.service-title-wrap {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12rpx;
}
.service-title {
font-size: 36rpx;
font-weight: 700;
color: #1F2937;
}
.service-tag {
background: linear-gradient(135deg, #FF9B33 0%, #FF8500 100%);
color: #FFFFFF;
font-size: 22rpx;
padding: 4rpx 16rpx;
border-radius: 8rpx;
}
.service-price-wrap {
display: flex;
align-items: baseline;
}
.price-symbol {
font-size: 28rpx;
color: #FF9B33;
font-weight: 600;
}
.service-price {
font-size: 44rpx;
color: #FF9B33;
font-weight: 700;
}
.price-unit {
font-size: 24rpx;
color: #9CA3AF;
margin-left: 4rpx;
}
.service-subtitle {
font-size: 26rpx;
color: #6B7280;
line-height: 1.5;
}
/* 详情卡片 */
.detail-card {
background: #FFFFFF;
margin: 0 24rpx 24rpx;
border-radius: 20rpx;
padding: 32rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
}
.card-title {
display: flex;
align-items: center;
font-size: 32rpx;
font-weight: 600;
color: #1F2937;
margin-bottom: 24rpx;
}
.card-icon {
color: #FF9B33;
margin-right: 12rpx;
}
.detail-content {
line-height: 1.8;
}
.detail-text {
font-size: 28rpx;
color: #4B5563;
line-height: 1.8;
}
/* 服务流程 */
.flow-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.flow-item {
display: flex;
align-items: flex-start;
gap: 20rpx;
}
.flow-number {
width: 44rpx;
height: 44rpx;
background: linear-gradient(135deg, #FF9B33 0%, #FF8500 100%);
color: #FFFFFF;
font-size: 24rpx;
font-weight: 600;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.flow-content {
flex: 1;
}
.flow-title {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #1F2937;
margin-bottom: 8rpx;
}
.flow-desc {
display: block;
font-size: 26rpx;
color: #6B7280;
line-height: 1.5;
}
/* 注意事项 */
.notice-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.notice-item {
display: flex;
align-items: flex-start;
gap: 16rpx;
}
.notice-dot {
width: 12rpx;
height: 12rpx;
background: #FF9B33;
border-radius: 50%;
margin-top: 12rpx;
flex-shrink: 0;
}
.notice-text {
font-size: 28rpx;
color: #4B5563;
line-height: 1.6;
flex: 1;
}
/* 底部占位 */
.bottom-placeholder {
height: 140rpx;
}
/* 底部下单栏 */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #FFFFFF;
padding: 24rpx 32rpx;
padding-bottom: calc(24rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
z-index: 100;
}
.price-info {
display: flex;
align-items: baseline;
}
.total-label {
font-size: 28rpx;
color: #6B7280;
}
.total-price {
font-size: 40rpx;
color: #FF9B33;
font-weight: 700;
}
.order-btn {
background: linear-gradient(135deg, #FF9B33 0%, #FF8500 100%);
color: #FFFFFF;
font-size: 30rpx;
font-weight: 600;
padding: 24rpx 56rpx;
border-radius: 40rpx;
border: none;
line-height: 1.5;
}
.order-btn::after {
border: none;
}
/* 弹窗遮罩 */
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 200;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
.popup-mask.show {
opacity: 1;
visibility: visible;
}
/* 弹窗内容 */
.popup-content {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #FFFFFF;
border-radius: 24rpx 24rpx 0 0;
z-index: 201;
transform: translateY(100%);
transition: transform 0.3s ease-out;
display: flex;
flex-direction: column;
max-height: 85vh;
}
.popup-content.show {
transform: translateY(0);
}
.popup-header {
display: flex;
align-items: center;
justify-content: center;
padding: 32rpx;
position: relative;
border-bottom: 1rpx solid #F3F4F6;
flex-shrink: 0;
}
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: #1F2937;
}
.popup-close {
position: absolute;
right: 32rpx;
top: 50%;
transform: translateY(-50%);
color: #9CA3AF;
}
.popup-body {
flex: 1;
overflow-y: auto;
padding: 32rpx;
max-height: 60vh;
width: auto;
}
/* 弹窗内服务信息 */
.order-service-info {
display: flex;
align-items: center;
gap: 20rpx;
padding-bottom: 24rpx;
border-bottom: 1rpx solid #F3F4F6;
margin-bottom: 24rpx;
}
.order-service-image {
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
flex-shrink: 0;
}
.order-service-text {
flex: 1;
}
.order-service-title {
display: block;
font-size: 30rpx;
font-weight: 600;
color: #1F2937;
margin-bottom: 12rpx;
}
.order-service-price {
display: block;
font-size: 32rpx;
color: #FF9B33;
font-weight: 700;
}
/* 表单行 */
.order-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 0;
border-bottom: 1rpx solid #F3F4F6;
}
.order-row.column {
flex-direction: column;
align-items: flex-start;
gap: 16rpx;
}
.order-label {
font-size: 28rpx;
color: #374151;
font-weight: 500;
flex-shrink: 0;
}
.order-input {
flex: 1;
text-align: right;
font-size: 28rpx;
color: #1F2937;
margin-left: 24rpx;
}
.input-placeholder {
color: #9CA3AF;
}
.picker-value {
font-size: 28rpx;
color: #1F2937;
}
.picker-value.placeholder {
color: #9CA3AF;
}
/* 数量控制 */
.quantity-control {
display: flex;
align-items: center;
gap: 16rpx;
}
.quantity-btn {
width: 52rpx;
height: 52rpx;
background: #F3F4F6;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #1F2937;
font-weight: 600;
}
.quantity-btn.disabled {
color: #CCCCCC;
background: #F9FAFB;
}
.quantity-value {
font-size: 30rpx;
color: #1F2937;
font-weight: 600;
min-width: 40rpx;
text-align: center;
}
/* 文本域 */
.order-textarea {
width: 100%;
height: 160rpx;
background: #F9FAFB;
border-radius: 12rpx;
padding: 20rpx;
font-size: 28rpx;
color: #1F2937;
box-sizing: border-box;
}
.textarea-count {
font-size: 24rpx;
color: #9CA3AF;
align-self: flex-end;
}
/* 弹窗底部 */
.popup-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 32rpx;
padding-bottom: calc(24rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #F3F4F6;
background: #FFFFFF;
flex-shrink: 0;
}
.popup-price {
display: flex;
align-items: baseline;
}
.popup-price-label {
font-size: 28rpx;
color: #6B7280;
}
.popup-price-value {
font-size: 40rpx;
color: #FF9B33;
font-weight: 700;
}
.popup-submit-btn {
background: linear-gradient(135deg, #FF9B33 0%, #FF8500 100%);
color: #FFFFFF;
font-size: 30rpx;
font-weight: 600;
padding: 24rpx 56rpx;
border-radius: 40rpx;
border: none;
line-height: 1.5;
}
.popup-submit-btn::after {
border: none;
}

View File

@@ -0,0 +1,195 @@
// pages/mine/comp_address/comp_address.js
Component({
properties: {
visible: {
type: Boolean,
value: false
}
},
data: {
addresses: [],
editData: {
_index: -1,
label: '',
province: '',
city: '',
district: '',
address: '',
postcode: '',
isDefault: false
},
isEditing: false
},
observers: {
visible(newVal) {
if (newVal) {
this.initAddressData()
}
}
},
methods: {
initAddressData() {
const app = getApp()
const user = app.globalData.user || {}
const addresses = user.addresses || []
this.setData({
addresses: addresses.map(item => ({ ...item })),
isEditing: false,
editData: {
_index: -1,
label: '',
province: '',
city: '',
district: '',
address: '',
postcode: '',
isDefault: false
}
})
},
onClose() {
this.triggerEvent('close')
},
onSheetTap() {},
onAddAddress() {
this.setData({
isEditing: true,
editData: {
_index: -1,
label: '',
province: '',
city: '',
district: '',
address: '',
postcode: '',
isDefault: this.data.addresses.length === 0
}
})
},
onEditAddress(e) {
const index = e.currentTarget.dataset.index
const item = this.data.addresses[index]
this.setData({
isEditing: true,
editData: {
_index: index,
label: item.label || '',
province: item.province || '',
city: item.city || '',
district: item.district || '',
address: item.address || '',
postcode: item.postcode || '',
isDefault: item.isDefault || false
}
})
},
onDeleteAddress(e) {
const index = e.currentTarget.dataset.index
wx.showModal({
title: '提示',
content: '确定删除该地址吗?',
success: (res) => {
if (res.confirm) {
const addresses = this.data.addresses.slice()
addresses.splice(index, 1)
if (addresses.length > 0 && !addresses.some(a => a.isDefault)) {
addresses[0].isDefault = true
}
this.setData({ addresses })
this.triggerEvent('save', { addresses })
}
}
})
},
onSetDefault(e) {
const index = e.currentTarget.dataset.index
const addresses = this.data.addresses.map((item, i) => ({
...item,
isDefault: i === index
}))
this.setData({ addresses })
this.triggerEvent('save', { addresses })
},
onBackToList() {
this.setData({ isEditing: false })
},
onRegionChange(e) {
const region = e.detail.value
this.setData({
'editData.province': region[0] || '',
'editData.city': region[1] || '',
'editData.district': region[2] || ''
})
},
onFieldInput(e) {
const field = e.currentTarget.dataset.field
this.setData({
[`editData.${field}`]: e.detail.value
})
},
onDefaultChange(e) {
this.setData({
'editData.isDefault': e.detail.value
})
},
onSaveEdit() {
const editData = this.data.editData
if (!editData.label.trim()) {
wx.showToast({ title: '请输入地址标签', icon: 'none' })
return
}
if (!editData.province || !editData.city || !editData.district) {
wx.showToast({ title: '请选择所在地区', icon: 'none' })
return
}
if (!editData.address.trim()) {
wx.showToast({ title: '请输入详细地址', icon: 'none' })
return
}
const addresses = this.data.addresses.slice()
const newItem = {
label: editData.label.trim(),
province: editData.province,
city: editData.city,
district: editData.district,
address: editData.address.trim(),
postcode: editData.postcode.trim(),
isDefault: editData.isDefault
}
if (editData.isDefault) {
addresses.forEach(item => { item.isDefault = false })
}
if (editData._index >= 0) {
addresses[editData._index] = newItem
} else {
if (addresses.length === 0) {
newItem.isDefault = true
}
addresses.push(newItem)
}
this.setData({
addresses,
isEditing: false
})
this.triggerEvent('save', { addresses })
}
}
})

View File

@@ -0,0 +1,6 @@
{
"component": true,
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon"
}
}

View File

@@ -0,0 +1,79 @@
<view class="address-overlay" wx:if="{{visible}}" bindtap="onClose">
<view class="address-sheet" catchtap="onSheetTap">
<view class="address-header">
<view class="address-back" wx:if="{{isEditing}}" bindtap="onBackToList">
<t-icon name="chevron-left" size="36rpx" color="#999" />
</view>
<text class="address-title">{{isEditing ? (editData._index >= 0 ? '编辑地址' : '新增地址') : '邮寄地址'}}</text>
<view class="address-close" bindtap="onClose">
<t-icon name="close" size="36rpx" color="#999" />
</view>
</view>
<view class="address-body" wx:if="{{!isEditing}}">
<view class="address-empty" wx:if="{{addresses.length === 0}}">
<t-icon name="location" size="80rpx" color="#CCCCCC" />
<text class="address-empty-text">暂无地址,点击添加</text>
</view>
<view class="address-list" wx:if="{{addresses.length > 0}}">
<view class="address-card" wx:for="{{addresses}}" wx:key="index">
<view class="address-card-main" bindtap="onEditAddress" data-index="{{index}}">
<view class="address-card-top">
<text class="address-label">{{item.label}}</text>
<text class="address-default-tag" wx:if="{{item.isDefault}}">默认</text>
</view>
<view class="address-card-content">
<text class="address-region">{{item.province}} {{item.city}} {{item.district}}</text>
<text class="address-detail">{{item.address}}</text>
<text class="address-postcode" wx:if="{{item.postcode}}">邮编:{{item.postcode}}</text>
</view>
</view>
<view class="address-card-actions">
<view class="address-action-default" bindtap="onSetDefault" data-index="{{index}}">
<t-icon name="{{item.isDefault ? 'check-circle-filled' : 'circle'}}" size="32rpx" color="{{item.isDefault ? '#FF8500' : '#999'}}" />
<text class="action-text {{item.isDefault ? 'active' : ''}}">{{item.isDefault ? '默认地址' : '设为默认'}}</text>
</view>
<view class="address-action-delete" bindtap="onDeleteAddress" data-index="{{index}}">
<t-icon name="delete" size="32rpx" color="#999" />
<text class="action-text">删除</text>
</view>
</view>
</view>
</view>
</view>
<view class="address-body" wx:if="{{isEditing}}">
<view class="address-form">
<view class="form-item">
<text class="form-label">地址标签</text>
<input class="form-input" value="{{editData.label}}" placeholder="如:家庭、公司" maxlength="20" bindinput="onFieldInput" data-field="label" />
</view>
<view class="form-item">
<text class="form-label">所在地区</text>
<picker mode="region" value="{{[editData.province, editData.city, editData.district]}}" bindchange="onRegionChange">
<view class="form-picker">
{{editData.province || '请选择地区'}}{{editData.city ? ' ' + editData.city : ''}}{{editData.district ? ' ' + editData.district : ''}}
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">详细地址</text>
<input class="form-input" value="{{editData.address}}" placeholder="请输入详细地址" maxlength="100" bindinput="onFieldInput" data-field="address" />
</view>
<view class="form-item form-item-switch">
<text class="form-label">设为默认地址</text>
<switch checked="{{editData.isDefault}}" bindchange="onDefaultChange" color="#FF8500" />
</view>
</view>
</view>
<view class="address-footer">
<button class="address-add-btn" wx:if="{{!isEditing}}" bindtap="onAddAddress">添加新地址</button>
<button class="address-save-btn" wx:if="{{isEditing}}" bindtap="onSaveEdit">保存</button>
</view>
</view>
</view>

View File

@@ -0,0 +1,237 @@
.address-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.address-sheet {
background: #FFFFFF;
border-radius: 32rpx 32rpx 0 0;
padding-bottom: env(safe-area-inset-bottom);
animation: slideUp 0.3s ease;
max-height: 85vh;
overflow-y: auto;
display: flex;
flex-direction: column;
}
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.address-header {
display: flex;
align-items: center;
justify-content: center;
padding: 32rpx;
position: relative;
border-bottom: 1rpx solid #F5F5F5;
flex-shrink: 0;
}
.address-title {
font-size: 32rpx;
font-weight: 600;
color: #1A1A1A;
}
.address-back {
position: absolute;
left: 32rpx;
top: 50%;
transform: translateY(-50%);
padding: 8rpx;
}
.address-close {
position: absolute;
right: 32rpx;
top: 50%;
transform: translateY(-50%);
padding: 8rpx;
}
.address-body {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.address-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 32rpx;
}
.address-empty-text {
margin-top: 24rpx;
font-size: 28rpx;
color: #999999;
}
.address-list {
padding: 24rpx 32rpx;
}
.address-card {
background: #FAFAFA;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 24rpx;
}
.address-card-main {
margin-bottom: 16rpx;
}
.address-card-top {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 12rpx;
}
.address-label {
font-size: 30rpx;
font-weight: 600;
color: #1A1A1A;
}
.address-default-tag {
font-size: 22rpx;
color: #FF8500;
background: rgba(255, 133, 0, 0.1);
padding: 4rpx 12rpx;
border-radius: 8rpx;
}
.address-card-content {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.address-region {
font-size: 28rpx;
color: #666666;
}
.address-detail {
font-size: 28rpx;
color: #1A1A1A;
line-height: 1.5;
}
.address-postcode {
font-size: 24rpx;
color: #999999;
}
.address-card-actions {
display: flex;
align-items: center;
justify-content: space-between;
border-top: 1rpx solid #EEEEEE;
padding-top: 16rpx;
}
.address-action-default,
.address-action-delete {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx;
}
.action-text {
font-size: 26rpx;
color: #666666;
}
.action-text.active {
color: #FF8500;
}
.address-footer {
padding: 4rpx 32rpx 48rpx;
flex-shrink: 0;
}
.address-add-btn {
background: linear-gradient(135deg, #FF9B33 0%, #FF8500 100%);
color: #FFFFFF;
font-size: 30rpx;
font-weight: 600;
padding: 28rpx;
border-radius: 20rpx;
border: none;
line-height: 1.5;
}
.address-add-btn::after {
border: none;
}
.address-form {
padding: 0 32rpx;
}
.form-item {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 100rpx;
border-bottom: 1rpx solid #F5F5F5;
}
.form-item-switch {
justify-content: space-between;
}
.form-label {
font-size: 30rpx;
color: #1A1A1A;
font-weight: 500;
flex-shrink: 0;
margin-right: 24rpx;
}
.form-input {
flex: 1;
text-align: right;
font-size: 30rpx;
color: #1A1A1A;
}
.form-picker {
flex: 1;
text-align: right;
font-size: 30rpx;
color: #666666;
}
.address-save-btn {
background: linear-gradient(135deg, #FF9B33 0%, #FF8500 100%);
color: #FFFFFF;
font-size: 30rpx;
font-weight: 600;
padding: 28rpx;
border-radius: 20rpx;
border: none;
line-height: 1.5;
}
.address-save-btn::after {
border: none;
}

View File

@@ -0,0 +1,135 @@
// pages/mine/comp_profile/comp_profile.js
Component({
properties: {
visible: {
type: Boolean,
value: false
}
},
data: {
editData: {
name: '',
sex: 'male',
birth: '1970-1-1',
province: '',
city: '',
district: '',
phone: ''
},
today: ''
},
lifetimes: {
attached() {
const today = new Date()
this.setData({
today: `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
})
},
ready() {
this.initEditData()
}
},
observers: {
visible(newVal) {
if (newVal) {
this.initEditData()
}
}
},
methods: {
initEditData() {
const app = getApp()
const user = app.globalData.user || {}
const profile = user.profile || {}
const location = user.location || {}
let birth = ''
if (profile.birth) {
const date = new Date(profile.birth)
birth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
}
this.setData({
editData: {
avatar: profile.avatar || '/images/home-active2.png',
name: profile.name || '',
phone: profile.mobile || '',
sex: profile.sex || '',
birth: birth,
province: location.province || '',
city: location.city || '',
district: location.district || ''
}
})
},
onClose() {
this.triggerEvent('close')
},
onSheetTap() {},
onChooseAvatar() {
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePath = res.tempFiles[0].tempFilePath
this.setData({
'editData.avatar': tempFilePath
})
}
})
},
onSelectSex(e) {
const sex = e.currentTarget.dataset.sex
this.setData({
'editData.sex': sex
})
},
onBirthChange(e) {
this.setData({
'editData.birth': e.detail.value
})
},
onRegionChange(e) {
const region = e.detail.value
this.setData({
'editData.province': region[0] || '',
'editData.city': region[1] || '',
'editData.district': region[2] || ''
})
},
onFieldInput(e) {
const field = e.currentTarget.dataset.field
this.setData({
[`editData.${field}`]: e.detail.value
})
},
onSave() {
const editData = this.data.editData
if (editData.idnumber && editData.idnumber.length !== 18) {
wx.showToast({
title: '身份证号应为18位',
icon: 'none'
})
return
}
this.triggerEvent('save', {
editData: editData
})
}
}
})

View File

@@ -0,0 +1,6 @@
{
"component": true,
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon"
}
}

View File

@@ -0,0 +1,64 @@
<view class="profile-overlay" wx:if="{{visible}}" bindtap="onClose">
<view class="profile-sheet" catchtap="onSheetTap">
<view class="profile-header">
<text class="profile-title">个人信息</text>
<view class="profile-close" bindtap="onClose">
<t-icon name="close" size="36rpx" color="#999" />
</view>
</view>
<view class="profile-body">
<view class="profile-item profile-item-avatar" bindtap="onChooseAvatar">
<text class="profile-label">头像</text>
<view class="profile-avatar-wrap">
<image class="profile-avatar" src="{{editData.avatar}}" mode="aspectFill" />
<t-icon name="chevron-right" size="32rpx" color="#ccc" />
</view>
</view>
<view class="profile-item">
<text class="profile-label">姓名</text>
<input class="profile-input" value="{{editData.name}}" placeholder="请输入姓名" maxlength="20" bindinput="onFieldInput" data-field="name" />
</view>
<view class="profile-item">
<text class="profile-label">性别</text>
<view class="profile-sex-wrap">
<view class="profile-sex-tag {{editData.sex === 'male' ? 'profile-sex-tag-active' : ''}}" bindtap="onSelectSex" data-sex="male">
<text class="profile-sex-text">男</text>
</view>
<view class="profile-sex-tag {{editData.sex === 'female' ? 'profile-sex-tag-active' : ''}}" bindtap="onSelectSex" data-sex="female">
<text class="profile-sex-text">女</text>
</view>
</view>
</view>
<view class="profile-item">
<text class="profile-label">出生日期</text>
<picker mode="date" value="{{editData.birth}}" start="1930-01-01" end="{{today}}" bindchange="onBirthChange">
<view class="picker">
{{editData.birth || '请选择出生日期'}}
</view>
</picker>
</view>
<view class="profile-item">
<text class="profile-label">所在地区</text>
<picker mode="region" value="{{[editData.province, editData.city, editData.district]}}" bindchange="onRegionChange">
<view class="picker">
{{editData.province || '请选择地区'}}{{editData.city ? '' + editData.city : ''}}{{editData.district ? '' + editData.district : ''}}
</view>
</picker>
</view>
<view class="profile-item">
<text class="profile-label">手机号</text>
<text class="profile-value">{{editData.phone || '未绑定'}}</text>
</view>
</view>
<view class="profile-footer">
<button class="profile-save-btn" bindtap="onSave">保存</button>
</view>
</view>
</view>

View File

@@ -0,0 +1,158 @@
.profile-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.profile-sheet {
background: #FFFFFF;
border-radius: 32rpx 32rpx 0 0;
padding-bottom: env(safe-area-inset-bottom);
animation: slideUp 0.3s ease;
max-height: 85vh;
overflow-y: auto;
display: flex;
flex-direction: column;
}
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.profile-header {
display: flex;
align-items: center;
justify-content: center;
padding: 32rpx;
position: relative;
border-bottom: 1rpx solid #F5F5F5;
flex-shrink: 0;
}
.profile-title {
font-size: 32rpx;
font-weight: 600;
color: #1A1A1A;
}
.profile-close {
position: absolute;
right: 32rpx;
top: 50%;
transform: translateY(-50%);
padding: 8rpx;
}
.profile-body {
padding: 0 32rpx;
flex: 1;
overflow-y: auto;
}
.profile-item {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 100rpx;
border-bottom: 1rpx solid #F5F5F5;
}
.profile-item-avatar {
justify-content: space-between;
}
.profile-item-last {
border-bottom: none;
}
.profile-label {
font-size: 30rpx;
color: #1A1A1A;
font-weight: 500;
flex-shrink: 0;
margin-right: 24rpx;
}
.profile-input {
flex: 1;
text-align: right;
font-size: 30rpx;
color: #1A1A1A;
}
.picker {
flex: 1;
text-align: right;
font-size: 30rpx;
color: #666666;
}
.profile-value {
font-size: 30rpx;
color: #666666;
}
.profile-avatar-wrap {
display: flex;
align-items: center;
gap: 16rpx;
}
.profile-avatar {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
background: #F5F5F5;
}
.profile-sex-wrap {
display: flex;
gap: 20rpx;
}
.profile-sex-tag {
padding: 10rpx 48rpx;
border-radius: 8rpx;
background: #F5F5F5;
}
.profile-sex-tag-active {
background: linear-gradient(135deg, #FF9B33 0%, #FF8500 100%);
}
.profile-sex-text {
font-size: 28rpx;
color: #666666;
}
.profile-sex-tag-active .profile-sex-text {
color: #FFFFFF;
}
.profile-footer {
padding: 4rpx 32rpx 48rpx;
flex-shrink: 0;
}
.profile-save-btn {
background: linear-gradient(135deg, #FF9B33 0%, #FF8500 100%);
color: #FFFFFF;
font-size: 30rpx;
font-weight: 600;
padding: 28rpx;
border-radius: 20rpx;
border: none;
line-height: 1.5;
}
.profile-save-btn::after {
border: none;
}

228
pages/mine/mine.js Normal file
View File

@@ -0,0 +1,228 @@
const API = require('../../utils/api.js')
Page({
data: {
userInfo: {
avatar: '/images/home-active2.png',
name: '用户',
phone: '',
sex: '',
birth: '',
email: '',
idnumber: '',
ssn: '',
isLoggedIn: false
},
profileVisible: false,
menuGroups: [{
groupName: '个人服务',
items: [{
name: '个人信息',
icon: 'verify',
bindtap: 'onTapProfile'
}, {
name: '健康档案',
icon: 'user-vip',
bindtap: 'onTapHealth'
}, {
name: '邮寄地址',
icon: 'location',
bindtap: 'onTapAddress'
}]
}, {
groupName: '陪诊',
items: [{
name: '陪诊记录',
icon: 'assignment',
bindtap: 'onTapEscortRecord'
}]
}, {
groupName: '其他',
items: [{
name: '关于我们',
icon: 'info-circle',
bindtap: 'onTapAboutUs'
}]
}]
},
onLoad() {
this.loadUserInfo()
},
onShow() {
this.loadUserInfo()
},
loadUserInfo() {
const app = getApp()
if (app.globalData.user && app.globalData.user.profile) {
const profile = app.globalData.user.profile
this.setData({
'userInfo.isLoggedIn': true,
'userInfo.name': profile.name || '用户',
'userInfo.phone': profile.mobile || '',
'userInfo.sex': profile.sex || '',
'userInfo.birth': profile.birth ? this.formatDate(profile.birth) : '',
'userInfo.email': profile.email || '',
'userInfo.idnumber': profile.idnumber || '',
'userInfo.ssn': profile.ssn || ''
})
}
},
formatDate(date) {
if (!date) return ''
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
onTapProfile() {
this.setData({ profileVisible: true })
},
onTapAddress() {
this.setData({ addressVisible: true })
},
onAddressClose() {
this.setData({ addressVisible: false })
},
onAddressSave(e) {
const app = getApp()
const user = app.globalData.user
if (!user || !user._id) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
wx.showLoading({ title: '保存中...' })
const updateData = {
_id: user._id,
addresses: e.detail.addresses
}
API.user.update(updateData)
.then((data) => {
if (data.code === 0) {
app.globalData.user = data.data.user
wx.showToast({ title: '保存成功', icon: 'success' })
} else {
wx.showToast({ title: data.msg || '保存失败', icon: 'none' })
}
})
.catch(() => {
wx.showToast({ title: '网络请求失败', icon: 'none' })
})
.finally(() => {
wx.hideLoading()
})
},
onTapHealth() {
this.setData({ healthVisible: true })
},
onHealthClose() {
this.setData({ healthVisible: false })
},
onTapEscortRecord() {
wx.navigateTo({
url: '/pages/escort_record_list/escort_record_list'
})
},
onTapSetting() {
this.setData({ settingVisible: true })
},
onSettingClose() {
this.setData({ settingVisible: false })
},
onProfileClose() {
this.setData({ profileVisible: false })
},
onProfileSave(e) {
const newUserInfo = e.detail.editData
const app = getApp()
const user = app.globalData.user
if (!user || !user._id) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
wx.showLoading({ title: '保存中...' })
const updateData = {
_id: user._id,
profile: {
name: newUserInfo.name,
sex: newUserInfo.sex,
birth: newUserInfo.birth ? new Date(newUserInfo.birth).toISOString() : null,
mobile: newUserInfo.phone
},
location: {
province: newUserInfo.province,
city: newUserInfo.city,
district: newUserInfo.district,
},
}
API.user.update(updateData)
.then((data) => {
if (data.code === 0) {
app.globalData.user = data.data.user
this.loadUserInfo()
wx.showToast({ title: '保存成功', icon: 'success' })
this.setData({ profileVisible: false })
} else {
wx.showToast({ title: data.msg || '保存失败', icon: 'none' })
}
})
.catch(() => {
wx.showToast({ title: '网络请求失败', icon: 'none' })
})
.finally(() => {
wx.hideLoading()
})
},
handleLogin(e) {
const phoneCode = e.detail.code ? e.detail.code : ''
API.user.wxGetPhoneNumber({ code: phoneCode })
.then((data) => {
const phoneNumber = data.data.phoneNumber
wx.setStorageSync('user_phonenumber', phoneNumber)
wx.login({
success: (res) => {
if (res.code) {
API.user.wxSignin({ phoneNumber, code: res.code })
.then((data) => {
if (data.code == 0) {
const app = getApp()
app.globalData.user = data.data.user
this.loadUserInfo()
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
})
})
.catch((err) => {
wx.showToast({ title: '网络请求失败', icon: 'none' })
console.log('请求失败', err)
})
}
})

11
pages/mine/mine.json Normal file
View File

@@ -0,0 +1,11 @@
{
"navigationStyle": "custom",
"navigationBarTitleText": "Home",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon",
"comp-profile": "./comp_profile/comp_profile",
"comp-address": "./comp_address/comp_address"
}
}

41
pages/mine/mine.wxml Normal file
View File

@@ -0,0 +1,41 @@
<view class="container">
<view class="header-card" bindtap="onTapProfile">
<image class="avatar" src="{{userInfo.avatar}}" mode="aspectFill"></image>
<text class="user-name">{{userInfo.name}}</text>
</view>
<view class="menu-section" wx:for="{{menuGroups}}" wx:key="groupName" wx:if="{{userInfo.isLoggedIn || item.groupName === '其他'}}">
<view class="menu-list">
<view class="menu-item" wx:for="{{item.items}}" wx:for-item="menuItem" wx:key="name" hover-class="menu-item-hover" bindtap="{{menuItem.bindtap}}">
<t-icon name="{{menuItem.icon}}" class="menu-icon" size="40rpx" />
<text class="menu-text">{{menuItem.name}}</text>
<t-icon name="chevron-right" class="menu-arrow" size="36rpx" />
</view>
</view>
</view>
<view class="login-section" wx:if="{{!userInfo.isLoggedIn}}">
<button class="login-btn" open-type="getPhoneNumber" bindgetphonenumber="handleLogin">
登录
</button>
</view>
<view class="footer-section">
<text class="version">版本号V 0.1.0</text>
<text class="copyright">© 2026 北京奕华盛科技 版权所有</text>
</view>
<comp-profile
id="profileComp"
visible="{{profileVisible}}"
bind:close="onProfileClose"
bind:save="onProfileSave"
/>
<comp-address
id="addressComp"
visible="{{addressVisible}}"
bind:close="onAddressClose"
bind:save="onAddressSave"
/>
</view>

153
pages/mine/mine.wxss Normal file
View File

@@ -0,0 +1,153 @@
page {
background-color: #F5F5F5;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.container {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header-card {
background: #FFFFFF;
padding: 150rpx 32rpx 48rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.avatar {
width: 120rpx;
height: 120rpx;
}
.user-name {
font-size: 36rpx;
font-weight: 600;
color: #1A1A1A;
margin-top: 24rpx;
}
.user-phone {
font-size: 26rpx;
color: #999999;
margin-top: 12rpx;
}
.login-section {
padding: 32rpx;
display: flex;
flex-direction: column;
gap: 20rpx;
}
.login-btn {
background: linear-gradient(135deg, #FF9B33 0%, #FF8500 100%);
color: #FFFFFF;
font-size: 30rpx;
font-weight: 600;
padding: 28rpx;
border-radius: 20rpx;
border: none;
line-height: 1.5;
}
.login-btn::after {
border: none;
}
.avatar-btn {
background: #FFFFFF;
color: #FF9B33;
font-size: 28rpx;
font-weight: 500;
padding: 24rpx;
border-radius: 20rpx;
border: 1rpx solid #FF9B33;
line-height: 1.5;
}
.avatar-btn::after {
border: none;
}
.nickname-input-wrap {
background: #FFFFFF;
border-radius: 20rpx;
padding: 24rpx 32rpx;
}
.nickname-input {
font-size: 28rpx;
color: #1F2937;
}
.menu-section {
padding: 24rpx 22rpx 0rpx 22rpx;
}
.menu-group-header {
font-size: 26rpx;
color: #999999;
padding: 16rpx 0;
margin-bottom: 16rpx;
}
.menu-list {
background: #FFFFFF;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
}
.menu-item {
display: flex;
align-items: center;
padding: 32rpx;
border-bottom: 1rpx solid #F5F5F5;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-item-hover {
background: #FAFAFA;
}
.menu-icon {
color: #FF9B33;
margin-right: 24rpx;
}
.menu-text {
flex: 1;
font-size: 30rpx;
color: #1A1A1A;
font-weight: 500;
}
.menu-arrow {
color: #CCCCCC;
}
.footer-section {
display: flex;
flex-direction: column;
align-items: center;
padding: 60rpx 32rpx;
margin-top: auto;
}
.version {
font-size: 24rpx;
color: #B0B0B0;
margin-bottom: 12rpx;
}
.copyright {
font-size: 22rpx;
color: #B0B0B0;
}