skill_detail.html 7.5 KB
{% 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;');}
</script>
{% endblock %}