gitlink-cli/showcase/index.html

356 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>gitlink-cli 功能增强展示</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif; background: #f0f2f5; color: #333; }
.header { background: linear-gradient(135deg, #1a1a2e, #16213e); color: white; padding: 40px 20px; text-align: center; }
.header h1 { font-size: 2em; margin-bottom: 8px; }
.header p { color: #8892b0; font-size: 1.1em; }
.header .repo-info { margin-top: 12px; font-size: 0.9em; color: #64ffda; }
.header .repo-info code { background: rgba(255,255,255,0.1); padding: 4px 12px; border-radius: 4px; }
.stats-bar { display: flex; justify-content: center; gap: 32px; padding: 20px; background: white; border-bottom: 1px solid #e8e8e8; flex-wrap: wrap; }
.stat-item { text-align: center; }
.stat-item .num { font-size: 2em; font-weight: bold; color: #1890ff; }
.stat-item .num.green { color: #52c41a; }
.stat-item .label { color: #999; font-size: 0.85em; }
.container { max-width: 1200px; margin: 0 auto; padding: 24px; }
.module-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(550px, 1fr)); gap: 20px; }
.module-card { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow: hidden; }
.module-header { padding: 16px 20px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #f0f0f0; }
.module-header:hover { background: #fafafa; }
.module-header h3 { font-size: 1.1em; display: flex; align-items: center; gap: 8px; }
.module-header .tag { font-size: 0.75em; background: #e6f7ff; color: #1890ff; padding: 2px 8px; border-radius: 10px; }
.module-header .arrow { transition: transform 0.3s; font-size: 0.8em; color: #999; }
.module-header .arrow.open { transform: rotate(180deg); }
.module-body { padding: 0; }
.module-body.collapsed { display: none; }
.cmd-section { padding: 10px 20px 4px; color: #1890ff; font-weight: 600; font-size: 0.88em; background: #f0f5ff; border-bottom: 1px solid #e6f0ff; border-top: 1px solid #e6f0ff; }
.cmd-section:first-child { border-top: none; }
.cmd-row { border-bottom: 1px solid #f5f5f5; }
.cmd-row:last-child { border-bottom: none; }
.cmd-row:hover { background: #fafbfc; }
.cmd-top { display: flex; align-items: center; padding: 10px 20px; gap: 12px; }
.cmd-name { font-family: 'SFMono-Regular', Consolas, monospace; font-size: 0.9em; color: #1890ff; font-weight: 600; min-width: 70px; }
.cmd-desc { color: #666; font-size: 0.85em; flex: 1; }
.cmd-toggle { color: #999; cursor: pointer; font-size: 0.85em; border: none; background: none; }
.cmd-toggle:hover { color: #1890ff; }
.cmd-detail { padding: 0 20px 10px 90px; color: #999; font-size: 0.8em; display: none; }
.cmd-detail.show { display: block; }
.cmd-params { padding: 6px 20px 10px 90px; display: none; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
.cmd-params.show { display: flex; }
.param-group { display: flex; flex-direction: column; gap: 2px; }
.param-group label { font-size: 0.75em; color: #999; }
.param-group input { border: 1px solid #d9d9d9; border-radius: 4px; padding: 4px 8px; font-size: 0.85em; width: 140px; }
.param-group input:focus { border-color: #1890ff; outline: none; box-shadow: 0 0 0 2px rgba(24,144,255,0.2); }
.run-btn { background: #1890ff; color: white; border: none; padding: 5px 14px; border-radius: 6px; cursor: pointer; font-size: 0.85em; white-space: nowrap; align-self: flex-end; }
.run-btn:hover { background: #40a9ff; }
.run-btn:disabled { background: #d9d9d9; cursor: not-allowed; }
.result-panel { background: #1e1e1e; margin: 4px 20px 14px; border-radius: 8px; overflow: hidden; display: none; }
.result-panel.show { display: block; }
.result-toolbar { background: #2d2d2d; padding: 8px 12px; display: flex; justify-content: space-between; align-items: center; }
.result-toolbar .cmd-text { color: #4ec9b0; font-family: monospace; font-size: 0.85em; }
.result-toolbar .status { font-size: 0.8em; padding: 2px 8px; border-radius: 4px; }
.result-toolbar .status.ok { background: #2ea04380; color: #4ec9b0; }
.result-toolbar .status.err { background: #da363380; color: #f85149; }
.result-toolbar .close-btn { color: #666; cursor: pointer; font-size: 1.2em; }
.result-toolbar .close-btn:hover { color: #999; }
.result-body { padding: 12px; max-height: 400px; overflow: auto; }
.result-body pre { color: #d4d4d4; font-family: 'SFMono-Regular', Consolas, monospace; font-size: 0.82em; line-height: 1.5; white-space: pre-wrap; word-break: break-all; margin: 0; }
.footer { text-align: center; color: #999; padding: 40px 20px; font-size: 0.85em; }
@media (max-width: 600px) {
.module-grid { grid-template-columns: 1fr; }
.cmd-params { padding-left: 20px; }
}
</style>
</head>
<body>
<div class="header">
<h1>gitlink-cli 功能增强</h1>
<p>软件演化与运维 课程实践 — 进阶任务 子任务一</p>
<div class="repo-info">演示仓库:<code>chroe/gitlink-cli</code> — 展开命令、填入参数、点击运行查看真实结果</div>
</div>
<div class="stats-bar">
<div class="stat-item"><div class="num">9</div><div class="label">新增模块</div></div>
<div class="stat-item"><div class="num green">47</div><div class="label">新增命令</div></div>
<div class="stat-item"><div class="num">7</div><div class="label">批量操作</div></div>
<div class="stat-item"><div class="num">47+</div><div class="label">单元测试</div></div>
</div>
<div class="container"><div class="module-grid" id="modules"></div></div>
<div class="footer">gitlink-cli 功能增强 — 软件演化与运维课程实践</div>
<script>
const MODULES = [
{
name: "milestone", title: "里程碑管理",
desc: "管理项目的版本规划节点,可用于跟踪项目进度和发布计划",
color: "#722ed1",
commands: [
{ name: "list", desc: "列出所有里程碑", detail: "获取项目中所有已创建的里程碑列表,包括开启和关闭的数量统计", params: [] },
{ name: "view", desc: "查看里程碑详情", detail: "查看某个里程碑的详细信息,包括关联的疑修数量和完成进度", params: [{k:"id",l:"里程碑ID",v:"2760"}] },
{ name: "create", desc: "创建里程碑", detail: "新建一个版本规划节点,可设置名称、描述、截止日期", params: [{k:"name",l:"名称",v:"v2.0"},{k:"description",l:"描述",v:"新里程碑"},{k:"due",l:"截止日期",v:"2026-07-01"}] },
{ name: "update", desc: "更新里程碑", detail: "修改里程碑的名称、描述或截止日期", params: [{k:"id",l:"里程碑ID",v:"2760"},{k:"description",l:"新描述",v:"已更新"}] },
{ name: "close", desc: "关闭里程碑", detail: "标记里程碑为已完成状态", params: [{k:"id",l:"里程碑ID",v:"2760"}] },
{ name: "delete", desc: "删除里程碑", detail: "删除指定的里程碑(不可恢复)", params: [{k:"id",l:"里程碑ID",v:"2760"}] },
]
},
{
name: "webhook", title: "Webhook 配置",
desc: "管理项目的事件通知,当仓库发生 push、PR 等事件时自动通知外部服务",
color: "#eb2f96",
commands: [
{ name: "list", desc: "列出所有 Webhook", detail: "获取项目中配置的所有事件通知地址列表", params: [] },
{ name: "view", desc: "查看 Webhook 详情", detail: "查看某个 Webhook 的详细配置信息,包括事件类型和最近触发状态", params: [{k:"id",l:"Webhook ID",v:"51106"}] },
{ name: "create", desc: "创建 Webhook", detail: "新增一个事件通知配置,指定 URL 和监听的事件类型", params: [{k:"url",l:"回调URL",v:"https://httpbin.org/post"}] },
{ name: "update", desc: "更新 Webhook", detail: "修改 Webhook 的 URL、事件类型等配置", params: [{k:"id",l:"Webhook ID",v:"51106"},{k:"url",l:"新URL",v:"https://httpbin.org/post"}] },
{ name: "test", desc: "测试 Webhook", detail: "发送一个测试事件,验证 Webhook 配置是否正确", params: [{k:"id",l:"Webhook ID",v:"51106"}] },
{ name: "delete", desc: "删除 Webhook", detail: "删除指定的事件通知配置", params: [{k:"id",l:"Webhook ID",v:"51106"}] },
]
},
{
name: "label", title: "标签管理",
desc: "管理疑修Issue的标签用于分类和优先级标记",
color: "#fa8c16",
commands: [
{ name: "list", desc: "列出所有标签", detail: "获取项目中所有疑修标签,包括标签颜色和关联的疑修数量", params: [] },
{ name: "create", desc: "创建标签", detail: "新建一个疑修标签,可设置名称、颜色和描述。创建后点 +list 查看新标签的 ID", params: [{k:"name",l:"标签名",v:"test-label"},{k:"color",l:"颜色",v:"#ff6600"},{k:"description",l:"描述",v:"test"}] },
{ name: "update", desc: "更新标签", detail: "修改标签的名称、颜色或描述。先点 +list 查看 ID", params: [{k:"id",l:"标签ID",v:"328745"},{k:"description",l:"新描述",v:"updated"}] },
{ name: "delete", desc: "删除标签", detail: "删除指定的疑修标签。先点 +list 查看 ID", params: [{k:"id",l:"标签ID",v:"328745"}] },
]
},
{
name: "commit", title: "提交记录",
desc: "查看仓库的代码提交历史、变更文件和逐行追溯",
color: "#52c41a",
commands: [
{ name: "list", desc: "列出提交记录", detail: "查看仓库的所有代码提交历史,支持按分支/标签过滤和分页", params: [{k:"limit",l:"数量",v:"5"}] },
{ name: "view", desc: "查看提交详情", detail: "查看某次提交修改了哪些文件以及每个文件的增删行数。SHA 从 +list 获取", params: [{k:"sha",l:"提交SHA",v:"a82717bbf372aee48bbd1452c3d6be9256b0febf"}] },
{ name: "diff", desc: "查看代码差异", detail: "查看某次提交的具体代码变更内容。SHA 从 +list 获取", params: [{k:"sha",l:"提交SHA",v:"a82717bbf372aee48bbd1452c3d6be9256b0febf"}] },
{ name: "blame", desc: "逐行追溯", detail: "查看文件每一行代码是谁在什么时候、通过哪次提交修改的", params: [{k:"path",l:"文件路径",v:"README.md"}] },
]
},
{
name: "wiki", title: "Wiki 页面管理",
desc: "管理项目维基页面,支持创建、查看、编辑和删除 Wiki 文档",
color: "#13c2c2",
commands: [
{ name: "list", desc: "列出 Wiki 页面", detail: "获取项目中所有 Wiki 页面列表", params: [] },
{ name: "view", desc: "查看 Wiki 页面", detail: "查看某个 Wiki 页面的详细内容", params: [{k:"name",l:"页面名称",v:"Home"}] },
{ name: "create", desc: "创建 Wiki 页面", detail: "新建一个 Wiki 页面,内容自动进行 Base64 编码", params: [{k:"name",l:"页面标题",v:"TestPage"},{k:"content",l:"页面内容",v:"Hello Wiki!"},{k:"message",l:"提交信息",v:"create test page"}] },
{ name: "update", desc: "更新 Wiki 页面", detail: "修改已有的 Wiki 页面内容,内容自动进行 Base64 编码", params: [{k:"name",l:"页面标题",v:"Home"},{k:"content",l:"新内容",v:"Updated content"},{k:"message",l:"提交信息",v:"update page"}] },
{ name: "delete", desc: "删除 Wiki 页面", detail: "删除指定的 Wiki 页面(不可恢复)", params: [{k:"name",l:"页面标题",v:"TestPage"}] },
]
},
{
name: "file", title: "文件管理",
desc: "管理仓库中的文件和目录,支持在线创建、编辑、删除和查看文件",
color: "#2f54eb",
commands: [
{ name: "list", desc: "列出仓库文件", detail: "获取仓库指定路径下的文件和目录列表,支持分支选择和关键词搜索", params: [{k:"ref",l:"分支/标签",v:"master"}] },
{ name: "tree", desc: "查看文件树", detail: "以树形结构浏览仓库目录,支持递归展开所有子目录", params: [{k:"sha",l:"分支/提交SHA",v:"master"},{k:"limit",l:"每页数量",v:"20"}] },
{ name: "get", desc: "获取文件内容", detail: "查看指定文件或目录的内容详情", params: [{k:"path",l:"文件路径",v:"README.md"},{k:"ref",l:"分支",v:"master"}] },
{ name: "create", desc: "创建文件", detail: "在仓库中新建文件,内容自动进行 Base64 编码", params: [{k:"path",l:"文件路径",v:"test/hello.txt"},{k:"content",l:"文件内容",v:"Hello World!"},{k:"message",l:"提交信息",v:"add test file"},{k:"branch",l:"目标分支",v:"master"}] },
{ name: "delete", desc: "删除文件", detail: "删除仓库中的文件,需要提供文件的 blob SHA", params: [{k:"path",l:"文件路径",v:"test/hello.txt"},{k:"sha",l:"文件SHA",v:""},{k:"message",l:"提交信息",v:"delete test file"},{k:"branch",l:"目标分支",v:"master"}] },
]
},
{
name: "member", title: "成员管理",
desc: "管理项目成员支持添加、移除、修改角色和查看成员列表。用户ID 可用 search +users 查找",
color: "#f5222d",
commands: [
{ name: "list", desc: "列出项目成员", detail: "获取项目中所有协作者的列表,返回结果中包含每位成员的 id数字和 login用户名可按关键词搜索", params: [{k:"keyword",l:"搜索关键词",v:""}] },
{ name: "add", desc: "添加成员", detail: "邀请用户加入项目成为协作者。user-id 是数字 ID可先点 +list 查看现有成员的 ID或用 search +users 按用户名查找", params: [{k:"user-id",l:"用户数字ID",v:"141645"}] },
{ name: "remove", desc: "移除成员", detail: "将指定用户移出项目。先点 +list 查看成员的数字 ID", params: [{k:"user-id",l:"用户数字ID",v:"141645"}] },
{ name: "update", desc: "修改成员角色", detail: "更改成员的项目角色Manager管理员、Developer开发者、Reporter报告者。先点 +list 查看成员 ID", params: [{k:"user-id",l:"用户数字ID",v:"141645"},{k:"role",l:"角色",v:"Developer"}] },
]
},
{
name: "watch", title: "仓库关注",
desc: "关注仓库以接收更新通知,支持关注/取消关注和查看关注者列表",
color: "#faad14",
commands: [
{ name: "watch", desc: "关注仓库", detail: "开始关注当前仓库,接收后续的事件通知", params: [] },
{ name: "unwatch", desc: "取消关注", detail: "停止关注当前仓库,不再接收通知", params: [] },
{ name: "watchers", desc: "查看关注者", detail: "列出关注了当前仓库的所有用户", params: [] },
]
},
{
name: "star", title: "仓库点赞",
desc: "为仓库点赞Star表达对项目的认可和支持",
color: "#eb2f96",
commands: [
{ name: "star", desc: "点赞仓库", detail: "为当前仓库点赞Star", params: [] },
{ name: "unstar", desc: "取消点赞", detail: "取消对当前仓库的点赞", params: [] },
{ name: "stars", desc: "查看点赞者", detail: "列为当前仓库点赞的所有用户", params: [] },
]
},
{
name: "issue", title: "批量 Issue 操作",
desc: "批量管理疑修Issue批量关闭、分配、设置标签和里程碑支持逗号列表或 CSV 文件导入",
color: "#13c2c2",
commands: [
{ name: "batch-close", desc: "批量关闭 Issue", detail: "按编号批量关闭 Issue支持 --numbers 逗号分隔或 --from CSV 文件。开启预览模式可先查看将执行的操作而不实际执行", params: [{k:"numbers",l:"Issue编号",v:"1,2,3"},{k:"dry-run",l:"预览模式",v:"false",bool:true}] },
{ name: "batch-assign", desc: "批量分配 Issue", detail: "将多个 Issue 批量分配给指定用户处理,用户名为 GitLink 登录名", params: [{k:"numbers",l:"Issue编号",v:"1,2,3"},{k:"user",l:"负责人用户名",v:"chroe"},{k:"dry-run",l:"预览模式",v:"false",bool:true}] },
{ name: "batch-label", desc: "批量设置标签", detail: "批量为 Issue 添加或移除标签。标签ID 先用 label +list 查看", params: [{k:"numbers",l:"Issue编号",v:"1,2,3"},{k:"add",l:"添加标签ID",v:""},{k:"remove",l:"移除标签ID",v:""},{k:"dry-run",l:"预览模式",v:"false",bool:true}] },
{ name: "batch-milestone", desc: "批量设置里程碑", detail: "批量将 Issue 关联到指定的里程碑。里程碑ID 先用 milestone +list 查看", params: [{k:"numbers",l:"Issue编号",v:"1,2,3"},{k:"milestone",l:"里程碑ID",v:"2760"},{k:"dry-run",l:"预览模式",v:"false",bool:true}] },
]
},
{
name: "repo", title: "批量仓库管理",
desc: "批量删除或 Fork 仓库,支持逗号分隔列表或 CSV 文件批量导入,建议先用预览模式确认",
color: "#722ed1",
commands: [
{ name: "batch-delete", desc: "批量删除仓库", detail: "按 owner/repo 格式批量删除仓库,支持 --repos 逗号分隔或 --from CSV 文件。⚠️ 危险操作,建议先开启预览模式确认", params: [{k:"repos",l:"仓库列表",v:"chroe/test1,chroe/test2"},{k:"dry-run",l:"预览模式",v:"true",bool:true}] },
{ name: "batch-fork", desc: "批量 Fork 仓库", detail: "批量 Fork 多个仓库到自己的账号下", params: [{k:"repos",l:"仓库列表",v:"chroe/test1,chroe/test2"},{k:"dry-run",l:"预览模式",v:"true",bool:true}] },
]
},
{
name: "org", title: "批量组织邀请",
desc: "批量邀请成员加入组织,支持逗号分隔或 CSV 文件批量导入,建议先用预览模式确认",
color: "#fa8c16",
commands: [
{ name: "batch-invite", desc: "批量邀请成员", detail: "批量邀请用户加入组织支持设置角色member/admin支持 --users 逗号分隔或 --from CSV 文件", params: [{k:"id",l:"组织ID",v:"1"},{k:"users",l:"用户名列表",v:"alice,bob"},{k:"role",l:"角色",v:"member"},{k:"dry-run",l:"预览模式",v:"true",bool:true}] },
]
},
];
const container = document.getElementById('modules');
MODULES.forEach(mod => {
const card = document.createElement('div');
card.className = 'module-card';
const cmdHTML = mod.commands.map(cmd => {
if (cmd.section) {
return `<div class="cmd-section">${cmd.section}</div>`;
}
const uid = `${mod.name}-${cmd.name}`;
const paramsHTML = cmd.params.map(p => {
if (p.bool) {
return `<div class="param-group"><label>${p.l}</label><select id="p-${uid}-${p.k}"><option value="false"${p.v!=='true'?' selected':''}>否</option><option value="true"${p.v==='true'?' selected':''}>是</option></select></div>`;
}
return `<div class="param-group"><label>${p.l}</label><input id="p-${uid}-${p.k}" value="${p.v}" placeholder="${p.l}"></div>`;
}).join('');
return `
<div class="cmd-row">
<div class="cmd-top">
<span class="cmd-name">+${cmd.name}</span>
<span class="cmd-desc">${cmd.desc}</span>
<button class="cmd-toggle" onclick="toggleDetail('${uid}')">详情</button>
</div>
<div class="cmd-detail" id="detail-${uid}">${cmd.detail}</div>
<div class="cmd-params" id="params-${uid}">
${paramsHTML}
<button class="run-btn" onclick="runCommand('${mod.name}','${cmd.name}','${uid}')" id="btn-${uid}">▶ 运行</button>
</div>
<div class="result-panel" id="result-${uid}"></div>
</div>`;
}).join('');
card.innerHTML = `
<div class="module-header" onclick="toggleModule(this)">
<h3><span style="color:${mod.color};font-size:1.3em;">&#9632;</span> ${mod.title} <span class="tag">${mod.commands.length} 个命令</span></h3>
<span class="arrow">&#9660;</span>
</div>
<div class="module-body collapsed">
<p style="padding:12px 20px;color:#666;font-size:0.9em;border-bottom:1px solid #f0f0f0;background:#fafbfc;">${mod.desc}</p>
${cmdHTML}
</div>`;
container.appendChild(card);
});
function toggleModule(header) {
const body = header.nextElementSibling;
const arrow = header.querySelector('.arrow');
body.classList.toggle('collapsed');
arrow.classList.toggle('open');
}
function toggleDetail(uid) {
const detail = document.getElementById(`detail-${uid}`);
const params = document.getElementById(`params-${uid}`);
detail.classList.toggle('show');
params.classList.toggle('show');
}
function getArgs(uid, params) {
return params.map(p => {
const val = document.getElementById(`p-${uid}-${p.k}`).value.trim();
// Boolean flag: only pass --flag when value is "true", omit for false
if (p.bool) {
return val === 'true' ? `--${p.k}` : '';
}
if (!val) return '';
// Use = syntax for bool values (pflag requires it)
if (val === 'true' || val === 'false') {
return `--${p.k}=${val}`;
}
if (val.includes(' ')) {
return `--${p.k} "${val}"`;
}
return `--${p.k} ${val}`;
}).filter(Boolean).join(' ');
}
async function runCommand(module, command, uid) {
const btn = document.getElementById(`btn-${uid}`);
const panel = document.getElementById(`result-${uid}`);
const mod = MODULES.find(m => m.name === module);
const cmd = mod.commands.find(c => c.name === command);
const args = getArgs(uid, cmd.params);
const apiModule = cmd.module || module;
btn.disabled = true;
btn.textContent = '运行中...';
let url = `/api/run?module=${apiModule}&command=${command}`;
if (args) url += `&args=${encodeURIComponent(args)}`;
try {
const resp = await fetch(url);
const data = await resp.json();
const statusClass = data.ok ? 'ok' : 'err';
const statusText = data.ok ? '成功' : '失败';
let output;
if (!data.ok && data.error) {
output = data.error;
} else if (data.output === null || data.output === undefined) {
output = '(操作完成,无返回数据)';
} else if (typeof data.output === 'string') {
output = data.output || '(操作完成,无返回数据)';
} else {
const s = JSON.stringify(data.output, null, 2);
output = (s === 'null' || s === '{}' || s === '""') ? '(操作完成,无返回数据)' : s;
}
}
panel.innerHTML = `
<div class="result-toolbar">
<span class="cmd-text">$ ${data.command || module + ' +' + command}</span>
<span class="status ${statusClass}">${statusText}</span>
<span class="close-btn" onclick="panel.classList.remove('show')">&times;</span>
</div>
<div class="result-body"><pre>${escapeHtml(output)}</pre></div>`;
panel.classList.add('show');
} catch (e) {
panel.innerHTML = `
<div class="result-toolbar">
<span class="cmd-text">$ ${module} +${command}</span>
<span class="status err">请求失败</span>
<span class="close-btn" onclick="panel.classList.remove('show')">&times;</span>
</div>
<div class="result-body"><pre>${escapeHtml(e.message)}</pre></div>`;
panel.classList.add('show');
}
btn.disabled = false;
btn.textContent = '▶ 运行';
}
function escapeHtml(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
</script>
</body>
</html>