This commit is contained in:
Lwei
2026-06-22 11:23:05 +08:00
commit 234d126ff6
998 changed files with 355523 additions and 0 deletions
@@ -0,0 +1,11 @@
<!-- 底部占位 -->
<div class="bottom-safe"></div>
<!-- 底部导航 -->
<van-tabbar v-model="activeTab" active-color="#058397" fixed placeholder safe-area-inset-bottom>
<van-tabbar-item icon="home-o" name="home">首页</van-tabbar-item>
<van-tabbar-item icon="search" name="nurses">找护工</van-tabbar-item>
<van-tabbar-item icon="plus" name="publish" dot>发布订单</van-tabbar-item>
<van-tabbar-item icon="notes-o" name="order">订单</van-tabbar-item>
<van-tabbar-item icon="user-o" name="mine">我的</van-tabbar-item>
</van-tabbar>
+67
View File
@@ -0,0 +1,67 @@
// var apiUrl = "$apiUrl";
// var token = "$token";
// var apiRequest = function (url, data = {}, method = "GET") {
// let _RES = null;
// $.ajax({
// url: apiUrl + url,
// beforeSend: function (request) {
// request.setRequestHeader("Authorization", token);
// },
// method: method,
// data: data,
// async: false,
// success: function (res) {
// _RES = res
// }
// });
// return _RES;
// }
// //延时异步请求
// var apiSyncRequest = function (url, data = {}, method = "GET") {
// var defer = $.Deferred();
// $.ajax({
// url: apiUrl + url,
// beforeSend: function (request) {
// request.setRequestHeader("Authorization", token);
// },
// method: method,
// data: data,
// async: false,
// success: function (res) {
// defer.resolve(res);
// }
// });
// return defer.promise();
// }
// var getQueryVal = function (name) {
// return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.href) || [, ""])[1].replace(/\+/g, '%20')) || null
// };
// var wxconfig = function () {
// let rs = apiRequest("Wechat/JSSDK");
// wx.config(rs.data);
// wx.error(function (res) {
// console.log(res)
// });
// }
// 底部导航路由配置
const TabBarRoutes = {
home: '/PatientApp/Index/index',
nurses: '/PatientApp/Nursing/index',
publish: '/PatientApp/Order/create',
order: '/PatientApp/Order/index',
mine: '/PatientApp/Mine/index'
};
// 通用跳转方法
function onTabBarChange(name) {
if (TabBarRoutes[name]) {
window.location.href = TabBarRoutes[name];
}
}
console.log('');
+218
View File
@@ -0,0 +1,218 @@
{extend name="PatientAPP/layout" /}
{block name="css"}
<link rel="stylesheet" href="/PatientAPP/Index/index.css" />
{/block}
{block name="app"}
<div id="app" v-cloak>
<!-- 头部区域 -->
<div class="header-section">
<div class="header-top">
<div class="user-info">
<div class="user-avatar">
<van-icon name="user-o" size="24" />
</div>
<div>
<div class="user-name">您好,患者用户</div>
<div class="user-desc">祝您早日康复</div>
</div>
</div>
<div class="header-actions">
<div class="icon-btn" @click="onNotice">
<van-icon name="bell" size="17" />
</div>
<div class="icon-btn" @click="onSetting">
<van-icon name="setting-o" size="17" />
</div>
</div>
</div>
<!-- 搜索栏 -->
<div class="search-wrapper">
<van-search placeholder="搜索护工姓名/技能" shape="round" background="transparent" @search="onSearch" />
</div>
</div>
<!-- 快捷功能区(专业版) -->
<div class="quick-actions">
<div class="qa-header">
<div class="qa-title"><span class="qa-title-dot"></span> 服务导航</div>
<div class="qa-more">更多服务 <van-icon name="arrow" size="11" /></div>
</div>
<div class="qa-body">
<div class="qa-grid">
<div class="qa-item" @click="goPublish('hospital')">
<div class="qa-icon-wrap primary">
<van-icon class="qa-icon-primary" name="edit" size="23" />
</div>
<span class="qa-label">发布订单</span>
</div>
<div class="qa-item" @click="goNurses()">
<div class="qa-icon-wrap info">
<van-icon class="qa-icon-info" name="friends-o" size="23" />
</div>
<span class="qa-label">选择护工</span>
</div>
<div class="qa-item" @click="goOrders()">
<div class="qa-icon-wrap cyan">
<van-icon class="qa-icon-cyan" name="notes-o" size="23" />
</div>
<span class="qa-label">我的订单</span>
</div>
<div class="qa-item" @click="goService()">
<div class="qa-icon-wrap warning">
<van-icon class="qa-icon-warning" name="service-o" size="23" />
</div>
<span class="qa-label">在线客服</span>
</div>
<div class="qa-item" @click="goPublish('home')">
<div class="qa-icon-wrap success">
<van-icon name="hotel-o" size="23" color="#1a7a32" />
</div>
<span class="qa-label">住院陪护</span>
</div>
<div class="qa-item" @click="goPublish('accompany')">
<div class="qa-icon-wrap purple">
<van-icon class="qa-icon-purple" name="guide-o" size="23" />
</div>
<span class="qa-label">门诊陪护</span>
</div>
<div class="qa-item" @click="goService()">
<div class="qa-icon-wrap gold">
<van-icon class="qa-icon-gold" name="certificate" size="23" />
</div>
<span class="qa-label">资质认证</span>
</div>
<div class="qa-item" @click="onNotice()">
<div class="qa-icon-wrap danger">
<van-icon class="qa-icon-danger" name="info-o" size="23" />
</div>
<span class="qa-label">帮助中心</span>
</div>
</div>
</div>
<!-- 数据统计条 -->
<div class="qa-stats">
<div class="qa-stat">
<div class="qa-stat-num">1,280+</div>
<div class="qa-stat-lbl">专业护工</div>
</div>
<div class="qa-stat">
<div class="qa-stat-num">98.6%</div>
<div class="qa-stat-lbl">好评率</div>
</div>
<div class="qa-stat">
<div class="qa-stat-num">52,000+</div>
<div class="qa-stat-lbl">服务次数</div>
</div>
<div class="qa-stat">
<div class="qa-stat-num">24h</div>
<div class="qa-stat-lbl">快速响应</div>
</div>
</div>
</div>
<!-- 公告栏 -->
<div class="notice-section">
<van-notice-bar left-icon="volume-o" :scrollable="true" text="系统公告:智能陪护系统已全新升级,提供更优质的陪护服务体验!" />
</div>
<!-- 快速下单入口 -->
<div class="order-entry">
<div class="order-entry-header">
<div class="order-entry-left">
<div class="order-entry-title">需要专业陪护服务?</div>
<div class="order-entry-desc">一键发布需求,专业持证护工快速响应</div>
<div class="order-entry-btns">
<van-button type="primary" round size="small" @click="goPublish('hospital')">就诊陪护</van-button>
<van-button type="primary" round plain size="small" @click="goPublish('home')">住院陪护</van-button>
<van-button type="primary" round plain size="small"
@click="goPublish('accompany')">居家陪护</van-button>
</div>
</div>
<div class="order-entry-right">
<van-icon name="medal-o" size="30" color="rgba(255,255,255,0.85)" />
</div>
</div>
</div>
<!-- 推荐护工 -->
<div class="nurse-section">
<div class="section-header">
<div class="section-title">
<van-icon name="medal-o" color="#018fa5" size="18" /> <span style="font-weight:600">&nbsp;推荐护工</span>
</div>
<div class="section-more" @click="goNurses">
查看更多 <van-icon name="arrow" />
</div>
</div>
<div class="nurse-list">
<div class="nurse-card" v-for="(nurse, index) in nurseList" :key="index" @click="viewNurse(nurse)">
<div class="nurse-avatar">
<van-icon name="contact" />
</div>
<div class="nurse-info">
<div class="nurse-name-row">
<span class="nurse-name">{{ nurse.name }}</span>
<span class="nurse-rating">
<van-icon name="star" /> {{ nurse.rating }}
</span>
</div>
<div class="nurse-tags">
<span class="nurse-tag green" v-for="(tag, i) in nurse.certs.slice(0, 2)" :key="'c'+i">{{ tag
}}</span>
<span class="nurse-tag orange" v-if="nurse.isHot">热门</span>
</div>
<div class="nurse-tags">
<span class="nurse-tag teal" v-for="(skill, i) in nurse.skills.slice(0, 3)" :key="'s'+i">{{
skill }}</span>
</div>
<div class="nurse-bottom">
<span class="nurse-price"><span>&yen;</span>{{ nurse.price }}<span>/小时</span></span>
<span class="nurse-exp">{{ nurse.exp }}年经验 | {{ nurse.orders }}次服务</span>
</div>
</div>
</div>
</div>
<!-- 底部栏 -->
{include file="PatientAPP/Component/van-tabbar" /}
</div>
{/block}
{block name="js"}
<script>
const app = Vue.createApp({
data() {
return {
activeTab: 'home',
nurseList: [
{ id: 1, name: '张秀芳', rating: 4.9, price: 68, exp: 8, orders: 320, isHot: true, certs: ['护理证', '健康证'], skills: ['术后护理', '老年照护', '康复训练'] },
{ id: 2, name: '李国华', rating: 4.8, price: 75, exp: 10, orders: 456, isHot: true, certs: ['护士资格证', '急救证'], skills: ['重症监护', '输液换药', '心理疏导'] },
{ id: 3, name: '王美玲', rating: 4.9, price: 62, exp: 5, orders: 218, isHot: false, certs: ['护理证', '育婴师'], skills: ['母婴护理', '月子照护', '营养配餐'] },
{ id: 4, name: '陈建国', rating: 4.7, price: 58, exp: 6, orders: 167, isHot: false, certs: ['护理证', '康复师'], skills: ['行动辅助', '日常起居', '陪同就医'] },
],
};
},
methods: {
onSearch(val) { console.log('搜索:', val); },
onNotice() { vant.showToast({ message: '暂无新消息', position: 'top' }); },
onSetting() { vant.showToast('设置'); },
goPublish(type) {
vant.dialog.alert({
title: '发布陪护订单',
message: type ? `正在进入${type === 'hospital' ? '院内' : type === 'home' ? '居家' : '陪同'}陪护订单发布页面...` : '正在进入订单发布页面...',
});
},
goNurses() { vant.showToast('跳转到护工列表'); },
goOrders() { vant.showToast('跳转到我的订单'); },
goService() { vant.showToast('联系在线客服'); },
viewNurse(nurse) {
vant.showDialog({ title: nurse.name, message: `评分: ${nurse.rating}\n价格: ¥${nurse.price}/小时\n经验: ${nurse.exp}\n服务: ${nurse.orders}` });
//vant.dialog.alert({ title: nurse.name, message: `评分: ${nurse.rating}分\n价格: ¥${nurse.price}/小时\n经验: ${nurse.exp}年\n服务: ${nurse.orders}次` });
},
},
watch: {
activeTab(val) { if (val !== 'home') {
onTabBarChange(val);
}},
},
});
app.use(vant);
app.use(vant.Lazyload);
app.mount('#app');
</script>
{/block}
+332
View File
@@ -0,0 +1,332 @@
{extend name="PatientAPP/layout" /}
{block name="css"}
<style>
.mine-container { background: #f5f6fa; min-height: 100vh; padding-bottom: 70px; }
.mine-header {
background: linear-gradient(135deg, #018fa5 0%, #027a8c 50%, #016577 100%);
padding: 40px 20px 30px;
position: relative;
overflow: hidden;
}
.mine-header::before {
content: '';
position: absolute;
top: -50%;
right: -20%;
width: 200px;
height: 200px;
background: rgba(255,255,255,0.08);
border-radius: 50%;
}
.mine-header::after {
content: '';
position: absolute;
bottom: -30%;
left: -10%;
width: 150px;
height: 150px;
background: rgba(255,255,255,0.05);
border-radius: 50%;
}
.user-profile { display: flex; align-items: center; position: relative; z-index: 1; }
.user-avatar-box {
width: 65px; height: 65px;
border-radius: 50%;
background: rgba(255,255,255,0.2);
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
border: 2px solid rgba(255,255,255,0.35);
}
.user-avatar-box .van-icon { color: #fff; font-size: 32px; }
.user-text { color: #fff; flex: 1; }
.user-nickname { font-size: 19px; font-weight: 600; margin-bottom: 4px; }
.user-phone { font-size: 13px; opacity: 0.8; }
.user-arrow { color: rgba(255,255,255,0.7); }
/* 统计卡片 */
.stats-card {
margin: -18px 15px 12px;
background: #fff;
border-radius: 14px;
padding: 18px 10px;
box-shadow: 0 3px 12px rgba(1,143,165,0.1);
position: relative;
z-index: 2;
display: flex;
}
.stat-item { flex: 1; text-align: center; cursor: pointer; }
.stat-num { font-size: 21px; font-weight: 700; color: #333; }
.stat-label { font-size: 12px; color: #999; margin-top: 4px; }
.stat-item:nth-child(1) .stat-num { color: #018fa5; }
.stat-item:nth-child(2) .stat-num { color: #e6a23c; }
.stat-item:nth-child(3) .stat-num { color: #67c23a; }
/* 订单快捷入口 */
.order-section {
margin: 0 15px 12px;
background: #fff;
border-radius: 14px;
padding: 16px 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
}
.section-header-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px 14px;
}
.section-title-text { font-size: 15px; font-weight: 600; color: #333; }
.section-more-link { font-size: 12px; color: #999; display: flex; align-items: center; }
.order-grid { display: flex; padding: 0 10px; }
.order-item { flex: 1; text-align: center; cursor: pointer; padding: 8px 0; }
.order-icon-wrap {
width: 44px; height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 8px;
position: relative;
}
.order-icon-wrap .van-badge { position: absolute; top: -2px; right: -2px; }
.order-label { font-size: 12px; color: #666; }
/* 功能菜单 */
.menu-section {
margin: 0 15px 12px;
background: #fff;
border-radius: 14px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
}
.menu-section .van-cell { padding: 14px 16px; }
.menu-section .van-cell__left-icon { color: #018fa5; margin-right: 10px; font-size: 20px; }
.menu-icon-wrap {
width: 28px; height: 28px;
border-radius: 7px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
flex-shrink: 0;
}
.menu-icon-wrap .van-icon { font-size: 16px; color: #fff; }
</style>
{/block}
{block name="app"}
<div id="app" v-cloak>
<div class="mine-container">
<!-- 头部用户信息 -->
<div class="mine-header">
<div class="user-profile">
<div class="user-avatar-box">
<van-icon name="user-o" />
</div>
<div class="user-text">
<div class="user-nickname">{{ userInfo.nickname }}</div>
<div class="user-phone">{{ userInfo.phone }}</div>
</div>
<van-icon name="arrow" class="user-arrow" size="16" />
</div>
</div>
<!-- 订单统计 -->
<div class="stats-card">
<div class="stat-item" @click="goOrderList('all')">
<div class="stat-num">{{ stats.totalOrders }}</div>
<div class="stat-label">全部订单</div>
</div>
<div class="stat-item" @click="goOrderList('pending')">
<div class="stat-num">{{ stats.pending }}</div>
<div class="stat-label">待服务</div>
</div>
<div class="stat-item" @click="goOrderList('completed')">
<div class="stat-num">{{ stats.completed }}</div>
<div class="stat-label">已完成</div>
</div>
</div>
<!-- 我的订单 -->
<div class="order-section">
<div class="section-header-bar">
<span class="section-title-text">我的订单</span>
<div class="section-more-link" @click="goOrderList('all')">
全部订单 <van-icon name="arrow" size="11" />
</div>
</div>
<div class="order-grid">
<div class="order-item" @click="goOrderList('pending_payment')">
<div class="order-icon-wrap" style="background:#fff4e5;">
<van-icon name="clock-o" color="#e6a23c" size="20" />
<van-badge :content="orderBadge.pendingPayment" v-if="orderBadge.pendingPayment > 0" />
</div>
<span class="order-label">待付款</span>
</div>
<div class="order-item" @click="goOrderList('pending_service')">
<div class="order-icon-wrap" style="background:#e8f4fd;">
<van-icon name="todo-list-o" color="#1989fa" size="20" />
<van-badge :content="orderBadge.pendingService" v-if="orderBadge.pendingService > 0" />
</div>
<span class="order-label">待服务</span>
</div>
<div class="order-item" @click="goOrderList('in_progress')">
<div class="order-icon-wrap" style="background:#eaf9f1;">
<van-icon name="guide-o" color="#07c160" size="20" />
<van-badge :content="orderBadge.inProgress" v-if="orderBadge.inProgress > 0" />
</div>
<span class="order-label">进行中</span>
</div>
<div class="order-item" @click="goOrderList('completed')">
<div class="order-icon-wrap" style="background:#f0f2f5;">
<van-icon name="certificate" color="#969799" size="20" />
</div>
<span class="order-label">已完成</span>
</div>
<div class="order-item" @click="goOrderList('review')">
<div class="order-icon-wrap" style="background:#fef0f0;">
<van-icon name="star-o" color="#ee0a24" size="20" />
<van-badge :content="orderBadge.review" v-if="orderBadge.review > 0" />
</div>
<span class="order-label">待评价</span>
</div>
</div>
</div>
<!-- 工具服务 -->
<div class="menu-section">
<van-cell title="我的收藏" is-link @click="onMenuClick('collect')">
<template #icon>
<div class="menu-icon-wrap" style="background:linear-gradient(135deg,#ff6b6b,#ff8e53);">
<van-icon name="like-o" />
</div>
</template>
</van-cell>
<van-cell title="常用护工" is-link @click="onMenuClick('favorite_nurse')">
<template #icon>
<div class="menu-icon-wrap" style="background:linear-gradient(135deg,#018fa5,#02b5d1);">
<van-icon name="contact" />
</div>
</template>
</van-cell>
<van-cell title="地址管理" is-link @click="onMenuClick('address')">
<template #icon>
<div class="menu-icon-wrap" style="background:linear-gradient(135deg,#67c23a,#85ce61);">
<van-icon name="location-o" />
</div>
</template>
</van-cell>
<van-cell title="在线客服" is-link @click="onMenuClick('service')">
<template #icon>
<div class="menu-icon-wrap" style="background:linear-gradient(135deg,#e6a23c,#f0c78a);">
<van-icon name="service-o" />
</div>
</template>
</van-cell>
</div>
<!-- 设置相关 -->
<div class="menu-section">
<van-cell title="账号设置" is-link @click="onMenuClick('setting')">
<template #icon>
<div class="menu-icon-wrap" style="background:linear-gradient(135deg,#909399,#b1b3b8);">
<van-icon name="setting-o" />
</div>
</template>
</van-cell>
<van-cell title="关于我们" is-link @click="onMenuClick('about')">
<template #icon>
<div class="menu-icon-wrap" style="background:linear-gradient(135deg,#7232dd,#9b69e6);">
<van-icon name="info-o" />
</div>
</template>
</van-cell>
</div>
<!-- 退出按钮 -->
<div style="padding: 20px 15px;">
<van-button type="danger" block round plain @click="onLogout">退出登录</van-button>
</div>
<!-- 底部栏 -->
{include file="PatientAPP/Component/van-tabbar" /}
</div>
</div>
{/block}
{block name="js"}
<script>
const app = Vue.createApp({
data() {
return {
activeTab: 'mine',
userInfo: {
nickname: '患者用户',
phone: '138****8888',
avatar: ''
},
stats: {
totalOrders: 12,
pending: 2,
completed: 10
},
orderBadge: {
pendingPayment: 0,
pendingService: 1,
inProgress: 1,
review: 0
},
menuList: [
{ icon: 'like-o', title: '我的收藏', color: '#ff6b6b', action: 'collect' },
{ icon: 'contact', title: '常用护工', color: '#018fa5', action: 'favorite_nurse' },
{ icon: 'location-o', title: '地址管理', color: '#67c23a', action: 'address' },
{ icon: 'service-o', title: '在线客服', color: '#e6a23c', action: 'service' },
]
};
},
methods: {
goOrderList(type) {
const map = {
all: '全部订单',
pending_payment: '待付款',
pending_service: '待服务',
pending: '待服务',
in_progress: '进行中',
completed: '已完成',
review: '待评价'
};
vant.showToast({ message: `查看${map[type] || '订单列表'}`, position: 'top' });
},
onMenuClick(action) {
const map = {
collect: '我的收藏',
favorite_nurse: '常用护工',
address: '地址管理',
service: '在线客服',
setting: '账号设置',
about: '关于我们'
};
vant.showToast({ message: map[action] || action, position: 'top' });
},
onLogout() {
vant.showDialog({
title: '提示',
message: '确定要退出登录吗?'
}).then(() => {
vant.showToast({ type: 'success', message: '已退出登录' });
}).catch(() => {});
}
},
watch: {
activeTab(val) {
if (val !== 'mine') onTabBarChange(val);
}
}
});
app.use(vant);
app.use(vant.Lazyload);
app.mount('#app');
</script>
{/block}
+318
View File
@@ -0,0 +1,318 @@
{extend name="PatientAPP/layout" /}
{block name="css"}
<link rel="stylesheet" href="index.css" />
{/block}
{block name="app"}
<div id="app" v-cloak>
<!-- 头部区域 -->
<div class="header-section">
<!-- 搜索栏 -->
<div class="header-search" @click="showSearch = true">
<van-icon name="search" size="16" color="#999" />
<span>搜索护工姓名/技能...</span>
</div>
<!-- 筛选标签栏 -->
<div class="filter-bar">
<div class="filter-tag" :class="{ active: filterSkill }" @click="toggleFilter('skill')">{{ filterSkill || '技能类型' }}</div>
<div class="filter-tag" :class="{ active: filterPrice }" @click="toggleFilter('price')">{{ currentPriceLabel }}</div>
<div class="filter-tag" :class="{ active: filterExp }" @click="toggleFilter('exp')">{{ currentExpLabel }}</div>
<div class="filter-tag" :class="{ active: filterDateRange.length > 0 }" @click="showDateCalendar = true">{{ currentDateLabel }}</div>
<div class="filter-tag" :class="{ active: sortBy !== 'rating' }" @click="toggleFilter('sort')">{{ currentSortLabel }}</div>
</div>
<!-- 筛选面板(内联展开) -->
<transition name="slide">
<div class="filter-panel" v-if="showFilterPanel" @click.stop>
<div class="filter-panel-list">
<span v-for="item in popupOptions" :key="item.value"
class="filter-panel-item" :class="{ selected: isPopupSelected(item.value) }"
@click="onPopupSelect(item.value)">{{ item.text }}</span>
</div>
</div>
</transition>
</div>
<!-- 统计信息 -->
<div class="stats-bar">
<div class="stats-item">
<span class="stats-num">{{ filteredList.length }}</span>
<span class="stats-label">位护工</span>
</div>
<div class="stats-divider"></div>
<div class="stats-item">
<span class="stats-num">98.6%</span>
<span class="stats-label">好评率</span>
</div>
<div class="stats-divider"></div>
<div class="stats-item">
<span class="stats-num">24h</span>
<span class="stats-label">快速响应</span>
</div>
</div>
<!-- 护工列表 -->
<div class="nurse-list-wrapper">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<div class="nurse-card-wrap" v-for="(nurse, index) in filteredList" :key="nurse.id" @click="viewNurse(nurse)">
<div class="nurse-card-main">
<!-- 左侧头像 -->
<div class="nurse-avatar-box">
<div class="nurse-avatar-inner" :class="{ 'avatar-online': nurse.online }">
<van-icon name="contact" size="36" color="#fff" />
</div>
<span class="online-dot" v-if="nurse.online"></span>
</div>
<!-- 中间信息 -->
<div class="nurse-detail">
<div class="nurse-name-row">
<span class="nurse-name">{{ nurse.name }}</span>
<van-tag type="primary" size="small" plain v-if="nurse.isHot">热门</van-tag>
<van-tag type="success" size="small" plain v-if="nurse.certified">认证</van-tag>
</div>
<div class="nurse-meta-row">
<van-rate :value="nurse.rating" readonly size="12" color="#ff976a" void-icon="star" void-color="#eee" />
<span class="nurse-score">{{ nurse.rating }}</span>
<span class="nurse-sep">|</span>
<span class="nurse-exp-text">{{ nurse.exp }}年经验</span>
<span class="nurse-sep">|</span>
<span class="nurse-orders-text">服务{{ nurse.orders }}次</span>
</div>
<div class="nurse-skill-tags">
<van-tag type="primary" plain size="medium" v-for="(skill, i) in nurse.skills.slice(0, 4)" :key="'s'+i">{{ skill }}</van-tag>
</div>
</div>
<!-- 右侧价格 -->
<div class="nurse-price-col">
<div class="nurse-price-row">
<span class="price-symbol">&yen;</span>
<span class="price-num">{{ nurse.price }}</span>
<span class="price-unit">/时</span>
</div>
<van-button type="primary" size="small" round class="book-btn" @click.stop="bookNurse(nurse)">预约</van-button>
</div>
</div>
<!-- 底部认证信息 -->
<div class="nurse-cert-row" v-if="nurse.certs && nurse.certs.length">
<van-icon name="shield-o" size="14" color="#07c160" />
<span class="cert-text">{{ nurse.certs.join(' · ') }}</span>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
<!-- 时间范围选择弹窗 -->
<van-popup v-model:show="showDateCalendar" round :style="{ width: '85%', padding: '20px' }">
<div style="font-size: 16px; font-weight: 600; margin-bottom: 16px; text-align: center;">选择时间范围</div>
<div style="display: flex; flex-direction: column; gap: 12px;">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 14px; color: #666; width: 36px;">开始</span>
<input type="date" v-model="dateStart" style="flex:1; border:1px solid #e0e0e0; border-radius:8px; padding:10px 12px; font-size:14px; outline:none;" />
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 14px; color: #666; width: 36px;">结束</span>
<input type="date" v-model="dateEnd" style="flex:1; border:1px solid #e0e0e0; border-radius:8px; padding:10px 12px; font-size:14px; outline:none;" />
</div>
</div>
<div style="margin-top: 18px; display:flex; gap: 12px;">
<van-button style="flex:1" @click="showDateCalendar = false">取消</van-button>
<van-button style="flex:1" type="primary" @click="onDateConfirm">确定</van-button>
</div>
</van-popup>
<!-- 搜索弹窗 -->
<van-popup v-model:show="showSearch" position="top" :style="{ height: 'auto' }">
<van-search
v-model="searchKeyword"
placeholder="搜索护工姓名/技能/资质"
shape="round"
show-action
@search="onSearch"
@cancel="showSearch = false"
>
<template #action>
<div @click="onSearch">搜索</div>
</template>
</van-search>
</van-popup>
<!-- 底部栏 -->
{include file="PatientAPP/Component/van-tabbar" /}
</div>
{/block}
{block name="js"}
<script>
const app = Vue.createApp({
data() {
return {
activeTab: 'nurses',
showSearch: false,
searchKeyword: '',
refreshing: false,
loading: false,
finished: true,
filterSkill: '',
filterPrice: '',
filterExp: '',
sortBy: 'rating',
showFilterPanel: false,
currentFilterType: '',
showDateCalendar: false,
filterDateRange: [],
dateStart: '',
dateEnd: '',
skillOptions: [
{ text: '全部技能', value: '' },
{ text: '术后护理', value: '术后护理' },
{ text: '老年照护', value: '老年照护' },
{ text: '母婴护理', value: '母婴护理' },
{ text: '康复训练', value: '康复训练' },
{ text: '重症监护', value: '重症监护' },
{ text: '输液换药', value: '输液换药' },
{ text: '心理疏导', value: '心理疏导' },
],
priceOptions: [
{ text: '价格排序', value: '' },
{ text: '50元以下', value: 'low' },
{ text: '50-70元', value: 'mid' },
{ text: '70元以上', value: 'high' },
],
expOptions: [
{ text: '经验不限', value: '' },
{ text: '3年以下', value: 'junior' },
{ text: '3-5年', value: 'mid' },
{ text: '5年以上', value: 'senior' },
],
sortOptions: [
{ text: '评分最高', value: 'rating' },
{ text: '价格最低', value: 'price_asc' },
{ text: '服务最多', value: 'orders' },
{ text: '经验最久', value: 'exp' },
],
nurseList: [
{ id: 1, name: '张秀芳', rating: 4.9, price: 68, exp: 8, orders: 320, isHot: true, certified: true, online: true, certs: ['护理证', '健康证'], skills: ['术后护理', '老年照护', '康复训练'] },
{ id: 2, name: '李国华', rating: 4.8, price: 75, exp: 10, orders: 456, isHot: true, certified: true, online: true, certs: ['护士资格证', '急救证'], skills: ['重症监护', '输液换药', '心理疏导'] },
{ id: 3, name: '王美玲', rating: 4.9, price: 62, exp: 5, orders: 218, isHot: false, certified: true, online: false, certs: ['护理证', '育婴师'], skills: ['母婴护理', '月子照护', '营养配餐'] },
{ id: 4, name: '陈建国', rating: 4.7, price: 58, exp: 6, orders: 167, isHot: false, certified: true, online: true, certs: ['护理证', '康复师'], skills: ['行动辅助', '日常起居', '陪同就医'] },
{ id: 5, name: '刘晓红', rating: 4.6, price: 55, exp: 3, orders: 89, isHot: false, certified: false, online: true, certs: ['护理证'], skills: ['基础护理', '饮食照料', '卫生清洁'] },
{ id: 6, name: '赵文斌', rating: 4.8, price: 80, exp: 12, orders: 520, isHot: true, certified: true, online: false, certs: ['护士资格证', '急救证', '康复师'], skills: ['重症监护', '术后护理', '康复训练', '心理疏导'] },
{ id: 7, name: '孙丽萍', rating: 4.5, price: 48, exp: 2, orders: 56, isHot: false, certified: false, online: true, certs: ['护理员培训证'], skills: ['生活照料', '陪伴聊天', '协助活动'] },
{ id: 8, name: '周明辉', rating: 4.7, price: 65, exp: 7, orders: 289, isHot: false, certified: true, online: true, certs: ['健康证', '急救证'], skills: ['老年照护', '慢性病管理', '用药提醒'] },
{ id: 9, name: '吴秀英', rating: 4.9, price: 72, exp: 9, orders: 380, isHot: true, certified: true, online: true, certs: ['护理证', '育婴师', '营养师证'], skills: ['母婴护理', '月子照护', '营养配餐', '产后恢复'] },
{ id: 10, name: '郑海涛', rating: 4.6, price: 60, exp: 4, orders: 145, isHot: false, certified: true, online: false, certs: ['护理证', '康复师'], skills: ['康复训练', '行动辅助', '按摩理疗'] },
],
};
},
computed: {
filteredList() {
let list = [...this.nurseList];
if (this.searchKeyword) {
const kw = this.searchKeyword.toLowerCase();
list = list.filter(n =>
n.name.includes(kw) ||
n.skills.some(s => s.includes(kw)) ||
n.certs.some(c => c.includes(kw))
);
}
if (this.filterSkill) list = list.filter(n => n.skills.includes(this.filterSkill));
if (this.filterPrice === 'low') list = list.filter(n => n.price < 50);
else if (this.filterPrice === 'mid') list = list.filter(n => n.price >= 50 && n.price <= 70);
else if (this.filterPrice === 'high') list = list.filter(n => n.price > 70);
if (this.filterExp === 'junior') list = list.filter(n => n.exp < 3);
else if (this.filterExp === 'mid') list = list.filter(n => n.exp >= 3 && n.exp <= 5);
else if (this.filterExp === 'senior') list = list.filter(n => n.exp > 5);
if (this.sortBy === 'rating') list.sort((a, b) => b.rating - a.rating);
else if (this.sortBy === 'price_asc') list.sort((a, b) => a.price - b.price);
else if (this.sortBy === 'orders') list.sort((a, b) => b.orders - a.orders);
else if (this.sortBy === 'exp') list.sort((a, b) => b.exp - a.exp);
return list;
},
currentPriceLabel() { const item = this.priceOptions.find(o => o.value === this.filterPrice); return item ? item.text : '价格区间'; },
currentExpLabel() { const item = this.expOptions.find(o => o.value === this.filterExp); return item ? item.text : '工作经验'; },
currentSortLabel() { const item = this.sortOptions.find(o => o.value === this.sortBy); return item ? item.text : '默认排序'; },
currentDateLabel() {
if (this.filterDateRange.length === 2) {
return `${this.filterDateRange[0]} ~ ${this.filterDateRange[1]}`;
}
return '时间范围';
},
popupTitle() {
const map = { skill: '技能类型', price: '价格区间', exp: '工作经验', sort: '排序方式' };
return map[this.currentFilterType] || '';
},
popupOptions() {
const map = { skill: this.skillOptions, price: this.priceOptions, exp: this.expOptions, sort: this.sortOptions };
return map[this.currentFilterType] || [];
},
},
methods: {
onBack() { history.back(); },
onSearch() { this.showSearch = false; vant.showToast({ message: `搜索: ${this.searchKeyword}`, position: 'top' }); },
toggleFilter(type) {
if (this.showFilterPanel && this.currentFilterType === type) {
this.showFilterPanel = false;
} else {
this.currentFilterType = type;
this.showFilterPanel = true;
}
},
isPopupSelected(val) {
if (this.currentFilterType === 'skill') return this.filterSkill === val;
if (this.currentFilterType === 'price') return this.filterPrice === val;
if (this.currentFilterType === 'exp') return this.filterExp === val;
if (this.currentFilterType === 'sort') return this.sortBy === val;
return false;
},
onPopupSelect(val) {
if (this.currentFilterType === 'skill') this.filterSkill = val;
else if (this.currentFilterType === 'price') this.filterPrice = val;
else if (this.currentFilterType === 'exp') this.filterExp = val;
else if (this.currentFilterType === 'sort') this.sortBy = val;
this.showFilterPanel = false;
},
onDateConfirm() {
if (this.dateStart && this.dateEnd) {
this.filterDateRange = [this.dateStart, this.dateEnd];
} else {
this.filterDateRange = [];
}
this.showDateCalendar = false;
},
onRefresh() {
setTimeout(() => { this.refreshing = false; vant.showToast('刷新成功'); }, 1000);
},
onLoad() { this.loading = false; this.finished = true; },
viewNurse(nurse) {
vant.showDialog({
title: nurse.name,
message: `评分: ${nurse.rating}\n价格: ¥${nurse.price}/小时\n经验: ${nurse.exp}\n服务: ${nurse.orders}\n资质: ${nurse.certs.join('、')}\n技能: ${nurse.skills.join('、')}`,
});
},
bookNurse(nurse) {
vant.showConfirmDialog({
title: '确认预约',
message: `确定要预约 ${nurse.name} 吗?\n价格: ¥${nurse.price}/小时`,
}).then(() => {
vant.showToast({ message: '预约成功,请等待确认', icon: 'success' });
}).catch(() => {});
},
},
watch: {
activeTab(val) { if (val !== 'nurses') { onTabBarChange(val); } },
},
});
app.use(vant);
app.use(vant.Lazyload);
app.mount('#app');
</script>
{/block}
+553
View File
@@ -0,0 +1,553 @@
{extend name="PatientAPP/layout" /}
{block name="css"}
<link rel="stylesheet" href="/PatientAPP/Order/create.css" />
{/block}
{block name="app"}
<div id="app" v-cloak>
<!-- 头部区域 -->
<div class="header-section">
<div class="header-top">
<div class="header-left">
<van-icon name="arrow-left" size="20" @click="goBack" />
<span class="page-title">发布订单</span>
</div>
<div class="header-actions">
<div class="icon-btn" @click="onDraft">
<van-icon name="description" size="17" />
<span style="font-size:11px;margin-left:3px;">草稿</span>
</div>
</div>
</div>
</div>
<!-- 表单主体 -->
<div class="form-wrapper">
<!-- 服务类型选择 -->
<div class="form-card form-card-type">
<div class="fc-label"><van-icon name="apps-o" color="#018fa5" size="16" /> 选择服务类型</div>
<div class="type-grid">
<div class="type-item"
:class="{ 'type-active': form.serviceType === item.value }"
v-for="(item, index) in serviceTypes"
:key="index"
@click="selectServiceType(item.value)">
<div class="type-icon-wrap" :class="'wrap-' + item.value">
<van-icon :name="item.icon" size="24" :color="form.serviceType === item.value ? '#fff' : item.color" />
</div>
<span class="type-name">{{ item.label }}</span>
<span class="type-desc">{{ item.desc }}</span>
</div>
</div>
</div>
<!-- 患者信息 -->
<div class="form-card">
<div class="fc-header">
<div class="fc-label"><van-icon name="contact" color="#018fa5" size="16" /> 患者信息</div>
</div>
<div class="fc-body">
<van-field v-model="form.patientName" label="姓名" placeholder="请输入患者姓名" clearable />
<van-field v-model="form.patientPhone" label="联系电话" type="tel" placeholder="请输入联系电话" clearable />
<van-field v-model="form.patientAge" label="年龄" type="digit" placeholder="请输入年龄" />
<van-field v-model="form.patientGender" label="性别" readonly placeholder="请选择" is-link @click="showGenderPicker = true" />
</div>
</div>
<!-- 服务时间 -->
<div class="form-card">
<div class="fc-header">
<div class="fc-label"><van-icon name="clock-o" color="#018fa5" size="16" /> 服务时间</div>
</div>
<div class="fc-body">
<van-field
:model-value="form.startDate || ''"
label="开始日期"
readonly
placeholder="请选择开始日期"
is-link
@click="openStartDatePicker"
/>
<van-field
:model-value="form.startTime || ''"
label="开始时间"
readonly
placeholder="请选择开始时间"
is-link
@click="openStartTimePicker"
/>
<van-field
:model-value="form.endDate || ''"
label="结束日期"
readonly
placeholder="请先选开始日期"
is-link
@click="openEndDatePicker"
/>
<van-field
:model-value="form.endTime || ''"
label="结束时间"
readonly
placeholder="请先选结束日期"
is-link
@click="openEndTimePicker"
/>
<div class="duration-tip" v-if="calcDuration">
<van-icon name="info-o" size="14" />
<span>预计服务时长:<strong>{{ calcDuration }}</strong></span>
</div>
</div>
</div>
<!-- 服务地点 -->
<div class="form-card">
<div class="fc-header">
<div class="fc-label"><van-icon name="location-o" color="#018fa5" size="16" /> 服务地点</div>
<van-tag type="primary" size="medium" plain round @click="useCurrentLocation">使用当前位置</van-tag>
</div>
<div class="fc-body">
<van-field v-model="form.hospitalName" label="医院/地点" placeholder="请输入医院名称或服务地点" />
<van-field
v-model="form.detailAddress"
label="详细地址"
type="textarea"
rows="2"
autosize
placeholder="请输入详细地址,如病房号、楼层等"
/>
</div>
</div>
<!-- 护工要求 -->
<div class="form-card">
<div class="fc-header">
<div class="fc-label"><van-icon name="shield-o" color="#018fa5" size="16" /> 护工要求</div>
</div>
<div class="fc-body">
<van-field v-model="form.nurseGender" label="护工性别" readonly placeholder="不限" is-link @click="showNurseGenderPicker = true" />
<van-field v-model="form.experienceReq" label="经验要求" readonly placeholder="不限" is-link @click="showExpPicker = true" />
<div class="skill-tags-section">
<div class="skill-tags-label">特殊技能需求(可多选)</div>
<div class="skill-tags-list">
<span
class="skill-tag"
:class="{ 'skill-active': form.skills.includes(skill) }"
v-for="(skill, index) in skillOptions"
:key="index"
@click="toggleSkill(skill)"
>{{ skill }}</span>
</div>
</div>
</div>
</div>
<!-- 备注说明 -->
<div class="form-card">
<div class="fc-header">
<div class="fc-label"><van-icon name="edit" color="#018fa5" size="16" /> 备注说明</div>
</div>
<div class="fc-body">
<van-field
v-model="form.remark"
type="textarea"
rows="4"
autosize
maxlength="200"
show-word-limit
placeholder="请详细描述患者的身体状况、护理需求等特殊情况,以便匹配合适的护工..."
/>
</div>
</div>
<!-- 费用预估 -->
<div class="form-card price-preview" v-if="estimatedPrice > 0">
<div class="price-row">
<span class="price-label">费用预估</span>
<div class="price-value">
<span class="yen">&yen;</span>
<span class="amount">{{ estimatedPrice }}</span>
</div>
</div>
<div class="price-detail">
<div class="price-item">
<span>基础费用({{ basePrice }}元/小时 x {{ totalHours }}小时)</span>
<span>&yen;{{ basePrice * totalHours }}</span>
</div>
<div class="price-item" v-if="form.skills.length > 0">
<span>技能加成({{ form.skills.length }}项技能 +10%</span>
<span>&yen;{{ Math.round(basePrice * totalHours * 0.1) }}</span>
</div>
</div>
</div>
</div>
<!-- 底部操作栏 -->
<div class="bottom-bar">
<div class="bottom-left">
<div class="total-price">
<span class="tp-label">预估总价:</span>
<span class="tp-value"><span class="tp-yen">&yen;</span>{{ estimatedPrice || '0' }}</span>
</div>
<div class="tp-desc">实际价格以接单后确认为准</div>
</div>
<van-button type="primary" class="submit-btn" round :loading="submitting" @click="submitOrder">
发布订单
</van-button>
</div>
<!-- 性别选择器 -->
<van-popup v-model:show="showGenderPicker" position="bottom" round>
<van-picker :columns="genderColumns" @confirm="onGenderConfirm" @cancel="showGenderPicker = false" title="选择性别" />
</van-popup>
<!-- 护工性别选择器 -->
<van-popup v-model:show="showNurseGenderPicker" position="bottom" round>
<van-picker :columns="nurseGenderColumns" @confirm="onNurseGenderConfirm" @cancel="showNurseGenderPicker = false" title="选择护工性别" />
</van-popup>
<!-- 经验要求选择器 -->
<van-popup v-model:show="showExpPicker" position="bottom" round>
<van-picker :columns="expColumns" @confirm="onExpConfirm" @cancel="showExpPicker = false" title="选择经验要求" />
</van-popup>
<!-- 开始日期选择器 -->
<van-popup v-model:show="showStartDatePicker" position="bottom" round>
<van-date-picker
:key="'sd-' + startDateKey"
v-model="startDatePickerValue"
title="选择开始日期"
:min-date="minDate"
:max-date="maxDate"
@confirm="onStartDateConfirm"
@cancel="showStartDatePicker = false"
/>
</van-popup>
<!-- 开始时间选择器 -->
<van-popup v-model:show="showStartTimePicker" position="bottom" round>
<van-time-picker
:key="'st-' + startTimeKey"
v-model="startTimePickerValue"
title="选择开始时间"
:columns-type="['hour', 'minute']"
@confirm="onStartTimeConfirm"
@cancel="showStartTimePicker = false"
/>
</van-popup>
<!-- 结束日期选择器 -->
<van-popup v-model:show="showEndDatePicker" position="bottom" round>
<van-date-picker
:key="'ed-' + endDateKey"
v-model="endDatePickerValue"
title="选择结束日期"
:min-date="startMinEndDate"
:max-date="maxDate"
@confirm="onEndDateConfirm"
@cancel="showEndDatePicker = false"
/>
</van-popup>
<!-- 结束时间选择器 -->
<van-popup v-model:show="showEndTimePicker" position="bottom" round>
<van-time-picker
:key="'et-' + endTimeKey"
v-model="endTimePickerValue"
title="选择结束时间"
:columns-type="['hour', 'minute']"
@confirm="onEndTimeConfirm"
@cancel="showEndTimePicker = false"
/>
</van-popup>
</div>
{/block}
{block name="js"}
<script>
const app = Vue.createApp({
data() {
const now = new Date();
return {
submitting: false,
showGenderPicker: false,
showNurseGenderPicker: false,
showExpPicker: false,
minDate: new Date(),
maxDate: new Date(Date.now() + 365 * 86400000),
// 开始日期选择器
showStartDatePicker: false,
startDateKey: 0,
startDatePickerValue: ['2026', '5', '9'],
// 开始时间选择器
showStartTimePicker: false,
startTimeKey: 0,
startTimePickerValue: ['8', '0'],
// 结束日期选择器
showEndDatePicker: false,
endDateKey: 0,
endDatePickerValue: ['2026', '5', '10'],
// 结束时间选择器
showEndTimePicker: false,
endTimeKey: 0,
endTimePickerValue: ['18', '0'],
genderColumns: ['男', '女'],
nurseGenderColumns: ['不限', '男', '女'],
expColumns: ['不限', '1年以上', '3年以上', '5年以上', '8年以上'],
serviceTypes: [
{ value: 'hospital', label: '住院陪护', icon: 'hotel-o', desc: '院内专业护理', color: '#018fa5' },
{ value: 'outpatient', label: '门诊陪护', icon: 'guide-o', desc: '陪同就诊检查', color: '#e08c28' },
{ value: 'home', label: '居家陪护', icon: 'home-o', desc: '上门居家照料', color: '#2ba245' },
{ value: 'maternal', label: '母婴护理', icon: 'like-o', desc: '月子期专业照护', color: '#e91e8b' },
],
skillOptions: ['术后护理', '老年照护', '康复训练', '输液换药', '重症监护', '母婴护理', '营养配餐', '心理疏导', '行动辅助', '中医理疗'],
form: {
serviceType: '',
patientName: '',
patientPhone: '',
patientAge: '',
patientGender: '',
startDate: '',
startTime: '',
endDate: '',
endTime: '',
hospitalName: '',
detailAddress: '',
nurseGender: '',
experienceReq: '',
skills: [],
remark: '',
},
basePriceMap: {
hospital: 68,
outpatient: 58,
home: 52,
maternal: 88,
},
};
},
computed: {
startMinEndDate() {
if (!this.form.startDate) return this.minDate;
const d = new Date(this.form.startDate.replace(/-/g, '/'));
return d;
},
totalHours() {
if (!this.form.startDate || !this.form.endDate) return 0;
const start = new Date(`${this.form.startDate.replace(/-/g, '/')} ${this.form.startTime || '00:00'}`);
const end = new Date(`${this.form.endDate.replace(/-/g, '/')} ${this.form.endTime || '00:00'}`);
let diff = (end - start) / 3600000;
if (diff <= 0) diff = 0;
return Math.round(diff * 10) / 10;
},
totalDays() {
if (!this.form.startDate || !this.form.endDate) return 0;
const s = new Date(this.form.startDate.replace(/-/g, '/'));
const e = new Date(this.form.endDate.replace(/-/g, '/'));
return Math.ceil((e - s) / 86400000);
},
basePrice() {
return this.basePriceMap[this.form.serviceType] || 60;
},
estimatedPrice() {
if (this.totalHours === 0) return 0;
let price = this.basePrice * this.totalHours;
if (this.form.skills.length > 0) {
price = Math.round(price * 1.1);
}
return price;
},
calcDuration() {
if (!this.form.startDate || !this.form.endDate) return '';
const days = this.totalDays;
const hours = this.totalHours;
if (days <= 0 && hours <= 0) return '';
if (days > 0) return `${days}${hours}小时`;
return `${hours}小时`;
},
},
methods: {
goBack() { history.back(); },
onDraft() { vant.showToast('暂无草稿'); },
selectServiceType(type) { this.form.serviceType = type; },
useCurrentLocation() {
vant.showToast({ message: '正在获取位置...', loading: true, duration: 1500 });
setTimeout(() => {
this.form.hospitalName = '当前位置附近';
vant.clearToast();
vant.showToast('已获取位置');
}, 1000);
},
toggleSkill(skill) {
const idx = this.form.skills.indexOf(skill);
if (idx > -1) {
this.form.skills.splice(idx, 1);
} else {
this.form.skills.push(skill);
}
},
padZero(n) { return String(n).padStart(2, '0'); },
openStartDatePicker() {
if (this.form.startDate) {
const p = this.form.startDate.split('-');
this.startDatePickerValue = [p[0], String(parseInt(p[1])), String(parseInt(p[2]))];
} else {
const now = new Date();
this.startDatePickerValue = [
String(now.getFullYear()),
String(now.getMonth() + 1),
String(now.getDate())
];
}
this.startDateKey++;
this.showStartDatePicker = true;
},
openStartTimePicker() {
if (!this.form.startDate) {
vant.showToast('请先选择开始日期');
return;
}
if (this.form.startTime) {
const p = this.form.startTime.split(':');
this.startTimePickerValue = [String(parseInt(p[0])), String(parseInt(p[1]))];
} else {
this.startTimePickerValue = ['8', '0'];
}
this.startTimeKey++;
this.showStartTimePicker = true;
},
openEndDatePicker() {
if (!this.form.startDate) {
vant.showToast('请先选择开始日期');
return;
}
if (this.form.endDate) {
const p = this.form.endDate.split('-');
this.endDatePickerValue = [p[0], String(parseInt(p[1])), String(parseInt(p[2]))];
} else {
const start = new Date(this.form.startDate.replace(/-/g, '/'));
start.setDate(start.getDate() + 1);
this.endDatePickerValue = [
String(start.getFullYear()),
String(start.getMonth() + 1),
String(start.getDate())
];
}
this.endDateKey++;
this.showEndDatePicker = true;
},
openEndTimePicker() {
if (!this.form.endDate) {
vant.showToast('请先选择结束日期');
return;
}
if (this.form.endTime) {
const p = this.form.endTime.split(':');
this.endTimePickerValue = [String(parseInt(p[0])), String(parseInt(p[1]))];
} else {
this.endTimePickerValue = ['18', '0'];
}
this.endTimeKey++;
this.showEndTimePicker = true;
},
onStartDateConfirm({ selectedValues }) {
const v = selectedValues;
this.form.startDate = `${v[0]}-${this.padZero(v[1])}-${this.padZero(v[2])}`;
// 如果已选结束日期早于开始日期,清空结束日期
if (this.form.endDate && new Date(this.form.endDate.replace(/-/g, '/')) < new Date(this.form.startDate.replace(/-/g, '/'))) {
this.form.endDate = '';
this.form.endTime = '';
}
this.showStartDatePicker = false;
},
onStartTimeConfirm({ selectedValues }) {
const v = selectedValues;
this.form.startTime = `${this.padZero(v[0])}:${this.padZero(v[1])}`;
this.showStartTimePicker = false;
},
onEndDateConfirm({ selectedValues }) {
const v = selectedValues;
this.form.endDate = `${v[0]}-${this.padZero(v[1])}-${this.padZero(v[2])}`;
// 校验结束日期不能早于开始日期
if (new Date(this.form.endDate.replace(/-/g, '/')) < new Date(this.form.startDate.replace(/-/g, '/'))) {
vant.showToast('结束日期不能早于开始日期');
this.form.endDate = '';
return;
}
this.showEndDatePicker = false;
},
onEndTimeConfirm({ selectedValues }) {
const v = selectedValues;
const endTimeStr = `${this.padZero(v[0])}:${this.padZero(v[1])}`;
// 校验结束时间须晚于开始时间
if (this.form.endDate === this.form.startDate && this.form.startTime) {
const endFull = new Date(`${this.form.endDate.replace(/-/g, '/')} ${endTimeStr}`);
const startFull = new Date(`${this.form.startDate.replace(/-/g, '/')} ${this.form.startTime}`);
if (endFull <= startFull) {
vant.showToast('结束时间须晚于开始时间');
return;
}
}
this.form.endTime = endTimeStr;
this.showEndTimePicker = false;
},
onGenderConfirm({ selectedOptions }) { this.form.patientGender = selectedOptions[0]?.text || ''; this.showGenderPicker = false; },
onNurseGenderConfirm({ selectedOptions }) { this.form.nurseGender = selectedOptions[0]?.text || ''; this.showNurseGenderPicker = false; },
onExpConfirm({ selectedOptions }) { this.form.experienceReq = selectedOptions[0]?.text || ''; this.showExpPicker = false; },
formatDateOnly(date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
},
validateForm() {
if (!this.form.serviceType) { vant.showToast('请选择服务类型'); return false; }
if (!this.form.patientName) { vant.showToast('请输入患者姓名'); return false; }
if (!this.form.patientPhone) { vant.showToast('请输入联系电话'); return false; }
if (!/^1\d{10}$/.test(this.form.patientPhone)) { vant.showToast('手机号格式不正确'); return false; }
if (!this.form.startDate || !this.form.startTime) { vant.showToast('请选择开始服务时间'); return false; }
if (!this.form.endDate || !this.form.endTime) { vant.showToast('请选择结束服务时间'); return false; }
if (this.totalHours <= 0) { vant.showToast('结束时间须晚于开始时间'); return false; }
if (!this.form.hospitalName) { vant.showToast('请输入服务地点'); return false; }
return true;
},
submitOrder() {
if (!this.validateForm()) return;
vant.showConfirmDialog({
title: '确认发布',
message: `服务类型:${this.serviceTypes.find(s => s.value === this.form.serviceType)?.label}\n服务时间:${this.form.startDate} ${this.form.startTime} ~ ${this.form.endDate} ${this.form.endTime}\n服务时长:${this.calcDuration}\n预估费用:¥${this.estimatedPrice}`,
confirmButtonText: '确认发布',
cancelButtonText: '再想想',
}).then(() => {
this.submitting = true;
setTimeout(() => {
this.submitting = false;
vant.showDialog({
title: '发布成功',
message: '订单已发布,系统将为您匹配合适的护工,请耐心等待接单。',
confirmButtonText: '查看订单',
}).then(() => {
window.location.href = '/PatientApp/Order/index';
});
}, 1500);
}).catch(() => {});
},
},
watch: {},
});
app.use(vant);
app.use(vant.Lazyload);
app.mount('#app');
</script>
{/block}
+382
View File
@@ -0,0 +1,382 @@
{extend name="PatientAPP/layout" /}
{block name="css"}
<link rel="stylesheet" href="/PatientAPP/Order/order.css" />
{/block}
{block name="app"}
<div id="app" v-cloak>
<!-- 头部区域 -->
<div class="header-section">
<div class="header-top">
<div class="page-title">我的订单</div>
<div class="header-actions">
<div class="icon-btn" @click="onSearch">
<van-icon name="search" size="17" />
</div>
</div>
</div>
<!-- Tab标签栏 -->
<van-tabs v-model:active="activeTab" swipeable sticky :offset-top="0" color="#fff" line-width="20" line-height="3">
<van-tab title="全部" name="all"></van-tab>
<van-tab title="待接单" name="pending"></van-tab>
<van-tab title="进行中" name="ongoing"></van-tab>
<van-tab title="已完成" name="completed"></van-tab>
<van-tab title="已取消" name="cancelled"></van-tab>
</van-tabs>
</div>
<!-- 订单列表 -->
<div class="order-list-wrapper">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<div class="order-card" v-for="(order, index) in filteredOrders" :key="order.id" @click="viewOrder(order)">
<!-- 卡片头部:状态 + 订单号 + 时间 -->
<div class="oc-top">
<div class="oc-top-left">
<span class="oc-status-dot" :class="'dot-' + order.status"></span>
<span class="oc-status-text" :class="'text-' + order.status">{{ order.statusText }}</span>
<span class="oc-no">{{ order.orderNo }}</span>
</div>
<span class="oc-time">{{ order.createTime }}</span>
</div>
<!-- 核心信息区 -->
<div class="oc-main">
<!-- 左侧服务类型图标 -->
<div class="oc-service-icon" :class="'icon-' + order.status">
<van-icon :name="serviceIcon(order.serviceType)" size="24" />
</div>
<!-- 右侧详情 -->
<div class="oc-detail">
<div class="oc-title-row">
<span class="oc-service-type">{{ order.serviceType }}</span>
<van-tag v-if="order.status === 'ongoing'" type="primary" size="medium" round>服务中</van-tag>
<van-tag v-if="order.status === 'pending'" color="#fef4ec" text-color="#e08c28" size="medium" round>等待接单</van-tag>
</div>
<div class="oc-meta-row">
<div class="oc-meta-item">
<van-icon name="clock-o" size="14" />
<span>{{ order.serviceTime }}</span>
</div>
</div>
<div class="oc-meta-row">
</div>
<div class="oc-meta-row">
<div class="oc-meta-item">
<van-icon name="location-o" size="14" />
<span class="oc-location">{{ order.location }}</span>
</div>
</div>
<!-- 护工信息条 -->
<div class="oc-nurse-bar" v-if="order.nurseName" @click.stop="togglePhone(order)">
<div class="nurse-avatar-sm">
<van-icon name="contact" size="14" />
</div>
<span class="nurse-name-sm">{{ order.nurseName }}</span>
<span class="nurse-exp-sm">· {{ order.nurseExp }}年经验</span>
<span class="nurse-phone-sm">
{{ order.showPhone ? order.nursePhone : order.nursePhoneHidden }}
<van-icon :name="order.showPhone ? 'eye-o' : 'closed-eye'" size="12" style="margin-left:4px;vertical-align:middle;" />
</span>
</div>
</div>
</div>
<!-- 备注预览 -->
<div class="oc-remark" v-if="order.remark">
<!-- <van-icon name="chat-o" size="13" /> -->
<span style="color: #cc6600;font-weight: 600;">订单备注:</span>
<span>{{ order.remark }}</span>
</div>
<!-- 底部操作栏 -->
<div class="oc-bottom">
<div class="oc-price-block">
<span class="oc-price-label">实付金额</span>
<div class="oc-price-value">
<span class="oc-yen">&yen;</span>
<span class="oc-amount">{{ order.totalAmount }}</span>
</div>
</div>
<div class="oc-actions">
<van-button
v-if="order.status === 'pending'"
size="small"
round
plain
hairline
@click.stop="cancelOrder(order)"
>取消订单</van-button>
<template v-if="order.status === 'ongoing'">
<van-button
size="small"
round
plain
hairline
type="primary"
@click.stop="extendOrder(order)"
>延长服务</van-button>
<van-button
size="small"
round
type="primary"
@click.stop="contactNurse(order)"
>联系护工</van-button>
</template>
<template v-if="order.status === 'completed' && !order.reviewed">
<van-button
size="small"
round
plain
hairline
@click.stop="viewOrder(order)"
>查看详情</van-button>
<van-button
size="small"
round
type="success"
@click.stop="reviewOrder(order)"
>去评价</van-button>
</template>
<template v-if="order.status === 'completed' && order.reviewed">
<van-button
size="small"
round
plain
hairline
@click.stop="viewReview(order)"
>查看评价</van-button>
</template>
<template v-if="order.status === 'cancelled'">
<van-button
size="small"
round
plain
hairline
@click.stop="reorder(order)"
>再次下单</van-button>
</template>
</div>
</div>
</div>
<!-- 空状态 -->
<van-empty
v-if="!loading && filteredOrders.length === 0"
description="暂无订单记录"
image="search"
>
<van-button round type="primary" size="small" class="empty-btn" @click="goPublish">立即下单</van-button>
</van-empty>
</van-list>
</van-pull-refresh>
</div>
<!-- 搜索弹窗 -->
<van-popup v-model:show="showSearch" position="top" :style="{ height: 'auto' }">
<van-search
v-model="searchKeyword"
placeholder="搜索订单号/服务类型/地点"
shape="round"
show-action
@search="doSearch"
@cancel="showSearch = false"
>
<template #action>
<div @click="doSearch">搜索</div>
</template>
</van-search>
</van-popup>
<!-- 底部栏 -->
{include file="PatientAPP/Component/van-tabbar" /}
</div>
{/block}
{block name="js"}
<script>
const app = Vue.createApp({
data() {
return {
activeTab: 'all',
showSearch: false,
searchKeyword: '',
refreshing: false,
loading: false,
finished: true,
stats: { pending: 2, ongoing: 1, completed: 8 },
orderList: [
{
id: 1, orderNo: 'MC20260509001', status: 'ongoing', statusText: '进行中',
createTime: '2026-05-08 14:30', serviceType: '住院陪护',
serviceTime: '2026-05-08 15:00 ~ 2026-05-11 15:00 (3天)',
location: '北京协和医院 住院部12楼1205病房',
nurseName: '张秀芳', nurseExp: 8, nurseRating: 4.9,
nursePhone: '13812345678', nursePhoneHidden: '138****5678', showPhone: false,
remark: '术后护理,需要协助翻身、拍背,注意引流管',
totalAmount: 2448, reviewed: false,
},
{
id: 2, orderNo: 'MC20260508002', status: 'pending', statusText: '待接单',
createTime: '2026-05-09 10:00', serviceType: '门诊陪护',
serviceTime: '2026-05-10 08:30 ~ 12:30 (半天)',
location: '北京大学第一医院 门诊楼3层',
nurseName: '', nurseExp: '', nurseRating: '',
remark: '陪同就诊,协助挂号、缴费、取药',
totalAmount: 196, reviewed: false,
},
{
id: 3, orderNo: 'MC20260507003', status: 'pending', statusText: '待接单',
createTime: '2026-05-07 16:20', serviceType: '居家陪护',
serviceTime: '2026-05-10 09:00 ~ 17:00 (1天)',
location: '北京市朝阳区建国路88号院3号楼2单元501',
nurseName: '', nurseExp: '', nurseRating: '',
remark: '老人日常照料,做饭、打扫卫生、陪同散步',
totalAmount: 520, reviewed: false,
},
{
id: 4, orderNo: 'MC20260505004', status: 'completed', statusText: '已完成',
createTime: '2026-05-03 09:15', serviceType: '住院陪护',
serviceTime: '2026-05-03 10:00 ~ 2026-05-06 10:00 (3天)',
location: '中日友好医院 住院部8楼802病房',
nurseName: '李国华', nurseExp: 10, nurseRating: 4.8,
nursePhone: '13987654321', nursePhoneHidden: '139****4321', showPhone: false,
remark: '重症监护后转普通病房的康复期护理',
totalAmount: 2700, reviewed: true,
},
{
id: 5, orderNo: 'MC20260430005', status: 'completed', statusText: '已完成',
createTime: '2026-04-28 11:40', serviceType: '母婴护理',
serviceTime: '2026-04-29 08:00 ~ 2026-05-13 08:00 (14天)',
location: '北京妇产医院 家庭住所(朝阳区望京)',
nurseName: '王美玲', nurseExp: 5, nurseRating: 4.9,
nursePhone: '13666668888', nursePhoneHidden: '3666668888', showPhone: false,
remark: '月子期间母婴护理,新生儿照料和产妇恢复',
totalAmount: 11200, reviewed: true,
},
{
id: 6, orderNo: 'MC20260425006', status: 'completed', statusText: '已完成',
createTime: '2026-04-23 15:00', serviceType: '门诊陪护',
serviceTime: '2026-04-25 07:30 ~ 13:00 (半天)',
location: '首都医科大学附属北京天坛医院',
nurseName: '陈建国', nurseExp: 6, nurseRating: 4.7,
nursePhone: '15811112222', nursePhoneHidden: '158****2222', showPhone: false,
remark: '陪同做核磁共振检查,老人行动不便需轮椅辅助',
totalAmount: 232, reviewed: false,
},
{
id: 7, orderNo: 'MC20260420007', status: 'cancelled', statusText: '已取消',
createTime: '2026-04-18 09:30', serviceType: '居家陪护',
serviceTime: '2026-04-20 14:00 ~ 18:00 (4小时)',
location: '北京市海淀区中关村大街1号',
nurseName: '', nurseExp: '', nurseRating: '',
remark: '临时有事取消了',
totalAmount: 240, reviewed: false,
},
{
id: 8, orderNo: 'MC20260415008', status: 'completed', statusText: '已完成',
createTime: '2026-04-13 10:20', serviceType: '住院陪护',
serviceTime: '2026-04-15 08:00 ~ 2026-04-18 08:00 (3天)',
location: '中国人民解放军总医院(301医院) 住院部东楼608',
nurseName: '赵文斌', nurseExp: 12, nurseRating: 4.8,
nursePhone: '17799887766', nursePhoneHidden: '177****7766', showPhone: false,
remark: '心脏手术后康复护理,监测生命体征',
totalAmount: 2880, reviewed: true,
},
{
id: 9, orderNo: 'MC20260410009', status: 'completed', statusText: '已完成',
createTime: '2026-04-08 14:00', serviceType: '门诊陪护',
serviceTime: '2026-04-10 09:00 ~ 11:30 (2.5小时)',
location: '北京安贞医院 心脏中心',
nurseName: '刘晓红', nurseExp: 3, nurseRating: 4.6,
remark: '陪同复查心电图和超声心动图',
totalAmount: 138, reviewed: true,
},
{
id: 10, orderNo: 'MC20260405010', status: 'completed', statusText: '已完成',
createTime: '2026-04-02 16:45', serviceType: '居家陪护',
serviceTime: '2026-04-05 09:00 ~ 18:00 (连续5天)',
location: '北京市丰台区方庄南路18号院',
nurseName: '周明辉', nurseExp: 7, nurseRating: 4.7,
remark: '脑梗塞后遗症患者居家康复训练和生活照料',
totalAmount: 2250, reviewed: false,
},
],
};
},
computed: {
filteredOrders() {
let list = this.orderList;
if (this.activeTab !== 'all') {
list = list.filter(o => o.status === this.activeTab);
}
if (this.searchKeyword) {
const kw = this.searchKeyword.toLowerCase();
list = list.filter(o =>
o.orderNo.toLowerCase().includes(kw) ||
o.serviceType.includes(kw) ||
o.location.includes(kw) ||
(o.nurseName && o.nurseName.includes(kw))
);
}
return list;
},
},
methods: {
onSearch() { this.showSearch = true; },
doSearch() { this.showSearch = false; vant.showToast({ message: `搜索: ${this.searchKeyword}`, position: 'top' }); },
switchTab(tab) { this.activeTab = tab; },
onRefresh() { setTimeout(() => { this.refreshing = false; vant.showToast('刷新成功'); }, 1000); },
onLoad() { this.loading = false; this.finished = true; },
togglePhone(order) { order.showPhone = !order.showPhone; },
viewOrder(order) { vant.showDialog({ title: '订单详情', message: `订单号: ${order.orderNo}\n服务: ${order.serviceType}\n时间: ${order.serviceTime}\n金额: ¥${order.totalAmount}\n状态: ${order.statusText}` }); },
cancelOrder(order) {
vant.showConfirmDialog({ title: '确认取消', message: '确定要取消该订单吗?取消后需重新下单。' }).then(() => {
order.status = 'cancelled';
order.statusText = '已取消';
this.stats.pending--;
vant.showToast({ message: '订单已取消', icon: 'success' });
}).catch(() => {});
},
contactNurse(order) {
vant.showDialog({ title: '联系护工', message: `正在呼叫 ${order.nurseName}...\n电话: 138****8888` });
},
extendOrder(order) {
vant.showConfirmDialog({ title: '延长服务', message: '确定要延长当前服务时长吗?护工将收到延长时间通知。' }).then(() => {
vant.showToast({ message: '已发送延长请求', icon: 'success' });
}).catch(() => {});
},
reviewOrder(order) {
vant.showDialog({ title: '服务评价', message: '评价功能开发中...' });
},
viewReview(order) {
vant.showToast('查看评价详情');
},
reorder(order) {
vant.showToast({ message: '正在跳转到下单页面...', icon: 'success' });
},
goPublish() {
vant.showToast('跳转到发布订单页');
},
serviceIcon(type) {
const map = { '住院陪护': 'hotel-o', '门诊陪护': 'guide-o', '居家陪护': 'home-o', '母婴护理': 'like-o' };
return map[type] || 'service-o';
},
},
watch: {
activeTab(val) { if (val !== 'orders') { onTabBarChange(val); } },
},
});
app.use(vant);
app.use(vant.Lazyload);
app.mount('#app');
</script>
{/block}
+30
View File
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户端_智能陪护系统</title>
<!-- 引入样式文件 -->
<link rel="stylesheet" href="/Static/vant@4.css" />
<!-- 解决加载闪烁 -->
<style>
[v-cloak] {
display: none !important;
}
</style>
{block name="css"}{/block}
</head>
<body>
{block name="app"}{/block}
</body>
<!-- 引入 Vue 和 Vant 的 JS 文件 -->
<script src="/Static/vue@3.js"></script>
<script src="/Static/vant.min.js"></script>
<script>
{$GlobalJS|raw}
</script>
{block name="js"}{/block}
</html>