5594 lines
174 KiB
HTML
5594 lines
174 KiB
HTML
<!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()">
|
||
×
|
||
</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>
|