初始化上传医疗项目到 medical-mall
This commit is contained in:
781
pages/mall/merchant/ai-consultation.uvue
Normal file
781
pages/mall/merchant/ai-consultation.uvue
Normal file
@@ -0,0 +1,781 @@
|
||||
<!-- 机构端 - AI问诊页面 -->
|
||||
<template>
|
||||
<view class="ai-page">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<view class="detail-navbar">
|
||||
<view class="detail-navbar-back" @click="uni.navigateBack()">
|
||||
<text class="back-arrow">‹</text>
|
||||
<text class="back-text">返回</text>
|
||||
</view>
|
||||
<text class="detail-navbar-title">AI问诊</text>
|
||||
<view style="width: 120rpx;"></view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 高风险提示横幅(始终显示) -->
|
||||
<view class="risk-banner">
|
||||
<text class="risk-banner-icon">⚠️</text>
|
||||
<text class="risk-banner-text">AI问诊仅供参考,不能代替专业医生诊断,紧急情况请立即拨打120</text>
|
||||
</view>
|
||||
|
||||
<!-- 症状快捷选择 -->
|
||||
<view v-if="!hasConversation" class="quick-select-area">
|
||||
<text class="qs-title">请选择主要症状(可多选)</text>
|
||||
<view class="qs-tags">
|
||||
<view
|
||||
v-for="sym in symptomOptions"
|
||||
:key="sym"
|
||||
class="qs-tag"
|
||||
:class="selectedSymptoms.includes(sym) ? 'qs-tag-selected' : ''"
|
||||
@click="toggleSymptom(sym)"
|
||||
>
|
||||
{{ sym }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="selectedSymptoms.length > 0" class="qs-confirm-row">
|
||||
<view class="qs-confirm-btn" @click="startWithSymptoms">开始问诊</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 对话区域 -->
|
||||
<scroll-view
|
||||
class="chat-scroll"
|
||||
direction="vertical"
|
||||
:scroll-into-view="lastMsgId"
|
||||
:scroll-with-animation="true"
|
||||
>
|
||||
<!-- 欢迎提示 -->
|
||||
<view class="welcome-msg" v-if="messages.length === 0 && !hasConversation">
|
||||
<view class="welcome-avatar">🤖</view>
|
||||
<view class="welcome-bubble">
|
||||
<text class="welcome-title">您好!我是医养AI助手</text>
|
||||
<text class="welcome-desc">我可以帮助您初步了解用户症状、进行健康风险评估,以及提供护理建议。请选择上方症状或直接输入描述。</text>
|
||||
<text class="welcome-disclaimer">⚠ 本工具仅供辅助参考,最终诊疗请遵循医嘱。</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<view
|
||||
v-for="(msg, idx) in messages"
|
||||
:key="idx"
|
||||
:id="'msg-' + idx"
|
||||
class="msg-row"
|
||||
:class="msg.role === 'user' ? 'msg-row-right' : 'msg-row-left'"
|
||||
>
|
||||
<!-- AI头像 -->
|
||||
<view v-if="msg.role === 'ai'" class="msg-avatar ai-avatar">🤖</view>
|
||||
|
||||
<view class="msg-bubble-wrap" :class="msg.role === 'user' ? 'mbw-right' : 'mbw-left'">
|
||||
<!-- 高风险提醒卡片 -->
|
||||
<view v-if="msg.riskLevel === 'high'" class="risk-card">
|
||||
<view class="risk-card-header">
|
||||
<text class="risk-card-icon">🚨</text>
|
||||
<text class="risk-card-title">高风险提示</text>
|
||||
</view>
|
||||
<text class="risk-card-body">{{ msg.content }}</text>
|
||||
<view class="risk-card-actions">
|
||||
<view class="risk-action-btn rca-call" @click="callEmergency">一键拨打120</view>
|
||||
<view class="risk-action-btn rca-notify" @click="notifyFamily">通知家属</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 普通气泡 -->
|
||||
<view v-else class="msg-bubble" :class="msg.role === 'user' ? 'bubble-user' : 'bubble-ai'">
|
||||
<text class="msg-text">{{ msg.content }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 建议操作列表(AI回复带操作) -->
|
||||
<view v-if="msg.suggestions && msg.suggestions.length > 0" class="suggestion-list">
|
||||
<view
|
||||
v-for="sug in msg.suggestions"
|
||||
:key="sug"
|
||||
class="suggestion-item"
|
||||
@click="sendMessage(sug)"
|
||||
>
|
||||
{{ sug }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户头像 -->
|
||||
<view v-if="msg.role === 'user'" class="msg-avatar user-avatar">👩⚕️</view>
|
||||
</view>
|
||||
|
||||
<!-- AI加载中 -->
|
||||
<view v-if="isLoading" class="msg-row msg-row-left">
|
||||
<view class="msg-avatar ai-avatar">🤖</view>
|
||||
<view class="loading-bubble">
|
||||
<view class="loading-dot"></view>
|
||||
<view class="loading-dot"></view>
|
||||
<view class="loading-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view id="msg-bottom" style="height: 20rpx;"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 快捷问题(有对话后显示) -->
|
||||
<view v-if="hasConversation && quickReplies.length > 0" class="quick-replies">
|
||||
<scroll-view direction="horizontal" class="qr-scroll">
|
||||
<view
|
||||
v-for="qr in quickReplies"
|
||||
:key="qr"
|
||||
class="qr-chip"
|
||||
@click="sendMessage(qr)"
|
||||
>
|
||||
{{ qr }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 输入区 -->
|
||||
<view class="input-bar">
|
||||
<view class="input-bar-inner">
|
||||
<input
|
||||
class="chat-input"
|
||||
v-model="inputText"
|
||||
placeholder="描述症状或询问护理建议..."
|
||||
:confirm-type="'send'"
|
||||
@confirm="onSend"
|
||||
:maxlength="200"
|
||||
/>
|
||||
<view class="send-btn" :class="inputText.trim() ? 'send-btn-active' : ''" @click="onSend">
|
||||
发送
|
||||
</view>
|
||||
</view>
|
||||
<view class="input-actions">
|
||||
<view class="ia-btn" @click="clearConversation">清空对话</view>
|
||||
<view class="ia-btn" @click="exportRecord">导出记录</view>
|
||||
<view class="ia-btn ia-btn-primary" @click="referToDoctor">转诊医生</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
type MessageType = {
|
||||
role: string
|
||||
content: string
|
||||
riskLevel: string
|
||||
suggestions: string[]
|
||||
}
|
||||
|
||||
// 模拟AI回复知识库(实际接入大模型API)
|
||||
const AI_RESPONSES : UTSJSONObject = {
|
||||
'头晕': {
|
||||
content: '头晕可能与血压波动、低血糖、颈椎问题等有关。\n\n建议:\n1. 立即测量血压\n2. 询问是否有进食\n3. 让老人平躺休息\n\n如出现突发剧烈头晕伴言语不清,请立即就医!',
|
||||
riskLevel: 'medium',
|
||||
suggestions: ['血压偏高怎么办', '低血糖症状有哪些', '需要叫救护车吗']
|
||||
},
|
||||
'胸痛': {
|
||||
content: '胸痛是高度危险的症状!\n\n可能原因:心绞痛、心肌梗死、气胸等。\n\n⚠ 建议立即评估:\n· 是否伴随出汗、恶心\n· 是否放射至左臂或下颌\n· 疼痛是否持续超过5分钟\n\n如有以上情况,属于急危症,请立即拨打120!',
|
||||
riskLevel: 'high',
|
||||
suggestions: ['心肌梗死急救步骤', '如何拨打120']
|
||||
},
|
||||
'发烧': {
|
||||
content: '发热(体温>37.3°C)在老年人中需要特别关注,因为老年人感染反应可能不典型。\n\n建议:\n1. 测量实际体温\n2. 补充水分\n3. 体温>38.5°C 给予退热处理\n4. 持续高热或伴随寒战,建议就医检查血常规',
|
||||
riskLevel: 'medium',
|
||||
suggestions: ['退烧药怎么用', '老年人发烧危险信号']
|
||||
},
|
||||
'default': {
|
||||
content: '感谢您的描述。作为AI助手,我可以提供初步护理建议,但无法替代医生的专业诊断。\n\n请详细描述:\n· 症状持续时间\n· 症状严重程度(1-10分)\n· 是否有用药变化\n\n如情况紧急请立即联系医生或拨打120。',
|
||||
riskLevel: 'low',
|
||||
suggestions: ['当前用药有哪些', '如何联系值班医生', '需要上门服务吗']
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
inputText: '' as string,
|
||||
messages: [] as MessageType[],
|
||||
isLoading: false as boolean,
|
||||
hasConversation: false as boolean,
|
||||
lastMsgId: '' as string,
|
||||
selectedSymptoms: [] as string[],
|
||||
symptomOptions: ['头晕', '胸痛', '发烧', '呼吸困难', '腹痛', '跌倒', '意识模糊', '血压异常', '血糖异常', '情绪异常'] as string[],
|
||||
quickReplies: ['症状加重了', '需要上门服务', '联系家属', '转诊医生', '今日用药'] as string[]
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleSymptom(sym: string) {
|
||||
const idx = this.selectedSymptoms.indexOf(sym)
|
||||
if (idx >= 0) {
|
||||
this.selectedSymptoms.splice(idx, 1)
|
||||
} else {
|
||||
this.selectedSymptoms.push(sym)
|
||||
}
|
||||
},
|
||||
|
||||
startWithSymptoms() {
|
||||
const text = this.selectedSymptoms.join('、')
|
||||
this.selectedSymptoms = []
|
||||
this.hasConversation = true
|
||||
this.sendMessage(`用户主诉:${text}`)
|
||||
},
|
||||
|
||||
onSend() {
|
||||
const text = this.inputText.trim()
|
||||
if (!text) return
|
||||
this.inputText = ''
|
||||
this.hasConversation = true
|
||||
this.sendMessage(text)
|
||||
},
|
||||
|
||||
sendMessage(text: string) {
|
||||
this.messages.push({
|
||||
role: 'user',
|
||||
content: text,
|
||||
riskLevel: 'none',
|
||||
suggestions: []
|
||||
} as MessageType)
|
||||
this.lastMsgId = 'msg-' + (this.messages.length - 1)
|
||||
this.isLoading = true
|
||||
|
||||
// 模拟AI响应延迟(实际需接入大模型API)
|
||||
setTimeout(() => {
|
||||
this.generateAIResponse(text)
|
||||
}, 1200)
|
||||
},
|
||||
|
||||
generateAIResponse(userText: string) {
|
||||
let responseData : UTSJSONObject | null = null
|
||||
|
||||
// 关键词匹配
|
||||
if (userText.includes('头晕')) {
|
||||
responseData = AI_RESPONSES['头晕'] as UTSJSONObject
|
||||
} else if (userText.includes('胸痛') || userText.includes('心脏')) {
|
||||
responseData = AI_RESPONSES['胸痛'] as UTSJSONObject
|
||||
} else if (userText.includes('发烧') || userText.includes('发热')) {
|
||||
responseData = AI_RESPONSES['发烧'] as UTSJSONObject
|
||||
} else {
|
||||
responseData = AI_RESPONSES['default'] as UTSJSONObject
|
||||
}
|
||||
|
||||
const content = String(responseData['content'] ?? '') || ''
|
||||
const riskLevel = String(responseData['riskLevel'] ?? 'low') || 'low'
|
||||
const sugsRaw = responseData.getArray('suggestions')
|
||||
const sugs : string[] = []
|
||||
if (sugsRaw != null) {
|
||||
for (let i = 0; i < sugsRaw.length; i++) {
|
||||
sugs.push(String(sugsRaw[i]) || '')
|
||||
}
|
||||
}
|
||||
|
||||
this.isLoading = false
|
||||
this.messages.push({
|
||||
role: 'ai',
|
||||
content: content,
|
||||
riskLevel: riskLevel,
|
||||
suggestions: sugs
|
||||
} as MessageType)
|
||||
this.lastMsgId = 'msg-bottom'
|
||||
},
|
||||
|
||||
clearConversation() {
|
||||
uni.showModal({
|
||||
title: '清空对话',
|
||||
content: '确定要清空当前对话记录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.messages = []
|
||||
this.hasConversation = false
|
||||
this.lastMsgId = ''
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
exportRecord() {
|
||||
uni.showToast({ title: '导出功能开发中', icon: 'none' })
|
||||
},
|
||||
|
||||
referToDoctor() {
|
||||
uni.showToast({ title: '转诊功能开发中', icon: 'none' })
|
||||
},
|
||||
|
||||
callEmergency() {
|
||||
uni.makePhoneCall({ phoneNumber: '120' })
|
||||
},
|
||||
|
||||
notifyFamily() {
|
||||
uni.showToast({ title: '通知家属功能开发中', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.ai-page {
|
||||
background-color: #f0f2f7;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ===== 导航栏 ===== */
|
||||
.detail-navbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
background-color: #ffffff;
|
||||
border-bottom-width: 1rpx;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: #eeeeee;
|
||||
box-sizing: border-box;
|
||||
padding-top: var(--status-bar-height);
|
||||
height: calc(88rpx + var(--status-bar-height));
|
||||
}
|
||||
|
||||
.detail-navbar-back {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 30rpx;
|
||||
height: 88rpx;
|
||||
width: 120rpx;
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
font-size: 44rpx;
|
||||
color: #333333;
|
||||
line-height: 1;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
.back-text {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.detail-navbar-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
}
|
||||
|
||||
/* ===== 高风险横幅 ===== */
|
||||
.risk-banner {
|
||||
background-color: #E64A19;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 16rpx 24rpx;
|
||||
}
|
||||
|
||||
.risk-banner-icon {
|
||||
font-size: 28rpx;
|
||||
margin-right: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.risk-banner-text {
|
||||
font-size: 22rpx;
|
||||
color: #ffffff;
|
||||
line-height: 1.5;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ===== 症状快选 ===== */
|
||||
.quick-select-area {
|
||||
background-color: #ffffff;
|
||||
padding: 24rpx 30rpx 20rpx 30rpx;
|
||||
border-bottom-width: 8rpx;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: #f0f2f7;
|
||||
}
|
||||
|
||||
.qs-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.qs-tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.qs-tag {
|
||||
font-size: 24rpx;
|
||||
color: #555;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 30rpx;
|
||||
padding: 10rpx 24rpx;
|
||||
margin-right: 14rpx;
|
||||
margin-bottom: 14rpx;
|
||||
border-width: 1rpx;
|
||||
border-style: solid;
|
||||
border-color: #e5e5e5;
|
||||
}
|
||||
|
||||
.qs-tag-selected {
|
||||
background-color: #eef2fe;
|
||||
border-color: rgb(66,121,240);
|
||||
color: rgb(66,121,240);
|
||||
}
|
||||
|
||||
.qs-confirm-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.qs-confirm-btn {
|
||||
background-color: rgb(66,121,240);
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
padding: 0 40rpx;
|
||||
border-radius: 36rpx;
|
||||
}
|
||||
|
||||
/* ===== 聊天滚动区 ===== */
|
||||
.chat-scroll {
|
||||
flex: 1;
|
||||
padding: 20rpx 0;
|
||||
}
|
||||
|
||||
/* ===== 欢迎消息 ===== */
|
||||
.welcome-msg {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding: 20rpx 24rpx;
|
||||
}
|
||||
|
||||
.welcome-avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
background-color: #eef2fe;
|
||||
font-size: 36rpx;
|
||||
line-height: 72rpx;
|
||||
text-align: center;
|
||||
margin-right: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.welcome-bubble {
|
||||
flex: 1;
|
||||
background-color: #ffffff;
|
||||
border-radius: 4rpx 16rpx 16rpx 16rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
display: block;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.welcome-desc {
|
||||
font-size: 24rpx;
|
||||
color: #555;
|
||||
line-height: 1.6;
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.welcome-disclaimer {
|
||||
font-size: 20rpx;
|
||||
color: #E64A19;
|
||||
background-color: #fff5f0;
|
||||
padding: 8rpx 14rpx;
|
||||
border-radius: 4rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ===== 消息行 ===== */
|
||||
.msg-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding: 12rpx 24rpx;
|
||||
}
|
||||
|
||||
.msg-row-left {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.msg-row-right {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.msg-avatar {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 32rpx;
|
||||
font-size: 32rpx;
|
||||
line-height: 64rpx;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ai-avatar {
|
||||
background-color: #eef2fe;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
background-color: #e8f5e9;
|
||||
}
|
||||
|
||||
.msg-bubble-wrap {
|
||||
max-width: 75%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mbw-left {
|
||||
margin-left: 12rpx;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.mbw-right {
|
||||
margin-right: 12rpx;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.msg-bubble {
|
||||
border-radius: 16rpx;
|
||||
padding: 18rpx 22rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
.bubble-ai {
|
||||
background-color: #ffffff;
|
||||
border-top-left-radius: 4rpx;
|
||||
}
|
||||
|
||||
.bubble-user {
|
||||
background-color: rgb(66,121,240);
|
||||
border-top-right-radius: 4rpx;
|
||||
}
|
||||
|
||||
.msg-text {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.bubble-ai .msg-text {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.bubble-user .msg-text {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* ===== 高风险卡片 ===== */
|
||||
.risk-card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
border-top-left-radius: 4rpx;
|
||||
border-top-width: 4rpx;
|
||||
border-top-style: solid;
|
||||
border-top-color: #E64A19;
|
||||
padding: 20rpx 24rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(230,74,25,0.15);
|
||||
}
|
||||
|
||||
.risk-card-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.risk-card-icon {
|
||||
font-size: 30rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.risk-card-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #E64A19;
|
||||
}
|
||||
|
||||
.risk-card-body {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
line-height: 1.7;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.risk-card-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.risk-action-btn {
|
||||
height: 64rpx;
|
||||
line-height: 64rpx;
|
||||
padding: 0 28rpx;
|
||||
border-radius: 32rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.rca-call {
|
||||
background-color: #E64A19;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.rca-notify {
|
||||
background-color: #fff5f0;
|
||||
color: #E64A19;
|
||||
border-width: 1rpx;
|
||||
border-style: solid;
|
||||
border-color: #E64A19;
|
||||
}
|
||||
|
||||
/* ===== 建议操作列表 ===== */
|
||||
.suggestion-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
font-size: 22rpx;
|
||||
color: rgb(66,121,240);
|
||||
border-width: 1rpx;
|
||||
border-style: solid;
|
||||
border-color: rgb(66,121,240);
|
||||
border-radius: 20rpx;
|
||||
padding: 6rpx 18rpx;
|
||||
margin-right: 10rpx;
|
||||
margin-bottom: 10rpx;
|
||||
background-color: #eef2fe;
|
||||
}
|
||||
|
||||
/* ===== 加载中气泡 ===== */
|
||||
.loading-bubble {
|
||||
background-color: #ffffff;
|
||||
border-radius: 4rpx 16rpx 16rpx 16rpx;
|
||||
padding: 18rpx 28rpx;
|
||||
margin-left: 12rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
.loading-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 6rpx;
|
||||
background-color: #bbb;
|
||||
margin: 0 4rpx;
|
||||
}
|
||||
|
||||
/* ===== 快捷回复 ===== */
|
||||
.quick-replies {
|
||||
background-color: #ffffff;
|
||||
border-top-width: 1rpx;
|
||||
border-top-style: solid;
|
||||
border-top-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.qr-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.qr-chip {
|
||||
display: inline-flex;
|
||||
font-size: 24rpx;
|
||||
color: rgb(66,121,240);
|
||||
background-color: #eef2fe;
|
||||
border-radius: 20rpx;
|
||||
padding: 10rpx 24rpx;
|
||||
margin: 12rpx 8rpx;
|
||||
}
|
||||
|
||||
/* ===== 输入区 ===== */
|
||||
.input-bar {
|
||||
background-color: #ffffff;
|
||||
border-top-width: 1rpx;
|
||||
border-top-style: solid;
|
||||
border-top-color: #eeeeee;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.input-bar-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 16rpx 20rpx 12rpx 20rpx;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
border-width: 1rpx;
|
||||
border-style: solid;
|
||||
border-color: #e5e5e5;
|
||||
border-radius: 36rpx;
|
||||
padding: 0 24rpx;
|
||||
font-size: 28rpx;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
padding: 0 28rpx;
|
||||
border-radius: 36rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
margin-left: 16rpx;
|
||||
color: #bbb;
|
||||
background-color: #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.send-btn-active {
|
||||
background-color: rgb(66,121,240);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.input-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
padding: 0 20rpx 12rpx 20rpx;
|
||||
}
|
||||
|
||||
.ia-btn {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
border-width: 1rpx;
|
||||
border-style: solid;
|
||||
border-color: #ddd;
|
||||
border-radius: 20rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
margin-left: 12rpx;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.ia-btn-primary {
|
||||
color: rgb(66,121,240);
|
||||
border-color: rgb(66,121,240);
|
||||
background-color: #eef2fe;
|
||||
}
|
||||
</style>
|
||||
852
pages/mall/merchant/chat-workbench.uvue
Normal file
852
pages/mall/merchant/chat-workbench.uvue
Normal file
@@ -0,0 +1,852 @@
|
||||
<!-- 机构端 - 客服工作台(演示版,全 mock 数据) -->
|
||||
<template>
|
||||
<view class="workbench">
|
||||
|
||||
<!-- ===== 顶部导航 ===== -->
|
||||
<view class="wb-header">
|
||||
<view class="wb-back" @click="goBack">
|
||||
<text class="wb-back-icon">‹</text>
|
||||
</view>
|
||||
<text class="wb-header-title">客服工作台</text>
|
||||
<view class="wb-header-right">
|
||||
<view class="wb-status-wrap">
|
||||
<view class="wb-status-dot"></view>
|
||||
<text class="wb-status-text">在线</text>
|
||||
</view>
|
||||
<view class="wb-pending-wrap" v-if="pendingCount > 0">
|
||||
<text class="wb-pending-text">{{ pendingCount }}条待处理</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 三模块 Tab 切换栏 ===== -->
|
||||
<view class="wb-tabs">
|
||||
<view class="wb-tab-item" :class="{ 'wb-tab-active': activeTab === 0 }" @click="activeTab = 0">
|
||||
<text class="wb-tab-icon">💬</text>
|
||||
<text class="wb-tab-label">会话列表</text>
|
||||
<view v-if="totalUnread > 0 && activeTab !== 0" class="wb-tab-badge">
|
||||
<text class="wb-tab-badge-num">{{ totalUnread }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="wb-tab-item" :class="{ 'wb-tab-active': activeTab === 1 }" @click="activeTab = 1">
|
||||
<text class="wb-tab-icon">🗨</text>
|
||||
<text class="wb-tab-label">聊天记录</text>
|
||||
</view>
|
||||
<view class="wb-tab-item" :class="{ 'wb-tab-active': activeTab === 2 }" @click="activeTab = 2">
|
||||
<text class="wb-tab-icon">📋</text>
|
||||
<text class="wb-tab-label">用户资料</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 当前会话提示条 -->
|
||||
<view v-if="activeTab !== 0" class="session-hint-bar">
|
||||
<text class="session-hint-avatar">{{ currentSession.avatar }}</text>
|
||||
<text class="session-hint-name">{{ currentSession.userName }}</text>
|
||||
<view class="session-hint-status" :class="'shs-' + currentSession.status">
|
||||
<text class="session-hint-status-text">{{ currentSession.status === 'pending' ? '待回复' : currentSession.status === 'active' ? '服务中' : '已结束' }}</text>
|
||||
</view>
|
||||
<view class="session-hint-switch" @click="activeTab = 0">
|
||||
<text class="session-hint-switch-text">切换会话</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 内容区 ===== -->
|
||||
<view class="wb-content">
|
||||
|
||||
<!-- ──────────── 模块一:会话列表 ──────────── -->
|
||||
<view v-if="activeTab === 0" class="module-sessions">
|
||||
<view class="sessions-search-wrap">
|
||||
<input class="sessions-search-input" placeholder="🔍 搜索用户或会话内容..." v-model="searchKey" />
|
||||
</view>
|
||||
<scroll-view scroll-y class="sessions-scroll">
|
||||
<view
|
||||
v-for="sess in filteredSessions"
|
||||
:key="sess.id"
|
||||
class="session-card"
|
||||
:class="{ 'session-card-active': currentSessionId === sess.id }"
|
||||
@click="selectSession(sess.id)"
|
||||
>
|
||||
<view class="sc-avatar-wrap">
|
||||
<text class="sc-avatar-emoji">{{ sess.avatar }}</text>
|
||||
<view v-if="sess.unread > 0" class="sc-unread-badge">
|
||||
<text class="sc-unread-num">{{ sess.unread > 99 ? '99+' : sess.unread }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="sc-info">
|
||||
<view class="sc-row-top">
|
||||
<text class="sc-name">{{ sess.userName }}</text>
|
||||
<text class="sc-time">{{ sess.lastTime }}</text>
|
||||
</view>
|
||||
<text class="sc-consult-type">{{ sess.consultType }}</text>
|
||||
<view class="sc-row-bottom">
|
||||
<text class="sc-preview" :class="{ 'sc-preview-bold': sess.unread > 0 }">{{ sess.lastMsg }}</text>
|
||||
<view class="sc-status-tag" :class="'sc-status-' + sess.status">
|
||||
<text class="sc-status-text">{{ sess.status === 'pending' ? '待回复' : sess.status === 'active' ? '服务中' : '已结束' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="sc-arrow">›</text>
|
||||
</view>
|
||||
<view v-if="filteredSessions.length === 0" class="sessions-empty">
|
||||
<text class="sessions-empty-icon">🔍</text>
|
||||
<text class="sessions-empty-text">暂无匹配会话</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- ──────────── 模块二:聊天记录 ──────────── -->
|
||||
<view v-else-if="activeTab === 1" class="module-chat">
|
||||
<scroll-view scroll-y class="chat-messages-scroll" :scroll-into-view="scrollTarget" :scroll-with-animation="true">
|
||||
<view class="chat-messages-inner">
|
||||
<view
|
||||
v-for="msg in currentMessages"
|
||||
:key="msg.id"
|
||||
:id="'msg_' + msg.id"
|
||||
class="msg-row"
|
||||
:class="{ 'msg-row-system': msg.type === 'system' }"
|
||||
>
|
||||
<!-- 系统消息 -->
|
||||
<view v-if="msg.type === 'system'" class="msg-system">
|
||||
<text class="msg-system-text">{{ msg.content }}</text>
|
||||
</view>
|
||||
<!-- 用户消息(左) -->
|
||||
<view v-else-if="msg.fromUser" class="msg-left">
|
||||
<text class="msg-avatar">{{ currentSession.avatar }}</text>
|
||||
<view class="msg-body">
|
||||
<text class="msg-sender">{{ currentSession.userName }}</text>
|
||||
<view v-if="msg.type === 'text'" class="bubble bubble-user">
|
||||
<text class="bubble-text">{{ msg.content }}</text>
|
||||
<text class="bubble-time">{{ msg.time }}</text>
|
||||
</view>
|
||||
<view v-else-if="msg.type === 'image'" class="bubble bubble-user bubble-image">
|
||||
<text class="bubble-image-icon">🖼</text>
|
||||
<text class="bubble-image-hint">[图片消息]</text>
|
||||
<text class="bubble-time">{{ msg.time }}</text>
|
||||
</view>
|
||||
<view v-else-if="msg.type === 'product'" class="msg-card-wrap">
|
||||
<view class="product-card">
|
||||
<view class="pc-thumb">
|
||||
<text class="pc-thumb-emoji">{{ msg.cardData != null ? (msg.cardData as ProductCardData).emoji : '📦' }}</text>
|
||||
</view>
|
||||
<view class="pc-info">
|
||||
<text class="pc-name">{{ msg.cardData != null ? (msg.cardData as ProductCardData).name : '' }}</text>
|
||||
<text class="pc-price">¥{{ msg.cardData != null ? (msg.cardData as ProductCardData).price : '' }}</text>
|
||||
<text class="pc-tag">{{ msg.cardData != null ? (msg.cardData as ProductCardData).tag : '' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="card-time">{{ msg.time }}</text>
|
||||
</view>
|
||||
<view v-else-if="msg.type === 'order'" class="msg-card-wrap">
|
||||
<view class="order-card">
|
||||
<view class="oc-row1">
|
||||
<text class="oc-label">订单号</text>
|
||||
<text class="oc-no">{{ msg.cardData != null ? (msg.cardData as OrderCardData).orderNo : '' }}</text>
|
||||
<view class="oc-status-tag" :class="'oc-' + (msg.cardData != null ? (msg.cardData as OrderCardData).statusCode : '')">
|
||||
<text class="oc-status-text">{{ msg.cardData != null ? (msg.cardData as OrderCardData).status : '' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="oc-amount">¥{{ msg.cardData != null ? (msg.cardData as OrderCardData).amount : '' }}</text>
|
||||
<text class="oc-items">{{ msg.cardData != null ? (msg.cardData as OrderCardData).itemsDesc : '' }}</text>
|
||||
</view>
|
||||
<text class="card-time">{{ msg.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 商家消息(右) -->
|
||||
<view v-else class="msg-right">
|
||||
<view class="msg-body msg-body-right">
|
||||
<view v-if="msg.type === 'text'" class="bubble bubble-me">
|
||||
<text class="bubble-text">{{ msg.content }}</text>
|
||||
<text class="bubble-time bubble-time-right">{{ msg.time }}</text>
|
||||
</view>
|
||||
<view v-else-if="msg.type === 'image'" class="bubble bubble-me bubble-image">
|
||||
<text class="bubble-image-icon">🖼</text>
|
||||
<text class="bubble-image-hint">[图片消息]</text>
|
||||
<text class="bubble-time bubble-time-right">{{ msg.time }}</text>
|
||||
</view>
|
||||
<view v-else-if="msg.type === 'product'" class="msg-card-wrap msg-card-right">
|
||||
<view class="product-card">
|
||||
<view class="pc-thumb">
|
||||
<text class="pc-thumb-emoji">{{ msg.cardData != null ? (msg.cardData as ProductCardData).emoji : '📦' }}</text>
|
||||
</view>
|
||||
<view class="pc-info">
|
||||
<text class="pc-name">{{ msg.cardData != null ? (msg.cardData as ProductCardData).name : '' }}</text>
|
||||
<text class="pc-price">¥{{ msg.cardData != null ? (msg.cardData as ProductCardData).price : '' }}</text>
|
||||
<text class="pc-tag">{{ msg.cardData != null ? (msg.cardData as ProductCardData).tag : '' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="card-time card-time-right">{{ msg.time }}</text>
|
||||
</view>
|
||||
<view v-else-if="msg.type === 'order'" class="msg-card-wrap msg-card-right">
|
||||
<view class="order-card">
|
||||
<view class="oc-row1">
|
||||
<text class="oc-label">订单号</text>
|
||||
<text class="oc-no">{{ msg.cardData != null ? (msg.cardData as OrderCardData).orderNo : '' }}</text>
|
||||
<view class="oc-status-tag" :class="'oc-' + (msg.cardData != null ? (msg.cardData as OrderCardData).statusCode : '')">
|
||||
<text class="oc-status-text">{{ msg.cardData != null ? (msg.cardData as OrderCardData).status : '' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="oc-amount">¥{{ msg.cardData != null ? (msg.cardData as OrderCardData).amount : '' }}</text>
|
||||
<text class="oc-items">{{ msg.cardData != null ? (msg.cardData as OrderCardData).itemsDesc : '' }}</text>
|
||||
</view>
|
||||
<text class="card-time card-time-right">{{ msg.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="msg-avatar me-avatar">🏥</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部输入区 -->
|
||||
<view class="chat-input-area">
|
||||
<view v-if="showQuickReplies" class="quick-reply-panel">
|
||||
<view class="qr-header">
|
||||
<text class="qr-title">⚡ 快捷回复</text>
|
||||
<text class="qr-close" @click="showQuickReplies = false">✕</text>
|
||||
</view>
|
||||
<scroll-view scroll-y class="qr-scroll">
|
||||
<view v-for="qr in quickReplies" :key="qr" class="qr-item" @click="sendQuickReply(qr)">
|
||||
<text class="qr-text">{{ qr }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="chat-toolbar">
|
||||
<view class="toolbar-btn" @click="insertImageMsg">
|
||||
<text class="toolbar-icon">🖼</text>
|
||||
<text class="toolbar-label">图片</text>
|
||||
</view>
|
||||
<view class="toolbar-btn" @click="insertProductMsg">
|
||||
<text class="toolbar-icon">📦</text>
|
||||
<text class="toolbar-label">商品</text>
|
||||
</view>
|
||||
<view class="toolbar-btn" @click="insertOrderMsg">
|
||||
<text class="toolbar-icon">📋</text>
|
||||
<text class="toolbar-label">订单</text>
|
||||
</view>
|
||||
<view class="toolbar-btn" @click="showQuickReplies = !showQuickReplies" :class="{ 'toolbar-btn-active': showQuickReplies }">
|
||||
<text class="toolbar-icon">⚡</text>
|
||||
<text class="toolbar-label">快捷</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="input-row">
|
||||
<textarea class="chat-textarea" v-model="inputText" placeholder="输入回复内容..." :maxlength="500" />
|
||||
<view class="send-btn" :class="{ 'send-btn-active': inputText.trim() !== '' }" @click="sendTextMsg">
|
||||
<text class="send-btn-text">发送</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ──────────── 模块三:用户资料 ──────────── -->
|
||||
<view v-else-if="activeTab === 2" class="module-info">
|
||||
<view class="info-user-card">
|
||||
<text class="info-big-avatar">{{ currentSession.avatar }}</text>
|
||||
<view class="info-user-main">
|
||||
<text class="info-user-name">{{ currentSession.userName }}</text>
|
||||
<view class="info-tags-row">
|
||||
<view v-for="tag in (currentUserInfo.tags as string[])" :key="tag" class="info-tag">
|
||||
<text class="info-tag-text">{{ tag }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="info-section">
|
||||
<view class="info-section-header">
|
||||
<text class="info-section-icon">👤</text>
|
||||
<text class="info-section-title">基本信息</text>
|
||||
</view>
|
||||
<view class="info-kv">
|
||||
<text class="info-key">手机号</text>
|
||||
<text class="info-val">{{ currentUserInfo.phone }}</text>
|
||||
</view>
|
||||
<view class="info-kv">
|
||||
<text class="info-key">注册时间</text>
|
||||
<text class="info-val">{{ currentUserInfo.registerDate }}</text>
|
||||
</view>
|
||||
<view class="info-kv">
|
||||
<text class="info-key">历史订单</text>
|
||||
<text class="info-val info-val-red">{{ currentUserInfo.totalOrders }} 单</text>
|
||||
</view>
|
||||
<view class="info-kv">
|
||||
<text class="info-key">累计消费</text>
|
||||
<text class="info-val info-val-red">¥{{ currentUserInfo.totalSpent }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="info-section" v-if="currentUserInfo.consultProduct != null">
|
||||
<view class="info-section-header">
|
||||
<text class="info-section-icon">🛒</text>
|
||||
<text class="info-section-title">正在咨询</text>
|
||||
</view>
|
||||
<view class="consult-product">
|
||||
<text class="cp-big-emoji">{{ (currentUserInfo.consultProduct as ConsultProduct).emoji }}</text>
|
||||
<view class="cp-detail">
|
||||
<text class="cp-pname">{{ (currentUserInfo.consultProduct as ConsultProduct).name }}</text>
|
||||
<text class="cp-pprice">¥{{ (currentUserInfo.consultProduct as ConsultProduct).price }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="info-section">
|
||||
<view class="info-section-header">
|
||||
<text class="info-section-icon">👁</text>
|
||||
<text class="info-section-title">最近浏览</text>
|
||||
</view>
|
||||
<view v-for="item in (currentUserInfo.recentViewed as RecentItem[])" :key="item.name" class="recent-row">
|
||||
<text class="recent-emoji">{{ item.emoji }}</text>
|
||||
<view class="recent-detail">
|
||||
<text class="recent-name">{{ item.name }}</text>
|
||||
<text class="recent-price">¥{{ item.price }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="info-section">
|
||||
<view class="info-section-header">
|
||||
<text class="info-section-icon">📋</text>
|
||||
<text class="info-section-title">最近订单</text>
|
||||
</view>
|
||||
<view v-for="order in (currentUserInfo.recentOrders as RecentOrder[])" :key="order.no" class="order-mini-card">
|
||||
<view class="omc-row1">
|
||||
<text class="omc-no">{{ order.no }}</text>
|
||||
<view class="omc-status-tag" :class="'omc-' + order.statusCode">
|
||||
<text class="omc-status-text">{{ order.status }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="omc-row2">
|
||||
<text class="omc-items">{{ order.itemsDesc }}</text>
|
||||
<text class="omc-amount">¥{{ order.amount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="info-actions">
|
||||
<view class="info-action-btn primary" @click="goToChat">
|
||||
<text class="info-action-text">进入聊天</text>
|
||||
</view>
|
||||
<view class="info-action-btn" @click="closeSession">
|
||||
<text class="info-action-text">结束会话</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
type ProductCardData = {
|
||||
emoji: string
|
||||
name: string
|
||||
price: string
|
||||
tag: string
|
||||
}
|
||||
|
||||
type OrderCardData = {
|
||||
orderNo: string
|
||||
status: string
|
||||
statusCode: string
|
||||
amount: string
|
||||
itemsDesc: string
|
||||
}
|
||||
|
||||
type ChatMessage = {
|
||||
id: string
|
||||
fromUser: boolean
|
||||
type: string
|
||||
content: string
|
||||
time: string
|
||||
cardData: ProductCardData | OrderCardData | null
|
||||
}
|
||||
|
||||
type Session = {
|
||||
id: string
|
||||
userName: string
|
||||
avatar: string
|
||||
lastMsg: string
|
||||
lastTime: string
|
||||
unread: number
|
||||
status: string
|
||||
consultType: string
|
||||
}
|
||||
|
||||
type ConsultProduct = {
|
||||
emoji: string
|
||||
name: string
|
||||
price: string
|
||||
}
|
||||
|
||||
type RecentItem = {
|
||||
emoji: string
|
||||
name: string
|
||||
price: string
|
||||
}
|
||||
|
||||
type RecentOrder = {
|
||||
no: string
|
||||
status: string
|
||||
statusCode: string
|
||||
amount: string
|
||||
itemsDesc: string
|
||||
}
|
||||
|
||||
type UserInfo = {
|
||||
phone: string
|
||||
registerDate: string
|
||||
totalOrders: number
|
||||
totalSpent: string
|
||||
tags: string[]
|
||||
consultProduct: ConsultProduct | null
|
||||
recentViewed: RecentItem[]
|
||||
recentOrders: RecentOrder[]
|
||||
}
|
||||
|
||||
const MOCK_SESSIONS: Session[] = [
|
||||
{ id: 'sess_001', userName: '李奶奶(家属:李女士)', avatar: '👩🦳', lastMsg: '请问居家护理服务是每天上门吗?', lastTime: '10:32', unread: 3, status: 'pending', consultType: '居家护理咨询' },
|
||||
{ id: 'sess_002', userName: '王大爷', avatar: '👴', lastMsg: '好的,我明白了,谢谢!', lastTime: '09:15', unread: 0, status: 'active', consultType: '慢病管理套餐' },
|
||||
{ id: 'sess_003', userName: '张先生(代父咨询)', avatar: '🧑', lastMsg: '这个订单什么时候能安排上门?', lastTime: '昨天', unread: 1, status: 'pending', consultType: '订单跟进' }
|
||||
]
|
||||
|
||||
const MOCK_MESSAGES: Record<string, ChatMessage[]> = {
|
||||
'sess_001': [
|
||||
{ id: 'm001', fromUser: false, type: 'system', content: '会话开始 · 居家护理咨询 · 2026-03-24 10:20', time: '', cardData: null },
|
||||
{ id: 'm002', fromUser: true, type: 'text', content: '您好,我是李奶奶的女儿,想咨询一下居家护理服务的安排。', time: '10:20', cardData: null },
|
||||
{ id: 'm003', fromUser: false, type: 'text', content: '您好!感谢您的咨询,我们的居家护理服务可以每日或隔日上门,具体频次根据长者身体状况定制。', time: '10:21', cardData: null },
|
||||
{ id: 'm004', fromUser: true, type: 'text', content: '请问上门的护理员都是专业持证的吗?', time: '10:22', cardData: null },
|
||||
{ id: 'm005', fromUser: false, type: 'text', content: '是的,我们所有上门人员均持有护理员资格证,并经过机构内部培训和背景审查。', time: '10:23', cardData: null },
|
||||
{ id: 'm006', fromUser: false, type: 'product', content: '', time: '10:25', cardData: { emoji: '🩺', name: '居家护理基础套餐(月服务)', price: '2800', tag: '每日上门 · 含基础生活护理' } as ProductCardData },
|
||||
{ id: 'm007', fromUser: true, type: 'image', content: '', time: '10:28', cardData: null },
|
||||
{ id: 'm008', fromUser: true, type: 'text', content: '请问居家护理服务是每天上门吗?', time: '10:32', cardData: null }
|
||||
],
|
||||
'sess_002': [
|
||||
{ id: 'm101', fromUser: false, type: 'system', content: '会话开始 · 慢病管理套餐 · 2026-03-24 09:00', time: '', cardData: null },
|
||||
{ id: 'm102', fromUser: true, type: 'text', content: '我父亲有高血压和糖尿病,想了解一下慢病管理套餐包含哪些内容?', time: '09:00', cardData: null },
|
||||
{ id: 'm103', fromUser: false, type: 'text', content: '您好!我们慢病管理套餐包含:每月4次上门随访、血压血糖监测记录、用药指导和饮食建议,以及每季度体检报告解读。', time: '09:01', cardData: null },
|
||||
{ id: 'm104', fromUser: false, type: 'product', content: '', time: '09:02', cardData: { emoji: '💊', name: '慢病综合管理套餐(季度)', price: '4200', tag: '含随访+监测+体检解读' } as ProductCardData },
|
||||
{ id: 'm105', fromUser: true, type: 'text', content: '价格合理,这个套餐可以报销医保吗?', time: '09:05', cardData: null },
|
||||
{ id: 'm106', fromUser: false, type: 'text', content: '目前部分项目可以使用长护险,具体需要根据您所在区域的长护险政策,我们可以协助您申请。', time: '09:06', cardData: null },
|
||||
{ id: 'm107', fromUser: true, type: 'order', content: '', time: '09:10', cardData: { orderNo: 'SV20240315002', status: '服务中', statusCode: 'active', amount: '2800.00', itemsDesc: '居家护理基础套餐 × 1月' } as OrderCardData },
|
||||
{ id: 'm108', fromUser: true, type: 'text', content: '好的,我明白了,谢谢!', time: '09:15', cardData: null }
|
||||
],
|
||||
'sess_003': [
|
||||
{ id: 'm201', fromUser: false, type: 'system', content: '会话开始 · 订单跟进 · 2026-03-23 15:00', time: '', cardData: null },
|
||||
{ id: 'm202', fromUser: true, type: 'text', content: '你好,我父亲上周下了一个陪诊服务的订单,现在是什么状态了?', time: '昨天 15:00', cardData: null },
|
||||
{ id: 'm203', fromUser: false, type: 'text', content: '您好!请问您的订单号是多少?我来为您查询。', time: '昨天 15:01', cardData: null },
|
||||
{ id: 'm204', fromUser: true, type: 'order', content: '', time: '昨天 15:02', cardData: { orderNo: 'SV20240320007', status: '待上门', statusCode: 'pending', amount: '380.00', itemsDesc: '专业陪诊服务(半日) × 1次' } as OrderCardData },
|
||||
{ id: 'm205', fromUser: false, type: 'text', content: '已为您查到,该订单目前处于"待上门"状态,陪诊师将于明天上午9:00~10:00联系您父亲确认时间。', time: '昨天 15:05', cardData: null },
|
||||
{ id: 'm206', fromUser: false, type: 'text', content: '您也可以在小程序"服务订单"中实时查看服务进度,如需备注特殊要求请告知。', time: '昨天 15:06', cardData: null },
|
||||
{ id: 'm207', fromUser: true, type: 'text', content: '这个订单什么时候能安排上门?', time: '昨天 15:08', cardData: null }
|
||||
]
|
||||
}
|
||||
|
||||
const MOCK_USER_INFO: Record<string, UserInfo> = {
|
||||
'sess_001': {
|
||||
phone: '186****3421', registerDate: '2025-11-10', totalOrders: 4, totalSpent: '8,400',
|
||||
tags: ['长者家属', '居家护理', '高意向'],
|
||||
consultProduct: { emoji: '🩺', name: '居家护理基础套餐', price: '2800' } as ConsultProduct,
|
||||
recentViewed: [
|
||||
{ emoji: '🩺', name: '居家护理基础套餐', price: '2800' } as RecentItem,
|
||||
{ emoji: '🛁', name: '洗浴护理服务', price: '188' } as RecentItem,
|
||||
{ emoji: '🦽', name: '轮椅租赁月服务', price: '360' } as RecentItem
|
||||
],
|
||||
recentOrders: [
|
||||
{ no: 'SV20240210003', status: '已完成', statusCode: 'done', amount: '2800', itemsDesc: '居家护理基础套餐' } as RecentOrder,
|
||||
{ no: 'SV20240115001', status: '已完成', statusCode: 'done', amount: '188', itemsDesc: '洗浴护理服务 × 1次' } as RecentOrder
|
||||
]
|
||||
},
|
||||
'sess_002': {
|
||||
phone: '139****8802', registerDate: '2025-08-22', totalOrders: 7, totalSpent: '15,600',
|
||||
tags: ['高消费', '慢病管理', '复购客户'],
|
||||
consultProduct: { emoji: '💊', name: '慢病综合管理套餐', price: '4200' } as ConsultProduct,
|
||||
recentViewed: [
|
||||
{ emoji: '💊', name: '慢病综合管理套餐', price: '4200' } as RecentItem,
|
||||
{ emoji: '🩸', name: '上门抽血检测服务', price: '98' } as RecentItem,
|
||||
{ emoji: '❤️', name: '心电图上门检测', price: '128' } as RecentItem
|
||||
],
|
||||
recentOrders: [
|
||||
{ no: 'SV20240315002', status: '服务中', statusCode: 'active', amount: '2800', itemsDesc: '居家护理基础套餐' } as RecentOrder,
|
||||
{ no: 'SV20240201008', status: '已完成', statusCode: 'done', amount: '4200', itemsDesc: '慢病综合管理套餐' } as RecentOrder
|
||||
]
|
||||
},
|
||||
'sess_003': {
|
||||
phone: '158****7760', registerDate: '2026-01-05', totalOrders: 1, totalSpent: '380',
|
||||
tags: ['新用户', '陪诊咨询'],
|
||||
consultProduct: { emoji: '🏥', name: '专业陪诊服务(半日)', price: '380' } as ConsultProduct,
|
||||
recentViewed: [
|
||||
{ emoji: '🏥', name: '专业陪诊服务(半日)', price: '380' } as RecentItem,
|
||||
{ emoji: '🚑', name: '专业陪诊服务(全日)', price: '680' } as RecentItem
|
||||
],
|
||||
recentOrders: [
|
||||
{ no: 'SV20240320007', status: '待上门', statusCode: 'pending', amount: '380', itemsDesc: '专业陪诊服务(半日)× 1次' } as RecentOrder
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const QUICK_REPLIES: string[] = [
|
||||
'您好,感谢您的咨询!我是在线客服,请问有什么可以帮您?',
|
||||
'好的,我马上为您查询,请稍候。',
|
||||
'我们的服务人员均持有专业资格证,请放心。',
|
||||
'您的订单已安排,服务人员将提前联系您确认时间。',
|
||||
'如需进一步了解,欢迎预约免费上门评估服务。',
|
||||
'感谢您的信任,祝长辈身体健康!'
|
||||
]
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
sessions: MOCK_SESSIONS as Session[],
|
||||
messageMap: MOCK_MESSAGES as Record<string, ChatMessage[]>,
|
||||
userInfoMap: MOCK_USER_INFO as Record<string, UserInfo>,
|
||||
quickReplies: QUICK_REPLIES as string[],
|
||||
activeTab: 0,
|
||||
currentSessionId: 'sess_001',
|
||||
inputText: '',
|
||||
scrollTarget: '',
|
||||
showQuickReplies: false,
|
||||
searchKey: ''
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
filteredSessions(): Session[] {
|
||||
if (this.searchKey.trim() === '') return this.sessions
|
||||
const key = this.searchKey.toLowerCase()
|
||||
return this.sessions.filter((s: Session) =>
|
||||
s.userName.toLowerCase().includes(key) || s.lastMsg.toLowerCase().includes(key)
|
||||
)
|
||||
},
|
||||
currentSession(): Session {
|
||||
const found = this.sessions.find((s: Session) => s.id === this.currentSessionId)
|
||||
return found ?? this.sessions[0]
|
||||
},
|
||||
currentMessages(): ChatMessage[] {
|
||||
return this.messageMap[this.currentSessionId] ?? []
|
||||
},
|
||||
currentUserInfo(): UserInfo {
|
||||
return this.userInfoMap[this.currentSessionId] ?? this.userInfoMap['sess_001']
|
||||
},
|
||||
pendingCount(): number {
|
||||
let count = 0
|
||||
for (let i = 0; i < this.sessions.length; i++) {
|
||||
if (this.sessions[i].status === 'pending') count++
|
||||
}
|
||||
return count
|
||||
},
|
||||
totalUnread(): number {
|
||||
let total = 0
|
||||
for (let i = 0; i < this.sessions.length; i++) {
|
||||
total += this.sessions[i].unread
|
||||
}
|
||||
return total
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.scrollToBottom()
|
||||
},
|
||||
|
||||
methods: {
|
||||
selectSession(id: string) {
|
||||
this.currentSessionId = id
|
||||
this.showQuickReplies = false
|
||||
const idx = this.sessions.findIndex((s: Session) => s.id === id)
|
||||
if (idx >= 0) {
|
||||
this.sessions[idx].unread = 0
|
||||
if (this.sessions[idx].status === 'pending') this.sessions[idx].status = 'active'
|
||||
}
|
||||
this.activeTab = 1
|
||||
setTimeout(() => { this.scrollToBottom() }, 150)
|
||||
},
|
||||
|
||||
goToChat() {
|
||||
this.activeTab = 1
|
||||
setTimeout(() => { this.scrollToBottom() }, 150)
|
||||
},
|
||||
|
||||
scrollToBottom() {
|
||||
const msgs = this.currentMessages
|
||||
if (msgs.length === 0) return
|
||||
const last = msgs[msgs.length - 1]
|
||||
this.scrollTarget = ''
|
||||
setTimeout(() => { this.scrollTarget = 'msg_' + last.id }, 100)
|
||||
},
|
||||
|
||||
nowTime(): string {
|
||||
const d = new Date()
|
||||
return d.getHours().toString().padStart(2, '0') + ':' + d.getMinutes().toString().padStart(2, '0')
|
||||
},
|
||||
|
||||
pushMessage(msg: ChatMessage) {
|
||||
if (this.messageMap[this.currentSessionId] == null) {
|
||||
this.messageMap[this.currentSessionId] = []
|
||||
}
|
||||
this.messageMap[this.currentSessionId].push(msg)
|
||||
const idx = this.sessions.findIndex((s: Session) => s.id === this.currentSessionId)
|
||||
if (idx >= 0) {
|
||||
this.sessions[idx].lastMsg = msg.type === 'text' ? msg.content :
|
||||
msg.type === 'image' ? '[图片]' :
|
||||
msg.type === 'product' ? '[商品推荐]' : '[订单信息]'
|
||||
this.sessions[idx].lastTime = this.nowTime()
|
||||
}
|
||||
setTimeout(() => { this.scrollToBottom() }, 100)
|
||||
},
|
||||
|
||||
sendTextMsg() {
|
||||
const text = this.inputText.trim()
|
||||
if (text === '') return
|
||||
this.inputText = ''
|
||||
this.showQuickReplies = false
|
||||
this.pushMessage({ id: 'local_' + Date.now().toString(), fromUser: false, type: 'text', content: text, time: this.nowTime(), cardData: null } as ChatMessage)
|
||||
},
|
||||
|
||||
sendQuickReply(text: string) {
|
||||
this.showQuickReplies = false
|
||||
this.pushMessage({ id: 'qr_' + Date.now().toString(), fromUser: false, type: 'text', content: text, time: this.nowTime(), cardData: null } as ChatMessage)
|
||||
},
|
||||
|
||||
insertImageMsg() {
|
||||
this.pushMessage({ id: 'img_' + Date.now().toString(), fromUser: false, type: 'image', content: '', time: this.nowTime(), cardData: null } as ChatMessage)
|
||||
},
|
||||
|
||||
insertProductMsg() {
|
||||
this.pushMessage({ id: 'prod_' + Date.now().toString(), fromUser: false, type: 'product', content: '', time: this.nowTime(), cardData: { emoji: '🩺', name: '居家护理定制套餐', price: '3200', tag: '可定制频次,支持长护险' } as ProductCardData } as ChatMessage)
|
||||
},
|
||||
|
||||
insertOrderMsg() {
|
||||
this.pushMessage({ id: 'ord_' + Date.now().toString(), fromUser: false, type: 'order', content: '', time: this.nowTime(), cardData: { orderNo: 'SV' + Date.now().toString().slice(-8), status: '待接单', statusCode: 'pending', amount: '2800.00', itemsDesc: '居家护理基础套餐 × 1月' } as OrderCardData } as ChatMessage)
|
||||
},
|
||||
|
||||
closeSession() {
|
||||
uni.showModal({
|
||||
title: '结束会话',
|
||||
content: '确认结束与该用户的当前咨询会话吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const idx = this.sessions.findIndex((s: Session) => s.id === this.currentSessionId)
|
||||
if (idx >= 0) this.sessions[idx].status = 'closed'
|
||||
this.pushMessage({ id: 'sys_' + Date.now().toString(), fromUser: false, type: 'system', content: '会话已由客服结束 · ' + this.nowTime(), time: '', cardData: null } as ChatMessage)
|
||||
this.activeTab = 0
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.workbench { display: flex; flex-direction: column; height: 100vh; background-color: #f4f6f9; }
|
||||
|
||||
/* 顶部导航 */
|
||||
.wb-header { height: 88rpx; background-color: #1a6fd4; display: flex; flex-direction: row; align-items: center; padding: 0 24rpx; flex-shrink: 0; }
|
||||
.wb-back { width: 60rpx; height: 60rpx; display: flex; align-items: center; justify-content: center; }
|
||||
.wb-back-icon { font-size: 52rpx; color: #fff; font-weight: bold; line-height: 1; }
|
||||
.wb-header-title { font-size: 34rpx; font-weight: bold; color: #fff; flex: 1; margin-left: 8rpx; }
|
||||
.wb-header-right { display: flex; flex-direction: row; align-items: center; gap: 16rpx; }
|
||||
.wb-status-wrap { display: flex; flex-direction: row; align-items: center; background-color: rgba(255,255,255,0.15); border-radius: 20rpx; padding: 6rpx 16rpx; }
|
||||
.wb-status-dot { width: 14rpx; height: 14rpx; border-radius: 7rpx; background-color: #52c41a; margin-right: 8rpx; }
|
||||
.wb-status-text { font-size: 22rpx; color: #fff; }
|
||||
.wb-pending-wrap { background-color: #ff4d4f; border-radius: 20rpx; padding: 6rpx 16rpx; }
|
||||
.wb-pending-text { font-size: 22rpx; color: #fff; font-weight: bold; }
|
||||
|
||||
/* Tab 栏 */
|
||||
.wb-tabs { display: flex; flex-direction: row; background-color: #fff; border-bottom: 2rpx solid #e8e8e8; flex-shrink: 0; }
|
||||
.wb-tab-item { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 18rpx 0 14rpx; position: relative; }
|
||||
.wb-tab-item:active { background-color: #f5f7fa; }
|
||||
.wb-tab-icon { font-size: 36rpx; }
|
||||
.wb-tab-label { font-size: 24rpx; color: #888; margin-top: 4rpx; }
|
||||
.wb-tab-active .wb-tab-label { color: #1a6fd4; font-weight: bold; }
|
||||
.wb-tab-active { border-bottom: 4rpx solid #1a6fd4; }
|
||||
.wb-tab-badge { position: absolute; top: 10rpx; right: 16rpx; min-width: 28rpx; height: 28rpx; background-color: #ff4d4f; border-radius: 14rpx; display: flex; align-items: center; justify-content: center; padding: 0 6rpx; }
|
||||
.wb-tab-badge-num { font-size: 18rpx; color: #fff; font-weight: bold; }
|
||||
|
||||
/* 当前会话提示条 */
|
||||
.session-hint-bar { background-color: #f0f7ff; border-bottom: 1rpx solid #d0e8ff; padding: 12rpx 24rpx; display: flex; flex-direction: row; align-items: center; flex-shrink: 0; }
|
||||
.session-hint-avatar { font-size: 32rpx; margin-right: 10rpx; }
|
||||
.session-hint-name { font-size: 24rpx; color: #1a6fd4; font-weight: bold; flex: 1; }
|
||||
.session-hint-status { border-radius: 8rpx; padding: 2rpx 12rpx; margin-right: 16rpx; }
|
||||
.shs-pending { background-color: #fff3e0; }
|
||||
.shs-pending .session-hint-status-text { font-size: 20rpx; color: #ff8c00; }
|
||||
.shs-active { background-color: #e8f5e9; }
|
||||
.shs-active .session-hint-status-text { font-size: 20rpx; color: #388e3c; }
|
||||
.shs-closed { background-color: #f5f5f5; }
|
||||
.shs-closed .session-hint-status-text { font-size: 20rpx; color: #999; }
|
||||
.session-hint-switch { background-color: #1a6fd4; border-radius: 20rpx; padding: 6rpx 20rpx; }
|
||||
.session-hint-switch-text { font-size: 20rpx; color: #fff; }
|
||||
|
||||
/* 内容区 */
|
||||
.wb-content { flex: 1; overflow: hidden; display: flex; flex-direction: column; }
|
||||
|
||||
/* ====== 模块一:会话列表 ====== */
|
||||
.module-sessions { flex: 1; display: flex; flex-direction: column; background-color: #fff; overflow: hidden; }
|
||||
.sessions-search-wrap { padding: 20rpx 24rpx; background-color: #f8f9fc; border-bottom: 1rpx solid #eee; flex-shrink: 0; }
|
||||
.sessions-search-input { background-color: #fff; border: 1rpx solid #e0e0e0; border-radius: 40rpx; padding: 14rpx 28rpx; font-size: 28rpx; width: 100%; box-sizing: border-box; }
|
||||
.sessions-scroll { flex: 1; height: 0; }
|
||||
|
||||
.session-card { display: flex; flex-direction: row; align-items: center; padding: 28rpx 24rpx; border-bottom: 1rpx solid #f0f0f0; }
|
||||
.session-card:active { background-color: #f0f7ff; }
|
||||
.session-card-active { background-color: #e8f4ff; border-left: 6rpx solid #1a6fd4; }
|
||||
.sc-avatar-wrap { position: relative; margin-right: 20rpx; flex-shrink: 0; }
|
||||
.sc-avatar-emoji { font-size: 80rpx; }
|
||||
.sc-unread-badge { position: absolute; top: -6rpx; right: -6rpx; min-width: 32rpx; height: 32rpx; background-color: #ff4d4f; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; padding: 0 8rpx; border: 2rpx solid #fff; }
|
||||
.sc-unread-num { font-size: 18rpx; color: #fff; font-weight: bold; }
|
||||
.sc-info { flex: 1; overflow: hidden; }
|
||||
.sc-row-top { display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 4rpx; }
|
||||
.sc-name { font-size: 30rpx; font-weight: bold; color: #1a1a1a; flex: 1; }
|
||||
.sc-time { font-size: 22rpx; color: #aaa; flex-shrink: 0; margin-left: 16rpx; }
|
||||
.sc-consult-type { font-size: 22rpx; color: #1a6fd4; display: block; margin-bottom: 8rpx; }
|
||||
.sc-row-bottom { display: flex; flex-direction: row; align-items: center; }
|
||||
.sc-preview { font-size: 26rpx; color: #999; flex: 1; }
|
||||
.sc-preview-bold { color: #333; font-weight: bold; }
|
||||
.sc-status-tag { border-radius: 8rpx; padding: 4rpx 14rpx; flex-shrink: 0; margin-left: 12rpx; }
|
||||
.sc-status-pending { background-color: #fff3e0; }
|
||||
.sc-status-pending .sc-status-text { color: #ff8c00; font-size: 22rpx; }
|
||||
.sc-status-active { background-color: #e8f5e9; }
|
||||
.sc-status-active .sc-status-text { color: #388e3c; font-size: 22rpx; }
|
||||
.sc-status-closed { background-color: #f5f5f5; }
|
||||
.sc-status-closed .sc-status-text { color: #999; font-size: 22rpx; }
|
||||
.sc-arrow { font-size: 36rpx; color: #ccc; margin-left: 12rpx; flex-shrink: 0; }
|
||||
.sessions-empty { padding: 100rpx 0; display: flex; flex-direction: column; align-items: center; }
|
||||
.sessions-empty-icon { font-size: 80rpx; margin-bottom: 20rpx; }
|
||||
.sessions-empty-text { font-size: 28rpx; color: #bbb; }
|
||||
|
||||
/* ====== 模块二:聊天记录 ====== */
|
||||
.module-chat { flex: 1; display: flex; flex-direction: column; background-color: #f4f6f9; overflow: hidden; }
|
||||
.chat-messages-scroll { flex: 1; height: 0; }
|
||||
.chat-messages-inner { padding: 24rpx 20rpx 20rpx; display: flex; flex-direction: column; }
|
||||
|
||||
.msg-row { margin-bottom: 28rpx; }
|
||||
.msg-row-system { display: flex; justify-content: center; }
|
||||
.msg-system { background-color: rgba(0,0,0,0.05); border-radius: 10rpx; padding: 10rpx 24rpx; }
|
||||
.msg-system-text { font-size: 22rpx; color: #999; }
|
||||
|
||||
.msg-left { display: flex; flex-direction: row; align-items: flex-start; }
|
||||
.msg-right { display: flex; flex-direction: row; align-items: flex-start; justify-content: flex-end; }
|
||||
.msg-avatar { font-size: 64rpx; flex-shrink: 0; }
|
||||
.me-avatar { margin-left: 14rpx; }
|
||||
.msg-body { margin-left: 14rpx; max-width: 80%; }
|
||||
.msg-body-right { margin-left: 0; margin-right: 14rpx; display: flex; flex-direction: column; align-items: flex-end; }
|
||||
.msg-sender { font-size: 22rpx; color: #999; margin-bottom: 8rpx; display: block; }
|
||||
|
||||
.bubble { border-radius: 18rpx; padding: 20rpx 24rpx; display: flex; flex-direction: column; }
|
||||
.bubble-user { background-color: #fff; border-top-left-radius: 4rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06); }
|
||||
.bubble-me { background-color: #d4e8ff; border-top-right-radius: 4rpx; }
|
||||
.bubble-text { font-size: 30rpx; color: #1a1a1a; line-height: 1.6; }
|
||||
.bubble-time { font-size: 20rpx; color: #bbb; margin-top: 8rpx; }
|
||||
.bubble-time-right { text-align: right; }
|
||||
.bubble-image { flex-direction: row; align-items: center; gap: 12rpx; }
|
||||
.bubble-image-icon { font-size: 44rpx; }
|
||||
.bubble-image-hint { font-size: 26rpx; color: #666; }
|
||||
|
||||
.msg-card-wrap { display: flex; flex-direction: column; }
|
||||
.msg-card-right { align-items: flex-end; }
|
||||
.card-time { font-size: 20rpx; color: #bbb; margin-top: 8rpx; padding: 0 4rpx; }
|
||||
.card-time-right { text-align: right; }
|
||||
|
||||
.product-card { background-color: #fff; border-radius: 16rpx; display: flex; flex-direction: row; align-items: center; padding: 20rpx; min-width: 500rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06); border: 1rpx solid #f0f0f0; }
|
||||
.pc-thumb { width: 96rpx; height: 96rpx; background-color: #f0f7ff; border-radius: 14rpx; display: flex; align-items: center; justify-content: center; margin-right: 20rpx; flex-shrink: 0; }
|
||||
.pc-thumb-emoji { font-size: 56rpx; }
|
||||
.pc-info { flex: 1; }
|
||||
.pc-name { font-size: 28rpx; color: #1a1a1a; font-weight: bold; display: block; margin-bottom: 6rpx; }
|
||||
.pc-price { font-size: 32rpx; color: #e84040; font-weight: bold; display: block; margin-bottom: 4rpx; }
|
||||
.pc-tag { font-size: 22rpx; color: #888; display: block; }
|
||||
|
||||
.order-card { background-color: #fff; border-radius: 16rpx; padding: 20rpx 24rpx; min-width: 500rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06); border: 1rpx solid #f0f0f0; }
|
||||
.oc-row1 { display: flex; flex-direction: row; align-items: center; margin-bottom: 10rpx; }
|
||||
.oc-label { font-size: 20rpx; color: #999; background-color: #f5f5f5; border-radius: 4rpx; padding: 2rpx 10rpx; margin-right: 12rpx; flex-shrink: 0; }
|
||||
.oc-no { font-size: 22rpx; color: #555; flex: 1; }
|
||||
.oc-status-tag { border-radius: 6rpx; padding: 4rpx 14rpx; flex-shrink: 0; }
|
||||
.oc-pending { background-color: #fff3e0; }
|
||||
.oc-pending .oc-status-text { color: #ff8c00; font-size: 22rpx; }
|
||||
.oc-active { background-color: #e8f5e9; }
|
||||
.oc-active .oc-status-text { color: #388e3c; font-size: 22rpx; }
|
||||
.oc-done { background-color: #f5f5f5; }
|
||||
.oc-done .oc-status-text { color: #888; font-size: 22rpx; }
|
||||
.oc-amount { font-size: 36rpx; color: #e84040; font-weight: bold; display: block; margin-bottom: 4rpx; }
|
||||
.oc-items { font-size: 24rpx; color: #888; display: block; }
|
||||
|
||||
.chat-input-area { background-color: #fff; border-top: 1rpx solid #e8e8e8; flex-shrink: 0; }
|
||||
.quick-reply-panel { background-color: #f8f9fc; border-bottom: 1rpx solid #eee; padding: 16rpx 20rpx; }
|
||||
.qr-header { display: flex; flex-direction: row; align-items: center; justify-content: space-between; margin-bottom: 14rpx; }
|
||||
.qr-title { font-size: 24rpx; color: #555; font-weight: bold; }
|
||||
.qr-close { font-size: 28rpx; color: #aaa; padding: 8rpx; }
|
||||
.qr-scroll { max-height: 240rpx; }
|
||||
.qr-item { background-color: #fff; border-radius: 10rpx; padding: 14rpx 20rpx; margin-bottom: 10rpx; border: 1rpx solid #e8e8e8; }
|
||||
.qr-item:active { background-color: #e8f4ff; }
|
||||
.qr-text { font-size: 26rpx; color: #333; line-height: 1.5; }
|
||||
|
||||
.chat-toolbar { display: flex; flex-direction: row; padding: 16rpx 20rpx 8rpx; gap: 20rpx; border-bottom: 1rpx solid #f0f0f0; }
|
||||
.toolbar-btn { display: flex; flex-direction: column; align-items: center; padding: 10rpx 24rpx; border-radius: 12rpx; background-color: #f5f7fa; }
|
||||
.toolbar-btn:active { background-color: #e0ecff; }
|
||||
.toolbar-btn-active { background-color: #e0ecff; }
|
||||
.toolbar-icon { font-size: 38rpx; }
|
||||
.toolbar-label { font-size: 20rpx; color: #666; margin-top: 4rpx; }
|
||||
|
||||
.input-row { display: flex; flex-direction: row; align-items: flex-end; padding: 16rpx 20rpx 20rpx; }
|
||||
.chat-textarea { flex: 1; background-color: #f5f7fa; border-radius: 12rpx; padding: 14rpx 18rpx; font-size: 28rpx; color: #1a1a1a; min-height: 80rpx; max-height: 160rpx; margin-right: 16rpx; border: 1rpx solid #e0e0e0; box-sizing: border-box; }
|
||||
.send-btn { padding: 20rpx 36rpx; border-radius: 12rpx; background-color: #d0d0d0; flex-shrink: 0; }
|
||||
.send-btn-active { background-color: #1a6fd4; }
|
||||
.send-btn-text { font-size: 28rpx; color: #fff; font-weight: bold; }
|
||||
|
||||
/* ====== 模块三:用户资料 ====== */
|
||||
.module-info { flex: 1; height: 0; overflow-y: auto; background-color: #f4f6f9; padding-bottom: 60rpx; }
|
||||
|
||||
.info-user-card { background-color: #1a6fd4; padding: 40rpx 30rpx 36rpx; display: flex; flex-direction: row; align-items: center; }
|
||||
.info-big-avatar { font-size: 100rpx; margin-right: 24rpx; flex-shrink: 0; }
|
||||
.info-user-main { flex: 1; }
|
||||
.info-user-name { font-size: 36rpx; font-weight: bold; color: #fff; display: block; margin-bottom: 12rpx; }
|
||||
.info-tags-row { display: flex; flex-direction: row; flex-wrap: wrap; gap: 12rpx; }
|
||||
.info-tag { background-color: rgba(255,255,255,0.2); border-radius: 20rpx; padding: 4rpx 18rpx; border: 1rpx solid rgba(255,255,255,0.4); }
|
||||
.info-tag-text { font-size: 22rpx; color: #fff; }
|
||||
|
||||
.info-section { background-color: #fff; margin-top: 16rpx; padding: 24rpx 28rpx; }
|
||||
.info-section-header { display: flex; flex-direction: row; align-items: center; margin-bottom: 20rpx; }
|
||||
.info-section-icon { font-size: 32rpx; margin-right: 10rpx; }
|
||||
.info-section-title { font-size: 28rpx; font-weight: bold; color: #333; }
|
||||
|
||||
.info-kv { display: flex; flex-direction: row; align-items: center; padding: 14rpx 0; border-bottom: 1rpx solid #f5f5f5; }
|
||||
.info-kv:last-child { border-bottom: none; }
|
||||
.info-key { font-size: 26rpx; color: #999; width: 160rpx; flex-shrink: 0; }
|
||||
.info-val { font-size: 26rpx; color: #333; }
|
||||
.info-val-red { color: #e84040; font-weight: bold; }
|
||||
|
||||
.consult-product { display: flex; flex-direction: row; align-items: center; background-color: #f0f7ff; border-radius: 14rpx; padding: 20rpx; }
|
||||
.cp-big-emoji { font-size: 72rpx; margin-right: 20rpx; flex-shrink: 0; }
|
||||
.cp-detail { flex: 1; }
|
||||
.cp-pname { font-size: 28rpx; color: #1a1a1a; font-weight: bold; display: block; margin-bottom: 8rpx; }
|
||||
.cp-pprice { font-size: 36rpx; color: #e84040; font-weight: bold; display: block; }
|
||||
|
||||
.recent-row { display: flex; flex-direction: row; align-items: center; padding: 16rpx 0; border-bottom: 1rpx solid #f5f5f5; }
|
||||
.recent-row:last-child { border-bottom: none; }
|
||||
.recent-emoji { font-size: 52rpx; margin-right: 18rpx; flex-shrink: 0; }
|
||||
.recent-detail { flex: 1; }
|
||||
.recent-name { font-size: 26rpx; color: #333; display: block; margin-bottom: 4rpx; }
|
||||
.recent-price { font-size: 24rpx; color: #e84040; display: block; }
|
||||
|
||||
.order-mini-card { background-color: #fafafa; border-radius: 12rpx; padding: 18rpx 20rpx; margin-bottom: 16rpx; border: 1rpx solid #f0f0f0; }
|
||||
.order-mini-card:last-child { margin-bottom: 0; }
|
||||
.omc-row1 { display: flex; flex-direction: row; align-items: center; margin-bottom: 10rpx; }
|
||||
.omc-no { font-size: 22rpx; color: #888; flex: 1; }
|
||||
.omc-status-tag { border-radius: 8rpx; padding: 4rpx 14rpx; }
|
||||
.omc-done { background-color: #f5f5f5; }
|
||||
.omc-done .omc-status-text { color: #888; font-size: 22rpx; }
|
||||
.omc-active { background-color: #e8f5e9; }
|
||||
.omc-active .omc-status-text { color: #388e3c; font-size: 22rpx; }
|
||||
.omc-pending { background-color: #fff3e0; }
|
||||
.omc-pending .omc-status-text { color: #ff8c00; font-size: 22rpx; }
|
||||
.omc-row2 { display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
|
||||
.omc-items { font-size: 24rpx; color: #666; flex: 1; }
|
||||
.omc-amount { font-size: 28rpx; color: #e84040; font-weight: bold; flex-shrink: 0; margin-left: 16rpx; }
|
||||
|
||||
.info-actions { margin: 24rpx 28rpx 0; display: flex; flex-direction: row; gap: 20rpx; }
|
||||
.info-action-btn { flex: 1; padding: 24rpx 0; border-radius: 14rpx; background-color: #f0f0f0; display: flex; align-items: center; justify-content: center; }
|
||||
.info-action-btn.primary { background-color: #1a6fd4; }
|
||||
.info-action-text { font-size: 28rpx; color: #555; font-weight: bold; }
|
||||
.info-action-btn.primary .info-action-text { color: #fff; }
|
||||
</style>
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- 商家端 - 聊天页面 -->
|
||||
<!-- 机构端 - 咊询聊天页面 -->
|
||||
<template>
|
||||
<view class="chat-page">
|
||||
<!-- 聊天头部 -->
|
||||
@@ -9,7 +9,7 @@
|
||||
<view class="header-info">
|
||||
<view class="header-info-text-wrapper">
|
||||
<text class="chat-title">{{ chatTitle }}</text>
|
||||
<text class="chat-status">在线</text>
|
||||
<text class="chat-status">在线服务中</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="header-actions">
|
||||
@@ -28,9 +28,9 @@
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
<view class="chat-messages">
|
||||
<!-- 系统消息 -->
|
||||
<view class="message-item system">
|
||||
<text class="system-text">已接入客户对话</text>
|
||||
<!-- 系统提示 -->
|
||||
<view class="message-item system">
|
||||
<text class="system-text">已接入用户咊询对话(AI问诊仅为参考,不构成诊断依据)</text>
|
||||
</view>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
@@ -87,7 +87,7 @@
|
||||
<input
|
||||
class="message-input"
|
||||
v-model="inputText"
|
||||
placeholder="请输入消息..."
|
||||
placeholder="请输入咊询内容..."
|
||||
:focus="inputFocus"
|
||||
@confirm="sendMessage"
|
||||
confirm-type="send"
|
||||
@@ -141,7 +141,7 @@
|
||||
return {
|
||||
sessionId: '',
|
||||
chatUserId: '',
|
||||
chatTitle: '客户',
|
||||
chatTitle: '用户咊询',
|
||||
inputText: '',
|
||||
chatMessages: [] as ChatMessageType[],
|
||||
scrollToView: '',
|
||||
|
||||
@@ -9,16 +9,16 @@
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<view class="header">
|
||||
<text class="title">由于您的需求,为 {{ userName }} 配置专属商品折扣</text>
|
||||
<text class="subtitle">专属打折商品的折扣不受全场默认 VIP 折扣影响。</text>
|
||||
<text class="title">为 {{ userName }} 配置定向关怀补贴</text>
|
||||
<text class="subtitle">定向关怀补贴项目,优先于全场默认优惠,帮助长者获得更多專属关怀。</text>
|
||||
</view>
|
||||
|
||||
<view class="action-bar">
|
||||
<button class="add-btn" @click="openProductSelect">+ 添加更多覆盖商品</button>
|
||||
<button class="add-btn" @click="openProductSelect">+ 添加更多定向优惠项目</button>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="list-container">
|
||||
<view v-if="discounts.length === 0" class="empty-tip">该客户暂无专属折扣商品</view>
|
||||
<view v-if="discounts.length === 0" class="empty-tip">该用户暂无定向关怀补贴项目</view>
|
||||
|
||||
<view v-for="item in discounts" :key="item.id" class="discount-item">
|
||||
<view class="product-info">
|
||||
@@ -27,7 +27,7 @@
|
||||
<text class="product-name">{{ item.product_name }}</text>
|
||||
<view class="price-row">
|
||||
<text class="original-price">原价 ¥{{ item.base_price }}</text>
|
||||
<text class="current-discount">当前设置: {{ parseFloat(item.discount_rate) * 10 }} 折</text>
|
||||
<text class="current-discount">当前设置: {{ parseFloat(item.discount_rate) * 10 }} 折优惠</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -41,7 +41,7 @@
|
||||
<!-- 修改折扣弹窗 -->
|
||||
<view class="modal" v-if="showEditModal">
|
||||
<view class="modal-content">
|
||||
<view class="modal-header">设置该商品的折扣倍数</view>
|
||||
<view class="modal-header">设置该项目的优惠比例</view>
|
||||
<view class="modal-body">
|
||||
<input type="digit" class="input" v-model="editForm.rate" placeholder="示例:填写 0.85 代表 85折" />
|
||||
</view>
|
||||
@@ -55,7 +55,7 @@
|
||||
<!-- 选择商品弹窗 -->
|
||||
<view class="modal" v-if="showProductSelect">
|
||||
<view class="modal-content product-modal-content">
|
||||
<view class="modal-header">选择要设置折扣的商品</view>
|
||||
<view class="modal-header">选择要设置优惠的服务项目</view>
|
||||
<scroll-view scroll-y class="product-scroll">
|
||||
<view class="product-p-item" v-for="p in allProducts" :key="p.id" @click="selectProductForDiscount(p)">
|
||||
<image :src="p.main_image_url || '/static/images/default-product.png'" class="p-img" mode="aspectFill" />
|
||||
@@ -64,7 +64,7 @@
|
||||
<text class="p-price">¥{{p.base_price}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="allProducts.length === 0" class="empty-tip">暂无可选商品</view>
|
||||
<view v-if="allProducts.length === 0" class="empty-tip">暂无可选服务项目</view>
|
||||
</scroll-view>
|
||||
<view class="modal-footer">
|
||||
<button class="btn cancel" @click="showProductSelect = false">关闭</button>
|
||||
@@ -127,7 +127,7 @@
|
||||
async openProductSelect() {
|
||||
this.showProductSelect = true
|
||||
if (this.allProducts.length === 0) {
|
||||
uni.showLoading({ title: '获取商品中' })
|
||||
uni.showLoading({ title: '获取服务项目中' })
|
||||
try {
|
||||
let merchantId = ''
|
||||
const session = supa.getSession()
|
||||
@@ -233,7 +233,7 @@
|
||||
user_id: item['user_id'] != null ? String(item['user_id']) : '',
|
||||
product_id: pid,
|
||||
discount_rate: item['discount_rate'] != null ? String(item['discount_rate']) : '1.0',
|
||||
product_name: prod != null && prod['name'] != null ? String(prod['name']) : '未知商品',
|
||||
product_name: prod != null && prod['name'] != null ? String(prod['name']) : '未知服务',
|
||||
main_image_url: prod != null && prod['main_image_url'] != null ? String(prod['main_image_url']) : '',
|
||||
base_price: prod != null && prod['base_price'] != null ? String(prod['base_price']) : '0'
|
||||
} as DiscountDoc)
|
||||
@@ -312,7 +312,7 @@
|
||||
const that = this
|
||||
uni.showModal({
|
||||
title: '提醒',
|
||||
content: '确定移除此商品的打折?移除后将恢复通常价格',
|
||||
content: '确定移除此服务项目的优惠折扣?移除后将恢复原始价格',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({ title: '移除中' })
|
||||
@@ -390,11 +390,13 @@
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.product-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
}
|
||||
.product-image {
|
||||
@@ -414,6 +416,7 @@
|
||||
.price-row {
|
||||
margin-top: 10rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
@@ -482,6 +485,7 @@
|
||||
}
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-top: 1rpx solid #eee;
|
||||
}
|
||||
.modal-footer .btn {
|
||||
@@ -514,6 +518,7 @@
|
||||
}
|
||||
.product-p-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- 商家端 - 财务管理页面 -->
|
||||
<!-- 机构端 - 结算中心页面 -->
|
||||
<template>
|
||||
<view class="finance-page">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
@@ -10,11 +10,11 @@
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<view class="balance-card">
|
||||
<text class="balance-label">账户余额(元)</text>
|
||||
<text class="balance-label">可结算/补贴金额(元)</text>
|
||||
<text class="balance-value">¥{{ balance }}</text>
|
||||
<view class="balance-actions">
|
||||
<view class="action-btn withdraw" @click="withdraw">提现</view>
|
||||
<view class="action-btn detail" @click="viewDetail">明细</view>
|
||||
<view class="action-btn withdraw" @click="withdraw">申请结算/补贴</view>
|
||||
<view class="action-btn detail" @click="viewDetail">收支明细</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ stats.pendingWithdraw }}</text>
|
||||
<text class="stat-label">待提现</text>
|
||||
<text class="stat-label">待结算</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tabs">
|
||||
<view class="tab" :class="{ active: currentTab === 'record' }" @click="switchTab('record')">收支记录</view>
|
||||
<view class="tab" :class="{ active: currentTab === 'withdraw' }" @click="switchTab('withdraw')">提现记录</view>
|
||||
<view class="tab" :class="{ active: currentTab === 'withdraw' }" @click="switchTab('withdraw')">结算/补贴记录</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="records-list" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
|
||||
@@ -56,20 +56,20 @@
|
||||
|
||||
<view v-if="showWithdrawModal" class="modal-mask" @click="closeWithdrawModal">
|
||||
<view class="modal-content" @click.stop>
|
||||
<view class="modal-header"><text class="modal-title">提现</text><text class="modal-close" @click="closeWithdrawModal">×</text></view>
|
||||
<view class="modal-header"><text class="modal-title">结算补贴申请</text><text class="modal-close" @click="closeWithdrawModal">×</text></view>
|
||||
<view class="modal-body">
|
||||
<view class="form-item">
|
||||
<text class="label">可提现金额</text>
|
||||
<text class="label">可结算金额</text>
|
||||
<text class="value">¥{{ balance }}</text>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">提现金额</text>
|
||||
<input class="input" type="digit" v-model="withdrawAmount" placeholder="请输入提现金额"/>
|
||||
<text class="label">结算金额</text>
|
||||
<input class="input" type="digit" v-model="withdrawAmount" placeholder="请输入结算金额"/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<view class="modal-btn cancel" @click="closeWithdrawModal">取消</view>
|
||||
<view class="modal-btn confirm" @click="confirmWithdraw">确认提现</view>
|
||||
<view class="modal-btn confirm" @click="confirmWithdraw">确认结算</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -206,7 +206,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: '提现申请已提交', icon: 'success' })
|
||||
uni.showToast({ title: '结算申请已提交', icon: 'success' })
|
||||
this.closeWithdrawModal()
|
||||
},
|
||||
|
||||
@@ -225,27 +225,31 @@
|
||||
|
||||
<style>
|
||||
.finance-page { background-color: #f5f5f5; min-height: 100vh; }
|
||||
.balance-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 50rpx 30rpx; color: #fff; text-align: center; }
|
||||
.balance-card { background: linear-gradient(135deg, rgb(66, 121, 240) 0%, #5856D6 100%); padding: 50rpx 30rpx; color: #fff; text-align: center; }
|
||||
.balance-label { font-size: 26rpx; opacity: 0.9; display: block; margin-bottom: 20rpx; }
|
||||
.balance-value { font-size: 60rpx; font-weight: bold; display: block; margin-bottom: 40rpx; }
|
||||
.balance-actions { display: flex; justify-content: center; gap: 30rpx; }
|
||||
.balance-actions { display: flex; flex-direction: row; justify-content: center; gap: 30rpx; }
|
||||
.action-btn { padding: 16rpx 60rpx; border-radius: 40rpx; font-size: 28rpx; }
|
||||
.action-btn.withdraw { background-color: #fff; color: #667eea; }
|
||||
.action-btn.withdraw { background-color: #fff; color: rgb(66, 121, 240); }
|
||||
.action-btn.detail { background-color: rgba(255,255,255,0.2); color: #fff; }
|
||||
.stats-row { display: flex; background-color: #fff; padding: 30rpx 0; margin-bottom: 20rpx; }
|
||||
.stats-row { display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #fff; padding: 30rpx 0; margin-bottom: 20rpx; }
|
||||
.stat-item { flex: 1; text-align: center; border-right: 1rpx solid #f5f5f5; }
|
||||
.stat-item:last-child { border-right: none; }
|
||||
.stat-value { font-size: 32rpx; font-weight: bold; color: #333; display: block; }
|
||||
.stat-label { font-size: 24rpx; color: #999; }
|
||||
.tabs { display: flex; background-color: #fff; padding: 0 20rpx; }
|
||||
.tabs { display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #fff; padding: 0 20rpx; }
|
||||
.tab { flex: 1; text-align: center; padding: 24rpx 0; font-size: 28rpx; color: #666; position: relative; }
|
||||
.tab.active { color: #007AFF; font-weight: bold; }
|
||||
.tab.active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 40rpx; height: 4rpx; background-color: #007AFF; border-radius: 2rpx; }
|
||||
.tab.active { color: rgb(66, 121, 240); font-weight: bold; }
|
||||
.tab.active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 40rpx; height: 4rpx; background-color: rgb(66, 121, 240); border-radius: 2rpx; }
|
||||
.records-list { padding: 20rpx; height: calc(100vh - 500rpx); }
|
||||
.loading-container, .empty-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 100rpx 0; }
|
||||
.empty-icon { font-size: 100rpx; margin-bottom: 20rpx; }
|
||||
.empty-text, .loading-text { font-size: 28rpx; color: #999; }
|
||||
.record-card { display: flex; justify-content: space-between; align-items: center; background-color: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; }
|
||||
.record-card { display: flex; flex-direction: row; justify-content: space-between; align-items: center; background-color: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; }
|
||||
.record-info { flex: 1; }
|
||||
.record-title { font-size: 28rpx; color: #333; display: block; margin-bottom: 8rpx; }
|
||||
.record-time { font-size: 22rpx; color: #999; }
|
||||
@@ -254,7 +258,7 @@
|
||||
.record-amount.negative { color: #F44336; }
|
||||
.modal-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
||||
.modal-content { width: 80%; background-color: #fff; border-radius: 16rpx; }
|
||||
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 30rpx; border-bottom: 1rpx solid #f5f5f5; }
|
||||
.modal-header { display: flex; flex-direction: row; justify-content: space-between; align-items: center; padding: 30rpx; border-bottom: 1rpx solid #f5f5f5; }
|
||||
.modal-title { font-size: 32rpx; font-weight: bold; color: #333; }
|
||||
.modal-close { font-size: 44rpx; color: #999; }
|
||||
.modal-body { padding: 30rpx; }
|
||||
@@ -262,8 +266,8 @@
|
||||
.form-item .label { font-size: 26rpx; color: #999; display: block; margin-bottom: 10rpx; }
|
||||
.form-item .value { font-size: 28rpx; color: #333; }
|
||||
.input { height: 72rpx; border: 1rpx solid #e5e5e5; border-radius: 8rpx; padding: 0 20rpx; font-size: 28rpx; }
|
||||
.modal-footer { display: flex; border-top: 1rpx solid #f5f5f5; }
|
||||
.modal-footer { display: flex; flex-direction: row; border-top: 1rpx solid #f5f5f5; }
|
||||
.modal-btn { flex: 1; height: 88rpx; line-height: 88rpx; text-align: center; font-size: 28rpx; }
|
||||
.modal-btn.cancel { color: #666; border-right: 1rpx solid #f5f5f5; }
|
||||
.modal-btn.confirm { color: #007AFF; font-weight: bold; }
|
||||
.modal-btn.confirm { color: rgb(66, 121, 240); font-weight: bold; }
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<!-- 商家端 - 成长页(Tab4):经营指南、流量获取方法、店铺经营建议、学习入口 -->
|
||||
<!-- 机构端 - 机构成长页(安读Tab4 -->
|
||||
<template>
|
||||
<view class="growth-page">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<!-- Tab 页无返回按钮,显示顶部安全区 + 页面标题 -->
|
||||
<view class="mp-tab-navbar">
|
||||
<text class="mp-tab-title">成长</text>
|
||||
<text class="mp-tab-title">机构成长</text>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<view class="level-card">
|
||||
<view class="level-header">
|
||||
<view class="level-info">
|
||||
<text class="level-label">商家成长等级</text>
|
||||
<text class="level-label">机构服务等级</text>
|
||||
<view class="level-badge">
|
||||
<text class="level-text">{{ currentLevel.name }}</text>
|
||||
</view>
|
||||
@@ -28,14 +28,14 @@
|
||||
<text class="level-progress-text">{{ currentLevel.progress }}% · 距下一级还需 {{ currentLevel.nextTarget }}</text>
|
||||
</view>
|
||||
<view class="level-tips">
|
||||
<text class="level-tips-text">完善店铺信息、上传商品、获得订单可提升等级</text>
|
||||
<text class="level-tips-text">完善机构信息、发布服务、获得订单可提升等级</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 流量获取方法 -->
|
||||
<view class="section-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">📈 流量获取方法</text>
|
||||
<text class="section-title">📈 获客与转化建议</text>
|
||||
</view>
|
||||
<view class="tips-list">
|
||||
<view
|
||||
@@ -59,7 +59,7 @@
|
||||
<!-- 经营指南 -->
|
||||
<view class="section-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">📋 经营指南</text>
|
||||
<text class="section-title">📋 合规与运营指南</text>
|
||||
</view>
|
||||
<view class="guide-grid">
|
||||
<view
|
||||
@@ -77,7 +77,7 @@
|
||||
<!-- 店铺经营建议 -->
|
||||
<view class="section-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">💡 今日经营建议</text>
|
||||
<text class="section-title">💡 今日运营建议</text>
|
||||
</view>
|
||||
<view class="suggestion-list">
|
||||
<view
|
||||
@@ -104,7 +104,7 @@
|
||||
<!-- 学习中心入口 -->
|
||||
<view class="section-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">🎓 学习中心</text>
|
||||
<text class="section-title">🎓 培训中心</text>
|
||||
</view>
|
||||
<view class="learn-list">
|
||||
<view
|
||||
@@ -187,84 +187,84 @@
|
||||
return {
|
||||
refreshing: false,
|
||||
currentLevel: {
|
||||
name: '新手商家',
|
||||
icon: '🌱',
|
||||
name: '初入机构',
|
||||
icon: '🏥',
|
||||
progress: 30,
|
||||
nextTarget: '上传满5件商品'
|
||||
nextTarget: '发布5项以上服务'
|
||||
} as LevelType,
|
||||
|
||||
trafficTips: [
|
||||
{
|
||||
icon: '🏷️',
|
||||
title: '优化商品标题关键词',
|
||||
desc: '精准标题可提升搜索曝光量 3-5 倍',
|
||||
icon: '<EFBFBD>',
|
||||
title: '优化服务标题与科室标签',
|
||||
desc: '精准标签可提升搜索曝光量 3-5 倍',
|
||||
color: '#ff5000',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
icon: '📷',
|
||||
title: '提升主图吸引力',
|
||||
desc: '高质量主图点击率提升 40% 以上',
|
||||
color: '#007AFF',
|
||||
title: '优化服务封面与机构展示图',
|
||||
desc: '高质量展示图点击率提升 40% 以上',
|
||||
color: 'rgb(66, 121, 240)',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
icon: '💰',
|
||||
title: '设置限时优惠活动',
|
||||
desc: '促销期间订单量平均提升 60%',
|
||||
icon: '📊',
|
||||
title: '配置护理套餐/康复优惠',
|
||||
desc: '套餐优惠期间订单量平均提升 60%',
|
||||
color: '#34C759',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
icon: '⭐',
|
||||
title: '维护好评率',
|
||||
title: '提升服务满意度',
|
||||
desc: '好评率 >95% 可获得平台推荐流量',
|
||||
color: '#FF9500',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
icon: '📢',
|
||||
title: '参与平台活动',
|
||||
desc: '报名活动可获得额外曝光位置',
|
||||
title: '参与平台关怀活动',
|
||||
desc: '报名关怀活动可获得额外曝光位置',
|
||||
color: '#AF52DE',
|
||||
link: ''
|
||||
}
|
||||
] as TipType[],
|
||||
|
||||
operationGuides: [
|
||||
{ icon: '🛒', title: '商品管理', link: '/pages/mall/merchant/products' },
|
||||
{ icon: '📦', title: '库存优化', link: '/pages/mall/merchant/inventory' },
|
||||
{ icon: '🎯', title: '营销策略', link: '/pages/mall/merchant/promotions' },
|
||||
{ icon: '💬', title: '客服技巧', link: '/pages/mall/merchant/chat' },
|
||||
{ icon: '📊', title: '数据解读', link: '/pages/mall/merchant/statistics' },
|
||||
{ icon: '💳', title: '财务管理', link: '/pages/mall/merchant/finance' }
|
||||
{ icon: '🏥', title: '服务/商品管理', link: '/pages/mall/merchant/products' },
|
||||
{ icon: '🩺', title: '器械库存管理', link: '/pages/mall/merchant/inventory' },
|
||||
{ icon: '📊', title: '关怀活动运营', link: '/pages/mall/merchant/promotions' },
|
||||
{ icon: '💬', title: '和长者/家属沟通', link: '/pages/mall/merchant/chat' },
|
||||
{ icon: '📊', title: '服务运营统计', link: '/pages/mall/merchant/statistics' },
|
||||
{ icon: '💳', title: '结算中心', link: '/pages/mall/merchant/finance' }
|
||||
] as GuideType[],
|
||||
|
||||
suggestions: [
|
||||
{
|
||||
title: '完善店铺基础信息',
|
||||
desc: '补充店铺 Logo、简介、联系方式',
|
||||
title: '完善机构基础信息',
|
||||
desc: '补充机构 Logo、简介、联系方式',
|
||||
tag: '紧急',
|
||||
tagColor: '#ff3b30',
|
||||
done: false
|
||||
},
|
||||
{
|
||||
title: '上传至少3件在售商品',
|
||||
desc: '充足商品是获得流量的基础',
|
||||
title: '发布至少3项在售服务',
|
||||
desc: '充足的服务是获得流量的基础',
|
||||
tag: '重要',
|
||||
tagColor: '#ff9500',
|
||||
done: false
|
||||
},
|
||||
{
|
||||
title: '回复所有待回评价',
|
||||
title: '回复所有待回用户评价',
|
||||
desc: '及时回复可提升用户信任度',
|
||||
tag: '今日',
|
||||
tagColor: '#007aff',
|
||||
tagColor: 'rgb(66, 121, 240)',
|
||||
done: false
|
||||
},
|
||||
{
|
||||
title: '检查低库存商品',
|
||||
desc: '库存不足会导致订单自动取消',
|
||||
title: '检查器械低库存情况',
|
||||
desc: '库存不足会导致预约失败',
|
||||
tag: '常规',
|
||||
tagColor: '#34c759',
|
||||
done: false
|
||||
@@ -273,36 +273,44 @@
|
||||
|
||||
learnCourses: [
|
||||
{
|
||||
icon: '🚀',
|
||||
title: '新手商家开店必读',
|
||||
subtitle: '5分钟掌握开店核心流程',
|
||||
icon: '🏥',
|
||||
title: '新机构入驻必读',
|
||||
subtitle: '5分钟掌握入驻核心流程',
|
||||
tag: '入门',
|
||||
duration: '5 分钟',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
icon: '📸',
|
||||
title: '商品主图拍摄技巧',
|
||||
subtitle: '用手机也能拍出专业大片',
|
||||
icon: '🤺',
|
||||
title: '陪诊服务标准流程',
|
||||
subtitle: '干顶面、等候、取号成功必读',
|
||||
tag: '进阶',
|
||||
duration: '8 分钟',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
icon: '📣',
|
||||
title: '营销活动策划方法',
|
||||
subtitle: '节假日爆单秘诀全攻略',
|
||||
icon: '🏠',
|
||||
title: '居家护理服务规范',
|
||||
subtitle: '上门服务标准与安全要求',
|
||||
tag: '进阶',
|
||||
duration: '12 分钟',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
icon: '💬',
|
||||
title: '提升客服转化率',
|
||||
subtitle: '用话术留住每一位询问客户',
|
||||
icon: '🤖',
|
||||
title: 'AI和长者/家属咨询接待话术',
|
||||
subtitle: '用话术留住每一位和查用户',
|
||||
tag: '实战',
|
||||
duration: '10 分钟',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
icon: '📊',
|
||||
title: '慢病管理服务运营方法',
|
||||
subtitle: '慢病客户持续服务与复购指南',
|
||||
tag: '实战',
|
||||
duration: '15 分钟',
|
||||
link: ''
|
||||
}
|
||||
] as CourseType[]
|
||||
}
|
||||
|
||||
904
pages/mall/merchant/health-management.uvue
Normal file
904
pages/mall/merchant/health-management.uvue
Normal file
@@ -0,0 +1,904 @@
|
||||
<!-- 机构端 - 健康管理页面 -->
|
||||
<template>
|
||||
<view class="hm-page">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<view class="detail-navbar">
|
||||
<view class="detail-navbar-back" @click="uni.navigateBack()">
|
||||
<text class="back-arrow">‹</text>
|
||||
<text class="back-text">返回</text>
|
||||
</view>
|
||||
<text class="detail-navbar-title">健康管理</text>
|
||||
<view style="width: 120rpx;"></view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<scroll-view class="hm-scroll" direction="vertical">
|
||||
|
||||
<!-- 顶部用户选择器 -->
|
||||
<view class="user-selector-card">
|
||||
<view class="us-inner">
|
||||
<view class="us-avatar-wrap">
|
||||
<view class="us-avatar">{{ currentUser.name.substring(0, 1) }}</view>
|
||||
</view>
|
||||
<view class="us-info">
|
||||
<text class="us-name">{{ currentUser.name }}</text>
|
||||
<text class="us-sub">{{ currentUser.age }}岁 · {{ currentUser.gender }} · {{ currentUser.diagnosis }}</text>
|
||||
</view>
|
||||
<view class="us-switch-btn" @click="switchUser">切换</view>
|
||||
</view>
|
||||
<view class="us-tags-row">
|
||||
<view v-for="tag in currentUser.tags" :key="tag" class="us-tag" :class="tag === '高风险' ? 'us-tag-danger' : ''">{{ tag }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 健康数据面板 -->
|
||||
<view class="section-card mt16">
|
||||
<view class="section-card-title">
|
||||
<text class="sct-icon">📊</text>
|
||||
<text class="sct-text">今日健康数据</text>
|
||||
<text class="sct-time">{{ todayDate }}</text>
|
||||
</view>
|
||||
<view class="health-data-grid">
|
||||
<view v-for="item in healthMetrics" :key="item.key" class="hd-item" :class="item.status === 'warning' ? 'hd-item-warning' : item.status === 'danger' ? 'hd-item-danger' : ''">
|
||||
<text class="hd-icon">{{ item.icon }}</text>
|
||||
<text class="hd-val">{{ item.value }}</text>
|
||||
<text class="hd-unit">{{ item.unit }}</text>
|
||||
<text class="hd-label">{{ item.label }}</text>
|
||||
<view v-if="item.status !== 'normal'" class="hd-badge" :class="item.status === 'danger' ? 'hd-badge-danger' : 'hd-badge-warning'">{{ item.statusText }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 健康档案 -->
|
||||
<view class="section-card mt16">
|
||||
<view class="section-card-title">
|
||||
<text class="sct-icon">📋</text>
|
||||
<text class="sct-text">健康档案</text>
|
||||
<text class="sct-action" @click="editRecord">编辑</text>
|
||||
</view>
|
||||
<view class="record-row">
|
||||
<text class="record-label">身高/体重</text>
|
||||
<text class="record-val">{{ healthRecord.height }}cm / {{ healthRecord.weight }}kg</text>
|
||||
</view>
|
||||
<view class="record-row">
|
||||
<text class="record-label">血型</text>
|
||||
<text class="record-val">{{ healthRecord.bloodType }}</text>
|
||||
</view>
|
||||
<view class="record-row">
|
||||
<text class="record-label">主要诊断</text>
|
||||
<text class="record-val">{{ healthRecord.mainDiagnosis }}</text>
|
||||
</view>
|
||||
<view class="record-row">
|
||||
<text class="record-label">过敏史</text>
|
||||
<text class="record-val">{{ healthRecord.allergy || '无已知过敏' }}</text>
|
||||
</view>
|
||||
<view class="record-row">
|
||||
<text class="record-label">主治医生</text>
|
||||
<text class="record-val">{{ healthRecord.doctor }}</text>
|
||||
</view>
|
||||
<view class="record-row">
|
||||
<text class="record-label">就诊医院</text>
|
||||
<text class="record-val">{{ healthRecord.hospital }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 慢病管理 -->
|
||||
<view class="section-card mt16">
|
||||
<view class="section-card-title">
|
||||
<text class="sct-icon">🏥</text>
|
||||
<text class="sct-text">慢病管理</text>
|
||||
</view>
|
||||
<view v-for="disease in chronicDiseases" :key="disease.name" class="disease-item">
|
||||
<view class="disease-header">
|
||||
<text class="disease-name">{{ disease.name }}</text>
|
||||
<view class="disease-status" :class="disease.controlled ? 'ds-good' : 'ds-bad'">
|
||||
{{ disease.controlled ? '控制良好' : '需关注' }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="disease-indicators">
|
||||
<view v-for="ind in disease.indicators" :key="ind.name" class="di-row">
|
||||
<text class="di-label">{{ ind.name }}</text>
|
||||
<text class="di-val" :class="ind.abnormal ? 'di-val-warn' : ''">{{ ind.value }}</text>
|
||||
<text class="di-ref">参考:{{ ind.reference }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用药提醒 -->
|
||||
<view class="section-card mt16">
|
||||
<view class="section-card-title">
|
||||
<text class="sct-icon">💊</text>
|
||||
<text class="sct-text">用药提醒</text>
|
||||
<text class="sct-action" @click="addMedication">+ 添加</text>
|
||||
</view>
|
||||
<view v-if="medications.length === 0" class="empty-tip">
|
||||
<text class="empty-tip-text">暂无用药记录</text>
|
||||
</view>
|
||||
<view v-for="med in medications" :key="med.name" class="med-item">
|
||||
<view class="med-left">
|
||||
<view class="med-dot" :class="med.taken ? 'med-dot-done' : 'med-dot-pending'"></view>
|
||||
</view>
|
||||
<view class="med-info">
|
||||
<text class="med-name">{{ med.name }}</text>
|
||||
<text class="med-dose">{{ med.dose }} · {{ med.frequency }}</text>
|
||||
</view>
|
||||
<view class="med-time-col">
|
||||
<text class="med-time">{{ med.nextTime }}</text>
|
||||
<text class="med-status" :class="med.taken ? 'ms-done' : 'ms-pending'">{{ med.taken ? '已服' : '待服' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 复查预约 -->
|
||||
<view class="section-card mt16">
|
||||
<view class="section-card-title">
|
||||
<text class="sct-icon">📅</text>
|
||||
<text class="sct-text">复查预约</text>
|
||||
<text class="sct-action" @click="bookReview">+ 预约</text>
|
||||
</view>
|
||||
<view v-if="appointments.length === 0" class="empty-tip">
|
||||
<text class="empty-tip-text">暂无复查预约</text>
|
||||
</view>
|
||||
<view v-for="appt in appointments" :key="appt.id" class="appt-item">
|
||||
<view class="appt-date-col">
|
||||
<text class="appt-month">{{ appt.month }}</text>
|
||||
<text class="appt-day">{{ appt.day }}</text>
|
||||
</view>
|
||||
<view class="appt-info">
|
||||
<text class="appt-title">{{ appt.title }}</text>
|
||||
<text class="appt-desc">{{ appt.department }} · {{ appt.doctor }}</text>
|
||||
<text class="appt-location">{{ appt.hospital }}</text>
|
||||
</view>
|
||||
<view class="appt-tag" :class="appt.status === 'upcoming' ? 'at-upcoming' : 'at-done'">
|
||||
{{ appt.status === 'upcoming' ? '待复查' : '已完成' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 健康趋势图(简单柱状图) -->
|
||||
<view class="section-card mt16">
|
||||
<view class="section-card-title">
|
||||
<text class="sct-icon">📈</text>
|
||||
<text class="sct-text">本周血压趋势</text>
|
||||
</view>
|
||||
<view class="chart-area">
|
||||
<view v-for="(item, idx) in bpTrend" :key="idx" class="bar-group">
|
||||
<view class="bar-pair">
|
||||
<view class="bar bar-sys" :style="'height: ' + (item.sys / 200 * 120) + 'rpx;'"></view>
|
||||
<view class="bar bar-dia" :style="'height: ' + (item.dia / 200 * 120) + 'rpx;'"></view>
|
||||
</view>
|
||||
<text class="bar-label">{{ item.day }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="chart-legend">
|
||||
<view class="legend-item"><view class="legend-dot ld-sys"></view><text class="legend-text">收缩压</text></view>
|
||||
<view class="legend-item"><view class="legend-dot ld-dia"></view><text class="legend-text">舒张压</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view style="height: 60rpx;"></view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
type HealthMetricType = {
|
||||
key: string
|
||||
icon: string
|
||||
value: string
|
||||
unit: string
|
||||
label: string
|
||||
status: string
|
||||
statusText: string
|
||||
}
|
||||
|
||||
type IndicatorType = {
|
||||
name: string
|
||||
value: string
|
||||
reference: string
|
||||
abnormal: boolean
|
||||
}
|
||||
|
||||
type DiseaseType = {
|
||||
name: string
|
||||
controlled: boolean
|
||||
indicators: IndicatorType[]
|
||||
}
|
||||
|
||||
type MedicationType = {
|
||||
name: string
|
||||
dose: string
|
||||
frequency: string
|
||||
nextTime: string
|
||||
taken: boolean
|
||||
}
|
||||
|
||||
type AppointmentType = {
|
||||
id: string
|
||||
month: string
|
||||
day: string
|
||||
title: string
|
||||
department: string
|
||||
doctor: string
|
||||
hospital: string
|
||||
status: string
|
||||
}
|
||||
|
||||
type BpPointType = {
|
||||
day: string
|
||||
sys: number
|
||||
dia: number
|
||||
}
|
||||
|
||||
type UserType = {
|
||||
name: string
|
||||
age: number
|
||||
gender: string
|
||||
diagnosis: string
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
todayDate: '' as string,
|
||||
currentUser: {
|
||||
name: '李奶奶',
|
||||
age: 78,
|
||||
gender: '女',
|
||||
diagnosis: '高血压·糖尿病',
|
||||
tags: ['慢病用户', '高风险', '长期服务']
|
||||
} as UserType,
|
||||
healthMetrics: [
|
||||
{ key: 'bp', icon: '❤️', value: '148/92', unit: 'mmHg', label: '血压', status: 'warning', statusText: '偏高' },
|
||||
{ key: 'blood_sugar', icon: '🩸', value: '7.8', unit: 'mmol/L', label: '血糖', status: 'warning', statusText: '偏高' },
|
||||
{ key: 'heart_rate', icon: '💓', value: '76', unit: 'bpm', label: '心率', status: 'normal', statusText: '' },
|
||||
{ key: 'spo2', icon: '🫁', value: '97', unit: '%', label: '血氧', status: 'normal', statusText: '' },
|
||||
{ key: 'temp', icon: '🌡️', value: '36.5', unit: '°C', label: '体温', status: 'normal', statusText: '' },
|
||||
{ key: 'weight', icon: '⚖️', value: '62.5', unit: 'kg', label: '体重', status: 'normal', statusText: '' }
|
||||
] as HealthMetricType[],
|
||||
healthRecord: {
|
||||
height: 162,
|
||||
weight: 62.5,
|
||||
bloodType: 'A型',
|
||||
mainDiagnosis: '高血压3级、2型糖尿病、骨质疏松',
|
||||
allergy: '青霉素',
|
||||
doctor: '王主任',
|
||||
hospital: '嘉城医院内科'
|
||||
},
|
||||
chronicDiseases: [
|
||||
{
|
||||
name: '高血压',
|
||||
controlled: false,
|
||||
indicators: [
|
||||
{ name: '收缩压', value: '148 mmHg', reference: '<140', abnormal: true },
|
||||
{ name: '舒张压', value: '92 mmHg', reference: '<90', abnormal: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '2型糖尿病',
|
||||
controlled: false,
|
||||
indicators: [
|
||||
{ name: '空腹血糖', value: '7.8 mmol/L', reference: '3.9-7.0', abnormal: true },
|
||||
{ name: '糖化血红蛋白', value: '7.2%', reference: '<7.0%', abnormal: true }
|
||||
]
|
||||
}
|
||||
] as DiseaseType[],
|
||||
medications: [
|
||||
{ name: '硝苯地平控释片', dose: '30mg', frequency: '每日一次', nextTime: '今日 08:00', taken: true },
|
||||
{ name: '二甲双胍', dose: '500mg', frequency: '每日三次', nextTime: '今日 12:00', taken: false },
|
||||
{ name: '阿司匹林肠溶片', dose: '100mg', frequency: '每日一次', nextTime: '今日 08:00', taken: true },
|
||||
{ name: '骨化三醇胶丸', dose: '0.25μg', frequency: '每日两次', nextTime: '今日 20:00', taken: false }
|
||||
] as MedicationType[],
|
||||
appointments: [
|
||||
{ id: '1', month: '04月', day: '20', title: '高血压复查', department: '心内科', doctor: '王主任', hospital: '嘉城医院', status: 'upcoming' },
|
||||
{ id: '2', month: '04月', day: '28', title: '糖尿病随访', department: '内分泌科', doctor: '刘副主任', hospital: '嘉城医院', status: 'upcoming' }
|
||||
] as AppointmentType[],
|
||||
bpTrend: [
|
||||
{ day: '周一', sys: 145, dia: 88 },
|
||||
{ day: '周二', sys: 150, dia: 92 },
|
||||
{ day: '周三', sys: 142, dia: 86 },
|
||||
{ day: '周四', sys: 148, dia: 90 },
|
||||
{ day: '周五', sys: 152, dia: 94 },
|
||||
{ day: '周六', sys: 144, dia: 87 },
|
||||
{ day: '周日', sys: 148, dia: 92 }
|
||||
] as BpPointType[]
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const now = new Date()
|
||||
const m = (now.getMonth() + 1).toString().padStart(2, '0')
|
||||
const d = now.getDate().toString().padStart(2, '0')
|
||||
this.todayDate = `${m}月${d}日`
|
||||
},
|
||||
|
||||
methods: {
|
||||
switchUser() {
|
||||
uni.showToast({ title: '切换用户功能开发中', icon: 'none' })
|
||||
},
|
||||
editRecord() {
|
||||
uni.showToast({ title: '编辑档案功能开发中', icon: 'none' })
|
||||
},
|
||||
addMedication() {
|
||||
uni.showToast({ title: '添加用药功能开发中', icon: 'none' })
|
||||
},
|
||||
bookReview() {
|
||||
uni.showToast({ title: '复查预约功能开发中', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.hm-page {
|
||||
background-color: #f0f2f7;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.hm-scroll {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mt16 { margin-top: 16rpx; }
|
||||
|
||||
/* ===== 导航栏 ===== */
|
||||
.detail-navbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
background-color: #ffffff;
|
||||
border-bottom-width: 1rpx;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: #eeeeee;
|
||||
box-sizing: border-box;
|
||||
padding-top: var(--status-bar-height);
|
||||
height: calc(88rpx + var(--status-bar-height));
|
||||
}
|
||||
|
||||
.detail-navbar-back {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 30rpx;
|
||||
height: 88rpx;
|
||||
width: 120rpx;
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
font-size: 44rpx;
|
||||
color: #333333;
|
||||
line-height: 1;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
.back-text {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.detail-navbar-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
}
|
||||
|
||||
/* ===== 用户选择器 ===== */
|
||||
.user-selector-card {
|
||||
background: linear-gradient(135deg, rgb(66,121,240) 0%, rgb(40,80,180) 100%);
|
||||
padding: 24rpx 30rpx 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.us-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.us-avatar-wrap {
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.us-avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
background-color: rgba(255,255,255,0.3);
|
||||
color: #ffffff;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.us-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.us-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.us-sub {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255,255,255,0.8);
|
||||
}
|
||||
|
||||
.us-switch-btn {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255,255,255,0.9);
|
||||
border-width: 1rpx;
|
||||
border-style: solid;
|
||||
border-color: rgba(255,255,255,0.5);
|
||||
border-radius: 20rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
}
|
||||
|
||||
.us-tags-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.us-tag {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255,255,255,0.9);
|
||||
background-color: rgba(255,255,255,0.15);
|
||||
border-radius: 4rpx;
|
||||
padding: 4rpx 14rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.us-tag-danger {
|
||||
background-color: rgba(255,80,80,0.35);
|
||||
color: #ffcccc;
|
||||
}
|
||||
|
||||
/* ===== 通用卡片 ===== */
|
||||
.section-card {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.section-card-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 22rpx 30rpx 18rpx 30rpx;
|
||||
border-bottom-width: 1rpx;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.sct-icon { font-size: 30rpx; margin-right: 10rpx; }
|
||||
|
||||
.sct-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sct-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.sct-action {
|
||||
font-size: 24rpx;
|
||||
color: rgb(66,121,240);
|
||||
}
|
||||
|
||||
/* ===== 健康数据网格 ===== */
|
||||
.health-data-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
padding: 16rpx 16rpx 8rpx 16rpx;
|
||||
}
|
||||
|
||||
.hd-item {
|
||||
width: 33.33%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16rpx 10rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hd-item-warning {
|
||||
background-color: #fffbf0;
|
||||
}
|
||||
|
||||
.hd-item-danger {
|
||||
background-color: #fff0f0;
|
||||
}
|
||||
|
||||
.hd-icon {
|
||||
font-size: 36rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.hd-val {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.hd-unit {
|
||||
font-size: 18rpx;
|
||||
color: #999;
|
||||
margin-top: 2rpx;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.hd-label {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.hd-badge {
|
||||
position: absolute;
|
||||
top: 10rpx;
|
||||
right: 10rpx;
|
||||
font-size: 18rpx;
|
||||
padding: 2rpx 8rpx;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
.hd-badge-warning {
|
||||
color: #FF7800;
|
||||
background-color: #fff3e0;
|
||||
}
|
||||
|
||||
.hd-badge-danger {
|
||||
color: #E64A19;
|
||||
background-color: #fde8e0;
|
||||
}
|
||||
|
||||
/* ===== 健康档案 ===== */
|
||||
.record-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 16rpx 30rpx;
|
||||
border-bottom-width: 1rpx;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.record-label {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
min-width: 150rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.record-val {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ===== 慢病管理 ===== */
|
||||
.disease-item {
|
||||
padding: 16rpx 30rpx;
|
||||
border-bottom-width: 1rpx;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.disease-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.disease-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.disease-status {
|
||||
font-size: 22rpx;
|
||||
border-radius: 4rpx;
|
||||
padding: 4rpx 14rpx;
|
||||
}
|
||||
|
||||
.ds-good {
|
||||
color: #4CAF50;
|
||||
background-color: #e8f5e9;
|
||||
}
|
||||
|
||||
.ds-bad {
|
||||
color: #E64A19;
|
||||
background-color: #fde8e0;
|
||||
}
|
||||
|
||||
.disease-indicators {
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 8rpx;
|
||||
padding: 10rpx 16rpx;
|
||||
}
|
||||
|
||||
.di-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 6rpx 0;
|
||||
}
|
||||
|
||||
.di-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
min-width: 140rpx;
|
||||
}
|
||||
|
||||
.di-val {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.di-val-warn {
|
||||
color: #E64A19;
|
||||
}
|
||||
|
||||
.di-ref {
|
||||
font-size: 20rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
/* ===== 用药提醒 ===== */
|
||||
.med-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 20rpx 30rpx;
|
||||
border-bottom-width: 1rpx;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.med-left {
|
||||
width: 36rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.med-dot {
|
||||
width: 18rpx;
|
||||
height: 18rpx;
|
||||
border-radius: 9rpx;
|
||||
}
|
||||
|
||||
.med-dot-done {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
|
||||
.med-dot-pending {
|
||||
background-color: #FF7800;
|
||||
}
|
||||
|
||||
.med-info {
|
||||
flex: 1;
|
||||
margin-left: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.med-name {
|
||||
font-size: 28rpx;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.med-dose {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.med-time-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.med-time {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.med-status {
|
||||
font-size: 20rpx;
|
||||
border-radius: 4rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
}
|
||||
|
||||
.ms-done {
|
||||
color: #4CAF50;
|
||||
background-color: #e8f5e9;
|
||||
}
|
||||
|
||||
.ms-pending {
|
||||
color: #FF7800;
|
||||
background-color: #fff3e0;
|
||||
}
|
||||
|
||||
/* ===== 复查预约 ===== */
|
||||
.appt-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 20rpx 30rpx;
|
||||
border-bottom-width: 1rpx;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.appt-date-col {
|
||||
width: 72rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-right: 20rpx;
|
||||
background-color: #eef2fe;
|
||||
border-radius: 8rpx;
|
||||
padding: 8rpx 10rpx;
|
||||
}
|
||||
|
||||
.appt-month {
|
||||
font-size: 18rpx;
|
||||
color: rgb(66,121,240);
|
||||
}
|
||||
|
||||
.appt-day {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: rgb(66,121,240);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.appt-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.appt-title {
|
||||
font-size: 28rpx;
|
||||
color: #1a1a1a;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.appt-desc {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.appt-location {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.appt-tag {
|
||||
font-size: 22rpx;
|
||||
border-radius: 4rpx;
|
||||
padding: 4rpx 14rpx;
|
||||
}
|
||||
|
||||
.at-upcoming {
|
||||
color: rgb(66,121,240);
|
||||
background-color: #eef2fe;
|
||||
}
|
||||
|
||||
.at-done {
|
||||
color: #999;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* ===== 血压趋势图 ===== */
|
||||
.chart-area {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
justify-content: space-around;
|
||||
padding: 20rpx 30rpx 10rpx 30rpx;
|
||||
height: 180rpx;
|
||||
}
|
||||
|
||||
.bar-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bar-pair {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.bar {
|
||||
width: 14rpx;
|
||||
border-radius: 4rpx 4rpx 0 0;
|
||||
margin: 0 2rpx;
|
||||
min-height: 6rpx;
|
||||
}
|
||||
|
||||
.bar-sys {
|
||||
background-color: rgb(66,121,240);
|
||||
}
|
||||
|
||||
.bar-dia {
|
||||
background-color: #84a8f8;
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
font-size: 18rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
padding: 10rpx 30rpx 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 0 20rpx;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 3rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.ld-sys { background-color: rgb(66,121,240); }
|
||||
.ld-dia { background-color: #84a8f8; }
|
||||
|
||||
.legend-text {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* ===== 通用 ===== */
|
||||
.empty-tip {
|
||||
padding: 40rpx 30rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.empty-tip-text {
|
||||
font-size: 26rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
</style>
|
||||
@@ -1,58 +1,87 @@
|
||||
<!-- 商家端首页 -->
|
||||
<!-- 医养综合服务商城 · 机构工作台首页 -->
|
||||
<template>
|
||||
<view class="merchant-container">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<!-- Tab 页无返回按鈕,展示顶部安全区 + 页面标题 -->
|
||||
<!-- Tab 页无返回按钮,展示顶部安全区 + 蓝色顶栏 -->
|
||||
<view class="mp-tab-navbar">
|
||||
<text class="mp-tab-title">商家工作台</text>
|
||||
<text class="mp-tab-title">机构工作台</text>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<scroll-view direction="vertical" class="main-scroll" :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
|
||||
<!-- 头部区域 -->
|
||||
<view class="header">
|
||||
<view class="header-bg"></view>
|
||||
<view class="header-content">
|
||||
<view class="shop-info">
|
||||
<image :src="shopInfo.shop_logo || '/static/logo.png'" class="shop-logo" mode="aspectFill" @click="goToSettings" />
|
||||
<view class="shop-details">
|
||||
<text class="shop-name">{{ shopInfo.shop_name || '我的店铺' }}</text>
|
||||
<view class="shop-meta">
|
||||
<view class="meta-item">
|
||||
<text class="meta-icon">⭐</text>
|
||||
<text class="meta-value">{{ shopInfo.rating_avg || 5.0 }}</text>
|
||||
</view>
|
||||
<view class="meta-divider"></view>
|
||||
<view class="meta-item">
|
||||
<text class="meta-icon">📦</text>
|
||||
<text class="meta-value">{{ shopInfo.total_sales || 0 }}销量</text>
|
||||
</view>
|
||||
|
||||
<!-- ===== 顶部工作台头部(浅蓝渐变,老年友好) ===== -->
|
||||
<view class="wt-header">
|
||||
<view class="wt-header-top">
|
||||
<!-- 机构信息行 -->
|
||||
<view class="wt-shop-row">
|
||||
<image :src="shopInfo.shop_logo || '/static/logo.png'" class="wt-shop-logo" mode="aspectFill" @click="goToSettings" />
|
||||
<view class="wt-shop-info">
|
||||
<text class="wt-shop-name">{{ shopInfo.shop_name || '我的机构' }}</text>
|
||||
<view class="wt-shop-meta">
|
||||
<text class="wt-meta-txt">⭐ {{ shopInfo.rating_avg || 5.0 }}</text>
|
||||
<text class="wt-meta-sep">|</text>
|
||||
<text class="wt-meta-txt">已服务 {{ shopInfo.total_sales || 0 }} 次</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="header-actions">
|
||||
<view class="action-btn" @click="goToMessages">
|
||||
<text class="action-icon">🔔</text>
|
||||
<view v-if="unreadCount > 0" class="action-badge"><text>{{ unreadCount > 99 ? '99+' : unreadCount }}</text></view>
|
||||
<!-- 头部右侧功能按钮 -->
|
||||
<view class="wt-header-btns">
|
||||
<view class="wt-icon-btn" @click="goToMessages">
|
||||
<text class="wt-icon-txt">🔔</text>
|
||||
<text class="wt-icon-label">通知</text>
|
||||
<view v-if="unreadCount > 0" class="wt-badge">
|
||||
<text class="wt-badge-txt">{{ unreadCount > 99 ? '99+' : unreadCount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-btn" @click="goToSettings">
|
||||
<text class="action-icon">⚙️</text>
|
||||
<view class="wt-icon-btn" @click="goToSettings">
|
||||
<text class="wt-icon-txt">⚙️</text>
|
||||
<text class="wt-icon-label">设置</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 今日数据双行面板(白色底,嵌在头部底部) -->
|
||||
<view class="wt-data-panel">
|
||||
<!-- 第一行:服务订单状态快捷入口 -->
|
||||
<view class="wt-panel-row">
|
||||
<view
|
||||
v-for="item in orderStatusList"
|
||||
:key="item.key"
|
||||
class="wt-panel-item"
|
||||
@click="goToOrders(item.key)"
|
||||
>
|
||||
<view class="wt-panel-num-wrap">
|
||||
<text class="wt-panel-num">{{ item.count }}</text>
|
||||
<view v-if="item.count > 0" class="wt-panel-dot"></view>
|
||||
</view>
|
||||
<text class="wt-panel-lbl">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="wt-panel-sep"></view>
|
||||
<!-- 第二行:核心经营指标 -->
|
||||
<view class="wt-panel-row">
|
||||
<view class="wt-panel-item">
|
||||
<text class="wt-panel-biz-val">¥{{ formatNumber(bizStats.saleAmount) }}</text>
|
||||
<text class="wt-panel-biz-lbl">今日结算</text>
|
||||
</view>
|
||||
<view class="wt-panel-item">
|
||||
<text class="wt-panel-biz-val">{{ bizStats.orderCount || 0 }}</text>
|
||||
<text class="wt-panel-biz-lbl">今日服务单</text>
|
||||
</view>
|
||||
<view class="wt-panel-item">
|
||||
<text class="wt-panel-biz-val">{{ bizStats.visitorCount || 0 }}</text>
|
||||
<text class="wt-panel-biz-lbl">咨询人数</text>
|
||||
</view>
|
||||
<view class="wt-panel-item">
|
||||
<text class="wt-panel-biz-val">{{ bizStats.reviewCount || 0 }}</text>
|
||||
<text class="wt-panel-biz-lbl">待评价</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 骨架屏:数据首次加载中 -->
|
||||
<view v-if="!isPageReady" class="ske-body">
|
||||
<view class="ske-card-wrap">
|
||||
<view class="ske-row ske-mb16"><view class="ske-bar ske-w30 ske-h28"></view></view>
|
||||
<view class="ske-grid-row">
|
||||
<view v-for="n in 4" :key="n" class="ske-cell25">
|
||||
<view class="ske-icon-sq"></view>
|
||||
<view class="ske-bar ske-w40 ske-mt8 ske-h32"></view>
|
||||
<view class="ske-bar ske-w60 ske-mt6 ske-h20"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ske-card-wrap">
|
||||
<view class="ske-row ske-mb16"><view class="ske-bar ske-w30 ske-h28"></view></view>
|
||||
<view class="ske-grid-row">
|
||||
@@ -72,207 +101,178 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 主体内容区域 ===== -->
|
||||
<view v-if="isPageReady" class="content-area">
|
||||
<!-- 今日数据卡片 -->
|
||||
<view class="stats-card">
|
||||
<view class="stats-header">
|
||||
<view class="stats-title-row">
|
||||
<text class="stats-title">📊 今日数据</text>
|
||||
<text class="stats-date">{{ currentDate }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 常用功能宫格(无色块背景,大字大图标,老年友好) -->
|
||||
<view class="wt-card">
|
||||
<view class="wt-card-hd">
|
||||
<text class="wt-card-title">常用功能</text>
|
||||
</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stats-item">
|
||||
<view class="stats-icon-wrap blue">
|
||||
<text class="stats-icon">📋</text>
|
||||
</view>
|
||||
<text class="stats-value">{{ todayStats.orders || 0 }}</text>
|
||||
<text class="stats-label">订单数</text>
|
||||
<view class="feature-grid">
|
||||
<view class="feature-item" @click="goToProducts('add')">
|
||||
<view class="feat-icon-wrap"><text class="feat-icon-txt">➕</text></view>
|
||||
<text class="feat-label">发布服务</text>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<view class="stats-icon-wrap green">
|
||||
<text class="stats-icon">💰</text>
|
||||
</view>
|
||||
<text class="stats-value">¥{{ formatNumber(todayStats.sales) }}</text>
|
||||
<text class="stats-label">销售额</text>
|
||||
<view class="feature-item" @click="goToOrders('all')">
|
||||
<view class="feat-icon-wrap"><text class="feat-icon-txt">📋</text></view>
|
||||
<text class="feat-label">服务订单</text>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<view class="stats-icon-wrap orange">
|
||||
<text class="stats-icon">👥</text>
|
||||
</view>
|
||||
<text class="stats-value">{{ todayStats.visitors || 0 }}</text>
|
||||
<text class="stats-label">访客数</text>
|
||||
<view class="feature-item" @click="goToProducts('manage')">
|
||||
<view class="feat-icon-wrap"><text class="feat-icon-txt">🏥</text></view>
|
||||
<text class="feat-label">服务管理</text>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<view class="stats-icon-wrap purple">
|
||||
<text class="stats-icon">📈</text>
|
||||
</view>
|
||||
<text class="stats-value">{{ todayStats.conversion || 0 }}%</text>
|
||||
<text class="stats-label">转化率</text>
|
||||
<view class="feature-item" @click="goToStatistics">
|
||||
<view class="feat-icon-wrap"><text class="feat-icon-txt">📊</text></view>
|
||||
<text class="feat-label">数据统计</text>
|
||||
</view>
|
||||
<view class="feature-item" @click="goToInventory">
|
||||
<view class="feat-icon-wrap"><text class="feat-icon-txt">💊</text></view>
|
||||
<text class="feat-label">药械库存</text>
|
||||
</view>
|
||||
<view class="feature-item" @click="goToReviews">
|
||||
<view class="feat-icon-wrap"><text class="feat-icon-txt">⭐</text></view>
|
||||
<text class="feat-label">服务评价</text>
|
||||
</view>
|
||||
<view class="feature-item" @click="goToFinance">
|
||||
<view class="feat-icon-wrap"><text class="feat-icon-txt">💰</text></view>
|
||||
<text class="feat-label">结算补贴</text>
|
||||
</view>
|
||||
<view class="feature-item" @click="goToMembers">
|
||||
<view class="feat-icon-wrap"><text class="feat-icon-txt">👴</text></view>
|
||||
<text class="feat-label">服务对象</text>
|
||||
</view>
|
||||
<view class="feature-item" @click="goToPromotions">
|
||||
<view class="feat-icon-wrap"><text class="feat-icon-txt">🎁</text></view>
|
||||
<text class="feat-label">关怀活动</text>
|
||||
</view>
|
||||
<view class="feature-item" @click="goToSettings">
|
||||
<view class="feat-icon-wrap"><text class="feat-icon-txt">🏢</text></view>
|
||||
<text class="feat-label">机构资料</text>
|
||||
</view>
|
||||
<view class="feature-item" @click="goToHealthManagement">
|
||||
<view class="feat-icon-wrap"><text class="feat-icon-txt">❤️</text></view>
|
||||
<text class="feat-label">健康管理</text>
|
||||
</view>
|
||||
<view class="feature-item" @click="goToAiConsultation">
|
||||
<view class="feat-icon-wrap"><text class="feat-icon-txt">🤖</text></view>
|
||||
<text class="feat-label">AI问诊</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 待处理事项 -->
|
||||
<view class="section-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">🔔 待处理事项</text>
|
||||
</view>
|
||||
<view class="pending-grid">
|
||||
<view class="pending-item" @click="goToOrders('pending')">
|
||||
<view class="pending-icon-wrap orange">
|
||||
<text class="pending-icon">📦</text>
|
||||
</view>
|
||||
<view class="pending-info">
|
||||
<text class="pending-count" v-if="pendingCounts.pending_shipment > 0">{{ pendingCounts.pending_shipment }}</text>
|
||||
<text class="pending-text">待发货</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pending-item" @click="goToOrders('refund')">
|
||||
<view class="pending-icon-wrap red">
|
||||
<text class="pending-icon">↩️</text>
|
||||
</view>
|
||||
<view class="pending-info">
|
||||
<text class="pending-count" v-if="pendingCounts.refund_requests > 0">{{ pendingCounts.refund_requests }}</text>
|
||||
<text class="pending-text">退款</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pending-item" @click="goToInventory">
|
||||
<view class="pending-icon-wrap yellow">
|
||||
<text class="pending-icon">⚠️</text>
|
||||
</view>
|
||||
<view class="pending-info">
|
||||
<text class="pending-count" v-if="pendingCounts.low_stock > 0">{{ pendingCounts.low_stock }}</text>
|
||||
<text class="pending-text">库存预警</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pending-item" @click="goToReviews">
|
||||
<view class="pending-icon-wrap blue">
|
||||
<text class="pending-icon">💬</text>
|
||||
</view>
|
||||
<view class="pending-info">
|
||||
<text class="pending-count" v-if="pendingCounts.pending_reviews > 0">{{ pendingCounts.pending_reviews }}</text>
|
||||
<text class="pending-text">待回复</text>
|
||||
</view>
|
||||
<!-- 待办与通知卡片 -->
|
||||
<view class="wt-card">
|
||||
<view class="wt-card-hd">
|
||||
<text class="wt-card-title">待办与通知</text>
|
||||
<view class="wt-notice-badge-wrap" v-if="noticeBadgeCount > 0">
|
||||
<text class="wt-notice-badge-txt">{{ noticeBadgeCount }} 条待处理</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 常用功能 -->
|
||||
<view class="section-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">🚀 常用功能</text>
|
||||
</view>
|
||||
<view class="shortcuts-grid">
|
||||
<view class="shortcut-item" @click="goToProducts('add')">
|
||||
<view class="shortcut-icon-wrap gradient-blue">
|
||||
<text class="shortcut-icon">➕</text>
|
||||
</view>
|
||||
<text class="shortcut-text">发布商品</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToOrders('all')">
|
||||
<view class="shortcut-icon-wrap gradient-orange">
|
||||
<text class="shortcut-icon">📋</text>
|
||||
</view>
|
||||
<text class="shortcut-text">订单管理</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToProducts('manage')">
|
||||
<view class="shortcut-icon-wrap gradient-green">
|
||||
<text class="shortcut-icon">📦</text>
|
||||
</view>
|
||||
<text class="shortcut-text">商品管理</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToInventory">
|
||||
<view class="shortcut-icon-wrap gradient-purple">
|
||||
<text class="shortcut-icon">📊</text>
|
||||
</view>
|
||||
<text class="shortcut-text">库存管理</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToPromotions">
|
||||
<view class="shortcut-icon-wrap gradient-red">
|
||||
<text class="shortcut-icon">🎯</text>
|
||||
</view>
|
||||
<text class="shortcut-text">营销活动</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToStatistics">
|
||||
<view class="shortcut-icon-wrap gradient-cyan">
|
||||
<text class="shortcut-icon">📈</text>
|
||||
</view>
|
||||
<text class="shortcut-text">数据统计</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToFinance">
|
||||
<view class="shortcut-icon-wrap gradient-yellow">
|
||||
<text class="shortcut-icon">💰</text>
|
||||
</view>
|
||||
<text class="shortcut-text">财务结算</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToMembers">
|
||||
<view class="shortcut-icon-wrap gradient-pink">
|
||||
<text class="shortcut-icon">VIP</text>
|
||||
</view>
|
||||
<text class="shortcut-text">会员管理</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToSettings">
|
||||
<view class="shortcut-icon-wrap gradient-pink">
|
||||
<text class="shortcut-icon">🏪</text>
|
||||
</view>
|
||||
<text class="shortcut-text">店铺设置</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最新订单 -->
|
||||
<view class="section-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">🛒 最新订单</text>
|
||||
<text class="section-more" @click="goToOrders('all')">查看全部 ›</text>
|
||||
</view>
|
||||
<view v-if="recentOrders.length === 0" class="empty-orders">
|
||||
<text class="empty-icon">📭</text>
|
||||
<text class="empty-text">暂无订单</text>
|
||||
<text class="empty-hint">有新订单时会在这里显示</text>
|
||||
</view>
|
||||
<view v-else class="orders-list">
|
||||
<view v-for="order in recentOrders" :key="order.id" class="order-card" @click="goToOrderDetail(order.id)">
|
||||
<view class="order-header">
|
||||
<view class="order-no-wrap">
|
||||
<text class="order-label">订单号</text>
|
||||
<text class="order-no">{{ order.order_no }}</text>
|
||||
<view class="notice-list">
|
||||
<!-- 待上门 -->
|
||||
<view class="notice-item" @click="goToOrders('pending')" v-if="pendingCounts.pending_shipment > 0">
|
||||
<view class="notice-dot dot-orange"></view>
|
||||
<view class="notice-body">
|
||||
<view class="notice-title-row">
|
||||
<text class="notice-title">待上门服务</text>
|
||||
<view class="notice-tag tag-orange"><text>{{ pendingCounts.pending_shipment }}单</text></view>
|
||||
</view>
|
||||
<text class="order-status" :class="getOrderStatusClass(order.order_status)">{{ getOrderStatusText(order.order_status) }}</text>
|
||||
<text class="notice-desc">有用户已预约,请安排服务人员上门</text>
|
||||
</view>
|
||||
<view class="order-goods">
|
||||
<view v-for="item in order.items.slice(0, 3)" :key="item.id" class="goods-item">
|
||||
<image :src="item.image_url || '/static/images/default-product.png'" class="goods-image" mode="aspectFill" />
|
||||
<view class="goods-info">
|
||||
<text class="goods-name">{{ item.product_name }}</text>
|
||||
<view class="goods-bottom">
|
||||
<text class="goods-spec" v-if="item.sku_name">{{ item.sku_name }}</text>
|
||||
<text class="goods-qty">×{{ item.quantity }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 退款售后 -->
|
||||
<view class="notice-item" @click="goToOrders('refund')" v-if="pendingCounts.refund_requests > 0">
|
||||
<view class="notice-dot dot-red"></view>
|
||||
<view class="notice-body">
|
||||
<view class="notice-title-row">
|
||||
<text class="notice-title">取消/售后申请</text>
|
||||
<view class="notice-tag tag-red"><text>{{ pendingCounts.refund_requests }}单</text></view>
|
||||
</view>
|
||||
<text class="notice-desc">用户提交了取消或售后申请,请及时处理</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 库存预警 -->
|
||||
<view class="notice-item" @click="goToInventory" v-if="pendingCounts.low_stock > 0">
|
||||
<view class="notice-dot dot-orange"></view>
|
||||
<view class="notice-body">
|
||||
<view class="notice-title-row">
|
||||
<text class="notice-title">药械库存预警</text>
|
||||
<view class="notice-tag tag-orange"><text>{{ pendingCounts.low_stock }}种</text></view>
|
||||
</view>
|
||||
<text class="notice-desc">部分药品或医疗器械库存不足,请及时补货</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 未读消息 -->
|
||||
<view class="notice-item" @click="goToMessages" v-if="unreadCount > 0">
|
||||
<view class="notice-dot dot-gray"></view>
|
||||
<view class="notice-body">
|
||||
<view class="notice-title-row">
|
||||
<text class="notice-title">咨询消息提醒</text>
|
||||
<view class="notice-tag tag-gray"><text>{{ unreadCount }}条</text></view>
|
||||
</view>
|
||||
<text class="notice-desc">有新的用户咨询或系统消息待查看</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 空状态 -->
|
||||
<view v-if="noticeBadgeCount === 0" class="notice-empty">
|
||||
<text class="notice-empty-txt">暂无待处理事项,服务正常运转中 ✅</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最新服务订单(商品运营风格:图片+名称+指标) -->
|
||||
<view class="wt-card">
|
||||
<view class="wt-card-hd">
|
||||
<text class="wt-card-title">最新服务订单</text>
|
||||
<text class="wt-card-more" @click="goToOrders('all')">查看全部 ›</text>
|
||||
</view>
|
||||
<view v-if="recentOrders.length === 0" class="pi-empty">
|
||||
<text class="pi-empty-txt">暂无服务订单,有新订单时会在这里显示</text>
|
||||
</view>
|
||||
<view v-else class="pi-list">
|
||||
<view
|
||||
v-for="(order, idx) in recentOrders"
|
||||
:key="order.id"
|
||||
class="pi-item"
|
||||
:class="{ 'pi-item-last': idx === recentOrders.length - 1 }"
|
||||
@click="goToOrderDetail(order.id)"
|
||||
>
|
||||
<image
|
||||
:src="order.items.length > 0 ? (order.items[0].image_url || '/static/images/default-product.png') : '/static/images/default-product.png'"
|
||||
class="pi-img"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="pi-info">
|
||||
<text class="pi-name">{{ order.items.length > 0 ? order.items[0].product_name : '服务订单' }}</text>
|
||||
<view class="pi-metrics">
|
||||
<view class="pi-metric">
|
||||
<text class="pi-metric-val">{{ getOrderStatusText(order.order_status) }}</text>
|
||||
<text class="pi-metric-lbl">服务状态</text>
|
||||
</view>
|
||||
<view class="pi-metric">
|
||||
<text class="pi-metric-val">¥{{ order.total_amount.toFixed(2) }}</text>
|
||||
<text class="pi-metric-lbl">服务金额</text>
|
||||
</view>
|
||||
<view class="pi-metric">
|
||||
<text class="pi-metric-val">{{ formatTime(order.created_at) }}</text>
|
||||
<text class="pi-metric-lbl">预约时间</text>
|
||||
</view>
|
||||
<text class="goods-price">¥{{ item.price }}</text>
|
||||
</view>
|
||||
<view v-if="order.items.length > 3" class="goods-more">
|
||||
<text>还有{{ order.items.length - 3 }}件商品</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="order-footer">
|
||||
<text class="order-time">{{ formatTime(order.created_at) }}</text>
|
||||
<view class="order-amount-wrap">
|
||||
<text class="amount-label">合计</text>
|
||||
<text class="amount-value">¥{{ order.total_amount.toFixed(2) }}</text>
|
||||
</view>
|
||||
<view class="pi-action-btn">
|
||||
<text class="pi-action-txt">查看</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部安全区域(需要覆盖自定义 tabbar 高度) -->
|
||||
|
||||
<!-- 底部安全区域(覆盖自定义 tabbar 高度) -->
|
||||
<view class="safe-bottom"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<!-- 商家端自定义 TabBar -->
|
||||
<!-- 机构端自定义 TabBar -->
|
||||
<merchant-tab-bar :current="0"></merchant-tab-bar>
|
||||
</view>
|
||||
</template>
|
||||
@@ -331,6 +331,21 @@
|
||||
pending_reviews: number | null
|
||||
}
|
||||
|
||||
// 经营指标(头部第二行)
|
||||
type BizStatsType = {
|
||||
saleAmount: number | null
|
||||
orderCount: number | null
|
||||
visitorCount: number | null
|
||||
reviewCount: number | null
|
||||
}
|
||||
|
||||
// 订单状态条单项类型
|
||||
type OrderStatusItemType = {
|
||||
key: string
|
||||
label: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { MerchantTabBar },
|
||||
data() {
|
||||
@@ -364,7 +379,16 @@
|
||||
recentOrders: [] as OrderType[],
|
||||
unreadCount: 0,
|
||||
refreshing: false,
|
||||
isPageReady: false
|
||||
isPageReady: false,
|
||||
// 经营指标(头部第二行)
|
||||
bizStats: {
|
||||
saleAmount: null,
|
||||
orderCount: null,
|
||||
visitorCount: null,
|
||||
reviewCount: 0
|
||||
} as BizStatsType,
|
||||
// 订单状态条(由 buildOrderStatusList() 填充)
|
||||
orderStatusList: [] as OrderStatusItemType[]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -372,6 +396,13 @@
|
||||
currentDate(): string {
|
||||
const now = new Date()
|
||||
return `${now.getMonth() + 1}月${now.getDate()}日`
|
||||
},
|
||||
// 待办徽标总数
|
||||
noticeBadgeCount(): number {
|
||||
return (Number(this.pendingCounts.pending_shipment) || 0)
|
||||
+ (Number(this.pendingCounts.refund_requests) || 0)
|
||||
+ (Number(this.pendingCounts.low_stock) || 0)
|
||||
+ (this.unreadCount || 0)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -411,8 +442,11 @@
|
||||
this.startRealtimeSubscription()
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.loadAllData()
|
||||
this.startRealtimeSubscription()
|
||||
// 等待 initMerchantId 完成后再检查,若仍无有效 ID 则不请求
|
||||
if (this.merchantId) {
|
||||
this.loadAllData()
|
||||
this.startRealtimeSubscription()
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
@@ -426,14 +460,27 @@
|
||||
},
|
||||
|
||||
methods: {
|
||||
/** UUID 格式校验,非 UUID 不得用于 Supabase 过滤(否则 PostgREST 400)*/
|
||||
isValidUUID(id: string): boolean {
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)
|
||||
},
|
||||
|
||||
async initMerchantId() {
|
||||
try {
|
||||
const session = supa.getSession()
|
||||
if (session != null && session.user != null) {
|
||||
this.merchantId = session.user.getString('id') || ''
|
||||
const sid = session.user.getString('id') || ''
|
||||
if (sid && this.isValidUUID(sid)) this.merchantId = sid
|
||||
}
|
||||
if (!this.merchantId) {
|
||||
this.merchantId = uni.getStorageSync('user_id') || ''
|
||||
const stored = uni.getStorageSync('user_id') || ''
|
||||
if (stored && this.isValidUUID(stored)) {
|
||||
this.merchantId = stored
|
||||
} else if (stored) {
|
||||
// 测试账号(如 "demo-merchant-001")不是 UUID,跳过 API 请求,进入离线演示模式
|
||||
console.warn('[MerchantIndex] 非 UUID 用户 ID,跳过 Supabase 请求:', stored)
|
||||
this.isPageReady = true
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取商户ID失败:', e)
|
||||
@@ -477,11 +524,14 @@
|
||||
},
|
||||
|
||||
async loadAllData() {
|
||||
// merchantId 为空时直接跳过,避免发出 merchant_id=eq. 的无效请求
|
||||
if (!this.merchantId) { this.isPageReady = true; return }
|
||||
await this.loadMerchantData()
|
||||
await this.loadTodayStats()
|
||||
await this.loadPendingCounts()
|
||||
await this.loadRecentOrders()
|
||||
await this.loadUnreadCount()
|
||||
this.buildOrderStatusList()
|
||||
this.isPageReady = true
|
||||
// 保存缓存
|
||||
try {
|
||||
@@ -508,6 +558,30 @@
|
||||
return value.toFixed(2)
|
||||
},
|
||||
|
||||
/**
|
||||
* 构建订单状态条数据
|
||||
* 数据口径与服务订单页完全对齐:
|
||||
* 待接单 -> order_status=2
|
||||
* 服务中 -> order_status=3
|
||||
* 待评价 -> 暂占位,后期对接评价状态
|
||||
* 退款售后 -> order_status=0
|
||||
*/
|
||||
buildOrderStatusList() {
|
||||
this.orderStatusList = [
|
||||
{ key: 'pending', label: '待接单', count: Number(this.pendingCounts.pending_shipment) || 0 },
|
||||
{ key: 'shipped', label: '服务中', count: 0 },
|
||||
{ key: 'review', label: '待评价', count: Number(this.pendingCounts.pending_reviews) || 0 },
|
||||
{ key: 'refund', label: '退款售后', count: Number(this.pendingCounts.refund_requests) || 0 }
|
||||
] as OrderStatusItemType[]
|
||||
// 同步经营指标
|
||||
this.bizStats = {
|
||||
saleAmount: this.todayStats.sales,
|
||||
orderCount: this.todayStats.orders,
|
||||
visitorCount: this.todayStats.visitors,
|
||||
reviewCount: Number(this.pendingCounts.pending_reviews) || 0
|
||||
}
|
||||
},
|
||||
|
||||
async loadMerchantData() {
|
||||
try {
|
||||
const response = await supa
|
||||
@@ -800,11 +874,11 @@
|
||||
},
|
||||
|
||||
getOrderStatusText(status: number): string {
|
||||
if (status === 1) return '待付款'
|
||||
if (status === 2) return '待发货'
|
||||
if (status === 3) return '已发货'
|
||||
if (status === 1) return '待支付'
|
||||
if (status === 2) return '待接单'
|
||||
if (status === 3) return '服务中'
|
||||
if (status === 4) return '已完成'
|
||||
if (status === 0) return '退款中'
|
||||
if (status === 0) return '取消/售后'
|
||||
return '未知'
|
||||
},
|
||||
|
||||
@@ -866,122 +940,116 @@
|
||||
|
||||
goToOrderDetail(orderId: string) {
|
||||
uni.navigateTo({ url: `/pages/mall/merchant/order-detail?id=${orderId}` })
|
||||
},
|
||||
|
||||
goToHealthManagement() {
|
||||
uni.navigateTo({ url: '/pages/mall/merchant/health-management' })
|
||||
},
|
||||
|
||||
goToAiConsultation() {
|
||||
uni.navigateTo({ url: '/pages/mall/merchant/ai-consultation' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.merchant-container { background-color: #f5f7fa; min-height: 100vh; }
|
||||
/* ===== 容器与滚动 ===== */
|
||||
.merchant-container { background-color: #f4f5f7; min-height: 100vh; }
|
||||
.main-scroll { height: 100vh; }
|
||||
|
||||
.header { position: relative; padding-bottom: 30rpx; }
|
||||
.header-bg { position: absolute; top: 0; left: 0; right: 0; height: 300rpx; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 0 0 40rpx 40rpx; }
|
||||
.header-content { position: relative; padding: 40rpx 30rpx 0; }
|
||||
.shop-info { display: flex; flex-direction: row; align-items: center; }
|
||||
.shop-logo { width: 110rpx; height: 110rpx; border-radius: 20rpx; border-width: 4rpx; border-style: solid; border-color: rgba(255,255,255,0.8); margin-right: 24rpx; background-color: #fff; }
|
||||
.shop-details { flex: 1; }
|
||||
.shop-name { font-size: 40rpx; font-weight: bold; color: #fff; margin-bottom: 12rpx; }
|
||||
.shop-meta { display: flex; flex-direction: row; align-items: center; }
|
||||
.meta-item { display: flex; flex-direction: row; align-items: center; }
|
||||
.meta-icon { font-size: 24rpx; margin-right: 6rpx; }
|
||||
.meta-value { font-size: 26rpx; color: rgba(255,255,255,0.9); }
|
||||
.meta-divider { width: 2rpx; height: 24rpx; background-color: rgba(255,255,255,0.3); margin-left: 20rpx; margin-right: 20rpx; }
|
||||
.header-actions { display: flex; flex-direction: row; }
|
||||
.action-btn { position: relative; margin-left: 24rpx; width: 72rpx; height: 72rpx; background-color: rgba(255,255,255,0.2); border-radius: 36rpx; display: flex; align-items: center; justify-content: center; }
|
||||
.action-icon { font-size: 36rpx; }
|
||||
.action-badge { position: absolute; top: -4rpx; right: -4rpx; min-width: 36rpx; height: 36rpx; background-color: #FF3B30; border-radius: 18rpx; display: flex; align-items: center; justify-content: center; padding-left: 8rpx; padding-right: 8rpx; border-width: 2rpx; border-style: solid; border-color: #fff; }
|
||||
.action-badge-text { font-size: 20rpx; color: #fff; font-weight: bold; }
|
||||
/* ===== 小程序顶部导航(蓝色,医养品牌色) ===== */
|
||||
.mp-tab-navbar { height: calc(88rpx + var(--status-bar-height)); padding-top: var(--status-bar-height); background-color: rgb(66, 121, 240); display: flex; flex-direction: row; align-items: center; justify-content: center; }
|
||||
.mp-tab-title { font-size: 34rpx; font-weight: bold; color: #ffffff; }
|
||||
|
||||
.content-area { padding-left: 24rpx; padding-right: 24rpx; padding-bottom: 30rpx; margin-top: 10rpx; }
|
||||
/* ===== 顶部工作台头部(浅蓝渐变,贴近 mall merchant 风格) ===== */
|
||||
.wt-header { background: linear-gradient(to right, #eef2fe, #ece9fa); padding-bottom: 0; }
|
||||
.wt-header-top { padding: 32rpx 28rpx 24rpx; }
|
||||
.wt-shop-row { display: flex; flex-direction: row; align-items: center; }
|
||||
.wt-shop-logo { width: 100rpx; height: 100rpx; border-radius: 16rpx; border-width: 3rpx; border-style: solid; border-color: rgba(255,255,255,0.7); margin-right: 20rpx; background-color: #fff; flex-shrink: 0; }
|
||||
.wt-shop-info { flex: 1; }
|
||||
.wt-shop-name { font-size: 36rpx; font-weight: bold; color: #1a1f36; margin-bottom: 8rpx; }
|
||||
.wt-shop-meta { display: flex; flex-direction: row; align-items: center; }
|
||||
.wt-meta-txt { font-size: 24rpx; color: #6b7a99; }
|
||||
.wt-meta-sep { font-size: 24rpx; color: #c0c8d8; margin-left: 12rpx; margin-right: 12rpx; }
|
||||
.wt-header-btns { display: flex; flex-direction: row; }
|
||||
.wt-icon-btn { position: relative; margin-left: 20rpx; min-width: 80rpx; height: 80rpx; border-radius: 20rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; padding-left: 16rpx; padding-right: 16rpx; }
|
||||
.wt-icon-txt { font-size: 32rpx; line-height: 1; color: #3a4a6b; }
|
||||
.wt-icon-label { font-size: 20rpx; color: #3a4a6b; margin-top: 4rpx; }
|
||||
.wt-badge { position: absolute; top: -4rpx; right: -4rpx; min-width: 32rpx; height: 32rpx; background-color: #E1251B; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; padding-left: 6rpx; padding-right: 6rpx; border-width: 2rpx; border-style: solid; border-color: #fff; }
|
||||
.wt-badge-txt { font-size: 18rpx; color: #fff; font-weight: bold; }
|
||||
|
||||
.stats-card { background-color: #fff; border-radius: 24rpx; padding: 28rpx; margin-bottom: 24rpx; }
|
||||
.stats-header { margin-bottom: 24rpx; }
|
||||
.stats-title-row { display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
|
||||
.stats-title { font-size: 32rpx; font-weight: bold; color: #333; }
|
||||
.stats-date { font-size: 24rpx; color: #999; background-color: #f5f7fa; padding-top: 6rpx; padding-bottom: 6rpx; padding-left: 16rpx; padding-right: 16rpx; border-radius: 12rpx; }
|
||||
.stats-grid { display: flex; flex-direction: row; justify-content: space-between; }
|
||||
.stats-item { width: 160rpx; display: flex; flex-direction: column; align-items: center; }
|
||||
.stats-icon-wrap { width: 64rpx; height: 64rpx; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; margin-bottom: 12rpx; }
|
||||
.stats-icon-wrap.blue { background-color: #E3F2FD; }
|
||||
.stats-icon-wrap.green { background-color: #E8F5E9; }
|
||||
.stats-icon-wrap.orange { background-color: #FFF3E0; }
|
||||
.stats-icon-wrap.purple { background-color: #F3E5F5; }
|
||||
.stats-icon { font-size: 28rpx; }
|
||||
.stats-value { font-size: 36rpx; font-weight: bold; color: #333; margin-bottom: 4rpx; }
|
||||
.stats-label { font-size: 24rpx; color: #999; }
|
||||
/* 头部双行数据面板(订单状态 + 经营指标) */
|
||||
.wt-data-panel { background-color: #fff; border-radius: 16rpx 16rpx 0 0; padding-top: 8rpx; padding-bottom: 8rpx; }
|
||||
.wt-panel-row { display: flex; flex-direction: row; justify-content: space-between; padding-top: 16rpx; padding-bottom: 16rpx; }
|
||||
.wt-panel-sep { height: 1rpx; background-color: #f0f0f0; margin-left: 20rpx; margin-right: 20rpx; }
|
||||
.wt-panel-item { flex: 1; display: flex; flex-direction: column; align-items: center; }
|
||||
.wt-panel-num-wrap { position: relative; margin-bottom: 6rpx; }
|
||||
.wt-panel-num { font-size: 40rpx; font-weight: bold; color: #333; line-height: 1; }
|
||||
.wt-panel-dot { position: absolute; top: 0; right: -10rpx; width: 14rpx; height: 14rpx; background-color: #E1251B; border-radius: 7rpx; }
|
||||
.wt-panel-lbl { font-size: 22rpx; color: #999; }
|
||||
.wt-panel-biz-val { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 6rpx; }
|
||||
.wt-panel-biz-lbl { font-size: 22rpx; color: #999; }
|
||||
|
||||
.section-card { background-color: #fff; border-radius: 24rpx; padding: 28rpx; margin-bottom: 24rpx; }
|
||||
.section-header { display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 24rpx; }
|
||||
.section-title { font-size: 32rpx; font-weight: bold; color: #333; }
|
||||
.section-more { font-size: 26rpx; color: #007AFF; padding-top: 8rpx; padding-bottom: 8rpx; padding-left: 16rpx; padding-right: 16rpx; background-color: #E3F2FD; border-radius: 12rpx; }
|
||||
/* ===== 内容区 ===== */
|
||||
.content-area { padding: 0 0 30rpx; }
|
||||
|
||||
.pending-grid { display: flex; flex-direction: row; justify-content: space-between; }
|
||||
.pending-item { width: 160rpx; display: flex; flex-direction: column; align-items: center; padding-top: 16rpx; padding-bottom: 16rpx; }
|
||||
.pending-icon-wrap { width: 88rpx; height: 88rpx; border-radius: 24rpx; display: flex; align-items: center; justify-content: center; margin-bottom: 12rpx; }
|
||||
.pending-icon-wrap.orange { background-color: #FFF3E0; }
|
||||
.pending-icon-wrap.red { background-color: #FFEBEE; }
|
||||
.pending-icon-wrap.yellow { background-color: #FFFDE7; }
|
||||
.pending-icon-wrap.blue { background-color: #E3F2FD; }
|
||||
.pending-icon { font-size: 40rpx; }
|
||||
.pending-info { display: flex; flex-direction: column; align-items: center; }
|
||||
.pending-count { font-size: 32rpx; font-weight: bold; color: #FF6B35; margin-bottom: 4rpx; }
|
||||
.pending-text { font-size: 24rpx; color: #666; }
|
||||
/* ===== 通用白卡片(贴边,无圆角,mall merchant 风格) ===== */
|
||||
.wt-card { background-color: #fff; margin-bottom: 16rpx; padding: 24rpx 28rpx; }
|
||||
.wt-card-hd { display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
|
||||
.wt-card-title { font-size: 30rpx; font-weight: bold; color: #333; }
|
||||
.wt-card-more { font-size: 26rpx; color: rgb(66, 121, 240); }
|
||||
|
||||
.shortcuts-grid { display: flex; flex-direction: row; flex-wrap: wrap; }
|
||||
.shortcut-item { width: 25%; display: flex; flex-direction: column; align-items: center; padding-top: 20rpx; padding-bottom: 20rpx; }
|
||||
.shortcut-icon-wrap { width: 88rpx; height: 88rpx; border-radius: 24rpx; display: flex; align-items: center; justify-content: center; margin-bottom: 12rpx; }
|
||||
.shortcut-icon-wrap.gradient-blue { background-color: #667eea; }
|
||||
.shortcut-icon-wrap.gradient-orange { background-color: #f093fb; }
|
||||
.shortcut-icon-wrap.gradient-green { background-color: #4facfe; }
|
||||
.shortcut-icon-wrap.gradient-purple { background-color: #a18cd1; }
|
||||
.shortcut-icon-wrap.gradient-red { background-color: #ff9a9e; }
|
||||
.shortcut-icon-wrap.gradient-cyan { background-color: #a1c4fd; }
|
||||
.shortcut-icon-wrap.gradient-yellow { background-color: #f6d365; }
|
||||
.shortcut-icon-wrap.gradient-pink { background-color: #ffecd2; }
|
||||
.shortcut-icon { font-size: 40rpx; }
|
||||
.shortcut-text { font-size: 24rpx; color: #666; }
|
||||
/* ===== 功能宫格(无背景色,大图标,老年友好) ===== */
|
||||
.feature-grid { display: flex; flex-direction: row; flex-wrap: wrap; }
|
||||
.feature-item { width: 25%; display: flex; flex-direction: column; align-items: center; padding-top: 20rpx; padding-bottom: 20rpx; }
|
||||
.feat-icon-wrap { width: 88rpx; height: 88rpx; display: flex; align-items: center; justify-content: center; margin-bottom: 12rpx; }
|
||||
.feat-icon-txt { font-size: 52rpx; line-height: 1; }
|
||||
.feat-label { font-size: 26rpx; color: #333; text-align: center; }
|
||||
|
||||
.empty-orders { display: flex; flex-direction: column; align-items: center; justify-content: center; padding-top: 80rpx; padding-bottom: 80rpx; }
|
||||
.empty-icon { font-size: 100rpx; margin-bottom: 20rpx; }
|
||||
.empty-text { font-size: 30rpx; color: #666; margin-bottom: 8rpx; }
|
||||
.empty-hint { font-size: 24rpx; color: #999; }
|
||||
.orders-list { display: flex; flex-direction: column; }
|
||||
.order-card { background-color: #f9fafb; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; border-width: 1rpx; border-style: solid; border-color: #eee; }
|
||||
.order-header { display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
|
||||
.order-no-wrap { display: flex; flex-direction: row; align-items: center; }
|
||||
.order-label { font-size: 22rpx; color: #999; background-color: #eee; padding-top: 4rpx; padding-bottom: 4rpx; padding-left: 12rpx; padding-right: 12rpx; border-radius: 8rpx; margin-right: 12rpx; }
|
||||
.order-no { font-size: 26rpx; color: #333; font-weight: 500; }
|
||||
.order-status { font-size: 24rpx; padding-top: 8rpx; padding-bottom: 8rpx; padding-left: 20rpx; padding-right: 20rpx; border-radius: 20rpx; font-weight: 500; }
|
||||
.status-pending { background-color: #FFF3E0; color: #FF9800; }
|
||||
.status-paid { background-color: #E3F2FD; color: #2196F3; }
|
||||
.status-shipped { background-color: #E8F5E9; color: #4CAF50; }
|
||||
.status-completed { background-color: #F3E5F5; color: #9C27B0; }
|
||||
.status-refund { background-color: #FFEBEE; color: #F44336; }
|
||||
.order-goods { margin-bottom: 16rpx; }
|
||||
.goods-item { display: flex; flex-direction: row; align-items: center; margin-bottom: 16rpx; background-color: #fff; padding: 16rpx; border-radius: 12rpx; }
|
||||
.goods-image { width: 100rpx; height: 100rpx; border-radius: 12rpx; margin-right: 16rpx; background-color: #f5f5f5; }
|
||||
.goods-info { flex: 1; }
|
||||
.goods-name { font-size: 28rpx; color: #333; margin-bottom: 8rpx; font-weight: 500; }
|
||||
.goods-bottom { display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
|
||||
.goods-spec { font-size: 22rpx; color: #999; }
|
||||
.goods-qty { font-size: 24rpx; color: #999; }
|
||||
.goods-price { font-size: 28rpx; color: #FF6B35; font-weight: bold; }
|
||||
.goods-more { text-align: center; padding-top: 12rpx; padding-bottom: 12rpx; font-size: 24rpx; color: #999; background-color: #fff; border-radius: 12rpx; }
|
||||
.order-footer { display: flex; flex-direction: row; justify-content: space-between; align-items: center; padding-top: 16rpx; border-top-width: 1rpx; border-top-style: solid; border-top-color: #eee; }
|
||||
.order-time { font-size: 24rpx; color: #999; }
|
||||
.order-amount-wrap { display: flex; flex-direction: row; align-items: center; }
|
||||
.amount-label { font-size: 24rpx; color: #999; margin-right: 8rpx; }
|
||||
.amount-value { font-size: 32rpx; font-weight: bold; color: #FF6B35; }
|
||||
/* ===== 重要通知 ===== */
|
||||
.wt-notice-badge-wrap { background-color: #FFF0F0; border-radius: 20rpx; padding-top: 4rpx; padding-bottom: 4rpx; padding-left: 14rpx; padding-right: 14rpx; }
|
||||
.wt-notice-badge-txt { font-size: 22rpx; color: #E1251B; }
|
||||
.notice-empty { padding-top: 20rpx; padding-bottom: 20rpx; display: flex; align-items: center; justify-content: center; }
|
||||
.notice-empty-txt { font-size: 28rpx; color: #bbb; }
|
||||
.notice-list { display: flex; flex-direction: column; }
|
||||
.notice-item { display: flex; flex-direction: row; align-items: flex-start; padding-top: 16rpx; padding-bottom: 16rpx; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #f5f5f5; }
|
||||
.notice-dot { width: 14rpx; height: 14rpx; border-radius: 7rpx; margin-top: 8rpx; margin-right: 18rpx; flex-shrink: 0; }
|
||||
.dot-red { background-color: #E1251B; }
|
||||
.dot-orange { background-color: #FF7800; }
|
||||
.dot-gray { background-color: #bbb; }
|
||||
.notice-body { flex: 1; }
|
||||
.notice-title-row { display: flex; flex-direction: row; align-items: center; margin-bottom: 6rpx; }
|
||||
.notice-title { font-size: 30rpx; color: #333; font-weight: 500; flex: 1; }
|
||||
.notice-tag { font-size: 22rpx; padding-top: 3rpx; padding-bottom: 3rpx; padding-left: 10rpx; padding-right: 10rpx; border-radius: 8rpx; margin-left: 12rpx; flex-shrink: 0; }
|
||||
.tag-red { background-color: #FFF0F0; color: #E1251B; }
|
||||
.tag-orange { background-color: #FFF7E6; color: #FF7800; }
|
||||
.tag-gray { background-color: #F5F5F5; color: #999; }
|
||||
.notice-desc { font-size: 26rpx; color: #999; line-height: 1.5; }
|
||||
|
||||
.mp-tab-navbar { height: calc(88rpx + var(--status-bar-height)); padding-top: var(--status-bar-height); background-color: #ffffff; display: flex; flex-direction: row; align-items: center; justify-content: center; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #f0f0f0; }
|
||||
.mp-tab-title { font-size: 34rpx; font-weight: bold; color: #333333; }
|
||||
/* ===== 最新订单(商品运营风格) ===== */
|
||||
.pi-empty { padding-top: 20rpx; padding-bottom: 20rpx; display: flex; align-items: center; justify-content: center; }
|
||||
.pi-empty-txt { font-size: 28rpx; color: #bbb; }
|
||||
.pi-list { display: flex; flex-direction: column; }
|
||||
.pi-item { display: flex; flex-direction: row; align-items: center; padding-top: 16rpx; padding-bottom: 16rpx; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #f5f5f5; }
|
||||
.pi-item-last { border-bottom-width: 0; }
|
||||
.pi-img { width: 108rpx; height: 108rpx; border-radius: 12rpx; margin-right: 20rpx; background-color: #f5f5f5; flex-shrink: 0; }
|
||||
.pi-info { flex: 1; }
|
||||
.pi-name { font-size: 30rpx; color: #333; font-weight: 500; margin-bottom: 10rpx; overflow: hidden; }
|
||||
.pi-metrics { display: flex; flex-direction: row; }
|
||||
.pi-metric { flex: 1; display: flex; flex-direction: column; }
|
||||
.pi-metric-val { font-size: 26rpx; font-weight: bold; color: #333; margin-bottom: 2rpx; }
|
||||
.pi-metric-lbl { font-size: 22rpx; color: #999; }
|
||||
.pi-action-btn { background-color: #EFF4FF; border-radius: 10rpx; padding-top: 10rpx; padding-bottom: 10rpx; padding-left: 16rpx; padding-right: 16rpx; flex-shrink: 0; margin-left: 16rpx; }
|
||||
.pi-action-txt { font-size: 24rpx; color: rgb(66, 121, 240); }
|
||||
|
||||
/* ===== 底部安全区 ===== */
|
||||
.safe-bottom { height: 160rpx; }
|
||||
|
||||
/* ===== 骨架屏 ===== */
|
||||
@keyframes ske-pulse { 0% { opacity: 1; } 50% { opacity: 0.45; } 100% { opacity: 1; } }
|
||||
.ske-body { padding: 24rpx; }
|
||||
.ske-card-wrap { background: #fff; border-radius: 24rpx; padding: 28rpx; margin-bottom: 24rpx; }
|
||||
.ske-body { padding: 16rpx 0; }
|
||||
.ske-card-wrap { background: #fff; padding: 28rpx; margin-bottom: 16rpx; }
|
||||
.ske-bar { border-radius: 8rpx; background-color: #e8e8e8; animation: ske-pulse 1.4s ease-in-out infinite; }
|
||||
.ske-icon-sq { width: 64rpx; height: 64rpx; border-radius: 16rpx; background-color: #e8e8e8; margin-bottom: 12rpx; animation: ske-pulse 1.4s ease-in-out infinite; }
|
||||
.ske-grid-row { display: flex; flex-direction: row; flex-wrap: wrap; }
|
||||
@@ -993,6 +1061,12 @@
|
||||
.ske-w30 { width: 30%; } .ske-w40 { width: 40%; } .ske-w60 { width: 60%; } .ske-w70 { width: 70%; }
|
||||
.ske-h20 { height: 20rpx; } .ske-h22 { height: 22rpx; } .ske-h26 { height: 26rpx; } .ske-h28 { height: 28rpx; } .ske-h32 { height: 32rpx; }
|
||||
.ske-mt6 { margin-top: 6rpx; } .ske-mt8 { margin-top: 8rpx; } .ske-mb8 { margin-bottom: 8rpx; } .ske-mb16 { margin-bottom: 16rpx; }
|
||||
|
||||
/* ===== 订单状态样式(order-detail 等页面引用保留) ===== */
|
||||
.status-pending { background-color: #FFF3E0; color: #FF9800; }
|
||||
.status-paid { background-color: #E3F2FD; color: #2196F3; }
|
||||
.status-shipped { background-color: #E8F5E9; color: #4CAF50; }
|
||||
.status-completed { background-color: #F3E5F5; color: #9C27B0; }
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1000,3 +1074,4 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- 商家端 - 库存管理页面 -->
|
||||
<!-- 机构端 - 器械库存管理页面 -->
|
||||
<template>
|
||||
<view class="inventory-page">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
@@ -9,14 +9,21 @@
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<view class="filter-tabs">
|
||||
<view class="filter-tab" :class="{ active: currentFilter === 'all' }" @click="switchFilter('all')">全部</view>
|
||||
<view class="filter-tab" :class="{ active: currentFilter === 'low' }" @click="switchFilter('low')">器械预警</view>
|
||||
<view class="filter-tab" :class="{ active: currentFilter === 'out' }" @click="switchFilter('out')">已售罄</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-bar">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ stats.totalProducts }}</text>
|
||||
<text class="stat-label">商品总数</text>
|
||||
<text class="stat-label">在售服务项目数</text>
|
||||
</view>
|
||||
<view class="stat-item warning">
|
||||
<text class="stat-value">{{ stats.lowStock }}</text>
|
||||
<text class="stat-label">库存预警</text>
|
||||
<text class="stat-label">器械预警</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ stats.outOfStock }}</text>
|
||||
@@ -24,15 +31,11 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-tabs">
|
||||
<view class="filter-tab" :class="{ active: currentFilter === 'all' }" @click="switchFilter('all')">全部</view>
|
||||
<view class="filter-tab" :class="{ active: currentFilter === 'low' }" @click="switchFilter('low')">库存预警</view>
|
||||
<view class="filter-tab" :class="{ active: currentFilter === 'out' }" @click="switchFilter('out')">已售罄</view>
|
||||
</view>
|
||||
|
||||
|
||||
<scroll-view class="inventory-list" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh" @scrolltolower="loadMore">
|
||||
<view v-if="loading && products.length === 0" class="loading-container"><text class="loading-text">加载中...</text></view>
|
||||
<view v-else-if="products.length === 0" class="empty-container"><text class="empty-icon">📊</text><text class="empty-text">暂无商品</text></view>
|
||||
<view v-else-if="products.length === 0" class="empty-container"><text class="empty-icon">📊</text><text class="empty-text">暂无服务项目</text></view>
|
||||
<view v-else>
|
||||
<view v-for="product in products" :key="product.id" class="product-card">
|
||||
<image :src="product.main_image_url || '/static/images/default-product.png'" class="product-image" mode="aspectFill"/>
|
||||
@@ -43,7 +46,7 @@
|
||||
<text class="stock-value" :class="getStockClass(product.total_stock)">{{ product.total_stock }}</text>
|
||||
</view>
|
||||
<view class="warning-info" v-if="product.total_stock <= (product.warning_stock || 10)">
|
||||
<text class="warning-text">库存不足</text>
|
||||
<text class="warning-text">库存/名额不足</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="product-actions">
|
||||
@@ -58,7 +61,7 @@
|
||||
<view class="modal-header"><text class="modal-title">调整库存</text><text class="modal-close" @click="closeStockModal">×</text></view>
|
||||
<view class="modal-body">
|
||||
<view class="form-item">
|
||||
<text class="label">商品</text>
|
||||
<text class="label">服务项目</text>
|
||||
<text class="value">{{ currentProduct?.name }}</text>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
@@ -166,7 +169,7 @@
|
||||
const response = await query.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('加载商品失败:', response.error)
|
||||
console.error('加载服务项目失败:', response.error)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -314,24 +317,33 @@
|
||||
|
||||
<style>
|
||||
.inventory-page { background-color: #f5f5f5; min-height: 100vh; }
|
||||
.stats-bar { display: flex; background-color: #fff; padding: 30rpx 20rpx; margin-bottom: 20rpx; }
|
||||
.stats-bar { display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #fff; padding: 30rpx 20rpx; margin-bottom: 20rpx;
|
||||
}
|
||||
.stat-item { flex: 1; text-align: center; }
|
||||
.stat-value { font-size: 40rpx; font-weight: bold; color: #333; display: block; }
|
||||
.stat-item.warning .stat-value { color: #FF9800; }
|
||||
.stat-label { font-size: 24rpx; color: #999; }
|
||||
.filter-tabs { display: flex; background-color: #fff; padding: 0 20rpx; margin-bottom: 20rpx; }
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #fff;
|
||||
padding: 0 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.filter-tab { flex: 1; text-align: center; padding: 24rpx 0; font-size: 26rpx; color: #666; position: relative; }
|
||||
.filter-tab.active { color: #007AFF; font-weight: bold; }
|
||||
.filter-tab.active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 40rpx; height: 4rpx; background-color: #007AFF; border-radius: 2rpx; }
|
||||
.filter-tab.active { color: rgb(66, 121, 240); font-weight: bold; }
|
||||
.filter-tab.active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 40rpx; height: 4rpx; background-color: rgb(66, 121, 240); border-radius: 2rpx; }
|
||||
.inventory-list { padding: 0 20rpx; height: calc(100vh - 280rpx); }
|
||||
.loading-container, .empty-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 100rpx 0; }
|
||||
.empty-icon { font-size: 100rpx; margin-bottom: 20rpx; }
|
||||
.empty-text, .loading-text { font-size: 28rpx; color: #999; }
|
||||
.product-card { display: flex; align-items: center; background-color: #fff; border-radius: 16rpx; padding: 20rpx; margin-bottom: 16rpx; }
|
||||
.product-card { display: flex; flex-direction: row; align-items: center; background-color: #fff; border-radius: 16rpx; padding: 20rpx; margin-bottom: 16rpx; }
|
||||
.product-image { width: 120rpx; height: 120rpx; border-radius: 8rpx; margin-right: 20rpx; background-color: #f5f5f5; }
|
||||
.product-info { flex: 1; }
|
||||
.product-name { font-size: 28rpx; color: #333; font-weight: 500; display: block; margin-bottom: 10rpx; }
|
||||
.stock-info { display: flex; align-items: center; }
|
||||
.stock-info { display: flex; flex-direction: row; align-items: center; }
|
||||
.stock-label { font-size: 24rpx; color: #999; margin-right: 10rpx; }
|
||||
.stock-value { font-size: 28rpx; font-weight: bold; }
|
||||
.stock-value.normal { color: #4CAF50; }
|
||||
@@ -344,15 +356,15 @@
|
||||
.modal-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
||||
.modal-content { width: 80%; background-color: #fff; border-radius: 16rpx; }
|
||||
.modal-body { padding: 30rpx; }
|
||||
.adjust-type { display: flex; justify-content: space-between; margin-bottom: 30rpx; }
|
||||
.adjust-type { display: flex; flex-direction: row; justify-content: space-between; margin-bottom: 30rpx; }
|
||||
.type-btn { flex: 1; height: 64rpx; line-height: 64rpx; text-align: center; font-size: 24rpx; background-color: #f5f5f5; color: #666; margin: 0 10rpx; border-radius: 32rpx; border: 1rpx solid #eee; }
|
||||
.type-btn.active { background-color: #E3F2FD; color: #007AFF; border-color: #007AFF; }
|
||||
.type-btn.active { background-color: #E3F2FD; color: rgb(66, 121, 240); border-color: rgb(66, 121, 240); }
|
||||
.form-item { margin-bottom: 20rpx; }
|
||||
.form-item .label { font-size: 26rpx; color: #999; display: block; margin-bottom: 10rpx; }
|
||||
.form-item .value { font-size: 28rpx; color: #333; }
|
||||
.input { height: 72rpx; border: 1rpx solid #e5e5e5; border-radius: 8rpx; padding: 0 20rpx; font-size: 28rpx; }
|
||||
.modal-footer { display: flex; border-top: 1rpx solid #f5f5f5; }
|
||||
.modal-footer { display: flex; flex-direction: row; border-top: 1rpx solid #f5f5f5; }
|
||||
.modal-btn { flex: 1; height: 88rpx; line-height: 88rpx; text-align: center; font-size: 28rpx; }
|
||||
.modal-btn.cancel { color: #666; border-right: 1rpx solid #f5f5f5; }
|
||||
.modal-btn.confirm { color: #007AFF; font-weight: bold; }
|
||||
.modal-btn.confirm { color: rgb(66, 121, 240); font-weight: bold; }
|
||||
</style>
|
||||
|
||||
@@ -9,21 +9,21 @@
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<view class="tabs">
|
||||
<view class="tab" :class="{ active: activeTab === 0 }" @click="activeTab = 0">等级设置</view>
|
||||
<view class="tab" :class="{ active: activeTab === 1 }" @click="activeTab = 1">客户列表</view>
|
||||
<view class="tab" :class="{ active: activeTab === 0 }" @click="activeTab = 0">关怀等级设置</view>
|
||||
<view class="tab" :class="{ active: activeTab === 1 }" @click="activeTab = 1">服务对象列表</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="list-container" v-if="activeTab === 0">
|
||||
<view class="section-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">等级配置</text>
|
||||
<text class="card-title">关怀等级配置</text>
|
||||
<text class="add-btn" @click="showAddLevel = true">+ 添加等级</text>
|
||||
</view>
|
||||
<view class="level-list">
|
||||
<view v-for="level in levels" :key="level.id" class="level-item">
|
||||
<view class="level-info">
|
||||
<text class="level-name">{{ level.name }}</text>
|
||||
<text class="level-rate">{{ (level.discount_rate * 10).toFixed(1) }}折</text>
|
||||
<text class="level-rate">优惠比例: {{ (level.discount_rate * 10).toFixed(1) }}折</text>
|
||||
</view>
|
||||
<view class="level-actions">
|
||||
<text class="action-edit" @click="editLevel(level)">编辑</text>
|
||||
@@ -36,20 +36,20 @@
|
||||
|
||||
<scroll-view scroll-y class="list-container" v-if="activeTab === 1">
|
||||
<view class="user-list">
|
||||
<view v-if="users.length === 0" class="empty-tip">暂无注册客户</view>
|
||||
<view v-if="users.length === 0" class="empty-tip">暂无服务对象记录</view>
|
||||
<view v-for="user in users" :key="user.id" class="user-item">
|
||||
<image :src="user.avatar_url || '/static/images/default-avatar.png'" class="user-avatar" />
|
||||
<view class="user-info">
|
||||
<view class="user-title-row">
|
||||
<text class="user-name">{{ user.nickname || user.username || '未设置昵称' }}</text>
|
||||
<text class="user-name">{{ user.nickname || user.username || '未设置姓名' }}</text>
|
||||
<view class="user-tier-tag" v-if="user.tier_name">{{ user.tier_name }}</view>
|
||||
</view>
|
||||
<text class="user-email" v-if="user.email">{{ user.email }}</text>
|
||||
<text class="user-phone">{{ user.phone || '未绑定手机' }}</text>
|
||||
<text class="user-phone">{{ user.phone || '未绑定手机为服务对象' }}</text>
|
||||
</view>
|
||||
<view class="user-actions">
|
||||
<text class="action-set" @click="showSetTier(user)">设置VIP</text>
|
||||
<text class="action-set discount-btn" @click="goToExclusive(user)">专属折扣</text>
|
||||
<view class="user-actions">
|
||||
<text class="action-set" @click="showSetTier(user)">设置关怀等级</text>
|
||||
<text class="action-set discount-btn" @click="goToExclusive(user)">专属补贴</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -64,8 +64,8 @@
|
||||
<input class="input" v-model="currentLevel.name" placeholder="请输入名称" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">折扣率 (0-1)</text>
|
||||
<input class="input" type="digit" v-model="currentLevel.discount_rate" placeholder="如0.85表示85折" />
|
||||
<text class="label">优惠比例 (0-1)</text>
|
||||
<input class="input" type="digit" v-model="currentLevel.discount_rate" placeholder="如0.85表示八五折专属优惠" />
|
||||
</view>
|
||||
<view class="modal-btns">
|
||||
<text class="btn cancel" @click="showEditModal = false">取消</text>
|
||||
@@ -77,7 +77,7 @@
|
||||
<!-- 设置用户等级弹窗 -->
|
||||
<view class="modal" v-if="showTierModal">
|
||||
<view class="modal-content">
|
||||
<view class="modal-title">设置会员等级</view>
|
||||
<view class="modal-title">设置关怀等级</view>
|
||||
<view class="tier-options">
|
||||
<view v-for="level in levels" :key="level.id"
|
||||
class="tier-option"
|
||||
@@ -468,62 +468,65 @@
|
||||
|
||||
<style>
|
||||
.members-page { background-color: #f8f9fa; min-height: 100vh; }
|
||||
.tabs { display: flex; background: #fff; padding: 20rpx 0; border-bottom: 1rpx solid #eee; }
|
||||
/* ===== tabs 横排 ===== */
|
||||
.tabs { display: flex; flex-direction: row; background: #fff; padding: 20rpx 0; border-bottom: 1rpx solid #eee; }
|
||||
.tab { flex: 1; text-align: center; font-size: 28rpx; color: #666; }
|
||||
.tab.active { color: #007AFF; font-weight: bold; }
|
||||
|
||||
.tab.active { color: rgb(66, 121, 240); font-weight: bold; }
|
||||
|
||||
.list-container { padding: 20rpx; }
|
||||
.section-card { background: #fff; border-radius: 16rpx; padding: 30rpx; }
|
||||
.card-header { display: flex; justify-content: space-between; margin-bottom: 30rpx; align-items: center; }
|
||||
/* ===== 卡片头部横排 ===== */
|
||||
.card-header { display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 30rpx; }
|
||||
.card-title { font-size: 32rpx; font-weight: bold; }
|
||||
.add-btn { color: #007AFF; font-size: 26rpx; }
|
||||
|
||||
.level-item { display: flex; justify-content: space-between; border-bottom: 1rpx solid #f5f5f5; padding: 20rpx 0; }
|
||||
.level-name { font-size: 30rpx; display: block; }
|
||||
.level-rate { font-size: 24rpx; color: #FF9500; }
|
||||
.level-actions .text { font-size: 24rpx; margin-left: 20rpx; }
|
||||
.action-edit { color: #007AFF; margin-right: 20rpx; }
|
||||
.action-del { color: #FF3B30; }
|
||||
.add-btn { color: rgb(66, 121, 240); font-size: 26rpx; }
|
||||
|
||||
.search-bar { display: flex; padding: 20rpx; background: #fff; margin-bottom: 20rpx; border-radius: 12rpx; }
|
||||
/* ===== 等级列表项横排(左名称+折扣 / 右操作)===== */
|
||||
.level-item { display: flex; flex-direction: row; justify-content: space-between; align-items: center; border-bottom: 1rpx solid #f5f5f5; padding: 20rpx 0; }
|
||||
.level-info { display: flex; flex-direction: column; flex: 1; }
|
||||
.level-name { font-size: 30rpx; color: #333; margin-bottom: 4rpx; }
|
||||
.level-rate { font-size: 24rpx; color: #FF9500; }
|
||||
.level-actions { display: flex; flex-direction: row; align-items: center; }
|
||||
.action-edit { color: rgb(66, 121, 240); font-size: 26rpx; margin-right: 24rpx; }
|
||||
.action-del { color: #FF3B30; font-size: 26rpx; }
|
||||
|
||||
.search-bar { display: flex; flex-direction: row; align-items: center; padding: 20rpx; background: #fff; margin-bottom: 20rpx; border-radius: 12rpx; }
|
||||
.search-input { flex: 1; height: 72rpx; background: #f5f5f5; border-radius: 36rpx; padding: 0 30rpx; font-size: 26rpx; }
|
||||
.search-btn { margin-left: 20rpx; color: #007AFF; line-height: 72rpx; font-size: 28rpx; }
|
||||
|
||||
.user-item { display: flex; align-items: center; background: #fff; padding: 24rpx; border-radius: 16rpx; margin-bottom: 20rpx; }
|
||||
.user-avatar { width: 90rpx; height: 90rpx; border-radius: 45rpx; background: #eee; }
|
||||
.user-info { flex: 1; margin-left: 24rpx; }
|
||||
.user-name { font-size: 30rpx; font-weight: bold; display: block; }
|
||||
.search-btn { margin-left: 20rpx; color: rgb(66, 121, 240); line-height: 72rpx; font-size: 28rpx; }
|
||||
|
||||
/* ===== 用户列表项横排(左头像 / 中信息 / 右操作)===== */
|
||||
.user-item { display: flex; flex-direction: row; align-items: center; background: #fff; padding: 24rpx; border-radius: 16rpx; margin-bottom: 20rpx; }
|
||||
.user-avatar { width: 90rpx; height: 90rpx; border-radius: 45rpx; background: #eee; flex-shrink: 0; }
|
||||
.user-info { flex: 1; margin-left: 24rpx; display: flex; flex-direction: column; }
|
||||
/* 姓名行:昵称 + 等级标签横排 */
|
||||
.user-title-row { display: flex; flex-direction: row; align-items: center; flex-wrap: wrap; margin-bottom: 4rpx; }
|
||||
.user-name { font-size: 30rpx; font-weight: bold; color: #333; margin-right: 10rpx; }
|
||||
.user-email { font-size: 22rpx; color: #999; margin-bottom: 4rpx; }
|
||||
.user-phone { font-size: 24rpx; color: #999; }
|
||||
.user-tier-tag { display: inline-block; background: #FF9500; color: #fff; font-size: 20rpx; padding: 2rpx 12rpx; border-radius: 4rpx; margin-top: 8rpx; }
|
||||
.action-set {
|
||||
font-size: 24rpx;
|
||||
color: #2196F3;
|
||||
padding: 10rpx 20rpx;
|
||||
background-color: #E3F2FD;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
.action-set.discount-btn {
|
||||
color: #FF9800;
|
||||
background-color: #FFF8E1;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
.user-tier-tag { background: #FF9500; color: #fff; font-size: 20rpx; padding: 2rpx 12rpx; border-radius: 4rpx; }
|
||||
/* ===== 操作按钮区横排 ===== */
|
||||
.user-actions { display: flex; flex-direction: row; align-items: center; flex-shrink: 0; margin-left: 16rpx; }
|
||||
.action-set { font-size: 24rpx; color: rgb(66, 121, 240); padding: 10rpx 16rpx; background-color: #e8f0fe; border-radius: 8rpx; }
|
||||
.action-set.discount-btn { color: #FF9800; background-color: #FFF8E1; margin-left: 12rpx; }
|
||||
|
||||
/* 弹窗样式 */
|
||||
.modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 999; }
|
||||
.modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; flex-direction: row; align-items: center; justify-content: center; z-index: 999; }
|
||||
.modal-content { background: #fff; width: 600rpx; border-radius: 24rpx; padding: 40rpx; }
|
||||
.modal-title { text-align: center; font-size: 34rpx; font-weight: bold; margin-bottom: 30rpx; }
|
||||
.form-item { margin-bottom: 30rpx; }
|
||||
.label { font-size: 26rpx; color: #666; margin-bottom: 12rpx; display: block; }
|
||||
.input { background: #f5f5f5; height: 80rpx; border-radius: 12rpx; padding: 0 20rpx; font-size: 28rpx; }
|
||||
.modal-btns { display: flex; justify-content: flex-end; margin-top: 40rpx; }
|
||||
/* ===== modal 按钮横排 ===== */
|
||||
.modal-btns { display: flex; flex-direction: row; justify-content: flex-end; margin-top: 40rpx; }
|
||||
.btn { padding: 16rpx 40rpx; border-radius: 12rpx; font-size: 28rpx; margin-left: 20rpx; }
|
||||
.btn.cancel { background: #eee; color: #666; }
|
||||
.btn.confirm { background: #007AFF; color: #fff; }
|
||||
|
||||
.tier-options { display: flex; flex-wrap: wrap; }
|
||||
.btn.confirm { background: rgb(66, 121, 240); color: #fff; }
|
||||
|
||||
/* ===== 等级选择横向排列 ===== */
|
||||
.tier-options { display: flex; flex-direction: row; flex-wrap: wrap; }
|
||||
.tier-option { padding: 16rpx 30rpx; background: #f5f5f5; margin: 10rpx; border-radius: 36rpx; font-size: 26rpx; }
|
||||
.tier-option.selected { background: #007AFF; color: #fff; }
|
||||
.tier-option.selected { background: rgb(66, 121, 240); color: #fff; }
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
<!-- 商家端 - 商品管理详情页 -->
|
||||
<!-- 机构端 - 服务/商品详情页面 -->
|
||||
<template>
|
||||
<view class="product-manage-detail">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
@@ -9,10 +9,10 @@
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- 商品基本信息 -->
|
||||
<!-- 服务基本信息 -->
|
||||
<view class="product-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">商品信息</text>
|
||||
<text class="section-title">服务项目信息</text>
|
||||
<text class="edit-btn" @click="editProduct">编辑</text>
|
||||
</view>
|
||||
|
||||
@@ -27,33 +27,33 @@
|
||||
|
||||
<view class="product-info">
|
||||
<view class="info-item">
|
||||
<text class="info-label">商品名称</text>
|
||||
<text class="info-label">服务项目名称</text>
|
||||
<text class="info-value">{{ product.name }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">商品描述</text>
|
||||
<text class="info-label">服务简介</text>
|
||||
<text class="info-value">{{ product.description || '暂无描述' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">商品价格</text>
|
||||
<text class="info-label">参考价格</text>
|
||||
<text class="info-value price">¥{{ product.price }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">原价</text>
|
||||
<text class="info-label">门市价</text>
|
||||
<text class="info-value">¥{{ product.original_price || '未设置' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">库存数量</text>
|
||||
<text class="info-value">{{ product.stock }}件</text>
|
||||
<text class="info-label">可预约名额</text>
|
||||
<text class="info-value">{{ product.stock }}人次</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">销量</text>
|
||||
<text class="info-value">{{ product.sales }}件</text>
|
||||
<text class="info-label">服务次数</text>
|
||||
<text class="info-value">{{ product.sales }}次</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">商品状态</text>
|
||||
<text class="info-label">服务状态</text>
|
||||
<text class="info-value" :class="{ 'status-on': product.status === 1, 'status-off': product.status === 2 || product.status === 0 }">
|
||||
{{ product.status === 1 ? '上架' : (product.status === 2 || product.status === 0 ? '下架' : '其他') }}
|
||||
{{ product.status === 1 ? '服务中' : (product.status === 2 || product.status === 0 ? '已暂停' : '待上线') }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -62,7 +62,7 @@
|
||||
<!-- SKU规格管理 -->
|
||||
<view class="sku-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">规格管理</text>
|
||||
<text class="section-title">服务规格/套餐</text>
|
||||
<text class="add-btn" @click="addSku">添加规格</text>
|
||||
</view>
|
||||
|
||||
@@ -89,25 +89,25 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 销售数据 -->
|
||||
<!-- 服务数据 -->
|
||||
<view class="sales-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">销售数据</text>
|
||||
<text class="section-title">服务数据</text>
|
||||
<text class="view-detail" @click="viewSalesDetail">查看详情</text>
|
||||
</view>
|
||||
|
||||
<view class="sales-stats">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ salesData.today_sales }}</text>
|
||||
<text class="stat-label">今日销量</text>
|
||||
<text class="stat-label">今日服务次数</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ salesData.week_sales }}</text>
|
||||
<text class="stat-label">本周销量</text>
|
||||
<text class="stat-label">本周服务次数</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ salesData.month_sales }}</text>
|
||||
<text class="stat-label">本月销量</text>
|
||||
<text class="stat-label">本月服务次数</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">¥{{ salesData.total_revenue }}</text>
|
||||
@@ -119,7 +119,7 @@
|
||||
<!-- 评价管理 -->
|
||||
<view class="review-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">商品评价</text>
|
||||
<text class="section-title">服务评价</text>
|
||||
<text class="view-all" @click="viewAllReviews">查看全部</text>
|
||||
</view>
|
||||
|
||||
@@ -150,10 +150,10 @@
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-buttons">
|
||||
<button class="action-btn primary" @click="toggleProductStatus">
|
||||
{{ product.status === 1 ? '下架商品' : '上架商品' }}
|
||||
{{ product.status === 1 ? '暂停服务' : '开启服务' }}
|
||||
</button>
|
||||
<button class="action-btn secondary" @click="editProduct">编辑商品</button>
|
||||
<button class="action-btn danger" @click="deleteProduct">删除商品</button>
|
||||
<button class="action-btn secondary" @click="editProduct">编辑服务</button>
|
||||
<button class="action-btn danger" @click="deleteProduct">删除</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -278,16 +278,16 @@ export default {
|
||||
this.recentReviews = [
|
||||
{
|
||||
id: 'review_001',
|
||||
user_name: '用户***123',
|
||||
user_name: '家属***123',
|
||||
rating: 5,
|
||||
content: '商品质量很好,物流也很快,满意!',
|
||||
content: '服务人员非常专业,我父亲很满意!',
|
||||
created_at: '2024-01-14 15:30:00'
|
||||
},
|
||||
{
|
||||
id: 'review_002',
|
||||
user_name: '用户***456',
|
||||
user_name: '家属***456',
|
||||
rating: 4,
|
||||
content: '整体不错,就是包装有点简单。',
|
||||
content: '整体服务不错,希望以后能计划性更强一些。',
|
||||
created_at: '2024-01-13 09:20:00'
|
||||
}
|
||||
]
|
||||
@@ -366,11 +366,11 @@ export default {
|
||||
|
||||
toggleProductStatus() {
|
||||
const newStatus = this.product.status === 1 ? 0 : 1
|
||||
const actionText = newStatus === 1 ? '上架' : '下架'
|
||||
const actionText = newStatus === 1 ? '开启服务' : '暂停服务'
|
||||
|
||||
uni.showModal({
|
||||
title: `确认${actionText}`,
|
||||
content: `确定要${actionText}这个商品吗?`,
|
||||
content: `确定要${actionText}该服务项目吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.product.status = newStatus
|
||||
@@ -386,7 +386,7 @@ export default {
|
||||
deleteProduct() {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: '删除后将无法恢复,确定要删除这个商品吗?',
|
||||
content: '删除后将无法恢复,确定要删除该服务项目吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showToast({
|
||||
@@ -419,6 +419,7 @@ export default {
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
@@ -427,14 +428,16 @@ export default {
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
padding-left: 16rpx;
|
||||
border-left: 6rpx solid rgb(66, 121, 240);
|
||||
}
|
||||
|
||||
.edit-btn, .add-btn, .view-detail, .view-all {
|
||||
font-size: 26rpx;
|
||||
color: #007aff;
|
||||
color: rgb(66, 121, 240);
|
||||
}
|
||||
|
||||
.product-images {
|
||||
@@ -443,6 +446,7 @@ export default {
|
||||
|
||||
.image-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
@@ -462,6 +466,7 @@ export default {
|
||||
.add-image {
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 48rpx;
|
||||
@@ -475,6 +480,7 @@ export default {
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
@@ -484,7 +490,7 @@ export default {
|
||||
.info-label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
width: 150rpx;
|
||||
width: 180rpx;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
@@ -495,7 +501,7 @@ export default {
|
||||
}
|
||||
|
||||
.info-value.price {
|
||||
color: #ff4444;
|
||||
color: rgb(225, 37, 27);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -504,7 +510,7 @@ export default {
|
||||
}
|
||||
|
||||
.status-off {
|
||||
color: #ff4444 !important;
|
||||
color: #999 !important;
|
||||
}
|
||||
|
||||
.empty-sku {
|
||||
@@ -540,13 +546,14 @@ export default {
|
||||
|
||||
.sku-details {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.sku-price {
|
||||
font-size: 26rpx;
|
||||
color: #ff4444;
|
||||
color: rgb(225, 37, 27);
|
||||
font-weight: bold;
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
@@ -563,6 +570,7 @@ export default {
|
||||
|
||||
.sku-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 30rpx;
|
||||
}
|
||||
|
||||
@@ -573,37 +581,38 @@ export default {
|
||||
}
|
||||
|
||||
.action-btn.edit {
|
||||
background-color: #e3f2fd;
|
||||
color: #007aff;
|
||||
background-color: #e8f0fe;
|
||||
color: rgb(66, 121, 240);
|
||||
}
|
||||
|
||||
.action-btn.delete {
|
||||
background-color: #ffebee;
|
||||
color: #ff4444;
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.sales-stats {
|
||||
display: flex;
|
||||
gap: 30rpx;
|
||||
flex-direction: row;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 30rpx 0;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 10rpx;
|
||||
background: linear-gradient(135deg, #eef2fe, #ece9fa);
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
color: rgb(66, 121, 240);
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
@@ -613,6 +622,7 @@ export default {
|
||||
|
||||
.rating-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
@@ -625,6 +635,7 @@ export default {
|
||||
|
||||
.rating-stars {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.star {
|
||||
@@ -652,6 +663,7 @@ export default {
|
||||
|
||||
.review-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15rpx;
|
||||
@@ -664,6 +676,7 @@ export default {
|
||||
|
||||
.review-rating {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.review-content {
|
||||
@@ -685,8 +698,9 @@ export default {
|
||||
right: 0;
|
||||
background-color: #fff;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.08);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
@@ -699,17 +713,17 @@ export default {
|
||||
}
|
||||
|
||||
.action-buttons .action-btn.primary {
|
||||
background-color: #4caf50;
|
||||
background-color: rgb(66, 121, 240);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-buttons .action-btn.secondary {
|
||||
background-color: #ffa726;
|
||||
color: #fff;
|
||||
background-color: #E8F0FE;
|
||||
color: rgb(66, 121, 240);
|
||||
}
|
||||
|
||||
.action-buttons .action-btn.danger {
|
||||
background-color: #ff4444;
|
||||
color: #fff;
|
||||
background-color: #ffebee;
|
||||
color: #f44336;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- 商家端 - 商品编辑页面 -->
|
||||
<!-- 机构端 - 服务/商品编辑页面 -->
|
||||
<template>
|
||||
<view class="product-edit-page">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
@@ -9,26 +9,26 @@
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- 商品基本信息 -->
|
||||
<!-- 服务基本信息 -->
|
||||
<view class="section">
|
||||
<view class="section-title">基本信息</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">商品名称 *</text>
|
||||
<text class="label">服务名称 *</text>
|
||||
<input
|
||||
class="input"
|
||||
v-model="product.name"
|
||||
placeholder="请输入商品名称"
|
||||
placeholder="请输入服务名称"
|
||||
maxlength="100"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">商品副标题</text>
|
||||
<text class="label">服务副标题</text>
|
||||
<input
|
||||
class="input"
|
||||
v-model="product.subtitle"
|
||||
placeholder="请输入商品副标题"
|
||||
placeholder="请输入服务副标题"
|
||||
maxlength="200"
|
||||
/>
|
||||
</view>
|
||||
@@ -66,7 +66,7 @@
|
||||
|
||||
<!-- 商品图片 -->
|
||||
<view class="section">
|
||||
<view class="section-title">商品图片</view>
|
||||
<view class="section-title">展示图片</view>
|
||||
|
||||
<view class="image-section">
|
||||
<text class="label">主图 *</text>
|
||||
@@ -101,7 +101,7 @@
|
||||
<view class="section-title">价格库存</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">销售价 *</text>
|
||||
<text class="label">参考价 *</text>
|
||||
<view class="price-input">
|
||||
<text class="unit">¥</text>
|
||||
<input
|
||||
@@ -114,7 +114,7 @@
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">市场价</text>
|
||||
<text class="label">门市价</text>
|
||||
<view class="price-input">
|
||||
<text class="unit">¥</text>
|
||||
<input
|
||||
@@ -127,7 +127,7 @@
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">成本价</text>
|
||||
<text class="label">结算成本</text>
|
||||
<view class="price-input">
|
||||
<text class="unit">¥</text>
|
||||
<input
|
||||
@@ -140,12 +140,12 @@
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">VIP独立折扣</text>
|
||||
<text class="label">长者关怀价/专属补贴</text>
|
||||
<switch :checked="product.is_vip_discount" @change="e => { product.is_vip_discount = e.detail.value as boolean }" />
|
||||
</view>
|
||||
|
||||
<view class="form-item" v-if="product.is_vip_discount">
|
||||
<text class="label">VIP折扣率</text>
|
||||
<text class="label">关怀价折扣率</text>
|
||||
<input
|
||||
class="input"
|
||||
type="digit"
|
||||
@@ -155,7 +155,7 @@
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">总库存 *</text>
|
||||
<text class="label">库存数量/可预约名额 *</text>
|
||||
<input
|
||||
class="input"
|
||||
type="number"
|
||||
@@ -165,7 +165,7 @@
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">库存预警</text>
|
||||
<text class="label">器械库存预警</text>
|
||||
<input
|
||||
class="input"
|
||||
type="number"
|
||||
@@ -177,8 +177,8 @@
|
||||
|
||||
<!-- 会员阶梯价 -->
|
||||
<view class="section">
|
||||
<view class="section-title">会员等级价格 (选填)</view>
|
||||
<view class="section-desc">若不填写则按照商品销售价或默认折扣计算</view>
|
||||
<view class="section-title">长者关怀价/专属补贴价 (选填)</view>
|
||||
<view class="section-desc">若不填写则按照服务参考价或默认折扣计算</view>
|
||||
|
||||
<view v-for="(level, index) in memberLevels" :key="index" class="form-item">
|
||||
<text class="label">{{ level.name }}价格</text>
|
||||
@@ -188,15 +188,15 @@
|
||||
class="input"
|
||||
v-model="level.price"
|
||||
type="digit"
|
||||
placeholder="专属折扣价"
|
||||
placeholder="长者关怀价"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品属性 -->
|
||||
<!-- 服务属性 -->
|
||||
<view class="section">
|
||||
<view class="section-title">商品属性</view>
|
||||
<view class="section-title">服务属性</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">商品单位</text>
|
||||
@@ -208,43 +208,43 @@
|
||||
</view>
|
||||
|
||||
<view class="switch-item">
|
||||
<text class="label">热卖商品</text>
|
||||
<text class="label">热门服务</text>
|
||||
<switch
|
||||
:checked="product.is_hot"
|
||||
@change="product.is_hot = !product.is_hot"
|
||||
color="#007AFF"
|
||||
color="rgb(66, 121, 240)"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="switch-item">
|
||||
<text class="label">新品上架</text>
|
||||
<text class="label">新增服务</text>
|
||||
<switch
|
||||
:checked="product.is_new"
|
||||
@change="product.is_new = !product.is_new"
|
||||
color="#007AFF"
|
||||
color="rgb(66, 121, 240)"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="switch-item">
|
||||
<text class="label">推荐商品</text>
|
||||
<text class="label">推荐服务</text>
|
||||
<switch
|
||||
:checked="product.is_featured"
|
||||
@change="product.is_featured = !product.is_featured"
|
||||
color="#007AFF"
|
||||
color="rgb(66, 121, 240)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品详情 -->
|
||||
<!-- 详情说明 -->
|
||||
<view class="section">
|
||||
<view class="section-title">商品详情</view>
|
||||
<view class="section-title">详情说明</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">商品描述</text>
|
||||
<text class="label">服务描述</text>
|
||||
<textarea
|
||||
class="textarea"
|
||||
v-model="product.description"
|
||||
placeholder="请输入商品详细描述"
|
||||
placeholder="请输入服务详细说明"
|
||||
:maxlength="2000"
|
||||
/>
|
||||
</view>
|
||||
@@ -253,7 +253,7 @@
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-bar">
|
||||
<view class="submit-btn primary" @click="saveProduct">
|
||||
{{ isEdit ? '保存修改' : '发布商品' }}
|
||||
{{ isEdit ? '保存修改' : '发布服务项目' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -344,10 +344,10 @@
|
||||
if (productId && productId !== '') {
|
||||
this.productId = productId
|
||||
this.isEdit = true
|
||||
uni.setNavigationBarTitle({ title: '编辑商品' })
|
||||
uni.setNavigationBarTitle({ title: '编辑服务' })
|
||||
this.loadProductDetail(productId)
|
||||
} else {
|
||||
uni.setNavigationBarTitle({ title: '添加商品' })
|
||||
uni.setNavigationBarTitle({ title: '发布服务' })
|
||||
}
|
||||
this.initMerchantId()
|
||||
this.loadCategories()
|
||||
@@ -450,21 +450,36 @@
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('获取分类失败:', response.error)
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as any[]
|
||||
if (rawData == null) return
|
||||
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as any
|
||||
this.categories.push({
|
||||
id: item['id'] != null ? String(item['id']) : '',
|
||||
name: item['name'] != null ? String(item['name']) : ''
|
||||
} as CategoryType)
|
||||
if (rawData != null && rawData.length > 0) {
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as any
|
||||
this.categories.push({
|
||||
id: item['id'] != null ? String(item['id']) : '',
|
||||
name: item['name'] != null ? String(item['name']) : ''
|
||||
} as CategoryType)
|
||||
}
|
||||
} else {
|
||||
// 演示版默认医养分类
|
||||
this.categories = [
|
||||
{ id: 'med', name: '医疗服务' },
|
||||
{ id: 'drug', name: '药品器械' },
|
||||
{ id: 'care', name: '居家护理' },
|
||||
{ id: 'life', name: '生活服务' },
|
||||
{ id: 'health', name: '健康管理' }
|
||||
] as CategoryType[]
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取分类异常:', e)
|
||||
this.categories = [
|
||||
{ id: 'med', name: '医疗服务' },
|
||||
{ id: 'drug', name: '药品器械' },
|
||||
{ id: 'care', name: '居家护理' },
|
||||
{ id: 'life', name: '生活服务' },
|
||||
{ id: 'health', name: '健康管理' }
|
||||
] as CategoryType[]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -833,6 +848,8 @@
|
||||
margin-bottom: 30rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
padding-left: 16rpx;
|
||||
border-left: 6rpx solid rgb(66, 121, 240);
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
@@ -863,6 +880,7 @@
|
||||
|
||||
.price-input {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border: 1rpx solid #e5e5e5;
|
||||
border-radius: 8rpx;
|
||||
@@ -887,6 +905,7 @@
|
||||
border-radius: 8rpx;
|
||||
padding: 0 20rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -897,6 +916,7 @@
|
||||
|
||||
.switch-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
@@ -923,6 +943,7 @@
|
||||
|
||||
.image-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
@@ -950,6 +971,7 @@
|
||||
height: 100%;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 60rpx;
|
||||
@@ -969,6 +991,7 @@
|
||||
font-size: 28rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -994,7 +1017,8 @@
|
||||
}
|
||||
|
||||
.submit-btn.primary {
|
||||
background: linear-gradient(135deg, #007AFF 0%, #5856D6 100%);
|
||||
background: linear-gradient(135deg, rgb(66, 121, 240) 0%, #5856D6 100%);
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- 商家端 - 商品管理列表页面 -->
|
||||
<!-- 机构端 - 服务管理列表页面 -->
|
||||
<template>
|
||||
<view class="products-page">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
@@ -9,17 +9,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索商品名称"
|
||||
@confirm="handleSearch"
|
||||
/>
|
||||
<view class="search-btn" @click="handleSearch">搜索</view>
|
||||
</view>
|
||||
|
||||
|
||||
<!-- 筛选标签 -->
|
||||
<view class="filter-tabs">
|
||||
@@ -35,25 +25,37 @@
|
||||
:class="{ active: currentFilter === 'onsale' }"
|
||||
@click="switchFilter('onsale')"
|
||||
>
|
||||
上架
|
||||
可预约
|
||||
</view>
|
||||
<view
|
||||
class="filter-tab"
|
||||
:class="{ active: currentFilter === 'offsale' }"
|
||||
@click="switchFilter('offsale')"
|
||||
>
|
||||
下架
|
||||
</view>
|
||||
<view
|
||||
class="filter-tab"
|
||||
:class="{ active: currentFilter === 'low_stock' }"
|
||||
@click="switchFilter('low_stock')"
|
||||
>
|
||||
库存预警
|
||||
暂停服务
|
||||
</view>
|
||||
<view
|
||||
class="filter-tab"
|
||||
:class="{ active: currentFilter === 'low_stock' }"
|
||||
@click="switchFilter('low_stock')"
|
||||
>
|
||||
器械预警
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索服务/商品名称"
|
||||
@confirm="handleSearch"
|
||||
/>
|
||||
<view class="search-btn" @click="handleSearch">搜索</view>
|
||||
</view>
|
||||
|
||||
<!-- 服务列表 -->
|
||||
<scroll-view
|
||||
class="products-list"
|
||||
scroll-y
|
||||
@@ -67,9 +69,9 @@
|
||||
</view>
|
||||
|
||||
<view v-else-if="products.length === 0" class="empty-container">
|
||||
<text class="empty-icon">📦</text>
|
||||
<text class="empty-text">暂无商品</text>
|
||||
<view class="add-first-btn" @click="addProduct">添加第一个商品</view>
|
||||
<text class="empty-icon">🏥</text>
|
||||
<text class="empty-text">暂无服务项目</text>
|
||||
<view class="add-first-btn" @click="addProduct">添加第一个服务项目</view>
|
||||
</view>
|
||||
|
||||
<view v-else>
|
||||
@@ -79,46 +81,55 @@
|
||||
class="product-card"
|
||||
@click="viewProductDetail(product.id)"
|
||||
>
|
||||
<image
|
||||
:src="product.main_image_url || '/static/images/default-product.png'"
|
||||
class="product-image"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="product-info">
|
||||
<view class="product-header">
|
||||
<text class="product-name">{{ product.name }}</text>
|
||||
<text class="product-status" :class="getStatusClass(product.status)">
|
||||
{{ getStatusText(product.status) }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="product-subtitle">{{ product.subtitle || '暂无描述' }}</text>
|
||||
<view class="product-tags">
|
||||
<text v-if="product.is_hot" class="tag hot">热</text>
|
||||
<text v-if="product.is_new" class="tag new">新</text>
|
||||
<text v-if="product.is_featured" class="tag recommend">荐</text>
|
||||
<text v-if="product.is_vip_discount" class="tag vip">VIP</text>
|
||||
</view>
|
||||
<view class="product-stats">
|
||||
<view class="price-row">
|
||||
<text class="current-price">¥{{ product.base_price }}</text>
|
||||
<text v-if="product.market_price" class="original-price">¥{{ product.market_price }}</text>
|
||||
<!-- 上半部分:图片 + 服务信息 -->
|
||||
<view class="product-main">
|
||||
<image
|
||||
:src="product.main_image_url || '/static/images/default-product.png'"
|
||||
class="product-thumb"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="product-content">
|
||||
<!-- 第一层:服务名称 + 状态 badge -->
|
||||
<view class="product-header">
|
||||
<text class="product-name">{{ product.name }}</text>
|
||||
<text class="product-status-badge" :class="getStatusClass(product.status)">
|
||||
{{ getStatusText(product.status) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="stock-row">
|
||||
<text class="stock">库存: {{ product.total_stock || 0 }}</text>
|
||||
<text class="sales">销量: {{ product.sale_count || 0 }}</text>
|
||||
<!-- 第二层:描述 -->
|
||||
<text class="product-desc">{{ product.subtitle || '暂无描述' }}</text>
|
||||
<!-- 标签行(按需展示) -->
|
||||
<view v-if="product.is_hot || product.is_new || product.is_featured || product.is_vip_discount" class="product-tags">
|
||||
<text v-if="product.is_hot" class="tag tag-hot">热</text>
|
||||
<text v-if="product.is_new" class="tag tag-new">新</text>
|
||||
<text v-if="product.is_featured" class="tag tag-recommend">荐</text>
|
||||
<text v-if="product.is_vip_discount" class="tag tag-vip">关怀</text>
|
||||
</view>
|
||||
<!-- 第三层:价格高亮 -->
|
||||
<view class="product-price">
|
||||
<text class="price-current">¥{{ product.base_price }}</text>
|
||||
<text v-if="product.market_price" class="price-original">¥{{ product.market_price }}</text>
|
||||
</view>
|
||||
<!-- 第四层:名额、服务次数辅助信息 -->
|
||||
<view class="product-meta">
|
||||
<text class="meta-item">可约名额 {{ product.total_stock || 0 }}</text>
|
||||
<text class="meta-sep">·</text>
|
||||
<text class="meta-item">服务次数 {{ product.sale_count || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 分割线 -->
|
||||
<view class="product-divider"></view>
|
||||
<!-- 操作栏 -->
|
||||
<view class="product-actions" @click.stop>
|
||||
<view
|
||||
class="action-btn"
|
||||
:class="product.status === 1 ? 'warning' : 'success'"
|
||||
class="action-btn action-secondary"
|
||||
@click="toggleStatus(product)"
|
||||
>
|
||||
{{ product.status === 1 ? '下架' : '上架' }}
|
||||
{{ product.status === 1 ? '暂停服务' : '开启服务' }}
|
||||
</view>
|
||||
<view class="action-btn default" @click="editProduct(product.id)">编辑</view>
|
||||
<view class="action-btn danger" @click="deleteProduct(product)">删除</view>
|
||||
<view class="action-btn action-edit" @click="editProduct(product.id)">编辑</view>
|
||||
<view class="action-btn action-danger" @click="deleteProduct(product)">删除</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -132,10 +143,10 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 添加商品按钮 -->
|
||||
<!-- 添加服务按钮 -->
|
||||
<view class="add-product-btn" @click="addProduct">
|
||||
<text class="add-icon">+</text>
|
||||
<text class="add-text">添加商品</text>
|
||||
<text class="add-text">添加服务项目</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -160,6 +171,7 @@
|
||||
is_hot: boolean
|
||||
is_new: boolean
|
||||
is_featured: boolean
|
||||
is_vip_discount: boolean
|
||||
tags: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
@@ -182,14 +194,14 @@
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options: any) {
|
||||
const type = options.type as string
|
||||
if (type === 'add') {
|
||||
this.addProduct()
|
||||
} else if (type === 'low_stock') {
|
||||
this.currentFilter = 'low_stock'
|
||||
}
|
||||
this.initMerchantId()
|
||||
async onLoad(options: any) {
|
||||
const type = options.type as string
|
||||
if (type === 'add') {
|
||||
this.addProduct()
|
||||
} else if (type === 'low_stock') {
|
||||
this.currentFilter = 'low_stock'
|
||||
}
|
||||
await this.initMerchantId()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
@@ -342,11 +354,11 @@
|
||||
|
||||
async toggleStatus(product: ProductType) {
|
||||
const newStatus = product.status === 1 ? 2 : 1
|
||||
const actionText = newStatus === 1 ? '上架' : '下架'
|
||||
const actionText = newStatus === 1 ? '开启服务' : '暂停服务'
|
||||
|
||||
uni.showModal({
|
||||
title: `确认${actionText}`,
|
||||
content: `确定要${actionText}该商品吗?`,
|
||||
content: `确定要${actionText}该服务项目吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
@@ -377,7 +389,7 @@
|
||||
async deleteProduct(product: ProductType) {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: '删除后将无法恢复,确定要删除该商品吗?',
|
||||
content: '删除后将无法恢复,确定要删除该服务项目吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
@@ -410,318 +422,334 @@
|
||||
|
||||
getStatusText(status: number): string {
|
||||
if (status === 1) return '在售'
|
||||
if (status === 2 || status === 0) return '已下架'
|
||||
if (status === 2 || status === 0) return '已下线'
|
||||
return '待审核'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.products-page {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 140rpx;
|
||||
}
|
||||
.products-page {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 140rpx;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 30rpx;
|
||||
background-color: #fff;
|
||||
}
|
||||
.search-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 20rpx 30rpx;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 32rpx;
|
||||
padding: 0 30rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 32rpx;
|
||||
padding: 0 30rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
margin-left: 20rpx;
|
||||
padding: 16rpx 30rpx;
|
||||
background-color: #007AFF;
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
.search-btn {
|
||||
margin-left: 20rpx;
|
||||
padding: 16rpx 30rpx;
|
||||
background-color: rgb(66, 121, 240);
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
padding: 0 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #fff;
|
||||
padding: 0 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
position: relative;
|
||||
}
|
||||
.filter-tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
color: #007AFF;
|
||||
font-weight: bold;
|
||||
}
|
||||
.filter-tab.active {
|
||||
color: rgb(66, 121, 240);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.filter-tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40rpx;
|
||||
height: 4rpx;
|
||||
background-color: #007AFF;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
.filter-tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40rpx;
|
||||
height: 4rpx;
|
||||
background-color: rgb(66, 121, 240);
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
.products-list {
|
||||
padding: 0 20rpx;
|
||||
height: calc(100vh - 260rpx);
|
||||
}
|
||||
.products-list {
|
||||
padding: 0 20rpx;
|
||||
height: calc(100vh - 260rpx);
|
||||
}
|
||||
|
||||
.loading-container, .empty-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
}
|
||||
.loading-container, .empty-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 100rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.empty-icon {
|
||||
font-size: 100rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.empty-text, .loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
.empty-text, .loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.add-first-btn {
|
||||
margin-top: 30rpx;
|
||||
padding: 20rpx 60rpx;
|
||||
background-color: #007AFF;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
border-radius: 40rpx;
|
||||
}
|
||||
.add-first-btn {
|
||||
margin-top: 30rpx;
|
||||
padding: 20rpx 60rpx;
|
||||
background-color: rgb(66, 121, 240);
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
border-radius: 40rpx;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 24rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.product-card {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-right: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.product-main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.product-thumb {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 12rpx;
|
||||
background-color: #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.product-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
.product-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.product-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.product-status {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-left: 10rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.product-name {
|
||||
font-size: 28rpx;
|
||||
color: #222;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
line-height: 1.45;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.status-onsale {
|
||||
background-color: #E8F5E9;
|
||||
color: #4CAF50;
|
||||
}
|
||||
.product-status-badge {
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 20rpx;
|
||||
flex-shrink: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-offsale {
|
||||
background-color: #FFEBEE;
|
||||
color: #F44336;
|
||||
}
|
||||
.status-onsale {
|
||||
background-color: #E8F5E9;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: #FFF3E0;
|
||||
color: #FF9800;
|
||||
}
|
||||
.status-offsale {
|
||||
background-color: #F5F5F5;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.product-subtitle {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
display: block;
|
||||
margin-bottom: 10rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status-pending {
|
||||
background-color: #E8F0FE;
|
||||
color: rgb(66, 121, 240);
|
||||
}
|
||||
|
||||
.product-tags {
|
||||
display: flex;
|
||||
gap: 10rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
.product-desc {
|
||||
font-size: 24rpx;
|
||||
color: #aaa;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 10rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
.product-tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.tag.hot {
|
||||
background-color: #FF5722;
|
||||
color: #fff;
|
||||
}
|
||||
.tag {
|
||||
font-size: 18rpx;
|
||||
padding: 3rpx 10rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.tag.new {
|
||||
background-color: #2196F3;
|
||||
color: #fff;
|
||||
}
|
||||
.tag-hot {
|
||||
background-color: #FF5722;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tag.recommend {
|
||||
background-color: #9C27B0;
|
||||
color: #fff;
|
||||
}
|
||||
.tag-new {
|
||||
background-color: rgb(66, 121, 240);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tag.vip {
|
||||
background-color: #FFC107;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tag-recommend {
|
||||
background-color: #9C27B0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.product-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.tag-vip {
|
||||
background-color: #e8f0fe;
|
||||
color: rgb(66, 121, 240);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.price-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
.product-price {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.current-price {
|
||||
font-size: 32rpx;
|
||||
color: #FF3B30;
|
||||
font-weight: bold;
|
||||
}
|
||||
.price-current {
|
||||
font-size: 34rpx;
|
||||
color: rgb(225, 37, 27);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.original-price {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
text-decoration: line-through;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
.price-original {
|
||||
font-size: 22rpx;
|
||||
color: #ccc;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.stock-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
.product-meta {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.product-actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16rpx;
|
||||
margin-top: 20rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
}
|
||||
.meta-item {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 12rpx 24rpx;
|
||||
font-size: 24rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
.meta-sep {
|
||||
font-size: 22rpx;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.action-btn.success {
|
||||
background-color: #E8F5E9;
|
||||
color: #4CAF50;
|
||||
}
|
||||
.product-divider {
|
||||
height: 1rpx;
|
||||
background-color: #f5f5f5;
|
||||
margin: 0 24rpx;
|
||||
}
|
||||
|
||||
.action-btn.warning {
|
||||
background-color: #FFF3E0;
|
||||
color: #FF9800;
|
||||
}
|
||||
.product-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 16rpx 24rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.action-btn.default {
|
||||
background-color: #F5F5F5;
|
||||
color: #666;
|
||||
}
|
||||
.action-btn {
|
||||
padding: 12rpx 28rpx;
|
||||
font-size: 24rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
background-color: #FFEBEE;
|
||||
color: #F44336;
|
||||
}
|
||||
.action-edit {
|
||||
background-color: rgb(66, 121, 240);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.load-more, .no-more {
|
||||
padding: 30rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
.action-secondary {
|
||||
background-color: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.load-more-text, .no-more-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
.action-danger {
|
||||
background-color: #FFEBEE;
|
||||
color: #F44336;
|
||||
}
|
||||
|
||||
.add-product-btn {
|
||||
position: fixed;
|
||||
bottom: 30rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 300rpx;
|
||||
height: 88rpx;
|
||||
background: linear-gradient(135deg, #007AFF 0%, #5856D6 100%);
|
||||
border-radius: 44rpx;
|
||||
box-shadow: 0 8rpx 20rpx rgba(0, 122, 255, 0.3);
|
||||
}
|
||||
.load-more, .no-more {
|
||||
padding: 30rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 40rpx;
|
||||
color: #fff;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
.load-more-text, .no-more-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.add-text {
|
||||
font-size: 30rpx;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
.add-product-btn {
|
||||
position: fixed;
|
||||
bottom: 30rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 320rpx;
|
||||
height: 88rpx;
|
||||
background: linear-gradient(135deg, rgb(66, 121, 240) 0%, #5856D6 100%);
|
||||
border-radius: 44rpx;
|
||||
box-shadow: 0 8rpx 20rpx rgba(66, 121, 240, 0.35);
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 40rpx;
|
||||
color: #fff;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.add-text {
|
||||
font-size: 30rpx;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,9 @@
|
||||
<!-- 商家端 - 个人中心 -->
|
||||
<!-- 商家端 - 机构中心 -->
|
||||
<template>
|
||||
<view class="merchant-profile">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<view class="mp-tab-navbar">
|
||||
<text class="mp-tab-title">我的</text>
|
||||
<text class="mp-tab-title">机构中心</text>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<scroll-view direction="vertical" class="profile-scroll">
|
||||
@@ -11,11 +11,11 @@
|
||||
<view class="profile-header">
|
||||
<image :src="shopInfo.shop_logo || '/static/logo.png'" class="shop-logo" mode="aspectFill" @click="goToSettings" />
|
||||
<view class="shop-info">
|
||||
<text class="shop-name">{{ shopInfo.shop_name || '我的店铺' }}</text>
|
||||
<text class="shop-name">{{ shopInfo.shop_name || '我的机构' }}</text>
|
||||
<text class="shop-status">{{ getShopStatus() }}</text>
|
||||
<view class="shop-stats">
|
||||
<text class="stat-item">评分: {{ shopInfo.rating_avg || 5.0 }}/5.0</text>
|
||||
<text class="stat-item">总销量: {{ shopInfo.total_sales || 0 }}</text>
|
||||
<text class="stat-item">总服务量: {{ shopInfo.total_sales || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="settings-icon" @click="goToSettings">
|
||||
@@ -25,34 +25,34 @@
|
||||
|
||||
<!-- 前往店铺主页(跳转店铺详情,而非 shop-edit) -->
|
||||
<view class="shop-home-entry" @click="goToShopHome">
|
||||
<text class="shop-home-icon">🏪</text>
|
||||
<text class="shop-home-text">前往店铺主页</text>
|
||||
<text class="shop-home-icon"><EFBFBD></text>
|
||||
<text class="shop-home-text">查看机构主页</text>
|
||||
<text class="shop-home-arrow">›</text>
|
||||
</view>
|
||||
|
||||
<!-- 订单管理快捷入口 -->
|
||||
<view class="section-card">
|
||||
<view class="section-title-text">订单管理</view>
|
||||
<view class="section-title-text">服务订单</view>
|
||||
<view class="order-tabs">
|
||||
<view class="order-tab" @click="goToOrders('all')">
|
||||
<view class="tab-icon-wrap">
|
||||
<text class="tab-icon">📋</text>
|
||||
<text v-if="pendingCounts.pending_shipment > 0" class="tab-badge">{{ pendingCounts.pending_shipment }}</text>
|
||||
</view>
|
||||
<text class="tab-text">待发货</text>
|
||||
<text class="tab-text">待上门</text>
|
||||
</view>
|
||||
<view class="order-tab" @click="goToOrders('shipped')">
|
||||
<view class="tab-icon-wrap">
|
||||
<text class="tab-icon">🚚</text>
|
||||
<text class="tab-icon"><EFBFBD></text>
|
||||
</view>
|
||||
<text class="tab-text">已发货</text>
|
||||
<text class="tab-text">服务中</text>
|
||||
</view>
|
||||
<view class="order-tab" @click="goToOrders('refund')">
|
||||
<view class="tab-icon-wrap">
|
||||
<text class="tab-icon">↩️</text>
|
||||
<text v-if="pendingCounts.refund_requests > 0" class="tab-badge alert">{{ pendingCounts.refund_requests }}</text>
|
||||
</view>
|
||||
<text class="tab-text">退款</text>
|
||||
<text class="tab-text">取消/售后</text>
|
||||
</view>
|
||||
<view class="order-tab" @click="goToOrders('all')">
|
||||
<view class="tab-icon-wrap">
|
||||
@@ -65,23 +65,23 @@
|
||||
|
||||
<!-- 今日经营数据:复用 index 的 todayStats 字段 -->
|
||||
<view class="section-card">
|
||||
<view class="section-title-text">今日经营</view>
|
||||
<view class="section-title-text">今日运营</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">¥{{ formatNumber(todayStats.sales) }}</text>
|
||||
<text class="stat-label">营业额</text>
|
||||
<text class="stat-label">服务收入</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ todayStats.orders || 0 }}</text>
|
||||
<text class="stat-label">订单数</text>
|
||||
<text class="stat-label">服务单数</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ todayStats.visitors || 0 }}</text>
|
||||
<text class="stat-label">访客数</text>
|
||||
<text class="stat-label">问诊人数</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ todayStats.conversion || 0 }}%</text>
|
||||
<text class="stat-label">转化率</text>
|
||||
<text class="stat-label">预约转化率</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -111,26 +111,26 @@
|
||||
|
||||
<!-- 商品管理入口 -->
|
||||
<view class="section-card">
|
||||
<view class="section-title-text">商品管理</view>
|
||||
<view class="section-title-text">服务/商品管理</view>
|
||||
<view class="management-grid">
|
||||
<view class="management-item" @click="goToProducts">
|
||||
<text class="mgmt-icon">📦</text>
|
||||
<text class="mgmt-label">商品管理</text>
|
||||
<text class="mgmt-label">服务/商品管理</text>
|
||||
</view>
|
||||
<view class="management-item" @click="goToInventory">
|
||||
<text class="mgmt-icon">📊</text>
|
||||
<text class="mgmt-label">库存管理</text>
|
||||
<text class="mgmt-label">器械库存</text>
|
||||
<view v-if="pendingCounts.low_stock > 0" class="mgmt-badge">
|
||||
<text class="mgmt-badge-text">{{ pendingCounts.low_stock }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="management-item" @click="goToPromotions">
|
||||
<text class="mgmt-icon">🎉</text>
|
||||
<text class="mgmt-label">促销活动</text>
|
||||
<text class="mgmt-icon"><EFBFBD></text>
|
||||
<text class="mgmt-label">关怀活动</text>
|
||||
</view>
|
||||
<view class="management-item" @click="goToReviews">
|
||||
<text class="mgmt-icon">⭐</text>
|
||||
<text class="mgmt-label">评价管理</text>
|
||||
<text class="mgmt-label">服务评价</text>
|
||||
<view v-if="pendingCounts.pending_reviews > 0" class="mgmt-badge">
|
||||
<text class="mgmt-badge-text">{{ pendingCounts.pending_reviews }}</text>
|
||||
</view>
|
||||
@@ -142,17 +142,17 @@
|
||||
<view class="section-card function-menu">
|
||||
<view class="menu-item" @click="goToFinance">
|
||||
<text class="menu-icon">💳</text>
|
||||
<text class="menu-label">财务管理</text>
|
||||
<text class="menu-label">结算中心</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goToCustomers">
|
||||
<text class="menu-icon">👥</text>
|
||||
<text class="menu-label">客户管理</text>
|
||||
<text class="menu-label">用户档案管理</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goToMarketing">
|
||||
<text class="menu-icon">📢</text>
|
||||
<text class="menu-label">营销工具</text>
|
||||
<text class="menu-label">运营工具</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -519,11 +519,11 @@
|
||||
},
|
||||
|
||||
getOrderStatusText(status: number): string {
|
||||
if (status === 1) return '待付款'
|
||||
if (status === 2) return '待发货'
|
||||
if (status === 3) return '已发货'
|
||||
if (status === 1) return '待支付'
|
||||
if (status === 2) return '待接单'
|
||||
if (status === 3) return '服务中'
|
||||
if (status === 4) return '已完成'
|
||||
if (status === 0) return '退款中'
|
||||
if (status === 0) return '取消/售后'
|
||||
return '未知'
|
||||
},
|
||||
|
||||
@@ -669,7 +669,7 @@
|
||||
|
||||
.view-all-link {
|
||||
font-size: 24rpx;
|
||||
color: #667eea;
|
||||
color: rgb(66, 121, 240);
|
||||
}
|
||||
|
||||
/* ===== 店铺头部(渐变背景与 index header 保持同设计语言)===== */
|
||||
@@ -678,7 +678,7 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 40rpx 30rpx;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, rgb(66, 121, 240) 0%, #5856D6 100%);
|
||||
}
|
||||
|
||||
.shop-logo {
|
||||
@@ -830,7 +830,7 @@
|
||||
.stat-value {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
color: rgb(66, 121, 240);
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
@@ -851,7 +851,7 @@
|
||||
border-radius: 16rpx;
|
||||
border-left-width: 6rpx;
|
||||
border-left-style: solid;
|
||||
border-left-color: #667eea;
|
||||
border-left-color: rgb(66, 121, 240);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- 商家端 - 营销活动页面 -->
|
||||
<!-- 机构端 - 关怀活动页面 -->
|
||||
<template>
|
||||
<view class="promotions-page">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
@@ -10,17 +10,17 @@
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<view class="tabs">
|
||||
<view class="tab" :class="{ active: currentTab === 'coupon' }" @click="switchTab('coupon')">优惠券</view>
|
||||
<view class="tab" :class="{ active: currentTab === 'seckill' }" @click="switchTab('seckill')">秒杀活动</view>
|
||||
<view class="tab" :class="{ active: currentTab === 'group' }" @click="switchTab('group')">拼团活动</view>
|
||||
<view class="tab" :class="{ active: currentTab === 'coupon' }" @click="switchTab('coupon')">护理套餐券</view>
|
||||
<view class="tab" :class="{ active: currentTab === 'seckill' }" @click="switchTab('seckill')">康复体验</view>
|
||||
<view class="tab" :class="{ active: currentTab === 'group' }" @click="switchTab('group')">慢病管理服务包</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="promotions-list" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
|
||||
<view v-if="loading && promotions.length === 0" class="loading-container"><text class="loading-text">加载中...</text></view>
|
||||
<view v-else-if="promotions.length === 0" class="empty-container">
|
||||
<text class="empty-icon">🎉</text>
|
||||
<text class="empty-text">暂无活动</text>
|
||||
<view class="add-btn" @click="addPromotion">创建活动</view>
|
||||
<text class="empty-text">暂无关怀活动</text>
|
||||
<view class="add-promotion-btn" @click="addPromotion">+ 创建关怀活动</view>
|
||||
</view>
|
||||
<view v-else>
|
||||
<view v-for="promo in promotions" :key="promo.id" class="promotion-card">
|
||||
@@ -41,7 +41,7 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="add-promotion-btn" @click="addPromotion">+ 创建活动</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -179,32 +179,33 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.promotions-page { background-color: #f5f5f5; min-height: 100vh; padding-bottom: 140rpx; }
|
||||
.tabs { display: flex; background-color: #fff; padding: 0 20rpx; position: sticky; top: 0; z-index: 10; }
|
||||
.tabs { display: flex; flex-direction: row; background-color: #fff; padding: 0 20rpx; position: sticky; top: 0; z-index: 10; }
|
||||
.tab { flex: 1; text-align: center; padding: 24rpx 0; font-size: 28rpx; color: #666; position: relative; }
|
||||
.tab.active { color: #007AFF; font-weight: bold; }
|
||||
.tab.active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 40rpx; height: 4rpx; background-color: #007AFF; border-radius: 2rpx; }
|
||||
.tab.active { color: rgb(66, 121, 240); font-weight: bold; }
|
||||
.tab.active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 40rpx; height: 4rpx; background-color: rgb(66, 121, 240); border-radius: 2rpx; }
|
||||
.promotions-list { padding: 20rpx; height: calc(100vh - 200rpx); }
|
||||
.loading-container, .empty-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 100rpx 0; }
|
||||
.empty-icon { font-size: 100rpx; margin-bottom: 20rpx; }
|
||||
.empty-text, .loading-text { font-size: 28rpx; color: #999; }
|
||||
.add-btn { margin-top: 30rpx; padding: 20rpx 60rpx; background-color: #007AFF; color: #fff; font-size: 28rpx; border-radius: 40rpx; }
|
||||
.add-btn { margin-top: 30rpx; padding: 20rpx 60rpx; background-color: rgb(66, 121, 240); color: #fff; font-size: 28rpx; border-radius: 40rpx; }
|
||||
.promotion-card { background-color: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 20rpx; }
|
||||
.promo-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
|
||||
.promo-header { display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
|
||||
.promo-name { font-size: 30rpx; font-weight: bold; color: #333; }
|
||||
.promo-status { font-size: 22rpx; padding: 6rpx 16rpx; border-radius: 16rpx; }
|
||||
.status-1 { background-color: #E8F5E9; color: #4CAF50; }
|
||||
.status-0 { background-color: #FFF3E0; color: #FF9800; }
|
||||
.status-2 { background-color: #F5F5F5; color: #999; }
|
||||
.promo-info { margin-bottom: 20rpx; }
|
||||
.info-item { display: flex; font-size: 26rpx; margin-bottom: 10rpx; }
|
||||
.info-item { display: flex; flex-direction: row; font-size: 26rpx; margin-bottom: 10rpx; }
|
||||
.info-item .label { color: #999; min-width: 140rpx; }
|
||||
.info-item .value { color: #333; }
|
||||
.promo-actions { display: flex; justify-content: flex-end; gap: 16rpx; }
|
||||
.promo-actions { display: flex; flex-direction: row; justify-content: flex-end; gap: 16rpx; }
|
||||
.action-btn { padding: 12rpx 24rpx; font-size: 24rpx; background-color: #F5F5F5; color: #666; border-radius: 24rpx; }
|
||||
.action-btn.danger { background-color: #FFEBEE; color: #F44336; }
|
||||
.add-promotion-btn { position: fixed; bottom: 30rpx; left: 50%; transform: translateX(-50%); width: 300rpx; height: 88rpx; background: linear-gradient(135deg, #007AFF 0%, #5856D6 100%); border-radius: 44rpx; display: flex; align-items: center; justify-content: center; font-size: 30rpx; color: #fff; font-weight: bold; box-shadow: 0 8rpx 20rpx rgba(0,122,255,0.3); }
|
||||
.add-promotion-btn { position: fixed; bottom: 30rpx; left: 50%; transform: translateX(-50%); width: 300rpx; height: 88rpx; background: linear-gradient(135deg, rgb(66, 121, 240) 0%, #5856D6 100%); border-radius: 44rpx; display: flex; flex-direction: row; align-items: center; justify-content: center; font-size: 30rpx; color: #fff; font-weight: bold; box-shadow: 0 8rpx 20rpx rgba(0,122,255,0.3); }
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- 商家端 - 评价管理页面 -->
|
||||
<!-- 机构端 - 服务评价页面 -->
|
||||
<template>
|
||||
<view class="reviews-page">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
@@ -31,18 +31,18 @@
|
||||
<text class="review-time">{{ formatTime(review.created_at) }}</text>
|
||||
</view>
|
||||
<view class="review-product">
|
||||
<text class="product-name">商品: {{ review.product_name }}</text>
|
||||
<text class="product-name">服务项目: {{ review.product_name }}</text>
|
||||
</view>
|
||||
<view class="review-content">{{ review.content }}</view>
|
||||
<view v-if="review.images" class="review-images">
|
||||
<image v-for="(img, idx) in parseImages(review.images)" :key="idx" :src="img" class="review-image" mode="aspectFill" @click="previewImage(review.images, idx)"/>
|
||||
</view>
|
||||
<view v-if="review.reply" class="review-reply">
|
||||
<text class="reply-label">商家回复:</text>
|
||||
<text class="reply-label">机构回复:</text>
|
||||
<text class="reply-content">{{ review.reply }}</text>
|
||||
</view>
|
||||
<view v-else class="review-actions">
|
||||
<view class="action-btn" @click="replyReview(review)">回复评价</view>
|
||||
<view class="action-btn" @click="replyReview(review)">回复用户评价</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -254,43 +254,43 @@
|
||||
|
||||
<style>
|
||||
.reviews-page { background-color: #f5f5f5; min-height: 100vh; }
|
||||
.filter-tabs { display: flex; background-color: #fff; padding: 0 20rpx; margin-bottom: 20rpx; }
|
||||
.filter-tabs { display: flex; flex-direction: row; background-color: #fff; padding: 0 20rpx; margin-bottom: 20rpx; }
|
||||
.filter-tab { flex: 1; text-align: center; padding: 24rpx 0; font-size: 26rpx; color: #666; position: relative; }
|
||||
.filter-tab.active { color: #007AFF; font-weight: bold; }
|
||||
.filter-tab.active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 40rpx; height: 4rpx; background-color: #007AFF; border-radius: 2rpx; }
|
||||
.filter-tab.active { color: rgb(66, 121, 240); font-weight: bold; }
|
||||
.filter-tab.active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 40rpx; height: 4rpx; background-color: rgb(66, 121, 240); border-radius: 2rpx; }
|
||||
.reviews-list { padding: 0 20rpx; height: calc(100vh - 120rpx); }
|
||||
.loading-container, .empty-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 100rpx 0; }
|
||||
.empty-icon { font-size: 100rpx; margin-bottom: 20rpx; }
|
||||
.empty-text, .loading-text { font-size: 28rpx; color: #999; }
|
||||
.review-card { background-color: #fff; border-radius: 16rpx; margin-bottom: 20rpx; padding: 24rpx; }
|
||||
.review-header { display: flex; align-items: center; margin-bottom: 20rpx; }
|
||||
.review-header { display: flex; flex-direction: row; align-items: center; margin-bottom: 20rpx; }
|
||||
.user-avatar { width: 70rpx; height: 70rpx; border-radius: 50%; margin-right: 16rpx; background-color: #f5f5f5; }
|
||||
.user-info { flex: 1; }
|
||||
.user-name { font-size: 26rpx; color: #333; font-weight: 500; display: block; margin-bottom: 8rpx; }
|
||||
.rating { display: flex; }
|
||||
.rating { display: flex; flex-direction: row; }
|
||||
.star { font-size: 22rpx; color: #ddd; margin-right: 4rpx; }
|
||||
.star.filled { color: #FFB800; }
|
||||
.review-time { font-size: 22rpx; color: #999; }
|
||||
.review-product { font-size: 24rpx; color: #666; margin-bottom: 12rpx; }
|
||||
.review-content { font-size: 26rpx; color: #333; line-height: 1.5; margin-bottom: 16rpx; }
|
||||
.review-images { display: flex; flex-wrap: wrap; gap: 12rpx; margin-bottom: 16rpx; }
|
||||
.review-images { display: flex; flex-direction: row; flex-wrap: wrap; gap: 12rpx; margin-bottom: 16rpx; }
|
||||
.review-image { width: 120rpx; height: 120rpx; border-radius: 8rpx; }
|
||||
.review-reply { background-color: #f5f5f5; padding: 16rpx; border-radius: 8rpx; margin-top: 16rpx; }
|
||||
.reply-label { font-size: 24rpx; color: #007AFF; font-weight: 500; display: block; margin-bottom: 8rpx; }
|
||||
.reply-label { font-size: 24rpx; color: rgb(66, 121, 240); font-weight: 500; display: block; margin-bottom: 8rpx; }
|
||||
.reply-content { font-size: 24rpx; color: #666; }
|
||||
.review-actions { margin-top: 16rpx; text-align: right; }
|
||||
.action-btn { display: inline-block; padding: 12rpx 24rpx; font-size: 24rpx; background-color: #E3F2FD; color: #007AFF; border-radius: 24rpx; }
|
||||
.action-btn { display: inline-block; padding: 12rpx 24rpx; font-size: 24rpx; background-color: #E3F2FD; color: rgb(66, 121, 240); border-radius: 24rpx; }
|
||||
.load-more { padding: 30rpx 0; text-align: center; }
|
||||
.load-more-text { font-size: 24rpx; color: #999; }
|
||||
.modal-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); display: flex; align-items: flex-end; justify-content: center; z-index: 1000; }
|
||||
.modal-content { width: 100%; background-color: #fff; border-radius: 24rpx 24rpx 0 0; padding-bottom: env(safe-area-inset-bottom); }
|
||||
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 30rpx; border-bottom: 1rpx solid #f5f5f5; }
|
||||
.modal-header { display: flex; flex-direction: row; justify-content: space-between; align-items: center; padding: 30rpx; border-bottom: 1rpx solid #f5f5f5; }
|
||||
.modal-title { font-size: 32rpx; font-weight: bold; color: #333; }
|
||||
.modal-close { font-size: 44rpx; color: #999; }
|
||||
.modal-body { padding: 30rpx; }
|
||||
.reply-input { width: 100%; height: 200rpx; border: 1rpx solid #e5e5e5; border-radius: 8rpx; padding: 20rpx; font-size: 28rpx; box-sizing: border-box; }
|
||||
.modal-footer { display: flex; border-top: 1rpx solid #f5f5f5; }
|
||||
.modal-footer { display: flex; flex-direction: row; border-top: 1rpx solid #f5f5f5; }
|
||||
.modal-btn { flex: 1; height: 88rpx; line-height: 88rpx; text-align: center; font-size: 28rpx; }
|
||||
.modal-btn.cancel { color: #666; border-right: 1rpx solid #f5f5f5; }
|
||||
.modal-btn.confirm { color: #007AFF; font-weight: bold; }
|
||||
.modal-btn.confirm { color: rgb(66, 121, 240); font-weight: bold; }
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- 商家端 - 店铺编辑页面 -->
|
||||
<!-- 机构端 - 机构信息编辑页面 -->
|
||||
<template>
|
||||
<view class="shop-edit-page">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
@@ -10,15 +10,15 @@
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<view class="section">
|
||||
<view class="section-title">店铺信息</view>
|
||||
<view class="section-title">机构信息</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">店铺名称 *</text>
|
||||
<input class="input" v-model="shop.shop_name" placeholder="请输入店铺名称" maxlength="50"/>
|
||||
<text class="label">机构名称 *</text>
|
||||
<input class="input" v-model="shop.shop_name" placeholder="请输入机构名称" maxlength="50"/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">店铺Logo</text>
|
||||
<text class="label">机构Logo</text>
|
||||
<view class="logo-uploader" @click="chooseLogo">
|
||||
<image v-if="shop.shop_logo" :src="shop.shop_logo" class="logo-preview" mode="aspectFill"/>
|
||||
<view v-else class="logo-placeholder">+</view>
|
||||
@@ -26,16 +26,16 @@
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">店铺Banner</text>
|
||||
<text class="label">机构Banner</text>
|
||||
<view class="banner-uploader" @click="chooseBanner">
|
||||
<image v-if="shop.shop_banner" :src="shop.shop_banner" class="banner-preview" mode="aspectFill"/>
|
||||
<view v-else class="banner-placeholder">点击上传店铺Banner</view>
|
||||
<view v-else class="banner-placeholder">点击上传机构Banner</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">店铺简介</text>
|
||||
<textarea class="textarea" v-model="shop.description" placeholder="请输入店铺简介" maxlength="500"/>
|
||||
<text class="label">机构简介</text>
|
||||
<textarea class="textarea" v-model="shop.description" placeholder="请输入机构简介" maxlength="500"/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
},
|
||||
|
||||
async loadShop() {
|
||||
if (!this.merchantId) return
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_shops')
|
||||
@@ -180,7 +181,7 @@
|
||||
|
||||
async saveShop() {
|
||||
if (!this.shop.shop_name) {
|
||||
uni.showToast({ title: '请输入店铺名称', icon: 'none' })
|
||||
uni.showToast({ title: '请输入机构名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -246,7 +247,7 @@
|
||||
<style>
|
||||
.shop-edit-page { background-color: #f5f5f5; min-height: 100vh; padding-bottom: 160rpx; }
|
||||
.section { background-color: #fff; margin-bottom: 20rpx; padding: 30rpx; }
|
||||
.section-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 30rpx; padding-bottom: 20rpx; border-bottom: 1rpx solid #f5f5f5; }
|
||||
.section-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 30rpx; padding-bottom: 20rpx; border-bottom: 1rpx solid #f5f5f5; padding-left: 16rpx; border-left: 6rpx solid rgb(66, 121, 240); }
|
||||
.form-item { margin-bottom: 30rpx; }
|
||||
.label { font-size: 28rpx; color: #333; display: block; margin-bottom: 16rpx; }
|
||||
.input { height: 72rpx; border: 1rpx solid #e5e5e5; border-radius: 8rpx; padding: 0 20rpx; font-size: 28rpx; }
|
||||
@@ -256,5 +257,5 @@
|
||||
.logo-preview, .banner-preview { width: 100%; height: 100%; }
|
||||
.logo-placeholder, .banner-placeholder { width: 100%; height: 100%; background-color: #f5f5f5; display: flex; align-items: center; justify-content: center; font-size: 40rpx; color: #999; border: 2rpx dashed #ddd; }
|
||||
.submit-bar { position: fixed; bottom: 0; left: 0; right: 0; padding: 20rpx 30rpx; padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); background-color: #fff; box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05); }
|
||||
.submit-btn { height: 88rpx; line-height: 88rpx; text-align: center; font-size: 32rpx; font-weight: bold; border-radius: 44rpx; background: linear-gradient(135deg, #007AFF 0%, #5856D6 100%); color: #fff; }
|
||||
.submit-btn { height: 88rpx; line-height: 88rpx; text-align: center; font-size: 32rpx; font-weight: bold; border-radius: 44rpx; background: linear-gradient(135deg, rgb(66, 121, 240) 0%, #5856D6 100%); color: #fff; }
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- 商家端 - 数据统计页面 -->
|
||||
<!-- 机构端 - 服务数据统计页面 -->
|
||||
<template>
|
||||
<view class="statistics-page">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
@@ -16,29 +16,29 @@
|
||||
</view>
|
||||
|
||||
<view class="overview-section">
|
||||
<view class="section-title">数据概览</view>
|
||||
<view class="section-title">服务概览</view>
|
||||
<view class="overview-grid">
|
||||
<view class="overview-item">
|
||||
<text class="overview-value">¥{{ stats.todaySales }}</text>
|
||||
<text class="overview-label">销售额</text>
|
||||
<text class="overview-label">服务收入</text>
|
||||
</view>
|
||||
<view class="overview-item">
|
||||
<text class="overview-value">{{ stats.todayOrders }}</text>
|
||||
<text class="overview-label">订单数</text>
|
||||
<text class="overview-label">服务单数</text>
|
||||
</view>
|
||||
<view class="overview-item">
|
||||
<text class="overview-value">{{ stats.todayVisitors }}</text>
|
||||
<text class="overview-label">访客数</text>
|
||||
<text class="overview-label">咨询人数</text>
|
||||
</view>
|
||||
<view class="overview-item">
|
||||
<text class="overview-value">{{ stats.conversionRate }}%</text>
|
||||
<text class="overview-label">转化率</text>
|
||||
<text class="overview-label">预约转化率</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="trend-section">
|
||||
<view class="section-title">销售趋势</view>
|
||||
<view class="section-title">服务趋势</view>
|
||||
<view class="trend-chart">
|
||||
<view class="chart-bars">
|
||||
<view v-for="(item, index) in trendData" :key="index" class="chart-bar-wrapper">
|
||||
@@ -50,14 +50,14 @@
|
||||
</view>
|
||||
|
||||
<view class="product-section">
|
||||
<view class="section-title">热销商品</view>
|
||||
<view class="section-title">热门服务/商品</view>
|
||||
<view class="product-list">
|
||||
<view v-for="(product, index) in hotProducts" :key="product.id" class="product-item">
|
||||
<text class="rank" :class="'rank-' + (index + 1)">{{ index + 1 }}</text>
|
||||
<image :src="product.image" class="product-image" mode="aspectFill"/>
|
||||
<view class="product-info">
|
||||
<text class="product-name">{{ product.name }}</text>
|
||||
<text class="product-sales">销量: {{ product.sales }}</text>
|
||||
<text class="product-sales">服务次数: {{ product.sales }}</text>
|
||||
</view>
|
||||
<text class="product-revenue">¥{{ product.revenue }}</text>
|
||||
</view>
|
||||
@@ -204,24 +204,30 @@
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.statistics-page { background-color: #f5f5f5; min-height: 100vh; }
|
||||
.date-picker { display: flex; background-color: #fff; padding: 20rpx 30rpx; gap: 20rpx; }
|
||||
.statistics-page { background-color: #f4f5f7; min-height: 100vh; }
|
||||
.date-picker { display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #fff; padding: 20rpx 30rpx; gap: 20rpx; margin-bottom: 2rpx; }
|
||||
.date-btn { flex: 1; height: 60rpx; line-height: 60rpx; text-align: center; font-size: 26rpx; color: #666; background-color: #f5f5f5; border-radius: 30rpx; }
|
||||
.date-btn.active { background-color: #007AFF; color: #fff; }
|
||||
.overview-section, .trend-section, .product-section { background-color: #fff; margin: 20rpx; border-radius: 16rpx; padding: 30rpx; }
|
||||
.section-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 24rpx; }
|
||||
.overview-grid { display: flex; flex-wrap: wrap; }
|
||||
.date-btn.active { background-color: rgb(66, 121, 240); color: #fff; font-weight: bold; }
|
||||
.overview-section, .trend-section, .product-section { background-color: #fff; margin: 20rpx 20rpx 0; border-radius: 16rpx; padding: 30rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04); }
|
||||
.section-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 24rpx; padding-left: 18rpx; border-left-width: 6rpx; border-left-style: solid; border-left-color: rgb(66, 121, 240); }
|
||||
.overview-grid { display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap; }
|
||||
.overview-item { width: 50%; padding: 20rpx 0; text-align: center; box-sizing: border-box; }
|
||||
.overview-item:nth-child(odd) { border-right: 1rpx solid #f5f5f5; }
|
||||
.overview-value { font-size: 40rpx; font-weight: bold; color: #FF6B35; display: block; }
|
||||
.overview-item:nth-child(odd) { border-right-width: 1rpx; border-right-style: solid; border-right-color: #f5f5f5; }
|
||||
.overview-value { font-size: 40rpx; font-weight: bold; color: rgb(66, 121, 240); display: block; }
|
||||
.overview-label { font-size: 24rpx; color: #999; }
|
||||
.trend-chart { padding: 20rpx 0; }
|
||||
.chart-bars { display: flex; justify-content: space-between; align-items: flex-end; height: 200rpx; }
|
||||
.chart-bars { display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between; align-items: flex-end; height: 200rpx; }
|
||||
.chart-bar-wrapper { display: flex; flex-direction: column; align-items: center; flex: 1; }
|
||||
.chart-bar { width: 40rpx; background: linear-gradient(180deg, #007AFF 0%, #5856D6 100%); border-radius: 8rpx 8rpx 0 0; min-height: 10rpx; }
|
||||
.chart-bar { width: 40rpx; background: linear-gradient(180deg, rgb(66, 121, 240) 0%, #ece9fa 100%); border-radius: 8rpx 8rpx 0 0; min-height: 10rpx; }
|
||||
.chart-label { font-size: 22rpx; color: #999; margin-top: 10rpx; }
|
||||
.product-list { display: flex; flex-direction: column; }
|
||||
.product-item { display: flex; align-items: center; padding: 20rpx 0; border-bottom: 1rpx solid #f5f5f5; }
|
||||
.product-item { display: flex; align-items: center; padding: 20rpx 0; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #f5f5f5; }
|
||||
.product-item:last-child { border-bottom: none; }
|
||||
.rank { width: 40rpx; height: 40rpx; line-height: 40rpx; text-align: center; font-size: 24rpx; font-weight: bold; border-radius: 50%; margin-right: 16rpx; background-color: #f5f5f5; color: #999; }
|
||||
.rank-1 { background-color: #FFD700; color: #fff; }
|
||||
@@ -231,5 +237,5 @@
|
||||
.product-info { flex: 1; }
|
||||
.product-name { font-size: 26rpx; color: #333; display: block; }
|
||||
.product-sales { font-size: 22rpx; color: #999; }
|
||||
.product-revenue { font-size: 28rpx; font-weight: bold; color: #FF6B35; }
|
||||
.product-revenue { font-size: 28rpx; font-weight: bold; color: rgb(66, 121, 240); }
|
||||
</style>
|
||||
|
||||
158
pages/mall/merchant/医养综合服务商城.md
Normal file
158
pages/mall/merchant/医养综合服务商城.md
Normal file
@@ -0,0 +1,158 @@
|
||||
### 医养综合服务商城(老年友好版)
|
||||
|
||||
#### 商家入驻与管理
|
||||
|
||||
##### 入驻审核
|
||||
|
||||
系统应支持医疗服务类、医药健康类、养老服务类、生活服务类及其他适老化服务机构入驻。
|
||||
商家入驻时应提交营业执照、许可证和人员资质证明等材料。
|
||||
系统应支持线上审核、资质公示、店铺开通和装修配置。
|
||||
系统应对服务范围、收费标准及药械安全合规性进行审核。
|
||||
|
||||
##### 信用评级与监管
|
||||
|
||||
系统应建立 5 星制信用评级体系。
|
||||
信用评级应综合服务质量、用户评价和合规情况进行动态评分。
|
||||
评级结果应与曝光量、活动资源和治理策略联动。
|
||||
对于虚假宣传、违规收费、服务不合格等情况,系统应支持警告、处罚、暂停入驻和永久封禁等处理。
|
||||
|
||||
#### 服务/商品展示与推荐
|
||||
|
||||
##### 分类展示
|
||||
|
||||
商城首页应按医疗服务、药品器械、居家护理、生活服务、健康管理等大类展示内容。
|
||||
系统应支持放大字体、清晰图标和分类检索。
|
||||
系统应支持按销量、评分、距离等条件排序。
|
||||
|
||||
##### AI 智能推荐
|
||||
|
||||
系统应综合用户病种、护理等级、健康风险、用药情况、地理位置和消费习惯进行个性化推荐。
|
||||
系统应针对慢病客户、高龄独居长者、术后康复患者等不同人群提供差异化推荐。
|
||||
推荐能力应与全民健康信息平台、肿瘤慢病管理平台或本地健康画像数据联动。
|
||||
|
||||
##### 详情展示与咨询
|
||||
|
||||
商品和服务详情页应采用图文结合短视频的展示形式。
|
||||
详情页应重点展示价格、服务时长、服务范围、注意事项和商家资质。
|
||||
详情页应支持一键拨号、在线文字咨询或电话咨询。
|
||||
展示文案应保持简洁,并使用通俗术语。
|
||||
|
||||
#### 下单与支付
|
||||
|
||||
##### 下单与代下单
|
||||
|
||||
用户选择服务或商品后,系统应支持在少步骤流程内完成地址确认、时间预约和订单提交。
|
||||
系统应支持家属绑定长者账号后进行代下单和远程代支付。
|
||||
|
||||
##### 支付与抵扣
|
||||
|
||||
系统应支持微信、支付宝、社保卡、医保电子凭证、长护险待遇抵扣和子女代付等支付方式。
|
||||
支付页面应清晰展示支付金额、优惠金额和实付金额,不得出现隐藏收费。
|
||||
医保报销部分应按政策口径处理,其余部分应按自费支付结算。
|
||||
|
||||
##### 服务跟踪与评价
|
||||
|
||||
订单生成后,系统应支持接单、出发、到达、服务完成等状态的全流程追踪。
|
||||
在陪诊、送餐等场景下,系统应支持服务人员位置实时共享。
|
||||
服务完成后,系统应支持用户确认和快捷评分。
|
||||
|
||||
#### AI 问诊与健康管理
|
||||
|
||||
##### AI 问诊
|
||||
|
||||
系统应支持用户通过文字描述症状。
|
||||
系统应结合本地医疗知识图谱提供初步健康建议、疑似病因提示及挂号推荐。
|
||||
系统应明确标注“本功能仅供参考,不作为临床诊断依据”。
|
||||
针对胸痛、卒中先兆等高风险主诉,系统应优先提示紧急就医并自动通知预设家属。
|
||||
|
||||
##### 专业咨询
|
||||
|
||||
系统应支持医生、营养师、养老顾问等提供文字或视频咨询服务。
|
||||
系统应对普通咨询、专科咨询等服务类型进行清晰定价和预约管理。
|
||||
|
||||
##### 健康管理与设备接入
|
||||
|
||||
系统应支持对接血压计、血糖仪等健康设备。
|
||||
系统应自动同步健康指标并生成报告。
|
||||
系统应结合健康指标与商城服务、商品形成联动推荐。
|
||||
对于肿瘤或慢病患者,系统应设置专属板块,并提供用药提醒、复查预约、康复指导和专属服务推荐。
|
||||
|
||||
#### 流量变现与运营支持
|
||||
|
||||
##### 盈利模式
|
||||
|
||||
平台收入模型应包括商家入驻费、交易佣金、广告推荐位、搜索置顶和数据化增值服务等方式。
|
||||
具体计费标准、行业差异化策略和分账机制应在运营方案阶段进一步明确。
|
||||
|
||||
##### 用户运营
|
||||
|
||||
系统应支持促销活动、套餐优惠、适老化专题活动、积分体系和线下推广培训等运营能力。
|
||||
|
||||
### 院内系统在平台侧的延伸
|
||||
|
||||
#### 收费与支付系统自动化
|
||||
|
||||
##### 线上支付
|
||||
|
||||
系统应支持银行卡支付、医保电子凭证支付、社保卡支付等线上支付方式。
|
||||
|
||||
##### 费用预缴与自动结算
|
||||
|
||||
系统应支持门诊/住院费用线上预缴与自动核销。
|
||||
支付成功后,系统应同步更新就诊状态和结算状态。
|
||||
若支付失败或跨系统同步异常,系统应自动触发告警并保留线下缴费通道。
|
||||
|
||||
#### 线上服务与小程序开发
|
||||
|
||||
##### 适老化交互设计
|
||||
|
||||
线上终端应遵循老年友好设计原则。
|
||||
界面应保持简洁,支持字体放大和高对比度展示。
|
||||
关键流程应控制在 3 步以内。
|
||||
系统应支持语音播报和一键呼叫等辅助能力。
|
||||
|
||||
##### 多系统数据实时同步
|
||||
|
||||
线上小程序或 APP 应与院内 HIS、医保平台及其他关键系统实现数据同步。
|
||||
系统应保障就诊记录、费用、报销信息和订单信息在各终端显示一致。
|
||||
系统应减少人工录入和重复操作。
|
||||
|
||||
#### 跨系统数据同步
|
||||
|
||||
##### 同步机制
|
||||
|
||||
系统应提供同步机制。
|
||||
系统应对床态、处方、费用、服务轨迹等关键数据实现实时更新。
|
||||
系统应支持统计类数据的增量归集、异步处理和高峰错峰调度。
|
||||
|
||||
##### 容错与审计追踪
|
||||
|
||||
系统应建立完善的容错和审计机制。
|
||||
对于接口失败、数据校验异常、规则冲突或关键流程错误,系统应自动留痕、触发告警并支持追踪定位。
|
||||
在 DIP/DRG 结算、长护险报销等关键业务场景中,数据应可追溯。
|
||||
|
||||
### 系统管理与通用支撑
|
||||
|
||||
#### 账号角色与授权
|
||||
|
||||
系统应基于角色定义管理员、医护人员、护理人员、商家、普通用户和家属等不同身份的数据访问范围与操作权限。
|
||||
系统应支持细粒度授权和按模块分级控制。
|
||||
|
||||
#### 审计日志与异常告警
|
||||
|
||||
系统应记录登录日志、操作日志、接口日志和异常日志。
|
||||
关键日志保存时间应不少于 3 年。
|
||||
日志应支持按账号、时间、模块和对象进行检索。
|
||||
对于同步失败、支付失败、规则触发异常等情况,系统应支持站内消息、短信或其他形式的通知告警。
|
||||
|
||||
#### 统一消息通知
|
||||
|
||||
系统应提供统一消息中心。
|
||||
消息中心应支持待办提醒、审批提醒、服务提醒、支付提醒、风险提醒和整改提醒。
|
||||
消息应支持多终端同步查看。
|
||||
|
||||
#### 语音播报与辅助能力
|
||||
|
||||
APP 端应优先实现语音播报与辅助交互能力。
|
||||
系统应对关键指标、就诊提醒、订单进度和风险提示提供语音输出。
|
||||
小程序端在能力受限时可采用简化提醒方式。
|
||||
117
pages/mall/merchant/错误信息.txt
Normal file
117
pages/mall/merchant/错误信息.txt
Normal file
@@ -0,0 +1,117 @@
|
||||
VM12683 vendor.js:6839 GET http://119.146.131.237:9126/rest/v1/ml_shops?select=*&limit=1&merchant_id=eq.demo-merchant-001 400 (Bad Request)(env: Windows,mp,1.06.2504030; lib: 3.14.2)
|
||||
(anonymous) @ VM12683 vendor.js:6839
|
||||
invokeApi @ VM12683 vendor.js:6217
|
||||
promiseApi @ VM12683 vendor.js:6715
|
||||
(anonymous) @ VM12746 ak-req.js:250
|
||||
doOnce @ VM12746 ak-req.js:248
|
||||
_loop$ @ VM12746 ak-req.js:310
|
||||
s @ VM12687 regeneratorRuntime.js:1
|
||||
(anonymous) @ VM12687 regeneratorRuntime.js:1
|
||||
(anonymous) @ VM12687 regeneratorRuntime.js:1
|
||||
s @ VM12687 regeneratorRuntime.js:1
|
||||
_ @ VM12687 regeneratorRuntime.js:1
|
||||
(anonymous) @ VM12687 regeneratorRuntime.js:1
|
||||
(anonymous) @ VM12687 regeneratorRuntime.js:1
|
||||
fulfilled @ VM12683 vendor.js:9601
|
||||
Promise.then (async)
|
||||
step @ VM12683 vendor.js:9614
|
||||
(anonymous) @ VM12683 vendor.js:9616
|
||||
__awaiter @ VM12683 vendor.js:9598
|
||||
request @ VM12746 ak-req.js:177
|
||||
_callee19$ @ VM12684 aksupa.js:1838
|
||||
s @ VM12687 regeneratorRuntime.js:1
|
||||
(anonymous) @ VM12687 regeneratorRuntime.js:1
|
||||
(anonymous) @ VM12687 regeneratorRuntime.js:1
|
||||
(anonymous) @ VM12683 vendor.js:9616
|
||||
__awaiter @ VM12683 vendor.js:9598
|
||||
requestWithAutoRefresh @ VM12684 aksupa.js:1832
|
||||
_callee11$ @ VM12684 aksupa.js:1492
|
||||
s @ VM12687 regeneratorRuntime.js:1
|
||||
(anonymous) @ VM12687 regeneratorRuntime.js:1
|
||||
(anonymous) @ VM12687 regeneratorRuntime.js:1
|
||||
(anonymous) @ VM12683 vendor.js:9616
|
||||
__awaiter @ VM12683 vendor.js:9598
|
||||
select @ VM12684 aksupa.js:1425
|
||||
_callee$ @ VM12684 aksupa.js:715
|
||||
s @ VM12687 regeneratorRuntime.js:1
|
||||
(anonymous) @ VM12687 regeneratorRuntime.js:1
|
||||
(anonymous) @ VM12687 regeneratorRuntime.js:1
|
||||
(anonymous) @ VM12683 vendor.js:9616
|
||||
__awaiter @ VM12683 vendor.js:9598
|
||||
execute @ VM12684 aksupa.js:691
|
||||
_callee2$ @ shop-edit.uvue:108
|
||||
s @ VM12687 regeneratorRuntime.js:1
|
||||
(anonymous) @ VM12687 regeneratorRuntime.js:1
|
||||
(anonymous) @ VM12687 regeneratorRuntime.js:1
|
||||
(anonymous) @ VM12683 vendor.js:9616
|
||||
__awaiter @ VM12683 vendor.js:9598
|
||||
loadShop @ shop-edit.uvue:101
|
||||
_callee$ @ shop-edit.uvue:95
|
||||
s @ VM12687 regeneratorRuntime.js:1
|
||||
(anonymous) @ VM12687 regeneratorRuntime.js:1
|
||||
(anonymous) @ VM12687 regeneratorRuntime.js:1
|
||||
(anonymous) @ VM12683 vendor.js:9616
|
||||
__awaiter @ VM12683 vendor.js:9598
|
||||
initMerchantId @ shop-edit.uvue:86
|
||||
onLoad @ shop-edit.uvue:82
|
||||
callWithErrorHandling @ VM12683 vendor.js:1733
|
||||
callWithAsyncErrorHandling @ VM12683 vendor.js:1740
|
||||
hook.__weh.hook.__weh @ VM12683 vendor.js:2725
|
||||
invokeArrayFns @ VM12683 vendor.js:259
|
||||
callHook @ VM12683 vendor.js:8827
|
||||
methods.onLoad @ VM12683 vendor.js:9367
|
||||
Show 30 more frames
|
||||
Error: MiniProgramError
|
||||
{"errMsg":"navigateTo:fail page \"pages/mall/merchant/health-management\" is not found"}
|
||||
at Object.errorReport (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at Function.thirdErrorReport (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at Object.thirdErrorReport (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at i (VM12659 WASubContext.js:1)
|
||||
at Object.cb (VM12659 WASubContext.js:1)
|
||||
at X._privEmit (VM12659 WASubContext.js:1)
|
||||
at X.emit (VM12659 WASubContext.js:1)
|
||||
at VM12659 WASubContext.js:1
|
||||
at n (VM12659 WASubContext.js:1)
|
||||
at He (VM12659 WASubContext.js:1)(env: Windows,mp,1.06.2504030; lib: 3.14.2)
|
||||
VM12683 vendor.js:7499 {reason: {…}, promise: Promise}(env: Windows,mp,1.06.2504030; lib: 3.14.2)
|
||||
onError2 @ VM12683 vendor.js:7499
|
||||
(anonymous) @ VM12683 vendor.js:6764
|
||||
Error: SystemError (appServiceSDKScriptError)
|
||||
{"errMsg":"SocketTask.send:fail SocketTask.readyState is not OPEN"}
|
||||
at Function.errorReport (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at Object.<anonymous> (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at Object.cb (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at J._privEmit (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at J.emit (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at WAServiceMainContext.js?t=wechat&v=3.14.2:1
|
||||
at a (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at ze (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at Object.He (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at ae (WAServiceMainContext.js?t=wechat&v=3.14.2:1)(env: Windows,mp,1.06.2504030; lib: 3.14.2)
|
||||
Error: MiniProgramError
|
||||
{"errMsg":"navigateTo:fail page \"pages/mall/merchant/ai-consultation\" is not found"}
|
||||
at Object.errorReport (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at Function.thirdErrorReport (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at Object.thirdErrorReport (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at i (VM12659 WASubContext.js:1)
|
||||
at Object.cb (VM12659 WASubContext.js:1)
|
||||
at X._privEmit (VM12659 WASubContext.js:1)
|
||||
at X.emit (VM12659 WASubContext.js:1)
|
||||
at VM12659 WASubContext.js:1
|
||||
at n (VM12659 WASubContext.js:1)
|
||||
at He (VM12659 WASubContext.js:1)(env: Windows,mp,1.06.2504030; lib: 3.14.2)
|
||||
VM12683 vendor.js:7499 {reason: {…}, promise: Promise}(env: Windows,mp,1.06.2504030; lib: 3.14.2)
|
||||
onError2 @ VM12683 vendor.js:7499
|
||||
(anonymous) @ VM12683 vendor.js:6764
|
||||
Error: SystemError (appServiceSDKScriptError)
|
||||
{"errMsg":"SocketTask.send:fail SocketTask.readyState is not OPEN"}
|
||||
at Function.errorReport (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at Object.<anonymous> (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at Object.cb (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at J._privEmit (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at J.emit (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at WAServiceMainContext.js?t=wechat&v=3.14.2:1
|
||||
at a (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at ze (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at Object.He (WAServiceMainContext.js?t=wechat&v=3.14.2:1)
|
||||
at ae (WAServiceMainContext.js?t=wechat&v=3.14.2:1)(env: Windows,mp,1.06.2504030; lib: 3.14.2)
|
||||
Reference in New Issue
Block a user