|
|
@@ -0,0 +1,753 @@
|
|
|
+<template>
|
|
|
+ <view class="page">
|
|
|
+ <!-- 自定义导航栏 -->
|
|
|
+ <view class="status-bar" :style="{height: statusBarHeight + 'px'}"></view>
|
|
|
+ <view class="nav-bar">
|
|
|
+ <view class="nav-back" @click="goBack">
|
|
|
+ <image src="/static/public/back.png" mode="aspectFill" class="back-icon"></image>
|
|
|
+ </view>
|
|
|
+ <text class="nav-title">穴位定位</text>
|
|
|
+ <view class="nav-right">
|
|
|
+ <text class="nav-count">{{acupointList.length}}穴位</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 人体模型 + 穴位标注 -->
|
|
|
+ <view class="body-container">
|
|
|
+ <view class="body-wrapper">
|
|
|
+ <!-- 中线参考 -->
|
|
|
+ <view class="midline"></view>
|
|
|
+
|
|
|
+ <!-- 人体背部剪影 -->
|
|
|
+ <image
|
|
|
+ src="/static/device/body-back-silhouette.svg"
|
|
|
+ mode="aspectFit"
|
|
|
+ class="body-img"
|
|
|
+ @load="onImageLoad"
|
|
|
+ ></image>
|
|
|
+
|
|
|
+ <!-- 穴位点 -->
|
|
|
+ <view
|
|
|
+ v-for="(point, index) in positionedAcupoints"
|
|
|
+ :key="point.id"
|
|
|
+ class="acupoint-dot"
|
|
|
+ :class="{
|
|
|
+ 'acupoint-active': selectedIndex === index
|
|
|
+ }"
|
|
|
+ :style="{
|
|
|
+ left: point.screenX + 'px',
|
|
|
+ top: point.screenY + 'px'
|
|
|
+ }"
|
|
|
+ @click="onAcupointTap(index)"
|
|
|
+ >
|
|
|
+ <view class="dot-inner"></view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 穴位列表 -->
|
|
|
+ <view class="list-section">
|
|
|
+ <view class="list-header">
|
|
|
+ <text class="list-title">穴位清单</text>
|
|
|
+ </view>
|
|
|
+ <scroll-view class="list-scroll" scroll-y>
|
|
|
+ <view
|
|
|
+ v-for="(point, index) in acupointList"
|
|
|
+ :key="point.id"
|
|
|
+ class="list-item"
|
|
|
+ :class="{'list-item-active': selectedIndex === index}"
|
|
|
+ @click="onListItemTap(index)"
|
|
|
+ >
|
|
|
+ <view class="item-left">
|
|
|
+ <view class="item-index" :class="{'item-index-active': selectedIndex === index}">
|
|
|
+ <text class="index-text">{{index + 1}}</text>
|
|
|
+ </view>
|
|
|
+ <view class="item-info">
|
|
|
+ <text class="item-name">{{point.acupointName}}</text>
|
|
|
+ <text class="item-code">{{point.acupointCode}}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="item-right">
|
|
|
+ <text class="item-side" v-if="point.customSide || point.acupointSide !== 'center'">{{ point.customSide || sideLabel(point.acupointSide) }}</text>
|
|
|
+ <view class="item-params" v-if="point.method">
|
|
|
+ <text class="param-text">{{point.method}} {{point.temperature}}°C {{point.duration}}min</text>
|
|
|
+ </view>
|
|
|
+ <view class="item-coord-box" v-else>
|
|
|
+ <text class="item-coord">{{point.coordinateX}},{{point.coordinateY}}</text>
|
|
|
+ <text class="item-unit">mm</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 编辑弹窗 -->
|
|
|
+ <view class="popup-mask" v-if="showPopup" @click="closePopup">
|
|
|
+ <view class="popup-content" @click.stop>
|
|
|
+ <view class="popup-header">
|
|
|
+ <text class="popup-title">编辑穴位参数</text>
|
|
|
+ <text class="popup-acupoint-name">{{ currentEditAcupoint?.acupointName }}</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="popup-body">
|
|
|
+ <!-- 侧别 -->
|
|
|
+ <view class="form-group">
|
|
|
+ <text class="form-label">侧别</text>
|
|
|
+ <view class="tags-row">
|
|
|
+ <view
|
|
|
+ class="tag-item"
|
|
|
+ v-for="item in sideOptions"
|
|
|
+ :key="item"
|
|
|
+ :class="{'tag-item-active': editForm.side === item}"
|
|
|
+ @click="editForm.side = item"
|
|
|
+ >
|
|
|
+ {{item}}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 手法 -->
|
|
|
+ <view class="form-group">
|
|
|
+ <text class="form-label">手法选择</text>
|
|
|
+ <view class="tags-row">
|
|
|
+ <view
|
|
|
+ class="tag-item"
|
|
|
+ v-for="item in methodOptions"
|
|
|
+ :key="item"
|
|
|
+ :class="{'tag-item-active': editForm.method === item}"
|
|
|
+ @click="editForm.method = item"
|
|
|
+ >
|
|
|
+ {{item}}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 温度与时间 -->
|
|
|
+ <view class="form-row-2">
|
|
|
+ <view class="form-group half-width">
|
|
|
+ <text class="form-label">温度 (°C)</text>
|
|
|
+ <view class="stepper">
|
|
|
+ <view class="stepper-btn" @click="editForm.temperature > 30 && editForm.temperature--">-</view>
|
|
|
+ <input class="stepper-input" type="number" v-model="editForm.temperature" />
|
|
|
+ <view class="stepper-btn" @click="editForm.temperature < 60 && editForm.temperature++">+</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="form-group half-width">
|
|
|
+ <text class="form-label">时间 (分钟)</text>
|
|
|
+ <view class="stepper">
|
|
|
+ <view class="stepper-btn" @click="editForm.duration > 1 && editForm.duration--">-</view>
|
|
|
+ <input class="stepper-input" type="number" v-model="editForm.duration" />
|
|
|
+ <view class="stepper-btn" @click="editForm.duration < 60 && editForm.duration++">+</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="popup-footer">
|
|
|
+ <view class="btn btn-cancel" @click="closePopup">取消</view>
|
|
|
+ <view class="btn btn-confirm" @click="saveEdit">确定</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 加载提示 -->
|
|
|
+ <view class="loading-mask" v-if="loading">
|
|
|
+ <view class="loading-card">
|
|
|
+ <view class="loading-spinner"></view>
|
|
|
+ <text class="loading-text">加载中...</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { getBackAcupoints } from '@/utils/api/acupoint.js'
|
|
|
+
|
|
|
+export default {
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ statusBarHeight: 44,
|
|
|
+ loading: false,
|
|
|
+ acupointList: [],
|
|
|
+ selectedIndex: -1,
|
|
|
+ imageReady: false,
|
|
|
+ // SVG viewBox: 0 0 300 400, 一体式实心剪影
|
|
|
+ containerWidth: 345,
|
|
|
+ containerHeight: 460,
|
|
|
+ // 大椎穴(0,0)在容器中的位置
|
|
|
+ // 颈根/斜方肌起点(C7)在SVG中 y=78 -> 按比例放大1.15倍
|
|
|
+ originX: 172.5,
|
|
|
+ originY: 89.7,
|
|
|
+ // 毫米到像素缩放比
|
|
|
+ // 0.5 * 1.15 = 0.575
|
|
|
+ mmToPixel: 0.575,
|
|
|
+
|
|
|
+ // 弹窗表单
|
|
|
+ showPopup: false,
|
|
|
+ editForm: {
|
|
|
+ side: '',
|
|
|
+ method: '温和灸',
|
|
|
+ temperature: 45,
|
|
|
+ duration: 15
|
|
|
+ },
|
|
|
+ sideOptions: ['双侧', '中心', '左侧', '右侧'],
|
|
|
+ methodOptions: ['温和灸', '雀啄灸', '回旋灸']
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ /** 当前正在编辑的穴位 */
|
|
|
+ currentEditAcupoint() {
|
|
|
+ if (this.selectedIndex >= 0 && this.acupointList[this.selectedIndex]) {
|
|
|
+ return this.acupointList[this.selectedIndex]
|
|
|
+ }
|
|
|
+ return null
|
|
|
+ },
|
|
|
+ /** 计算屏幕坐标后的穴位列表 */
|
|
|
+ positionedAcupoints() {
|
|
|
+ if (!this.imageReady || this.acupointList.length === 0) return []
|
|
|
+
|
|
|
+ return this.acupointList.map(point => {
|
|
|
+ // coordinateX: 向右为正 (mm), coordinateY: 向下为正 (mm)
|
|
|
+ // 大椎穴为(0,0)原点
|
|
|
+ const screenX = this.originX + (point.coordinateX * this.mmToPixel)
|
|
|
+ const screenY = this.originY + (point.coordinateY * this.mmToPixel)
|
|
|
+ return {
|
|
|
+ ...point,
|
|
|
+ screenX: Math.max(8, Math.min(this.containerWidth - 8, screenX)),
|
|
|
+ screenY: Math.max(8, Math.min(this.containerHeight - 8, screenY))
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onLoad() {
|
|
|
+ const sysInfo = uni.getSystemInfoSync()
|
|
|
+ this.statusBarHeight = sysInfo.statusBarHeight || 44
|
|
|
+ this.loadAcupoints()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ goBack() {
|
|
|
+ uni.navigateBack()
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 图片加载完成 */
|
|
|
+ onImageLoad() {
|
|
|
+ this.imageReady = true
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 加载穴位数据 */
|
|
|
+ async loadAcupoints() {
|
|
|
+ this.loading = true
|
|
|
+ try {
|
|
|
+ const data = await getBackAcupoints(4)
|
|
|
+ if (Array.isArray(data)) {
|
|
|
+ this.acupointList = data
|
|
|
+ } else {
|
|
|
+ this.acupointList = []
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载穴位数据失败:', e)
|
|
|
+ uni.showToast({ title: '穴位数据加载失败', icon: 'none' })
|
|
|
+ this.acupointList = []
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ // 确保图片加载后穴位可显示
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (!this.imageReady) {
|
|
|
+ this.imageReady = true
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 点击穴位点 */
|
|
|
+ onAcupointTap(index) {
|
|
|
+ this.openPopup(index)
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 点击列表项 */
|
|
|
+ onListItemTap(index) {
|
|
|
+ this.openPopup(index)
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 打开编辑弹窗 */
|
|
|
+ openPopup(index) {
|
|
|
+ this.selectedIndex = index
|
|
|
+ const point = this.acupointList[index]
|
|
|
+ // 初始化表单数据
|
|
|
+ this.editForm = {
|
|
|
+ side: point.customSide || this.sideLabel(point.acupointSide) || '中心',
|
|
|
+ method: point.method || '温和灸',
|
|
|
+ temperature: point.temperature || 45,
|
|
|
+ duration: point.duration || 15
|
|
|
+ }
|
|
|
+ this.showPopup = true
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 关闭弹窗 */
|
|
|
+ closePopup() {
|
|
|
+ this.showPopup = false
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 保存编辑结果 */
|
|
|
+ saveEdit() {
|
|
|
+ if (this.selectedIndex >= 0) {
|
|
|
+ const point = this.acupointList[this.selectedIndex]
|
|
|
+ // 更新穴位列表中的对应数据(使用Vue的响应式需要特殊处理,但这里我们直接改属性,若无法触发视图更新可用$set或splice,但简单修改通常有效)
|
|
|
+ point.customSide = this.editForm.side
|
|
|
+ point.method = this.editForm.method
|
|
|
+ point.temperature = this.editForm.temperature
|
|
|
+ point.duration = this.editForm.duration
|
|
|
+ }
|
|
|
+ this.closePopup()
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 侧别标签 */
|
|
|
+ sideLabel(side) {
|
|
|
+ const map = { left: '左侧', right: '右侧', center: '中心' }
|
|
|
+ return map[side] || side
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 置信度标签 */
|
|
|
+ confidenceLabel(confidence) {
|
|
|
+ const map = { high: '高', medium: '中', low: '低' }
|
|
|
+ return map[confidence] || confidence
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.page {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100vh;
|
|
|
+ background: linear-gradient(to bottom, #389588, rgba(56,149,136,0.3) 40%, #F8F8F8 60%);
|
|
|
+}
|
|
|
+.status-bar {
|
|
|
+ background-color: transparent;
|
|
|
+}
|
|
|
+
|
|
|
+/* 导航栏 */
|
|
|
+.nav-bar {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ height: 44px;
|
|
|
+ padding: 0 15px;
|
|
|
+}
|
|
|
+.nav-back {
|
|
|
+ width: 25px;
|
|
|
+ height: 25px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.back-icon {
|
|
|
+ width: 25px;
|
|
|
+ height: 25px;
|
|
|
+}
|
|
|
+.nav-title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #FFFFFF;
|
|
|
+}
|
|
|
+.nav-right {
|
|
|
+ width: 60px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+.nav-count {
|
|
|
+ font-size: 12px;
|
|
|
+ color: rgba(255,255,255,0.8);
|
|
|
+}
|
|
|
+
|
|
|
+/* 人体模型容器 */
|
|
|
+.body-container {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: flex-start; /* 改为顶部对齐,防止高度不足时头部被居中截断 */
|
|
|
+ padding: 20px 10px; /* 增加顶部间距,保护头部不贴边 */
|
|
|
+ overflow-y: auto; /* 允许垂直滚动,适配屏幕高度不足的情况 */
|
|
|
+ min-height: 0;
|
|
|
+}
|
|
|
+.body-wrapper {
|
|
|
+ position: relative;
|
|
|
+ width: 345px;
|
|
|
+ height: 460px;
|
|
|
+ margin: auto 0; /* 空间充足时保持垂直居中,空间不足时贴顶 */
|
|
|
+ /* Removed bubble-like styling to keep it clean */
|
|
|
+}
|
|
|
+
|
|
|
+/* 中线 */
|
|
|
+.midline {
|
|
|
+ position: absolute;
|
|
|
+ left: 50%;
|
|
|
+ top: 60px;
|
|
|
+ bottom: 20px;
|
|
|
+ width: 1px;
|
|
|
+ background: transparent;
|
|
|
+ border-left: 1px dashed rgba(255, 255, 255, 0.4);
|
|
|
+ z-index: 2;
|
|
|
+ pointer-events: none;
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+.body-img {
|
|
|
+ position: relative;
|
|
|
+ width: 345px;
|
|
|
+ height: 460px;
|
|
|
+ z-index: 3;
|
|
|
+ /* Added a soft drop shadow to make the human shape stand out nicely */
|
|
|
+ filter: drop-shadow(0px 15px 25px rgba(0, 0, 0, 0.12));
|
|
|
+}
|
|
|
+
|
|
|
+/* 穴位点 */
|
|
|
+.acupoint-dot {
|
|
|
+ position: absolute;
|
|
|
+ width: 30px;
|
|
|
+ height: 30px;
|
|
|
+ margin-left: -15px;
|
|
|
+ margin-top: -15px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+.dot-inner {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #FFFFFF;
|
|
|
+ border: 1.5px solid #389588;
|
|
|
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
|
|
+ transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
|
+ z-index: 1;
|
|
|
+}
|
|
|
+
|
|
|
+/* 选中态 */
|
|
|
+.acupoint-active .dot-inner {
|
|
|
+ width: 14px;
|
|
|
+ height: 14px;
|
|
|
+ background: #F65252;
|
|
|
+ border: 2px solid #FFFFFF;
|
|
|
+ box-shadow: 0 4px 12px rgba(246, 82, 82, 0.4);
|
|
|
+ transform: scale(1.1);
|
|
|
+}
|
|
|
+
|
|
|
+/* 穴位列表 */
|
|
|
+.list-section {
|
|
|
+ background: #FFFFFF;
|
|
|
+ border-radius: 28px 28px 0 0;
|
|
|
+ padding: 20px 20px 0;
|
|
|
+ max-height: 40vh;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ box-shadow: 0 -10px 30px rgba(0, 0, 0, 0.06);
|
|
|
+}
|
|
|
+.list-header {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+.list-title {
|
|
|
+ font-size: 15px;
|
|
|
+ color: #333333;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+.list-scroll {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding-bottom: 20px;
|
|
|
+}
|
|
|
+.list-item {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 14px 16px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ border-radius: 16px;
|
|
|
+ background: #F8FAFA;
|
|
|
+ border: 1px solid rgba(56, 149, 136, 0.05);
|
|
|
+ transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
|
+}
|
|
|
+.list-item-active {
|
|
|
+ background: rgba(56, 149, 136, 0.08);
|
|
|
+ border-color: rgba(56, 149, 136, 0.3);
|
|
|
+ box-shadow: 0 4px 12px rgba(56, 149, 136, 0.1);
|
|
|
+ transform: scale(1.02);
|
|
|
+}
|
|
|
+.item-left {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.item-index {
|
|
|
+ width: 28px;
|
|
|
+ height: 28px;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: rgba(56, 149, 136, 0.1);
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ margin-right: 12px;
|
|
|
+ transition: all 0.25s;
|
|
|
+}
|
|
|
+.item-index-active {
|
|
|
+ background: rgba(246, 82, 82, 0.1);
|
|
|
+}
|
|
|
+.index-text {
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #389588;
|
|
|
+}
|
|
|
+.item-index-active .index-text {
|
|
|
+ color: #F65252;
|
|
|
+}
|
|
|
+.item-info {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+.item-name {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #545F71;
|
|
|
+ font-weight: 500;
|
|
|
+ line-height: 1.3;
|
|
|
+}
|
|
|
+.item-code {
|
|
|
+ font-size: 10px;
|
|
|
+ color: #9BA5B7;
|
|
|
+ margin-top: 2px;
|
|
|
+ font-family: monospace;
|
|
|
+}
|
|
|
+.item-right {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+.item-side {
|
|
|
+ font-size: 10px;
|
|
|
+ color: #389588;
|
|
|
+ padding: 2px 6px;
|
|
|
+ border-radius: 4px;
|
|
|
+ background: rgba(56, 149, 136, 0.08);
|
|
|
+}
|
|
|
+.item-coord-box {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ align-items: baseline;
|
|
|
+}
|
|
|
+.item-coord {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #9BA5B7;
|
|
|
+ font-family: monospace;
|
|
|
+}
|
|
|
+.item-unit {
|
|
|
+ font-size: 9px;
|
|
|
+ color: #c0c0c0;
|
|
|
+ margin-left: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 加载遮罩 */
|
|
|
+.loading-mask {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(0, 0, 0, 0.4);
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ z-index: 100;
|
|
|
+}
|
|
|
+.loading-card {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ background: #FFFFFF;
|
|
|
+ border-radius: 16px;
|
|
|
+ padding: 28px 36px;
|
|
|
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
|
|
|
+}
|
|
|
+@keyframes spin {
|
|
|
+ 0% { transform: rotate(0deg); }
|
|
|
+ 100% { transform: rotate(360deg); }
|
|
|
+}
|
|
|
+.loading-spinner {
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ border: 2px solid #EEF1F4;
|
|
|
+ border-top-color: #389588;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 0.8s linear infinite;
|
|
|
+ margin-bottom: 14px;
|
|
|
+}
|
|
|
+.loading-text {
|
|
|
+ color: #545F71;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+/* 弹窗样式 */
|
|
|
+.popup-mask {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+ z-index: 200;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ animation: fadeIn 0.3s ease;
|
|
|
+}
|
|
|
+@keyframes fadeIn {
|
|
|
+ from { opacity: 0; }
|
|
|
+ to { opacity: 1; }
|
|
|
+}
|
|
|
+.popup-content {
|
|
|
+ width: 320px;
|
|
|
+ background: #FFFFFF;
|
|
|
+ border-radius: 20px;
|
|
|
+ overflow: hidden;
|
|
|
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
|
|
+ animation: slideUp 0.3s ease;
|
|
|
+}
|
|
|
+@keyframes slideUp {
|
|
|
+ from { transform: translateY(20px); opacity: 0; }
|
|
|
+ to { transform: translateY(0); opacity: 1; }
|
|
|
+}
|
|
|
+.popup-header {
|
|
|
+ padding: 20px 24px 16px;
|
|
|
+ border-bottom: 1px solid #F0F2F5;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.popup-title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333333;
|
|
|
+}
|
|
|
+.popup-acupoint-name {
|
|
|
+ font-size: 15px;
|
|
|
+ color: #389588;
|
|
|
+ font-weight: 600;
|
|
|
+ background: rgba(56, 149, 136, 0.1);
|
|
|
+ padding: 4px 10px;
|
|
|
+ border-radius: 6px;
|
|
|
+}
|
|
|
+.popup-body {
|
|
|
+ padding: 20px 24px;
|
|
|
+}
|
|
|
+.form-group {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+.form-group:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+.form-row-2 {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+.half-width {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+.form-label {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #545F71;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ display: block;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+.tags-row {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+.tag-item {
|
|
|
+ padding: 8px 14px;
|
|
|
+ background: #F4F6F8;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #545F71;
|
|
|
+ border: 1px solid transparent;
|
|
|
+ transition: all 0.2s;
|
|
|
+}
|
|
|
+.tag-item-active {
|
|
|
+ background: rgba(56, 149, 136, 0.1);
|
|
|
+ color: #389588;
|
|
|
+ border-color: #389588;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+.stepper {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ background: #F4F6F8;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.stepper-btn {
|
|
|
+ width: 36px;
|
|
|
+ height: 36px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 18px;
|
|
|
+ color: #545F71;
|
|
|
+ background: #EEF1F4;
|
|
|
+}
|
|
|
+.stepper-btn:active {
|
|
|
+ background: #E2E6EA;
|
|
|
+}
|
|
|
+.stepper-input {
|
|
|
+ flex: 1;
|
|
|
+ height: 36px;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #333333;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+.popup-footer {
|
|
|
+ display: flex;
|
|
|
+ border-top: 1px solid #F0F2F5;
|
|
|
+}
|
|
|
+.btn {
|
|
|
+ flex: 1;
|
|
|
+ height: 50px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+.btn-cancel {
|
|
|
+ color: #9BA5B7;
|
|
|
+ background: #FFFFFF;
|
|
|
+}
|
|
|
+.btn-confirm {
|
|
|
+ color: #FFFFFF;
|
|
|
+ background: #389588;
|
|
|
+}
|
|
|
+
|
|
|
+/* 参数展示 */
|
|
|
+.item-params {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.param-text {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #389588;
|
|
|
+ background: rgba(56, 149, 136, 0.1);
|
|
|
+ padding: 2px 6px;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+</style>
|