skill_detail.html
7.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
{% extends "base.html" %}
{% block title %}{{ skill_name }} - Skills - WeChat Viewer{% endblock %}
{% block page_title %}{{ skill_name }}{% endblock %}
{% block body %}
<style>
body{overflow-y:auto!important;height:auto!important;}
.detail-container{max-width:960px;margin:0 auto;padding:20px;}
.detail-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;flex-wrap:wrap;gap:10px;}
.detail-header h1{font-size:20px;color:#2c3e50;}
.detail-header .back-btn{padding:6px 14px;background:#95a5a6;color:white;border:none;border-radius:6px;cursor:pointer;font-size:13px;text-decoration:none;}
.detail-stats{display:flex;gap:16px;margin-bottom:20px;flex-wrap:wrap;}
.stat-box{background:white;padding:12px 20px;border-radius:8px;box-shadow:0 1px 2px rgba(0,0,0,0.05);text-align:center;min-width:80px;}
.stat-box .num{font-size:22px;font-weight:bold;color:#2c3e50;}
.stat-box .lbl{font-size:11px;color:#999;margin-top:2px;}
.tabs{display:flex;gap:0;margin-bottom:0;}
.tab-btn{padding:10px 20px;background:#e8e8e8;border:none;cursor:pointer;font-size:13px;border-radius:8px 8px 0 0;color:#666;margin-right:2px;}
.tab-btn.active{background:white;color:#2c3e50;font-weight:600;}
.tab-content{display:none;background:white;border-radius:0 8px 8px 8px;padding:20px;box-shadow:0 1px 2px rgba(0,0,0,0.05);min-height:200px;}
.tab-content.active{display:block;}
.skill-content{font-size:14px;line-height:1.7;white-space:pre-wrap;word-break:break-word;max-height:70vh;overflow-y:auto;}
.skill-content h1,.skill-content h2,.skill-content h3{margin-top:16px;margin-bottom:8px;color:#2c3e50;}
.skill-content code{background:#f0f0f0;padding:2px 6px;border-radius:3px;font-size:13px;}
.skill-content pre{background:#2c3e50;color:#ecf0f1;padding:12px;border-radius:6px;overflow-x:auto;font-size:12px;line-height:1.5;}
.skill-content pre code{background:none;padding:0;color:inherit;}
.job-card{display:flex;align-items:center;gap:14px;background:#f8f9fa;border-radius:8px;padding:12px 16px;margin-bottom:8px;cursor:pointer;text-decoration:none;color:inherit;border:1px solid #eee;}
.job-card:hover{background:#e8f0fe;}
.job-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0;}
.job-dot.running{background:#27ae60;}
.job-dot.completed{background:#3498db;}
.job-dot.failed{background:#e74c3c;}
.job-info{flex:1;min-width:0;}
.job-info .jname{font-size:14px;font-weight:600;}
.job-info .jmeta{font-size:11px;color:#999;margin-top:3px;}
.loading{text-align:center;padding:40px;color:#999;}
.spinner{display:inline-block;width:24px;height:24px;border:3px solid #ddd;border-top-color:#3498db;border-radius:50%;animation:spin .6s linear infinite;}
@keyframes spin{to{transform:rotate(360deg)}}
.error-state{text-align:center;padding:60px 20px;color:#e74c3c;}
</style>
<div class="detail-container">
<div class="detail-header">
<h1>🧩 {{ skill_name }}</h1>
<a href="/skills" class="back-btn">← 返回列表</a>
</div>
<div id="stats-row" class="detail-stats"></div>
<div class="tabs">
<button class="tab-btn active" onclick="switchTab('content')">📄 SKILL.md</button>
<button class="tab-btn" id="jobs-tab-btn" onclick="switchTab('jobs')">📋 关联任务 <span id="jobs-count"></span></button>
</div>
<div id="tab-content-div" class="tab-content active">
<div class="loading"><div class="spinner"></div><p style="margin-top:12px;">加载中...</p></div>
</div>
<div id="tab-jobs-div" class="tab-content">
<div class="loading"><div class="spinner"></div><p style="margin-top:12px;">加载中...</p></div>
</div>
</div>
<script>
var currentTab = 'content';
var skillData = null;
function switchTab(tab) {
currentTab = tab;
document.querySelectorAll('.tab-btn').forEach(function(b){b.classList.remove('active')});
document.querySelectorAll('.tab-content').forEach(function(c){c.classList.remove('active')});
if (tab === 'content') {
document.querySelector('.tab-btn').classList.add('active');
document.getElementById('tab-content-div').classList.add('active');
} else {
document.getElementById('jobs-tab-btn').classList.add('active');
document.getElementById('tab-jobs-div').classList.add('active');
}
}
function renderMarkdown(text) {
if (!text) return '<p class="skill-meta">无内容</p>';
var lines = text.split('\n');
var html = '';
var inCodeBlock = false;
for (var i=0;i<lines.length;i++) {
var line = lines[i];
if (line.trim().startsWith('```')) {
if (inCodeBlock) { html += '</code></pre>\n'; inCodeBlock = false; }
else { html += '<pre><code>'; inCodeBlock = true; }
continue;
}
if (inCodeBlock) { html += escHtml(line) + '\n'; continue; }
if (/^###\s/.test(line)) { html += '<h3>' + escHtml(line.replace(/^###\s/,'')) + '</h3>\n'; }
else if (/^##\s/.test(line)) { html += '<h2>' + escHtml(line.replace(/^##\s/,'')) + '</h2>\n'; }
else if (/^#\s/.test(line)) { html += '<h1>' + escHtml(line.replace(/^#\s/,'')) + '</h1>\n'; }
else if (/^---\s*$/.test(line)) { html += '<hr>\n'; }
else if (/^\|.*\|$/.test(line) && /^\|[\s\-:]+\|$/.test(lines[i+1]||'')) { html += '<pre><code>' + escHtml(line) + '\n'; }
else if (/^\|.*\|$/.test(line)) { html += escHtml(line) + '\n'; }
else if (/^>\s/.test(line)) { html += '<blockquote style="color:#666;border-left:3px solid #ddd;padding-left:12px;margin:8px 0;">' + escHtml(line.replace(/^>\s/,'')) + '</blockquote>\n'; }
else if (/^[-*]\s/.test(line)) { html += '<li>' + escHtml(line.replace(/^[-*]\s/,'')) + '</li>\n'; }
else if (line.trim() === '') { html += '<br>\n'; }
else { html += escHtml(line) + '\n'; }
}
if (inCodeBlock) { html += '</code></pre>'; }
return html;
}
fetch('/api/skill/{{ skill_name }}').then(function(r){return r.json()}).then(function(j){
if (!j.ok) {
document.getElementById('tab-content-div').innerHTML = '<div class="error-state"><p>❌ Skill 未找到: {{ skill_name }}</p></div>';
return;
}
skillData = j.data;
// Stats row
var statsHtml = '';
statsHtml += '<div class="stat-box"><div class="num">' + (j.data.jobs.length) + '</div><div class="lbl">关联任务</div></div>';
statsHtml += '<div class="stat-box"><div class="num">' + (j.data.content ? Math.ceil(j.data.content.length/1024) : 0) + '</div><div class="lbl">KB</div></div>';
document.getElementById('stats-row').innerHTML = statsHtml;
// Content tab
document.getElementById('tab-content-div').innerHTML = '<div class="skill-content">' + renderMarkdown(j.data.content) + '</div>';
// Jobs tab
var jobsHtml = '';
if (!j.data.jobs.length) {
jobsHtml = '<div class="empty-state"><div class="icon">📋</div><p>暂无关联任务</p></div>';
} else {
for (var i=0;i<j.data.jobs.length;i++) {
var job = j.data.jobs[i];
jobsHtml += '<a href="/job/' + job.task_id + '" class="job-card">';
jobsHtml += '<div class="job-dot ' + job.status + '"></div>';
jobsHtml += '<div class="job-info"><div class="jname">' + escHtml(job.job_name || job.job_type) + '</div>';
jobsHtml += '<div class="jmeta">' + (job.started_at || '') + ' · ' + job.status + '</div></div>';
jobsHtml += '<div style="color:#ccc;font-size:16px;">→</div>';
jobsHtml += '</a>';
}
}
document.getElementById('tab-jobs-div').innerHTML = jobsHtml;
document.getElementById('jobs-count').textContent = '(' + j.data.jobs.length + ')';
}).catch(function(e){
document.getElementById('tab-content-div').innerHTML = '<div class="error-state"><p>⚠️ 加载失败: ' + e.message + '</p></div>';
});
function escHtml(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\"/g,'"');}
</script>
{% endblock %}