初始化上传医疗项目到 medical-mall

This commit is contained in:
2026-04-10 09:03:21 +08:00
parent ca8794ea3a
commit ce124e7119
421 changed files with 15139 additions and 2363 deletions

View 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>

View 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>

View File

@@ -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: '',

View File

@@ -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;

View File

@@ -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>

View File

@@ -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[]
}

View 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>

View File

@@ -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 @@

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,158 @@
### 医养综合服务商城(老年友好版)
#### 商家入驻与管理
##### 入驻审核
系统应支持医疗服务类、医药健康类、养老服务类、生活服务类及其他适老化服务机构入驻。
商家入驻时应提交营业执照、许可证和人员资质证明等材料。
系统应支持线上审核、资质公示、店铺开通和装修配置。
系统应对服务范围、收费标准及药械安全合规性进行审核。
##### 信用评级与监管
系统应建立 5 星制信用评级体系。
信用评级应综合服务质量、用户评价和合规情况进行动态评分。
评级结果应与曝光量、活动资源和治理策略联动。
对于虚假宣传、违规收费、服务不合格等情况,系统应支持警告、处罚、暂停入驻和永久封禁等处理。
#### 服务/商品展示与推荐
##### 分类展示
商城首页应按医疗服务、药品器械、居家护理、生活服务、健康管理等大类展示内容。
系统应支持放大字体、清晰图标和分类检索。
系统应支持按销量、评分、距离等条件排序。
##### AI 智能推荐
系统应综合用户病种、护理等级、健康风险、用药情况、地理位置和消费习惯进行个性化推荐。
系统应针对慢病客户、高龄独居长者、术后康复患者等不同人群提供差异化推荐。
推荐能力应与全民健康信息平台、肿瘤慢病管理平台或本地健康画像数据联动。
##### 详情展示与咨询
商品和服务详情页应采用图文结合短视频的展示形式。
详情页应重点展示价格、服务时长、服务范围、注意事项和商家资质。
详情页应支持一键拨号、在线文字咨询或电话咨询。
展示文案应保持简洁,并使用通俗术语。
#### 下单与支付
##### 下单与代下单
用户选择服务或商品后,系统应支持在少步骤流程内完成地址确认、时间预约和订单提交。
系统应支持家属绑定长者账号后进行代下单和远程代支付。
##### 支付与抵扣
系统应支持微信、支付宝、社保卡、医保电子凭证、长护险待遇抵扣和子女代付等支付方式。
支付页面应清晰展示支付金额、优惠金额和实付金额,不得出现隐藏收费。
医保报销部分应按政策口径处理,其余部分应按自费支付结算。
##### 服务跟踪与评价
订单生成后,系统应支持接单、出发、到达、服务完成等状态的全流程追踪。
在陪诊、送餐等场景下,系统应支持服务人员位置实时共享。
服务完成后,系统应支持用户确认和快捷评分。
#### AI 问诊与健康管理
##### AI 问诊
系统应支持用户通过文字描述症状。
系统应结合本地医疗知识图谱提供初步健康建议、疑似病因提示及挂号推荐。
系统应明确标注“本功能仅供参考,不作为临床诊断依据”。
针对胸痛、卒中先兆等高风险主诉,系统应优先提示紧急就医并自动通知预设家属。
##### 专业咨询
系统应支持医生、营养师、养老顾问等提供文字或视频咨询服务。
系统应对普通咨询、专科咨询等服务类型进行清晰定价和预约管理。
##### 健康管理与设备接入
系统应支持对接血压计、血糖仪等健康设备。
系统应自动同步健康指标并生成报告。
系统应结合健康指标与商城服务、商品形成联动推荐。
对于肿瘤或慢病患者,系统应设置专属板块,并提供用药提醒、复查预约、康复指导和专属服务推荐。
#### 流量变现与运营支持
##### 盈利模式
平台收入模型应包括商家入驻费、交易佣金、广告推荐位、搜索置顶和数据化增值服务等方式。
具体计费标准、行业差异化策略和分账机制应在运营方案阶段进一步明确。
##### 用户运营
系统应支持促销活动、套餐优惠、适老化专题活动、积分体系和线下推广培训等运营能力。
### 院内系统在平台侧的延伸
#### 收费与支付系统自动化
##### 线上支付
系统应支持银行卡支付、医保电子凭证支付、社保卡支付等线上支付方式。
##### 费用预缴与自动结算
系统应支持门诊/住院费用线上预缴与自动核销。
支付成功后,系统应同步更新就诊状态和结算状态。
若支付失败或跨系统同步异常,系统应自动触发告警并保留线下缴费通道。
#### 线上服务与小程序开发
##### 适老化交互设计
线上终端应遵循老年友好设计原则。
界面应保持简洁,支持字体放大和高对比度展示。
关键流程应控制在 3 步以内。
系统应支持语音播报和一键呼叫等辅助能力。
##### 多系统数据实时同步
线上小程序或 APP 应与院内 HIS、医保平台及其他关键系统实现数据同步。
系统应保障就诊记录、费用、报销信息和订单信息在各终端显示一致。
系统应减少人工录入和重复操作。
#### 跨系统数据同步
##### 同步机制
系统应提供同步机制。
系统应对床态、处方、费用、服务轨迹等关键数据实现实时更新。
系统应支持统计类数据的增量归集、异步处理和高峰错峰调度。
##### 容错与审计追踪
系统应建立完善的容错和审计机制。
对于接口失败、数据校验异常、规则冲突或关键流程错误,系统应自动留痕、触发告警并支持追踪定位。
在 DIP/DRG 结算、长护险报销等关键业务场景中,数据应可追溯。
### 系统管理与通用支撑
#### 账号角色与授权
系统应基于角色定义管理员、医护人员、护理人员、商家、普通用户和家属等不同身份的数据访问范围与操作权限。
系统应支持细粒度授权和按模块分级控制。
#### 审计日志与异常告警
系统应记录登录日志、操作日志、接口日志和异常日志。
关键日志保存时间应不少于 3 年。
日志应支持按账号、时间、模块和对象进行检索。
对于同步失败、支付失败、规则触发异常等情况,系统应支持站内消息、短信或其他形式的通知告警。
#### 统一消息通知
系统应提供统一消息中心。
消息中心应支持待办提醒、审批提醒、服务提醒、支付提醒、风险提醒和整改提醒。
消息应支持多终端同步查看。
#### 语音播报与辅助能力
APP 端应优先实现语音播报与辅助交互能力。
系统应对关键指标、就诊提醒、订单进度和风险提示提供语音输出。
小程序端在能力受限时可采用简化提醒方式。

View 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)