|
|
@@ -363,30 +363,46 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- .table-header {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: minmax(200px, 3fr) 120px 140px 100px 80px;
|
|
|
- gap: 16px;
|
|
|
- padding: 12px 20px;
|
|
|
- background: rgba(0, 0, 0, 0.2);
|
|
|
+ /* Version card */
|
|
|
+ .version-card {
|
|
|
+ background: var(--bg-card);
|
|
|
+ border: 1px solid var(--border-card);
|
|
|
+ border-radius: var(--radius);
|
|
|
+ overflow: hidden;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ animation: fadeUp 0.35s ease forwards;
|
|
|
+ opacity: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .version-card:hover {
|
|
|
+ border-color: rgba(255, 255, 255, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .version-head {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 14px;
|
|
|
+ padding: 14px 20px;
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
+ background: var(--bg-card-head);
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .v-author {
|
|
|
font-size: 13px;
|
|
|
- font-weight: 600;
|
|
|
color: var(--text-secondary);
|
|
|
- position: sticky;
|
|
|
- top: 0;
|
|
|
- z-index: 10;
|
|
|
}
|
|
|
|
|
|
- .table-body {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
+ .v-time {
|
|
|
+ margin-left: auto;
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--text-muted);
|
|
|
}
|
|
|
|
|
|
/* File row grid */
|
|
|
.file-row {
|
|
|
display: grid;
|
|
|
- grid-template-columns: minmax(200px, 3fr) 120px 140px 100px 80px;
|
|
|
+ grid-template-columns: 1fr 100px;
|
|
|
gap: 16px;
|
|
|
align-items: center;
|
|
|
padding: 12px 20px;
|
|
|
@@ -452,8 +468,21 @@
|
|
|
}
|
|
|
|
|
|
.commit-tag {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 5px;
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 500;
|
|
|
color: var(--accent);
|
|
|
+ background: var(--accent-dim);
|
|
|
+ padding: 3px 10px;
|
|
|
+ border-radius: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .commit-tag svg {
|
|
|
+ width: 14px;
|
|
|
+ height: 14px;
|
|
|
}
|
|
|
|
|
|
.btn-dl-wrap {
|
|
|
@@ -490,7 +519,7 @@
|
|
|
/* File group (folder) */
|
|
|
.fg-header {
|
|
|
display: grid;
|
|
|
- grid-template-columns: minmax(200px, 3fr) 120px 140px 100px 80px;
|
|
|
+ grid-template-columns: 1fr 100px;
|
|
|
gap: 16px;
|
|
|
align-items: center;
|
|
|
padding: 12px 20px;
|
|
|
@@ -794,22 +823,19 @@
|
|
|
$('contentBody').innerHTML = '<div class="state-box"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg><h2>暂无数据</h2><p>该阶段还没有提交记录</p></div>';
|
|
|
return;
|
|
|
}
|
|
|
- let h = `<div style="background: var(--bg-card); border: 1px solid var(--border-card); border-radius: var(--radius); overflow: hidden;">
|
|
|
- <div class="table-header">
|
|
|
- <div>文件 / 文件夹</div>
|
|
|
- <div>Commit ID</div>
|
|
|
- <div>提交时间</div>
|
|
|
- <div>作者</div>
|
|
|
- <div style="text-align: right;">操作</div>
|
|
|
- </div>
|
|
|
- <div class="table-body">`;
|
|
|
+ let h = '';
|
|
|
|
|
|
S.versions.forEach((v, i) => {
|
|
|
const groups = groupFiles(v.files);
|
|
|
- h += renderGroups(groups, v);
|
|
|
+ h += `<div class="version-card" style="animation-delay:${Math.min(i, 10) * 0.05}s">
|
|
|
+ <div class="version-head">
|
|
|
+ <span class="commit-tag">${IC.commit} ${esc(v.commit_id.substring(0, 8))}</span>
|
|
|
+ <span class="v-author">${v.author ? esc(v.author) : ''}</span>
|
|
|
+ <span class="v-time" title="${fmtTime(v.created_at)}">${relTime(v.created_at)}</span>
|
|
|
+ </div>
|
|
|
+ <div class="version-files">${renderGroups(groups, v)}</div>
|
|
|
+ </div>`;
|
|
|
});
|
|
|
-
|
|
|
- h += `</div></div>`;
|
|
|
if (S.hasMore) {
|
|
|
h += '<div class="load-more"><button class="load-more-btn" onclick="loadMore()">加载更多</button></div>';
|
|
|
}
|
|
|
@@ -825,21 +851,40 @@
|
|
|
// ============ File Grouping ============
|
|
|
function groupFiles(files) {
|
|
|
if (!files || !files.length) return [];
|
|
|
- const map = {};
|
|
|
+
|
|
|
+ const topLevelGroups = {};
|
|
|
+ const rootFiles = [];
|
|
|
+
|
|
|
files.forEach(f => {
|
|
|
const parts = f.relative_path.split('/');
|
|
|
- const parentDir = parts.slice(0, -1).join('/');
|
|
|
- if (!map[parentDir]) map[parentDir] = [];
|
|
|
- map[parentDir].push(f);
|
|
|
+ if (parts.length === 1) {
|
|
|
+ rootFiles.push(f);
|
|
|
+ } else {
|
|
|
+ const topDir = parts[0];
|
|
|
+ if (!topLevelGroups[topDir]) topLevelGroups[topDir] = [];
|
|
|
+ topLevelGroups[topDir].push(f);
|
|
|
+ }
|
|
|
});
|
|
|
+
|
|
|
const result = [];
|
|
|
- Object.entries(map).forEach(([dir, fls]) => {
|
|
|
- if (dir !== '') {
|
|
|
- result.push({ type: 'folder', name: dir, path: dir, files: fls });
|
|
|
- } else {
|
|
|
- fls.forEach(f => result.push({ type: 'file', file: f }));
|
|
|
+ Object.entries(topLevelGroups).forEach(([topDir, fls]) => {
|
|
|
+ let commonParts = fls[0].relative_path.split('/').slice(0, -1);
|
|
|
+ for (let i = 1; i < fls.length; i++) {
|
|
|
+ const parts = fls[i].relative_path.split('/').slice(0, -1);
|
|
|
+ let j = 0;
|
|
|
+ while (j < commonParts.length && j < parts.length && commonParts[j] === parts[j]) {
|
|
|
+ j++;
|
|
|
+ }
|
|
|
+ commonParts.length = j;
|
|
|
}
|
|
|
+ const groupName = commonParts.join('/');
|
|
|
+ result.push({ type: 'folder', name: groupName, path: groupName, files: fls });
|
|
|
+ });
|
|
|
+
|
|
|
+ rootFiles.forEach(f => {
|
|
|
+ result.push({ type: 'file', file: f });
|
|
|
});
|
|
|
+
|
|
|
result.sort((a, b) => {
|
|
|
if (a.type !== b.type) return a.type === 'folder' ? -1 : 1;
|
|
|
return (a.name || a.file.name).localeCompare(b.name || b.file.name);
|
|
|
@@ -859,35 +904,33 @@
|
|
|
<span class="fg-arrow" id="fa_${gid}">${IC.chevron}</span>
|
|
|
<span class="fg-icon">${IC.folder}</span>
|
|
|
<span class="fg-name">${esc(g.name)}/</span>
|
|
|
- <span class="fg-count">${g.files.length}</span>
|
|
|
+ <span class="fg-count">${g.files.length} 个文件</span>
|
|
|
</div>
|
|
|
- <div class="col-text commit-tag" title="${esc(version.commit_id)}">${esc(version.commit_id.substring(0, 8))}</div>
|
|
|
- <div class="col-text" title="${fmtTime(version.created_at)}">${relTime(version.created_at)}</div>
|
|
|
- <div class="col-text">${version.author ? esc(version.author) : '-'}</div>
|
|
|
<div></div>
|
|
|
</div>
|
|
|
<div class="fg-children" id="${gid}">
|
|
|
- ${g.files.map(f => fileRow(f, version, true)).join('')}
|
|
|
+ ${g.files.map(f => fileRow(f, version, true, g.path)).join('')}
|
|
|
</div>`;
|
|
|
} else {
|
|
|
- h += fileRow(g.file, version, false);
|
|
|
+ h += fileRow(g.file, version, false, null);
|
|
|
}
|
|
|
});
|
|
|
return h;
|
|
|
}
|
|
|
|
|
|
- function fileRow(f, version, isChild) {
|
|
|
+ function fileRow(f, version, isChild, groupPath) {
|
|
|
const padding = isChild ? 'padding-left: 44px;' : '';
|
|
|
+ let displayName = f.name;
|
|
|
+ if (groupPath && f.relative_path.startsWith(groupPath + '/')) {
|
|
|
+ displayName = f.relative_path.substring(groupPath.length + 1);
|
|
|
+ }
|
|
|
return `
|
|
|
<div class="file-row" style="${padding}">
|
|
|
<div class="file-name-col" title="${esc(f.relative_path)}">
|
|
|
<span class="f-icon">${IC.file}</span>
|
|
|
- <span class="f-name">${esc(f.name)}</span>
|
|
|
+ <span class="f-name">${esc(displayName)}</span>
|
|
|
<span class="f-size">${fmtSize(f.file_size)}</span>
|
|
|
</div>
|
|
|
- <div class="col-text commit-tag" title="${esc(version.commit_id)}">${esc(version.commit_id.substring(0, 8))}</div>
|
|
|
- <div class="col-text" title="${fmtTime(version.created_at)}">${relTime(version.created_at)}</div>
|
|
|
- <div class="col-text">${version.author ? esc(version.author) : '-'}</div>
|
|
|
<div class="btn-dl-wrap">
|
|
|
<a class="btn-dl" href="/files/${f.id}/content" download="${esc(f.name)}" onclick="event.stopPropagation();">${IC.download}</a>
|
|
|
</div>
|