模拟操作
This commit is contained in:
@@ -12,7 +12,8 @@ interface CinematicVideoPlayerProps {
|
||||
const props = withDefaults(defineProps<CinematicVideoPlayerProps>(), {
|
||||
poster: '',
|
||||
title: '产品演示视频',
|
||||
accent: '#14b8a6'
|
||||
// 默认强调色与全站品牌青蓝一致(--brand 亮色值)
|
||||
accent: '#0891b2'
|
||||
})
|
||||
|
||||
const videoRef = ref<HTMLVideoElement | null>(null)
|
||||
|
||||
@@ -55,6 +55,7 @@ const themeOptions = [
|
||||
const menuGroups = [
|
||||
{ key: 'home', label: '首页', to: '/', links: siteConfig.navigation.secondary.home },
|
||||
{ key: 'product', label: '产品', to: '/product', links: siteConfig.navigation.secondary.product },
|
||||
{ key: 'demo', label: '流程模拟', to: '/demo/full-journey', activePath: '/demo', links: siteConfig.navigation.secondary.demo },
|
||||
{ key: 'video', label: '视频', to: '/video/user', activePath: '/video', links: siteConfig.navigation.secondary.video },
|
||||
{ key: 'about', label: '关于我们', to: '/about', links: siteConfig.navigation.secondary.about }
|
||||
]
|
||||
@@ -87,6 +88,14 @@ const routeSearchItems = computed(() => [
|
||||
category: '页面',
|
||||
keywords: ['产品介绍', '功能', '价格', 'FAQ']
|
||||
},
|
||||
{
|
||||
id: 'route-demo',
|
||||
title: '流程模拟',
|
||||
description: '跨端全流程演示(患者扫码到支付完成),以及线下挂号、在线复诊、药店购药、订单支付、医生端接诊开方与 PC 端接诊开方、代付的可交互演示。',
|
||||
to: '/demo/full-journey',
|
||||
category: '页面',
|
||||
keywords: ['流程模拟', '演示', '全流程', '全景', '跨端', '挂号', '复诊', '购药', '支付', '医生端', '开方', 'PC端', '线下接诊', '在线复诊开方', '发送处方', '就诊人', '认领', '代付']
|
||||
},
|
||||
{
|
||||
id: 'route-video',
|
||||
title: '视频',
|
||||
@@ -109,6 +118,7 @@ const navigationSearchItems = computed(() => {
|
||||
const groups = [
|
||||
{ category: '首页标题', links: siteConfig.navigation.secondary.home },
|
||||
{ category: '产品标题', links: siteConfig.navigation.secondary.product },
|
||||
{ category: '流程模拟', links: siteConfig.navigation.secondary.demo },
|
||||
{ category: '视频分区', links: siteConfig.navigation.secondary.video },
|
||||
{ category: '关于我们', links: siteConfig.navigation.secondary.about }
|
||||
]
|
||||
|
||||
119
src/components/vue-bits/CountUp.vue
Normal file
119
src/components/vue-bits/CountUp.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<span ref="rootRef" class="count-up" :aria-label="value">{{ displayText }}</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
|
||||
/**
|
||||
* 数字滚动组件(CountUp)
|
||||
*
|
||||
* 为什么这样写:
|
||||
* - 官网多个指标(1200+ / 38% / 99.95% / 7x24)都是「数字 + 后缀」的混合字符串,
|
||||
* 因此组件直接接收原始字符串,内部解析出「前缀 + 数字 + 后缀」,滚动数字部分、原样保留其余部分,
|
||||
* 调用方无需拆字段,配置文件也不用改结构。
|
||||
* - 使用 IntersectionObserver 进入视口才启动,避免首屏外的指标提前跑完;只触发一次。
|
||||
* - 使用 requestAnimationFrame + easeOutCubic 缓动,结尾减速更有「落定」感。
|
||||
* - 尊重 prefers-reduced-motion:直接显示最终值,不做动画。
|
||||
*/
|
||||
interface Props {
|
||||
/** 原始展示值,如 "1200+"、"99.95%"、"7x24",会自动解析数字部分 */
|
||||
value: string;
|
||||
/** 动画时长(毫秒) */
|
||||
duration?: number;
|
||||
/** 进入视口多少比例后启动 */
|
||||
threshold?: number;
|
||||
/** 起始值(默认从 0 滚动) */
|
||||
from?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
duration: 1600,
|
||||
threshold: 0.4,
|
||||
from: 0
|
||||
});
|
||||
|
||||
const rootRef = ref<HTMLElement | null>(null);
|
||||
const started = ref(false);
|
||||
const current = ref(props.from);
|
||||
|
||||
let observer: IntersectionObserver | null = null;
|
||||
let rafId = 0;
|
||||
|
||||
/** 解析 "前缀 + 数字 + 后缀",例如 "99.95%" → { prefix: '', num: 99.95, decimals: 2, suffix: '%' } */
|
||||
const parsed = computed(() => {
|
||||
const match = String(props.value).match(/^([^0-9]*)([0-9]+(?:\.[0-9]+)?)([\s\S]*)$/);
|
||||
if (!match) {
|
||||
return { prefix: String(props.value), num: null as number | null, decimals: 0, suffix: '' };
|
||||
}
|
||||
const decimals = match[2].includes('.') ? match[2].split('.')[1].length : 0;
|
||||
return { prefix: match[1], num: Number(match[2]), decimals, suffix: match[3] };
|
||||
});
|
||||
|
||||
const displayText = computed(() => {
|
||||
const { prefix, num, decimals, suffix } = parsed.value;
|
||||
if (num === null) return prefix;
|
||||
const shown = started.value ? current.value : props.from;
|
||||
return `${prefix}${shown.toFixed(decimals)}${suffix}`;
|
||||
});
|
||||
|
||||
/** 启动滚动动画:easeOutCubic 缓动到目标值 */
|
||||
function startAnimation() {
|
||||
if (started.value || parsed.value.num === null) {
|
||||
started.value = true;
|
||||
return;
|
||||
}
|
||||
started.value = true;
|
||||
|
||||
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
const target = parsed.value.num;
|
||||
if (reduceMotion) {
|
||||
current.value = target;
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
const from = props.from;
|
||||
const tick = (now: number) => {
|
||||
const progress = Math.min(1, (now - startTime) / props.duration);
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
current.value = from + (target - from) * eased;
|
||||
if (progress < 1) {
|
||||
rafId = requestAnimationFrame(tick);
|
||||
} else {
|
||||
current.value = target;
|
||||
}
|
||||
};
|
||||
rafId = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!rootRef.value || !('IntersectionObserver' in window)) {
|
||||
startAnimation();
|
||||
return;
|
||||
}
|
||||
observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
startAnimation();
|
||||
observer?.disconnect();
|
||||
observer = null;
|
||||
}
|
||||
},
|
||||
{ threshold: props.threshold }
|
||||
);
|
||||
observer.observe(rootRef.value);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
observer?.disconnect();
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.count-up {
|
||||
/* 等宽数字避免滚动过程中抖动 */
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
</style>
|
||||
137
src/components/vue-bits/TiltCard.vue
Normal file
137
src/components/vue-bits/TiltCard.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div
|
||||
ref="frameRef"
|
||||
class="tilt-frame"
|
||||
:style="{ perspective: `${perspective}px` }"
|
||||
@pointermove="handlePointerMove"
|
||||
@pointerleave="handlePointerLeave"
|
||||
>
|
||||
<div ref="bodyRef" class="tilt-body" :class="{ 'tilt-body-active': isHovering }">
|
||||
<slot />
|
||||
<div v-if="showGlare" class="tilt-glare" aria-hidden="true" :style="glareStyle"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
|
||||
/**
|
||||
* 3D 倾斜卡片(TiltCard)
|
||||
*
|
||||
* 为什么这样写:
|
||||
* - 官网「驾驶舱/手机模拟器」需要跟随鼠标的 3D 倾斜效果(UI 规范中的「3D 倾斜驾驶舱」)。
|
||||
* - 只用 transform(rotateX/rotateY)+ opacity,不引入第三方动画库,保证流畅并符合项目动效约束。
|
||||
* - 用 rAF 循环做 lerp 平滑(阻尼跟随),而不是直接 set,避免鼠标快速移动时的生硬跳动。
|
||||
* - 触摸设备与 prefers-reduced-motion 用户自动禁用,保证可用性。
|
||||
*/
|
||||
interface Props {
|
||||
/** 最大倾斜角度(度) */
|
||||
maxTilt?: number;
|
||||
/** 透视距离,越小透视越强 */
|
||||
perspective?: number;
|
||||
/** 阻尼系数 0-1,越大跟随越快 */
|
||||
damping?: number;
|
||||
/** 悬停时的整体放大比例 */
|
||||
hoverScale?: number;
|
||||
/** 是否显示跟随光泽 */
|
||||
showGlare?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
maxTilt: 9,
|
||||
perspective: 1100,
|
||||
damping: 0.12,
|
||||
hoverScale: 1.012,
|
||||
showGlare: true
|
||||
});
|
||||
|
||||
const frameRef = ref<HTMLElement | null>(null);
|
||||
const bodyRef = ref<HTMLElement | null>(null);
|
||||
const isHovering = ref(false);
|
||||
const glarePos = ref({ x: 50, y: 38 });
|
||||
|
||||
/** 目标角度与当前角度分离,rAF 里做插值平滑 */
|
||||
const target = { rx: 0, ry: 0, scale: 1 };
|
||||
const state = { rx: 0, ry: 0, scale: 1 };
|
||||
let rafId = 0;
|
||||
let disabled = false;
|
||||
|
||||
const glareStyle = computed(() => ({
|
||||
opacity: isHovering.value ? 0.55 : 0,
|
||||
background: `radial-gradient(420px circle at ${glarePos.value.x}% ${glarePos.value.y}%, rgba(255, 255, 255, 0.26), transparent 62%)`
|
||||
}));
|
||||
|
||||
/** rAF 循环:向目标角度做 lerp,静止后停帧节能 */
|
||||
function loop() {
|
||||
state.rx += (target.rx - state.rx) * props.damping;
|
||||
state.ry += (target.ry - state.ry) * props.damping;
|
||||
state.scale += (target.scale - state.scale) * props.damping;
|
||||
|
||||
if (bodyRef.value) {
|
||||
bodyRef.value.style.transform = `rotateX(${state.rx.toFixed(3)}deg) rotateY(${state.ry.toFixed(3)}deg) scale(${state.scale.toFixed(4)})`;
|
||||
}
|
||||
|
||||
const settled =
|
||||
Math.abs(target.rx - state.rx) < 0.01 &&
|
||||
Math.abs(target.ry - state.ry) < 0.01 &&
|
||||
Math.abs(target.scale - state.scale) < 0.001;
|
||||
|
||||
rafId = settled ? 0 : requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
function kickLoop() {
|
||||
if (!rafId) rafId = requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
if (disabled || event.pointerType === 'touch' || !frameRef.value) return;
|
||||
const rect = frameRef.value.getBoundingClientRect();
|
||||
const px = (event.clientX - rect.left) / rect.width;
|
||||
const py = (event.clientY - rect.top) / rect.height;
|
||||
|
||||
isHovering.value = true;
|
||||
target.ry = (px - 0.5) * 2 * props.maxTilt;
|
||||
target.rx = -(py - 0.5) * 2 * props.maxTilt;
|
||||
target.scale = props.hoverScale;
|
||||
glarePos.value = { x: Math.round(px * 100), y: Math.round(py * 100) };
|
||||
kickLoop();
|
||||
}
|
||||
|
||||
function handlePointerLeave() {
|
||||
isHovering.value = false;
|
||||
target.rx = 0;
|
||||
target.ry = 0;
|
||||
target.scale = 1;
|
||||
kickLoop();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
disabled = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tilt-frame {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tilt-body {
|
||||
position: relative;
|
||||
transform-style: preserve-3d;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.tilt-glare {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.45s ease;
|
||||
mix-blend-mode: soft-light;
|
||||
}
|
||||
</style>
|
||||
@@ -30,6 +30,7 @@ const backdropClass = computed(() => {
|
||||
<nav aria-label="底部导航">
|
||||
<RouterLink to="/">首页</RouterLink>
|
||||
<RouterLink to="/product">产品</RouterLink>
|
||||
<RouterLink to="/demo/full-journey">流程模拟</RouterLink>
|
||||
<RouterLink to="/video/user">视频</RouterLink>
|
||||
<RouterLink to="/about">关于我们</RouterLink>
|
||||
<RouterLink to="/#business">业务介绍</RouterLink>
|
||||
|
||||
@@ -9,6 +9,9 @@ const routes = [
|
||||
children: [
|
||||
{ path: '', name: 'Home', component: () => import('@/views/index/index.vue') },
|
||||
{ path: 'product', name: 'Product', component: () => import('@/views/product/index.vue') },
|
||||
// 流程模拟:小程序真实流程的可交互演示(患者端 5 个流程 + 医生端接诊开方)
|
||||
{ path: 'demo', redirect: '/demo/full-journey' },
|
||||
{ path: 'demo/:flow(full-journey|offline-register|online-followup|pharmacy-buy|pharmacy-followup|order-pay|doctor-reception|pc-prescription|pc-reception)', name: 'FlowDemo', component: () => import('@/views/demo/index.vue') },
|
||||
{ path: 'video', redirect: '/video/user' },
|
||||
{ path: 'video/:audience(user|doctor|manager)', name: 'VideoShowcase', component: () => import('@/views/video/index.vue') },
|
||||
{ path: 'about', name: 'About', component: () => import('@/views/about/index.vue') },
|
||||
|
||||
@@ -8,6 +8,7 @@ export const siteConfig = {
|
||||
primary: [
|
||||
{ label: '首页', to: '/' },
|
||||
{ label: '产品', to: '/product' },
|
||||
{ label: '流程模拟', to: '/demo/full-journey' },
|
||||
{ label: '视频', to: '/video/user' },
|
||||
{ label: '关于我们', to: '/about' }
|
||||
],
|
||||
@@ -25,6 +26,18 @@ export const siteConfig = {
|
||||
{ label: 'FAQ', to: '/product#faq' },
|
||||
{ label: '联系方式', to: '/product#contact' }
|
||||
],
|
||||
// 流程模拟子导航:跨端全景 + 患者端五个业务流程 + 医生端 / PC 端接诊开方
|
||||
demo: [
|
||||
{ label: '全流程演示', to: '/demo/full-journey' },
|
||||
{ label: '线下挂号', to: '/demo/offline-register' },
|
||||
{ label: '在线复诊', to: '/demo/online-followup' },
|
||||
{ label: '药店购药', to: '/demo/pharmacy-buy' },
|
||||
{ label: '药店复诊', to: '/demo/pharmacy-followup' },
|
||||
{ label: '订单支付', to: '/demo/order-pay' },
|
||||
{ label: '医生端接诊开方', to: '/demo/doctor-reception' },
|
||||
{ label: 'PC端在线复诊开方', to: '/demo/pc-prescription' },
|
||||
{ label: 'PC端线下接诊与代付', to: '/demo/pc-reception' }
|
||||
],
|
||||
video: [
|
||||
{ label: '用户端', to: '/video/user' },
|
||||
{ label: '医生端', to: '/video/doctor' },
|
||||
@@ -81,11 +94,12 @@ export const siteConfig = {
|
||||
export const videoShowcase = {
|
||||
user: {
|
||||
label: '用户端',
|
||||
badge: 'Patient App',
|
||||
badge: '患者端演示',
|
||||
title: '把就医、复诊和健康管理放进一个温柔的移动体验。',
|
||||
subtitle: '面向患者和家属,展示预约复诊、报告查看、用药提醒、随访沟通与健康档案的完整路径。',
|
||||
cta: '预约用户端演示',
|
||||
accent: '#14b8a6',
|
||||
// 视频页强调色统一走品牌青蓝(与全站 --brand 一致),不再每端一个色
|
||||
accent: '#0891b2',
|
||||
metrics: [
|
||||
{ value: '3 步', label: '完成复诊预约' },
|
||||
{ value: '24h', label: '随访消息触达' },
|
||||
@@ -93,60 +107,60 @@ export const videoShowcase = {
|
||||
],
|
||||
scenes: ['互联网医院入口', '慢病随访服务', '复诊预约与报告查询'],
|
||||
videos: [
|
||||
{
|
||||
id: 'patient-home',
|
||||
title: '用户端总览',
|
||||
description: '从健康首页进入,快速看懂预约、报告、提醒和随访任务如何集中管理。',
|
||||
duration: '01:12',
|
||||
tag: '入门必看',
|
||||
src: siteConfig.hero.video.src,
|
||||
poster: siteConfig.hero.video.poster,
|
||||
chapters: [
|
||||
{ time: '00:08', title: '首页健康卡片', body: '把待办、报告和医生提醒集中呈现,用户打开就知道下一步。' },
|
||||
{ time: '00:28', title: '复诊与随访', body: '支持线上复诊、随访问卷、用药提醒和医患消息闭环。' },
|
||||
{ time: '00:52', title: '家庭成员管理', body: '家属可协助老人和儿童管理档案、预约与健康任务。' }
|
||||
],
|
||||
highlights: ['报告与处方清晰可追踪', '消息提醒不打扰但不错过', '适配移动端单手操作']
|
||||
},
|
||||
// {
|
||||
// id: 'patient-home',
|
||||
// title: '用户端总览',
|
||||
// description: '从健康首页进入,快速看懂预约、报告、提醒和随访任务如何集中管理。',
|
||||
// duration: '01:12',
|
||||
// tag: '入门必看',
|
||||
// src: siteConfig.hero.video.src,
|
||||
// poster: siteConfig.hero.video.poster,
|
||||
// chapters: [
|
||||
// { time: '00:08', title: '首页健康卡片', body: '把待办、报告和医生提醒集中呈现,用户打开就知道下一步。' },
|
||||
// { time: '00:28', title: '复诊与随访', body: '支持线上复诊、随访问卷、用药提醒和医患消息闭环。' },
|
||||
// { time: '00:52', title: '家庭成员管理', body: '家属可协助老人和儿童管理档案、预约与健康任务。' }
|
||||
// ],
|
||||
// highlights: ['报告与处方清晰可追踪', '消息提醒不打扰但不错过', '适配移动端单手操作']
|
||||
// },
|
||||
{
|
||||
id: 'patient-followup',
|
||||
title: '随访与复诊流程',
|
||||
title: '患者复诊流程',
|
||||
description: '展示患者收到提醒、填写问卷、提交复诊申请到收到医生建议的完整闭环。',
|
||||
duration: '01:36',
|
||||
duration: '02:20',
|
||||
tag: '服务流程',
|
||||
src: siteConfig.hero.video.src,
|
||||
src: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/blog/%E6%82%A3%E8%80%85%E7%AB%AF%E5%9C%A8%E7%BA%BF%E5%A4%8D%E8%AF%8A.mov',
|
||||
poster: siteConfig.hero.video.poster,
|
||||
chapters: [
|
||||
{ time: '00:06', title: '提醒触达', body: '用清楚的任务卡片提示患者按时完成随访和复诊动作。' },
|
||||
{ time: '00:31', title: '问卷填写', body: '把症状、用药、指标和反馈拆成移动端易填写的步骤。' },
|
||||
{ time: '01:05', title: '医生建议回传', body: '医生处理后,患者端能看到复诊建议、宣教内容和下一次提醒。' }
|
||||
// { time: '00:06', title: '提醒触达', body: '用清楚的任务卡片提示患者按时完成随访和复诊动作。' },
|
||||
// { time: '00:31', title: '问卷填写', body: '把症状、用药、指标和反馈拆成移动端易填写的步骤。' },
|
||||
// { time: '01:05', title: '医生建议回传', body: '医生处理后,患者端能看到复诊建议、宣教内容和下一次提醒。' }
|
||||
],
|
||||
highlights: ['随访任务清单化', '复诊状态实时可见', '医患沟通自然衔接']
|
||||
highlights: ['患者复诊流程']
|
||||
},
|
||||
{
|
||||
id: 'patient-family',
|
||||
title: '家庭成员代管',
|
||||
description: '面向老人、儿童和家庭照护场景,展示家属如何代管健康档案与复诊任务。',
|
||||
duration: '01:08',
|
||||
tag: '家庭场景',
|
||||
src: siteConfig.hero.video.src,
|
||||
poster: siteConfig.hero.video.poster,
|
||||
chapters: [
|
||||
{ time: '00:05', title: '成员切换', body: '同一账号下快速切换家庭成员,避免档案和任务混淆。' },
|
||||
{ time: '00:26', title: '健康档案', body: '重要报告、处方、随访记录按成员沉淀,后续查询更省心。' },
|
||||
{ time: '00:49', title: '照护提醒', body: '把用药、复诊和随访提醒同步给家属,减少遗漏。' }
|
||||
],
|
||||
highlights: ['多成员管理清晰', '适合家庭照护', '降低老人操作压力']
|
||||
}
|
||||
// {
|
||||
// id: 'patient-family',
|
||||
// title: '家庭成员代管',
|
||||
// description: '面向老人、儿童和家庭照护场景,展示家属如何代管健康档案与复诊任务。',
|
||||
// duration: '01:08',
|
||||
// tag: '家庭场景',
|
||||
// src: siteConfig.hero.video.src,
|
||||
// poster: siteConfig.hero.video.poster,
|
||||
// chapters: [
|
||||
// { time: '00:05', title: '成员切换', body: '同一账号下快速切换家庭成员,避免档案和任务混淆。' },
|
||||
// { time: '00:26', title: '健康档案', body: '重要报告、处方、随访记录按成员沉淀,后续查询更省心。' },
|
||||
// { time: '00:49', title: '照护提醒', body: '把用药、复诊和随访提醒同步给家属,减少遗漏。' }
|
||||
// ],
|
||||
// highlights: ['多成员管理清晰', '适合家庭照护', '降低老人操作压力']
|
||||
// }
|
||||
]
|
||||
},
|
||||
doctor: {
|
||||
label: '医生端',
|
||||
badge: 'Doctor Console',
|
||||
badge: '医生端演示',
|
||||
title: '让医生快速看清患者状态,并把关键任务稳稳接住。',
|
||||
subtitle: '面向医生团队,展示患者画像、风险提醒、复诊处理、随访审核和协同任务流。',
|
||||
cta: '预约医生端演示',
|
||||
accent: '#22d3ee',
|
||||
accent: '#0891b2',
|
||||
metrics: [
|
||||
{ value: '1 屏', label: '查看患者概况' },
|
||||
{ value: 'AI', label: '辅助风险分层' },
|
||||
@@ -203,11 +217,11 @@ export const videoShowcase = {
|
||||
},
|
||||
manager: {
|
||||
label: '小程序店长管理端',
|
||||
badge: 'Mini Program Manager',
|
||||
badge: '店长端演示',
|
||||
title: '让店长在小程序里看懂运营、服务和团队执行。',
|
||||
subtitle: '面向门店店长和运营负责人,展示门店指标、服务进度、人员任务、客户触达与异常提醒。',
|
||||
cta: '预约店长端演示',
|
||||
accent: '#059669',
|
||||
accent: '#0891b2',
|
||||
metrics: [
|
||||
{ value: '实时', label: '查看门店经营状态' },
|
||||
{ value: '清单化', label: '跟进服务任务' },
|
||||
|
||||
@@ -2431,6 +2431,12 @@ textarea {
|
||||
right: 1rem;
|
||||
top: 76px;
|
||||
z-index: 40;
|
||||
/* 菜单组展开后可能高于视口(流程模拟组有 7 个子链接),限高并让内部滚动,避免底部选项超出屏幕够不到 */
|
||||
max-height: calc(100vh - 92px);
|
||||
max-height: calc(100dvh - 92px);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.mobile-menu-group {
|
||||
@@ -2490,6 +2496,31 @@ textarea {
|
||||
transform: translateY(-1rem);
|
||||
}
|
||||
|
||||
/* 窄机型顶栏防溢出:搜索按钮收成纯图标,控件间距收紧,logo 文字保留但不再撑最小宽 */
|
||||
@media (max-width: 480px) {
|
||||
.site-header {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.site-actions {
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.site-search-trigger {
|
||||
height: 2.5rem;
|
||||
padding: 0;
|
||||
width: 2.5rem;
|
||||
}
|
||||
|
||||
.site-search-trigger span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.site-logo span {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
.home-page {
|
||||
background: transparent;
|
||||
color: #d7e2ea;
|
||||
@@ -2612,20 +2643,21 @@ textarea {
|
||||
padding: 0.72rem 1.35rem;
|
||||
}
|
||||
|
||||
/* 首页主 CTA:统一品牌青蓝渐变,与全站 --brand 同色系(替换原先的薄荷绿渐变) */
|
||||
.watch-button {
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #c8fff2 48%, #ffffff 100%);
|
||||
color: #041012;
|
||||
background: linear-gradient(135deg, #0e7490 0%, #0891b2 46%, #22d3ee 100%);
|
||||
color: #ffffff;
|
||||
display: inline-flex;
|
||||
font-weight: 700;
|
||||
gap: 0.5rem;
|
||||
text-decoration: none;
|
||||
transition: background 180ms ease-out, transform 180ms ease-out;
|
||||
box-shadow: 0 20px 48px rgba(20, 184, 166, 0.24), inset 0 1px 0 rgba(255, 255, 255, 0.72);
|
||||
transition: filter 180ms ease-out, transform 180ms ease-out;
|
||||
box-shadow: 0 18px 42px rgba(8, 145, 178, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.28);
|
||||
}
|
||||
|
||||
.watch-button:hover {
|
||||
background: #e5e7eb;
|
||||
filter: brightness(1.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@@ -2841,13 +2873,18 @@ textarea {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* 联系顾问/立即咨询:原为作品集模板遗留的紫红橙渐变,统一为品牌青蓝渐变 */
|
||||
.creator-contact-button {
|
||||
background: linear-gradient(123deg, #18011f 7%, #b600a8 37%, #7621b0 72%, #be4c00 100%);
|
||||
box-shadow: 0 4px 4px rgba(181, 1, 167, 0.25), 4px 4px 12px #7721b1 inset;
|
||||
background: linear-gradient(135deg, #0e7490 0%, #0891b2 48%, #22d3ee 100%);
|
||||
box-shadow: 0 14px 34px rgba(8, 145, 178, 0.36), inset 0 1px 0 rgba(255, 255, 255, 0.24);
|
||||
color: white;
|
||||
outline: 2px solid white;
|
||||
outline-offset: -3px;
|
||||
padding: 0.9rem 2.5rem;
|
||||
transition: filter 180ms ease-out, transform 180ms ease-out;
|
||||
}
|
||||
|
||||
.creator-contact-button:hover {
|
||||
filter: brightness(1.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.creator-services-section {
|
||||
@@ -3006,15 +3043,18 @@ textarea {
|
||||
max-width: 44rem;
|
||||
}
|
||||
|
||||
/* 案例卡「预约演示」描边按钮:描边与文字向品牌青靠拢,与主 CTA 同色系 */
|
||||
.live-project-button {
|
||||
border: 2px solid #d7e2ea;
|
||||
color: #d7e2ea;
|
||||
border: 2px solid rgba(34, 211, 238, 0.45);
|
||||
color: #a5f3fc;
|
||||
padding: 0.85rem 1.5rem;
|
||||
white-space: nowrap;
|
||||
transition: background 180ms ease-out, border-color 180ms ease-out;
|
||||
}
|
||||
|
||||
.live-project-button:hover {
|
||||
background: rgba(215, 226, 234, 0.1);
|
||||
background: rgba(34, 211, 238, 0.12);
|
||||
border-color: rgba(34, 211, 238, 0.7);
|
||||
}
|
||||
|
||||
.case-image-grid {
|
||||
@@ -5260,9 +5300,10 @@ textarea {
|
||||
}
|
||||
|
||||
/* Video showcase: cinematic but accessible C-end demo experience */
|
||||
/* 背景光斑统一品牌青蓝(--brand),不再混入 teal 第二色系 */
|
||||
.video-route-backdrop {
|
||||
background:
|
||||
radial-gradient(circle at 20% 0%, color-mix(in srgb, #14b8a6 14%, transparent), transparent 34rem),
|
||||
radial-gradient(circle at 20% 0%, color-mix(in srgb, #0891b2 14%, transparent), transparent 34rem),
|
||||
radial-gradient(circle at 86% 18%, color-mix(in srgb, #22d3ee 16%, transparent), transparent 30rem),
|
||||
var(--app-bg);
|
||||
}
|
||||
@@ -5286,7 +5327,8 @@ textarea {
|
||||
}
|
||||
|
||||
.video-page {
|
||||
--video-accent: #14b8a6;
|
||||
/* 强调色直接引用全站品牌变量:亮色 #0891b2、暗色自动切到 #22d3ee,保证与全站主题一致 */
|
||||
--video-accent: var(--brand);
|
||||
--video-ink: #102a32;
|
||||
--video-muted: #4d6570;
|
||||
--video-panel: rgba(255, 255, 255, 0.84);
|
||||
@@ -5435,11 +5477,12 @@ textarea {
|
||||
transition: background 200ms ease-out, border-color 200ms ease-out, color 200ms ease-out, transform 200ms ease-out;
|
||||
}
|
||||
|
||||
/* 主 CTA 与首页 watch-button 同款品牌青蓝渐变,全站按钮语言统一 */
|
||||
.video-primary-action {
|
||||
background: linear-gradient(135deg, #ffffff 0%, color-mix(in srgb, var(--video-accent) 30%, #ffffff) 55%, #ffffff 100%);
|
||||
border: 1px solid color-mix(in srgb, var(--video-accent) 36%, rgba(255, 255, 255, 0.72));
|
||||
box-shadow: 0 18px 42px color-mix(in srgb, var(--video-accent) 20%, rgba(10, 66, 74, 0.12));
|
||||
color: #061316;
|
||||
background: linear-gradient(135deg, #0e7490 0%, #0891b2 46%, #22d3ee 100%);
|
||||
border: 1px solid color-mix(in srgb, var(--video-accent) 40%, transparent);
|
||||
box-shadow: 0 18px 42px color-mix(in srgb, var(--video-accent) 26%, rgba(10, 66, 74, 0.12));
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.video-primary-action:hover {
|
||||
@@ -5658,7 +5701,7 @@ textarea {
|
||||
.cinematic-player-video {
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
182
src/views/demo/components/BrowserFrame.vue
Normal file
182
src/views/demo/components/BrowserFrame.vue
Normal file
@@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<div class="browser-frame">
|
||||
<!-- 标签栏:红黄绿窗控 + 页签 -->
|
||||
<div class="browser-tabs">
|
||||
<span class="browser-lights" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
<span class="browser-tab">
|
||||
<span class="browser-favicon" aria-hidden="true"></span>
|
||||
{{ tabTitle }}
|
||||
<svg class="browser-tab-close" width="8" height="8" viewBox="0 0 8 8" aria-hidden="true"><path d="M1 1l6 6M7 1L1 7" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
</span>
|
||||
<span class="browser-tab-add" aria-hidden="true">+</span>
|
||||
</div>
|
||||
<!-- 地址栏 -->
|
||||
<div class="browser-addr">
|
||||
<span class="browser-nav-btns" aria-hidden="true">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M15 5l-7 7 7 7" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" opacity="0.4"><path d="M9 5l7 7-7 7" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none"><path d="M20 11A8 8 0 1 0 20 13" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M20 5v6h-6" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</span>
|
||||
<span class="browser-url">
|
||||
<svg width="10" height="12" viewBox="0 0 12 14" fill="none" aria-hidden="true"><rect x="1" y="6" width="10" height="7" rx="1.6" fill="currentColor" opacity="0.75"/><path d="M3.5 6V4.5a2.5 2.5 0 0 1 5 0V6" stroke="currentColor" stroke-width="1.6"/></svg>
|
||||
{{ url }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 页面内容:逻辑尺寸 1024 x 640 -->
|
||||
<div class="browser-viewport">
|
||||
<slot />
|
||||
<!-- 覆盖层:居中弹窗、消息提示等 -->
|
||||
<div class="browser-overlay">
|
||||
<slot name="overlay" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 浏览器窗口壳(BrowserFrame)
|
||||
*
|
||||
* 为什么这样写:
|
||||
* - PC 管理端的流程模拟需要一个「桌面浏览器」语境,与手机壳(PhoneFrame)形成明确的端别区分。
|
||||
* - 视口逻辑尺寸固定 1024x640,复刻 Vben Admin 布局时按此宽度排版,外层由 demo 壳统一等比缩放。
|
||||
* - overlay 插槽承载居中弹窗(VbenModal 形态)与消息提示,盖在页面内容之上。
|
||||
*/
|
||||
defineProps({
|
||||
/** 浏览器页签标题 */
|
||||
tabTitle: { type: String, default: '萧康云医管理后台' },
|
||||
/** 地址栏 URL */
|
||||
url: { type: String, default: 'admin.xkyy.cn/doctor/doctor-reception' }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.browser-frame {
|
||||
width: 1024px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #dee1e6;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(0, 0, 0, 0.08),
|
||||
0 30px 70px rgba(10, 30, 46, 0.32),
|
||||
0 8px 20px rgba(10, 30, 46, 0.2);
|
||||
}
|
||||
|
||||
/* 标签栏 */
|
||||
.browser-tabs {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
height: 38px;
|
||||
padding: 0 12px;
|
||||
background: #dee1e6;
|
||||
}
|
||||
|
||||
.browser-lights {
|
||||
display: inline-flex;
|
||||
gap: 7px;
|
||||
margin: 0 6px 9px 2px;
|
||||
}
|
||||
|
||||
.browser-lights i {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.browser-lights i:nth-child(1) { background: #ff5f57; }
|
||||
.browser-lights i:nth-child(2) { background: #febc2e; }
|
||||
.browser-lights i:nth-child(3) { background: #28c840; }
|
||||
|
||||
.browser-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 30px;
|
||||
padding: 0 12px;
|
||||
border-radius: 9px 9px 0 0;
|
||||
background: #f7f8fa;
|
||||
font-size: 12px;
|
||||
color: #3c4043;
|
||||
max-width: 240px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.browser-favicon {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(135deg, #298dff, #4175ee);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.browser-tab-close {
|
||||
color: #9aa0a6;
|
||||
margin-left: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.browser-tab-add {
|
||||
margin-bottom: 7px;
|
||||
font-size: 15px;
|
||||
color: #5f6368;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 地址栏 */
|
||||
.browser-addr {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
height: 38px;
|
||||
padding: 0 12px;
|
||||
background: #f7f8fa;
|
||||
border-bottom: 1px solid #e4e7eb;
|
||||
}
|
||||
|
||||
.browser-nav-btns {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #5f6368;
|
||||
}
|
||||
|
||||
.browser-url {
|
||||
flex: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
height: 26px;
|
||||
padding: 0 13px;
|
||||
border-radius: 13px;
|
||||
background: #eef1f4;
|
||||
font-size: 12px;
|
||||
color: #3c4043;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.browser-url svg {
|
||||
color: #5f6368;
|
||||
}
|
||||
|
||||
/* 视口 */
|
||||
.browser-viewport {
|
||||
position: relative;
|
||||
width: 1024px;
|
||||
height: 640px;
|
||||
background: #f5f7fa;
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.browser-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.browser-overlay :deep(> *) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
</style>
|
||||
87
src/views/demo/components/FakeQrcode.vue
Normal file
87
src/views/demo/components/FakeQrcode.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<svg
|
||||
class="fake-qrcode"
|
||||
:width="size"
|
||||
:height="size"
|
||||
:viewBox="`0 0 ${GRID} ${GRID}`"
|
||||
role="img"
|
||||
aria-label="演示二维码(虚构,不可扫描)"
|
||||
>
|
||||
<rect :width="GRID" :height="GRID" fill="#ffffff" />
|
||||
<!-- 数据模块:伪随机分布 -->
|
||||
<rect
|
||||
v-for="cell in cells"
|
||||
:key="`${cell[0]}-${cell[1]}`"
|
||||
:x="cell[0]"
|
||||
:y="cell[1]"
|
||||
width="1"
|
||||
height="1"
|
||||
fill="#111827"
|
||||
/>
|
||||
<!-- 三个定位角 -->
|
||||
<g v-for="(pos, index) in finders" :key="index" :transform="`translate(${pos[0]}, ${pos[1]})`">
|
||||
<rect width="7" height="7" fill="#111827" />
|
||||
<rect x="1" y="1" width="5" height="5" fill="#ffffff" />
|
||||
<rect x="2" y="2" width="3" height="3" fill="#111827" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 演示用假二维码(FakeQrcode)
|
||||
*
|
||||
* 为什么这样写:
|
||||
* - 流程模拟里的「认领码 / 代支付码」需要二维码视觉,但不能依赖外部图片或真实生成库,
|
||||
* 用 SVG 按 25x25 网格画出定位角 + 伪随机数据模块,视觉可信且零依赖。
|
||||
* - 伪随机使用固定种子的线性同余,保证每次渲染图案一致(避免闪烁),seed 可让不同弹窗图案不同。
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
/** 渲染尺寸(px) */
|
||||
size: { type: Number, default: 220 },
|
||||
/** 图案种子:不同场景传不同值即可得到不同图案 */
|
||||
seed: { type: Number, default: 7 }
|
||||
})
|
||||
|
||||
const GRID = 25
|
||||
const finders = [
|
||||
[0, 0],
|
||||
[GRID - 7, 0],
|
||||
[0, GRID - 7]
|
||||
]
|
||||
|
||||
/** 判断坐标是否落在定位角区域(含 1 格留白),这些区域不画数据点 */
|
||||
function inFinder(x, y) {
|
||||
return (
|
||||
(x < 8 && y < 8) ||
|
||||
(x >= GRID - 8 && y < 8) ||
|
||||
(x < 8 && y >= GRID - 8)
|
||||
)
|
||||
}
|
||||
|
||||
/** 固定种子伪随机点阵 */
|
||||
const cells = computed(() => {
|
||||
let state = props.seed * 2654435761 % 2147483647
|
||||
const next = () => {
|
||||
state = (state * 48271) % 2147483647
|
||||
return state / 2147483647
|
||||
}
|
||||
const list = []
|
||||
for (let y = 0; y < GRID; y++) {
|
||||
for (let x = 0; x < GRID; x++) {
|
||||
if (inFinder(x, y)) continue
|
||||
if (next() < 0.46) list.push([x, y])
|
||||
}
|
||||
}
|
||||
return list
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fake-qrcode {
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
262
src/views/demo/components/PhoneFrame.vue
Normal file
262
src/views/demo/components/PhoneFrame.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<template>
|
||||
<div class="phone-frame" :class="`phone-tone-${statusTone}`">
|
||||
<!-- 侧键:音量与电源,纯装饰增强真实感 -->
|
||||
<span class="phone-key phone-key-mute" aria-hidden="true"></span>
|
||||
<span class="phone-key phone-key-volume-up" aria-hidden="true"></span>
|
||||
<span class="phone-key phone-key-volume-down" aria-hidden="true"></span>
|
||||
<span class="phone-key phone-key-power" aria-hidden="true"></span>
|
||||
|
||||
<div class="phone-screen" :style="{ background: screenBg }">
|
||||
<!-- 屏幕内容:由各流程的模拟页面填充,可从屏幕顶部铺满(自定义导航页需要) -->
|
||||
<div class="phone-content">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<!-- 灵动岛 + 状态栏:永远悬浮在内容上方 -->
|
||||
<div class="phone-statusbar" aria-hidden="true">
|
||||
<span class="statusbar-time">9:41</span>
|
||||
<span class="phone-island"></span>
|
||||
<span class="statusbar-icons">
|
||||
<!-- 信号 / WiFi / 电量,内联 SVG 跟随 tone 变色 -->
|
||||
<svg width="17" height="11" viewBox="0 0 17 11" fill="currentColor"><rect x="0" y="7" width="3" height="4" rx="0.8"/><rect x="4.5" y="5" width="3" height="6" rx="0.8"/><rect x="9" y="2.5" width="3" height="8.5" rx="0.8"/><rect x="13.5" y="0" width="3" height="11" rx="0.8"/></svg>
|
||||
<svg width="16" height="11" viewBox="0 0 16 11" fill="currentColor"><path d="M8 9.4a1.4 1.4 0 1 0 0 2.8 1.4 1.4 0 0 0 0-2.8Zm-3.1-2.6 1.4 1.4a2.4 2.4 0 0 1 3.4 0l1.4-1.4a4.4 4.4 0 0 0-6.2 0ZM2 3.9l1.4 1.4a6.5 6.5 0 0 1 9.2 0L14 3.9a8.5 8.5 0 0 0-12 0Z" transform="translate(0 -1.5)"/></svg>
|
||||
<svg width="25" height="12" viewBox="0 0 25 12" fill="none"><rect x="0.5" y="0.5" width="21" height="11" rx="3.2" stroke="currentColor" opacity="0.4"/><rect x="2" y="2" width="17" height="8" rx="1.8" fill="currentColor"/><path d="M23 4v4c1-.3 1.6-1 1.6-2S24 4.3 23 4Z" fill="currentColor" opacity="0.4"/></svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 微信小程序胶囊按钮:所有小程序页面右上角常驻 -->
|
||||
<div class="phone-capsule" :class="`capsule-tone-${capsuleTone}`" aria-hidden="true">
|
||||
<span class="capsule-dots"><i></i><i></i><i></i></span>
|
||||
<span class="capsule-divider"></span>
|
||||
<span class="capsule-target"></span>
|
||||
</div>
|
||||
|
||||
<!-- 覆盖层插槽:支付面板、弹窗等需要盖住整屏的内容 -->
|
||||
<div class="phone-overlay">
|
||||
<slot name="overlay" />
|
||||
</div>
|
||||
|
||||
<!-- Home 指示条 -->
|
||||
<span class="phone-home-indicator" :class="`indicator-tone-${indicatorTone}`" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 手机壳组件(PhoneFrame)
|
||||
*
|
||||
* 为什么这样写:
|
||||
* - 流程模拟的所有小程序界面都放在这个「手机」里展示,壳体统一负责灵动岛、状态栏、
|
||||
* 微信胶囊按钮和 Home 指示条,各流程只需要专注还原页面本身。
|
||||
* - 屏幕逻辑宽度固定 375px(微信小程序 750rpx = 375px,即 1rpx = 0.5px),
|
||||
* 这样复刻源码里的 rpx 尺寸时直接除以 2 即可,换算无歧义。
|
||||
* - 状态栏/胶囊的深浅色由 props 控制,因为不同页面导航底色不同(白底页用深色文字,蓝底页用白色)。
|
||||
*/
|
||||
defineProps({
|
||||
/** 状态栏文字深浅:light = 白字(深色导航用),dark = 黑字(浅色导航用) */
|
||||
statusTone: { type: String, default: 'dark' },
|
||||
/** 胶囊按钮深浅:light = 白底黑图标,dark = 半透明黑底白图标 */
|
||||
capsuleTone: { type: String, default: 'light' },
|
||||
/** Home 指示条深浅 */
|
||||
indicatorTone: { type: String, default: 'dark' },
|
||||
/** 屏幕兜底背景色 */
|
||||
screenBg: { type: String, default: '#f8f8f8' }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 手机壳体:钛金属质感边框 + 深色内衬 */
|
||||
.phone-frame {
|
||||
position: relative;
|
||||
width: 399px;
|
||||
padding: 12px;
|
||||
border-radius: 58px;
|
||||
background: linear-gradient(160deg, #3d4650, #14181d 42%, #2c343d 100%);
|
||||
box-shadow:
|
||||
inset 0 0 0 2px rgba(255, 255, 255, 0.08),
|
||||
0 34px 80px rgba(10, 30, 46, 0.4),
|
||||
0 8px 22px rgba(10, 30, 46, 0.28);
|
||||
}
|
||||
|
||||
/* 侧键 */
|
||||
.phone-key {
|
||||
position: absolute;
|
||||
width: 3px;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(180deg, #454f59, #1a1f25);
|
||||
}
|
||||
|
||||
.phone-key-mute { left: -3px; top: 138px; height: 26px; }
|
||||
.phone-key-volume-up { left: -3px; top: 186px; height: 48px; }
|
||||
.phone-key-volume-down { left: -3px; top: 244px; height: 48px; }
|
||||
.phone-key-power { right: -3px; top: 210px; height: 74px; }
|
||||
|
||||
/* 屏幕:逻辑尺寸 375 x 760 */
|
||||
.phone-screen {
|
||||
position: relative;
|
||||
width: 375px;
|
||||
height: 760px;
|
||||
border-radius: 46px;
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.phone-content {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 状态栏 */
|
||||
.phone-statusbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 54px;
|
||||
z-index: 6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 30px 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.phone-tone-dark .phone-statusbar { color: #10161d; }
|
||||
.phone-tone-light .phone-statusbar { color: #ffffff; }
|
||||
|
||||
.statusbar-time {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.statusbar-icons {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* 灵动岛 */
|
||||
.phone-island {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 116px;
|
||||
height: 33px;
|
||||
border-radius: 20px;
|
||||
background: #04060a;
|
||||
}
|
||||
|
||||
.phone-island::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 34% 30%, #24313f, #05070a 68%);
|
||||
}
|
||||
|
||||
/* 微信胶囊 */
|
||||
.phone-capsule {
|
||||
position: absolute;
|
||||
top: 62px;
|
||||
right: 12px;
|
||||
z-index: 6;
|
||||
width: 87px;
|
||||
height: 32px;
|
||||
border-radius: 17px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.capsule-tone-light {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.08);
|
||||
color: #06080c;
|
||||
}
|
||||
|
||||
.capsule-tone-dark {
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
border: 0.5px solid rgba(255, 255, 255, 0.28);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.capsule-dots {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3.5px;
|
||||
}
|
||||
|
||||
.capsule-dots i {
|
||||
width: 4.5px;
|
||||
height: 4.5px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.capsule-dots i:nth-child(2) { width: 5.5px; height: 5.5px; }
|
||||
|
||||
.capsule-divider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: currentColor;
|
||||
opacity: 0.22;
|
||||
}
|
||||
|
||||
.capsule-target {
|
||||
position: relative;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
border: 1.6px solid currentColor;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.capsule-target::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
/* 覆盖层:支付面板 / 弹窗 */
|
||||
.phone-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 8;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.phone-overlay :deep(> *) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Home 指示条 */
|
||||
.phone-home-indicator {
|
||||
position: absolute;
|
||||
bottom: 9px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 128px;
|
||||
height: 5px;
|
||||
border-radius: 3px;
|
||||
z-index: 9;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.indicator-tone-dark { background: rgba(8, 12, 18, 0.85); }
|
||||
.indicator-tone-light { background: rgba(255, 255, 255, 0.9); }
|
||||
</style>
|
||||
78
src/views/demo/components/WxNavBar.vue
Normal file
78
src/views/demo/components/WxNavBar.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<header class="wx-navbar" :style="{ background: bg, color }">
|
||||
<div class="wx-navbar-row">
|
||||
<button v-if="showBack" type="button" class="wx-navbar-back" aria-label="返回" @click="emit('back')">
|
||||
<svg width="12" height="21" viewBox="0 0 12 21" fill="none">
|
||||
<path d="M10.5 1.5 2 10.5l8.5 9" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<span class="wx-navbar-title">{{ title }}</span>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 小程序标准导航栏(WxNavBar)
|
||||
*
|
||||
* 为什么这样写:
|
||||
* - 复刻微信小程序默认导航(navigationStyle: default)的视觉:
|
||||
* 状态栏下方 44px 高、标题居中 17px、左侧返回箭头,右侧空间留给胶囊按钮。
|
||||
* - 顶部 padding 使用 PhoneFrame 的状态栏高度(54px),保证内容从正确位置开始。
|
||||
* - 背景/文字颜色由 props 控制,对应 pages.json 里每页的 navigationBarBackgroundColor / TextStyle。
|
||||
*/
|
||||
defineProps({
|
||||
title: { type: String, required: true },
|
||||
/** 导航背景色,对应 navigationBarBackgroundColor */
|
||||
bg: { type: String, default: '#f8fafc' },
|
||||
/** 标题与返回键颜色,对应 navigationBarTextStyle */
|
||||
color: { type: String, default: '#0b0f14' },
|
||||
/** 是否显示返回箭头(栈顶首页不显示) */
|
||||
showBack: { type: Boolean, default: true }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['back'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wx-navbar {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding-top: 54px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wx-navbar-row {
|
||||
position: relative;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wx-navbar-back {
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.wx-navbar-title {
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
</style>
|
||||
88
src/views/demo/components/WxTabBar.vue
Normal file
88
src/views/demo/components/WxTabBar.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<nav class="wx-tabbar" aria-label="小程序底部导航">
|
||||
<button
|
||||
v-for="(item, index) in items"
|
||||
:key="item.label"
|
||||
type="button"
|
||||
class="wx-tabbar-item"
|
||||
:style="{ color: index === active ? selectedColor : color }"
|
||||
@click="emit('select', index)"
|
||||
>
|
||||
<component :is="item.icon" :size="23" :stroke-width="index === active ? 2.2 : 1.8" aria-hidden="true" />
|
||||
<span>{{ item.label }}</span>
|
||||
<i v-if="item.badge" class="wx-tabbar-badge">{{ item.badge }}</i>
|
||||
</button>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 小程序底部 TabBar(WxTabBar)
|
||||
*
|
||||
* 为什么这样写:
|
||||
* - 复刻微信小程序 tabBar:白底、上分割线、图标在上文字在下、选中色高亮。
|
||||
* - 图标传 lucide 组件引用(源码里是 PNG 图标,演示用等价线性图标还原视觉气质)。
|
||||
* - 高度 56px + 底部 22px 安全区,给 Home 指示条留出空间,与真机观感一致。
|
||||
*/
|
||||
defineProps({
|
||||
/** tab 配置:{ label, icon: Component, badge?: string } */
|
||||
items: { type: Array, required: true },
|
||||
/** 当前选中索引 */
|
||||
active: { type: Number, default: 0 },
|
||||
/** 选中颜色:患者端/医生端都是 #1575FC */
|
||||
selectedColor: { type: String, default: '#1575fc' },
|
||||
/** 未选中颜色 */
|
||||
color: { type: String, default: '#666666' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['select'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wx-tabbar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 4;
|
||||
height: 78px;
|
||||
padding-bottom: 22px;
|
||||
display: flex;
|
||||
background: #ffffff;
|
||||
border-top: 0.5px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.wx-tabbar-item {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.wx-tabbar-badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: calc(50% + 6px);
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
border-radius: 9px;
|
||||
background: #fa5151;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
201
src/views/demo/flows.js
Normal file
201
src/views/demo/flows.js
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* 流程模拟注册表
|
||||
*
|
||||
* 为什么这样写:
|
||||
* - 把每个流程的元信息(名称、端别、讲解步骤)与对应的模拟组件集中登记,
|
||||
* 壳页面(demo/index.vue)只按注册表渲染,新增流程只需在这里加一项。
|
||||
* - 步骤文案与流程组件的屏幕一一对应:steps[i] 描述的就是组件在 step=i 时渲染的屏幕。
|
||||
* - 患者端界面复刻自 xk-client-wx(惠佑康患者端),医生端复刻自 xk-doctor-wx(萧康互联网医院医生端)。
|
||||
*/
|
||||
import FullJourneyFlow from './flows/FullJourneyFlow.vue'
|
||||
import OfflineRegisterFlow from './flows/OfflineRegisterFlow.vue'
|
||||
import OnlineFollowupFlow from './flows/OnlineFollowupFlow.vue'
|
||||
import PharmacyBuyFlow from './flows/PharmacyBuyFlow.vue'
|
||||
import PharmacyFollowupFlow from './flows/PharmacyFollowupFlow.vue'
|
||||
import OrderPayFlow from './flows/OrderPayFlow.vue'
|
||||
import DoctorReceptionFlow from './flows/DoctorReceptionFlow.vue'
|
||||
import PcPrescriptionFlow from './flows/PcPrescriptionFlow.vue'
|
||||
import PcReceptionFlow from './flows/PcReceptionFlow.vue'
|
||||
|
||||
/** 流程展示顺序:跨端全景开场,患者端业务闭环随后,医生端协同居中,诊所 PC 端收尾 */
|
||||
export const flowOrder = [
|
||||
'full-journey',
|
||||
'offline-register',
|
||||
'online-followup',
|
||||
'pharmacy-buy',
|
||||
'pharmacy-followup',
|
||||
'order-pay',
|
||||
'doctor-reception',
|
||||
'pc-prescription',
|
||||
'pc-reception'
|
||||
]
|
||||
|
||||
export const flowDemos = {
|
||||
'full-journey': {
|
||||
label: '全流程 · 一单串三端',
|
||||
side: 'journey',
|
||||
sideLabel: '跨端全景',
|
||||
appLabel: '患者端 × 医生端 × 诊所 PC · 全链路演示',
|
||||
accent: '#0891b2',
|
||||
// mixed:帧类型由每个步骤的 frame 字段决定,壳页面按步在手机 / PC 画面间切换
|
||||
frame: 'mixed',
|
||||
intro: '一次就诊的完整旅程:患者微信扫码提交复诊,医生手机上秒收提醒随时接诊,回到诊室 PC 工作台专业开方,处方推回患者微信直接支付——三端数据同一条链路,一步不断。',
|
||||
component: FullJourneyFlow,
|
||||
steps: [
|
||||
{ title: '打开微信 · 扫一扫', desc: '故事从患者的微信开始:点右上角「+」,选择「扫一扫」。', frame: 'phone', deviceLabel: '患者的手机 · 微信' },
|
||||
{ title: '扫码进入小程序', desc: '对准诊所台卡上的小程序码,识别即进入惠佑康患者端,无需下载。', frame: 'phone', deviceLabel: '患者的手机 · 微信扫一扫' },
|
||||
{ title: '首页 · 发起在线复诊', desc: '点击首页「在线复诊」入口卡,为家里老人发起一次线上复诊。', frame: 'phone', deviceLabel: '患者的手机 · 惠佑康小程序' },
|
||||
{ title: '确认复诊信息 · 提交', desc: '就诊人、病情主诉、用药史一屏核对,点击「提交复诊申请」,申请直达医生队列。', frame: 'phone', deviceLabel: '患者的手机 · 惠佑康小程序' },
|
||||
{ title: '医生手机 · 秒收提醒', desc: '镜头切到李国栋医生的手机:工作台「在线接诊」已亮起新申请角标,点击进入。', frame: 'phone', deviceLabel: '医生的手机 · 萧康互联网医院' },
|
||||
{ title: '在线接诊 · 接下患者', desc: '张淑芬的复诊申请排在队首,病情摘要一目了然,点击卡片接诊。', frame: 'phone', deviceLabel: '医生的手机 · 萧康互联网医院' },
|
||||
{ title: '诊室 PC · 开方工作台', desc: '回到诊室,接诊已同步到 PC 工作台:药品、诊断、医嘱都已备好,点击「发送处方」。', frame: 'pc', deviceLabel: '诊室电脑 · 诊所管理后台' },
|
||||
{ title: '确认发送 · 药师审核', desc: '发送前再核对一遍患者、诊断、药品与金额,点击「确认发送」,处方进入药师审核。', frame: 'pc', deviceLabel: '诊室电脑 · 诊所管理后台' },
|
||||
{ title: '患者微信 · 收到通知', desc: '镜头切回患者微信:处方通过审核,「服务通知」第一时间提醒,点击查看。', frame: 'phone', deviceLabel: '患者的手机 · 微信' },
|
||||
{ title: '微信支付 · 确认付款', desc: '订单金额与处方一致(¥86.00),支付面板已唤起,点击「支付」完成付款。', frame: 'phone', deviceLabel: '患者的手机 · 惠佑康小程序' },
|
||||
{ title: '支付完成 · 门店备药', desc: '一单走完三端:扫码、接诊、开方、支付全程留痕,门店开始按方备药。', frame: 'phone', deviceLabel: '患者的手机 · 惠佑康小程序' }
|
||||
]
|
||||
},
|
||||
'offline-register': {
|
||||
label: '线下挂号',
|
||||
side: 'patient',
|
||||
sideLabel: '患者端',
|
||||
appLabel: '患者端小程序 · 惠佑康',
|
||||
accent: '#1575fc',
|
||||
intro: '从打开微信扫一扫开始:进小程序选医生、挑时段、确认信息,一分钟完成到店挂号,号源与费用全程透明。',
|
||||
component: OfflineRegisterFlow,
|
||||
steps: [
|
||||
{ title: '打开微信 · 扫一扫', desc: '就医从微信开始:点右上角「+」,在菜单里选择「扫一扫」。' },
|
||||
{ title: '扫码进入小程序', desc: '对准诊所前台的小程序码,识别即进入惠佑康患者端,无需下载安装。' },
|
||||
{ title: '首页 · 进入挂号', desc: '小程序首页是所有服务的起点,点击「到店挂号」入口卡进入医生列表。' },
|
||||
{ title: '医生列表 · 选择医生', desc: '出诊医生按科室聚合,卡片直接给出职称、擅长与问诊量,点击「挂号」按钮继续。' },
|
||||
{ title: '医生详情 · 选择号源', desc: '查看医生出诊时段与剩余号源,约满的时段会置灰,点击可约时段的「挂号」。' },
|
||||
{ title: '确认挂号信息', desc: '核对就诊人、就诊时间与挂号费用,勾选须知后点击「确认挂号」。' },
|
||||
{ title: '挂号成功', desc: '挂号完成,到店报就诊序号即可候诊;记录随时可在「我的挂号」查询。' }
|
||||
]
|
||||
},
|
||||
'online-followup': {
|
||||
label: '在线复诊',
|
||||
side: 'patient',
|
||||
sideLabel: '患者端',
|
||||
appLabel: '患者端小程序 · 惠佑康',
|
||||
accent: '#1575fc',
|
||||
intro: '复诊患者不用再跑医院:微信扫码进小程序,提交病情资料,医生接诊问诊后在线开方,续方建议直达手机。',
|
||||
component: OnlineFollowupFlow,
|
||||
steps: [
|
||||
{ title: '打开微信 · 扫一扫', desc: '复诊入口就在微信里:点右上角「+」,选择「扫一扫」。' },
|
||||
{ title: '扫码进入小程序', desc: '对准诊所台卡上的小程序码,识别即进入惠佑康患者端。' },
|
||||
{ title: '首页 · 进入在线复诊', desc: '点击首页「在线复诊」入口卡,开始一次线上复诊。' },
|
||||
{ title: '选择就诊人', desc: '选择本次复诊的就诊人,家庭成员的档案都在同一账号下管理,选好后点「下一步」。' },
|
||||
{ title: '填写复诊单', desc: '描述本次病情主诉、勾选相关用药与健康信息,点击「下一步」提交复诊申请。' },
|
||||
{ title: '提交成功 · 等待接诊', desc: '复诊申请已进入医生的待接诊队列,医生接诊后会收到消息提醒。' },
|
||||
{ title: '医生问诊 · 在线沟通', desc: '医生接诊后先了解近况,输入框里已拟好回复,点击「发送」告知医生。' },
|
||||
{ title: '医生开方 · 处方直达', desc: '确认病情稳定后医生开具电子处方,处方卡片直接出现在会话里,药师审核通过即可支付。' }
|
||||
]
|
||||
},
|
||||
'pharmacy-buy': {
|
||||
label: '药店购药',
|
||||
side: 'patient',
|
||||
sideLabel: '患者端',
|
||||
appLabel: '患者端小程序 · 惠佑康',
|
||||
accent: '#1575fc',
|
||||
intro: '门店商品搬进小程序:微信扫柜台码进店,浏览、加购、结算一气呵成,非处方药下单即发。',
|
||||
component: PharmacyBuyFlow,
|
||||
steps: [
|
||||
{ title: '打开微信 · 扫一扫', desc: '在药店柜台看到台卡:打开微信,点右上角「+」选择「扫一扫」。' },
|
||||
{ title: '扫码进入药店小程序', desc: '识别药店小程序码,直接进入线上门店,货架和柜台同款同价。' },
|
||||
{ title: '门店 · 浏览药品', desc: '进入购药页,左侧分类、右侧双列药品,OTC 与处方药标识清晰,点击商品查看详情。' },
|
||||
{ title: '商品详情 · 加入购物车', desc: '查看价格、药品说明与门店服务保障,点击「加入购物车」。' },
|
||||
{ title: '购物车 · 去结算', desc: '确认数量与合计金额,点击「去结算」生成订单。' },
|
||||
{ title: '确认订单 · 提交', desc: '核对收货地址、金额明细与支付方式,点击「提交订单」进入支付环节。' }
|
||||
]
|
||||
},
|
||||
'pharmacy-followup': {
|
||||
label: '药店复诊',
|
||||
side: 'patient',
|
||||
sideLabel: '患者端',
|
||||
appLabel: '患者端小程序 · 惠佑康',
|
||||
accent: '#1575fc',
|
||||
intro: '在药店购买处方药?微信扫码进店后先线上复诊:填写用药信息,医生审核开方后凭电子处方购药,合规又省事。',
|
||||
component: PharmacyFollowupFlow,
|
||||
steps: [
|
||||
{ title: '打开微信 · 扫一扫', desc: '在药店咨询台扫码开始:点微信右上角「+」,选择「扫一扫」。' },
|
||||
{ title: '扫码进入药店小程序', desc: '识别台卡小程序码进入线上门店,处方药购买入口就在商品页。' },
|
||||
{ title: '选购处方药', desc: '处方药商品明确标注「处方药」,点击「立即购买」发起线上复诊开方。' },
|
||||
{ title: '填写用药信息', desc: '医生助理逐条确认线下就诊、用药史与确诊疾病,回答完点击选项继续。' },
|
||||
{ title: '提交申请 · 医生审核', desc: '回答完成后转接医生复诊,医生审核用药合理性,通常几分钟内完成。' },
|
||||
{ title: '电子处方已开具', desc: '医生审核通过开具电子处方,凭处方即可继续完成购药下单。' }
|
||||
]
|
||||
},
|
||||
'order-pay': {
|
||||
label: '订单支付',
|
||||
side: 'patient',
|
||||
sideLabel: '患者端',
|
||||
appLabel: '患者端小程序 · 惠佑康',
|
||||
accent: '#1575fc',
|
||||
intro: '处方审核通过后,微信服务通知第一时间提醒:点进订单,倒计时锁定号源与库存,微信支付一步完成。',
|
||||
component: OrderPayFlow,
|
||||
steps: [
|
||||
{ title: '微信 · 收到服务通知', desc: '处方审核通过后,「服务通知」第一时间推送到微信,点击查看。' },
|
||||
{ title: '服务通知 · 支付提醒', desc: '通知卡片写清就诊人、诊断与金额,点击「查看订单详情」直达订单。' },
|
||||
{ title: '订单详情 · 待支付', desc: '订单处于待支付状态,倒计时内完成支付即可锁定名额,点击「去支付」。' },
|
||||
{ title: '收银台 · 确认付款', desc: '核对就诊人、配送方式、药品清单与金额明细,点击「立即支付」。' },
|
||||
{ title: '微信支付', desc: '唤起微信支付面板,确认金额后点击「支付」完成付款。' },
|
||||
{ title: '支付成功', desc: '支付完成,订单进入履约流程,门店备药、物流状态都会同步到小程序。' }
|
||||
]
|
||||
},
|
||||
'doctor-reception': {
|
||||
label: '医生端 · 接诊开方',
|
||||
side: 'doctor',
|
||||
sideLabel: '医生端',
|
||||
appLabel: '医生端小程序 · 萧康互联网医院',
|
||||
accent: '#1575fc',
|
||||
intro: '医生视角的完整闭环:工作台看板掌握待办,在线接诊、问诊沟通、开方流转一台手机全部完成。',
|
||||
component: DoctorReceptionFlow,
|
||||
steps: [
|
||||
{ title: '工作台 · 待办一目了然', desc: '今日开方量、待接诊、处方金额都在工作台首屏,点击「在线接诊」处理新申请。' },
|
||||
{ title: '在线接诊 · 接下患者', desc: '当前接诊列表展示患者摘要与状态,点击待接诊的患者卡片进入会话。' },
|
||||
{ title: '问诊对话 · 了解病情', desc: '患者病情卡片随消息直达,医生确认后点击操作栏的「开方」进入开方页。' },
|
||||
{ title: '在线开方 · 选药定量', desc: '录入临床诊断、添加药品与用法用量,金额实时合计,点击「发送处方」。' },
|
||||
{ title: '处方发送 · 药师审核', desc: '处方进入药师审核流转,审核通过后自动推送给患者,等待支付购药。' }
|
||||
]
|
||||
},
|
||||
'pc-prescription': {
|
||||
label: 'PC 端 · 在线复诊开方',
|
||||
side: 'pc',
|
||||
sideLabel: '诊所 PC 端',
|
||||
appLabel: '诊所管理后台 · 医生接诊工作台(PC)',
|
||||
accent: '#4175ee',
|
||||
frame: 'pc',
|
||||
intro: '患者在小程序提交的复诊申请直达 PC 工作台:接诊、选药、定诊断、发处方一条龙,处方即刻推送患者微信,还能代付收款。',
|
||||
component: PcPrescriptionFlow,
|
||||
steps: [
|
||||
{ title: '在线复诊 · 进入队列', desc: '患者小程序提交的复诊申请直达工作台队列,卡片标出「在线复诊」来源,病情资料已同步,点击「接诊」。' },
|
||||
{ title: '接诊成功 · 进入开方', desc: '接诊后右侧切换为开方工作台,处方还是空的,点击工具栏的「添加商品」为患者选药。' },
|
||||
{ title: '添加商品 · 选药定量', desc: '按名称或拼音检索门店药品,勾选需要的药品并调好数量,底部实时合计,点击「确定」。' },
|
||||
{ title: '完善处方 · 诊断医嘱', desc: '药品进入处方表格,金额自动合计;补全临床诊断与用药医嘱后,点击右下角「发送处方」。' },
|
||||
{ title: '确认发送 · 二次核对', desc: '系统弹出处方摘要再核对一遍患者、诊断、药品与金额,确认无误点击「确认发送」。' },
|
||||
{ title: '发送成功 · 待患者支付', desc: '处方实时推送到患者微信,在线支付后门店按方发药;患者不便操作手机时,点击「代付收款」。' },
|
||||
{ title: '代付收款 · 扫码即付', desc: '生成代支付小程序码,家属或患者扫码直接进入微信支付,订单实时更新为已支付,全程后台留痕。' }
|
||||
]
|
||||
},
|
||||
'pc-reception': {
|
||||
label: 'PC 端 · 线下接诊与代付',
|
||||
side: 'pc',
|
||||
sideLabel: '诊所 PC 端',
|
||||
appLabel: '诊所管理后台 · 医生接诊工作台(PC)',
|
||||
accent: '#4175ee',
|
||||
frame: 'pc',
|
||||
intro: '到店患者的全程都在 PC 工作台:一分钟建档自动挂号,认领码把档案送回患者微信,接诊开方打出门诊处方笺,代付码解决老人不会付款的最后一步。',
|
||||
component: PcReceptionFlow,
|
||||
steps: [
|
||||
{ title: '接诊工作台 · 全景', desc: '左侧是今日就诊队列,右侧是接诊与开方主区。为到店新患者建档,点击「新建就诊人」。' },
|
||||
{ title: '新建就诊人 · 一分钟建档', desc: '录入基础信息与健康史,可勾选免除挂号费;保存后系统自动完成挂号,点击「确定」。' },
|
||||
{ title: '建档成功 · 自动挂号', desc: '新就诊人直接进入候诊队列。档案还没绑定患者微信,点击「二维码认领」生成认领码。' },
|
||||
{ title: '就诊人二维码认领', desc: '选择未认领的就诊人生成小程序码,患者用微信扫一扫,档案与处方记录即归入其账号,点击「关闭」。' },
|
||||
{ title: '线下接诊 · 开始看诊', desc: '患者候诊就绪,病史、过敏史都在档案里。医生点击「接诊」,进入线下开方工作台。' },
|
||||
{ title: '门诊开方 · 处方笺', desc: '门诊普通处方笺上选药定量、录入诊断医嘱,费用实时合计,点击右下角「发送处方」。' },
|
||||
{ title: '处方已发送 · 成功页', desc: '右区切换为发送成功页:订单号、诊断、金额一目了然。患者不便操作手机时,点击「代付收款」。' },
|
||||
{ title: '医生代支付 · 扫码收款', desc: '生成代支付小程序码,家属或患者扫码直接进入微信支付,订单实时更新为已支付。' }
|
||||
]
|
||||
}
|
||||
}
|
||||
1406
src/views/demo/flows/DoctorReceptionFlow.vue
Normal file
1406
src/views/demo/flows/DoctorReceptionFlow.vue
Normal file
File diff suppressed because it is too large
Load Diff
1750
src/views/demo/flows/FullJourneyFlow.vue
Normal file
1750
src/views/demo/flows/FullJourneyFlow.vue
Normal file
File diff suppressed because it is too large
Load Diff
831
src/views/demo/flows/OfflineRegisterFlow.vue
Normal file
831
src/views/demo/flows/OfflineRegisterFlow.vue
Normal file
@@ -0,0 +1,831 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 线下挂号流程模拟(患者端)
|
||||
*
|
||||
* 屏幕顺序:微信首页(扫一扫入口) → 扫码 → 小程序首页 → 医生列表 → 医生详情(选号源) → 确认挂号信息 → 挂号成功
|
||||
* UI 复刻自 xk-client-wx:pages/home/home.vue、pages/Inquiries/Inquiries.vue、
|
||||
* subPackages/doctor/doctor-detail.vue、RegisterConfirmStep、register-success/register-info。
|
||||
* 所有尺寸按 rpx / 2 = px 换算,颜色取源码真实值(主色 #4175EE、强调橙 #FEA54C、金额红 #F44336)。
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import { ChevronDown, ChevronRight, CircleCheck, Search } from 'lucide-vue-next'
|
||||
import PhoneFrame from '../components/PhoneFrame.vue'
|
||||
import WxNavBar from '../components/WxNavBar.vue'
|
||||
import PatientHomeScreen from './shared/PatientHomeScreen.vue'
|
||||
import WeChatHomeScreen from './shared/WeChatHomeScreen.vue'
|
||||
import WxScanScreen from './shared/WxScanScreen.vue'
|
||||
|
||||
const props = defineProps({
|
||||
step: { type: Number, default: 0 },
|
||||
direction: { type: String, default: 'forward' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['advance'])
|
||||
|
||||
/** 屏幕 key:与步骤一一对应,Transition 据此判断是否换屏 */
|
||||
const screenKey = computed(() => ['wx', 'scan', 'home', 'doctors', 'schedule', 'confirm', 'success'][props.step] || 'wx')
|
||||
|
||||
/** 扫一扫黑底、小程序首页蓝渐变头 → 状态栏白字;微信列表与其余浅色页面反之 */
|
||||
const statusTone = computed(() => (props.step === 1 || props.step === 2 ? 'light' : 'dark'))
|
||||
const capsuleTone = computed(() => (props.step === 1 || props.step === 2 ? 'dark' : 'light'))
|
||||
|
||||
/** 医生列表数据(虚构演示) */
|
||||
const doctors = [
|
||||
{
|
||||
name: '李国栋',
|
||||
title: '主治医师',
|
||||
hospital: '萧康中医诊所 · 中医内科',
|
||||
skill: '慢性胃炎、脾胃虚弱、失眠多梦的中医调理',
|
||||
consults: '1286',
|
||||
rate: '98%',
|
||||
score: '4.9',
|
||||
avatarBg: 'linear-gradient(135deg, #93c5fd, #3b82f6)'
|
||||
},
|
||||
{
|
||||
name: '周慧敏',
|
||||
title: '副主任医师',
|
||||
hospital: '萧康中医诊所 · 呼吸内科',
|
||||
skill: '慢性咳嗽、支气管炎、哮喘的诊治',
|
||||
consults: '2043',
|
||||
rate: '99%',
|
||||
score: '5.0',
|
||||
avatarBg: 'linear-gradient(135deg, #f9a8d4, #ec4899)'
|
||||
},
|
||||
{
|
||||
name: '王建平',
|
||||
title: '主任医师',
|
||||
hospital: '萧康中医诊所 · 中医骨伤科',
|
||||
skill: '颈肩腰腿痛、关节炎的针灸推拿治疗',
|
||||
consults: '3157',
|
||||
rate: '97%',
|
||||
score: '4.8',
|
||||
avatarBg: 'linear-gradient(135deg, #a7f3d0, #10b981)'
|
||||
}
|
||||
]
|
||||
|
||||
/** 医生详情页的号源时段(第三个约满,用于展示置灰态) */
|
||||
const slots = [
|
||||
{ time: '明天上午 08:00-12:00', price: '¥15.00', left: 12, total: 30, full: false },
|
||||
{ time: '明天下午 14:00-17:30', price: '¥15.00', left: 3, total: 20, full: false },
|
||||
{ time: '后天上午 08:00-12:00', price: '¥15.00', left: 0, total: 30, full: true }
|
||||
]
|
||||
|
||||
/** 确认页 / 成功页的预约信息行 */
|
||||
const confirmRows = [
|
||||
{ label: '预约诊所', value: '萧康中医诊所' },
|
||||
{ label: '预约科室', value: '中医内科' },
|
||||
{ label: '预约医生', value: '李国栋' },
|
||||
{ label: '预约序号', value: '12号' },
|
||||
{ label: '预计就诊时间', value: '10月18日 上午 08:00-12:00' },
|
||||
{ label: '挂号金额', value: '¥15.00', accent: true },
|
||||
{ label: '挂号类型', value: '普通门诊' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PhoneFrame :status-tone="statusTone" :capsule-tone="capsuleTone" screen-bg="#f8fafc">
|
||||
<Transition :name="`sim-push-${direction}`">
|
||||
<!-- 第 1 屏:微信首页(右上角 + → 扫一扫) -->
|
||||
<WeChatHomeScreen
|
||||
v-if="screenKey === 'wx'"
|
||||
key="wx"
|
||||
mode="scan"
|
||||
@scan="emit('advance', 1)"
|
||||
/>
|
||||
|
||||
<!-- 第 2 屏:扫一扫(对准门店台卡) -->
|
||||
<WxScanScreen
|
||||
v-else-if="screenKey === 'scan'"
|
||||
key="scan"
|
||||
label="萧康中医诊所"
|
||||
sub="扫码进入小程序 · 挂号问诊"
|
||||
@scanned="emit('advance', 2)"
|
||||
/>
|
||||
|
||||
<!-- 第 3 屏:小程序首页(点「到店挂号」) -->
|
||||
<PatientHomeScreen
|
||||
v-else-if="screenKey === 'home'"
|
||||
key="home"
|
||||
hotspot="register"
|
||||
@tap-register="emit('advance', 3)"
|
||||
/>
|
||||
|
||||
<!-- 第 4 屏:医生列表 -->
|
||||
<div v-else-if="screenKey === 'doctors'" key="doctors" class="sim-screen doctors-screen">
|
||||
<WxNavBar title="医生列表" @back="emit('advance', 2)" />
|
||||
<div class="sim-screen-body">
|
||||
<div class="inq-search">
|
||||
<div class="inq-search-input">
|
||||
<Search :size="15" aria-hidden="true" />
|
||||
<span>请输入关键字</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inq-filter-bar">
|
||||
<span>科室 <ChevronDown :size="13" aria-hidden="true" /></span>
|
||||
<span>排序 <ChevronDown :size="13" aria-hidden="true" /></span>
|
||||
<span>筛选 <ChevronDown :size="13" aria-hidden="true" /></span>
|
||||
</div>
|
||||
<p class="inq-title">选择坐诊医生</p>
|
||||
<div
|
||||
v-for="(doctor, index) in doctors"
|
||||
:key="doctor.name"
|
||||
class="inq-card"
|
||||
>
|
||||
<div class="inq-card-top">
|
||||
<span class="inq-avatar" :style="{ background: doctor.avatarBg }">{{ doctor.name.slice(0, 1) }}</span>
|
||||
<div class="inq-info">
|
||||
<div class="inq-name-row">
|
||||
<strong>{{ doctor.name }}</strong>
|
||||
<span>{{ doctor.title }}</span>
|
||||
</div>
|
||||
<p>{{ doctor.hospital }}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="inq-cta"
|
||||
:class="{ 'sim-hotspot': index === 0 }"
|
||||
@click="index === 0 && emit('advance', 4)"
|
||||
>挂号</button>
|
||||
</div>
|
||||
<p class="inq-skill">擅长:{{ doctor.skill }}</p>
|
||||
<div class="inq-stats">
|
||||
<span>问诊量 <i>{{ doctor.consults }}</i></span>
|
||||
<span>接诊率 <i>{{ doctor.rate }}</i></span>
|
||||
<span>综合评分 <i>{{ doctor.score }}</i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第 3 屏:医生详情 · 选号源 -->
|
||||
<div v-else-if="screenKey === 'schedule'" key="schedule" class="sim-screen schedule-screen">
|
||||
<WxNavBar title="医生详情" @back="emit('advance', 3)" />
|
||||
<div class="sim-screen-body">
|
||||
<div class="detail-card">
|
||||
<div class="detail-head">
|
||||
<div class="detail-head-copy">
|
||||
<strong>李国栋</strong>
|
||||
<p>主治医师 · 中医内科</p>
|
||||
<p>萧康中医诊所</p>
|
||||
</div>
|
||||
<span class="detail-avatar">李</span>
|
||||
</div>
|
||||
<div class="detail-stats">
|
||||
<span>问诊量 <i>1286</i></span>
|
||||
<span>接诊率 <i>98%</i></span>
|
||||
<span>综合评分 <i>4.9</i></span>
|
||||
</div>
|
||||
<p class="detail-skill">专业擅长:慢性胃炎、脾胃虚弱、失眠多梦、体虚乏力的中医辨证调理。</p>
|
||||
<span class="detail-more">查看全部 <ChevronRight :size="13" aria-hidden="true" /></span>
|
||||
</div>
|
||||
|
||||
<p class="appoin-title">预约挂号</p>
|
||||
<div class="appoin-list">
|
||||
<div v-for="(slot, index) in slots" :key="slot.time" class="appoin-row">
|
||||
<div class="appoin-row-main">
|
||||
<div class="appoin-copy">
|
||||
<span class="appoin-time">{{ slot.time }}</span>
|
||||
<span class="appoin-price">{{ slot.price }}</span>
|
||||
</div>
|
||||
<button
|
||||
v-if="!slot.full"
|
||||
type="button"
|
||||
class="appoin-btn"
|
||||
:class="{ 'sim-hotspot': index === 0 }"
|
||||
@click="index === 0 && emit('advance', 5)"
|
||||
>挂号</button>
|
||||
<button v-else type="button" class="appoin-btn appoin-btn-full">约满</button>
|
||||
</div>
|
||||
<p class="appoin-rest">
|
||||
剩余号 <i>{{ slot.left }}</i> <span>总号数 {{ slot.total }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第 4 屏:确认挂号信息 -->
|
||||
<div v-else-if="screenKey === 'confirm'" key="confirm" class="sim-screen confirm-screen">
|
||||
<WxNavBar title="选择就诊人" @back="emit('advance', 4)" />
|
||||
<div class="sim-screen-body confirm-body">
|
||||
<div class="step-bar">
|
||||
<div class="step-node step-node-done">
|
||||
<span class="step-circle">1</span>
|
||||
<span class="step-label">选择就诊人</span>
|
||||
</div>
|
||||
<span class="step-line"></span>
|
||||
<div class="step-node step-node-active">
|
||||
<span class="step-circle">2</span>
|
||||
<span class="step-label">确认预约信息,支付挂号费</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="block-title">预约信息</p>
|
||||
<div class="info-card">
|
||||
<div v-for="row in confirmRows" :key="row.label" class="info-row">
|
||||
<span class="info-label">{{ row.label }}</span>
|
||||
<span class="info-value" :class="{ 'info-value-accent': row.accent }">{{ row.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="block-title">就诊人</p>
|
||||
<div class="info-card">
|
||||
<div class="info-row">
|
||||
<span class="info-patient">张淑芬</span>
|
||||
<span class="info-plate">12号</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">身份证号</span>
|
||||
<span class="info-value">330***********1234</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">手机号</span>
|
||||
<span class="info-value">138****6688</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="confirm-footer">
|
||||
<label class="agree-row">
|
||||
<span class="agree-check"><CircleCheck :size="17" aria-hidden="true" /></span>
|
||||
我已阅读并同意<i>《预约挂号须知》</i>
|
||||
</label>
|
||||
<div class="confirm-actions">
|
||||
<button type="button" class="btn-prev" @click="emit('advance', 4)">上一步</button>
|
||||
<button type="button" class="btn-confirm sim-hotspot" @click="emit('advance', 6)">确认挂号</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第 5 屏:挂号成功(成功卡 + 挂号详情状态头) -->
|
||||
<div v-else key="success" class="sim-screen success-screen">
|
||||
<WxNavBar title="挂号详情" @back="emit('advance', 5)" />
|
||||
<div class="sim-screen-body confirm-body">
|
||||
<div class="success-card">
|
||||
<CircleCheck :size="60" :stroke-width="1.6" class="success-icon" aria-hidden="true" />
|
||||
<strong>挂号成功</strong>
|
||||
</div>
|
||||
|
||||
<div class="state-bar">
|
||||
<span class="state-left">
|
||||
<CircleCheck :size="18" aria-hidden="true" />
|
||||
已挂号成功
|
||||
</span>
|
||||
<span class="state-price">¥15.00</span>
|
||||
</div>
|
||||
|
||||
<p class="block-title">预约信息</p>
|
||||
<div class="info-card">
|
||||
<div v-for="row in confirmRows.slice(0, 5)" :key="row.label" class="info-row">
|
||||
<span class="info-label">{{ row.label }}</span>
|
||||
<span class="info-value">{{ row.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn-home">返回首页</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</PhoneFrame>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ============ 第 2 屏:医生列表 ============ */
|
||||
.doctors-screen {
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
.inq-search {
|
||||
padding: 8px 15px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.inq-search-input {
|
||||
height: 33px;
|
||||
border-radius: 999px;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 0 14px;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.inq-filter-bar {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
padding: 9px 0;
|
||||
}
|
||||
|
||||
.inq-filter-bar span {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.inq-title {
|
||||
margin: 15px 20px 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
/* 医生卡:顶部淡蓝渐变 */
|
||||
.inq-card {
|
||||
margin: 0 10px 12px;
|
||||
border-radius: 8px;
|
||||
padding: 11px 16px;
|
||||
background: linear-gradient(180deg, #e2f1ff 0%, #fbfdff 24%, #ffffff 100%);
|
||||
box-shadow: 0 0 9px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.inq-card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.inq-avatar {
|
||||
flex-shrink: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.inq-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.inq-name-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.inq-name-row strong {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.inq-name-row span {
|
||||
font-size: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.inq-info p {
|
||||
margin: 3px 0 0;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.inq-cta {
|
||||
flex-shrink: 0;
|
||||
width: 52px;
|
||||
height: 25px;
|
||||
border: none;
|
||||
border-radius: 13px;
|
||||
background: #4175ee;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.inq-skill {
|
||||
margin: 9px 0 0;
|
||||
font-size: 12px;
|
||||
color: #232323;
|
||||
line-height: 1.55;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.inq-stats {
|
||||
margin-top: 9px;
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.inq-stats i {
|
||||
font-style: normal;
|
||||
color: #fea54c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ============ 第 3 屏:医生详情 ============ */
|
||||
.schedule-screen {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
margin: 10px 10px 0;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.detail-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.detail-head-copy strong {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.detail-head-copy p {
|
||||
margin: 6px 0 0;
|
||||
font-size: 14px;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.detail-avatar {
|
||||
flex-shrink: 0;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #93c5fd, #3b82f6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.detail-stats {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.detail-stats i {
|
||||
font-style: normal;
|
||||
font-size: 14px;
|
||||
color: #fea54c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-skill {
|
||||
margin: 12px 0 0;
|
||||
font-size: 14px;
|
||||
color: #1e293b;
|
||||
line-height: 1.6;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-more {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 2px;
|
||||
font-size: 14px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.appoin-title {
|
||||
margin: 18px 20px 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.appoin-list {
|
||||
margin: 0 10px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 12px 10px;
|
||||
}
|
||||
|
||||
.appoin-row {
|
||||
padding: 8px 6px 10px;
|
||||
}
|
||||
|
||||
.appoin-row + .appoin-row {
|
||||
border-top: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.appoin-row-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.appoin-copy {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.appoin-time {
|
||||
font-size: 13.5px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.appoin-price {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #fea54c;
|
||||
}
|
||||
|
||||
.appoin-btn {
|
||||
width: 52px;
|
||||
height: 21px;
|
||||
border: none;
|
||||
border-radius: 13px;
|
||||
background: #4175ee;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.appoin-btn-full {
|
||||
background: #e0e1e3;
|
||||
color: #232323;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.appoin-rest {
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.appoin-rest i {
|
||||
font-style: normal;
|
||||
color: #4175ee;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.appoin-rest span {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
/* ============ 第 4 屏:确认挂号 ============ */
|
||||
.confirm-screen,
|
||||
.success-screen {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.confirm-body {
|
||||
padding: 16px 0 120px;
|
||||
}
|
||||
|
||||
/* 步骤条 */
|
||||
.step-bar {
|
||||
margin: 0 16px 18px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.step-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: #e2e8f0;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.step-node-done .step-circle,
|
||||
.step-node-active .step-circle {
|
||||
background: #4175ee;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
margin-top: 11px;
|
||||
border-radius: 1px;
|
||||
background: #4175ee;
|
||||
}
|
||||
|
||||
.block-title {
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
margin: 0 10px 6px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #232323;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.info-value-accent {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.info-patient {
|
||||
font-size: 14px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.info-plate {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
/* 底部固定栏 */
|
||||
.confirm-footer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
padding: 10px 16px 26px;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.agree-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.agree-check {
|
||||
color: #2979ff;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.agree-row i {
|
||||
font-style: normal;
|
||||
color: #4175ee;
|
||||
}
|
||||
|
||||
.confirm-actions {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-prev {
|
||||
width: 90px;
|
||||
height: 43px;
|
||||
border-radius: 32px;
|
||||
border: 1px solid #4175ee;
|
||||
background: #fff;
|
||||
color: #4175ee;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
flex: 1;
|
||||
height: 43px;
|
||||
border: none;
|
||||
border-radius: 32px;
|
||||
background: #4175ee;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ============ 第 5 屏:挂号成功 ============ */
|
||||
.success-card {
|
||||
margin: 5px 10px 12px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
color: #4175ee;
|
||||
}
|
||||
|
||||
.success-card strong {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.state-bar {
|
||||
margin: 0 10px 6px;
|
||||
height: 54px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.state-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.state-left svg {
|
||||
color: #2979ff;
|
||||
}
|
||||
|
||||
.state-price {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.btn-home {
|
||||
display: block;
|
||||
width: 343px;
|
||||
height: 43px;
|
||||
margin: 22px auto 0;
|
||||
border: none;
|
||||
border-radius: 99px;
|
||||
background: #4175ee;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
900
src/views/demo/flows/OnlineFollowupFlow.vue
Normal file
900
src/views/demo/flows/OnlineFollowupFlow.vue
Normal file
@@ -0,0 +1,900 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 在线复诊流程模拟(患者端)
|
||||
*
|
||||
* 屏幕顺序:微信首页(扫一扫) → 扫码 → 小程序首页 → 选择就诊人 → 填写复诊单 →
|
||||
* 等待接诊(服务通知横幅) → 医生问诊(患者回复) → 医生开方(电子处方卡)
|
||||
* UI 复刻自 xk-client-wx:PatientStep/RegisterPatientStep(就诊人卡片网格、主诉表单)、
|
||||
* subPackages/chat/chat.vue + MessageBubble(聊天页 #F4F6F9 底、患者气泡 #0A84FF、医生白泡、电子处方卡)。
|
||||
*
|
||||
* 问诊与开方拆成两步:第 7 步医生提问、患者点「发送」回复;第 8 步医生结论 + 处方卡追加进会话,
|
||||
* 两步共用聊天屏(key 相同不触发整屏推入),消息像真实会话一样增量出现。
|
||||
*/
|
||||
import { computed, ref } from 'vue'
|
||||
import { CircleCheck, FileText, Mic, Plus, Stethoscope } from 'lucide-vue-next'
|
||||
import PhoneFrame from '../components/PhoneFrame.vue'
|
||||
import WxNavBar from '../components/WxNavBar.vue'
|
||||
import PatientHomeScreen from './shared/PatientHomeScreen.vue'
|
||||
import WeChatHomeScreen from './shared/WeChatHomeScreen.vue'
|
||||
import WxScanScreen from './shared/WxScanScreen.vue'
|
||||
|
||||
const props = defineProps({
|
||||
step: { type: Number, default: 0 },
|
||||
direction: { type: String, default: 'forward' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['advance'])
|
||||
|
||||
const screenKey = computed(() => ['wx', 'scan', 'home', 'patient', 'form', 'waiting', 'chat', 'chat'][props.step] || 'wx')
|
||||
const statusTone = computed(() => (props.step === 1 || props.step === 2 ? 'light' : 'dark'))
|
||||
const capsuleTone = computed(() => (props.step === 1 || props.step === 2 ? 'dark' : 'light'))
|
||||
|
||||
/** 就诊人列表(虚构演示),selected 支持点击切换,还原真实选择交互 */
|
||||
const patients = [
|
||||
{ name: '张淑芬', meta: '女 · 64岁' },
|
||||
{ name: '王小虎', meta: '男 · 8岁' },
|
||||
{ name: '李建国', meta: '男 · 66岁' }
|
||||
]
|
||||
const selectedPatient = ref(0)
|
||||
|
||||
/** 相关西药 chips:选中第一个 */
|
||||
const drugs = ref([
|
||||
{ name: '蓝芩口服液', on: true },
|
||||
{ name: '香砂养胃丸', on: true },
|
||||
{ name: '奥美拉唑肠溶胶囊', on: false }
|
||||
])
|
||||
|
||||
function toggleDrug(index) {
|
||||
drugs.value[index].on = !drugs.value[index].on
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PhoneFrame :status-tone="statusTone" :capsule-tone="capsuleTone" screen-bg="#f8fafc">
|
||||
<Transition :name="`sim-push-${direction}`">
|
||||
<!-- 第 1 屏:微信首页(右上角 + → 扫一扫) -->
|
||||
<WeChatHomeScreen
|
||||
v-if="screenKey === 'wx'"
|
||||
key="wx"
|
||||
mode="scan"
|
||||
@scan="emit('advance', 1)"
|
||||
/>
|
||||
|
||||
<!-- 第 2 屏:扫一扫 -->
|
||||
<WxScanScreen
|
||||
v-else-if="screenKey === 'scan'"
|
||||
key="scan"
|
||||
label="萧康中医诊所"
|
||||
sub="扫码进入小程序 · 在线复诊"
|
||||
@scanned="emit('advance', 2)"
|
||||
/>
|
||||
|
||||
<!-- 第 3 屏:小程序首页(点「在线复诊」) -->
|
||||
<PatientHomeScreen
|
||||
v-else-if="screenKey === 'home'"
|
||||
key="home"
|
||||
hotspot="followup"
|
||||
@tap-followup="emit('advance', 3)"
|
||||
/>
|
||||
|
||||
<!-- 第 4 屏:选择就诊人 -->
|
||||
<div v-else-if="screenKey === 'patient'" key="patient" class="sim-screen patient-screen">
|
||||
<WxNavBar title="选择就诊人" @back="emit('advance', 2)" />
|
||||
<div class="sim-screen-body patient-body">
|
||||
<div class="step-bar">
|
||||
<div class="step-node step-node-active">
|
||||
<span class="step-circle">1</span>
|
||||
<span class="step-label">选择就诊人</span>
|
||||
</div>
|
||||
<span class="step-line step-line-dim"></span>
|
||||
<div class="step-node">
|
||||
<span class="step-circle">2</span>
|
||||
<span class="step-label">确认复诊信息</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ask-head">
|
||||
<div class="ask-title-row">
|
||||
<strong>您需要为谁复诊</strong>
|
||||
<span class="ask-type-tag">在线复诊</span>
|
||||
</div>
|
||||
<p>请选择患者信息,信息仅用于医生诊断参考</p>
|
||||
</div>
|
||||
|
||||
<div class="patient-grid">
|
||||
<button
|
||||
v-for="(patient, index) in patients"
|
||||
:key="patient.name"
|
||||
type="button"
|
||||
class="patient-card"
|
||||
:class="{ 'patient-card-on': index === selectedPatient }"
|
||||
@click="selectedPatient = index"
|
||||
>
|
||||
<strong>{{ patient.name }}</strong>
|
||||
<span>{{ patient.meta }}</span>
|
||||
<i v-if="index === selectedPatient" class="patient-check"><CircleCheck :size="15" aria-hidden="true" /></i>
|
||||
</button>
|
||||
<button type="button" class="patient-card patient-card-add">
|
||||
<Plus :size="26" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-footer">
|
||||
<button type="button" class="btn-next sim-hotspot" @click="emit('advance', 4)">下一步</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第 5 屏:填写复诊单 -->
|
||||
<div v-else-if="screenKey === 'form'" key="form" class="sim-screen patient-screen">
|
||||
<WxNavBar title="选择就诊人" @back="emit('advance', 3)" />
|
||||
<div class="sim-screen-body patient-body form-body">
|
||||
<div class="ask-head">
|
||||
<div class="ask-title-row">
|
||||
<strong>填写病情资料</strong>
|
||||
<span class="ask-type-tag">在线复诊</span>
|
||||
</div>
|
||||
<p>为 张淑芬(女 · 64岁)复诊,请如实描述当前情况</p>
|
||||
</div>
|
||||
|
||||
<div class="form-card">
|
||||
<p class="form-label"><i>*</i>患者主诉</p>
|
||||
<div class="form-textarea">
|
||||
胃部隐痛复发,饭后胀气明显,持续约一周;此前已在贵院确诊慢性胃炎,希望医生评估是否续用上次药方。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-card">
|
||||
<p class="form-label"><i>*</i>选择相关西药</p>
|
||||
<div class="drug-chips">
|
||||
<button
|
||||
v-for="(drug, index) in drugs"
|
||||
:key="drug.name"
|
||||
type="button"
|
||||
class="drug-chip"
|
||||
:class="{ 'drug-chip-on': drug.on }"
|
||||
@click="toggleDrug(index)"
|
||||
>{{ drug.name }}</button>
|
||||
</div>
|
||||
<p class="form-hint">曾用药信息帮助医生判断续方安全性</p>
|
||||
</div>
|
||||
|
||||
<div class="form-card">
|
||||
<p class="form-label">健康信息</p>
|
||||
<div class="radio-row">
|
||||
<span>肝功能异常</span>
|
||||
<span class="radio-group"><i class="radio-on"></i>无 <i class="radio-off"></i>有</span>
|
||||
</div>
|
||||
<div class="radio-row">
|
||||
<span>药物过敏史</span>
|
||||
<span class="radio-group"><i class="radio-on"></i>无 <i class="radio-off"></i>有</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-footer">
|
||||
<button type="button" class="btn-next sim-hotspot" @click="emit('advance', 5)">下一步</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第 6 屏:等待接诊(聊天页 + 服务通知横幅) -->
|
||||
<div v-else-if="screenKey === 'waiting'" key="waiting" class="sim-screen chat-screen">
|
||||
<WxNavBar title="在线问诊" bg="#ffffff" color="#333333" @back="emit('advance', 4)" />
|
||||
<div class="chat-doctor-bar">
|
||||
<div class="chat-doctor-info">
|
||||
<strong>李国栋</strong>
|
||||
<span class="chat-online"><i></i>在线</span>
|
||||
</div>
|
||||
<span class="chat-detail-pill">详情</span>
|
||||
</div>
|
||||
<div class="sim-screen-body chat-body">
|
||||
<div class="chat-notice">
|
||||
发起问诊视为同意<i>《互联网诊疗风险告知及知情同意书》</i>
|
||||
</div>
|
||||
<div class="chat-time">09:41</div>
|
||||
<div class="chat-system">复诊申请已提交,等待医生接诊</div>
|
||||
|
||||
<div class="msg-row msg-row-mine">
|
||||
<div class="msg-bubble msg-bubble-mine">
|
||||
胃部隐痛复发,饭后胀气明显,持续约一周;此前已确诊慢性胃炎,希望评估续方。
|
||||
</div>
|
||||
<span class="msg-avatar msg-avatar-patient">张</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-footer chat-footer-disabled">医生接诊后即可发送消息</div>
|
||||
|
||||
<!-- 服务通知横幅:微信推送样式,点击进入会话 -->
|
||||
<button type="button" class="push-banner sim-banner-drop sim-hotspot" @click="emit('advance', 6)">
|
||||
<span class="push-icon"><Stethoscope :size="18" aria-hidden="true" /></span>
|
||||
<span class="push-copy">
|
||||
<strong>服务通知</strong>
|
||||
<small>李国栋医生已接诊,点击进入会话</small>
|
||||
</span>
|
||||
<span class="push-go">查看</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 第 7/8 屏:医生问诊 → 开方(同一聊天屏,消息随步骤增量出现) -->
|
||||
<div v-else key="chat" class="sim-screen chat-screen">
|
||||
<WxNavBar title="李国栋" bg="#ffffff" color="#333333" @back="emit('advance', 5)" />
|
||||
<div class="chat-doctor-bar">
|
||||
<div class="chat-doctor-info">
|
||||
<strong>李国栋</strong>
|
||||
<span class="chat-online"><i></i>在线</span>
|
||||
</div>
|
||||
<span class="chat-detail-pill">详情</span>
|
||||
</div>
|
||||
<div class="sim-screen-body chat-body">
|
||||
<div class="chat-time">09:47</div>
|
||||
<div class="chat-system">医生已接诊</div>
|
||||
|
||||
<div class="msg-row">
|
||||
<span class="msg-avatar msg-avatar-doctor">李</span>
|
||||
<div class="msg-bubble msg-bubble-other">
|
||||
您好,我是李国栋医生。看到您的复诊资料了,最近饮食是否规律?夜间是否有反酸?
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 患者回复与后续开方:第 8 步才出现 -->
|
||||
<template v-if="step >= 7">
|
||||
<div class="msg-row msg-row-mine sim-msg-in">
|
||||
<div class="msg-bubble msg-bubble-mine">
|
||||
饮食还算规律,偶尔夜里有点反酸,其他和上次差不多。
|
||||
</div>
|
||||
<span class="msg-avatar msg-avatar-patient">张</span>
|
||||
</div>
|
||||
|
||||
<div class="msg-row sim-msg-in">
|
||||
<span class="msg-avatar msg-avatar-doctor">李</span>
|
||||
<div class="msg-bubble msg-bubble-other">
|
||||
情况稳定,可以按原方案续用。我为您开具电子处方,注意饭后半小时服药。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="msg-row sim-msg-in">
|
||||
<span class="msg-avatar msg-avatar-doctor">李</span>
|
||||
<div class="rx-card">
|
||||
<div class="rx-head">
|
||||
<FileText :size="16" aria-hidden="true" />
|
||||
<span>电子处方</span>
|
||||
</div>
|
||||
<div class="rx-body">
|
||||
<p class="rx-line"><span>诊断</span>慢性胃炎(复诊续方)</p>
|
||||
<p class="rx-line"><span>药品</span>蓝芩口服液 ×2 · 香砂养胃丸 ×1</p>
|
||||
<p class="rx-price">合计 <i>¥86.00</i></p>
|
||||
</div>
|
||||
<button type="button" class="rx-btn">查看详情</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-system">处方已通过药师审核,可前往支付</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 输入栏:问诊步预填草稿 + 发送即回复;开方后恢复占位态 -->
|
||||
<div class="chat-footer">
|
||||
<span class="chat-mic"><Mic :size="20" aria-hidden="true" /></span>
|
||||
<template v-if="step === 6">
|
||||
<span class="chat-input chat-input-draft">饮食还算规律,偶尔夜里有点反酸…</span>
|
||||
<button type="button" class="chat-send sim-hotspot" @click="emit('advance', 7)">发送</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="chat-input">输入消息...</span>
|
||||
<span class="chat-plus"><Plus :size="20" aria-hidden="true" /></span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</PhoneFrame>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ============ 选择就诊人 / 复诊单 ============ */
|
||||
.patient-screen {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.patient-body {
|
||||
padding: 16px 16px 110px;
|
||||
}
|
||||
|
||||
.step-bar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.step-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
max-width: 140px;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: #e2e8f0;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.step-node-active .step-circle {
|
||||
background: #4175ee;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
margin-top: 11px;
|
||||
background: #4175ee;
|
||||
}
|
||||
|
||||
.step-line-dim {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.ask-head {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ask-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ask-title-row strong {
|
||||
font-size: 19px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.ask-type-tag {
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(135deg, #4175ee, #298dff);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ask-head p {
|
||||
margin: 8px 0 0;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* 就诊人网格卡 */
|
||||
.patient-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.patient-card {
|
||||
position: relative;
|
||||
height: 62px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 0 10px;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.patient-card strong {
|
||||
font-size: 16px;
|
||||
color: #0f172a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.patient-card span {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.patient-card-on {
|
||||
border-color: #536dfe;
|
||||
background: rgba(83, 109, 254, 0.04);
|
||||
}
|
||||
|
||||
.patient-check {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
color: #536dfe;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.patient-card-add {
|
||||
align-items: center;
|
||||
border-style: dashed;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* 复诊表单 */
|
||||
.form-body .ask-head {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.03);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
margin: 0 0 10px;
|
||||
font-size: 14.5px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.form-label i {
|
||||
color: #f44336;
|
||||
font-style: normal;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
background: #f8fafc;
|
||||
border-radius: 6px;
|
||||
padding: 11px;
|
||||
font-size: 13.5px;
|
||||
line-height: 1.7;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.drug-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.drug-chip {
|
||||
padding: 7px 13px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.drug-chip-on {
|
||||
border-color: #4175ee;
|
||||
background: rgba(65, 117, 238, 0.08);
|
||||
color: #4175ee;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
margin: 10px 0 0;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
background: #f1f5f9;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.radio-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.radio-group i {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.radio-on {
|
||||
border: 4.5px solid #2979ff;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.radio-off {
|
||||
border: 1.5px solid #cbd5e1;
|
||||
background: #fff;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
/* 底部按钮 */
|
||||
.form-footer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 12px 16px 28px;
|
||||
background: linear-gradient(180deg, rgba(248, 250, 252, 0), #f8fafc 36%);
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.btn-next {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 45px;
|
||||
border: none;
|
||||
border-radius: 99px;
|
||||
background: linear-gradient(135deg, #4175ee, #298dff);
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 8px 18px rgba(41, 141, 255, 0.32);
|
||||
}
|
||||
|
||||
/* ============ 聊天页 ============ */
|
||||
.chat-screen {
|
||||
background: #f4f6f9;
|
||||
}
|
||||
|
||||
.chat-doctor-bar {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 9px 15px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(6px);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.chat-doctor-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.chat-doctor-info strong {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.chat-online {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.chat-online i {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #52c41a;
|
||||
}
|
||||
|
||||
.chat-detail-pill {
|
||||
padding: 4px 11px;
|
||||
border-radius: 999px;
|
||||
background: #f0f5ff;
|
||||
color: #0a84ff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chat-body {
|
||||
padding: 12px 12px 20px;
|
||||
}
|
||||
|
||||
.chat-notice {
|
||||
padding: 9px 12px;
|
||||
border-radius: 6px;
|
||||
background: #f5f5f5;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.chat-notice i {
|
||||
font-style: normal;
|
||||
color: #0a84ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.chat-time {
|
||||
margin: 12px auto 0;
|
||||
width: fit-content;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
font-size: 11px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.chat-system {
|
||||
margin: 12px auto 0;
|
||||
width: fit-content;
|
||||
padding: 4px 12px;
|
||||
border-radius: 5px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 消息行与气泡 */
|
||||
.msg-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.msg-row-mine {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.msg-avatar {
|
||||
flex-shrink: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.msg-avatar-doctor {
|
||||
background: linear-gradient(135deg, #93c5fd, #3b82f6);
|
||||
}
|
||||
|
||||
.msg-avatar-patient {
|
||||
background: linear-gradient(135deg, #fcd34d, #f59e0b);
|
||||
}
|
||||
|
||||
.msg-bubble {
|
||||
max-width: 72%;
|
||||
padding: 9px 12px;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.msg-bubble-mine {
|
||||
background: #0a84ff;
|
||||
color: #fff;
|
||||
border-radius: 6px 2px 6px 6px;
|
||||
}
|
||||
|
||||
.msg-bubble-other {
|
||||
background: #fff;
|
||||
color: #333;
|
||||
border-radius: 2px 6px 6px 6px;
|
||||
}
|
||||
|
||||
/* 电子处方卡片消息 */
|
||||
.rx-card {
|
||||
width: 240px;
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.rx-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 9px 12px;
|
||||
background: #eff6ff;
|
||||
color: #0a84ff;
|
||||
font-size: 13.5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rx-body {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.rx-line {
|
||||
margin: 0 0 6px;
|
||||
font-size: 12.5px;
|
||||
color: #333;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.rx-line span {
|
||||
color: #999;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.rx-price {
|
||||
margin: 8px 0 0;
|
||||
font-size: 12.5px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.rx-price i {
|
||||
font-style: normal;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.rx-btn {
|
||||
display: block;
|
||||
width: calc(100% - 24px);
|
||||
margin: 4px 12px 12px;
|
||||
height: 30px;
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
background: #0a84ff;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 底部输入栏 */
|
||||
.chat-footer {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px 24px;
|
||||
background: #f7f7f7;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.chat-footer-disabled {
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
.chat-mic,
|
||||
.chat-plus {
|
||||
display: flex;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
height: 36px;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
color: #b9bec6;
|
||||
}
|
||||
|
||||
/* 问诊步:输入框里是待发送的草稿,颜色用正文色 */
|
||||
.chat-input-draft {
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.chat-send {
|
||||
flex-shrink: 0;
|
||||
height: 34px;
|
||||
padding: 0 14px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: #07c160;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 新消息进入:轻微上浮淡入,模拟真实会话的增量感 */
|
||||
.sim-msg-in {
|
||||
animation: msg-in 0.4s ease-out both;
|
||||
}
|
||||
|
||||
@keyframes msg-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.sim-msg-in {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 服务通知横幅 */
|
||||
.push-banner {
|
||||
position: absolute;
|
||||
top: 58px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
z-index: 7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 11px 13px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.97);
|
||||
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.22);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.push-icon {
|
||||
flex-shrink: 0;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 9px;
|
||||
background: linear-gradient(135deg, #4175ee, #298dff);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.push-copy {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.push-copy strong {
|
||||
font-size: 13px;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.push-copy small {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.push-go {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #0a84ff;
|
||||
}
|
||||
</style>
|
||||
916
src/views/demo/flows/OrderPayFlow.vue
Normal file
916
src/views/demo/flows/OrderPayFlow.vue
Normal file
@@ -0,0 +1,916 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 订单支付流程模拟(患者端)
|
||||
*
|
||||
* 屏幕顺序:微信首页(点服务通知) → 服务通知(处方审核通过) → 待支付订单(倒计时) →
|
||||
* 收银台(处方支付) → 微信支付面板(底部弹出) → 支付成功
|
||||
* UI 复刻自 xk-client-wx:my/unpaid-order.vue(待支付卡、倒计时 #F44336、去支付 #536DFE)、
|
||||
* my/record-pay.vue(就诊人标签胶囊 #536DFE、配送方式、金额明细、立即支付)、shop/paied.vue(付款成功卡)。
|
||||
* 微信支付面板按微信收银台通用形态还原(绿色确认钮 #07C160)。
|
||||
*
|
||||
* 为什么收银台与支付面板共用一个屏幕 key:支付面板是「盖在收银台上」的底部弹层,
|
||||
* 不是新页面,共用 key 可以避免整屏推入动画,只让面板自己滑出,和真机体验一致。
|
||||
*/
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { Bell, ChevronRight, CircleCheck, X } from 'lucide-vue-next'
|
||||
import PhoneFrame from '../components/PhoneFrame.vue'
|
||||
import WxNavBar from '../components/WxNavBar.vue'
|
||||
import WeChatHomeScreen from './shared/WeChatHomeScreen.vue'
|
||||
|
||||
const props = defineProps({
|
||||
step: { type: Number, default: 0 },
|
||||
direction: { type: String, default: 'forward' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['advance'])
|
||||
|
||||
/** 步骤 → 屏幕:微信入口两屏在前;收银台与支付面板共用(面板是覆盖层) */
|
||||
const screenKey = computed(() => {
|
||||
if (props.step === 0) return 'wx'
|
||||
if (props.step === 1) return 'notice'
|
||||
if (props.step === 2) return 'order'
|
||||
if (props.step <= 4) return 'cashier'
|
||||
return 'done'
|
||||
})
|
||||
|
||||
/** 待支付倒计时:从 14:59 实时递减,还原 u-count-down 的紧迫感 */
|
||||
const countdown = ref(899)
|
||||
let timer = 0
|
||||
|
||||
const countdownText = computed(() => {
|
||||
const minutes = String(Math.floor(countdown.value / 60)).padStart(2, '0')
|
||||
const seconds = String(countdown.value % 60).padStart(2, '0')
|
||||
return { minutes, seconds }
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
timer = window.setInterval(() => {
|
||||
if (countdown.value > 0) countdown.value -= 1
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) window.clearInterval(timer)
|
||||
})
|
||||
|
||||
/** 收银台药品清单(虚构演示) */
|
||||
const drugs = [
|
||||
{ name: '阿莫西林胶囊 0.25g×24粒', price: '¥12.80', count: 'x2', usage: '口服,一次2粒,一日3次' },
|
||||
{ name: '蒲地蓝消炎口服液 10ml×12支', price: '¥24.30', count: 'x3', usage: '口服,一次1支,一日3次' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PhoneFrame status-tone="dark" capsule-tone="light" screen-bg="#f8fafc">
|
||||
<Transition :name="`sim-push-${direction}`">
|
||||
<!-- 第 1 屏:微信首页(点「服务通知」) -->
|
||||
<WeChatHomeScreen
|
||||
v-if="screenKey === 'wx'"
|
||||
key="wx"
|
||||
mode="notice"
|
||||
notice-digest="惠佑康:处方审核通过,请及时完成支付"
|
||||
@notice="emit('advance', 1)"
|
||||
/>
|
||||
|
||||
<!-- 第 2 屏:服务通知会话(处方审核通过卡片) -->
|
||||
<div v-else-if="screenKey === 'notice'" key="notice" class="sim-screen notice-screen">
|
||||
<header class="notice-navbar">
|
||||
<button type="button" class="notice-back" aria-label="返回" @click="emit('advance', 0)">‹</button>
|
||||
服务通知
|
||||
</header>
|
||||
<div class="sim-screen-body notice-body">
|
||||
<div class="notice-time">10:21</div>
|
||||
<button type="button" class="notice-card sim-hotspot" @click="emit('advance', 2)">
|
||||
<span class="notice-app">
|
||||
<i class="notice-app-icon"><Bell :size="12" aria-hidden="true" /></i>
|
||||
惠佑康健康服务
|
||||
</span>
|
||||
<strong class="notice-title">处方审核通过通知</strong>
|
||||
<span class="notice-row"><i>就诊人</i>张淑芬</span>
|
||||
<span class="notice-row"><i>诊断</i>慢性胃炎(复诊续方)</span>
|
||||
<span class="notice-row"><i>应付金额</i><b>¥128.50</b></span>
|
||||
<span class="notice-row"><i>温馨提示</i>请在 15 分钟内完成支付,超时订单自动取消</span>
|
||||
<span class="notice-foot">查看订单详情<ChevronRight :size="14" aria-hidden="true" /></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第 3 屏:待支付订单 -->
|
||||
<div v-else-if="screenKey === 'order'" key="order" class="sim-screen order-screen">
|
||||
<WxNavBar title="订单详情" @back="emit('advance', 1)" />
|
||||
<div class="sim-screen-body order-body">
|
||||
<div class="unpaid-card">
|
||||
<strong class="unpaid-title">订单待支付</strong>
|
||||
<p class="unpaid-desc">
|
||||
已为您锁定本次复诊购药名额,请在
|
||||
<span class="cd-chip">{{ countdownText.minutes }}</span>
|
||||
<i class="cd-sep">:</i>
|
||||
<span class="cd-chip">{{ countdownText.seconds }}</span>
|
||||
内完成支付,超时名额自动取消。
|
||||
</p>
|
||||
<button type="button" class="btn-go-pay sim-hotspot" @click="emit('advance', 3)">去支付</button>
|
||||
</div>
|
||||
|
||||
<div class="order-card">
|
||||
<p class="order-card-title">问诊医生</p>
|
||||
<div class="order-doctor">
|
||||
<span class="order-avatar">李</span>
|
||||
<div class="order-doctor-copy">
|
||||
<strong>李国栋 <i>主治医师</i></strong>
|
||||
<p>萧康互联网医院 · 中医内科</p>
|
||||
</div>
|
||||
<span class="order-type-tag">复诊开方</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="order-card">
|
||||
<p class="order-card-title">订单明细</p>
|
||||
<div class="order-info-row"><span>订单编号</span><i>2026101809470012</i></div>
|
||||
<div class="order-info-row"><span>下单时间</span><i>2026-10-18 09:47</i></div>
|
||||
<div class="order-info-row"><span>应付金额</span><i class="order-amount">¥128.50</i></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-footer">
|
||||
<button type="button" class="btn-pay-now" @click="emit('advance', 3)">立即支付</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第 2/3 屏:收银台(第 3 步叠加微信支付面板) -->
|
||||
<div v-else-if="screenKey === 'cashier'" key="cashier" class="sim-screen cashier-screen">
|
||||
<WxNavBar title="预约购药" @back="emit('advance', 2)" />
|
||||
<div class="sim-screen-body cashier-body">
|
||||
<div class="cash-card cash-patient">
|
||||
<span class="cash-tag">就诊人</span>
|
||||
<strong>张淑芬</strong>
|
||||
<span class="cash-patient-meta">女 · 64岁</span>
|
||||
</div>
|
||||
|
||||
<div class="cash-card cash-delivery">
|
||||
<span class="delivery-opt delivery-opt-on">快递到家</span>
|
||||
<span class="delivery-opt">到店自取</span>
|
||||
</div>
|
||||
|
||||
<div class="cash-card cash-addr">
|
||||
<span class="cash-tag">收货地址</span>
|
||||
<div class="cash-addr-copy">
|
||||
<strong>张淑芬 138****6688</strong>
|
||||
<p>杭州市余杭区文一西路 968 号 3 幢 502 室</p>
|
||||
</div>
|
||||
<ChevronRight :size="16" class="cash-arrow" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<div class="cash-card">
|
||||
<p class="cash-store">萧康大药房(余杭店)</p>
|
||||
<div v-for="drug in drugs" :key="drug.name" class="cash-drug">
|
||||
<div class="cash-drug-head">
|
||||
<strong>{{ drug.name }}</strong>
|
||||
<div class="cash-drug-amount">
|
||||
<span>{{ drug.price }}</span>
|
||||
<i>{{ drug.count }}</i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="cash-drug-usage">{{ drug.usage }}</p>
|
||||
<span class="cash-manual">查看说明书</span>
|
||||
</div>
|
||||
<div class="cash-money-row"><span>商品总价(包含服务费)</span><i>¥98.50</i></div>
|
||||
<div class="cash-money-row"><span>快递费</span><i>¥5.00</i></div>
|
||||
<div class="cash-money-row"><span>诊疗费</span><i>¥25.00</i></div>
|
||||
<div class="cash-money-row cash-money-total"><span>总金额</span><b>¥128.50</b></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cashier-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-pay-main"
|
||||
:class="{ 'sim-hotspot': step === 3 }"
|
||||
@click="emit('advance', 4)"
|
||||
>立即支付</button>
|
||||
</div>
|
||||
|
||||
<!-- 微信支付面板:遮罩 + 底部弹层 -->
|
||||
<Transition name="sim-dim">
|
||||
<div v-if="step === 4" class="wxpay-dim" aria-hidden="true"></div>
|
||||
</Transition>
|
||||
<Transition name="sim-sheet">
|
||||
<div v-if="step === 4" class="wxpay-sheet">
|
||||
<div class="wxpay-head">
|
||||
<button type="button" class="wxpay-close" aria-label="关闭" @click="emit('advance', 3)">
|
||||
<X :size="18" aria-hidden="true" />
|
||||
</button>
|
||||
<span>请确认支付</span>
|
||||
</div>
|
||||
<p class="wxpay-amount"><i>¥</i>128.50</p>
|
||||
<div class="wxpay-row"><span>收款方</span><i>萧康大药房(余杭店)</i></div>
|
||||
<div class="wxpay-row">
|
||||
<span>支付方式</span>
|
||||
<i class="wxpay-way">零钱 <ChevronRight :size="14" aria-hidden="true" /></i>
|
||||
</div>
|
||||
<button type="button" class="btn-wxpay sim-hotspot" @click="emit('advance', 5)">支付</button>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<!-- 第 4 屏:支付成功 -->
|
||||
<div v-else key="done" class="sim-screen done-screen">
|
||||
<WxNavBar title="确认付款信息" :show-back="false" />
|
||||
<div class="sim-screen-body done-body">
|
||||
<div class="done-card">
|
||||
<CircleCheck :size="60" :stroke-width="1.6" class="done-icon" aria-hidden="true" />
|
||||
<strong>付款成功</strong>
|
||||
<div class="done-actions">
|
||||
<button type="button" class="done-btn-order">查看订单</button>
|
||||
<button type="button" class="done-btn-home">返回首页</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="done-tip">门店已开始备药,物流状态将同步到「我的订单」</p>
|
||||
</div>
|
||||
<div class="done-footer">
|
||||
<button type="button" class="btn-done-confirm">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</PhoneFrame>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ============ 第 1 屏:待支付订单 ============ */
|
||||
.order-screen {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.order-body {
|
||||
padding: 12px 10px 90px;
|
||||
}
|
||||
|
||||
/* ============ 服务通知会话页(微信推送形态) ============ */
|
||||
.notice-screen {
|
||||
background: #ededed;
|
||||
}
|
||||
|
||||
.notice-navbar {
|
||||
position: relative;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #191919;
|
||||
}
|
||||
|
||||
.notice-back {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #191919;
|
||||
font-size: 26px;
|
||||
line-height: 1;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.notice-body {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.notice-time {
|
||||
margin: 6px auto 12px;
|
||||
width: fit-content;
|
||||
font-size: 11.5px;
|
||||
color: #9a9a9a;
|
||||
}
|
||||
|
||||
/* 通知卡片:白底圆角,小程序名 + 标题 + 键值行 + 底部查看详情 */
|
||||
.notice-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
padding: 13px 15px 0;
|
||||
text-align: left;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.notice-app {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #9a9a9a;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.notice-app-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(135deg, #4175ee, #298dff);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.notice-title {
|
||||
font-size: 16.5px;
|
||||
font-weight: 600;
|
||||
color: #191919;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.notice-row {
|
||||
display: flex;
|
||||
font-size: 13.5px;
|
||||
color: #191919;
|
||||
line-height: 1.5;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
.notice-row i {
|
||||
flex-shrink: 0;
|
||||
width: 68px;
|
||||
font-style: normal;
|
||||
color: #9a9a9a;
|
||||
}
|
||||
|
||||
.notice-row b {
|
||||
font-weight: 600;
|
||||
color: #fa5151;
|
||||
}
|
||||
|
||||
.notice-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 6px;
|
||||
padding: 11px 0;
|
||||
border-top: 0.5px solid #ececec;
|
||||
font-size: 13.5px;
|
||||
color: #576b95;
|
||||
}
|
||||
|
||||
.unpaid-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.unpaid-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.unpaid-desc {
|
||||
margin: 8px 0 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.cd-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 22px;
|
||||
height: 20px;
|
||||
padding: 0 3px;
|
||||
border-radius: 4px;
|
||||
background: #f44336;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.cd-sep {
|
||||
font-style: normal;
|
||||
color: #f44336;
|
||||
font-weight: 700;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.btn-go-pay {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 41px;
|
||||
margin-top: 16px;
|
||||
border: none;
|
||||
border-radius: 32px;
|
||||
background: #536dfe;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.order-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.order-card-title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.order-doctor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.order-avatar {
|
||||
flex-shrink: 0;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #93c5fd, #3b82f6);
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.order-doctor-copy {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.order-doctor-copy strong {
|
||||
font-size: 15px;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.order-doctor-copy strong i {
|
||||
font-style: normal;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #64748b;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.order-doctor-copy p {
|
||||
margin: 3px 0 0;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.order-type-tag {
|
||||
flex-shrink: 0;
|
||||
padding: 3px 9px;
|
||||
border-radius: 999px;
|
||||
background: rgba(83, 109, 254, 0.08);
|
||||
color: #536dfe;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.order-info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 7px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.order-info-row span {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.order-info-row i {
|
||||
font-style: normal;
|
||||
color: #475569;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.order-amount {
|
||||
font-weight: 700;
|
||||
color: #1e293b !important;
|
||||
}
|
||||
|
||||
.order-footer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 4;
|
||||
height: 66px;
|
||||
padding: 5px 14px 18px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.btn-pay-now {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 43px;
|
||||
border: none;
|
||||
border-radius: 32px;
|
||||
background: #536dfe;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ============ 第 2/3 屏:收银台 ============ */
|
||||
.cashier-screen {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.cashier-body {
|
||||
padding: 10px 10px 90px;
|
||||
}
|
||||
|
||||
.cash-card {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 13px 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.cash-tag {
|
||||
flex-shrink: 0;
|
||||
width: 58px;
|
||||
height: 19px;
|
||||
border-radius: 15px;
|
||||
background: #536dfe;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cash-patient {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cash-patient strong {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.cash-patient-meta {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.cash-delivery {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.delivery-opt {
|
||||
flex: 1;
|
||||
height: 34px;
|
||||
border-radius: 6px;
|
||||
background: #f5f6f8;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.delivery-opt-on {
|
||||
background: rgba(83, 109, 254, 0.08);
|
||||
color: #536dfe;
|
||||
font-weight: 600;
|
||||
border: 1px solid rgba(83, 109, 254, 0.5);
|
||||
}
|
||||
|
||||
.cash-addr {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cash-addr-copy {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cash-addr-copy strong {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.cash-addr-copy p {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.cash-arrow {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.cash-store {
|
||||
margin: 0 0 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.cash-drug {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.cash-drug-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cash-drug-head strong {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #232323;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.cash-drug-amount {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.cash-drug-amount span {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.cash-drug-amount i {
|
||||
font-style: normal;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.cash-drug-usage {
|
||||
margin: 6px 0 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.cash-manual {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
background: #ecf5ff;
|
||||
color: #2b85e4;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.cash-money-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cash-money-row span {
|
||||
font-weight: 600;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.cash-money-row i {
|
||||
font-style: normal;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.cash-money-total {
|
||||
border-top: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.cash-money-total b {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #536dfe;
|
||||
}
|
||||
|
||||
.cashier-footer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 4;
|
||||
height: 57px;
|
||||
padding: 5px 15px 14px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.btn-pay-main {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
border: none;
|
||||
border-radius: 32px;
|
||||
background: #536dfe;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 微信支付面板 */
|
||||
.wxpay-dim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 8;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.wxpay-sheet {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9;
|
||||
background: #fff;
|
||||
border-radius: 12px 12px 0 0;
|
||||
padding: 0 20px 30px;
|
||||
}
|
||||
|
||||
.wxpay-head {
|
||||
position: relative;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.wxpay-close {
|
||||
position: absolute;
|
||||
left: -6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #888;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wxpay-amount {
|
||||
margin: 8px 0 22px;
|
||||
text-align: center;
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.wxpay-amount i {
|
||||
font-style: normal;
|
||||
font-size: 20px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.wxpay-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 11px 0;
|
||||
border-top: 1px solid #f2f2f2;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.wxpay-row span {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.wxpay-row i {
|
||||
font-style: normal;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.wxpay-way {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.btn-wxpay {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
margin-top: 18px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #07c160;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ============ 第 4 屏:支付成功 ============ */
|
||||
.done-screen {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.done-body {
|
||||
padding: 5px 10px 90px;
|
||||
}
|
||||
|
||||
.done-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 30px 24px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.done-icon {
|
||||
color: #4175ee;
|
||||
}
|
||||
|
||||
.done-card strong {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.done-actions {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.done-btn-order {
|
||||
width: 80px;
|
||||
height: 27px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #707070;
|
||||
background: #fff;
|
||||
color: #232323;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.done-btn-home {
|
||||
width: 80px;
|
||||
height: 27px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #1777ff;
|
||||
background: #fff;
|
||||
color: #1777ff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.done-tip {
|
||||
margin: 14px 0 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.done-footer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 4;
|
||||
padding: 8px 16px 24px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.btn-done-confirm {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 43px;
|
||||
border: none;
|
||||
border-radius: 99px;
|
||||
background: #4175ee;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
1594
src/views/demo/flows/PcPrescriptionFlow.vue
Normal file
1594
src/views/demo/flows/PcPrescriptionFlow.vue
Normal file
File diff suppressed because it is too large
Load Diff
1581
src/views/demo/flows/PcReceptionFlow.vue
Normal file
1581
src/views/demo/flows/PcReceptionFlow.vue
Normal file
File diff suppressed because it is too large
Load Diff
1023
src/views/demo/flows/PharmacyBuyFlow.vue
Normal file
1023
src/views/demo/flows/PharmacyBuyFlow.vue
Normal file
File diff suppressed because it is too large
Load Diff
683
src/views/demo/flows/PharmacyFollowupFlow.vue
Normal file
683
src/views/demo/flows/PharmacyFollowupFlow.vue
Normal file
@@ -0,0 +1,683 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 药店复诊流程模拟(患者端)
|
||||
*
|
||||
* 屏幕顺序:微信首页(扫一扫) → 扫码 → 处方药详情(需处方提示) → 用药信息(医生助理对话) →
|
||||
* 转接医生审核 → 电子处方开具
|
||||
* UI 复刻自 xk-client-wx:shop/orders-detail.vue(处方药标签 #D0E4FF/#4175EE、底部四钮)、
|
||||
* follow-up/medication-info.vue 的 AssistantConsultationShell(助手蓝 #0a84ff、问答气泡与选项按钮)。
|
||||
* 处方开具后点击「立即购药」接续「订单支付」流程。
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { BadgeCheck, Bot, FileText, Home, Pill, ShoppingCart } from 'lucide-vue-next'
|
||||
import PhoneFrame from '../components/PhoneFrame.vue'
|
||||
import WxNavBar from '../components/WxNavBar.vue'
|
||||
import WeChatHomeScreen from './shared/WeChatHomeScreen.vue'
|
||||
import WxScanScreen from './shared/WxScanScreen.vue'
|
||||
|
||||
const props = defineProps({
|
||||
step: { type: Number, default: 0 },
|
||||
direction: { type: String, default: 'forward' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['advance'])
|
||||
const router = useRouter()
|
||||
|
||||
const screenKey = computed(() => ['wx', 'scan', 'rx-detail', 'assistant', 'review', 'prescription'][props.step] || 'wx')
|
||||
|
||||
/** 扫一扫是黑底相机页,状态栏切白字 */
|
||||
const statusTone = computed(() => (props.step === 1 ? 'light' : 'dark'))
|
||||
const capsuleTone = computed(() => (props.step === 1 ? 'dark' : 'light'))
|
||||
|
||||
/** 处方开具后进入支付流程,保持业务闭环 */
|
||||
function goOrderPay() {
|
||||
router.push('/demo/order-pay')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PhoneFrame :status-tone="statusTone" :capsule-tone="capsuleTone" screen-bg="#f5f5f5">
|
||||
<Transition :name="`sim-push-${direction}`">
|
||||
<!-- 第 1 屏:微信首页(右上角 + → 扫一扫) -->
|
||||
<WeChatHomeScreen
|
||||
v-if="screenKey === 'wx'"
|
||||
key="wx"
|
||||
mode="scan"
|
||||
@scan="emit('advance', 1)"
|
||||
/>
|
||||
|
||||
<!-- 第 2 屏:扫一扫(对准药店台卡) -->
|
||||
<WxScanScreen
|
||||
v-else-if="screenKey === 'scan'"
|
||||
key="scan"
|
||||
label="萧康大药房(余杭店)"
|
||||
sub="扫码进入小程序 · 处方药复诊购买"
|
||||
@scanned="emit('advance', 2)"
|
||||
/>
|
||||
|
||||
<!-- 第 3 屏:处方药商品详情 -->
|
||||
<div v-else-if="screenKey === 'rx-detail'" key="rx-detail" class="sim-screen rx-screen">
|
||||
<WxNavBar title="商品详情" />
|
||||
<div class="sim-screen-body rx-body">
|
||||
<div class="rx-tip-bar">处方药需凭处方购买,提交需求后医生在线复诊开方</div>
|
||||
<div class="rx-hero">
|
||||
<Pill :size="96" :stroke-width="1.1" aria-hidden="true" />
|
||||
<span class="rx-cf-tag">处方药</span>
|
||||
</div>
|
||||
<div class="rx-info-card">
|
||||
<p class="rx-name">阿莫西林胶囊 0.25g×24粒</p>
|
||||
<div class="rx-price-row">
|
||||
<span class="rx-price">¥12.80</span>
|
||||
<span class="rx-sold">已售 3520</span>
|
||||
</div>
|
||||
<p class="rx-desc"><span>药品说明</span>用于敏感菌所致的呼吸道感染、咽炎、扁桃体炎等,需凭处方购买使用。</p>
|
||||
</div>
|
||||
<div class="rx-store-card">
|
||||
<div class="rx-store-row"><span>药房</span>萧康大药房(余杭店)</div>
|
||||
<div class="rx-store-row"><span>运费</span>运费5元 · 满99元包运费</div>
|
||||
<div class="rx-store-row"><span>服务</span>正品保证 隐私发货 需药师审核发货</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rx-footer">
|
||||
<span class="rx-foot-icon"><Home :size="19" aria-hidden="true" />首页</span>
|
||||
<span class="rx-foot-icon"><ShoppingCart :size="19" aria-hidden="true" />购物车</span>
|
||||
<button type="button" class="rx-btn-cart">加入购物车</button>
|
||||
<button type="button" class="rx-btn-buy sim-hotspot" @click="emit('advance', 3)">立即购买</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第 2 屏:用药信息(医生助理问答) -->
|
||||
<div v-else-if="screenKey === 'assistant'" key="assistant" class="sim-screen assist-screen">
|
||||
<div class="assist-header">
|
||||
<span class="assist-title">购药用药确认</span>
|
||||
</div>
|
||||
<div class="sim-screen-body assist-body">
|
||||
<div class="assist-notice">
|
||||
继续操作视为同意<i>《互联网诊疗风险告知及知情同意书》</i>
|
||||
</div>
|
||||
|
||||
<div class="assist-row">
|
||||
<span class="assist-avatar"><Bot :size="20" aria-hidden="true" /></span>
|
||||
<div class="assist-bubble">
|
||||
您好! 我是医生助理, 已收到您的用药申请:「阿莫西林胶囊 0.25g×24粒」, 为了帮助医生判断准确, 请按照您的真实情况回答以下问题。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="assist-row">
|
||||
<span class="assist-avatar"><Bot :size="20" aria-hidden="true" /></span>
|
||||
<div class="assist-bubble">您是否曾在线下就诊过?</div>
|
||||
</div>
|
||||
|
||||
<div class="assist-row assist-row-mine">
|
||||
<div class="assist-bubble assist-bubble-mine">是</div>
|
||||
<span class="assist-avatar assist-avatar-user">张</span>
|
||||
</div>
|
||||
|
||||
<div class="assist-row">
|
||||
<span class="assist-avatar"><Bot :size="20" aria-hidden="true" /></span>
|
||||
<div class="assist-bubble">您是否曾使用过此类药品?</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="assist-options">
|
||||
<button type="button" class="assist-opt assist-opt-primary sim-hotspot" @click="emit('advance', 4)">是</button>
|
||||
<button type="button" class="assist-opt">否</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第 3 屏:转接医生审核 -->
|
||||
<div v-else-if="screenKey === 'review'" key="review" class="sim-screen assist-screen">
|
||||
<div class="assist-header">
|
||||
<span class="assist-title">购药用药确认</span>
|
||||
</div>
|
||||
<div class="sim-screen-body assist-body">
|
||||
<div class="assist-row assist-row-mine">
|
||||
<div class="assist-bubble assist-bubble-mine">是</div>
|
||||
<span class="assist-avatar assist-avatar-user">张</span>
|
||||
</div>
|
||||
|
||||
<div class="assist-row">
|
||||
<span class="assist-avatar"><Bot :size="20" aria-hidden="true" /></span>
|
||||
<div class="assist-bubble">请问您是因为什么疾病使用该药品呢?</div>
|
||||
</div>
|
||||
|
||||
<div class="assist-row assist-row-mine">
|
||||
<div class="assist-bubble assist-bubble-mine">急性咽炎</div>
|
||||
<span class="assist-avatar assist-avatar-user">张</span>
|
||||
</div>
|
||||
|
||||
<div class="assist-row">
|
||||
<span class="assist-avatar"><Bot :size="20" aria-hidden="true" /></span>
|
||||
<div class="assist-bubble">
|
||||
您已确诊过此疾病,并使用过药,且无相关禁忌症和不良反应。请问您是否还有信息需要补充?如无,我将依据病情为您开具处方。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="assist-row assist-row-mine">
|
||||
<div class="assist-bubble assist-bubble-mine">无需补充,立即开方</div>
|
||||
<span class="assist-avatar assist-avatar-user">张</span>
|
||||
</div>
|
||||
|
||||
<div class="assist-row">
|
||||
<span class="assist-avatar"><Bot :size="20" aria-hidden="true" /></span>
|
||||
<div class="assist-bubble">
|
||||
请稍等, 正在为您转接医生进行复诊开方...
|
||||
<span class="sim-typing"><i></i><i></i><i></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务通知:处方已开具 -->
|
||||
<button type="button" class="push-banner sim-banner-drop sim-hotspot" @click="emit('advance', 5)">
|
||||
<span class="push-icon"><FileText :size="18" aria-hidden="true" /></span>
|
||||
<span class="push-copy">
|
||||
<strong>服务通知</strong>
|
||||
<small>您的电子处方已开具,点击查看</small>
|
||||
</span>
|
||||
<span class="push-go">查看</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 第 4 屏:电子处方开具 -->
|
||||
<div v-else key="prescription" class="sim-screen rx-detail-screen">
|
||||
<WxNavBar title="处方详情" @back="emit('advance', 4)" />
|
||||
<div class="sim-screen-body rx-detail-body">
|
||||
<div class="rx-sheet">
|
||||
<div class="rx-sheet-head">
|
||||
<FileText :size="17" aria-hidden="true" />
|
||||
<span>电子处方</span>
|
||||
<i class="rx-audit-tag"><BadgeCheck :size="12" aria-hidden="true" />审核通过</i>
|
||||
</div>
|
||||
<div class="rx-sheet-body">
|
||||
<div class="rx-sheet-row"><span>患者</span>张淑芬(女 · 64岁)</div>
|
||||
<div class="rx-sheet-row"><span>诊断</span>急性咽炎(复诊)</div>
|
||||
<div class="rx-drug-block">
|
||||
<p class="rx-drug-rp">Rp.</p>
|
||||
<div class="rx-drug-item">
|
||||
<div class="rx-drug-name">
|
||||
<strong>阿莫西林胶囊 0.25g×24粒</strong>
|
||||
<span>x1</span>
|
||||
</div>
|
||||
<p class="rx-drug-usage">用法:口服,一次2粒,一日3次,饭后服用</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rx-sheet-row"><span>医师</span>李国栋(已签名)</div>
|
||||
<div class="rx-sheet-row"><span>药师</span>王丽(审核通过)</div>
|
||||
<div class="rx-sheet-total">
|
||||
合计 <strong>¥12.80</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="rx-valid-tip">处方有效期 3 天,请及时完成购药</p>
|
||||
</div>
|
||||
<div class="rx-detail-footer">
|
||||
<button type="button" class="btn-go-buy sim-hotspot" @click="goOrderPay">立即购药</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</PhoneFrame>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ============ 第 1 屏:处方药详情 ============ */
|
||||
.rx-screen {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.rx-body {
|
||||
padding-bottom: 70px;
|
||||
}
|
||||
|
||||
.rx-tip-bar {
|
||||
margin: 10px 10px 0;
|
||||
padding: 10px 15px;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #1890ff;
|
||||
background: #e6f3ff;
|
||||
color: #1890ff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.rx-hero {
|
||||
position: relative;
|
||||
height: 240px;
|
||||
margin-top: 10px;
|
||||
background: linear-gradient(160deg, #fef2f2, #ffffff 70%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.rx-cf-tag {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 0;
|
||||
width: 50px;
|
||||
height: 20px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
background: #d0e4ff;
|
||||
color: #4175ee;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rx-info-card {
|
||||
background: #fff;
|
||||
padding: 10px 20px 14px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.rx-name {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.rx-price-row {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.rx-price {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #f93030;
|
||||
}
|
||||
|
||||
.rx-sold {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.rx-desc {
|
||||
margin: 10px 0 0;
|
||||
font-size: 12px;
|
||||
color: #232323;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.rx-desc span {
|
||||
color: #999;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.rx-store-card {
|
||||
background: #fff;
|
||||
padding: 6px 20px;
|
||||
}
|
||||
|
||||
.rx-store-row {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
padding: 8px 0;
|
||||
font-size: 12px;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.rx-store-row span {
|
||||
flex-shrink: 0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.rx-footer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 4;
|
||||
height: 74px;
|
||||
padding: 6px 10px 20px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rx-foot-icon {
|
||||
width: 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.rx-btn-cart {
|
||||
flex: 1;
|
||||
height: 38px;
|
||||
border: none;
|
||||
border-radius: 22px;
|
||||
background: #cbe0ff;
|
||||
color: #4175ee;
|
||||
font-size: 15px;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.rx-btn-buy {
|
||||
flex: 1;
|
||||
height: 38px;
|
||||
border: none;
|
||||
border-radius: 22px;
|
||||
background: #4175ee;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* ============ 第 2/3 屏:医生助理对话壳 ============ */
|
||||
.assist-screen {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.assist-header {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 62px 15px 10px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.assist-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.assist-body {
|
||||
padding: 10px 15px 24px;
|
||||
}
|
||||
|
||||
.assist-notice {
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
background: #f5f5f5;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.assist-notice i {
|
||||
font-style: normal;
|
||||
color: #0a84ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.assist-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.assist-row-mine {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.assist-avatar {
|
||||
flex-shrink: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 5px;
|
||||
background: linear-gradient(135deg, #38bdf8, #0a84ff);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.assist-avatar-user {
|
||||
background: linear-gradient(135deg, #fcd34d, #f59e0b);
|
||||
}
|
||||
|
||||
.assist-bubble {
|
||||
max-width: 74%;
|
||||
padding: 9px 12px;
|
||||
background: #fff;
|
||||
border-radius: 2px 6px 6px 6px;
|
||||
color: #333;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.assist-bubble-mine {
|
||||
background: #0a84ff;
|
||||
color: #fff;
|
||||
border-radius: 6px 2px 6px 6px;
|
||||
}
|
||||
|
||||
.assist-options {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 15px 28px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.assist-opt {
|
||||
flex: 1;
|
||||
min-height: 44px;
|
||||
border-radius: 22px;
|
||||
border: 1px solid #ddd;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.assist-opt-primary {
|
||||
border: none;
|
||||
background: #0a84ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 服务通知横幅(与在线复诊一致的推送形态) */
|
||||
.push-banner {
|
||||
position: absolute;
|
||||
top: 58px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
z-index: 7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 11px 13px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.97);
|
||||
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.22);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.push-icon {
|
||||
flex-shrink: 0;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 9px;
|
||||
background: linear-gradient(135deg, #38bdf8, #0a84ff);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.push-copy {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.push-copy strong {
|
||||
font-size: 13px;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.push-copy small {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.push-go {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #0a84ff;
|
||||
}
|
||||
|
||||
/* ============ 第 4 屏:电子处方 ============ */
|
||||
.rx-detail-screen {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.rx-detail-body {
|
||||
padding: 12px 15px 90px;
|
||||
}
|
||||
|
||||
.rx-sheet {
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.rx-sheet-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 12px 14px;
|
||||
background: #eff6ff;
|
||||
color: #0a84ff;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rx-audit-tag {
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
background: #f0fdf4;
|
||||
color: #059669;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rx-sheet-body {
|
||||
padding: 6px 14px 14px;
|
||||
}
|
||||
|
||||
.rx-sheet-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
font-size: 13.5px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.rx-sheet-row span {
|
||||
flex-shrink: 0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.rx-drug-block {
|
||||
margin: 6px 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.rx-drug-rp {
|
||||
margin: 0 0 6px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.rx-drug-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.rx-drug-name strong {
|
||||
font-size: 14px;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.rx-drug-name span {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.rx-drug-usage {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.rx-sheet-total {
|
||||
margin-top: 8px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.rx-sheet-total strong {
|
||||
font-size: 18px;
|
||||
color: #ef4444;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.rx-valid-tip {
|
||||
margin: 12px 4px 0;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rx-detail-footer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 4;
|
||||
padding: 10px 15px 26px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.btn-go-buy {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
border: none;
|
||||
border-radius: 32px;
|
||||
background: #536dfe;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
507
src/views/demo/flows/shared/PatientHomeScreen.vue
Normal file
507
src/views/demo/flows/shared/PatientHomeScreen.vue
Normal file
@@ -0,0 +1,507 @@
|
||||
<template>
|
||||
<div class="sim-screen home-screen">
|
||||
<div class="sim-screen-body home-body">
|
||||
<!-- 顶部蓝色渐变头(诊所模式):复刻 pages/home/home.vue 的 .header -->
|
||||
<div class="home-header">
|
||||
<span class="home-bg-circle" aria-hidden="true"></span>
|
||||
<div class="home-nav-row">
|
||||
<strong class="home-store-name">萧康中医诊所</strong>
|
||||
<span class="home-switch-badge">切换</span>
|
||||
</div>
|
||||
<div class="home-location-bar">
|
||||
<MapPin :size="13" aria-hidden="true" />
|
||||
<span>杭州市余杭区文一西路 968 号 · 距您 1.2km</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- banner 轮播卡:复刻 .banner-card(h300rpx 圆角32 蓝阴影) -->
|
||||
<div class="home-banner">
|
||||
<div class="home-banner-card">
|
||||
<div class="banner-copy">
|
||||
<strong>秋季养生 · 名医坐诊</strong>
|
||||
<span>膏方调理季,老中医一对一问诊</span>
|
||||
<i class="banner-cta">立即预约</i>
|
||||
</div>
|
||||
<Stethoscope class="banner-icon" :size="64" :stroke-width="1.2" aria-hidden="true" />
|
||||
<span class="banner-dots" aria-hidden="true"><i class="dot-on"></i><i></i><i></i></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 金刚区:一行 5 个图标入口 -->
|
||||
<div class="home-zone">
|
||||
<div v-for="item in zoneItems" :key="item.label" class="zone-item">
|
||||
<span class="zone-icon" :style="{ background: item.bg, color: item.color }">
|
||||
<component :is="item.icon" :size="24" :stroke-width="1.9" aria-hidden="true" />
|
||||
</span>
|
||||
<span class="zone-label">{{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 核心入口双卡:到店挂号(蓝)+ 在线复诊(绿),hotspot 由流程指定 -->
|
||||
<div class="home-entries">
|
||||
<button
|
||||
type="button"
|
||||
class="entry-card"
|
||||
:class="{ 'sim-hotspot': hotspot === 'register' }"
|
||||
@click="emit('tap-register')"
|
||||
>
|
||||
<span class="entry-title" style="color: #4175ee">到店挂号</span>
|
||||
<span class="entry-sub">预约专家 不排队</span>
|
||||
<CalendarCheck class="entry-icon" :size="42" :stroke-width="1.4" style="color: #4175ee" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="entry-card"
|
||||
:class="{ 'sim-hotspot': hotspot === 'followup' }"
|
||||
@click="emit('tap-followup')"
|
||||
>
|
||||
<span class="entry-title" style="color: #00b578">在线复诊</span>
|
||||
<span class="entry-sub">在线选药 极速达</span>
|
||||
<ClipboardPlus class="entry-icon" :size="42" :stroke-width="1.4" style="color: #00b578" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 医生推荐列表:复刻医生卡(头像+职称 tag+擅长+底部统计栏) -->
|
||||
<div class="home-section-head">
|
||||
<strong>医生推荐</strong>
|
||||
<span>更多 ›</span>
|
||||
</div>
|
||||
<div v-for="doctor in doctors" :key="doctor.name" class="home-doctor-card">
|
||||
<div class="doctor-main">
|
||||
<span class="doctor-avatar" :style="{ background: doctor.avatarBg }">
|
||||
{{ doctor.name.slice(0, 1) }}
|
||||
<i class="doctor-online-dot" aria-hidden="true"></i>
|
||||
</span>
|
||||
<div class="doctor-info">
|
||||
<div class="doctor-name-row">
|
||||
<strong>{{ doctor.name }}</strong>
|
||||
<span class="doctor-title-tag">{{ doctor.title }}</span>
|
||||
</div>
|
||||
<p class="doctor-depart">{{ doctor.depart }} | 萧康中医诊所</p>
|
||||
<span class="doctor-skill-tag">擅长:{{ doctor.skill }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="doctor-stats">
|
||||
<div><strong>{{ doctor.consults }}</strong><span>问诊量</span></div>
|
||||
<div><strong>{{ doctor.rate }}</strong><span>接诊率</span></div>
|
||||
<div><strong>{{ doctor.response }}</strong><span>平均响应</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WxTabBar :items="tabItems" :active="0" selected-color="#1575fc" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 患者端小程序首页(诊所模式)· 共享模拟屏
|
||||
*
|
||||
* 为什么这样写:
|
||||
* - 「线下挂号」与「在线复诊」两个流程的第一屏都是首页,只是引导点击的入口不同,
|
||||
* 抽成共享组件用 hotspot 属性控制哪张入口卡发光,避免两份重复的复刻代码。
|
||||
* - 尺寸与颜色按 xk-client-wx/pages/home/home.vue 源码换算(rpx / 2 = px):
|
||||
* 蓝渐变头 #298DFF→#6BAFFF、底圆角 60rpx→30px、入口卡标题 #4175EE / #00B578 等。
|
||||
*/
|
||||
import { CalendarCheck, ClipboardPlus, FileText, Home, LayoutGrid, MapPin, MessageCircle, Newspaper, Pill, Stethoscope, User } from 'lucide-vue-next'
|
||||
import WxTabBar from '../../components/WxTabBar.vue'
|
||||
|
||||
defineProps({
|
||||
/** 需要引导点击的入口:register = 到店挂号,followup = 在线复诊 */
|
||||
hotspot: { type: String, default: 'register' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['tap-register', 'tap-followup'])
|
||||
|
||||
/** 金刚区入口:图标底色参考小程序 icons8 彩色图标的气质 */
|
||||
const zoneItems = [
|
||||
{ label: '在线复诊', icon: Stethoscope, bg: '#e8f3ff', color: '#298dff' },
|
||||
{ label: '到店挂号', icon: CalendarCheck, bg: '#eaefff', color: '#4175ee' },
|
||||
{ label: '药店购药', icon: Pill, bg: '#e6f7f0', color: '#00b578' },
|
||||
{ label: '我的处方', icon: FileText, bg: '#fff3e6', color: '#fea54c' },
|
||||
{ label: '全部服务', icon: LayoutGrid, bg: '#f0f2f5', color: '#64748b' }
|
||||
]
|
||||
|
||||
/** 推荐医生(虚构演示数据) */
|
||||
const doctors = [
|
||||
{
|
||||
name: '李国栋',
|
||||
title: '主治医师',
|
||||
depart: '中医内科',
|
||||
skill: '慢性胃炎、脾胃虚弱调理',
|
||||
consults: '1286',
|
||||
rate: '98%',
|
||||
response: '5分钟',
|
||||
avatarBg: 'linear-gradient(135deg, #93c5fd, #3b82f6)'
|
||||
},
|
||||
{
|
||||
name: '周慧敏',
|
||||
title: '副主任医师',
|
||||
depart: '呼吸内科',
|
||||
skill: '慢性咳嗽、支气管炎、哮喘',
|
||||
consults: '2043',
|
||||
rate: '99%',
|
||||
response: '3分钟',
|
||||
avatarBg: 'linear-gradient(135deg, #f9a8d4, #ec4899)'
|
||||
}
|
||||
]
|
||||
|
||||
/** 底部 tab:首页 / 资讯 / 消息 / 我的(selectedColor #1575FC 摘自 pages.json) */
|
||||
const tabItems = [
|
||||
{ label: '首页', icon: Home },
|
||||
{ label: '资讯', icon: Newspaper },
|
||||
{ label: '消息', icon: MessageCircle, badge: '2' },
|
||||
{ label: '我的', icon: User }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-screen {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.home-body {
|
||||
padding-bottom: 92px;
|
||||
}
|
||||
|
||||
/* 蓝渐变头:#298DFF → #6BAFFF → 页面底色,底部大圆角 */
|
||||
.home-header {
|
||||
position: relative;
|
||||
padding: 58px 16px 80px;
|
||||
background: linear-gradient(180deg, #298dff 0%, #6bafff 80%, #f5f7fa 100%);
|
||||
border-radius: 0 0 30px 30px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.home-bg-circle {
|
||||
position: absolute;
|
||||
right: -60px;
|
||||
top: -50px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.home-nav-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.home-store-name {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.home-switch-badge {
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #4175ee;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.home-location-bar {
|
||||
position: relative;
|
||||
margin-top: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
backdrop-filter: blur(4px);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* banner 卡:上移压住渐变头 */
|
||||
.home-banner {
|
||||
margin: -60px 16px 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.home-banner-card {
|
||||
position: relative;
|
||||
height: 150px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(120deg, #1e63d0 0%, #3f8cff 58%, #7cc4ff 100%);
|
||||
box-shadow: 0 15px 35px rgba(41, 141, 255, 0.25);
|
||||
overflow: hidden;
|
||||
padding: 26px 24px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.banner-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.banner-copy strong {
|
||||
font-size: 19px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.banner-copy span {
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.banner-cta {
|
||||
margin-top: 6px;
|
||||
align-self: flex-start;
|
||||
font-style: normal;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 5px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: #1e63d0;
|
||||
}
|
||||
|
||||
.banner-icon {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.banner-dots {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
bottom: 10px;
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.banner-dots i {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.banner-dots .dot-on {
|
||||
width: 12px;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 金刚区 */
|
||||
.home-zone {
|
||||
display: flex;
|
||||
padding: 0 16px 10px;
|
||||
}
|
||||
|
||||
.zone-item {
|
||||
width: 20%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.zone-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.zone-label {
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 核心入口双卡 */
|
||||
.home-entries {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.entry-card {
|
||||
position: relative;
|
||||
height: 90px;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.03);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
padding-left: 16px;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.entry-title {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.entry-sub {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.entry-icon {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 10px;
|
||||
opacity: 0.28;
|
||||
}
|
||||
|
||||
/* 医生推荐 */
|
||||
.home-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 18px 12px;
|
||||
}
|
||||
|
||||
.home-section-head strong {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.home-section-head span {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.home-doctor-card {
|
||||
margin: 0 16px 16px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.04);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.doctor-main {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.doctor-avatar {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.doctor-online-dot {
|
||||
position: absolute;
|
||||
right: 1px;
|
||||
bottom: 1px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #00b578;
|
||||
border: 2px solid #fff;
|
||||
}
|
||||
|
||||
.doctor-info {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.doctor-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.doctor-name-row strong {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.doctor-title-tag {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: #f0f2f5;
|
||||
color: #666;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.doctor-depart {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.doctor-skill-tag {
|
||||
align-self: flex-start;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
background: rgba(65, 117, 238, 0.1);
|
||||
color: #4175ee;
|
||||
font-size: 11px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.doctor-stats {
|
||||
display: flex;
|
||||
background: #f8fbff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.doctor-stats div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.doctor-stats strong {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.doctor-stats span {
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
320
src/views/demo/flows/shared/WeChatHomeScreen.vue
Normal file
320
src/views/demo/flows/shared/WeChatHomeScreen.vue
Normal file
@@ -0,0 +1,320 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 微信首页(聊天列表)模拟屏:患者端流程的统一起点
|
||||
*
|
||||
* 两种引导模式:
|
||||
* - mode="scan":右上角「+」展开菜单 → 点「扫一扫」进入扫码页(线下挂号 / 复诊 / 购药流程的入口)
|
||||
* - mode="notice":直接点列表里的「服务通知」查看小程序推送(订单支付流程的入口)
|
||||
*
|
||||
* UI 按微信 8.x 形态复刻:#ededed 导航、白底会话列表、底部四 tab(微信绿 #07c160)。
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
import { Bell, CircleUserRound, Compass, MessageCircle, Plus, ScanLine, Search, UserPlus, Users, Wallet } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps({
|
||||
mode: { type: String, default: 'scan' },
|
||||
/** 服务通知的摘要文案,各流程可按剧情覆盖 */
|
||||
noticeDigest: { type: String, default: '惠佑康:您有一条新的就诊提醒' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['scan', 'notice'])
|
||||
|
||||
/** 「+」菜单展开状态:仅 scan 模式使用 */
|
||||
const menuOpen = ref(false)
|
||||
|
||||
const chats = [
|
||||
{ name: '妈妈', digest: '记得帮我看看那个药怎么买', time: '09:32', avatarBg: 'linear-gradient(135deg, #fbbf24, #f59e0b)', short: '妈' },
|
||||
{ name: '社区家庭医生群', digest: '王医生:本周随访安排已更新', time: '08:47', avatarBg: 'linear-gradient(135deg, #34d399, #10b981)', short: '医' },
|
||||
{ name: '文件传输助手', digest: '[图片]', time: '昨天', avatarBg: 'linear-gradient(135deg, #38bdf8, #0ea5e9)', short: '文' }
|
||||
]
|
||||
|
||||
function onPlusTap() {
|
||||
if (props.mode !== 'scan') return
|
||||
menuOpen.value = !menuOpen.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sim-screen wechat-screen">
|
||||
<!-- 微信导航 -->
|
||||
<header class="wc-navbar">
|
||||
<span class="wc-title">微信</span>
|
||||
<span class="wc-nav-actions">
|
||||
<Search :size="19" aria-hidden="true" />
|
||||
<button
|
||||
type="button"
|
||||
class="wc-plus"
|
||||
:class="{ 'sim-hotspot': mode === 'scan' && !menuOpen }"
|
||||
aria-label="更多功能"
|
||||
@click="onPlusTap"
|
||||
>
|
||||
<Plus :size="21" aria-hidden="true" />
|
||||
</button>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<!-- 会话列表 -->
|
||||
<div class="sim-screen-body wc-list">
|
||||
<!-- 服务通知:小程序消息的统一入口 -->
|
||||
<button
|
||||
type="button"
|
||||
class="wc-chat"
|
||||
:class="{ 'sim-hotspot': mode === 'notice' }"
|
||||
@click="mode === 'notice' && emit('notice')"
|
||||
>
|
||||
<span class="wc-avatar wc-avatar-notice" aria-hidden="true">
|
||||
<Bell :size="20" />
|
||||
</span>
|
||||
<span class="wc-chat-main">
|
||||
<strong>服务通知</strong>
|
||||
<small>{{ noticeDigest }}</small>
|
||||
</span>
|
||||
<span class="wc-chat-side">
|
||||
<i class="wc-time">10:21</i>
|
||||
<i v-if="mode === 'notice'" class="wc-badge">1</i>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button v-for="chat in chats" :key="chat.name" type="button" class="wc-chat">
|
||||
<span class="wc-avatar" :style="{ background: chat.avatarBg }" aria-hidden="true">{{ chat.short }}</span>
|
||||
<span class="wc-chat-main">
|
||||
<strong>{{ chat.name }}</strong>
|
||||
<small>{{ chat.digest }}</small>
|
||||
</span>
|
||||
<span class="wc-chat-side"><i class="wc-time">{{ chat.time }}</i></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 「+」下拉菜单 -->
|
||||
<Transition name="wc-menu">
|
||||
<div v-if="menuOpen" class="wc-menu" role="menu">
|
||||
<span class="wc-menu-item"><Users :size="17" aria-hidden="true" />发起群聊</span>
|
||||
<span class="wc-menu-item"><UserPlus :size="17" aria-hidden="true" />添加朋友</span>
|
||||
<button type="button" class="wc-menu-item sim-hotspot" role="menuitem" @click="emit('scan')">
|
||||
<ScanLine :size="17" aria-hidden="true" />扫一扫
|
||||
</button>
|
||||
<span class="wc-menu-item"><Wallet :size="17" aria-hidden="true" />收付款</span>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- 微信底部 tab -->
|
||||
<nav class="wc-tabbar" aria-hidden="true">
|
||||
<span class="wc-tab wc-tab-on"><MessageCircle :size="21" /><i>微信</i></span>
|
||||
<span class="wc-tab"><CircleUserRound :size="21" /><i>通讯录</i></span>
|
||||
<span class="wc-tab"><Compass :size="21" /><i>发现</i></span>
|
||||
<span class="wc-tab"><CircleUserRound :size="21" /><i>我</i></span>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wechat-screen {
|
||||
background: #ededed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wc-navbar {
|
||||
flex-shrink: 0;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background: #ededed;
|
||||
color: #191919;
|
||||
}
|
||||
|
||||
.wc-title {
|
||||
font-size: 16.5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wc-nav-actions {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
color: #191919;
|
||||
}
|
||||
|
||||
.wc-plus {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #191919;
|
||||
display: flex;
|
||||
padding: 2px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.wc-list {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.wc-chat {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 11px 14px;
|
||||
border: none;
|
||||
background: #fff;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.wc-chat + .wc-chat {
|
||||
border-top: 0.5px solid #ececec;
|
||||
}
|
||||
|
||||
.wc-avatar {
|
||||
flex-shrink: 0;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 5px;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 服务通知头像:微信橙黄铃铛 */
|
||||
.wc-avatar-notice {
|
||||
background: linear-gradient(135deg, #fbbf5e, #fa9d3b);
|
||||
}
|
||||
|
||||
.wc-chat-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.wc-chat-main strong {
|
||||
font-size: 15.5px;
|
||||
font-weight: 500;
|
||||
color: #191919;
|
||||
}
|
||||
|
||||
.wc-chat-main small {
|
||||
font-size: 12.5px;
|
||||
color: #9a9a9a;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.wc-chat-side {
|
||||
flex-shrink: 0;
|
||||
align-self: flex-start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 5px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.wc-time {
|
||||
font-style: normal;
|
||||
font-size: 11px;
|
||||
color: #b2b2b2;
|
||||
}
|
||||
|
||||
.wc-badge {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
border-radius: 50%;
|
||||
background: #fa5151;
|
||||
color: #fff;
|
||||
font-style: normal;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 「+」下拉菜单:微信深灰浮层 */
|
||||
.wc-menu {
|
||||
position: absolute;
|
||||
top: 78px;
|
||||
right: 10px;
|
||||
z-index: 8;
|
||||
width: 142px;
|
||||
padding: 4px 0;
|
||||
border-radius: 9px;
|
||||
background: #4c4c4c;
|
||||
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wc-menu::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: 14px;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-bottom: 6px solid #4c4c4c;
|
||||
}
|
||||
|
||||
.wc-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
padding: 11px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.wc-menu-item + .wc-menu-item {
|
||||
border-top: 0.5px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.wc-menu-enter-active,
|
||||
.wc-menu-leave-active {
|
||||
transition: opacity 0.18s ease, transform 0.18s ease;
|
||||
transform-origin: top right;
|
||||
}
|
||||
|
||||
.wc-menu-enter-from,
|
||||
.wc-menu-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.92) translateY(-4px);
|
||||
}
|
||||
|
||||
/* 微信 tabbar */
|
||||
.wc-tabbar {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
padding: 6px 0 22px;
|
||||
background: #f7f7f7;
|
||||
border-top: 0.5px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.wc-tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
color: #191919;
|
||||
}
|
||||
|
||||
.wc-tab i {
|
||||
font-style: normal;
|
||||
font-size: 10.5px;
|
||||
}
|
||||
|
||||
.wc-tab-on {
|
||||
color: #07c160;
|
||||
}
|
||||
</style>
|
||||
198
src/views/demo/flows/shared/WxScanScreen.vue
Normal file
198
src/views/demo/flows/shared/WxScanScreen.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 微信「扫一扫」相机页模拟:黑色取景背景 + 扫描框 + 上下往返的扫描线
|
||||
*
|
||||
* 取景框里放一张门店台卡(FakeQrcode 生成的仿真小程序码),
|
||||
* 点击台卡即模拟「识别成功」,由父流程推进到小程序页面。
|
||||
*/
|
||||
import { Image, Zap } from 'lucide-vue-next'
|
||||
import FakeQrcode from '../../components/FakeQrcode.vue'
|
||||
|
||||
defineProps({
|
||||
/** 台卡标题:如「萧康中医诊所」 */
|
||||
label: { type: String, default: '萧康中医诊所' },
|
||||
/** 台卡副题 */
|
||||
sub: { type: String, default: '扫码进入小程序 · 挂号问诊' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['scanned'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sim-screen scan-screen">
|
||||
<header class="scan-navbar">扫一扫</header>
|
||||
|
||||
<div class="scan-stage">
|
||||
<!-- 取景框:四角亮标 + 扫描线 -->
|
||||
<button type="button" class="scan-frame sim-hotspot" aria-label="模拟扫码成功" @click="emit('scanned')">
|
||||
<i class="scan-corner scan-corner-tl" aria-hidden="true"></i>
|
||||
<i class="scan-corner scan-corner-tr" aria-hidden="true"></i>
|
||||
<i class="scan-corner scan-corner-bl" aria-hidden="true"></i>
|
||||
<i class="scan-corner scan-corner-br" aria-hidden="true"></i>
|
||||
<span class="scan-line" aria-hidden="true"></span>
|
||||
|
||||
<!-- 对准的门店台卡 -->
|
||||
<span class="scan-card">
|
||||
<FakeQrcode :size="112" seed="7" />
|
||||
<strong>{{ label }}</strong>
|
||||
<small>{{ sub }}</small>
|
||||
</span>
|
||||
</button>
|
||||
<p class="scan-tip">将小程序码放入框内,即可自动识别</p>
|
||||
</div>
|
||||
|
||||
<div class="scan-tools" aria-hidden="true">
|
||||
<span class="scan-tool"><Zap :size="19" />轻触照亮</span>
|
||||
<span class="scan-tool"><Image :size="19" />相册</span>
|
||||
</div>
|
||||
|
||||
<nav class="scan-tabs" aria-hidden="true">
|
||||
<span class="scan-tab scan-tab-on">扫码</span>
|
||||
<span class="scan-tab">付款码</span>
|
||||
<span class="scan-tab">识物</span>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.scan-screen {
|
||||
background: #101418;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.scan-navbar {
|
||||
flex-shrink: 0;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.scan-stage {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.scan-frame {
|
||||
position: relative;
|
||||
width: 218px;
|
||||
height: 218px;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.scan-corner {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #07c160;
|
||||
}
|
||||
|
||||
.scan-corner-tl { left: 0; top: 0; border-right: none; border-bottom: none; }
|
||||
.scan-corner-tr { right: 0; top: 0; border-left: none; border-bottom: none; }
|
||||
.scan-corner-bl { left: 0; bottom: 0; border-right: none; border-top: none; }
|
||||
.scan-corner-br { right: 0; bottom: 0; border-left: none; border-top: none; }
|
||||
|
||||
/* 扫描线:上下往返 */
|
||||
.scan-line {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
top: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, rgba(7, 193, 96, 0.9), transparent);
|
||||
animation: scan-sweep 2.2s ease-in-out infinite;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@keyframes scan-sweep {
|
||||
0%, 100% { transform: translateY(12px); }
|
||||
50% { transform: translateY(200px); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.scan-line {
|
||||
animation: none;
|
||||
transform: translateY(104px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 取景框里的门店台卡 */
|
||||
.scan-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 12px 14px 10px;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.scan-card strong {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.scan-card small {
|
||||
font-size: 10.5px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.scan-tip {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
|
||||
.scan-tools {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 52px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.scan-tool {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.scan-tabs {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 34px;
|
||||
padding: 4px 0 26px;
|
||||
}
|
||||
|
||||
.scan-tab {
|
||||
font-size: 13.5px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.scan-tab-on {
|
||||
color: #07c160;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
1403
src/views/demo/index.vue
Normal file
1403
src/views/demo/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import { RouterLink } from 'vue-router'
|
||||
import { ArrowLeft, ArrowRight, Calendar, CheckCircle2, Clock3, Play, ShieldCheck, Sparkles, Star } from 'lucide-vue-next'
|
||||
import { homeCaseCards, homeMarqueeItems, homeServiceItems, metrics, siteConfig } from '@/site.config'
|
||||
import AnimatedContent from '@/components/vue-bits/AnimatedContent.vue'
|
||||
import CountUp from '@/components/vue-bits/CountUp.vue'
|
||||
import DecryptedText from '@/components/vue-bits/DecryptedText.vue'
|
||||
import BlurText from '@/components/vue-bits/BlurText.vue'
|
||||
import Magnet from '@/components/vue-bits/Magnet.vue'
|
||||
@@ -78,8 +79,9 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<source :src="siteConfig.hero.video.src" type="video/mp4">
|
||||
</video>
|
||||
<!-- 绸缎氛围层颜色与品牌青蓝(--brand)同色相,避免 teal 与 cyan 两个色系打架 -->
|
||||
<div class="hero-silk-layer" aria-hidden="true">
|
||||
<Silk color="#14B8A6" :speed="2.6" :scale="1.2" :noise-intensity="0.78" :rotation="0.16" />
|
||||
<Silk color="#0EA5C4" :speed="2.6" :scale="1.2" :noise-intensity="0.78" :rotation="0.16" />
|
||||
</div>
|
||||
<div class="bottom-blur-overlay" aria-hidden="true"></div>
|
||||
|
||||
@@ -187,19 +189,19 @@ onBeforeUnmount(() => {
|
||||
<section id="intro" ref="aboutRef" class="creator-about-section">
|
||||
<div class="floating-medical-chip chip-a">
|
||||
<Sparkles :size="20" aria-hidden="true" />
|
||||
AI Alert
|
||||
智能预警
|
||||
</div>
|
||||
<div class="floating-medical-chip chip-b">
|
||||
<ShieldCheck :size="20" aria-hidden="true" />
|
||||
Data Safe
|
||||
数据安全
|
||||
</div>
|
||||
<div class="floating-medical-chip chip-c">
|
||||
<CheckCircle2 :size="20" aria-hidden="true" />
|
||||
Follow-up
|
||||
随访提醒
|
||||
</div>
|
||||
<div class="floating-medical-chip chip-d">
|
||||
<Star :size="20" aria-hidden="true" />
|
||||
99.95%
|
||||
99.95% 可用
|
||||
</div>
|
||||
|
||||
<AnimatedContent class-name="about-inner" :distance="52" :duration="1" ease="power4.out">
|
||||
@@ -309,8 +311,9 @@ onBeforeUnmount(() => {
|
||||
</AnimatedContent>
|
||||
|
||||
<AnimatedContent class-name="contact-metrics" :distance="50" direction="horizontal" :reverse="true" :duration="0.9">
|
||||
<!-- 指标数字滚动入场:进入视口后从 0 滚动到目标值 -->
|
||||
<div v-for="item in metrics" :key="item.label">
|
||||
<strong>{{ item.value }}</strong>
|
||||
<strong><CountUp :value="item.value" /></strong>
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
</AnimatedContent>
|
||||
|
||||
@@ -3,8 +3,10 @@ import { computed, ref } from 'vue'
|
||||
import { ArrowRight, BadgeCheck, Check, CircleDollarSign, ClipboardCheck, Headphones, MessageCircle, Play, ShieldCheck, Star, Video } from 'lucide-vue-next'
|
||||
import { caseStudies, faqs, pricingFeatureGroups, pricingPlans, productFeatures, siteConfig } from '@/site.config'
|
||||
import AnimatedContent from '@/components/vue-bits/AnimatedContent.vue'
|
||||
import CountUp from '@/components/vue-bits/CountUp.vue'
|
||||
import DecryptedText from '@/components/vue-bits/DecryptedText.vue'
|
||||
import Magnet from '@/components/vue-bits/Magnet.vue'
|
||||
import TiltCard from '@/components/vue-bits/TiltCard.vue'
|
||||
|
||||
const openFaq = ref(0)
|
||||
const demoVideoPlaying = ref(false)
|
||||
@@ -118,42 +120,45 @@ const reviews = [
|
||||
</AnimatedContent>
|
||||
|
||||
<AnimatedContent :distance="72" direction="horizontal" :reverse="true" :duration="0.95" class-name="product-snapshot-shell">
|
||||
<div class="product-snapshot">
|
||||
<div class="snapshot-header">
|
||||
<span>萧康云医工作台</span>
|
||||
<strong>实时运行</strong>
|
||||
</div>
|
||||
<div class="snapshot-main">
|
||||
<aside>
|
||||
<span v-for="item in ['患者画像', '协同任务', '风险预警', '运营分析']" :key="item">{{ item }}</span>
|
||||
</aside>
|
||||
<div class="snapshot-content">
|
||||
<div class="snapshot-alert">
|
||||
<ShieldCheck :size="20" aria-hidden="true" />
|
||||
<span>慢病高风险患者已自动分派给责任医生</span>
|
||||
</div>
|
||||
<div class="snapshot-cards">
|
||||
<div>
|
||||
<small>待处理随访</small>
|
||||
<strong>246</strong>
|
||||
<!-- 3D 倾斜驾驶舱:工作台快照跟随鼠标产生透视倾斜,数字滚动入场 -->
|
||||
<TiltCard :max-tilt="8" :perspective="1200">
|
||||
<div class="product-snapshot">
|
||||
<div class="snapshot-header">
|
||||
<span>萧康云医工作台</span>
|
||||
<strong>实时运行</strong>
|
||||
</div>
|
||||
<div class="snapshot-main">
|
||||
<aside>
|
||||
<span v-for="item in ['患者画像', '协同任务', '风险预警', '运营分析']" :key="item">{{ item }}</span>
|
||||
</aside>
|
||||
<div class="snapshot-content">
|
||||
<div class="snapshot-alert">
|
||||
<ShieldCheck :size="20" aria-hidden="true" />
|
||||
<span>慢病高风险患者已自动分派给责任医生</span>
|
||||
</div>
|
||||
<div>
|
||||
<small>会诊申请</small>
|
||||
<strong>38</strong>
|
||||
<div class="snapshot-cards">
|
||||
<div>
|
||||
<small>待处理随访</small>
|
||||
<strong><CountUp value="246" /></strong>
|
||||
</div>
|
||||
<div>
|
||||
<small>会诊申请</small>
|
||||
<strong><CountUp value="38" /></strong>
|
||||
</div>
|
||||
<div>
|
||||
<small>质控通过率</small>
|
||||
<strong><CountUp value="97.8%" /></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<small>质控通过率</small>
|
||||
<strong>97.8%</strong>
|
||||
<div class="snapshot-bars" aria-hidden="true">
|
||||
<span style="width: 76%"></span>
|
||||
<span style="width: 92%"></span>
|
||||
<span style="width: 58%"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="snapshot-bars" aria-hidden="true">
|
||||
<span style="width: 76%"></span>
|
||||
<span style="width: 92%"></span>
|
||||
<span style="width: 58%"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TiltCard>
|
||||
</AnimatedContent>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import { ArrowRight, BadgeCheck, CheckCircle2, Clock3, Layers3, ListVideo, Play, Smartphone, Sparkles, Stethoscope, Store } from 'lucide-vue-next'
|
||||
import { ArrowRight, BadgeCheck, ListVideo, Smartphone } from 'lucide-vue-next'
|
||||
import CinematicVideoPlayer from '@/components/CinematicVideoPlayer.vue'
|
||||
import AnimatedContent from '@/components/vue-bits/AnimatedContent.vue'
|
||||
import DecryptedText from '@/components/vue-bits/DecryptedText.vue'
|
||||
import Magnet from '@/components/vue-bits/Magnet.vue'
|
||||
import SplitText from '@/components/vue-bits/SplitText.vue'
|
||||
import { videoShowcase } from '@/site.config'
|
||||
@@ -13,8 +12,8 @@ const route = useRoute()
|
||||
|
||||
const videoTabs = [
|
||||
{ key: 'user', label: '用户端', to: '/video/user', icon: Smartphone },
|
||||
{ key: 'doctor', label: '医生端', to: '/video/doctor', icon: Stethoscope },
|
||||
{ key: 'manager', label: '小程序店长管理端', to: '/video/manager', icon: Store }
|
||||
// { key: 'doctor', label: '医生端', to: '/video/doctor', icon: Stethoscope },
|
||||
// { key: 'manager', label: '小程序店长管理端', to: '/video/manager', icon: Store }
|
||||
]
|
||||
|
||||
const currentKey = computed(() => {
|
||||
@@ -64,18 +63,8 @@ function videoTo(id) {
|
||||
:to="{ opacity: 1, y: 0, filter: 'blur(0px)' }"
|
||||
/>
|
||||
|
||||
<p class="video-hero-subtitle">
|
||||
<DecryptedText
|
||||
:key="`${currentKey}-subtitle`"
|
||||
:text="currentVideo.subtitle"
|
||||
animate-on="view"
|
||||
reveal-direction="center"
|
||||
:speed="30"
|
||||
:max-iterations="10"
|
||||
encrypted-class-name="decrypted-ghost"
|
||||
class-name="decrypted-final"
|
||||
/>
|
||||
</p>
|
||||
<!-- 副标题直接输出:去掉乱码翻牌动画,让企业站文案安静可读 -->
|
||||
<p class="video-hero-subtitle">{{ currentVideo.subtitle }}</p>
|
||||
|
||||
<nav class="video-role-tabs" aria-label="视频角色导航">
|
||||
<RouterLink
|
||||
@@ -97,9 +86,9 @@ function videoTo(id) {
|
||||
<ArrowRight :size="18" aria-hidden="true" />
|
||||
</RouterLink>
|
||||
</Magnet>
|
||||
<a href="#chapters" class="video-secondary-action">
|
||||
查看章节
|
||||
</a>
|
||||
<RouterLink to="/demo/full-journey" class="video-secondary-action">
|
||||
体验流程模拟
|
||||
</RouterLink>
|
||||
</div>
|
||||
</AnimatedContent>
|
||||
|
||||
@@ -154,113 +143,113 @@ function videoTo(id) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="video-metrics-section">
|
||||
<div class="mx-auto grid max-w-7xl gap-4 px-5 md:grid-cols-3 md:px-8">
|
||||
<AnimatedContent
|
||||
v-for="(metric, index) in currentVideo.metrics"
|
||||
:key="`${currentKey}-${metric.label}`"
|
||||
:delay="index * 0.07"
|
||||
:distance="38"
|
||||
class-name="video-metric-reveal"
|
||||
>
|
||||
<article class="video-metric-card">
|
||||
<strong>{{ metric.value }}</strong>
|
||||
<span>{{ metric.label }}</span>
|
||||
</article>
|
||||
</AnimatedContent>
|
||||
</div>
|
||||
</section>
|
||||
<!-- <section class="video-metrics-section">-->
|
||||
<!-- <div class="mx-auto grid max-w-7xl gap-4 px-5 md:grid-cols-3 md:px-8">-->
|
||||
<!-- <AnimatedContent-->
|
||||
<!-- v-for="(metric, index) in currentVideo.metrics"-->
|
||||
<!-- :key="`${currentKey}-${metric.label}`"-->
|
||||
<!-- :delay="index * 0.07"-->
|
||||
<!-- :distance="38"-->
|
||||
<!-- class-name="video-metric-reveal"-->
|
||||
<!-- >-->
|
||||
<!-- <article class="video-metric-card">-->
|
||||
<!-- <strong>{{ metric.value }}</strong>-->
|
||||
<!-- <span>{{ metric.label }}</span>-->
|
||||
<!-- </article>-->
|
||||
<!-- </AnimatedContent>-->
|
||||
<!-- </div>-->
|
||||
<!-- </section>-->
|
||||
|
||||
<section id="chapters" class="video-section video-section-muted">
|
||||
<div class="mx-auto grid max-w-7xl gap-10 px-5 md:px-8 lg:grid-cols-[0.82fr_1.18fr]">
|
||||
<AnimatedContent :distance="44" :duration="0.85" class-name="video-section-heading">
|
||||
<span>Chapter Flow</span>
|
||||
<h2>按观看节奏拆解关键场景。</h2>
|
||||
<p>适合销售演示、客户培训和内部对齐,先让用户看懂价值,再进入细节。</p>
|
||||
</AnimatedContent>
|
||||
<!-- <section id="chapters" class="video-section video-section-muted">-->
|
||||
<!-- <div class="mx-auto grid max-w-7xl gap-10 px-5 md:px-8 lg:grid-cols-[0.82fr_1.18fr]">-->
|
||||
<!-- <AnimatedContent :distance="44" :duration="0.85" class-name="video-section-heading">-->
|
||||
<!-- <span>Chapter Flow</span>-->
|
||||
<!-- <h2>按观看节奏拆解关键场景。</h2>-->
|
||||
<!-- <p>适合销售演示、客户培训和内部对齐,先让用户看懂价值,再进入细节。</p>-->
|
||||
<!-- </AnimatedContent>-->
|
||||
|
||||
<div class="video-chapter-list">
|
||||
<AnimatedContent
|
||||
v-for="(chapter, index) in selectedVideo.chapters"
|
||||
:key="`${currentKey}-${selectedVideo.id}-${chapter.title}`"
|
||||
:delay="index * 0.08"
|
||||
:distance="46"
|
||||
direction="horizontal"
|
||||
:reverse="index % 2 === 1"
|
||||
class-name="video-chapter-reveal"
|
||||
>
|
||||
<article class="video-chapter-card">
|
||||
<span>{{ chapter.time }}</span>
|
||||
<div>
|
||||
<h3>{{ chapter.title }}</h3>
|
||||
<p>{{ chapter.body }}</p>
|
||||
</div>
|
||||
<Play :size="19" fill="currentColor" aria-hidden="true" />
|
||||
</article>
|
||||
</AnimatedContent>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- <div class="video-chapter-list">-->
|
||||
<!-- <AnimatedContent-->
|
||||
<!-- v-for="(chapter, index) in selectedVideo.chapters"-->
|
||||
<!-- :key="`${currentKey}-${selectedVideo.id}-${chapter.title}`"-->
|
||||
<!-- :delay="index * 0.08"-->
|
||||
<!-- :distance="46"-->
|
||||
<!-- direction="horizontal"-->
|
||||
<!-- :reverse="index % 2 === 1"-->
|
||||
<!-- class-name="video-chapter-reveal"-->
|
||||
<!-- >-->
|
||||
<!-- <article class="video-chapter-card">-->
|
||||
<!-- <span>{{ chapter.time }}</span>-->
|
||||
<!-- <div>-->
|
||||
<!-- <h3>{{ chapter.title }}</h3>-->
|
||||
<!-- <p>{{ chapter.body }}</p>-->
|
||||
<!-- </div>-->
|
||||
<!-- <Play :size="19" fill="currentColor" aria-hidden="true" />-->
|
||||
<!-- </article>-->
|
||||
<!-- </AnimatedContent>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </section>-->
|
||||
|
||||
<section class="video-section">
|
||||
<div class="mx-auto max-w-7xl px-5 md:px-8">
|
||||
<AnimatedContent :distance="42" :duration="0.85" class-name="video-section-heading centered">
|
||||
<span>Experience Details</span>
|
||||
<h2>{{ currentVideo.label }}体验亮点</h2>
|
||||
</AnimatedContent>
|
||||
<!-- <section class="video-section">-->
|
||||
<!-- <div class="mx-auto max-w-7xl px-5 md:px-8">-->
|
||||
<!-- <AnimatedContent :distance="42" :duration="0.85" class-name="video-section-heading centered">-->
|
||||
<!-- <span>Experience Details</span>-->
|
||||
<!-- <h2>{{ currentVideo.label }}体验亮点</h2>-->
|
||||
<!-- </AnimatedContent>-->
|
||||
|
||||
<div class="video-highlight-grid">
|
||||
<AnimatedContent
|
||||
v-for="(highlight, index) in selectedVideo.highlights"
|
||||
:key="`${currentKey}-${selectedVideo.id}-${highlight}`"
|
||||
:delay="index * 0.08"
|
||||
:distance="42"
|
||||
class-name="video-highlight-reveal"
|
||||
>
|
||||
<article class="video-highlight-card">
|
||||
<CheckCircle2 :size="24" aria-hidden="true" />
|
||||
<h3>{{ highlight }}</h3>
|
||||
<p>围绕真实 C 端观看和演示场景,保持内容清楚、交互轻盈、重点突出。</p>
|
||||
</article>
|
||||
</AnimatedContent>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- <div class="video-highlight-grid">-->
|
||||
<!-- <AnimatedContent-->
|
||||
<!-- v-for="(highlight, index) in selectedVideo.highlights"-->
|
||||
<!-- :key="`${currentKey}-${selectedVideo.id}-${highlight}`"-->
|
||||
<!-- :delay="index * 0.08"-->
|
||||
<!-- :distance="42"-->
|
||||
<!-- class-name="video-highlight-reveal"-->
|
||||
<!-- >-->
|
||||
<!-- <article class="video-highlight-card">-->
|
||||
<!-- <CheckCircle2 :size="24" aria-hidden="true" />-->
|
||||
<!-- <h3>{{ highlight }}</h3>-->
|
||||
<!-- <p>围绕真实 C 端观看和演示场景,保持内容清楚、交互轻盈、重点突出。</p>-->
|
||||
<!-- </article>-->
|
||||
<!-- </AnimatedContent>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </section>-->
|
||||
|
||||
<section class="video-section video-final-section">
|
||||
<div class="mx-auto grid max-w-7xl gap-8 px-5 md:px-8 lg:grid-cols-[1fr_0.9fr]">
|
||||
<AnimatedContent :distance="48" :duration="0.9" class-name="video-final-copy">
|
||||
<div class="video-eyebrow">
|
||||
<Sparkles :size="16" aria-hidden="true" />
|
||||
Demo Ready
|
||||
</div>
|
||||
<h2>需要替换真实视频素材时,只改配置即可。</h2>
|
||||
<p>三端页面已经按视频库建模,后续可以为每条视频单独配置地址、封面、章节、标签、时长和讲解重点。</p>
|
||||
<Magnet :padding="84" :magnet-strength="5">
|
||||
<RouterLink to="/product#contact" class="video-primary-action">
|
||||
联系顾问
|
||||
<ArrowRight :size="18" aria-hidden="true" />
|
||||
</RouterLink>
|
||||
</Magnet>
|
||||
</AnimatedContent>
|
||||
<!-- <section class="video-section video-final-section">-->
|
||||
<!-- <div class="mx-auto grid max-w-7xl gap-8 px-5 md:px-8 lg:grid-cols-[1fr_0.9fr]">-->
|
||||
<!-- <AnimatedContent :distance="48" :duration="0.9" class-name="video-final-copy">-->
|
||||
<!-- <div class="video-eyebrow">-->
|
||||
<!-- <Sparkles :size="16" aria-hidden="true" />-->
|
||||
<!-- Demo Ready-->
|
||||
<!-- </div>-->
|
||||
<!-- <h2>需要替换真实视频素材时,只改配置即可。</h2>-->
|
||||
<!-- <p>三端页面已经按视频库建模,后续可以为每条视频单独配置地址、封面、章节、标签、时长和讲解重点。</p>-->
|
||||
<!-- <Magnet :padding="84" :magnet-strength="5">-->
|
||||
<!-- <RouterLink to="/product#contact" class="video-primary-action">-->
|
||||
<!-- 联系顾问-->
|
||||
<!-- <ArrowRight :size="18" aria-hidden="true" />-->
|
||||
<!-- </RouterLink>-->
|
||||
<!-- </Magnet>-->
|
||||
<!-- </AnimatedContent>-->
|
||||
|
||||
<AnimatedContent :distance="48" direction="horizontal" :reverse="true" :duration="0.9" class-name="video-scene-panel">
|
||||
<div class="video-scene-head">
|
||||
<Layers3 :size="24" aria-hidden="true" />
|
||||
<span>适用场景</span>
|
||||
</div>
|
||||
<div class="video-scene-list">
|
||||
<span v-for="scene in currentVideo.scenes" :key="scene">{{ scene }}</span>
|
||||
</div>
|
||||
<div class="video-timeline" aria-hidden="true">
|
||||
<span v-for="chapter in selectedVideo.chapters" :key="chapter.time"></span>
|
||||
</div>
|
||||
<div class="video-duration-note">
|
||||
<Clock3 :size="18" aria-hidden="true" />
|
||||
建议演示时长 90 秒以内
|
||||
</div>
|
||||
</AnimatedContent>
|
||||
</div>
|
||||
</section>
|
||||
<!-- <AnimatedContent :distance="48" direction="horizontal" :reverse="true" :duration="0.9" class-name="video-scene-panel">-->
|
||||
<!-- <div class="video-scene-head">-->
|
||||
<!-- <Layers3 :size="24" aria-hidden="true" />-->
|
||||
<!-- <span>适用场景</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="video-scene-list">-->
|
||||
<!-- <span v-for="scene in currentVideo.scenes" :key="scene">{{ scene }}</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="video-timeline" aria-hidden="true">-->
|
||||
<!-- <span v-for="chapter in selectedVideo.chapters" :key="chapter.time"></span>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="video-duration-note">-->
|
||||
<!-- <Clock3 :size="18" aria-hidden="true" />-->
|
||||
<!-- 建议演示时长 90 秒以内-->
|
||||
<!-- </div>-->
|
||||
<!-- </AnimatedContent>-->
|
||||
<!-- </div>-->
|
||||
<!-- </section>-->
|
||||
</main>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user