2025-05-27 15:46:31 +08:00

5594 lines
174 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>按摩计划管理</title>
<script src="{{ url_for('static', filename='js/socketio/socket.io.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/interact.min.js') }}"></script>
<style>
:root {
--primary-color: #a75cd0;
--primary-light: #9f3ee8;
--background-color: #f5f5f5;
--card-background: rgba(255, 255, 255, 0.8);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
color: #333;
min-height: 100vh;
height: 100vh; /* 确保body占满整个视口高度 */
display: flex; /* 使用flex布局 */
flex-direction: column; /* 垂直排列 */
overflow: hidden; /* 防止页面滚动 */
}
.container {
/* 移除max-width限制使其能够占满iframe大小 */
width: 100%;
height: 100%; /* 使容器占满整个页面高度 */
margin: 0;
display: flex;
flex-direction: column; /* 垂直布局 */
overflow: hidden; /* 防止容器产生滚动 */
padding-bottom: 15px;
}
.filter-bar {
border-radius: 15px;
padding: 15px 20px 0 20px;
}
.filter-toggle {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.filter-toggle button {
display: flex;
align-items: center;
background: #ffffffdb;
border: none;
color: var(--primary-color);
font-size: 18px;
font-weight: bold;
padding: 5px 8px;
border-radius: 8px;
cursor: pointer;
}
.filter-toggle .toggle-icon {
width: 18px;
height: 18px;
margin-left: 8px;
stroke: var(--primary-color);
transition: transform 0.3s ease;
}
.filter-toggle.active .toggle-icon {
transform: rotate(180deg);
}
/* 刷新按钮样式 */
.refresh-toggle {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
margin-left: 10px;
}
.refresh-toggle button {
display: flex;
align-items: center;
background: #ffffffdb;
border: none;
color: var(--primary-color);
font-size: 18px;
font-weight: bold;
padding: 5px 8px;
border-radius: 8px;
cursor: pointer;
}
/* 创建按钮样式 */
.create-toggle {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
margin-left: auto; /* 推到右侧 */
}
.create-toggle button {
display: flex;
align-items: center;
background: var(--primary-color);
border: none;
color: white;
font-size: 16px;
font-weight: 500;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
box-shadow: 0 2px 6px rgba(138, 43, 226, 0.3);
transition: all 0.2s ease;
}
.create-toggle button:hover {
background: #8a2be2;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(138, 43, 226, 0.4);
}
.create-toggle .create-icon {
width: 16px;
height: 16px;
margin-right: 8px;
stroke: white;
}
.refresh-toggle .refresh-icon {
width: 18px;
height: 18px;
margin-left: 8px;
stroke: var(--primary-color);
transition: transform 0.3s ease;
}
/* 点击刷新按钮时添加动画效果类 */
.refresh-toggle .refresh-icon.rotating {
animation: rotate360 0.8s ease;
}
@keyframes rotate360 {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 加载中动画样式 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
padding: 3rem 1rem;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(167, 92, 208, 0.1);
border-radius: 50%;
border-top: 4px solid var(--primary-color);
animation: spin 1.2s linear infinite;
margin-bottom: 15px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-text {
font-size: 16px;
color: var(--primary-color);
font-weight: 500;
text-align: center;
opacity: 0.8;
}
/* 错误提示样式 */
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
padding: 3rem 1rem;
}
.error-icon {
width: 60px;
height: 60px;
margin-bottom: 15px;
color: #e74c3c;
}
.error-message {
font-size: 16px;
color: #e74c3c;
font-weight: 500;
text-align: center;
margin-bottom: 15px;
}
.retry-btn {
padding: 8px 20px;
background: var(--primary-color);
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
font-size: 15px;
box-shadow: 0 2px 6px rgba(167, 92, 208, 0.3);
transition: all 0.2s ease;
}
.retry-btn:hover {
background: #8a2be2;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(167, 92, 208, 0.4);
}
.filter-content {
padding: 15px 5px 10px 5px;
transition: max-height 0.3s ease, opacity 0.3s ease;
overflow: hidden;
max-height: 0;
opacity: 0;
}
.filter-content.active {
max-height: 300px;
opacity: 1;
}
.filter-group {
margin-bottom: 15px;
}
.filter-group:last-child {
margin-bottom: 0;
}
.filter-group label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
display: block;
font-weight: 500;
}
.filter-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 8px;
}
.filter-btn {
padding: 6px 15px;
border: 1px solid rgba(167, 92, 208, 0.2);
border-radius: 20px;
background-color: rgba(255, 255, 255, 0.7);
color: #555;
cursor: pointer;
transition: all 0.2s ease;
font-size: 13px;
}
.filter-btn:hover {
background-color: rgba(167, 92, 208, 0.1);
border-color: rgba(167, 92, 208, 0.3);
}
.filter-btn.active {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
box-shadow: 0 2px 6px rgba(167, 92, 208, 0.3);
}
.plans-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 15px;
flex: 1; /* 添加flex:1使其占据剩余空间 */
overflow-y: auto; /* 允许垂直滚动 */
padding: 0 15px 0;
align-content: flex-start; /* 修复筛选后垂直间隙问题 */
grid-auto-rows: min-content; /* 修复筛选后垂直间隙问题 */
}
.detail-view {
display: none; /* 初始状态为隐藏 */
background: var(--card-background);
border-radius: 15px;
padding: 16px;
backdrop-filter: blur(10px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
flex: 1; /* 使其占据容器的剩余空间 */
overflow: hidden; /* 防止整体滚动 */
margin-bottom: -15px;
}
/* 当detail-view显示时应用的样式 */
.detail-view.active {
display: flex; /* 使用flex布局 */
flex-direction: column; /* 垂直排列内容 */
}
.plan-card {
background: var(--card-background);
border-radius: 15px;
padding: 1.5rem;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.3);
position: relative;
}
.plan-card-container {
width: 100%;
height: auto;
min-height: 100px;
margin: 0;
position: relative;
transition: opacity 0.3s ease;
margin-top: 5px;
}
.plan-card-placeholder {
width: 100%;
transition: all 0.5s ease;
animation: pulse 2s infinite ease-in-out;
position: relative;
overflow: hidden;
}
/* 添加脉冲动画效果给占位符 */
@keyframes pulse {
0% {
opacity: 0.6;
}
50% {
opacity: 0.8;
}
100% {
opacity: 0.6;
}
}
/* 添加平滑过渡效果 */
.plan-card {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.plan-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 15px rgba(138, 43, 226, 0.2);
}
.plan-card h3 {
color: var(--primary-color);
margin-bottom: 1rem;
font-size: 1.2rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
margin-right: 30px;
}
.plan-info {
font-size: 14px;
color: #666;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
.delete-row-btn {
background: transparent;
color: #777;
border: none;
}
.delete-row-btn {
background: transparent;
color: #777;
border: none;
padding: 0.3rem;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.delete-row-btn:hover {
background: rgba(255, 109, 109, 0.1);
color: #ff6d6d;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
position: relative;
height: 20px;
}
.back-btn {
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
background: transparent;
color: var(--primary-color);
border: none;
padding: 0.5rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
font-size: 1rem;
}
.back-btn:hover {
background: rgba(138, 43, 226, 0.1);
}
.back-btn svg {
width: 20px;
height: 20px;
margin-right: 0.5rem;
}
.action-buttons {
display: flex;
gap: 0.5rem;
right: 0;
position: absolute;
}
.choose-btn,
.edit-btn,
.save-btn,
.cancel-btn {
/* background: var(--primary-color); */
background: linear-gradient(
to bottom,
rgb(212, 96, 241, 0.8),
rgba(145, 66, 197, 0.8)
);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.edit-btn:hover,
.save-btn:hover {
background: var(--primary-light);
}
.cancel-btn {
background: #666;
}
.cancel-btn:hover {
background: #777;
}
#detailTitle {
margin: 0 auto;
text-align: center;
color: var(--primary-color);
font-size: 20px;
}
.table-box {
overflow-y: auto; /* 改为auto只在需要时显示滚动条 */
overflow-x: auto; /* 水平方向也设为auto */
/* 移除固定的max-height改为flex-grow占据剩余空间 */
flex: 1; /* 占据剩余空间 */
width: 100%;
}
.task-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.task-table th,
.task-table td {
padding: 10px;
text-align: left;
border-bottom: 1px solid rgba(138, 43, 226, 0.2);
}
.task-table th {
background: rgba(138, 43, 226, 0.1);
color: var(--primary-color);
white-space: nowrap;
}
.task-table tr:hover {
background: rgba(138, 43, 226, 0.05);
}
.task-table input[type="number"] {
width: 80px;
padding: 0.3rem;
border: 1px solid rgba(138, 43, 226, 0.2);
border-radius: 4px;
background: rgba(255, 255, 255, 0.9);
}
.input-group {
position: relative;
display: flex;
align-items: center;
}
.reset-auto {
background: rgba(138, 43, 226, 0.1);
color: var(--primary-color);
border: none;
border-radius: 4px;
padding: 0.2rem 0.4rem;
margin-left: 0.3rem;
cursor: pointer;
font-size: 0.8rem;
transition: all 0.2s ease;
}
.reset-auto:hover {
background: rgba(138, 43, 226, 0.2);
}
.reset-auto:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.task-table select {
padding: 0.3rem;
border: 1px solid rgba(138, 43, 226, 0.2);
border-radius: 4px;
background: rgba(255, 255, 255, 0.9);
min-width: 120px;
}
.task-table input[disabled],
.task-table select[disabled] {
background: rgba(0, 0, 0, 0.05);
cursor: not-allowed;
}
.error-message {
color: red;
text-align: center;
margin: 1rem 0;
}
.success-message {
color: green;
text-align: center;
margin: 1rem 0;
}
.badge {
display: inline-block;
padding: 6px 10px;
border-radius: 15px;
background: rgba(138, 43, 226, 0.1);
color: var(--primary-color);
margin-right: 10px;
}
.massage-type {
margin-bottom: 12px;
background: rgba(138, 43, 226, 0.05);
border-radius: 10px;
}
.massage-type-title {
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
}
.massage-type p {
margin: 0.3rem 0;
color: #666;
}
.more-options {
position: absolute;
top: 1rem;
right: 1rem;
cursor: pointer;
border-radius: 50%;
transition: background-color 0.3s ease;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
}
.more-options:hover {
background: rgba(138, 43, 226, 0.1);
}
.more-options svg {
width: 20px;
height: 20px;
fill: var(--primary-color);
}
.dropdown-menu {
position: absolute;
top: calc(100% + 5px);
right: 0;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
display: none;
z-index: 3;
}
.dropdown-menu.show {
display: block;
}
.dropdown-item {
padding: 0.8rem 1.2rem;
color: #333;
cursor: pointer;
transition: background-color 0.3s ease;
white-space: nowrap;
display: flex;
align-items: center;
gap: 0.5rem;
}
.dropdown-item:hover {
background: rgba(138, 43, 226, 0.1);
}
.dropdown-item svg {
width: 16px;
height: 16px;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
backdrop-filter: blur(3px);
animation: fadeIn 0.3s ease;
opacity: 1;
transition: opacity 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.modal {
background: rgba(255, 255, 255, 0.9);
padding: 2rem;
border-radius: 15px;
width: 90%;
max-width: 500px;
position: relative;
box-shadow: 0 8px 30px rgba(138, 43, 226, 0.3);
transform: translateY(0);
animation: slideUp 0.3s ease;
backdrop-filter: blur(10px);
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
margin-bottom: 1.5rem;
color: var(--primary-color);
padding-bottom: 0.8rem;
border-bottom: 1px solid rgba(138, 43, 226, 0.1);
text-align: center;
}
.modal-header h3 {
margin: 0;
font-size: 1.4rem;
font-weight: 600;
}
.modal-content {
/* margin-bottom: 1.5rem; */
}
.modal-input {
width: 100%;
padding: 0.8rem;
border: 1px solid rgba(138, 43, 226, 0.2);
border-radius: 8px;
/* margin-bottom: 1rem; */
font-size: 1rem;
background: rgba(255, 255, 255, 0.8);
transition: all 0.3s ease;
}
.modal-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(138, 43, 226, 0.1);
}
.modal-input.error {
border-color: #e74c3c;
box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.1);
}
.error-text {
color: #e74c3c;
font-size: 0.9rem;
margin-top: -0.5rem;
margin-bottom: 1rem;
display: none;
animation: shakeError 0.5s ease;
}
@keyframes shakeError {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
50% {
transform: translateX(5px);
}
75% {
transform: translateX(-5px);
}
}
.modal-buttons {
display: flex;
justify-content: center;
gap: 1rem;
padding-top: 0.5rem;
}
.modal-btn {
padding: 0.8rem 1.8rem;
border: none;
border-radius: 25px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.95rem;
font-weight: 500;
min-width: 100px;
}
.modal-btn.primary {
background: var(--primary-color);
color: white;
box-shadow: 0 4px 10px rgba(138, 43, 226, 0.3);
}
.modal-btn.primary:hover {
background: #8a2be2;
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(138, 43, 226, 0.4);
}
.modal-btn.primary:active {
transform: translateY(0);
box-shadow: 0 2px 5px rgba(138, 43, 226, 0.3);
}
.modal-btn.secondary {
background: #f1f1f1;
color: #555;
}
.modal-btn.secondary:hover {
background: #e5e5e5;
transform: translateY(-2px);
}
.modal-btn.secondary:active {
transform: translateY(0);
}
/* 删除按钮样式 */
.modal-btn.delete {
background: #e74c3c;
color: white;
box-shadow: 0 4px 10px rgba(231, 76, 60, 0.3);
}
.modal-btn.delete:hover {
background: #c0392b;
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(231, 76, 60, 0.4);
}
.modal-btn.delete:active {
transform: translateY(0);
box-shadow: 0 2px 5px rgba(231, 76, 60, 0.3);
}
/* 删除警告图标样式 */
.delete-warning {
display: flex;
justify-content: center;
margin-bottom: 15px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
/* 固定高度的模态框内容,优化用户体验 */
.modal p {
text-align: center;
font-size: 1.05rem;
color: #555;
line-height: 1.5;
margin: 1.5rem 0;
}
.fixed-name-part {
background: rgba(138, 43, 226, 0.1);
padding: 0.8rem;
border-radius: 8px 0 0 8px;
color: var(--primary-color);
font-weight: 500;
border: 1px solid rgba(138, 43, 226, 0.2);
border-right: none;
display: block; /* 确保显示 */
white-space: nowrap; /* 防止换行 */
}
.input-wrapper {
margin-bottom: 1rem;
display: flex; /* 确保flex布局 */
align-items: center; /* 垂直居中 */
}
.input-wrapper .modal-input {
border-radius: 0 8px 8px 0;
border-left: none;
}
.input-wrapper .modal-input:focus {
outline: none;
border-color: rgba(138, 43, 226, 0.2);
}
/* 修改滚动条样式 */
.plans-grid::-webkit-scrollbar,
.detail-view::-webkit-scrollbar {
width: 6px;
}
.plans-grid::-webkit-scrollbar-thumb,
.detail-view::-webkit-scrollbar-thumb {
background-color: rgba(138, 43, 226, 0.2);
border-radius: 3px;
}
.plans-grid::-webkit-scrollbar-track,
.detail-view::-webkit-scrollbar-track {
background-color: transparent;
}
.filter-info {
width: 100%;
margin-bottom: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
background: var(--card-background);
border-radius: 8px;
}
.filter-tags {
display: flex;
gap: 0.5rem;
}
.filter-tag {
background: var(--primary-color);
color: white;
padding: 0.3rem 0.8rem;
border-radius: 15px;
font-size: 0.9rem;
}
.clear-filter {
background: none;
border: 1px solid var(--primary-color);
color: var(--primary-color);
padding: 0.3rem 0.8rem;
border-radius: 15px;
cursor: pointer;
transition: all 0.3s ease;
}
.clear-filter:hover {
background: var(--primary-color);
color: white;
}
.no-data-message {
text-align: center;
padding: 20px;
color: #666;
background-color: #f8f8f8;
border-radius: 15px;
}
.plan-input {
border: none;
width: 150px;
padding: 0.3rem;
border: 1px solid rgba(138, 43, 226, 0.2);
border-radius: 4px;
background: rgba(255, 255, 255, 0.9);
}
.massage-type {
margin-bottom: 1rem;
padding: 1rem;
background: rgba(138, 43, 226, 0.05);
border-radius: 10px;
}
.intro-text {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
margin: 6px 0;
font-size: 12px;
color: #666;
transition: all 0.3s ease;
}
.intro-text.expanded {
-webkit-line-clamp: unset;
}
.show-more-btn {
background: none;
border: none;
color: var(--primary-color);
cursor: pointer;
padding: 6px 10px;
margin-bottom: 10px;
border-radius: 4px;
transition: all 0.3s ease;
}
.show-more-btn:hover {
background: rgba(138, 43, 226, 0.1);
}
/* 弹窗的背景遮罩 */
.pwd-modal {
/* 默认隐藏 */
display: none;
position: fixed;
z-index: 1000;
/* 确保弹窗在最顶层 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
/* 背景透明 */
justify-content: center;
align-items: center;
}
.modal-content {
/* background-color: rgba(255, 255, 255, 0.7); */
/* 背景透明度 */
/* backdrop-filter: blur(5px); */
/* 背景模糊效果 */
padding: 10px 20px;
border-radius: 15px;
text-align: center;
width: 100%;
/* box-shadow: 0px 0px 24px rgba(255, 255, 255, 0.65); */
/* 与其他元素一致的阴影效果 */
}
.modal-content #pwd-input {
padding: 16px 10px;
border-radius: 10px;
border: 1px solid #dedede;
background-color: #f5f5f5;
outline: none;
}
/* 按钮样式 */
.model-btn button {
margin: 5px;
padding: 10px 25px;
border: none;
border-radius: 10px;
background: linear-gradient(
to right bottom,
rgb(212, 96, 241),
rgba(145, 66, 197, 0.5)
);
/* 紫色渐变 */
color: white;
font-size: 16px;
cursor: pointer;
transition: background 0.3s ease;
}
.model-btn button:active {
background: linear-gradient(
to right bottom,
rgba(145, 66, 197, 1),
rgb(212, 96, 241)
);
/* 悬停时加深渐变 */
}
/* 弹窗的背景遮罩 */
.popup-modal {
display: none;
/* 默认隐藏 */
position: fixed;
z-index: 1000;
/* 确保弹窗在最顶层 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
/* 背景透明 */
justify-content: center;
align-items: center;
}
/* 弹窗的内容 */
.popup-content {
background-color: rgba(255, 255, 255, 0.7);
/* 背景透明度 */
backdrop-filter: blur(5px);
/* 背景模糊效果 */
padding: 20px;
border-radius: 15px;
text-align: center;
width: 300px;
box-shadow: 0px 0px 24px rgba(255, 255, 255, 0.65);
/* 与其他元素一致的阴影效果 */
}
#popup-message {
padding: 20px 0;
}
/* 按钮样式 */
#popup-buttons button {
margin: 5px;
padding: 10px 25px;
border: none;
border-radius: 10px;
background: linear-gradient(
to right bottom,
rgb(212, 96, 241),
rgba(145, 66, 197, 0.5)
);
/* 紫色渐变 */
color: white;
font-size: 16px;
cursor: pointer;
transition: background 0.3s ease;
}
#popup-buttons button:active {
background: linear-gradient(
to right bottom,
rgba(145, 66, 197, 1),
rgb(212, 96, 241)
);
/* 悬停时加深渐变 */
}
.edit-point-btn,
.delete-row-btn {
background: transparent;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0.3rem;
border-radius: 4px;
transition: all 0.3s ease;
}
.edit-point-btn {
color: var(--primary-color);
}
.edit-point-btn:hover {
background-color: rgba(138, 43, 226, 0.1);
}
.delete-row-btn {
color: #777;
}
.delete-row-btn:hover {
background: rgba(255, 109, 109, 0.1);
color: #ff6d6d;
}
.visualization-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
backdrop-filter: blur(3px);
}
.visualization-content {
position: relative;
width: 600px;
max-width: 95%;
margin: 50px auto 30px;
background: white;
border-radius: 15px;
padding: 20px;
box-shadow: 0 8px 30px rgba(138, 43, 226, 0.3);
overflow-y: scroll;
}
.visualization-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
border-bottom: 1px solid rgba(138, 43, 226, 0.1);
padding-bottom: 12px;
}
.visualization-header h3 {
margin: 0;
color: var(--primary-color);
font-size: 20px;
}
.visualization-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #888;
transition: color 0.3s ease;
}
.visualization-close:hover {
color: #333;
}
.visualization-body {
display: flex;
flex-direction: column;
gap: 15px;
}
.control-panel {
width: 100%;
background: white;
border-radius: 10px;
padding: 0;
height: 340px;
overflow-y: auto;
position: relative;
}
.mode-selector {
margin-bottom: 25px;
}
.mode-selector h4 {
margin: 0 0 15px 0;
color: #333;
font-size: 16px;
font-weight: 500;
}
.mode-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.mode-button {
padding: 10px 15px;
border: none;
border-radius: 8px;
background-color: #f0f0f0;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
text-align: center;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
width: calc(25% - 8px);
}
.mode-button:hover {
background-color: #e8e8e8;
transform: translateY(-2px);
}
.mode-button.active {
background: linear-gradient(
to bottom,
rgba(186, 137, 238, 0.9),
rgba(167, 92, 208, 0.9)
);
color: white;
box-shadow: 0 2px 5px rgba(138, 43, 226, 0.3);
}
#pathControls {
margin-bottom: 20px;
}
.control-group {
margin-bottom: 20px;
background: #f9f6fd;
padding: 15px;
border-radius: 8px;
}
.control-group label {
display: block;
/* margin-bottom: 10px; */
font-size: 14px;
color: #555;
font-weight: 500;
}
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 8px;
border-radius: 10px;
background: #e0e0e0;
outline: none;
margin: 20px 0 10px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 22px;
height: 22px;
border-radius: 50%;
background: linear-gradient(to bottom, #ba89ee, #a75cd0);
cursor: pointer;
border: 2px solid white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.direction-selector {
display: flex;
gap: 10px;
}
.direction-button {
flex: 1;
padding: 10px;
border: none;
background-color: #f0f0f0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-top: 15px;
}
.direction-button:hover {
background-color: #e8e8e8;
transform: translateY(-2px);
}
.direction-button.active {
background: linear-gradient(
to bottom,
rgba(186, 137, 238, 0.9),
rgba(167, 92, 208, 0.9)
);
color: white;
box-shadow: 0 2px 5px rgba(138, 43, 226, 0.3);
}
.selected-points {
background-color: #f9f6fd;
padding: 15px;
border-radius: 8px;
margin: 15px 0;
border: 1px dashed rgba(138, 43, 226, 0.2);
}
.selected-points h4 {
margin: 0 0 10px 0;
color: #333;
font-size: 16px;
font-weight: 500;
}
.point-info {
margin: 10px 0;
color: #555;
font-size: 14px;
padding: 8px;
background: white;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.clear-button {
margin-top: 10px;
padding: 8px 15px;
border: none;
border-radius: 8px;
background-color: #ff7e7e;
color: white;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
}
.clear-button:hover {
background-color: #ff5e5e;
transform: translateY(-2px);
}
.confirm-button {
background: linear-gradient(to bottom, #ba89ee, #a75cd0);
color: white;
width: 100%;
padding: 12px;
font-size: 16px;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 5px;
box-shadow: 0 4px 10px rgba(138, 43, 226, 0.3);
}
.confirm-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(138, 43, 226, 0.4);
}
/* 修改样式部分 */
.button-group {
display: flex;
gap: 10px;
width: 560px;
bottom: 50px;
}
.clear-button {
flex: 1;
margin-top: 0;
padding: 12px;
border: none;
border-radius: 8px;
background-color: #ff7e7e;
color: white;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
box-shadow: 0 4px 10px rgba(255, 126, 126, 0.3);
}
.clear-button:hover {
background-color: #ff5e5e;
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(255, 126, 126, 0.4);
}
.confirm-button {
flex: 2;
background: linear-gradient(to bottom, #ba89ee, #a75cd0);
color: white;
padding: 12px;
font-size: 16px;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 0;
box-shadow: 0 4px 10px rgba(138, 43, 226, 0.3);
}
.control-header {
display: flex;
justify-content: space-between;
align-items: center;
/* margin-bottom: 10px; */
}
/* 开关样式 */
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
margin-left: 10px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.switch .slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #f0f0f0;
transition: 0.4s;
border-radius: 24px;
}
.switch .slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: 0.4s;
border-radius: 50%;
}
.switch input:checked + .slider {
background-color: #a75cd0;
}
.switch input:checked + .slider:before {
transform: translateX(26px);
}
.switch-label {
position: absolute;
left: -35px;
top: 50%;
transform: translateY(-50%);
font-size: 12px;
color: #666;
}
/* 更新滑块样式以适应新布局 */
/* .control-group input[type="range"] {
margin-top: 10px;
} */
#detail-content {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.detail-content {
display: flex;
flex-direction: column;
flex: 1; /* 使其占据父容器的剩余空间 */
overflow: hidden; /* 防止外部滚动 */
}
/* 添加新的样式 */
.add-row-tr {
background-color: rgba(138, 43, 226, 0.03);
cursor: pointer;
transition: all 0.3s ease;
}
.add-row-tr:hover {
background-color: rgba(138, 43, 226, 0.1);
}
.add-row-icon {
display: flex;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
border-radius: 50%;
background-color: var(--primary-light);
margin: 0 auto;
color: white;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(138, 43, 226, 0.3);
}
.add-row-icon:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(138, 43, 226, 0.4);
}
/* 拖拽相关样式 */
.drag-handle {
cursor: grab;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
color: #777;
touch-action: none;
width: 24px;
height: 24px;
}
.drag-handle:hover,
.drag-handle:active {
background: rgba(138, 43, 226, 0.1);
color: var(--primary-color);
}
.drag-handle svg {
width: 18px;
height: 18px;
stroke-width: 1.5;
}
.draggable-row.dragging {
opacity: 0.9;
background: rgba(138, 43, 226, 0.1);
cursor: grabbing;
z-index: 100;
position: relative;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
.draggable-row.drop-target {
border-top: 2px solid var(--primary-color);
background: rgba(138, 43, 226, 0.05);
}
.draggable-row {
touch-action: none;
user-select: none;
transition: background 0.2s ease;
}
.dragPlaceholder {
height: 3px;
background-color: var(--primary-color);
margin: 0;
padding: 0;
transition: all 0.2s ease;
}
/* 为表格行添加动画效果 */
#task-table-body tr {
transition: transform 0.2s ease, background-color 0.2s ease;
}
/* 仅在拖拽模式下禁用拖拽的行的动画,其他行保持动画效果 */
#task-table-body tr.dragging {
transition: none;
}
.hidden {
display: none !important;
}
.fixed-name-part {
background: rgba(138, 43, 226, 0.1);
padding: 0.8rem;
border-radius: 8px 0 0 8px;
color: var(--primary-color);
font-weight: 500;
border: 1px solid rgba(138, 43, 226, 0.2);
border-right: none;
display: block; /* 确保显示 */
white-space: nowrap; /* 防止换行 */
}
/* 前缀显示区域样式 */
.prefix-display {
text-align: center;
margin-bottom: 12px;
padding: 10px;
background: rgba(138, 43, 226, 0.05);
border-radius: 8px;
border: 1px dashed rgba(138, 43, 226, 0.2);
}
.prefix-part {
color: var(--primary-color);
font-weight: 500;
padding: 3px 8px;
background: rgba(138, 43, 226, 0.1);
border-radius: 4px;
}
.prefix-separator {
color: #999;
font-weight: 400;
}
.input-wrapper {
margin-bottom: 1rem;
display: flex; /* 确保flex布局 */
align-items: center; /* 垂直居中 */
}
/* 创建新疗程模态框样式 */
.create-form {
/* margin-bottom: 1rem; */
}
.form-group {
/* margin-bottom: 1.2rem; */
}
.form-group label {
display: block;
font-size: 15px;
font-weight: 500;
color: #555;
margin-bottom: 8px;
}
.select-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
}
.select-btn {
padding: 6px 12px;
border: 1px solid rgba(138, 43, 226, 0.2);
border-radius: 20px;
background-color: rgba(255, 255, 255, 0.7);
color: #555;
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
}
.select-btn:hover {
background-color: rgba(138, 43, 226, 0.1);
border-color: rgba(138, 43, 226, 0.3);
}
.select-btn.active {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
box-shadow: 0 2px 6px rgba(138, 43, 226, 0.3);
}
.name-preview {
font-size: 13px;
margin-top: 5px;
padding: 8px;
background: rgba(138, 43, 226, 0.05);
border-radius: 6px;
color: #666;
}
.preview-content {
font-weight: 500;
color: var(--primary-color);
}
/* 密码输入框样式 */
#pwd-input {
width: 100%;
padding: 0.8rem;
border: 1px solid rgba(138, 43, 226, 0.2);
border-radius: 8px;
margin: 1rem 0;
font-size: 1rem;
background: rgba(255, 255, 255, 0.8);
transition: all 0.3s ease;
}
#pwd-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(138, 43, 226, 0.1);
}
</style>
</head>
<body>
<div class="container">
<div class="filter-bar">
<div
class="filter-header"
style="
display: flex;
justify-content: flex-start;
align-items: center;
"
>
<div class="filter-toggle">
<button id="filterToggleBtn">
<span>筛选</span>
<svg
class="toggle-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
</div>
<div class="refresh-toggle">
<button id="refreshBtn">
<span>刷新</span>
<svg
class="refresh-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="23 4 23 10 17 10"></polyline>
<polyline points="1 20 1 14 7 14"></polyline>
<path
d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"
></path>
</svg>
</button>
</div>
<div class="create-toggle">
<button id="createBtn">
<svg
class="create-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
<span>创建新疗程</span>
</button>
</div>
</div>
<div class="filter-content" id="filterContent">
<div class="filter-group">
<label>按摩部位</label>
<div class="filter-buttons">
<button
class="filter-btn active"
data-filter="body_part"
data-value="all"
>
全部
</button>
<button
class="filter-btn"
data-filter="body_part"
data-value="back"
>
背部
</button>
<button
class="filter-btn"
data-filter="body_part"
data-value="belly"
>
腹部
</button>
<button
class="filter-btn"
data-filter="body_part"
data-value="shoulder"
>
肩膀
</button>
<button
class="filter-btn"
data-filter="body_part"
data-value="waist"
>
腰部
</button>
<button
class="filter-btn"
data-filter="body_part"
data-value="leg"
>
腿部
</button>
</div>
</div>
<div class="filter-group">
<label>按摩头</label>
<div class="filter-buttons">
<button
class="filter-btn active"
data-filter="choose_task"
data-value="all"
>
全部
</button>
<button
class="filter-btn"
data-filter="choose_task"
data-value="thermotherapy"
>
深部热疗
</button>
<button
class="filter-btn"
data-filter="choose_task"
data-value="shockwave"
>
点阵按摩
</button>
<button
class="filter-btn"
data-filter="choose_task"
data-value="ball"
>
全能滚珠
</button>
<button
class="filter-btn"
data-filter="choose_task"
data-value="finger"
>
指疗通络
</button>
<button
class="filter-btn"
data-filter="choose_task"
data-value="roller"
>
滚滚刺疗
</button>
<button
class="filter-btn"
data-filter="choose_task"
data-value="stone"
>
温砭舒揉
</button>
<button
class="filter-btn"
data-filter="choose_task"
data-value="ion"
>
离子光灸
</button>
<button
class="filter-btn"
data-filter="choose_task"
data-value="heat"
>
能量热疗
</button>
<button
class="filter-btn"
data-filter="choose_task"
data-value="spheres"
>
天球滚捏
</button>
</div>
</div>
</div>
</div>
<div class="plans-grid" id="plansGrid"></div>
<div class="detail-view" id="detailView">
<div class="detail-header">
<button class="back-btn" onclick="showPlansList()">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M19 12H5M12 19l-7-7 7-7" />
</svg>
返回列表
</button>
<div class="action-buttons">
<button class="choose-btn" onclick="choosePlan()">
选择此方案
</button>
<button class="edit-btn" onclick="editPlan()">编辑</button>
<button class="save-btn" onclick="savePlan()" style="display: none">
保存
</button>
<button
class="cancel-btn"
onclick="cancelEdit()"
style="display: none"
>
取消
</button>
</div>
</div>
<h2 id="detailTitle"></h2>
<div class="detail-content" id="detailContent"></div>
</div>
</div>
<div class="modal-overlay" id="copyModal">
<div class="modal">
<div class="modal-header">
<h3>复制按摩疗程</h3>
</div>
<div class="modal-content">
<input
type="text"
class="modal-input"
id="newPlanName"
placeholder="请输入新的疗程名称"
/>
<div class="error-text" id="nameError">该名称已存在,请重新输入</div>
</div>
<div class="modal-buttons">
<button class="modal-btn secondary" onclick="closeCopyModal()">
取消
</button>
<button class="modal-btn primary" onclick="confirmCopy()">
确认
</button>
</div>
</div>
</div>
<div class="modal-overlay" id="deleteModal">
<div class="modal">
<div class="modal-header">
<h3>删除按摩疗程</h3>
</div>
<div class="modal-content">
<div class="delete-warning">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="#e74c3c"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
width="60"
height="60"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
</div>
<p>确定要删除这个疗程吗?<br /><strong>此操作无法撤销。</strong></p>
</div>
<div class="modal-buttons">
<button class="modal-btn secondary" onclick="closeDeleteModal()">
取消
</button>
<button class="modal-btn delete" onclick="confirmDelete()">
确认删除
</button>
</div>
</div>
</div>
<!-- 创建新疗程模态框 -->
<div class="modal-overlay" id="createModal">
<div class="modal">
<div class="modal-header">
<h3>创建新疗程</h3>
</div>
<div class="modal-content">
<div class="create-form">
<!-- 按摩部位选择 -->
<div class="form-group">
<label>按摩部位</label>
<div class="select-buttons" id="bodyPartButtons">
<button class="select-btn active" data-value="back">
背部
</button>
<button class="select-btn" data-value="belly">腹部</button>
<button class="select-btn" data-value="shoulder">肩膀</button>
<button class="select-btn" data-value="waist">腰部</button>
<button class="select-btn" data-value="leg">腿部</button>
</div>
</div>
<!-- 按摩头选择 -->
<div class="form-group">
<label>按摩头</label>
<div class="select-buttons" id="massageHeadButtons">
<button class="select-btn active" data-value="thermotherapy">深部热疗</button>
<button class="select-btn" data-value="shockwave">点阵按摩</button>
<button class="select-btn" data-value="ball">全能滚珠</button>
<button class="select-btn" data-value="finger">指疗通络</button>
<button class="select-btn" data-value="roller">滚滚刺疗</button>
<button class="select-btn" data-value="stone">温砭舒揉</button>
<button class="select-btn" data-value="ion">离子光灸</button>
<button class="select-btn" data-value="heat">能量热疗</button>
<button class="select-btn" data-value="spheres">天球滚捏</button>
</div>
</div>
<!-- 疗程名称输入 -->
<div class="form-group">
<label>疗程名称</label>
<input
type="text"
class="modal-input"
id="createPlanName"
placeholder="请输入疗程名称"
/>
<div class="name-preview" id="planNamePreview">
<span class="preview-content"></span>
</div>
<div class="error-text" id="createNameError">
该名称已存在,请重新输入
</div>
</div>
</div>
</div>
<div class="modal-buttons">
<button class="modal-btn secondary" onclick="closeCreateModal()">
取消
</button>
<button class="modal-btn primary" onclick="confirmCreatePlan()">
创建
</button>
</div>
</div>
</div>
<div id="pwd-modal" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h3>输入密码</h3>
</div>
<div class="modal-content">
<p>请输入操作密码以继续</p>
<input
id="pwd-input"
type="password"
class="modal-input"
placeholder="请输入密码"
/>
</div>
<div class="modal-buttons">
<button class="modal-btn secondary" onclick="pwdCancel()">
取消
</button>
<button class="modal-btn primary" onclick="pwdConfirm()">确认</button>
</div>
</div>
</div>
<!-- Popup Modal -->
<div id="popup-modal" class="popup-modal">
<div class="popup-content">
<p id="popup-message">这里是消息内容</p>
<div id="popup-buttons">
<button
id="confirm-btn"
onclick="confirmAction()"
i18n="popup.confirm_btn"
>
确认
</button>
<button
id="cancel-btn"
onclick="cancelAction()"
i18n="popup.cancel_btn"
>
取消
</button>
</div>
</div>
</div>
<!-- 修改可视化模态框,添加时间和力度滑块 -->
<div class="visualization-modal" id="visualizationModal">
<div class="visualization-content">
<div class="visualization-header">
<h3>配置按摩参数</h3>
<button class="visualization-close" onclick="closeVisualization()">
&times;
</button>
</div>
<div class="visualization-body">
<div class="control-panel">
<div class="mode-selector">
<h4>按摩手法选择</h4>
<div class="mode-buttons">
<button class="mode-button active" data-mode="point">
定穴点按法
</button>
<button class="mode-button" data-mode="point_rub">定点揉摩法</button>
<button class="mode-button" data-mode="line">循经直推法</button>
<button class="mode-button" data-mode="in_spiral">
螺旋内揉法
</button>
<button class="mode-button" data-mode="out_spiral">
螺旋外散法
</button>
<button class="mode-button" data-mode="ellipse">
周天环摩法
</button>
<button class="mode-button" data-mode="lemniscate">
双环疏经法
</button>
<button class="mode-button" data-mode="cycloid">
摆浪通络法
</button>
</div>
</div>
<!-- 添加时间和力度控制区 -->
<div class="control-group">
<div class="control-header">
<label
>时间: <span id="timeValue">自动</span
><span id="timeUnit" style="display: none"></span></label
>
<label class="switch">
<input type="checkbox" id="timeAuto" checked />
<span class="slider round"></span>
<span class="switch-label">自动</span>
</label>
</div>
<input
type="range"
id="timeSlider"
min="5"
max="450"
step="5"
value="30"
style="display: none"
/>
</div>
<div class="control-group">
<div class="control-header">
<label>力度: <span id="strengthValue">自动</span></label>
<label class="switch">
<input type="checkbox" id="strengthAuto" checked />
<span class="slider round"></span>
<span class="switch-label">自动</span>
</label>
</div>
<input
type="range"
id="strengthSlider"
min="5"
max="70"
step="1"
value="30"
style="display: none"
/>
</div>
<div id="pathControls" style="display: none">
<div class="control-group">
<div class="control-header">
<label>范围: <span id="widthValue">自动</span></label>
<label class="switch">
<input type="checkbox" id="widthAuto" checked />
<span class="slider round"></span>
<span class="switch-label">自动</span>
</label>
</div>
<input
type="range"
id="widthSlider"
min="10"
max="100"
step="5"
value="30"
style="display: none"
/>
</div>
<div class="control-group">
<label>圈数 (1-10): <span id="cyclesValue">5</span></label>
<input
type="range"
id="cyclesSlider"
min="1"
max="10"
step="1"
value="5"
/>
</div>
<div class="control-group">
<label>方向:</label>
<div class="direction-selector">
<button class="direction-button active" data-direction="CW">
顺时针
</button>
<button class="direction-button" data-direction="CCW">
逆时针
</button>
</div>
</div>
</div>
<div class="selected-points">
<h4>已选择的穴位</h4>
<div class="point-info" id="point1">起点穴位:未选择</div>
<div class="point-info" id="point2">终点穴位:未选择</div>
</div>
</div>
<div class="button-group">
<button class="confirm-button" onclick="confirmPointSelection()">
确认选择
</button>
<button class="clear-button" id="clearPoints">清除选择</button>
</div>
</div>
</div>
</div>
<script>
let massagePlans = {};
let isEditMode = false;
let currentPlanName = "";
let planToCopy = "";
let planToDelete = "";
// 从URL参数中获取初始筛选条件
const initialChooseTask = "{{ initial_choose_task }}";
const initialBodyPart = "{{ initial_body_part }}";
// 判断URL是否包含筛选参数
const hasFilterParams = initialChooseTask || initialBodyPart;
// 按摩头类型映射
const headTypeMap = {
thermotherapy: "深部热疗",
shockwave: "点阵按摩",
ball: "全能滚珠",
finger: "指疗通络",
roller: "滚滚刺疗",
stone: "温砭舒揉",
ion: "离子光灸",
heat: "能量热疗",
spheres: "天球滚捏",
};
// 默认参数值
const DEFAULT_PARAMS = {
width: 30,
cycles: 3,
direction: "clockwise"
};
// 参考图片尺寸 - 滑条对应的标准尺寸
const REFERENCE_SIZE = {
width: 640,
height: 400
};
// 实际图片尺寸
const ACTUAL_SIZE = {
width: 490,
height: 687
};
// // 实际图片尺寸
// const ACTUAL_SIZE = {
// width: 640,
// height: 400
// };
// 参数转换函数 - 将滑条值转换为实际图片尺寸对应的值
function convertWidthParam(sliderValue) {
// 计算参考图和实际图的对角线长度
const referenceDiagonal = Math.sqrt(Math.pow(REFERENCE_SIZE.width, 2) + Math.pow(REFERENCE_SIZE.height, 2));
const actualDiagonal = Math.sqrt(Math.pow(ACTUAL_SIZE.width, 2) + Math.pow(ACTUAL_SIZE.height, 2));
// 使用对角线比例作为缩放系数
const scaleFactor = actualDiagonal / referenceDiagonal;
// 根据对角线比例计算实际值
actualWidth = Math.round(sliderValue * scaleFactor);
console.log("actualWidth: ", actualWidth);
return actualWidth;
}
// 按摩手法映射
const pathTypeMap = {
line: "循经直推法",
in_spiral: "螺旋外散法",
out_spiral: "螺旋内揉法",
ellipse: "周天环摩法",
lemniscate: "双环疏经法",
cycloid: "摆浪通络法",
point: "定穴点按法",
point_rub: "定点揉摩法"
};
// 部位映射
const bodyPartMap = {
back: "背部",
belly: "腹部",
shoulder: "肩膀",
waist: "腰部",
leg: "腿部",
};
// 方向映射
const directionMap = {
CW: "顺时针",
CCW: "逆时针",
};
const acupointArray = {
back: [
"督俞左",
"督俞右",
"心俞左",
"心俞右",
"厥阴左俞",
"厥阴右俞",
"肺俞左",
"肺俞右",
"膈俞左",
"膈俞右",
"肝俞左",
"肝俞右",
"胆俞左",
"胆俞右",
"脾俞左",
"脾俞右",
"胃俞左",
"胃俞右",
"附分左",
"附分右",
"魄户左",
"魄户右",
"膏肓左",
"膏肓右",
"神堂左",
"神堂右",
"譩譆左",
"譩譆右",
"膈关左",
"膈关右",
"阳纲左",
"阳纲右",
"意舍左",
"意舍右",
"胃仓左",
"胃仓右",
],
belly: [
"滑肉左",
"滑肉右",
"大横左",
"大横右",
"天枢左",
"天枢右",
"外陵左",
"外陵右",
"水分",
"神阙",
"气海",
"石门",
"关元",
],
shoulder: [
"风门左",
"风门右",
"大杼左",
"大杼右",
"肩中左",
"肩中右",
"肩外左",
"肩外右",
"曲垣左",
"曲垣右",
],
waist: [
"肾俞左",
"肾俞右",
"气海左俞",
"气海右俞",
"大肠左俞",
"大肠右俞",
"关元左俞",
"关元右俞",
"京门左",
"京门右",
"小肠左俞",
"小肠右俞",
"膀胱左俞",
"膀胱右俞",
"中膂左俞",
"中膂右俞",
"白环左俞",
"白环右俞",
"胞肓左俞",
"胞肓右俞",
"秩边左俞",
"秩边右俞",
"会阳左",
"会阳右",
"志室左",
"志室右",
],
leg: [
"殷门左",
"殷门右",
"上委中左",
"上委中右",
"承山左",
"承山右",
"承筋左",
"承筋右",
"合阳左",
"合阳右",
],
};
// 页面加载后初始化
document.addEventListener("DOMContentLoaded", function () {
// 初始设置选择按钮的显示状态
const chooseBtn = document.querySelector(".choose-btn");
if (chooseBtn) {
chooseBtn.style.display = hasFilterParams ? "inline-block" : "none";
}
// 添加文档点击事件监听器,用于关闭所有下拉菜单
document.addEventListener("click", () => {
const dropdowns = document.querySelectorAll(".dropdown-menu");
dropdowns.forEach((dropdown) => dropdown.classList.remove("show"));
});
// 根据URL参数控制按钮显示
const filterToggleBtn = document.getElementById("filterToggleBtn");
const createBtn = document.getElementById("createBtn");
const refreshBtn = document.getElementById("refreshBtn");
const filterContent = document.getElementById("filterContent");
const filterToggle = document.querySelector(".filter-toggle");
if (hasFilterParams) {
// 如果有URL参数隐藏筛选按钮和创建按钮
// if (filterToggleBtn) {
// filterToggleBtn.parentElement.style.display = "none";
// }
// if (createBtn) {
// createBtn.parentElement.style.display = "none";
// }
// if (refreshBtn) {
// refreshBtn.parentElement.style.display = "none";
// }
const filterBar = document.querySelector(".filter-bar");
filterBar.style.display = "none";
// 确保筛选内容区域不展开
if (filterContent) {
filterContent.classList.remove("active");
}
if (filterToggle) {
filterToggle.classList.remove("active");
}
} else {
// 没有URL参数时添加筛选器切换逻辑
if (filterToggleBtn) {
filterToggleBtn.addEventListener("click", function () {
filterContent.classList.toggle("active");
filterToggle.classList.toggle("active");
});
}
}
// 刷新按钮点击事件
refreshBtn.addEventListener("click", function () {
// 获取刷新图标并添加旋转动画类
const refreshIcon = this.querySelector(".refresh-icon");
refreshIcon.classList.add("rotating");
// 动画结束后移除旋转类
setTimeout(() => {
refreshIcon.classList.remove("rotating");
}, 800); // 与动画持续时间相同
// 调用获取按摩计划函数
fetchMassagePlans();
});
});
// 获取按摩计划数据
async function fetchMassagePlans() {
console.log(123123);
// 显示加载中的状态
const plansGrid = document.getElementById("plansGrid");
plansGrid.innerHTML = `
<div class="loading-container">
<div class="loading-spinner"></div>
<div class="loading-text">加载中,请稍候...</div>
</div>
`;
try {
// 构建URL参数
const params = new URLSearchParams();
if (initialChooseTask)
params.append("choose_task", initialChooseTask);
if (initialBodyPart) params.append("body_part", initialBodyPart);
const url =
"/get_massage_plan" +
(params.toString() ? `?${params.toString()}` : "");
// 设置超时,保证用户体验
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
const response = await fetch(url, {
signal: controller.signal,
}).finally(() => clearTimeout(timeoutId));
if (!response.ok) {
throw new Error(`服务器返回错误: ${response.status}`);
}
const result = await response.json();
if (result.status === "success") {
massagePlans = result.data;
renderPlansList();
} else {
throw new Error(result.message || "获取计划数据失败");
}
} catch (error) {
console.error("获取按摩计划失败:", error);
let errorMessage = "加载数据失败";
if (error.name === "AbortError") {
errorMessage = "请求超时,请检查网络连接";
} else if (error.message) {
errorMessage = `加载失败: ${error.message}`;
}
plansGrid.innerHTML = `<div class="error-container">
<svg class="error-icon" viewBox="0 0 24 24" fill="none" stroke="#e74c3c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>
<div class="error-message">${errorMessage}</div>
<button onclick="fetchMassagePlans()" class="retry-btn">重试</button>
</div>`;
}
}
// 渲染计划列表
function renderPlansList() {
const plansGrid = document.getElementById("plansGrid");
plansGrid.innerHTML = "";
// 将计划转换为数组并排序
const planEntries = Object.entries(massagePlans).sort((a, b) => {
// 首先按照是否可删除排序(不可删除的排在前面)
const aCanDelete = a[1].can_delete || false;
const bCanDelete = b[1].can_delete || false;
if (!aCanDelete && bCanDelete) return -1;
if (aCanDelete && !bCanDelete) return 1;
// 如果可删除状态相同,按名称排序
return a[0].localeCompare(b[0]);
});
// 如果没有数据,显示提示信息
if (planEntries.length === 0) {
const noDataMsg = document.createElement("div");
noDataMsg.className = "no-data-message";
noDataMsg.textContent = "没有找到符合条件的按摩计划";
plansGrid.appendChild(noDataMsg);
return;
}
// 创建一个文档片段,以便一次性添加所有卡片的占位元素
const fragment = document.createDocumentFragment();
// 为每个计划创建一个带有占位符的容器
planEntries.forEach(([name, plan], index) => {
// 创建一个卡片容器,但不渲染完整内容
const cardContainer = document.createElement("div");
cardContainer.className = "plan-card-container";
cardContainer.dataset.planName = name;
cardContainer.dataset.planIndex = index;
// 创建一个占位符,实际内容将在可见时加载
const placeholder = document.createElement("div");
placeholder.className = "plan-card-placeholder";
placeholder.style.height = "120px"; // 预估的卡片高度
placeholder.style.borderRadius = "15px";
placeholder.style.backgroundColor = "rgba(240, 240, 240, 0.5)";
cardContainer.appendChild(placeholder);
fragment.appendChild(cardContainer);
});
// 一次性将所有占位符添加到DOM
plansGrid.appendChild(fragment);
// 创建并配置Intersection Observer
const observerOptions = {
root: plansGrid, // 观察容器是plansGrid
rootMargin: "100px 0px", // 视口外100px预加载
threshold: 0.01, // 只需要很小一部分进入视口就触发
};
// 定义观察者回调函数
const observerCallback = (entries, observer) => {
entries.forEach((entry) => {
const container = entry.target;
const planName = container.dataset.planName;
const planIndex = parseInt(container.dataset.planIndex);
if (entry.isIntersecting) {
// 容器进入视口,加载实际内容
const [name, plan] = planEntries[planIndex];
// 只有在容器中没有真正的卡片时才创建
if (!container.querySelector(".plan-card")) {
// 移除占位符
container.innerHTML = "";
// 创建真实卡片
const card = createPlanCard(name, plan);
container.appendChild(card);
}
} else {
// 容器离开视口,可以选择销毁内容以节省内存
// 这步是可选的,取决于内存使用情况,可以保留内容或只保留核心信息
if (isMobileDevice() && container.querySelector(".plan-card")) {
// 在移动设备上,当卡片离开视图时销毁内容,减轻内存压力
const planName = container.dataset.planName;
const placeholder = document.createElement("div");
placeholder.className = "plan-card-placeholder";
placeholder.style.height = "120px";
placeholder.style.borderRadius = "15px";
placeholder.style.backgroundColor = "rgba(240, 240, 240, 0.5)";
container.innerHTML = "";
container.appendChild(placeholder);
}
}
});
};
// 创建观察者
const observer = new IntersectionObserver(
observerCallback,
observerOptions
);
// 开始观察所有卡片容器
document
.querySelectorAll(".plan-card-container")
.forEach((container) => {
observer.observe(container);
});
}
// 判断是否为移动设备
function isMobileDevice() {
return (
window.innerWidth <= 768 ||
"ontouchstart" in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0
);
}
// 创建计划卡片函数从renderPlansList中抽取出来
function createPlanCard(name, plan) {
const card = document.createElement("div");
card.className = "plan-card";
const moreOptions = document.createElement("div");
moreOptions.className = "more-options";
moreOptions.innerHTML = `
<svg viewBox="0 0 24 24">
<circle cx="12" cy="6" r="2"/>
<circle cx="12" cy="12" r="2"/>
<circle cx="12" cy="18" r="2"/>
</svg>
<div class="dropdown-menu">
<div class="dropdown-item" onclick="event.stopPropagation(); showCopyModal('${name}')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
复制疗程
</div>
${
plan.can_delete
? `
<div class="dropdown-item" onclick="event.stopPropagation(); showDeleteConfirmation('${name}')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
<line x1="10" y1="11" x2="10" y2="17"/>
<line x1="14" y1="11" x2="14" y2="17"/>
</svg>
删除疗程
</div>
`
: ""
}
</div>
`;
moreOptions.onclick = (e) => {
e.stopPropagation();
const dropdown = moreOptions.querySelector(".dropdown-menu");
dropdown.classList.toggle("show");
};
card.appendChild(moreOptions);
const content = document.createElement("div");
card.onclick = () => showPlanDetail(name);
content.innerHTML = `
<h3>${name.split("-")[2] || name}</h3>
<div class="plan-info">
<div>
<span class="badge">${
bodyPartMap[plan.body_part] || plan.body_part
}</span>
<span class="badge">${
headTypeMap[plan.choose_task] || plan.choose_task
}</span>
<p>包含 ${plan.task_plan.length} 个任务</p>
</div>
</div>
`;
card.appendChild(content);
return card;
}
// 获取单个按摩计划数据
async function fetchSinglePlan(planName) {
try {
const response = await fetch(
`/get_massage_plan?plan_name=${encodeURIComponent(planName)}`
);
const result = await response.json();
if (result.status === "success") {
return result.data;
}
return null;
} catch (error) {
console.error("获取按摩计划详情失败:", error);
return null;
}
}
// 检查路径类型是否支持特定属性
function isPropertySupported(pathType, property) {
if (pathType === "point" || pathType === "point_rub" || pathType === "line") {
return !["cycles", "direction", "width"].includes(property);
}
return true;
}
// 当前编辑的行索引
let currentEditingRow = -1;
// 创建广播通道
const channel = new BroadcastChannel("massage_plan_channel");
// 生成编辑表格的行
function generateEditableRow(task, index) {
const isBasicPath = task.path === "point" || task.path === "point_rub" || task.path === "line";
// 根据手法类型决定显示内容
const timeDisplay = task.time || "自动";
const strengthDisplay = task.strength || "自动";
const cyclesDisplay = isBasicPath ? "-" : task.cycles || "自动";
const directionDisplay = isBasicPath
? "-"
: task.direction
? directionMap[task.direction] || task.direction
: "自动";
const widthDisplay = isBasicPath ? "-" : task.width || "自动";
return `
<tr data-index="${index}" class="draggable-row">
<td>
<div style="display: flex; gap: 5px;">
${
isEditMode
? `
<div class="drag-handle">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="6.5" cy="4" r="2.5" fill="currentColor" stroke="none"/>
<circle cx="6.5" cy="12" r="2.5" fill="currentColor" stroke="none"/>
<circle cx="6.5" cy="20" r="2.5" fill="currentColor" stroke="none"/>
<circle cx="17.5" cy="4" r="2.5" fill="currentColor" stroke="none"/>
<circle cx="17.5" cy="12" r="2.5" fill="currentColor" stroke="none"/>
<circle cx="17.5" cy="20" r="2.5" fill="currentColor" stroke="none"/>
</svg>
</div>
`
: ""
}
<button class="edit-point-btn" onclick="openVisualization(${index})">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/>
</svg>
</button>
<button class="delete-row-btn" onclick="deleteRow(${index})">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
<line x1="10" y1="11" x2="10" y2="17"/>
<line x1="14" y1="11" x2="14" y2="17"/>
</svg>
</button>
</div>
</td>
<td>${task.start_point || "-"}</td>
<td>${task.end_point || "-"}</td>
<td>${pathTypeMap[task.path] || task.path}</td>
<td>${timeDisplay}</td>
<td>${strengthDisplay}</td>
<td>${cyclesDisplay}</td>
<td>${directionDisplay}</td>
<td>${widthDisplay}</td>
</tr>
`;
}
// 清理计划数据,确保不支持的参数被清除
function cleanupPlanData() {
const plan = massagePlans[currentPlanName];
if (!plan || !plan.task_plan) return;
plan.task_plan.forEach((task) => {
const isBasicPath = task.path === "point" || task.path === "point_rub" || task.path === "line";
if (isBasicPath) {
// 清除不支持的参数
task.cycles = null;
task.direction = null;
task.width = null;
}
});
}
const socket = io();
socket.on("connect", () => {
console.log("Connected to server");
});
socket.on("connect_error", (error) => {
console.error("Connection error:", error);
});
function choosePlan() {
socket.emit("send_command", `select_plan:${currentPlanName}`);
console.log("send_command", `select_plan:${currentPlanName}`);
if (window.parent && window.parent.transitionTo) {
window.parent.transitionTo("step4", "step3", 3);
}
localStorage.setItem("acupunctureName", currentPlanName);
}
// 编辑按钮
function editPlan() {
// toggleEditMode();
showPwdModal().then((confirm) => {
if (confirm) {
if (pwdInput.value === "") {
showPopup("密码不能为空。");
return;
} else {
fetch("/plans_edit", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
password: pwdInput.value,
}),
})
.then((response) => response.json())
.then((data) => {
if (data.success) {
toggleEditMode();
} else {
showPopup("密码错误!", { confirm: true, cancel: false });
}
})
.catch((error) => {
console.error("Error:", error);
});
}
}
});
}
// 切换编辑模式
function toggleEditMode() {
isEditMode = !isEditMode;
const editBtn = document.querySelector(".edit-btn");
const saveBtn = document.querySelector(".save-btn");
const cancelBtn = document.querySelector(".cancel-btn");
const detailTitle = document.getElementById("detailTitle");
if (isEditMode) {
// 进入编辑模式
editBtn.style.display = "none";
saveBtn.style.display = "inline-flex";
cancelBtn.style.display = "inline-flex";
detailTitle.contentEditable = true;
detailTitle.classList.add("editable");
// 重新渲染表格以显示编辑控件
const plan = massagePlans[currentPlanName];
if (plan) {
renderPlanDetail(plan);
// 设置拖拽排序功能
setTimeout(setupDragSort, 0);
}
} else {
// 退出编辑模式
editBtn.style.display = "inline-flex";
saveBtn.style.display = "none";
cancelBtn.style.display = "none";
detailTitle.contentEditable = false;
detailTitle.classList.remove("editable");
// 重新渲染表格移除编辑控件
const plan = massagePlans[currentPlanName];
if (plan) {
renderPlanDetail(plan);
// 禁用拖拽
interact(".draggable-row").draggable(false).dropzone(false);
}
}
}
// 取消编辑
async function cancelEdit() {
// 重新获取数据以恢复原始状态
const plan = await fetchSinglePlan(currentPlanName);
if (plan) {
massagePlans[currentPlanName] = plan;
}
toggleEditMode();
}
// 保存计划
async function savePlan() {
try {
// 在保存前清理数据
cleanupPlanData();
const response = await fetch("/set_massage_plan", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
plan_name: currentPlanName,
plan_data: massagePlans[currentPlanName],
}),
});
const result = await response.json();
if (result.status === "success") {
showMessage("保存成功", "success");
toggleEditMode();
// 如果在编辑模式下,重新设置拖拽功能
if (isEditMode) {
setTimeout(setupDragSort, 0);
}
fetchMassagePlans();
} else {
showMessage("保存失败: " + result.message, "error");
}
} catch (error) {
showMessage("保存失败: " + error.message, "error");
}
}
// 显示消息
function showMessage(message, type) {
const messageDiv = document.createElement("div");
messageDiv.className =
type === "success" ? "success-message" : "error-message";
messageDiv.textContent = message;
const detailContent = document.getElementById("detailContent");
detailContent.insertBefore(messageDiv, detailContent.firstChild);
setTimeout(() => messageDiv.remove(), 3000);
}
// 修改显示计划详情函数
async function showPlanDetail(planName) {
currentEditingPlanId = null;
try {
// 获取最新的计划数据
const plan =
(await fetchSinglePlan(planName)) || massagePlans[planName];
if (!plan) {
console.error("获取按摩计划详情失败: 未找到计划数据");
return;
}
currentPlan = plan;
currentPlanName = planName; // 设置当前计划名称
const detailView = document.getElementById("detailView");
const plansGrid = document.querySelector(".plans-grid");
plansGrid.style.display = "none";
detailView.classList.add("active"); // 使用类来控制显示而不是直接设置style
// 隐藏筛选栏
const filterBar = document.querySelector(".filter-bar");
if (filterBar) {
filterBar.classList.add("hidden");
}
const detailTitle = document.getElementById("detailTitle");
const detailContent = document.getElementById("detailContent");
// 根据URL参数控制选择按钮的显示
const chooseBtn = document.querySelector(".choose-btn");
if (chooseBtn) {
chooseBtn.style.display = hasFilterParams ? "inline-block" : "none";
}
// 根据can_delete属性和URL参数控制编辑按钮的显示
const editBtn = document.querySelector(".edit-btn");
if (editBtn) {
editBtn.style.display =
plan.can_delete && !hasFilterParams ? "inline-flex" : "none";
}
// 只显示最后一部分名称
detailTitle.textContent = planName.split("-")[2] || planName;
// 显示加载状态
detailContent.innerHTML =
'<div style="text-align: center; padding: 2rem;">加载中...</div>';
// 更新本地缓存的数据
massagePlans[planName] = plan;
renderPlanDetail(plan);
} catch (error) {
console.error("获取按摩计划详情失败:", error);
const detailContent = document.getElementById("detailContent");
detailContent.innerHTML =
'<div style="text-align: center; color: red; padding: 2rem;">获取数据失败</div>';
}
}
// 渲染计划详情
function renderPlanDetail(plan) {
const detailContent = document.getElementById("detailContent");
// 发送身体部位变更消息,使可视化视图切换到对应的部位
if (plan.body_part) {
channel.postMessage({
type: "change_body_part",
body_part: plan.body_part,
});
}
let content = `
<div class="plan-info">
<div>
<span class="badge">按摩部位: ${
bodyPartMap[plan.body_part] || plan.body_part
}</span>
<span class="badge">按摩头: ${
headTypeMap[plan.choose_task] || plan.choose_task
}</span>
</div>
</div>
<div class="massage-type">
<div class="massage-type-title">按摩手法简介</div>
<p id="massageIntro" class="intro-text">${
plan.introduction ?? "该按摩手法暂无详细介绍"
}</p>
<button class="show-more-btn" onclick="toggleIntro()">详情</button>
</div>
<div class="table-box">
<table class="task-table">
<thead>
<tr>
<th>操作</th>
<th>起点穴位</th>
<th>终点穴位</th>
<th>按摩手法</th>
<th>时间(秒)</th>
<th>力度</th>
<th>圈数</th>
<th>运动方向</th>
<th>范围</th>
</tr>
</thead>
<tbody id="task-table-body">
`;
if (isEditMode) {
// 渲染现有的任务行
if (plan.task_plan && plan.task_plan.length > 0) {
plan.task_plan.forEach((task, index) => {
content += generateEditableRow(task, index);
});
} else {
// 如果没有任务,显示一个空行提示
content += `
<tr>
<td colspan="9" style="text-align: center; padding: 15px; color: #777;">
尚未添加任务,请使用下方的"+"按钮添加
</td>
</tr>
`;
}
// 添加一个带加号的空行用于添加新任务
content += `
<tr class="add-row-tr" onclick="addNewRow()">
<td colspan="9" style="text-align: center; padding: 15px;">
<div class="add-row-icon">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="white" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</div>
</td>
</tr>
`;
} else {
if (plan.task_plan && plan.task_plan.length > 0) {
plan.task_plan.forEach((task, index) => {
const isBasicPath = task.path === "point" || task.path === "point_rub" || task.path === "line";
// 根据手法类型决定显示内容
const timeDisplay = task.time || "自动";
const strengthDisplay = task.strength || "自动";
const cyclesDisplay = isBasicPath ? "-" : task.cycles || "自动";
const directionDisplay = isBasicPath
? "-"
: task.direction
? directionMap[task.direction] || task.direction
: "自动";
const widthDisplay = isBasicPath ? "-" : task.width || "自动";
content += `
<tr>
<td>
<button class="edit-point-btn" onclick="viewVisualization(${index})">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M20.4 15a9 9 0 1 0-16.8 0"/>
</svg>
</button>
</td>
<td>${task.start_point || "-"}</td>
<td>${task.end_point || "-"}</td>
<td>${pathTypeMap[task.path] || task.path}</td>
<td>${timeDisplay}</td>
<td>${strengthDisplay}</td>
<td>${cyclesDisplay}</td>
<td>${directionDisplay}</td>
<td>${widthDisplay}</td>
</tr>
`;
});
} else {
// 如果没有任务,显示一个提示
content += `
<tr>
<td colspan="9" style="text-align: center; padding: 15px; color: #777;">
尚未添加任何任务
</td>
</tr>
`;
}
}
content += `
</tbody>
</table>
</div>
`;
detailContent.innerHTML = content;
}
// 返回列表视图
function showPlansList() {
// 如果在编辑模式下,先取消编辑
if (isEditMode) {
cancelEdit().then(() => {
// 取消编辑后再返回列表
document.querySelector(".plans-grid").style.display = "grid";
const detailView = document.getElementById("detailView");
detailView.classList.remove("active");
// 重新显示筛选栏
const filterBar = document.querySelector(".filter-bar");
if (filterBar) {
filterBar.classList.remove("hidden");
}
});
} else {
// 不在编辑模式下,直接返回列表
document.querySelector(".plans-grid").style.display = "grid";
const detailView = document.getElementById("detailView");
detailView.classList.remove("active");
// 重新显示筛选栏
const filterBar = document.querySelector(".filter-bar");
if (filterBar) {
filterBar.classList.remove("hidden");
}
}
}
// 生成新的计划名称
function generateNewPlanName(baseName) {
// 分解原始名称为三部分
const parts = baseName.split("-");
let nameBase;
let copySuffix = "(复制)";
if (parts.length === 3) {
// 标准格式: 部位-按摩头-名称
nameBase = parts[2];
} else {
// 非标准格式
nameBase = baseName;
}
// 检查原名称是否已经包含"(复制)"
const copyRegex = /^(.+)(复制(\d*)$/;
const match = nameBase.match(copyRegex);
if (match) {
// 已经是个副本,提取基础名称
const basePart = match[1];
const copyNum = match[2] ? parseInt(match[2]) : 0;
// 找到下一个可用的副本编号
let nextNum = copyNum ? copyNum + 1 : 1;
let newName;
// 组合完整名称并检查是否已存在
if (parts.length === 3) {
do {
newName = `${parts[0]}-${parts[1]}-${basePart}(复制${nextNum}`;
nextNum++;
} while (isNameExists(newName));
} else {
do {
newName = `${basePart}(复制${nextNum}`;
nextNum++;
} while (isNameExists(newName));
}
return newName;
} else {
// 原名称不包含"(复制)",直接添加后缀
let newName;
if (parts.length === 3) {
newName = `${parts[0]}-${parts[1]}-${nameBase}${copySuffix}`;
// 检查是否已存在,如果存在则添加数字
let counter = 1;
while (isNameExists(newName)) {
newName = `${parts[0]}-${parts[1]}-${nameBase}(复制${counter}`;
counter++;
}
} else {
newName = `${nameBase}${copySuffix}`;
// 检查是否已存在,如果存在则添加数字
let counter = 1;
while (isNameExists(newName)) {
newName = `${nameBase}(复制${counter}`;
counter++;
}
}
return newName;
}
}
// 显示复制对话框
function showCopyModal(planName) {
planToCopy = planName;
const modal = document.getElementById("copyModal");
const input = document.getElementById("newPlanName");
const error = document.getElementById("nameError");
// 确保错误提示是隐藏的
error.style.display = "none";
input.classList.remove("error");
// 分解原始名称
const parts = planName.split("-");
if (parts.length === 3) {
// 标准格式: 部位-按摩头-名称
const bodyPart = parts[0];
const massageHead = parts[1];
const lastPart = parts[2];
// 生成新的后缀名称(添加复制标记)
const newSuffix = generateCopySuffix(lastPart);
// 清除之前的内容
const container = document.querySelector(".modal-content");
container.innerHTML = "";
// 添加提示说明
const hintText = document.createElement("p");
hintText.style.marginBottom = "15px";
hintText.style.fontSize = "14px";
hintText.style.color = "#666";
hintText.style.textAlign = "left";
hintText.textContent = "请输入新的疗程名称:";
container.appendChild(hintText);
// 创建前缀显示区域
const prefixDisplay = document.createElement("div");
prefixDisplay.className = "prefix-display";
// 创建部位部分
const bodyPartSpan = document.createElement("span");
bodyPartSpan.className = "prefix-part";
bodyPartSpan.textContent = bodyPart;
prefixDisplay.appendChild(bodyPartSpan);
// 添加分隔符
const separator1 = document.createElement("span");
separator1.className = "prefix-separator";
separator1.textContent = " - ";
prefixDisplay.appendChild(separator1);
// 创建按摩头部分
const massageHeadSpan = document.createElement("span");
massageHeadSpan.className = "prefix-part";
massageHeadSpan.textContent = massageHead;
prefixDisplay.appendChild(massageHeadSpan);
// 添加前缀显示区域到容器
container.appendChild(prefixDisplay);
// 设置输入框和恢复原来的样式
input.style = ""; // 清除之前可能设置的所有样式
input.value = newSuffix;
input.className = "modal-input"; // 恢复原有类
container.appendChild(input);
// 添加错误提示div
const errorDiv = document.createElement("div");
errorDiv.className = "error-text";
errorDiv.id = "nameError";
errorDiv.textContent = "该名称已存在,请重新输入";
errorDiv.style.display = "none";
container.appendChild(errorDiv);
} else {
// 非标准格式,使用整个名称
const container = document.querySelector(".modal-content");
container.innerHTML = "";
// 添加提示
const hintText = document.createElement("p");
hintText.style.marginBottom = "15px";
hintText.style.fontSize = "14px";
hintText.style.color = "#666";
hintText.textContent = "请输入新的疗程名称:";
container.appendChild(hintText);
// 设置输入框
input.style = ""; // 清除之前可能设置的所有样式
input.className = "modal-input"; // 恢复原有类
input.value = generateNewPlanName(planName);
container.appendChild(input);
// 添加错误提示div
const errorDiv = document.createElement("div");
errorDiv.className = "error-text";
errorDiv.id = "nameError";
errorDiv.textContent = "该名称已存在,请重新输入";
errorDiv.style.display = "none";
container.appendChild(errorDiv);
}
// 平滑显示模态框
modal.style.display = "flex";
modal.style.opacity = "0";
setTimeout(() => {
modal.style.opacity = "1";
}, 10);
// 选中输入框的全部文本
setTimeout(() => {
input.focus();
input.select();
}, 50);
}
// 生成复制后缀名称
function generateCopySuffix(baseName) {
// 检查原名称是否已经包含"(复制)"
const copyRegex = /^(.+)(复制(\d*)$/;
const match = baseName.match(copyRegex);
if (match) {
// 已经是个副本,提取基础名称
const basePart = match[1];
const copyNum = match[2] ? parseInt(match[2]) : 0;
// 找到下一个可用的副本编号
let nextNum = copyNum ? copyNum + 1 : 1;
let newSuffix;
// 组合新名称并检查是否已存在
do {
newSuffix = `${basePart}(复制${nextNum}`;
const fullName = `${planToCopy
.split("-")
.slice(0, 2)
.join("-")}-${newSuffix}`;
nextNum++;
if (!isNameExists(fullName)) break;
} while (true);
return newSuffix;
} else {
// 原名称不包含"(复制)",直接添加后缀
let newSuffix = `${baseName}(复制)`;
let fullName = `${planToCopy
.split("-")
.slice(0, 2)
.join("-")}-${newSuffix}`;
// 检查是否已存在,如果存在则添加数字
let counter = 1;
while (isNameExists(fullName)) {
newSuffix = `${baseName}(复制${counter}`;
fullName = `${planToCopy
.split("-")
.slice(0, 2)
.join("-")}-${newSuffix}`;
counter++;
}
return newSuffix;
}
}
// 关闭复制对话框
function closeCopyModal() {
const modal = document.getElementById("copyModal");
// 平滑隐藏模态框
modal.style.opacity = "0";
setTimeout(() => {
modal.style.display = "none";
}, 300);
planToCopy = "";
}
// 检查名称是否已存在
function isNameExists(name) {
return name in massagePlans;
}
// 确认复制
async function confirmCopy() {
const input = document.getElementById("newPlanName");
const error = document.getElementById("nameError");
const newSuffix = input.value.trim();
if (!newSuffix) {
input.classList.add("error");
error.textContent = "请输入疗程名称";
error.style.display = "block";
return;
}
// 构建完整的新名称
let newName;
const parts = planToCopy.split("-");
if (parts.length === 3) {
// 组合前缀和用户输入的后缀
newName = `${parts[0]}-${parts[1]}-${newSuffix}`;
} else {
// 对于非标准格式,直接使用用户输入的名称
newName = newSuffix;
}
if (isNameExists(newName)) {
input.classList.add("error");
error.textContent = "该名称已存在,请重新输入";
error.style.display = "block";
return;
}
try {
// 深拷贝计划数据
const planData = JSON.parse(JSON.stringify(massagePlans[planToCopy]));
const response = await fetch("/set_massage_plan", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
plan_name: newName,
plan_data: planData,
}),
});
const result = await response.json();
if (result.status === "success") {
// 更新本地数据确保设置can_delete为true
planData.can_delete = true;
massagePlans[newName] = planData;
renderPlansList();
showMessage("复制成功", "success");
closeCopyModal();
} else {
showMessage("复制失败: " + result.message, "error");
}
} catch (error) {
showMessage("复制失败: " + error.message, "error");
}
}
// 添加按下回车键确认复制的功能及输入监听
const newPlanNameInput = document.getElementById("newPlanName");
// 监听回车键
newPlanNameInput.addEventListener("keyup", function (event) {
if (event.key === "Enter") {
confirmCopy();
}
});
// 监听所有输入事件(包括复制粘贴、删除等)
newPlanNameInput.addEventListener("input", function () {
const error = document.getElementById("nameError");
if (error.style.display === "block") {
error.style.display = "none";
this.classList.remove("error");
}
});
// 显示删除确认对话框
function showDeleteConfirmation(planName) {
planToDelete = planName;
const modal = document.getElementById("deleteModal");
// 平滑显示模态框
modal.style.display = "flex";
modal.style.opacity = "0";
setTimeout(() => {
modal.style.opacity = "1";
}, 10);
}
// 关闭删除确认对话框
function closeDeleteModal() {
const modal = document.getElementById("deleteModal");
// 平滑隐藏模态框
modal.style.opacity = "0";
setTimeout(() => {
modal.style.display = "none";
}, 300);
planToDelete = "";
}
// 确认删除
async function confirmDelete() {
if (!planToDelete) return;
try {
const response = await fetch("/set_massage_plan", {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
plan_name: planToDelete,
}),
});
const result = await response.json();
if (result.status === "success") {
// 删除成功后重新获取疗程列表
await fetchMassagePlans();
closeDeleteModal();
} else {
alert("删除失败: " + result.message);
}
} catch (error) {
console.error("删除疗程失败:", error);
alert("删除失败,请稍后重试");
}
}
// 修改添加新行的函数,增加滚动到新行的功能
function addNewRow() {
if (!isEditMode) return; // 仅在编辑模式下可用
const plan = massagePlans[currentPlanName];
if (!plan) return;
// 确保task_plan存在
if (!plan.task_plan) {
plan.task_plan = [];
}
// 创建一个新的空任务对象
const newTask = {
start_point: "",
end_point: "",
path: "point", // 默认选择点按法
time: null, // 默认时间
strength: null, // 默认力度
cycles: null,
direction: null,
width: null,
};
// 将新任务添加到计划中
plan.task_plan.push(newTask);
// 重新渲染表格
renderPlanDetail(plan);
// 重新设置拖拽功能
setTimeout(setupDragSort, 0);
// 自动打开新行的可视化模态框
setTimeout(() => {
// 滚动到表格底部
const tableBox = document.querySelector(".table-box");
if (tableBox) {
tableBox.scrollTop = tableBox.scrollHeight;
}
openVisualization(plan.task_plan.length - 1);
}, 100);
}
function deleteRow(index) {
if (window.parent && window.parent.showPopup) {
window.parent.showPopup("确认删除?").then(async (confirm) => {
if (confirm) {
const plan = massagePlans[currentPlanName];
if (!plan || !plan.task_plan) return;
// 删除指定索引的任务
plan.task_plan.splice(index, 1);
// 重新渲染表格
renderPlanDetail(plan);
// 如果在编辑模式下,重新设置拖拽功能
if (isEditMode) {
setTimeout(setupDragSort, 0);
}
}
});
}
}
// 切换简介展开/收起
function toggleIntro() {
const introText = document.getElementById("massageIntro");
const btn = document.querySelector(".show-more-btn");
if (introText.classList.contains("expanded")) {
introText.classList.remove("expanded");
btn.textContent = "详情";
} else {
introText.classList.add("expanded");
btn.textContent = "收起";
}
}
// 密码框
const pwdModal = document.getElementById("pwd-modal");
const pwdInput = document.getElementById("pwd-input");
let pwdResolve;
function showPwdModal() {
// 平滑显示模态框
pwdModal.style.display = "flex";
pwdModal.style.opacity = "0";
setTimeout(() => {
pwdModal.style.opacity = "1";
}, 10);
// 清空并聚焦输入框
pwdInput.value = "";
setTimeout(() => {
pwdInput.focus();
}, 50);
// 返回Promise等待用户选择
return new Promise((resolve) => {
pwdResolve = resolve; // 保存 resolve 函数
});
}
// 绑定 keydown 事件
pwdInput.addEventListener("keydown", function (event) {
// 检查按下的键是否是 Enter 键
if (event.key === "Enter" || event.keyCode === 13) {
// 回车键按下时的逻辑
pwdConfirm();
}
});
function pwdConfirm() {
// 平滑隐藏模态框
pwdModal.style.opacity = "0";
setTimeout(() => {
pwdModal.style.display = "none";
}, 300);
pwdResolve(true);
}
function pwdCancel() {
// 平滑隐藏模态框
pwdModal.style.opacity = "0";
setTimeout(() => {
pwdModal.style.display = "none";
}, 300);
pwdResolve(false);
}
// 弹窗显示的Promise用户点击后resolve结果
let popupResolve;
function showPopup(message, buttons = { confirm: true, cancel: true }) {
// 设置弹窗的消息内容,使用 innerHTML 以支持换行
document.getElementById("popup-message").innerHTML = message;
// 控制按钮显示
document.getElementById("confirm-btn").style.display = buttons.confirm
? "inline-block"
: "none";
document.getElementById("cancel-btn").style.display = buttons.cancel
? "inline-block"
: "none";
// 显示弹窗
document.getElementById("popup-modal").style.display = "flex";
// 返回Promise等待用户选择
return new Promise((resolve) => {
popupResolve = resolve; // 保存 resolve 函数
});
}
function confirmAction() {
// 用户点击确认按钮关闭弹窗并返回true
document.getElementById("popup-modal").style.display = "none";
popupResolve(true);
}
function cancelAction() {
// 用户点击取消按钮关闭弹窗并返回false
document.getElementById("popup-modal").style.display = "none";
popupResolve(false);
}
// 修改打开可视化函数
function openVisualization(index) {
currentEditingRow = index;
const modal = document.getElementById("visualizationModal");
modal.style.display = "flex";
// 首先设置默认的自动状态
document.getElementById("timeAuto").checked = true;
document.getElementById("timeSlider").style.display = "none";
document.getElementById("timeUnit").style.display = "none";
document.getElementById("timeValue").textContent = "自动";
document.getElementById("strengthAuto").checked = true;
document.getElementById("strengthSlider").style.display = "none";
document.getElementById("strengthValue").textContent = "自动";
document.getElementById("widthAuto").checked = true;
document.getElementById("widthSlider").style.display = "none";
document.getElementById("widthValue").textContent = "自动";
// 重置控制面板状态
const task = massagePlans[currentPlanName].task_plan[index];
// 确保切换到正确的身体部位
const bodyPart = massagePlans[currentPlanName].body_part || "back";
// 发送身体部位切换消息
channel.postMessage({
type: "change_body_part",
body_part: bodyPart,
});
// 如果任务路径是point则设置模式为point否则保持不变
// const pathMode = task.path === 'none' ? 'point' : task.path;
const pathMode = task.path;
currentMode = pathMode;
const modeButtons = document.querySelectorAll(".mode-button");
modeButtons.forEach((btn) => {
// 如果是point模式激活对应的按钮
// btn.classList.toggle('active', btn.dataset.mode === (pathMode === 'point' ? 'none' : pathMode));
btn.classList.toggle("active", btn.dataset.mode === pathMode);
});
// 更新参数控制面板
updatePathControls(pathMode);
// 设置时间和力度滑块
if (task.time) {
document.getElementById("timeAuto").checked = false;
document.getElementById("timeSlider").style.display = "block";
document.getElementById("timeSlider").value = task.time;
document.getElementById("timeValue").textContent = task.time;
document.getElementById("timeUnit").style.display = "contents";
}
if (task.strength) {
document.getElementById("strengthAuto").checked = false;
document.getElementById("strengthSlider").style.display = "block";
document.getElementById("strengthSlider").value = task.strength;
document.getElementById("strengthValue").textContent = task.strength;
}
// 设置其他参数的滑块
if (task.width) {
document.getElementById("widthAuto").checked = false;
document.getElementById("widthSlider").style.display = "block";
document.getElementById("widthSlider").value = task.width;
document.getElementById("widthValue").textContent = task.width;
}
if (task.cycles) {
document.getElementById("cyclesSlider").value = task.cycles;
document.getElementById("cyclesValue").textContent = task.cycles;
}
if (task.direction) {
document.querySelectorAll(".direction-button").forEach((btn) => {
btn.classList.toggle(
"active",
btn.dataset.direction === task.direction
);
});
}
// 清除已选择的点
selectedPoints = [];
// 如果有已存在的穴位数据,加载并显示
if (task.start_point) {
// 先显示穴位名称
document.getElementById(
"point1"
).textContent = `起点穴位:${task.start_point}`;
selectedPoints.push({
name: task.start_point,
x: 0, // 临时坐标,将在收到坐标数据后更新
y: 0,
});
if (task.end_point) {
document.getElementById(
"point2"
).textContent = `终点穴位:${task.end_point}`;
selectedPoints.push({
name: task.end_point,
x: 0, // 临时坐标,将在收到坐标数据后更新
y: 0,
});
}
// 请求穴位坐标
channel.postMessage({
type: "get_points_coordinates",
points: [task.start_point, task.end_point].filter(Boolean),
});
} else {
document.getElementById("point1").textContent = "起点穴位:未选择";
document.getElementById("point2").textContent = "终点穴位:未选择";
// 发送清除点的消息
channel.postMessage({
type: "clear_points",
});
}
// 发送模式变更消息
channel.postMessage({
type: "mode_change",
mode: pathMode,
});
}
// 重置所有控件到默认状态
function resetToDefaults() {
// 重置自动开关 - 默认为自动模式
["time", "strength", "width"].forEach((param) => {
const auto = document.getElementById(`${param}Auto`);
const slider = document.getElementById(`${param}Slider`);
const value = document.getElementById(`${param}Value`);
// 确保默认为自动模式
auto.checked = true;
slider.style.display = "none";
value.textContent = "自动";
});
// 重置其他控件
document.getElementById("cyclesSlider").value = 1;
document.getElementById("cyclesValue").textContent = 1;
// 重置方向按钮
document.querySelectorAll(".direction-button").forEach((btn, index) => {
btn.classList.toggle("active", index === 0); // 默认选择第一个
});
}
// 确认点选择
function confirmPointSelection() {
if (currentEditingRow === -1) return;
if (selectedPoints.length === 0) {
if (window.parent && window.parent.showPopup) {
window.parent
.showPopup("请至少选择一个穴位", { confirm: true, cancel: false })
.then(() => {
// 用户确认后不执行任何操作,保持当前状态
});
} else {
alert("请至少选择一个穴位");
}
return;
}
const task = massagePlans[currentPlanName].task_plan[currentEditingRow];
const selectedMode = document.querySelector(".mode-button.active")
.dataset.mode;
// 更新任务数据
task.start_point = selectedPoints[0].name;
// 设置正确的路径类型和终点穴位
if (selectedMode === "none" || selectedMode === "point" || selectedMode === "point_rub") {
task.path = selectedMode === "point_rub" ? "point_rub" : "point";
task.end_point = selectedPoints[0].name;
} else {
task.path = selectedMode;
task.end_point =
selectedPoints.length > 1 ? selectedPoints[1].name : null;
}
// 设置时间和力度,只有在非自动模式下才设置具体值
const timeAuto = document.getElementById("timeAuto").checked;
const strengthAuto = document.getElementById("strengthAuto").checked;
// 设置为null表示自动模式
task.time = timeAuto
? null
: parseInt(document.getElementById("timeSlider").value);
task.strength = strengthAuto
? null
: parseInt(document.getElementById("strengthSlider").value);
// 只有对于复杂手法才需要设置这些参数
if (
selectedMode !== "none" &&
selectedMode !== "line" &&
selectedMode !== "point" &&
selectedMode !== "point_rub"
) {
const widthAuto = document.getElementById("widthAuto").checked;
task.width = widthAuto
? null
: parseInt(document.getElementById("widthSlider").value);
task.cycles = parseInt(document.getElementById("cyclesSlider").value);
task.direction = document.querySelector(
".direction-button.active"
).dataset.direction;
} else {
// 对于基本手法这些参数设为null
task.width = null;
task.cycles = null;
task.direction = null;
}
// 重新渲染表格
renderPlanDetail(massagePlans[currentPlanName]);
closeVisualization();
}
// 页面加载时获取数据
window.onload = function () {
// 初始化interact.js的触摸支持
if (typeof interact !== "undefined") {
// 确保正确处理触摸事件
interact.supportsTouch(true);
// 设置交互选项
interact.pointerMoveTolerance(5); // 设置一个容差值,减少触摸抖动
}
// 初始化筛选按钮事件
initializeFilters();
// 设置筛选内容区域的初始状态
const filterContent = document.getElementById("filterContent");
// 获取疗程数据
fetchMassagePlans();
};
// 当前筛选条件
let currentFilters = {
body_part: "all",
choose_task: "all",
};
// 初始化筛选按钮事件
function initializeFilters() {
document.querySelectorAll(".filter-btn").forEach((btn) => {
btn.addEventListener("click", () => {
const filterType = btn.dataset.filter;
const filterValue = btn.dataset.value;
// 移除同组按钮的active类
document
.querySelectorAll(`.filter-btn[data-filter="${filterType}"]`)
.forEach((b) => b.classList.remove("active"));
// 添加当前按钮的active类
btn.classList.add("active");
// 更新筛选条件
currentFilters[filterType] = filterValue;
// 应用筛选
applyFilters();
});
});
}
// 应用筛选条件
function applyFilters() {
const planCards = document.querySelectorAll(".plan-card-container");
planCards.forEach((card) => {
const planName = card.dataset.planName;
const plan = massagePlans[planName];
if (!plan) return;
const matchesBodyPart =
currentFilters.body_part === "all" ||
plan.body_part === currentFilters.body_part;
const matchesChooseTask =
currentFilters.choose_task === "all" ||
plan.choose_task === currentFilters.choose_task;
// 显示或隐藏卡片
if (matchesBodyPart && matchesChooseTask) {
card.style.display = "";
card.style.opacity = "1";
} else {
card.style.display = "none";
card.style.opacity = "0";
}
});
// 检查是否有显示的卡片
const visibleCards = Array.from(planCards).filter(
(card) => card.style.display !== "none"
);
// 如果没有匹配的卡片,显示提示信息
const noDataMsg = document.querySelector(".no-data-message");
if (visibleCards.length === 0) {
if (!noDataMsg) {
const msg = document.createElement("div");
msg.className = "no-data-message";
msg.textContent = "没有找到符合条件的按摩计划";
document.getElementById("plansGrid").appendChild(msg);
}
} else if (noDataMsg) {
noDataMsg.remove();
}
}
// 修改renderPlansList函数在渲染后应用筛选
function renderPlansList() {
const plansGrid = document.getElementById("plansGrid");
plansGrid.innerHTML = "";
// 将计划转换为数组并排序
const planEntries = Object.entries(massagePlans).sort((a, b) => {
// 首先按照是否可删除排序(不可删除的排在前面)
const aCanDelete = a[1].can_delete || false;
const bCanDelete = b[1].can_delete || false;
if (!aCanDelete && bCanDelete) return -1;
if (aCanDelete && !bCanDelete) return 1;
// 如果可删除状态相同,按名称排序
return a[0].localeCompare(b[0]);
});
// 如果没有数据,显示提示信息
if (planEntries.length === 0) {
const noDataMsg = document.createElement("div");
noDataMsg.className = "no-data-message";
noDataMsg.textContent = "没有找到符合条件的按摩计划";
plansGrid.appendChild(noDataMsg);
return;
}
// 创建一个文档片段,以便一次性添加所有卡片的占位元素
const fragment = document.createDocumentFragment();
// 为每个计划创建一个带有占位符的容器
planEntries.forEach(([name, plan], index) => {
// 创建一个卡片容器,但不渲染完整内容
const cardContainer = document.createElement("div");
cardContainer.className = "plan-card-container";
cardContainer.dataset.planName = name;
cardContainer.dataset.planIndex = index;
// 创建一个占位符,实际内容将在可见时加载
const placeholder = document.createElement("div");
placeholder.className = "plan-card-placeholder";
placeholder.style.height = "120px"; // 预估的卡片高度
placeholder.style.borderRadius = "15px";
placeholder.style.backgroundColor = "rgba(240, 240, 240, 0.5)";
cardContainer.appendChild(placeholder);
fragment.appendChild(cardContainer);
});
// 一次性将所有占位符添加到DOM
plansGrid.appendChild(fragment);
// 创建并配置Intersection Observer
const observerOptions = {
root: plansGrid,
rootMargin: "100px 0px",
threshold: 0.01,
};
// 定义观察者回调函数
const observerCallback = (entries, observer) => {
entries.forEach((entry) => {
const container = entry.target;
const planName = container.dataset.planName;
const planIndex = parseInt(container.dataset.planIndex);
if (entry.isIntersecting) {
// 容器进入视口,加载实际内容
const [name, plan] = planEntries[planIndex];
// 只有在容器中没有真正的卡片时才创建
if (!container.querySelector(".plan-card")) {
// 移除占位符
container.innerHTML = "";
// 创建真实卡片
const card = createPlanCard(name, plan);
container.appendChild(card);
}
} else {
// 容器离开视口,可以选择销毁内容以节省内存
if (isMobileDevice() && container.querySelector(".plan-card")) {
const planName = container.dataset.planName;
const placeholder = document.createElement("div");
placeholder.className = "plan-card-placeholder";
placeholder.style.height = "120px";
placeholder.style.borderRadius = "15px";
placeholder.style.backgroundColor = "rgba(240, 240, 240, 0.5)";
container.innerHTML = "";
container.appendChild(placeholder);
}
}
});
};
// 创建观察者
const observer = new IntersectionObserver(
observerCallback,
observerOptions
);
// 开始观察所有卡片容器
document
.querySelectorAll(".plan-card-container")
.forEach((container) => {
observer.observe(container);
});
// 应用当前的筛选条件
applyFilters();
}
// 定义穴位坐标映射
const points = {};
// 初始化穴位坐标
function initPoints() {
// 遍历acupointArray中的所有部位
Object.keys(acupointArray).forEach((bodyPart) => {
// 发送获取坐标的消息
channel.postMessage({
type: "get_points_coordinates",
bodyPart: bodyPart,
points: acupointArray[bodyPart],
});
});
}
// 设置拖拽排序功能
function setupDragSort() {
if (!isEditMode) return;
// 获取表格本体
const taskTableBody = document.getElementById("task-table-body");
if (!taskTableBody) return;
// 清理之前的interact实例
try {
interact(".draggable-row").unset();
} catch (e) {
console.log("No previous interaction to clean up");
}
// 定义拖拽源行和目标行
let draggedRow = null;
let draggedRowIndex = -1;
let placeholder = null;
let currentDropTarget = null;
let initialRowPositions = []; // 存储行的初始位置
let rowShifts = {}; // 存储每行的偏移量
let isDragging = false;
let dropIndicator = null; // 插入指示线
// 创建插入指示线元素
function createDropIndicator() {
const indicator = document.createElement("div");
indicator.className = "drop-indicator";
indicator.style.height = "2px";
indicator.style.backgroundColor = "#4CAF50";
indicator.style.position = "absolute";
indicator.style.left = "0";
indicator.style.right = "0";
indicator.style.zIndex = "1001";
indicator.style.pointerEvents = "none";
indicator.style.transition = "top 0.1s ease";
return indicator;
}
// 使用interact.js设置拖拽
interact(".draggable-row").draggable({
inertia: false,
autoScroll: {
container: document.querySelector(".table-box"),
speed: 300,
margin: 50,
},
allowFrom: ".drag-handle",
ignoreFrom: "button, input, select, .edit-point-btn, .delete-row-btn",
modifiers: [
interact.modifiers.restrict({
restriction: "parent",
endOnly: true,
}),
],
listeners: {
start: function (event) {
isDragging = true;
// 阻止默认行为,防止触摸设备上的滚动
event.preventDefault();
const row = event.target;
if (!row || !row.parentNode) return;
// 保存原始索引
draggedRowIndex = parseInt(row.getAttribute("data-index"));
// 添加拖拽样式
row.classList.add("dragging");
draggedRow = row;
// 记录所有行的初始位置
initialRowPositions = [];
const rows = Array.from(
taskTableBody.querySelectorAll(".draggable-row")
);
rows.forEach((r) => {
if (!r) return;
const rect = r.getBoundingClientRect();
initialRowPositions.push({
row: r,
index: parseInt(r.getAttribute("data-index")),
top: rect.top,
height: rect.height,
});
// 重置所有行的transform
if (r !== row) {
r.style.transform = "";
}
});
// 创建占位符
placeholder = document.createElement("tr");
placeholder.classList.add("dragPlaceholder");
placeholder.style.height = `${row.offsetHeight}px`;
// 记录初始鼠标/触摸位置
const clientX =
event.clientX || (event.touches && event.touches[0].clientX);
const clientY =
event.clientY || (event.touches && event.touches[0].clientY);
event.target.setAttribute("data-start-x", clientX);
event.target.setAttribute("data-start-y", clientY);
// 将被拖拽的行设为绝对定位,使其脱离文档流
const rect = row.getBoundingClientRect();
row.style.position = "absolute";
row.style.zIndex = "1000";
row.style.width = `${rect.width}px`;
row.style.left = `${rect.left}px`;
row.style.top = `${rect.top}px`;
// 在原位置插入占位符
row.parentNode.insertBefore(placeholder, row.nextSibling);
// 初始化行位移记录
rowShifts = {};
rows.forEach((r) => {
if (r !== row) {
rowShifts[r.getAttribute("data-index")] = 0;
}
});
},
move: function (event) {
if (!isDragging || !draggedRow || !draggedRow.parentNode) return;
// 阻止默认行为,防止触摸设备上的滚动
event.preventDefault();
// 输出表格容器的滚动位置
const tableBox = document.querySelector(".table-box");
if (tableBox) {
console.log(
"表格容器滚动位置 (scrollTop):",
tableBox.scrollTop
);
}
// 更新拖拽行的位置,让它跟随鼠标/手指移动
const target = event.target;
// 获取当前鼠标/触摸位置
const clientX =
event.clientX || (event.touches && event.touches[0].clientX);
const clientY =
event.clientY || (event.touches && event.touches[0].clientY);
const dx =
clientX -
(parseFloat(target.getAttribute("data-start-x")) || 0);
const dy =
clientY -
(parseFloat(target.getAttribute("data-start-y")) || 0);
target.style.transform = `translate(${dx}px, ${dy}px)`;
// 查找最近的可放置行
const rows = Array.from(
taskTableBody.querySelectorAll(".draggable-row:not(.dragging)")
);
// 获取鼠标/触摸点的当前位置
const pointerY = clientY;
let closestRow = null;
let closestDistance = Infinity;
let insertAbove = false;
rows.forEach((row) => {
const box = row.getBoundingClientRect();
const rowMiddle = box.top + box.height / 2;
const distance = Math.abs(pointerY - rowMiddle);
if (distance < closestDistance) {
closestDistance = distance;
closestRow = row;
insertAbove = pointerY < rowMiddle;
}
});
// 如果找到最近的行,处理动画
if (closestRow) {
// 添加防抖动阈值 - 只有距离足够近才触发移动
const minDistanceThreshold = 20; // 设置一个合理的阈值,单位为像素
// 如果当前有目标,需要多一些距离才改变(实现滞后效果,防止临界点抖动)
const hysteresis = currentDropTarget === closestRow ? 10 : 0;
if (closestDistance > minDistanceThreshold + hysteresis) {
// 距离太远,不触发移动
return;
}
// 清除之前的目标样式
if (currentDropTarget && currentDropTarget !== closestRow) {
currentDropTarget.classList.remove("drop-target");
}
currentDropTarget = closestRow;
closestRow.classList.add("drop-target");
// 更新插入指示线位置
if (dropIndicator) {
const rect = closestRow.getBoundingClientRect();
const tableRect = taskTableBody.getBoundingClientRect();
// 计算指示线的位置
let indicatorTop;
if (insertAbove) {
indicatorTop = rect.top - tableRect.top;
} else {
indicatorTop = rect.bottom - tableRect.top;
}
// 设置指示线位置
dropIndicator.style.top = `${indicatorTop}px`;
dropIndicator.style.left = `${tableRect.left}px`;
dropIndicator.style.width = `${tableRect.width}px`;
}
const closestRowIndex = parseInt(
closestRow.getAttribute("data-index")
);
const closestRowPos = initialRowPositions.find(
(item) => item.index === closestRowIndex
);
if (!closestRowPos) return;
// 行高,用于计算偏移量
const rowHeight = draggedRow.offsetHeight;
// 找出被拖拽行在初始位置数组中的索引
const draggedRowPosIndex = initialRowPositions.findIndex(
(item) => item.index === draggedRowIndex
);
// 找出目标行在初始位置数组中的索引
const targetRowPosIndex = initialRowPositions.findIndex(
(item) => item.index === closestRowIndex
);
// 如果目标行和拖拽行是同一行,不做处理
if (targetRowPosIndex === draggedRowPosIndex) {
return;
}
// 重置所有行的偏移量
rows.forEach((row) => {
const rowIndex = parseInt(row.getAttribute("data-index"));
rowShifts[rowIndex] = 0;
});
// 设置受影响行的偏移量
if (draggedRowPosIndex < targetRowPosIndex) {
// 向下拖动
if (insertAbove) {
// 插入到目标行上方
for (let i = 0; i < initialRowPositions.length; i++) {
const pos = initialRowPositions[i];
// 只处理位于拖动行和目标行之间的行
if (i > draggedRowPosIndex && i < targetRowPosIndex) {
rowShifts[pos.index] = -rowHeight;
if (pos.row) {
pos.row.style.transform = `translateY(${-rowHeight}px)`;
}
} else if (pos.index !== draggedRowIndex) {
// 恢复其他行
if (pos.row) {
pos.row.style.transform = "";
}
}
}
} else {
// 插入到目标行下方
for (let i = 0; i < initialRowPositions.length; i++) {
const pos = initialRowPositions[i];
// 只处理位于拖动行和目标行之间的行(不包括目标行)
if (i > draggedRowPosIndex && i <= targetRowPosIndex) {
rowShifts[pos.index] = -rowHeight;
if (pos.row) {
pos.row.style.transform = `translateY(${-rowHeight}px)`;
}
} else if (pos.index !== draggedRowIndex) {
// 恢复其他行
if (pos.row) {
pos.row.style.transform = "";
}
}
}
}
} else if (draggedRowPosIndex > targetRowPosIndex) {
// 向上拖动
if (insertAbove) {
// 插入到目标行上方
for (let i = 0; i < initialRowPositions.length; i++) {
const pos = initialRowPositions[i];
// 只处理位于目标行和拖动行之间的行(包括目标行)
if (i >= targetRowPosIndex && i < draggedRowPosIndex) {
rowShifts[pos.index] = rowHeight;
if (pos.row) {
pos.row.style.transform = `translateY(${rowHeight}px)`;
}
} else if (pos.index !== draggedRowIndex) {
// 恢复其他行
if (pos.row) {
pos.row.style.transform = "";
}
}
}
} else {
// 插入到目标行下方
for (let i = 0; i < initialRowPositions.length; i++) {
const pos = initialRowPositions[i];
// 只处理位于目标行之后和拖动行之间的行
if (i > targetRowPosIndex && i < draggedRowPosIndex) {
rowShifts[pos.index] = rowHeight;
if (pos.row) {
pos.row.style.transform = `translateY(${rowHeight}px)`;
}
} else if (pos.index !== draggedRowIndex) {
// 恢复其他行
if (pos.row) {
pos.row.style.transform = "";
}
}
}
}
}
}
},
end: function (event) {
if (!isDragging) return;
isDragging = false;
const row = event.target;
if (!row || !row.parentNode) return;
const tableBox = document.querySelector(".table-box");
const scrollTop = tableBox ? tableBox.scrollTop : 0;
// 记录重排前的索引位置
const targetIndex = draggedRowIndex;
// 恢复行的样式
row.classList.remove("dragging");
row.style.position = "";
row.style.zIndex = "";
row.style.width = "";
row.style.left = "";
row.style.top = "";
row.style.transform = "";
// 移除所有的目标样式
if (currentDropTarget) {
currentDropTarget.classList.remove("drop-target");
currentDropTarget = null;
}
// 移除插入指示线
if (dropIndicator && dropIndicator.parentNode) {
dropIndicator.parentNode.removeChild(dropIndicator);
dropIndicator = null;
}
// 计算最终目标位置
let targetPosIndex = -1;
// 基于行的偏移量确定最终位置
const draggedPosIndex = initialRowPositions.findIndex(
(item) => item.index === draggedRowIndex
);
// 查找有位移的行,确定新位置
const rowsWithPositiveShift = [];
const rowsWithNegativeShift = [];
for (const [index, shift] of Object.entries(rowShifts)) {
const posIndex = initialRowPositions.findIndex(
(item) => item.index === parseInt(index)
);
if (shift > 0) {
rowsWithPositiveShift.push({
index: parseInt(index),
posIndex,
shift,
});
} else if (shift < 0) {
rowsWithNegativeShift.push({
index: parseInt(index),
posIndex,
shift,
});
}
}
if (rowsWithPositiveShift.length > 0) {
// 向上拖动,目标位置是最上面有正位移的行的位置
rowsWithPositiveShift.sort((a, b) => a.posIndex - b.posIndex);
targetPosIndex = rowsWithPositiveShift[0].posIndex;
} else if (rowsWithNegativeShift.length > 0) {
// 向下拖动,目标位置是最下面有负位移的行的位置之后
rowsWithNegativeShift.sort((a, b) => b.posIndex - a.posIndex);
targetPosIndex = rowsWithNegativeShift[0].posIndex + 1;
} else {
// 没有位移,保持原位置
targetPosIndex = draggedPosIndex;
}
// 移除所有行的transform
const rows = Array.from(
taskTableBody.querySelectorAll(".draggable-row:not(.dragging)")
);
rows.forEach((r) => {
r.style.transform = "";
});
// 移除占位符
if (placeholder && placeholder.parentNode) {
placeholder.parentNode.removeChild(placeholder);
placeholder = null;
}
// 如果目标位置有效并且不是原始位置,更新任务顺序
if (
targetPosIndex >= 0 &&
targetPosIndex < initialRowPositions.length &&
targetPosIndex !== draggedPosIndex
) {
// 更新计划中的任务顺序
const plan = massagePlans[currentPlanName];
if (plan && plan.task_plan) {
// 获取目标位置的实际数据索引
const targetDataIndex =
targetPosIndex < initialRowPositions.length
? initialRowPositions[targetPosIndex].index
: plan.task_plan.length - 1;
// 调整目标索引
let finalTargetIndex = targetDataIndex;
if (targetPosIndex > draggedPosIndex) {
finalTargetIndex -= 1;
}
// 移动数组元素
const taskToMove = plan.task_plan[draggedRowIndex];
plan.task_plan.splice(draggedRowIndex, 1);
plan.task_plan.splice(finalTargetIndex, 0, taskToMove);
// 记录当前滚动位置
const savedScrollTop = scrollTop;
console.log("保存当前滚动位置:", savedScrollTop);
// 重新渲染表格
renderPlanDetail(plan);
// 在渲染后恢复滚动位置
setTimeout(() => {
const tableBox = document.querySelector(".table-box");
if (tableBox) {
tableBox.scrollTop = savedScrollTop;
console.log("恢复滚动位置:", savedScrollTop);
// 防止滚动被其他代码重置
let retryCount = 0;
const checkInterval = setInterval(() => {
if (Math.abs(tableBox.scrollTop - savedScrollTop) > 5) {
console.log(
"滚动位置被重置,重新恢复到:",
savedScrollTop
);
tableBox.scrollTop = savedScrollTop;
}
if (++retryCount >= 5) {
clearInterval(checkInterval);
}
}, 50); // 每50ms检查一次最多检查5次
}
// 重新设置拖拽功能
setupDragSort();
}, 100);
return;
}
} else {
// 如果没有移动或移动失败,重新渲染表格(恢复原状)
renderPlanDetail(massagePlans[currentPlanName]);
// 恢复滚动位置
if (tableBox) {
tableBox.scrollTop = scrollTop;
}
setTimeout(setupDragSort, 0);
}
// 清理变量
draggedRow = null;
draggedRowIndex = -1;
initialRowPositions = [];
rowShifts = {};
},
},
});
}
// 扩展channel.onmessage处理函数
channel.onmessage = (event) => {
const { type, data } = event.data;
console.log(type, data);
if (type === "points_coordinates") {
// 更新points对象
Object.assign(points, data);
// 如果当前有选中的点,更新它们的坐标并重新绘制
if (selectedPoints.length > 0) {
selectedPoints.forEach((point) => {
if (data[point.name]) {
point.x = data[point.name][0];
point.y = data[point.name][1];
// 只有在可视化模态框打开时才更新显示
if (
document.getElementById("visualizationModal").style
.display === "flex"
) {
const index = selectedPoints.indexOf(point) + 1;
document.getElementById(`point${index}`).textContent = `${
index === 1 ? "起点穴位" : "终点穴位"
}${point.name} (${Math.round(point.x)}, ${Math.round(
point.y
)})`;
}
}
});
// 如果所有点都有坐标了,发送绘制消息
if (
selectedPoints.every((point) => point.x !== 0 && point.y !== 0)
) {
// 检查可视化模态框是否打开
const isModalOpen =
document.getElementById("visualizationModal").style.display ===
"flex";
// 获取当前任务数据(无论模态框是否打开)
const task =
currentEditingRow >= 0
? massagePlans[currentPlanName].task_plan[currentEditingRow]
: null;
// 确定要使用哪些参数
let params = undefined;
if (currentMode !== "point" && currentMode !== "point_rub" && currentMode !== "line" && currentMode !== "none") {
if (isModalOpen) {
// 从UI控件获取参数编辑模式
const sliderWidth = parseInt(
document.getElementById("widthSlider").value
);
// 转换宽度参数
const actualWidth = convertWidthParam(sliderWidth);
params = {
width: actualWidth,
cycles: parseInt(
document.getElementById("cyclesSlider").value
),
direction: document.querySelector(
".direction-button.active"
).dataset.direction,
};
} else if (task) {
// 使用任务数据中的参数(查看模式)
// 如果任务中保存的是滑条值,则需要转换
const width = task.width || DEFAULT_PARAMS.width;
const actualWidth = convertWidthParam(width);
params = {
width: actualWidth,
cycles: task.cycles || DEFAULT_PARAMS.cycles,
direction: task.direction || DEFAULT_PARAMS.direction,
};
}
}
channel.postMessage({
type: "draw_shape",
mode: currentMode,
points: selectedPoints,
params: params,
});
}
}
} else if (type === "point_selected") {
// 处理点选择消息
if (currentMode === "none") return;
console.log(currentMode);
// none模式和point模式的处理逻辑一致都是定穴点按法
if (currentMode === "none" || currentMode === "point" || currentMode === "point_rub") {
// 清空之前可能选择的点
if (selectedPoints.length > 0) {
selectedPoints = [];
document.getElementById("point1").textContent =
"起点穴位:未选择";
document.getElementById("point2").textContent =
"终点穴位:未选择";
// 发送清除点的消息
channel.postMessage({
type: "clear_points",
});
}
// 添加新的穴位为起点
selectedPoints.push(data);
// document.getElementById("point1").textContent = `起点穴位:${
// data.name
// } (${Math.round(data.x)}, ${Math.round(data.y)})`;
// document.getElementById("point2").textContent = `终点穴位:${
// data.name
// } (${Math.round(data.x)}, ${Math.round(data.y)})`;
document.getElementById("point1").textContent = `起点穴位:${
data.name
}`;
document.getElementById("point2").textContent = `终点穴位:${
data.name
}`;
// 发送绘制消息使用point模式
channel.postMessage({
type: "draw_shape",
mode: "point",
points: selectedPoints,
params: undefined,
});
return;
}
// 如果已经有两个点且不是point模式清空选择
if (selectedPoints.length >= 2 && currentMode !== "point" && currentMode !== "point_rub") {
selectedPoints = [];
document.getElementById("point1").textContent = "起点穴位:未选择";
document.getElementById("point2").textContent = "终点穴位:未选择";
}
// 添加新的点
selectedPoints.push(data);
const pointIndex = selectedPoints.length;
// 只有在可视化模态框打开时才更新显示
if (
document.getElementById("visualizationModal").style.display ===
"flex"
) {
document.getElementById(`point${pointIndex}`).textContent = `${
pointIndex === 1 ? "起点穴位" : "终点穴位"
}${data.name} (${Math.round(data.x)}, ${Math.round(data.y)})`;
}
// 如果只有一个点,无论什么模式都发送点绘制
if (selectedPoints.length === 1) {
channel.postMessage({
type: "draw_shape",
mode: "point",
points: selectedPoints,
params: undefined,
});
}
// 如果已选择两个点,根据当前模式发送绘制消息
else if (selectedPoints.length === 2) {
// 检查可视化模态框是否打开
const isModalOpen =
document.getElementById("visualizationModal").style.display ===
"flex";
// 获取当前任务数据(无论模态框是否打开)
const task =
currentEditingRow >= 0
? massagePlans[currentPlanName].task_plan[currentEditingRow]
: null;
// 确定要使用哪些参数
let params = undefined;
if (
currentMode !== "point" &&
currentMode !== "line" &&
currentMode !== "none"
) {
if (isModalOpen) {
// 从UI控件获取参数编辑模式
const width = parseInt(
document.getElementById("widthSlider").value
);
const cycles = parseInt(
document.getElementById("cyclesSlider").value
);
const direction = document.querySelector(
".direction-button.active"
).dataset.direction;
params = {
width: width,
cycles: cycles,
direction: direction,
};
} else if (task) {
// 使用任务数据中的参数(查看模式)
const width = task.width || DEFAULT_PARAMS.width;
// 转换宽度参数
const actualWidth = convertWidthParam(width);
params = {
width: actualWidth,
cycles: task.cycles || DEFAULT_PARAMS.cycles,
direction: task.direction || DEFAULT_PARAMS.direction,
};
}
}
channel.postMessage({
type: "draw_shape",
mode: currentMode,
points: selectedPoints,
params: params,
});
}
}
};
// 初始化事件监听
document.addEventListener("DOMContentLoaded", () => {
// 模式按钮事件处理
document.querySelectorAll(".mode-button").forEach((btn) => {
btn.addEventListener("click", () => {
// 移除其他按钮的active类
document
.querySelectorAll(".mode-button")
.forEach((b) => b.classList.remove("active"));
// 添加当前按钮的active类
btn.classList.add("active");
const mode = btn.dataset.mode;
currentMode = mode;
// 清空已选择的穴位
selectedPoints = [];
document.getElementById("point1").textContent = "起点穴位:未选择";
document.getElementById("point2").textContent = "终点穴位:未选择";
// 发送清除点的消息
channel.postMessage({
type: "clear_points",
});
// 更新控制面板显示
updatePathControls(mode);
});
});
// 方向按钮事件处理
document.querySelectorAll(".direction-button").forEach((btn) => {
btn.addEventListener("click", () => {
document
.querySelectorAll(".direction-button")
.forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
updateVisualization();
});
});
// 圈数滑块事件处理
document
.getElementById("cyclesSlider")
.addEventListener("input", (e) => {
document.getElementById("cyclesValue").textContent = e.target.value;
updateVisualization();
});
// 自动开关事件处理
["time", "strength", "width"].forEach((param) => {
const auto = document.getElementById(`${param}Auto`);
const slider = document.getElementById(`${param}Slider`);
const value = document.getElementById(`${param}Value`);
const unit = document.getElementById(`${param}Unit`);
auto.addEventListener("change", () => {
if (auto.checked) {
slider.style.display = "none";
value.textContent = "自动";
if (unit) {
unit.style.display = "none";
}
} else {
slider.style.display = "block";
value.textContent = slider.value;
if (unit) {
unit.style.display = "contents";
}
}
updateVisualization();
});
});
// 滑块事件处理
["time", "strength", "width"].forEach((param) => {
const slider = document.getElementById(`${param}Slider`);
const value = document.getElementById(`${param}Value`);
const auto = document.getElementById(`${param}Auto`);
slider.addEventListener("input", (e) => {
if (!auto.checked) {
value.textContent = e.target.value;
}
if (param === "width") {
updateVisualization();
}
});
});
// 清除点按钮事件处理
document.getElementById("clearPoints").addEventListener("click", () => {
selectedPoints = [];
document.getElementById("point1").textContent = "起点穴位:未选择";
document.getElementById("point2").textContent = "终点穴位:未选择";
// 发送清除点的消息
channel.postMessage({
type: "clear_points",
});
});
});
// 关闭可视化模态框
function closeVisualization() {
document.getElementById("point1").textContent = "起点穴位:未选择";
document.getElementById("point2").textContent = "终点穴位:未选择";
document.getElementById("visualizationModal").style.display = "none";
currentEditingRow = -1;
// 清除已选择的点
selectedPoints = [];
// 发送清除可视化的消息
channel.postMessage({
type: "clear_points",
});
}
// 更新路径控制面板的显示
function updatePathControls(mode) {
const pathControls = document.getElementById("pathControls");
// 根据模式显示或隐藏路径控制面板
if (mode === "none" || mode === "point" || mode === "point_rub" || mode === "line") {
pathControls.style.display = "none";
} else {
pathControls.style.display = "block";
}
// 发送模式变更消息
channel.postMessage({
type: "mode_change",
mode: mode === "none" ? "point" : mode,
});
}
// 更新可视化
function updateVisualization() {
if (selectedPoints.length < 1) return;
const mode = document.querySelector(".mode-button.active").dataset.mode;
if (mode === "none") return;
// 获取当前参数
const sliderWidth = document.getElementById("widthAuto").checked
? DEFAULT_PARAMS.width
: parseInt(document.getElementById("widthSlider").value);
const actualWidth = convertWidthParam(sliderWidth);
const params = {
width: actualWidth,
cycles: parseInt(document.getElementById("cyclesSlider").value),
direction: document.querySelector(".direction-button.active").dataset
.direction,
};
// 发送绘制消息
channel.postMessage({
type: "draw_shape",
mode: mode === "none" ? "point" : mode,
points: selectedPoints,
params: mode !== "point" && mode !== "point_rub" && mode !== "line" ? params : undefined,
});
}
// 在非编辑模式下查看可视化,不打开模态框
function viewVisualization(index) {
// 保存当前查看的行索引(可能在后续功能中需要)
currentEditingRow = index;
// 获取任务数据
const task = massagePlans[currentPlanName].task_plan[index];
// 确保切换到正确的身体部位
const bodyPart = massagePlans[currentPlanName].body_part || "back";
// 发送身体部位切换消息
channel.postMessage({
type: "change_body_part",
body_part: bodyPart,
});
// 确定路径模式
// const pathMode = task.path === 'none' ? 'point' : task.path;
const pathMode = task.path;
// 设置当前模式,这对于从可视化页面接收到坐标点数据后的绘制非常重要
currentMode = pathMode;
// 清除已选择的点
selectedPoints = [];
// 发送清除点的消息
channel.postMessage({
type: "clear_points",
});
// 如果有已存在的穴位数据,加载并显示
if (task.start_point) {
// 添加起点穴位
selectedPoints.push({
name: task.start_point,
x: 0, // 临时坐标,将在收到坐标数据后更新
y: 0,
});
// 如果有终点且不同于起点,也添加进去
if (task.end_point && task.end_point !== task.start_point) {
selectedPoints.push({
name: task.end_point,
x: 0, // 临时坐标,将在收到坐标数据后更新
y: 0,
});
}
// 准备任务参数
let taskParams = undefined;
if (pathMode !== "point" && pathMode !== "point_rub" && pathMode !== "line") {
const width = task.width || DEFAULT_PARAMS.width;
// 转换宽度参数
const actualWidth = convertWidthParam(width);
taskParams = {
width: actualWidth,
cycles: task.cycles || DEFAULT_PARAMS.cycles,
direction: task.direction || DEFAULT_PARAMS.direction,
};
}
// 请求穴位坐标
channel.postMessage({
type: "get_points_coordinates",
points: [task.start_point, task.end_point].filter(Boolean),
});
// 发送模式变更消息
channel.postMessage({
type: "mode_change",
mode: pathMode,
});
}
}
// 创建新疗程相关函数
// 显示创建模态框
function showCreateModal() {
const modal = document.getElementById("createModal");
const input = document.getElementById("createPlanName");
const error = document.getElementById("createNameError");
// 确保错误提示是隐藏的
error.style.display = "none";
input.classList.remove("error");
input.value = "";
// 默认选中第一个按钮
selectBodyPart("back");
selectMassageHead("thermotherapy");
// 更新名称预览
updateNamePreview();
// 平滑显示模态框
modal.style.display = "flex";
modal.style.opacity = "0";
setTimeout(() => {
modal.style.opacity = "1";
}, 10);
// 聚焦输入框
setTimeout(() => {
input.focus();
}, 50);
}
// 关闭创建模态框
function closeCreateModal() {
const modal = document.getElementById("createModal");
// 平滑隐藏模态框
modal.style.opacity = "0";
setTimeout(() => {
modal.style.display = "none";
}, 300);
}
// 选择按摩部位
function selectBodyPart(value) {
const buttons = document.querySelectorAll(
"#bodyPartButtons .select-btn"
);
buttons.forEach((button) => {
if (button.getAttribute("data-value") === value) {
button.classList.add("active");
} else {
button.classList.remove("active");
}
});
// 更新名称预览
updateNamePreview();
}
// 选择按摩头
function selectMassageHead(value) {
const buttons = document.querySelectorAll(
"#massageHeadButtons .select-btn"
);
buttons.forEach((button) => {
if (button.getAttribute("data-value") === value) {
button.classList.add("active");
} else {
button.classList.remove("active");
}
});
// 更新名称预览
updateNamePreview();
}
// 更新名称预览
function updateNamePreview() {
const bodyPart = document
.querySelector("#bodyPartButtons .select-btn.active")
.getAttribute("data-value");
const massageHead = document
.querySelector("#massageHeadButtons .select-btn.active")
.getAttribute("data-value");
const planName =
document.getElementById("createPlanName").value.trim() ||
new Date()
.toLocaleString("zh-CN", {
year: "2-digit",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})
.replace(/[\s\/:]/g, "");
const bodyPartText = bodyPartMap[bodyPart] || bodyPart;
const massageHeadText = headTypeMap[massageHead] || massageHead;
document.querySelector(
"#planNamePreview .preview-content"
).textContent = `${massageHeadText}-${bodyPartText}-${planName}`;
}
// 确认创建新疗程
async function confirmCreatePlan() {
const input = document.getElementById("createPlanName");
const error = document.getElementById("createNameError");
const planName = input.value.trim();
// 获取选中的按摩部位和按摩头
const bodyPart = document
.querySelector("#bodyPartButtons .select-btn.active")
.getAttribute("data-value");
const massageHead = document
.querySelector("#massageHeadButtons .select-btn.active")
.getAttribute("data-value");
// 获取中文名称
const bodyPartText = bodyPartMap[bodyPart] || bodyPart;
const massageHeadText = headTypeMap[massageHead] || massageHead;
// 使用默认值如果名称为空
const suffix =
planName ||
new Date()
.toLocaleString("zh-CN", {
year: "2-digit",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})
.replace(/[\s\/:]/g, "");
// 构建完整的新疗程名称,使用中文名称,与预览一致
const fullPlanName = `${massageHeadText}-${bodyPartText}-${suffix}`;
if (isNameExists(fullPlanName)) {
input.classList.add("error");
error.textContent = "该名称已存在,请重新输入";
error.style.display = "block";
return;
}
try {
// 创建空白疗程数据,内部数据仍使用代码值而非中文
const planData = {
body_part: bodyPart, // 保存英文代码用于内部处理
choose_task: massageHead, // 保存英文代码用于内部处理
task_plan: [],
introduction: `${bodyPartText}${massageHeadText}按摩疗程`,
can_delete: true,
};
// 保存到服务器
const response = await fetch("/set_massage_plan", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
plan_name: fullPlanName, // 使用中文名称保存
plan_data: planData,
}),
});
const result = await response.json();
if (result.status === "success") {
// 更新本地数据
massagePlans[fullPlanName] = planData;
// 关闭模态框
closeCreateModal();
// 显示成功消息
setTimeout(() => {
// 打开新创建的疗程详情页
showPlanDetail(fullPlanName);
// 直接切换到编辑模式
setTimeout(() => {
editPlan();
}, 300);
}, 100);
} else {
error.textContent = "创建失败: " + result.message;
error.style.display = "block";
}
} catch (e) {
error.textContent = "创建失败: " + e.message;
error.style.display = "block";
}
}
// 监听创建按钮点击
document.addEventListener("DOMContentLoaded", function () {
// 创建按钮点击事件
const createBtn = document.getElementById("createBtn");
if (createBtn) {
createBtn.addEventListener("click", showCreateModal);
}
// 监听创建模态框中的按钮
const bodyPartButtons = document.querySelectorAll(
"#bodyPartButtons .select-btn"
);
bodyPartButtons.forEach((button) => {
button.addEventListener("click", function () {
selectBodyPart(this.getAttribute("data-value"));
});
});
const massageHeadButtons = document.querySelectorAll(
"#massageHeadButtons .select-btn"
);
massageHeadButtons.forEach((button) => {
button.addEventListener("click", function () {
selectMassageHead(this.getAttribute("data-value"));
});
});
// 监听名称输入
const createPlanNameInput = document.getElementById("createPlanName");
if (createPlanNameInput) {
createPlanNameInput.addEventListener("input", function () {
updateNamePreview();
const error = document.getElementById("createNameError");
if (error.style.display === "block") {
error.style.display = "none";
this.classList.remove("error");
}
});
// 回车确认
createPlanNameInput.addEventListener("keyup", function (event) {
if (event.key === "Enter") {
confirmCreatePlan();
}
});
}
});
</script>
</body>
</html>