|
|
@@ -50,16 +50,21 @@
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
<el-row :gutter="12">
|
|
|
- <el-col :span="12">
|
|
|
+ <el-col :span="8">
|
|
|
<el-form-item label="肩宽(cm)">
|
|
|
<el-input v-model="bodyForm.shoulderWidth" disabled />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
- <el-col :span="12">
|
|
|
+ <el-col :span="8">
|
|
|
<el-form-item label="身长(cm)">
|
|
|
<el-input v-model="bodyForm.bodyHeight" disabled />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="脊柱长(cm)">
|
|
|
+ <el-input v-model="bodyForm.spineLength" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
</el-row>
|
|
|
<el-row :gutter="12">
|
|
|
<el-col :span="8">
|
|
|
@@ -199,68 +204,39 @@
|
|
|
<stop offset="55%" stop-color="#f1f5f9" />
|
|
|
<stop offset="100%" stop-color="#e8eef5" />
|
|
|
</radialGradient>
|
|
|
- <linearGradient id="simBodyGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
|
- <stop offset="0%" stop-color="#fdf8f5" />
|
|
|
- <stop offset="35%" stop-color="#f5ebe3" />
|
|
|
- <stop offset="100%" stop-color="#e8ddd4" />
|
|
|
- </linearGradient>
|
|
|
- <linearGradient id="simStroke" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
|
- <stop offset="0%" stop-color="#c9b8a8" />
|
|
|
- <stop offset="100%" stop-color="#a8988a" />
|
|
|
- </linearGradient>
|
|
|
- <filter id="simShadow" x="-25%" y="-15%" width="150%" height="130%">
|
|
|
- <feDropShadow dx="0" dy="6" stdDeviation="8" flood-color="#5c4a3d" flood-opacity="0.10" />
|
|
|
- </filter>
|
|
|
</defs>
|
|
|
|
|
|
<rect width="100%" height="100%" fill="url(#simWash)" rx="14" />
|
|
|
- <rect width="100%" height="100%" fill="url(#sim-grid)" rx="14" opacity="0.85" />
|
|
|
+ <image
|
|
|
+ :href="bodyBackImage"
|
|
|
+ x="0"
|
|
|
+ y="-10"
|
|
|
+ width="400"
|
|
|
+ height="600"
|
|
|
+ preserveAspectRatio="xMidYMid slice"
|
|
|
+ class="body-photo"
|
|
|
+ />
|
|
|
+ <rect width="100%" height="100%" fill="url(#sim-grid)" rx="14" opacity="0.35" />
|
|
|
|
|
|
<line class="axis-line" :x1="SVG_C7_X" y1="12" :x2="SVG_C7_X" y2="568" stroke="rgba(148,163,184,0.4)" stroke-width="1" stroke-dasharray="4 6" />
|
|
|
<line class="axis-line" x1="12" :y1="SVG_C7_Y" x2="388" :y2="SVG_C7_Y" stroke="rgba(148,163,184,0.4)" stroke-width="1" stroke-dasharray="5 5" />
|
|
|
|
|
|
- <!-- 人体轮廓 -->
|
|
|
- <g filter="url(#simShadow)" stroke="url(#simStroke)" stroke-width="1.35" stroke-linejoin="round" stroke-linecap="round">
|
|
|
- <path d="
|
|
|
- M 200 28
|
|
|
- C 186 28, 174 38, 172 56
|
|
|
- C 170 70, 174 86, 178 96
|
|
|
- C 180 102, 162 108, 138 114
|
|
|
- C 114 124, 98 136, 88 150
|
|
|
- C 80 168, 76 192, 74 220
|
|
|
- C 72 252, 68 288, 64 318
|
|
|
- C 60 340, 54 352, 62 358
|
|
|
- C 68 364, 84 356, 96 332
|
|
|
- C 108 306, 112 272, 116 238
|
|
|
- C 120 208, 124 186, 128 192
|
|
|
- C 134 204, 136 238, 138 278
|
|
|
- C 142 330, 146 374, 148 408
|
|
|
- C 152 454, 144 510, 138 548
|
|
|
- C 134 568, 130 578, 144 582
|
|
|
- C 160 586, 170 582, 176 556
|
|
|
- C 182 514, 190 452, 200 424
|
|
|
- C 210 452, 218 514, 224 556
|
|
|
- C 230 582, 240 586, 256 582
|
|
|
- C 270 578, 266 568, 262 548
|
|
|
- C 256 510, 248 454, 252 408
|
|
|
- C 254 374, 258 330, 262 278
|
|
|
- C 264 238, 266 204, 272 192
|
|
|
- C 276 186, 280 208, 284 238
|
|
|
- C 288 272, 292 306, 304 332
|
|
|
- C 316 356, 332 364, 338 358
|
|
|
- C 346 352, 340 340, 336 318
|
|
|
- C 332 288, 328 252, 326 220
|
|
|
- C 324 192, 320 168, 312 150
|
|
|
- C 304 136, 286 124, 262 114
|
|
|
- C 238 108, 220 102, 222 96
|
|
|
- C 226 86, 230 70, 228 56
|
|
|
- C 226 38, 214 28, 200 28 Z" fill="url(#simBodyGrad)" />
|
|
|
- <path d="M 148 126 C 136 134, 124 158, 132 192 C 136 208, 152 212, 158 196 C 164 174, 162 142, 148 126 Z" fill="none" stroke="rgba(92,74,61,0.18)" stroke-width="1.1" />
|
|
|
- <path d="M 252 126 C 264 134, 276 158, 268 192 C 264 208, 248 212, 242 196 C 236 174, 238 142, 252 126 Z" fill="none" stroke="rgba(92,74,61,0.18)" stroke-width="1.1" />
|
|
|
- <path d="M 148 372 C 170 402, 186 412, 200 406 C 214 412, 230 402, 252 372" fill="none" stroke="rgba(92,74,61,0.14)" stroke-width="1.1" />
|
|
|
- </g>
|
|
|
-
|
|
|
- <line :x1="SVG_C7_X" y1="92" :x2="SVG_C7_X" y2="390" stroke="rgba(99,102,241,0.35)" stroke-width="2" stroke-dasharray="8 6" stroke-linecap="round" />
|
|
|
+ <!-- 背部中线参考 -->
|
|
|
+ <line :x1="SVG_C7_X" :y1="SVG_C7_Y - 20" :x2="SVG_C7_X" :y2="SVG_S4_Y" stroke="rgba(99,102,241,0.35)" stroke-width="2" stroke-dasharray="8 6" stroke-linecap="round" />
|
|
|
+ <line
|
|
|
+ :x1="SVG_C7_X + 18"
|
|
|
+ :y1="SVG_C7_Y"
|
|
|
+ :x2="SVG_C7_X + 18"
|
|
|
+ :y2="currentSpineEndY"
|
|
|
+ stroke="rgba(14,165,233,0.9)"
|
|
|
+ stroke-width="3"
|
|
|
+ stroke-linecap="round"
|
|
|
+ />
|
|
|
+ <circle :cx="SVG_C7_X + 18" :cy="currentSpineEndY" r="5" fill="#0ea5e9" stroke="#fff" stroke-width="2" />
|
|
|
+ <text :x="SVG_C7_X + 28" :y="currentSpineEndY - 10" font-size="10" fill="#0369a1" font-weight="700" font-family="system-ui, sans-serif">
|
|
|
+ {{ fmtNum(currentSpineLengthCm) }} cm
|
|
|
+ </text>
|
|
|
+ <text :x="SVG_C7_X + 28" :y="currentSpineEndY + 6" font-size="10" fill="#0369a1" font-family="system-ui, sans-serif">脊柱终点</text>
|
|
|
|
|
|
<!-- C7 大椎穴标记 -->
|
|
|
<g :transform="`translate(${SVG_C7_X}, ${SVG_C7_Y})`">
|
|
|
@@ -328,6 +304,15 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <div v-if="unmappedSteps.length" class="visual-note">
|
|
|
+ <div class="visual-note-title">以下穴位未叠加到后背图</div>
|
|
|
+ <div class="visual-note-text">
|
|
|
+ <span v-for="s in unmappedSteps" :key="s.stepOrder" class="visual-note-item">
|
|
|
+ {{ s.stepOrder }}. {{ s.acupointName }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</el-card>
|
|
|
|
|
|
<!-- 推送数据 JSON -->
|
|
|
@@ -351,6 +336,7 @@ import { ElMessage } from 'element-plus'
|
|
|
import { getProfilePage } from '@/api/user'
|
|
|
import { getPlanPage, getUserPlanPage } from '@/api/plan'
|
|
|
import { getSimulationData } from '@/api/simulation'
|
|
|
+import bodyBackImage from '@/assets/acupoint-back-realistic.jpg'
|
|
|
|
|
|
const selectedUserId = ref(null)
|
|
|
const selectedMode = ref(null)
|
|
|
@@ -372,6 +358,7 @@ const bodyForm = reactive({
|
|
|
age: '',
|
|
|
shoulderWidth: '',
|
|
|
bodyHeight: '',
|
|
|
+ spineLength: '',
|
|
|
fingerWidth1: '',
|
|
|
fingerWidth15: '',
|
|
|
fingerWidth3: '',
|
|
|
@@ -408,23 +395,46 @@ const pushDataJson = computed(() => {
|
|
|
catch { return String(pushData.value) }
|
|
|
})
|
|
|
|
|
|
-// SVG coordinate mapping: C7 大椎穴 at (SVG_C7_X, SVG_C7_Y), scale 0.38 px/mm
|
|
|
+// SVG coordinate mapping: align mm offsets to the realistic back photo.
|
|
|
const SVG_C7_X = 200
|
|
|
// 参考经络示意图:大椎位于颈后下缘、后正中线处,视觉上略低于头颈连接点
|
|
|
const SVG_C7_Y = 112
|
|
|
-const SCALE = 0.38
|
|
|
+const SVG_S4_Y = 520
|
|
|
+const DEFAULT_SPINE_LENGTH_MM = 450
|
|
|
+const MAX_VISUAL_SPINE_LENGTH_MM = 600
|
|
|
+const FRONT_ACUPOINT_NAMES = new Set(['中脘穴', '气海穴', '关元穴'])
|
|
|
+
|
|
|
+const currentSpineLengthMm = computed(() => {
|
|
|
+ const spineLengthCm = Number(pushData.value?.bodyData?.spineLength ?? bodyForm.spineLength)
|
|
|
+ return Number.isFinite(spineLengthCm) && spineLengthCm > 0
|
|
|
+ ? spineLengthCm * 10
|
|
|
+ : DEFAULT_SPINE_LENGTH_MM
|
|
|
+})
|
|
|
+
|
|
|
+const currentSpineLengthCm = computed(() => currentSpineLengthMm.value / 10)
|
|
|
+const visualScale = (SVG_S4_Y - SVG_C7_Y) / MAX_VISUAL_SPINE_LENGTH_MM
|
|
|
+const currentSpineEndY = computed(() =>
|
|
|
+ Math.min(SVG_S4_Y, SVG_C7_Y + currentSpineLengthMm.value * visualScale)
|
|
|
+)
|
|
|
|
|
|
function bodyToSvg(offsetXmm, offsetYmm) {
|
|
|
return {
|
|
|
- x: SVG_C7_X + Number(offsetXmm) * SCALE,
|
|
|
- y: SVG_C7_Y + Number(offsetYmm) * SCALE,
|
|
|
+ x: SVG_C7_X + Number(offsetXmm) * visualScale,
|
|
|
+ y: SVG_C7_Y + Number(offsetYmm) * visualScale,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+function isBackVisualStep(step) {
|
|
|
+ if (step.offsetX == null || step.offsetY == null) return false
|
|
|
+ if (FRONT_ACUPOINT_NAMES.has(step.acupointName)) return false
|
|
|
+ const isZeroFallback = Number(step.offsetX) === 0 && Number(step.offsetY) === 0 && step.acupointName !== '大椎穴'
|
|
|
+ return !isZeroFallback
|
|
|
+}
|
|
|
+
|
|
|
const markers = computed(() => {
|
|
|
const steps = pushData.value?.scheme?.steps || []
|
|
|
return steps
|
|
|
- .filter((s) => s.offsetX != null && s.offsetY != null)
|
|
|
+ .filter(isBackVisualStep)
|
|
|
.map((s, idx) => {
|
|
|
const { x, y } = bodyToSvg(s.offsetX, s.offsetY)
|
|
|
const techniqueColors = {
|
|
|
@@ -452,6 +462,11 @@ const markers = computed(() => {
|
|
|
})
|
|
|
})
|
|
|
|
|
|
+const unmappedSteps = computed(() => {
|
|
|
+ const steps = pushData.value?.scheme?.steps || []
|
|
|
+ return steps.filter((s) => !isBackVisualStep(s))
|
|
|
+})
|
|
|
+
|
|
|
function modeLabel(modeType) {
|
|
|
const map = { 1: '一键艾灸', 2: '专业模式', 3: '自定义模式', 4: '延年圣手' }
|
|
|
return map[modeType] || modeType
|
|
|
@@ -538,6 +553,7 @@ function onUserSelect(userId) {
|
|
|
bodyForm.age = u.age ?? ''
|
|
|
bodyForm.shoulderWidth = u.shoulderWidth ?? ''
|
|
|
bodyForm.bodyHeight = u.bodyHeight ?? ''
|
|
|
+ bodyForm.spineLength = u.spineLength ?? ''
|
|
|
bodyForm.fingerWidth1 = u.fingerWidth1 ?? ''
|
|
|
bodyForm.fingerWidth15 = u.fingerWidth15 ?? ''
|
|
|
bodyForm.fingerWidth3 = u.fingerWidth3 ?? ''
|
|
|
@@ -708,6 +724,11 @@ watch([selectedMode, selectedEfficacy, filteredPlans], ([mode, eff, list]) => {
|
|
|
width: 100%;
|
|
|
max-width: 420px;
|
|
|
border-radius: 14px;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #f8fafc;
|
|
|
+}
|
|
|
+.body-photo {
|
|
|
+ pointer-events: none;
|
|
|
}
|
|
|
|
|
|
.marker-legend {
|
|
|
@@ -799,6 +820,31 @@ watch([selectedMode, selectedEfficacy, filteredPlans], ([mode, eff, list]) => {
|
|
|
color: #64748b;
|
|
|
margin-top: 2px;
|
|
|
}
|
|
|
+.visual-note {
|
|
|
+ margin-top: 12px;
|
|
|
+ padding: 10px 12px;
|
|
|
+ border: 1px solid #fde68a;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #fffbeb;
|
|
|
+}
|
|
|
+.visual-note-title {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #92400e;
|
|
|
+}
|
|
|
+.visual-note-text {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 6px;
|
|
|
+ margin-top: 6px;
|
|
|
+}
|
|
|
+.visual-note-item {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #a16207;
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #fef3c7;
|
|
|
+}
|
|
|
|
|
|
.json-block {
|
|
|
background: #1e1e1e;
|