## P1 数据库基础 - zqyy_app: 创建 auth/biz schema、FDW 连接 etl_feiqiu - etl_feiqiu: 创建 app schema RLS 视图、商品库存预警表 - 清理 assistant_abolish 残留数据 ## P2 ETL/DWS 扩展 - 新增 DWS 助教订单贡献度表 (dws.assistant_order_contribution) - 新增 assistant_order_contribution_task 任务及 RLS 视图 - member_consumption 增加充值字段、assistant_daily 增加处罚字段 - 更新 ODS/DWD/DWS 任务文档及业务规则文档 - 更新 consistency_checker、flow_runner、task_registry 等核心模块 ## P3 小程序鉴权系统 - 新增 xcx_auth 路由/schema(微信登录 + JWT) - 新增 wechat/role/matching/application 服务层 - zqyy_app 鉴权表迁移 + 角色权限种子数据 - auth/dependencies.py 支持小程序 JWT 鉴权 ## 文档与审计 - 新增 DOCUMENTATION-MAP 文档导航 - 新增 7 份 BD_Manual 数据库变更文档 - 更新 DDL 基线快照(etl_feiqiu 6 schema + zqyy_app auth) - 新增全栈集成审计记录、部署检查清单更新 - 新增 BACKLOG 路线图、FDW→Core 迁移计划 ## Kiro 工程化 - 新增 5 个 Spec(P1/P2/P3/全栈集成/核心业务) - 新增审计自动化脚本(agent_on_stop/build_audit_context/compliance_prescan) - 新增 6 个 Hook(合规检查/会话日志/提交审计等) - 新增 doc-map steering 文件 ## 运维与测试 - 新增 ops 脚本:迁移验证/API 健康检查/ETL 监控/集成报告 - 新增属性测试:test_dws_contribution / test_auth_system - 清理过期 export 报告文件 - 更新 .gitignore 排除规则
955 lines
41 KiB
HTML
955 lines
41 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>任务列表 - 球房运营助手</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||
<link href="../css/banner.css" rel="stylesheet">
|
||
<link href="../css/ai-icons.css" rel="stylesheet">
|
||
<script>
|
||
tailwind.config = {
|
||
theme: {
|
||
extend: {
|
||
colors: {
|
||
primary: '#0052d9',
|
||
'primary-light': '#ecf2fe',
|
||
success: '#00a870',
|
||
warning: '#ed7b2f',
|
||
error: '#e34d59',
|
||
'gray-1': '#f3f3f3',
|
||
'gray-2': '#eeeeee',
|
||
'gray-3': '#e7e7e7',
|
||
'gray-4': '#dcdcdc',
|
||
'gray-5': '#c5c5c5',
|
||
'gray-6': '#a6a6a6',
|
||
'gray-7': '#8b8b8b',
|
||
'gray-8': '#777777',
|
||
'gray-9': '#5e5e5e',
|
||
'gray-10': '#4b4b4b',
|
||
'gray-11': '#393939',
|
||
'gray-12': '#2c2c2c',
|
||
'gray-13': '#242424',
|
||
},
|
||
fontFamily: {
|
||
sans: ['Noto Sans SC', 'sans-serif'],
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
<style>
|
||
body {
|
||
font-family: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, sans-serif;
|
||
padding-bottom: 70px;
|
||
}
|
||
/* 任务卡片边框颜色 */
|
||
.task-card {
|
||
position: relative;
|
||
border-left: 4px solid transparent;
|
||
}
|
||
.task-card.high-priority {
|
||
border-left-color: #f43f5e;
|
||
}
|
||
.task-card.priority {
|
||
border-left-color: #f97316;
|
||
}
|
||
.task-card.relationship {
|
||
border-left-color: #ec4899;
|
||
}
|
||
.task-card.callback {
|
||
border-left-color: #14b8a6;
|
||
}
|
||
/* 标签颜色 - 圆角矩形 */
|
||
.tag-high-priority {
|
||
background: linear-gradient(135deg, #dc2626 0%, #ef4444 100%);
|
||
border-radius: 4px;
|
||
}
|
||
.tag-priority {
|
||
background: linear-gradient(135deg, #ea580c 0%, #f97316 100%);
|
||
border-radius: 4px;
|
||
}
|
||
.tag-relationship {
|
||
background: linear-gradient(135deg, #db2777 0%, #ec4899 100%);
|
||
border-radius: 4px;
|
||
}
|
||
.tag-callback {
|
||
background: linear-gradient(135deg, #0d9488 0%, #14b8a6 100%);
|
||
border-radius: 4px;
|
||
}
|
||
/* 进度条动画 */
|
||
.progress-bar {
|
||
background: linear-gradient(90deg, #f59e0b 0%, #fbbf24 50%, #fcd34d 100%);
|
||
transition: width 0.6s ease-out;
|
||
}
|
||
/* 6段档位进度条 */
|
||
.tier-progress {
|
||
display: flex;
|
||
gap: 2px;
|
||
height: 8px;
|
||
}
|
||
.tier-segment {
|
||
border-radius: 2px;
|
||
background: rgba(255,255,255,0.25);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.tier-segment.completed {
|
||
background: linear-gradient(135deg, #34d399 0%, #10b981 100%);
|
||
}
|
||
.tier-segment.current {
|
||
background: linear-gradient(90deg, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0.25) 50%, rgba(255,255,255,0.25) 100%);
|
||
overflow: hidden;
|
||
}
|
||
.tier-segment.current .tier-fill {
|
||
height: 100%;
|
||
background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
|
||
border-radius: 2px;
|
||
}
|
||
/* 红戳样式 - 透明印章风格 */
|
||
.red-stamp {
|
||
position: relative;
|
||
display: inline-flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
.stamp-badge {
|
||
position: absolute;
|
||
top: -2px;
|
||
right: -28px;
|
||
transform: rotate(-12deg) scale(0);
|
||
width: 52px;
|
||
height: 52px;
|
||
border: 3px solid rgb(239, 68, 68);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: transparent;
|
||
pointer-events: none;
|
||
opacity: 0;
|
||
box-shadow:
|
||
inset 0 0 0 2px rgba(255, 255, 255, 0.95),
|
||
inset 0 0 10px 2px rgba(255, 255, 255, 0.7),
|
||
inset 0 0 20px 4px rgba(255, 255, 255, 0.4);
|
||
}
|
||
/* 盖戳动画 */
|
||
@keyframes stampDown {
|
||
0% {
|
||
transform: rotate(-12deg) scale(3);
|
||
opacity: 0;
|
||
}
|
||
50% {
|
||
transform: rotate(-12deg) scale(0.9);
|
||
opacity: 0.9;
|
||
}
|
||
70% {
|
||
transform: rotate(-12deg) scale(1.05);
|
||
opacity: 0.85;
|
||
}
|
||
100% {
|
||
transform: rotate(-12deg) scale(1);
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
.stamp-badge.stamp-animate {
|
||
animation: stampDown 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||
}
|
||
.stamp-badge .thumb {
|
||
font-size: 18px;
|
||
line-height: 1;
|
||
}
|
||
.stamp-badge .stamp-text {
|
||
font-size: 11px;
|
||
color: rgb(220, 38, 38);
|
||
font-weight: bold;
|
||
margin-top: 1px;
|
||
text-shadow:
|
||
0 0 2px rgba(255,255,255,1),
|
||
0 0 4px rgba(255,255,255,0.9),
|
||
1px 1px 0 rgba(255,255,255,0.9),
|
||
-1px -1px 0 rgba(255,255,255,0.9),
|
||
1px -1px 0 rgba(255,255,255,0.9),
|
||
-1px 1px 0 rgba(255,255,255,0.9);
|
||
}
|
||
/* 进度条按比例宽度:0-100(45.45%), 100-130(13.64%), 130-160(13.64%), 160-190(13.64%), 190-220(13.64%) */
|
||
.tier-segment-0 { flex: 100; }
|
||
.tier-segment-1 { flex: 30; }
|
||
.tier-segment-2 { flex: 30; }
|
||
.tier-segment-3 { flex: 30; }
|
||
.tier-segment-4 { flex: 30; }
|
||
/* 下降趋势样式 */
|
||
.trend-down {
|
||
color: rgba(255,255,255,0.5);
|
||
font-size: 12px;
|
||
}
|
||
/* 业绩卡片文字样式 */
|
||
.stat-value {
|
||
color: #ffffff;
|
||
text-shadow: 0 1px 3px rgba(0,0,0,0.25);
|
||
}
|
||
.stat-highlight {
|
||
color: #6ee7b7;
|
||
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||
}
|
||
.stat-label {
|
||
color: rgba(255,255,255,0.9);
|
||
text-shadow: 0 1px 2px rgba(0,0,0,0.15);
|
||
}
|
||
.stat-secondary {
|
||
color: rgba(255,255,255,0.7);
|
||
}
|
||
.stat-accent {
|
||
color: #fcd34d;
|
||
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||
}
|
||
/* 奖金金额突出样式 */
|
||
.bonus-amount {
|
||
text-shadow:
|
||
0 2px 4px rgba(0, 0, 0, 0.35),
|
||
0 0 12px rgba(251, 191, 36, 0.5);
|
||
}
|
||
/* === 任务分区 === */
|
||
.section-label {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
padding: 3px 10px;
|
||
border-radius: 4px;
|
||
}
|
||
/* 置顶卡片微亮边框 */
|
||
.task-card.pinned {
|
||
box-shadow: 0 1px 4px rgba(245, 158, 11, 0.12), 0 0 0 1px rgba(245, 158, 11, 0.08);
|
||
}
|
||
/* 放弃任务 */
|
||
.task-card.abandoned {
|
||
border-left-color: #d1d5db !important;
|
||
opacity: 0.55;
|
||
}
|
||
.task-card.abandoned .task-name {
|
||
color: #9ca3af;
|
||
}
|
||
.task-card.abandoned .task-desc {
|
||
color: #c5c5c5;
|
||
}
|
||
.task-card.abandoned .task-tag-wrap > span:first-child {
|
||
background: #d1d5db !important;
|
||
}
|
||
/* 备注指示器 */
|
||
.note-indicator {
|
||
font-size: 12px;
|
||
margin-left: 2px;
|
||
}
|
||
/* === 长按上下文菜单 === */
|
||
.context-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.35);
|
||
z-index: 100;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.2s ease;
|
||
}
|
||
.context-overlay.active {
|
||
opacity: 1;
|
||
pointer-events: all;
|
||
}
|
||
.context-menu {
|
||
position: fixed;
|
||
z-index: 101;
|
||
background: #fff;
|
||
border-radius: 14px;
|
||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
|
||
min-width: 192px;
|
||
padding: 6px 0;
|
||
opacity: 0;
|
||
transform: scale(0.88);
|
||
pointer-events: none;
|
||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||
transform-origin: top left;
|
||
}
|
||
.context-menu.active {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
pointer-events: all;
|
||
}
|
||
.ctx-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 13px 18px;
|
||
font-size: 14px;
|
||
color: #393939;
|
||
cursor: pointer;
|
||
transition: background 0.12s;
|
||
user-select: none;
|
||
}
|
||
.ctx-item:active {
|
||
background: #f3f3f3;
|
||
}
|
||
.ctx-item + .ctx-item {
|
||
border-top: 1px solid #f3f3f3;
|
||
}
|
||
/* === 备注 / 放弃弹窗 === */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 200;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: center;
|
||
padding-top: 18vh;
|
||
padding-bottom: 40vh;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.2s ease;
|
||
}
|
||
.modal-overlay.active {
|
||
opacity: 1;
|
||
pointer-events: all;
|
||
}
|
||
.modal-card {
|
||
background: #fff;
|
||
border-radius: 16px;
|
||
width: 88%;
|
||
max-width: 360px;
|
||
padding: 24px 20px 20px;
|
||
transform: translateY(24px);
|
||
transition: transform 0.25s ease;
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
||
}
|
||
.modal-overlay.active .modal-card {
|
||
transform: translateY(0);
|
||
}
|
||
.modal-card textarea {
|
||
width: 100%;
|
||
border: 1.5px solid #e7e7e7;
|
||
border-radius: 10px;
|
||
padding: 12px 14px;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
resize: none;
|
||
outline: none;
|
||
font-family: inherit;
|
||
color: #2c2c2c;
|
||
transition: border-color 0.2s;
|
||
box-sizing: border-box;
|
||
}
|
||
.modal-card textarea:focus {
|
||
border-color: #0052d9;
|
||
}
|
||
.modal-card textarea.error-border {
|
||
border-color: #e34d59;
|
||
}
|
||
.modal-error {
|
||
color: #e34d59;
|
||
font-size: 12px;
|
||
margin-top: 6px;
|
||
display: none;
|
||
}
|
||
.modal-error.show {
|
||
display: block;
|
||
}
|
||
.modal-submit-btn {
|
||
width: 100%;
|
||
padding: 12px;
|
||
border: none;
|
||
border-radius: 10px;
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
margin-top: 14px;
|
||
transition: background 0.2s, opacity 0.2s;
|
||
font-family: inherit;
|
||
}
|
||
.modal-submit-btn.primary {
|
||
background: #0052d9;
|
||
color: #fff;
|
||
}
|
||
.modal-submit-btn.primary:active {
|
||
background: #003eb3;
|
||
}
|
||
.modal-submit-btn.danger {
|
||
background: #e34d59;
|
||
color: #fff;
|
||
}
|
||
.modal-submit-btn.danger:active {
|
||
background: #c9363f;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="bg-gray-1 min-h-screen">
|
||
<!-- 顶部区域 - 用户信息和业绩卡片 -->
|
||
<div class="banner-bg theme-blue texture-aurora text-white pb-4">
|
||
<!-- 用户信息 -->
|
||
<div class="px-5 pt-10 pb-3">
|
||
<div class="flex items-center gap-4">
|
||
<div class="w-14 h-14 rounded-2xl bg-white/20 backdrop-blur-sm flex items-center justify-center shadow-lg overflow-hidden">
|
||
<img src="../img/zjtx.png" class="w-full h-full object-cover" alt="助教头像">
|
||
</div>
|
||
<div class="flex-1">
|
||
<div class="flex items-center gap-2 mb-1">
|
||
<span class="text-xl font-semibold">小燕</span>
|
||
<span class="px-2 py-0.5 bg-white/20 rounded-full text-xs">助教</span>
|
||
</div>
|
||
<p class="text-white/70 text-sm">广州朗朗桌球</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 业绩进度卡片 -->
|
||
<div class="mx-4">
|
||
<div class="bg-white/15 backdrop-blur-md rounded-2xl px-4 py-3 border border-white/20">
|
||
<!-- 第一层:标题行 -->
|
||
<div class="flex items-center justify-between mb-2">
|
||
<div class="flex items-baseline gap-2">
|
||
<span class="stat-label text-sm font-medium">距离100小时仅剩</span>
|
||
<span class="stat-accent text-xl font-bold">12.5小时</span>
|
||
</div>
|
||
<a href="performance.html" class="stat-secondary text-xs flex items-center gap-1 hover:text-white transition-colors">
|
||
查看详情
|
||
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<polyline points="9 18 15 12 9 6"/>
|
||
</svg>
|
||
</a>
|
||
</div>
|
||
|
||
<!-- 第二层:5段档位进度条 + 边界小时数(按比例宽度) -->
|
||
<div class="relative mb-6">
|
||
<div class="tier-progress">
|
||
<div class="tier-segment tier-segment-0 completed" title="0档 <100h"></div>
|
||
<div class="tier-segment tier-segment-1 current" title="1档 100-130h">
|
||
<div class="tier-fill" style="width: 58%"></div>
|
||
</div>
|
||
<div class="tier-segment tier-segment-2" title="2档 130-160h"></div>
|
||
<div class="tier-segment tier-segment-3" title="3档 160-190h"></div>
|
||
<div class="tier-segment tier-segment-4" title="4档 190-220h"></div>
|
||
</div>
|
||
<!-- 档位边界小时数(按比例定位:0, 100/220≈45.45%, 130/220≈59.09%, 160/220≈72.73%, 190/220≈86.36%, 220/220=100%) -->
|
||
<div class="absolute w-full top-full mt-1.5 flex text-[9px]" style="left: 0;">
|
||
<span class="text-white/60" style="position:absolute; left:0; transform:translateX(0);">0</span>
|
||
<span class="text-white/60" style="position:absolute; left:45.45%; transform:translateX(-50%);">100</span>
|
||
<span class="text-white/80 font-medium" style="position:absolute; left:59.09%; transform:translateX(-50%);">130</span>
|
||
<span class="text-white/60" style="position:absolute; left:72.73%; transform:translateX(-50%);">160</span>
|
||
<span class="text-white/60" style="position:absolute; left:86.36%; transform:translateX(-50%);">190</span>
|
||
<span class="text-white/60" style="position:absolute; right:0; transform:translateX(0);">220</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 第三层:核心数据 - 两列布局(左宽右窄) -->
|
||
<div class="flex items-stretch mb-2.5">
|
||
<!-- 左侧:课时数据 + 红戳(占60%) -->
|
||
<div class="pr-4 border-r border-white/25 flex justify-center items-center" style="flex: 3;">
|
||
<!-- 自动宽度容器:包含课时数据和红戳,居中显示 -->
|
||
<div class="red-stamp inline-block relative" style="padding-right: 35px;">
|
||
<div class="text-center">
|
||
<div class="flex items-baseline justify-center gap-1.5">
|
||
<span class="stat-highlight text-xl font-bold">77.5</span>
|
||
<span class="stat-secondary text-sm">|</span>
|
||
<span class="stat-accent text-xl font-bold">12</span>
|
||
<span class="stat-secondary text-sm">|</span>
|
||
<span class="stat-value text-xl font-bold">87.5</span>
|
||
</div>
|
||
<p class="stat-label text-xs mt-1.5">基础课 | 激励课 | 全部</p>
|
||
</div>
|
||
<!-- 红戳徽章 -->
|
||
<div class="stamp-badge" style="right: -5px; top: 5px;">
|
||
<span class="thumb">👍</span>
|
||
<span class="stamp-text">已完成</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 右侧:奖金激励(占40%) -->
|
||
<div class="pl-3 text-center flex flex-col justify-center" style="flex: 2;">
|
||
<div class="bonus-amount">
|
||
<span class="text-3xl font-bold text-amber-300">800</span>
|
||
<span class="text-base text-amber-300/80">元</span>
|
||
</div>
|
||
<p class="stat-label text-xs mt-1.5">达100h即得</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 第四层:预计收入 -->
|
||
<div class="flex items-center justify-between pt-2 border-t border-white/25">
|
||
<span class="stat-label text-xs">2月预计收入 | 比1月同期</span>
|
||
<a href="performance.html" class="flex items-center gap-1.5 group">
|
||
<span class="stat-value text-lg font-bold">¥6,206</span>
|
||
<span class="trend-down">↓368</span>
|
||
<svg class="w-3.5 h-3.5 stat-secondary group-hover:text-white transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<polyline points="9 18 15 12 9 6"/>
|
||
</svg>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 待办任务列表 -->
|
||
<div class="px-4 py-5">
|
||
<!-- 标题 -->
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h2 class="text-base font-semibold text-gray-13">今日 客户维护</h2>
|
||
<span class="text-sm text-gray-6">共 7 项</span>
|
||
</div>
|
||
|
||
<!-- 📌 置顶区域 -->
|
||
<div class="mb-5">
|
||
<div class="flex items-center gap-1.5 mb-2.5">
|
||
<span class="section-label text-amber-700 bg-amber-50">📌 置顶</span>
|
||
<span class="text-sm text-gray-6">2项</span>
|
||
</div>
|
||
<div class="space-y-3">
|
||
<!-- 置顶: 王先生 -->
|
||
<div class="task-card high-priority pinned block bg-white rounded-xl p-4 shadow-sm cursor-pointer" data-task-id="1" data-task-name="王先生" data-task-type="high-priority">
|
||
<div class="flex items-start justify-between">
|
||
<div class="flex-1">
|
||
<div class="flex items-center gap-2 mb-1.5 task-tag-wrap">
|
||
<span class="tag-high-priority px-2 py-0.5 text-white text-xs font-medium">高优先召回</span>
|
||
<span class="text-base font-semibold text-gray-13 task-name">王先生</span>
|
||
<span class="text-sm">💖</span>
|
||
<span class="note-indicator" title="有备注">📝</span>
|
||
</div>
|
||
<p class="text-sm text-gray-7 leading-relaxed task-desc">最近到店:15天前 · 余额:非常多</p>
|
||
<p class="text-sm text-gray-6 leading-relaxed task-desc"><span class="ai-inline-icon"></span>高流失风险,建议尽快联系</p>
|
||
</div>
|
||
<svg class="w-5 h-5 text-gray-5 flex-shrink-0 ml-2 mt-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<polyline points="9 18 15 12 9 6"/>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 置顶: 李女士 -->
|
||
<div class="task-card high-priority pinned block bg-white rounded-xl p-4 shadow-sm cursor-pointer" data-task-id="2" data-task-name="李女士" data-task-type="high-priority">
|
||
<div class="flex items-start justify-between">
|
||
<div class="flex-1">
|
||
<div class="flex items-center gap-2 mb-1.5 task-tag-wrap">
|
||
<span class="tag-high-priority px-2 py-0.5 text-white text-xs font-medium">高优先召回</span>
|
||
<span class="text-base font-semibold text-gray-13 task-name">李女士</span>
|
||
<span class="text-sm">🧡</span>
|
||
</div>
|
||
<p class="text-sm text-gray-7 leading-relaxed task-desc">最近到店:20天前 · 余额:非常多</p>
|
||
<p class="text-sm text-gray-6 leading-relaxed task-desc"><span class="ai-inline-icon"></span>VIP客户,储值余额较多</p>
|
||
</div>
|
||
<svg class="w-5 h-5 text-gray-5 flex-shrink-0 ml-2 mt-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<polyline points="9 18 15 12 9 6"/>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 一般任务区域 -->
|
||
<div class="mb-5">
|
||
<div class="flex items-center gap-1.5 mb-2.5">
|
||
<span class="section-label text-gray-9 bg-gray-2">一般任务</span>
|
||
<span class="text-sm text-gray-6">3项</span>
|
||
</div>
|
||
<div class="space-y-3">
|
||
<!-- 张先生 -->
|
||
<div class="task-card priority block bg-white rounded-xl p-4 shadow-sm cursor-pointer" data-task-id="3" data-task-name="张先生" data-task-type="priority">
|
||
<div class="flex items-start justify-between">
|
||
<div class="flex-1">
|
||
<div class="flex items-center gap-2 mb-1.5 task-tag-wrap">
|
||
<span class="tag-priority px-2 py-0.5 text-white text-xs font-medium">优先召回</span>
|
||
<span class="text-base font-semibold text-gray-13 task-name">张先生</span>
|
||
<span class="text-sm">💛</span>
|
||
<span class="note-indicator" title="有备注">📝</span>
|
||
</div>
|
||
<p class="text-sm text-gray-7 leading-relaxed task-desc">最近到店:10天前 · 余额:一般</p>
|
||
<p class="text-sm text-gray-6 leading-relaxed task-desc"><span class="ai-inline-icon"></span>消费频率下降,需关注</p>
|
||
</div>
|
||
<svg class="w-5 h-5 text-gray-5 flex-shrink-0 ml-2 mt-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<polyline points="9 18 15 12 9 6"/>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 刘先生 -->
|
||
<div class="task-card priority block bg-white rounded-xl p-4 shadow-sm cursor-pointer" data-task-id="4" data-task-name="刘先生" data-task-type="priority">
|
||
<div class="flex items-start justify-between">
|
||
<div class="flex-1">
|
||
<div class="flex items-center gap-2 mb-1.5 task-tag-wrap">
|
||
<span class="tag-priority px-2 py-0.5 text-white text-xs font-medium">优先召回</span>
|
||
<span class="text-base font-semibold text-gray-13 task-name">刘先生</span>
|
||
<span class="text-sm">💙</span>
|
||
</div>
|
||
<p class="text-sm text-gray-7 leading-relaxed task-desc">最近到店:8天前 · 余额:一般</p>
|
||
<p class="text-sm text-gray-6 leading-relaxed task-desc"><span class="ai-inline-icon"></span>偏好晚间时段,可推荐夜场套餐</p>
|
||
</div>
|
||
<svg class="w-5 h-5 text-gray-5 flex-shrink-0 ml-2 mt-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<polyline points="9 18 15 12 9 6"/>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 陈先生 -->
|
||
<div class="task-card relationship block bg-white rounded-xl p-4 shadow-sm cursor-pointer" data-task-id="5" data-task-name="陈先生" data-task-type="relationship">
|
||
<div class="flex items-start justify-between">
|
||
<div class="flex-1">
|
||
<div class="flex items-center gap-2 mb-1.5 task-tag-wrap">
|
||
<span class="tag-relationship px-2 py-0.5 text-white text-xs font-medium">关系构建</span>
|
||
<span class="text-base font-semibold text-gray-13 task-name">陈先生</span>
|
||
<span class="text-sm">💙</span>
|
||
</div>
|
||
<p class="text-sm text-gray-7 leading-relaxed task-desc">最近到店:5天前 · 余额:无</p>
|
||
<p class="text-sm text-gray-6 leading-relaxed task-desc"><span class="ai-inline-icon"></span>潜力客户,建议加强互动</p>
|
||
</div>
|
||
<svg class="w-5 h-5 text-gray-5 flex-shrink-0 ml-2 mt-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<polyline points="9 18 15 12 9 6"/>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ❌ 放弃的任务区域 -->
|
||
<div class="mb-2">
|
||
<div class="flex items-center gap-1.5 mb-2.5">
|
||
<span class="section-label text-gray-6 bg-gray-2">已放弃</span>
|
||
<span class="text-sm text-gray-6">2项</span>
|
||
</div>
|
||
<div class="space-y-3">
|
||
<!-- 放弃: 赵女士 -->
|
||
<div class="task-card callback abandoned block bg-white rounded-xl p-4 shadow-sm" data-task-id="6" data-task-name="赵女士" data-task-type="callback">
|
||
<div class="flex items-start justify-between">
|
||
<div class="flex-1">
|
||
<div class="flex items-center gap-2 mb-1.5 task-tag-wrap">
|
||
<span class="tag-callback px-2 py-0.5 text-white text-xs font-medium">客户回访</span>
|
||
<span class="text-base font-semibold text-gray-13 task-name">赵女士</span>
|
||
<span class="text-sm">🧡</span>
|
||
<span class="note-indicator" title="有备注">📝</span>
|
||
</div>
|
||
<p class="text-sm text-gray-7 leading-relaxed task-desc">最近到店:3天前 · 余额:非常多</p>
|
||
<p class="text-sm text-gray-6 leading-relaxed task-desc">放弃原因:客户已转会至其他球房</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 放弃: 周先生 -->
|
||
<div class="task-card callback abandoned block bg-white rounded-xl p-4 shadow-sm" data-task-id="7" data-task-name="周先生" data-task-type="callback">
|
||
<div class="flex items-start justify-between">
|
||
<div class="flex-1">
|
||
<div class="flex items-center gap-2 mb-1.5 task-tag-wrap">
|
||
<span class="tag-callback px-2 py-0.5 text-white text-xs font-medium">客户回访</span>
|
||
<span class="text-base font-semibold text-gray-13 task-name">周先生</span>
|
||
<span class="text-sm">💛</span>
|
||
<span class="note-indicator" title="有备注">📝</span>
|
||
</div>
|
||
<p class="text-sm text-gray-7 leading-relaxed task-desc">最近到店:5天前 · 余额:一般</p>
|
||
<p class="text-sm text-gray-6 leading-relaxed task-desc">放弃原因:联系方式失效,无法触达</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 长按上下文菜单 -->
|
||
<div class="context-overlay" id="contextOverlay"></div>
|
||
<div class="context-menu" id="contextMenu">
|
||
<div class="ctx-item" data-action="pin">
|
||
<span>📌</span><span>置顶任务</span>
|
||
</div>
|
||
<div class="ctx-item" data-action="abandon">
|
||
<span>❌</span><span>放弃任务</span>
|
||
</div>
|
||
<div class="ctx-item" data-action="ai">
|
||
<span>🤖</span><span>问问AI助手</span>
|
||
</div>
|
||
<div class="ctx-item" data-action="remark">
|
||
<span>📝</span><span>备注</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 备注 / 放弃 弹窗(可复用组件) -->
|
||
<div class="modal-overlay" id="remarkModal">
|
||
<div class="modal-card">
|
||
<h3 class="text-base font-semibold text-gray-13 mb-1" id="modalTitle">添加备注</h3>
|
||
<p class="text-xs text-gray-6 mb-3" id="modalSubtitle">为该客户的维护任务添加备注</p>
|
||
<textarea id="remarkInput" rows="4" placeholder="请输入备注内容..."></textarea>
|
||
<div class="modal-error" id="remarkError">请填写放弃原因,不能为空</div>
|
||
<button class="modal-submit-btn primary" id="modalSubmitBtn">保存备注</button>
|
||
<button class="mt-2 w-full text-center text-sm text-gray-6 py-2 bg-transparent border-0 cursor-pointer" id="modalCancelBtn">取消</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 悬浮助手按钮 -->
|
||
<script src="../js/ai-float-btn.js"></script>
|
||
|
||
<!-- 通用底部导航 -->
|
||
<script src="../js/bottom-nav.js"></script>
|
||
|
||
<!-- 盖戳动画 -->
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// 延迟一点触发盖戳动画,让页面先加载完
|
||
setTimeout(function() {
|
||
const stamp = document.querySelector('.stamp-badge');
|
||
if (stamp) {
|
||
stamp.classList.add('stamp-animate');
|
||
}
|
||
}, 300);
|
||
});
|
||
</script>
|
||
|
||
<!-- 长按菜单 + 备注弹窗 交互 -->
|
||
<script>
|
||
(function() {
|
||
/* ========== 导航映射 ========== */
|
||
var NAV = {
|
||
'high-priority': 'task-detail.html',
|
||
'priority': 'task-detail-priority.html',
|
||
'relationship': 'task-detail-relationship.html',
|
||
'callback': 'task-detail-callback.html'
|
||
};
|
||
|
||
/* ========== 长按检测 ========== */
|
||
var pressTimer = null;
|
||
var isLongPress = false;
|
||
var isMoved = false;
|
||
var startX = 0, startY = 0;
|
||
var currentCard = null;
|
||
var currentName = '';
|
||
var LONG_PRESS_MS = 500;
|
||
var MOVE_THRESHOLD = 8; // 超过8px判定为滑动,不触发点击
|
||
|
||
var overlay = document.getElementById('contextOverlay');
|
||
var menu = document.getElementById('contextMenu');
|
||
|
||
document.querySelectorAll('.task-card').forEach(function(card) {
|
||
/* --- touch --- */
|
||
card.addEventListener('touchstart', function(e) {
|
||
isLongPress = false;
|
||
isMoved = false;
|
||
startX = e.touches[0].clientX;
|
||
startY = e.touches[0].clientY;
|
||
currentCard = this;
|
||
var tx = startX, ty = startY, self = this;
|
||
pressTimer = setTimeout(function() {
|
||
isLongPress = true;
|
||
showContextMenu(tx, ty, self);
|
||
}, LONG_PRESS_MS);
|
||
}, { passive: true });
|
||
|
||
card.addEventListener('touchmove', function(e) {
|
||
var dx = Math.abs(e.touches[0].clientX - startX);
|
||
var dy = Math.abs(e.touches[0].clientY - startY);
|
||
if (dx > MOVE_THRESHOLD || dy > MOVE_THRESHOLD) {
|
||
isMoved = true;
|
||
clearTimeout(pressTimer);
|
||
}
|
||
}, { passive: true });
|
||
|
||
card.addEventListener('touchend', function(e) {
|
||
clearTimeout(pressTimer);
|
||
if (!isLongPress && !isMoved) {
|
||
navigateCard(this);
|
||
}
|
||
});
|
||
|
||
card.addEventListener('touchcancel', function() {
|
||
clearTimeout(pressTimer);
|
||
isMoved = true;
|
||
});
|
||
|
||
/* --- mouse (desktop preview) --- */
|
||
card.addEventListener('mousedown', function(e) {
|
||
if (e.button !== 0) return;
|
||
isLongPress = false;
|
||
isMoved = false;
|
||
startX = e.clientX;
|
||
startY = e.clientY;
|
||
currentCard = this;
|
||
var mx = e.clientX, my = e.clientY, self = this;
|
||
pressTimer = setTimeout(function() {
|
||
isLongPress = true;
|
||
showContextMenu(mx, my, self);
|
||
}, LONG_PRESS_MS);
|
||
});
|
||
|
||
card.addEventListener('mousemove', function(e) {
|
||
if (pressTimer && !isLongPress) {
|
||
var dx = Math.abs(e.clientX - startX);
|
||
var dy = Math.abs(e.clientY - startY);
|
||
if (dx > MOVE_THRESHOLD || dy > MOVE_THRESHOLD) {
|
||
isMoved = true;
|
||
clearTimeout(pressTimer);
|
||
}
|
||
}
|
||
});
|
||
|
||
card.addEventListener('mouseup', function() {
|
||
clearTimeout(pressTimer);
|
||
if (!isLongPress && !isMoved) {
|
||
navigateCard(this);
|
||
}
|
||
});
|
||
|
||
card.addEventListener('mouseleave', function() {
|
||
clearTimeout(pressTimer);
|
||
});
|
||
|
||
/* 禁用原生右键菜单 */
|
||
card.addEventListener('contextmenu', function(e) { e.preventDefault(); });
|
||
});
|
||
|
||
function navigateCard(card) {
|
||
var type = card.dataset.taskType;
|
||
if (NAV[type]) window.location.href = NAV[type];
|
||
}
|
||
|
||
/* ========== 上下文菜单 ========== */
|
||
function showContextMenu(x, y, card) {
|
||
currentCard = card;
|
||
currentName = card.dataset.taskName || '';
|
||
|
||
var mw = 192, mh = menu.offsetHeight || 220;
|
||
var left = x - mw / 2;
|
||
var top = y - mh - 12;
|
||
|
||
if (left < 10) left = 10;
|
||
if (left + mw > window.innerWidth - 10) left = window.innerWidth - mw - 10;
|
||
if (top < 10) top = y + 20;
|
||
|
||
menu.style.left = left + 'px';
|
||
menu.style.top = top + 'px';
|
||
|
||
overlay.classList.add('active');
|
||
menu.classList.add('active');
|
||
|
||
if (navigator.vibrate) navigator.vibrate(30);
|
||
}
|
||
|
||
function closeContextMenu() {
|
||
overlay.classList.remove('active');
|
||
menu.classList.remove('active');
|
||
}
|
||
|
||
overlay.addEventListener('click', closeContextMenu);
|
||
|
||
/* 菜单项点击 */
|
||
menu.querySelectorAll('.ctx-item').forEach(function(item) {
|
||
item.addEventListener('click', function() {
|
||
var action = this.dataset.action;
|
||
closeContextMenu();
|
||
switch (action) {
|
||
case 'pin':
|
||
showToast('已置顶「' + currentName + '」的任务');
|
||
break;
|
||
case 'abandon':
|
||
modalMode = 'abandon';
|
||
openRemarkModal();
|
||
break;
|
||
case 'ai':
|
||
showToast('正在唤起AI助手…');
|
||
break;
|
||
case 'remark':
|
||
modalMode = 'remark';
|
||
openRemarkModal();
|
||
break;
|
||
}
|
||
});
|
||
});
|
||
|
||
/* ========== 备注 / 放弃 弹窗 ========== */
|
||
var modalMode = 'remark';
|
||
var modalOverlay = document.getElementById('remarkModal');
|
||
var modalTitle = document.getElementById('modalTitle');
|
||
var modalSubtitle = document.getElementById('modalSubtitle');
|
||
var modalInput = document.getElementById('remarkInput');
|
||
var modalError = document.getElementById('remarkError');
|
||
var modalBtn = document.getElementById('modalSubmitBtn');
|
||
var modalCancel = document.getElementById('modalCancelBtn');
|
||
|
||
function openRemarkModal() {
|
||
modalInput.value = '';
|
||
modalError.classList.remove('show');
|
||
modalInput.classList.remove('error-border');
|
||
|
||
if (modalMode === 'abandon') {
|
||
modalTitle.textContent = '放弃任务 — ' + currentName;
|
||
modalSubtitle.textContent = '请填写放弃该客户维护任务的原因(必填)';
|
||
modalBtn.textContent = '确认原因 放弃该客户的维护';
|
||
modalBtn.className = 'modal-submit-btn danger';
|
||
modalInput.placeholder = '请输入放弃原因(必填)…';
|
||
} else {
|
||
modalTitle.textContent = '添加备注 — ' + currentName;
|
||
modalSubtitle.textContent = '为该客户的维护任务添加备注';
|
||
modalBtn.textContent = '保存备注';
|
||
modalBtn.className = 'modal-submit-btn primary';
|
||
modalInput.placeholder = '请输入备注内容…';
|
||
}
|
||
|
||
modalOverlay.classList.add('active');
|
||
setTimeout(function() { modalInput.focus(); }, 300);
|
||
}
|
||
|
||
function closeRemarkModal() {
|
||
modalOverlay.classList.remove('active');
|
||
modalInput.blur();
|
||
}
|
||
|
||
function submitRemark() {
|
||
var value = modalInput.value.trim();
|
||
|
||
if (modalMode === 'abandon' && !value) {
|
||
modalError.classList.add('show');
|
||
modalInput.classList.add('error-border');
|
||
modalInput.focus();
|
||
return;
|
||
}
|
||
|
||
closeRemarkModal();
|
||
|
||
if (modalMode === 'abandon') {
|
||
showToast('已放弃「' + currentName + '」的维护任务');
|
||
} else {
|
||
showToast('备注已保存');
|
||
}
|
||
}
|
||
|
||
modalBtn.addEventListener('click', submitRemark);
|
||
modalCancel.addEventListener('click', closeRemarkModal);
|
||
|
||
/* 点击遮罩关闭 */
|
||
modalOverlay.addEventListener('click', function(e) {
|
||
if (e.target === modalOverlay) closeRemarkModal();
|
||
});
|
||
|
||
/* 输入时清除错误提示 */
|
||
modalInput.addEventListener('input', function() {
|
||
if (this.value.trim()) {
|
||
modalError.classList.remove('show');
|
||
this.classList.remove('error-border');
|
||
}
|
||
});
|
||
|
||
/* ========== 轻提示 Toast ========== */
|
||
function showToast(msg) {
|
||
var t = document.getElementById('_toast');
|
||
if (!t) {
|
||
t = document.createElement('div');
|
||
t.id = '_toast';
|
||
t.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) scale(0.9);' +
|
||
'background:rgba(0,0,0,0.75);color:#fff;padding:12px 24px;border-radius:10px;' +
|
||
'font-size:14px;z-index:9999;opacity:0;transition:opacity .2s,transform .2s;' +
|
||
'pointer-events:none;text-align:center;max-width:80%;';
|
||
document.body.appendChild(t);
|
||
}
|
||
t.textContent = msg;
|
||
t.style.opacity = '1';
|
||
t.style.transform = 'translate(-50%,-50%) scale(1)';
|
||
setTimeout(function() {
|
||
t.style.opacity = '0';
|
||
t.style.transform = 'translate(-50%,-50%) scale(0.9)';
|
||
}, 1800);
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|