0
0

初步给项目、飞行器、容器页面添加了git拉取功能

This commit is contained in:
xubing
2025-12-05 20:17:05 +08:00
parent a7edfb82c5
commit 2c314b9b0b
19 changed files with 2557 additions and 156 deletions

File diff suppressed because it is too large Load Diff

13
src/panels/services/GitService.ts Normal file → Executable file
View File

@@ -1,3 +1,4 @@
// src/panels/services/GitService.ts
import * as fs from 'fs';
import git from 'isomorphic-git';
import http from 'isomorphic-git/http/node';
@@ -290,6 +291,10 @@ export class GitService {
/**
* 构建文件树
* 这里显式忽略:
* - .git 目录
* - .dcsp-data.json
* - 其它以 . 开头的隐藏文件/目录
*/
static async buildFileTree(dir: string, relativePath: string = ''): Promise<GitFileTree[]> {
try {
@@ -297,9 +302,15 @@ export class GitService {
const tree: GitFileTree[] = [];
for (const file of files) {
if (file.startsWith('.') && file !== '.git') continue;
// 1. 不要解析 .git
if (file === '.git') continue;
// 2. 不要解析项目数据文件
if (file === '.dcsp-data.json') continue;
// 3. 其它所有隐藏文件/目录统统忽略
if (file.startsWith('.')) continue;
const filePath = path.join(dir, file);
const stats = await fs.promises.stat(filePath);
const currentRelativePath = path.join(relativePath, file);

View File

@@ -31,7 +31,7 @@ export class ProjectService {
}
// =============== 项目相关方法 ===============
getProjects(): Project[] {
return this.projects;
}
@@ -44,8 +44,8 @@ export class ProjectService {
return this.projectPaths.get(projectId);
}
setProjectPath(projectId: string, path: string): void {
this.projectPaths.set(projectId, path);
setProjectPath(projectId: string, pathStr: string): void {
this.projectPaths.set(projectId, pathStr);
}
async createProject(name: string): Promise<string> {
@@ -123,7 +123,7 @@ export class ProjectService {
const aircraft = this.aircrafts.find(a => a.id === aircraftId);
if (!aircraft) return false;
// ⚠️ 修正点:先在删除 containers 之前,算出要删的 containerIds
// ⚠️ 算出要删的 containerIds
const relatedContainers = this.containers.filter(c => c.aircraftId === aircraftId);
const containerIds = relatedContainers.map(c => c.id);
@@ -138,8 +138,56 @@ export class ProjectService {
return true;
}
/**
* 从已存在的磁盘目录导入飞行器
* 不会创建/删除任何文件,只在内存中补充 Aircraft / Container / ModuleFolder 数据
*/
async importAircraftFromExistingFolder(projectId: string, aircraftName: string): Promise<string> {
// 已存在同名飞行器直接返回
const existed = this.getAircraftsByProject(projectId).find(a => a.name === aircraftName);
if (existed) {
return existed.id;
}
const newId = this.generateUniqueId('a', this.aircrafts);
const aircraft: Aircraft = {
id: newId,
name: aircraftName,
projectId
};
this.aircrafts.push(aircraft);
const projectPath = this.projectPaths.get(projectId);
if (!projectPath) {
return newId;
}
const aircraftDir = path.join(projectPath, aircraftName);
if (!fs.existsSync(aircraftDir)) {
// 目录不存在就不再解析子目录
return newId;
}
// 每个子目录视为一个容器(排除 .git
const entries = await fs.promises.readdir(aircraftDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (entry.name === '.git') continue;
await this.importContainerFromExistingFolder(newId, entry.name);
}
return newId;
}
// =============== 容器相关方法 ===============
/**
* ⚠️ 这里不再做 .git 过滤:
* - 我们只会通过 createContainer / importContainerFromExistingFolder 创建容器
* - importContainerFromExistingFolder 内部已经排除了 .git
* 所以不会出现名字叫 ".git" 的容器
*/
getContainersByAircraft(aircraftId: string): Container[] {
return this.containers.filter(c => c.aircraftId === aircraftId);
}
@@ -154,6 +202,7 @@ export class ProjectService {
this.containers.push(newContainer);
await this.createContainerDirectory(newContainer);
// UI 手动创建的容器,仍然保留“默认两个配置”的行为
await this.createDefaultConfigs(newContainer);
return newId;
@@ -179,6 +228,83 @@ export class ProjectService {
return true;
}
/**
* 从已存在的磁盘目录导入容器
*
* ✅ 你的最新需求:
* - 不创建“默认两个配置”
* - 自动扫描容器目录下的『子文件夹』
* - 每个子文件夹创建一个 ModuleFoldertype: 'local'
* - 排除 `.git` 子文件夹
*/
async importContainerFromExistingFolder(aircraftId: string, containerName: string): Promise<string> {
if (containerName === '.git') {
throw new Error('不能将 .git 导入为容器');
}
const existed = this.getContainersByAircraft(aircraftId).find(c => c.name === containerName);
if (existed) {
return existed.id;
}
const newId = this.generateUniqueId('c', this.containers);
const container: Container = {
id: newId,
name: containerName,
aircraftId
};
this.containers.push(container);
// 🚩 关键逻辑:扫描容器目录下的子文件夹 -> 创建 ModuleFolder
await this.scanContainerModuleFolders(container);
// 不再创建默认两个配置(不调用 createDefaultConfigs
// 也不创建目录(目录是 Git 克隆出来的,本来就存在)
return newId;
}
/**
* 扫描容器目录中的子文件夹(不含 .git将其作为本地模块文件夹记录
*/
private async scanContainerModuleFolders(container: Container): Promise<void> {
try {
const aircraft = this.aircrafts.find(a => a.id === container.aircraftId);
if (!aircraft) return;
const projectPath = this.projectPaths.get(aircraft.projectId);
if (!projectPath) return;
const containerDir = path.join(projectPath, aircraft.name, container.name);
if (!fs.existsSync(containerDir)) {
return;
}
const entries = await fs.promises.readdir(containerDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (entry.name === '.git') continue;
const folderId = this.generateUniqueId('local-', this.moduleFolders);
const relativePath = `/${aircraft.projectId}/${aircraft.name}/${container.name}/${entry.name}`;
const moduleFolder: ModuleFolder = {
id: folderId,
name: entry.name,
type: 'local',
localPath: relativePath,
containerId: container.id
};
this.moduleFolders.push(moduleFolder);
}
} catch (error) {
console.error('扫描容器目录生成模块文件夹失败:', error);
}
}
// =============== 配置相关方法 ===============
getConfigsByContainer(containerId: string): Config[] {
@@ -329,6 +455,10 @@ export class ProjectService {
}
}
/**
* 仅用于「新建容器」时的默认两个配置
* (导入容器时不会调用)
*/
private async createDefaultConfigs(container: Container): Promise<void> {
this.configs.push({
id: this.generateUniqueId('cfg', this.configs),
@@ -389,10 +519,12 @@ export class ProjectService {
this.configs = this.configs.filter(cfg => !containerIds.includes(cfg.containerId));
this.moduleFolders = this.moduleFolders.filter(folder => !containerIds.includes(folder.containerId));
// 加载新数据
// ⭐ 载入新数据时顺便过滤掉名字为 ".git" 的容器(避免历史数据带进来)
const cleanedContainers = data.containers.filter(c => c.name !== '.git');
this.projects.push(...data.projects);
this.aircrafts.push(...data.aircrafts);
this.containers.push(...data.containers);
this.containers.push(...cleanedContainers);
this.configs.push(...data.configs);
this.moduleFolders.push(...data.moduleFolders);

View File

@@ -1,4 +1,3 @@
// src/panels/views/AircraftView.ts
import { BaseView } from './BaseView';
import { AircraftViewData } from '../types/ViewTypes';
@@ -27,6 +26,7 @@ export class AircraftView extends BaseView {
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>飞行器配置</title>
${this.getStyles()}
${this.getRepoSelectScript()}
<style>
.header {
display: flex;
@@ -60,7 +60,7 @@ export class AircraftView extends BaseView {
display: flex;
align-items: center;
gap: 6px;
margin: 0 auto;
/* ❌ 原来这里有 margin: 0 auto; 已去掉,避免“获取仓库”按钮被居中 */
}
.btn-new:hover {
background: var(--vscode-button-hoverBackground);
@@ -78,6 +78,81 @@ export class AircraftView extends BaseView {
text-align: center;
padding: 20px;
}
/* 仓库 + 分支树公共样式 */
.config-section {
margin-top: 30px;
margin-bottom: 30px;
}
.section-title {
margin: 30px 0 15px 0;
padding-bottom: 10px;
border-bottom: 2px solid var(--vscode-panel-border);
color: var(--vscode-titleBar-activeForeground);
}
.url-input-section {
background: var(--vscode-panel-background);
padding: 15px;
border-radius: 4px;
margin-bottom: 15px;
border: 1px solid var(--vscode-input-border);
}
.branch-selection {
border: 1px solid var(--vscode-input-border);
}
.branch-tree {
font-family: 'Courier New', monospace;
user-select: none;
margin: 0;
padding: 0;
}
.branch-node {
padding: 2px 0;
display: flex;
align-items: center;
cursor: pointer;
line-height: 1.2;
}
.branch-node:hover {
background: var(--vscode-list-hoverBackground);
}
.branch-icon {
margin-right: 4px;
font-size: 16px;
width: 16px;
text-align: center;
}
.branch-expand {
margin-right: 2px;
cursor: pointer;
width: 14px;
text-align: center;
}
.branch-checkbox {
margin-right: 6px;
}
.branch-name {
flex: 1;
font-size: 13px;
}
.branch-children {
margin-left: 16px;
border-left: 1px solid var(--vscode-panel-border);
padding-left: 8px;
}
.branch-leaf {
margin-left: 16px;
}
.current-branch {
color: var(--vscode-gitDecoration-untrackedResourceForeground);
font-weight: bold;
}
.tree-icon {
font-size: 2em;
}
.branch-tree-title {
font-size: 1.2em;
}
</style>
</head>
<body>
@@ -104,6 +179,21 @@ export class AircraftView extends BaseView {
</tbody>
</table>
<!-- 飞行器云仓库 -->
<div class="config-section">
<h3 class="section-title">🛫 飞行器云仓库</h3>
<div class="url-input-section">
<h4>🔗 获取飞行器仓库</h4>
<div style="display: flex; gap: 10px; margin-top: 10px; align-items: center;">
<button class="btn-new" onclick="openRepoSelectForAircraft()">获取仓库</button>
<span style="font-size: 12px; color: var(--vscode-descriptionForeground);">
从仓库配置中选择 Git 仓库,选择分支后可将飞行器代码克隆到当前项目下(以分支名作为飞行器名称)
</span>
</div>
<div id="branchSelectionContainer"></div>
</div>
</div>
<script>
const vscode = acquireVsCodeApi();
@@ -184,8 +274,166 @@ export class AircraftView extends BaseView {
}
);
}
// ======== Git 仓库 & 分支树 - AircraftView 作用域 ========
let selectedBranches = new Set();
let branchTreeData = [];
function openRepoSelectForAircraft() {
vscode.postMessage({
type: 'openRepoSelectForAircraft'
});
}
function toggleBranch(branchName) {
const checkbox = document.getElementById('branch-' + branchName.replace(/[^a-zA-Z0-9-]/g, '-'));
if (checkbox && checkbox.checked) {
selectedBranches.add(branchName);
} else {
selectedBranches.delete(branchName);
}
}
function cloneSelectedBranches() {
if (selectedBranches.size === 0) {
alert('请至少选择一个分支');
return;
}
vscode.postMessage({
type: 'cloneBranches',
branches: Array.from(selectedBranches)
});
selectedBranches.clear();
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(checkbox => checkbox.checked = false);
document.getElementById('branchSelectionContainer').innerHTML = '';
}
function cancelBranchSelection() {
selectedBranches.clear();
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(checkbox => checkbox.checked = false);
document.getElementById('branchSelectionContainer').innerHTML = '';
vscode.postMessage({
type: 'cancelBranchSelection'
});
}
function toggleBranchNode(nodeId) {
const node = findNodeById(branchTreeData, nodeId);
if (node && !node.isLeaf) {
node.expanded = !node.expanded;
renderBranchTree(branchTreeData);
}
}
function expandAllBranches() {
setAllExpanded(branchTreeData, true);
renderBranchTree(branchTreeData);
}
function collapseAllBranches() {
setAllExpanded(branchTreeData, false);
renderBranchTree(branchTreeData);
}
function setAllExpanded(nodes, expanded) {
nodes.forEach(node => {
if (!node.isLeaf) {
node.expanded = expanded;
if (node.children) {
setAllExpanded(node.children, expanded);
}
}
});
}
function findNodeById(nodes, nodeId) {
for (const node of nodes) {
if (node.fullName === nodeId) return node;
if (node.children) {
const found = findNodeById(node.children, nodeId);
if (found) return found;
}
}
return null;
}
function renderBranchTree(treeData) {
const container = document.getElementById('branchSelectionContainer');
if (!treeData || treeData.length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 10px; color: var(--vscode-descriptionForeground);">未找到分支</div>';
return;
}
let html = '<div class="branch-selection" style="margin: 10px 0; padding: 10px; background: var(--vscode-panel-background); border-radius: 4px;">';
html += '<h4 class="branch-tree-title" style="margin: 0 0 8px 0;"><span class="tree-icon">🌳</span> 分支树</h4>';
html += '<div style="margin-bottom: 8px;">';
html += '<button class="back-btn" onclick="expandAllBranches()" style="margin-right: 8px; padding: 2px 6px; font-size: 11px;">展开全部</button>';
html += '<button class="back-btn" onclick="collapseAllBranches()" style="padding: 2px 6px; font-size: 11px;">收起全部</button>';
html += '</div>';
html += '<div class="branch-tree">';
html += renderBranchNodes(treeData, 0);
html += '</div>';
html += '<div style="margin-top: 12px;">';
html += '<button class="btn-new" onclick="cloneSelectedBranches()" style="margin-right: 8px; padding: 4px 8px; font-size: 12px;">✅ 克隆选中分支</button>';
html += '<button class="back-btn" onclick="cancelBranchSelection()" style="padding: 4px 8px; font-size: 12px;">取消</button>';
html += '</div></div>';
container.innerHTML = html;
}
function renderBranchNodes(nodes, level) {
let html = '';
nodes.forEach(node => {
const indent = level * 20;
const nodeId = node.fullName.replace(/[^a-zA-Z0-9-]/g, '-');
if (node.isLeaf) {
html += '<div class="branch-node branch-leaf" style="margin-left: ' + indent + 'px;">';
html += '<input type="checkbox" id="branch-' + nodeId + '" class="branch-checkbox" ';
html += (node.branch && node.branch.selected ? 'checked' : '') + ' onchange="toggleBranch(\\'' + node.fullName + '\\')">';
html += '<span class="branch-icon">🌿</span>';
html += '<span class="branch-name ' + (node.branch && node.branch.isCurrent ? 'current-branch' : '') + '">';
html += node.name;
html += (node.branch && node.branch.isCurrent ? ' ⭐' : '');
html += '</span>';
html += '</div>';
} else {
html += '<div class="branch-node" style="margin-left: ' + indent + 'px;">';
html += '<span class="branch-expand" onclick="toggleBranchNode(\\'' + node.fullName + '\\')">';
html += (node.expanded ? '🪵' : '🪵');
html += '</span>';
html += '<span class="branch-icon">🪵</span>';
html += '<span class="branch-name">' + node.name + '</span>';
html += '</div>';
if (node.expanded && node.children && node.children.length > 0) {
html += '<div class="branch-children">';
html += renderBranchNodes(node.children, level + 1);
html += '</div>';
}
}
});
return html;
}
window.addEventListener('message', event => {
const message = event.data;
if (message.type === 'branchesFetched') {
branchTreeData = message.branchTree || [];
renderBranchTree(branchTreeData);
}
});
</script>
</body>
</html>`;
}
}
}

View File

@@ -1,4 +1,3 @@
// src/panels/views/ContainerView.ts
import { BaseView } from './BaseView';
import { AircraftConfigData, ContainerViewData } from '../types/ViewTypes';
@@ -29,6 +28,117 @@ export class ContainerView extends BaseView {
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>容器管理</title>
${this.getStyles()}
${this.getRepoSelectScript()}
<style>
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.back-btn {
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.back-btn:hover {
background: var(--vscode-button-secondaryHoverBackground);
}
.btn-new {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
/* ❌ 原来这里有 margin: 0 auto; 已去掉,避免“获取仓库”按钮被居中 */
}
.btn-new:hover {
background: var(--vscode-button-hoverBackground);
}
/* 仓库 + 分支树公共样式 */
.config-section {
margin-top: 30px;
margin-bottom: 30px;
}
.section-title {
margin: 30px 0 15px 0;
padding-bottom: 10px;
border-bottom: 2px solid var(--vscode-panel-border);
color: var(--vscode-titleBar-activeForeground);
}
.url-input-section {
background: var(--vscode-panel-background);
padding: 15px;
border-radius: 4px;
margin-bottom: 15px;
border: 1px solid var(--vscode-input-border);
}
.branch-selection {
border: 1px solid var(--vscode-input-border);
}
.branch-tree {
font-family: 'Courier New', monospace;
user-select: none;
margin: 0;
padding: 0;
}
.branch-node {
padding: 2px 0;
display: flex;
align-items: center;
cursor: pointer;
line-height: 1.2;
}
.branch-node:hover {
background: var(--vscode-list-hoverBackground);
}
.branch-icon {
margin-right: 4px;
font-size: 16px;
width: 16px;
text-align: center;
}
.branch-expand {
margin-right: 2px;
cursor: pointer;
width: 14px;
text-align: center;
}
.branch-checkbox {
margin-right: 6px;
}
.branch-name {
flex: 1;
font-size: 13px;
}
.branch-children {
margin-left: 16px;
border-left: 1px solid var(--vscode-panel-border);
padding-left: 8px;
}
.branch-leaf {
margin-left: 16px;
}
.current-branch {
color: var(--vscode-gitDecoration-untrackedResourceForeground);
font-weight: bold;
}
.tree-icon {
font-size: 2em;
}
.branch-tree-title {
font-size: 1.2em;
}
</style>
</head>
<body>
<div class="header">
@@ -54,6 +164,21 @@ export class ContainerView extends BaseView {
</tbody>
</table>
<!-- 容器云仓库 -->
<div class="config-section">
<h3 class="section-title">📦 容器云仓库</h3>
<div class="url-input-section">
<h4>🔗 获取容器仓库</h4>
<div style="display: flex; gap: 10px; margin-top: 10px; align-items: center;">
<button class="btn-new" onclick="openRepoSelectForContainer()">获取仓库</button>
<span style="font-size: 12px; color: var(--vscode-descriptionForeground);">
从仓库配置中选择 Git 仓库,选择分支后可将容器代码克隆到当前飞行器下(以分支名作为容器名称)
</span>
</div>
<div id="branchSelectionContainer"></div>
</div>
</div>
<script>
const vscode = acquireVsCodeApi();
@@ -116,8 +241,166 @@ export class ContainerView extends BaseView {
}
);
}
// ======== Git 仓库 & 分支树 - ContainerView 作用域 ========
let selectedBranches = new Set();
let branchTreeData = [];
function openRepoSelectForContainer() {
vscode.postMessage({
type: 'openRepoSelectForContainer'
});
}
function toggleBranch(branchName) {
const checkbox = document.getElementById('branch-' + branchName.replace(/[^a-zA-Z0-9-]/g, '-'));
if (checkbox && checkbox.checked) {
selectedBranches.add(branchName);
} else {
selectedBranches.delete(branchName);
}
}
function cloneSelectedBranches() {
if (selectedBranches.size === 0) {
alert('请至少选择一个分支');
return;
}
vscode.postMessage({
type: 'cloneBranches',
branches: Array.from(selectedBranches)
});
selectedBranches.clear();
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(checkbox => checkbox.checked = false);
document.getElementById('branchSelectionContainer').innerHTML = '';
}
function cancelBranchSelection() {
selectedBranches.clear();
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(checkbox => checkbox.checked = false);
document.getElementById('branchSelectionContainer').innerHTML = '';
vscode.postMessage({
type: 'cancelBranchSelection'
});
}
function toggleBranchNode(nodeId) {
const node = findNodeById(branchTreeData, nodeId);
if (node && !node.isLeaf) {
node.expanded = !node.expanded;
renderBranchTree(branchTreeData);
}
}
function expandAllBranches() {
setAllExpanded(branchTreeData, true);
renderBranchTree(branchTreeData);
}
function collapseAllBranches() {
setAllExpanded(branchTreeData, false);
renderBranchTree(branchTreeData);
}
function setAllExpanded(nodes, expanded) {
nodes.forEach(node => {
if (!node.isLeaf) {
node.expanded = expanded;
if (node.children) {
setAllExpanded(node.children, expanded);
}
}
});
}
function findNodeById(nodes, nodeId) {
for (const node of nodes) {
if (node.fullName === nodeId) return node;
if (node.children) {
const found = findNodeById(node.children, nodeId);
if (found) return found;
}
}
return null;
}
function renderBranchTree(treeData) {
const container = document.getElementById('branchSelectionContainer');
if (!treeData || treeData.length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 10px; color: var(--vscode-descriptionForeground);">未找到分支</div>';
return;
}
let html = '<div class="branch-selection" style="margin: 10px 0; padding: 10px; background: var(--vscode-panel-background); border-radius: 4px;">';
html += '<h4 class="branch-tree-title" style="margin: 0 0 8px 0;"><span class="tree-icon">🌳</span> 分支树</h4>';
html += '<div style="margin-bottom: 8px;">';
html += '<button class="back-btn" onclick="expandAllBranches()" style="margin-right: 8px; padding: 2px 6px; font-size: 11px;">展开全部</button>';
html += '<button class="back-btn" onclick="collapseAllBranches()" style="padding: 2px 6px; font-size: 11px;">收起全部</button>';
html += '</div>';
html += '<div class="branch-tree">';
html += renderBranchNodes(treeData, 0);
html += '</div>';
html += '<div style="margin-top: 12px;">';
html += '<button class="btn-new" onclick="cloneSelectedBranches()" style="margin-right: 8px; padding: 4px 8px; font-size: 12px;">✅ 克隆选中分支</button>';
html += '<button class="back-btn" onclick="cancelBranchSelection()" style="padding: 4px 8px; font-size: 12px;">取消</button>';
html += '</div></div>';
container.innerHTML = html;
}
function renderBranchNodes(nodes, level) {
let html = '';
nodes.forEach(node => {
const indent = level * 20;
const nodeId = node.fullName.replace(/[^a-zA-Z0-9-]/g, '-');
if (node.isLeaf) {
html += '<div class="branch-node branch-leaf" style="margin-left: ' + indent + 'px;">';
html += '<input type="checkbox" id="branch-' + nodeId + '" class="branch-checkbox" ';
html += (node.branch && node.branch.selected ? 'checked' : '') + ' onchange="toggleBranch(\\'' + node.fullName + '\\')">';
html += '<span class="branch-icon">🌿</span>';
html += '<span class="branch-name ' + (node.branch && node.branch.isCurrent ? 'current-branch' : '') + '">';
html += node.name;
html += (node.branch && node.branch.isCurrent ? ' ⭐' : '');
html += '</span>';
html += '</div>';
} else {
html += '<div class="branch-node" style="margin-left: ' + indent + 'px;">';
html += '<span class="branch-expand" onclick="toggleBranchNode(\\'' + node.fullName + '\\')">';
html += (node.expanded ? '🪵' : '🪵');
html += '</span>';
html += '<span class="branch-icon">🪵</span>';
html += '<span class="branch-name">' + node.name + '</span>';
html += '</div>';
if (node.expanded && node.children && node.children.length > 0) {
html += '<div class="branch-children">';
html += renderBranchNodes(node.children, level + 1);
html += '</div>';
}
}
});
return html;
}
window.addEventListener('message', event => {
const message = event.data;
if (message.type === 'branchesFetched') {
branchTreeData = message.branchTree || [];
renderBranchTree(branchTreeData);
}
});
</script>
</body>
</html>`;
}
}
}

View File

@@ -1,4 +1,3 @@
// src/panels/views/ProjectView.ts
import { BaseView } from './BaseView';
import { ProjectViewData } from '../types/ViewTypes';
@@ -38,6 +37,7 @@ export class ProjectView extends BaseView {
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数字卫星构建平台</title>
${this.getStyles()}
${this.getRepoSelectScript()}
<style>
.satellite-icon {
font-size: 2.5em;
@@ -84,15 +84,102 @@ export class ProjectView extends BaseView {
.btn-action:hover {
background: var(--vscode-button-hoverBackground);
}
/* 统一“主要操作按钮”的样式 */
.btn-new {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
}
.btn-new:hover {
background: var(--vscode-button-hoverBackground);
}
.btn-open {
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-border);
}
/* 仓库 & 分支树公共样式 */
.config-section {
margin-top: 30px;
margin-bottom: 30px;
}
.section-title {
margin: 30px 0 15px 0;
padding-bottom: 10px;
border-bottom: 2px solid var(--vscode-panel-border);
color: var(--vscode-titleBar-activeForeground);
}
.url-input-section {
background: var(--vscode-panel-background);
padding: 15px;
border-radius: 4px;
margin-bottom: 15px;
border: 1px solid var(--vscode-input-border);
}
.branch-selection {
border: 1px solid var(--vscode-input-border);
}
.branch-tree {
font-family: 'Courier New', monospace;
user-select: none;
margin: 0;
padding: 0;
}
.branch-node {
padding: 2px 0;
display: flex;
align-items: center;
cursor: pointer;
line-height: 1.2;
}
.branch-node:hover {
background: var(--vscode-list-hoverBackground);
}
.branch-icon {
margin-right: 4px;
font-size: 16px;
width: 16px;
text-align: center;
}
.branch-expand {
margin-right: 2px;
cursor: pointer;
width: 14px;
text-align: center;
}
.branch-checkbox {
margin-right: 6px;
}
.branch-name {
flex: 1;
font-size: 13px;
}
.branch-children {
margin-left: 16px;
border-left: 1px solid var(--vscode-panel-border);
padding-left: 8px;
}
.branch-leaf {
margin-left: 16px;
}
.current-branch {
color: var(--vscode-gitDecoration-untrackedResourceForeground);
font-weight: bold;
}
.tree-icon {
font-size: 2em;
}
.branch-tree-title {
font-size: 1.2em;
}
</style>
</head>
<body>
@@ -126,6 +213,21 @@ export class ProjectView extends BaseView {
</tbody>
</table>
<!-- 项目云仓库 -->
<div class="config-section">
<h3 class="section-title">📚 项目云仓库</h3>
<div class="url-input-section">
<h4>🔗 获取项目仓库</h4>
<div style="display: flex; gap: 10px; margin-top: 10px; align-items: center;">
<button class="btn-new" onclick="openRepoSelectForProject()">获取仓库</button>
<span style="font-size: 12px; color: var(--vscode-descriptionForeground);">
从仓库配置中选择 Git 仓库,选择分支后可将完整项目克隆到本地(包含 dscp-data.json
</span>
</div>
<div id="branchSelectionContainer"></div>
</div>
</div>
<script>
const vscode = acquireVsCodeApi();
@@ -219,8 +321,166 @@ export class ProjectView extends BaseView {
}
);
}
// ======== Git 仓库 & 分支树 - ProjectView 作用域 ========
let selectedBranches = new Set();
let branchTreeData = [];
function openRepoSelectForProject() {
vscode.postMessage({
type: 'openRepoSelectForProject'
});
}
function toggleBranch(branchName) {
const checkbox = document.getElementById('branch-' + branchName.replace(/[^a-zA-Z0-9-]/g, '-'));
if (checkbox && checkbox.checked) {
selectedBranches.add(branchName);
} else {
selectedBranches.delete(branchName);
}
}
function cloneSelectedBranches() {
if (selectedBranches.size === 0) {
alert('请至少选择一个分支');
return;
}
vscode.postMessage({
type: 'cloneBranches',
branches: Array.from(selectedBranches)
});
selectedBranches.clear();
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(checkbox => checkbox.checked = false);
document.getElementById('branchSelectionContainer').innerHTML = '';
}
function cancelBranchSelection() {
selectedBranches.clear();
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(checkbox => checkbox.checked = false);
document.getElementById('branchSelectionContainer').innerHTML = '';
vscode.postMessage({
type: 'cancelBranchSelection'
});
}
function toggleBranchNode(nodeId) {
const node = findNodeById(branchTreeData, nodeId);
if (node && !node.isLeaf) {
node.expanded = !node.expanded;
renderBranchTree(branchTreeData);
}
}
function expandAllBranches() {
setAllExpanded(branchTreeData, true);
renderBranchTree(branchTreeData);
}
function collapseAllBranches() {
setAllExpanded(branchTreeData, false);
renderBranchTree(branchTreeData);
}
function setAllExpanded(nodes, expanded) {
nodes.forEach(node => {
if (!node.isLeaf) {
node.expanded = expanded;
if (node.children) {
setAllExpanded(node.children, expanded);
}
}
});
}
function findNodeById(nodes, nodeId) {
for (const node of nodes) {
if (node.fullName === nodeId) return node;
if (node.children) {
const found = findNodeById(node.children, nodeId);
if (found) return found;
}
}
return null;
}
function renderBranchTree(treeData) {
const container = document.getElementById('branchSelectionContainer');
if (!treeData || treeData.length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 10px; color: var(--vscode-descriptionForeground);">未找到分支</div>';
return;
}
let html = '<div class="branch-selection" style="margin: 10px 0; padding: 10px; background: var(--vscode-panel-background); border-radius: 4px;">';
html += '<h4 class="branch-tree-title" style="margin: 0 0 8px 0;"><span class="tree-icon">🌳</span> 分支树</h4>';
html += '<div style="margin-bottom: 8px;">';
html += '<button class="back-btn" onclick="expandAllBranches()" style="margin-right: 8px; padding: 2px 6px; font-size: 11px;">展开全部</button>';
html += '<button class="back-btn" onclick="collapseAllBranches()" style="padding: 2px 6px; font-size: 11px;">收起全部</button>';
html += '</div>';
html += '<div class="branch-tree">';
html += renderBranchNodes(treeData, 0);
html += '</div>';
html += '<div style="margin-top: 12px;">';
html += '<button class="btn-new" onclick="cloneSelectedBranches()" style="margin-right: 8px; padding: 4px 8px; font-size: 12px;">✅ 克隆选中分支</button>';
html += '<button class="back-btn" onclick="cancelBranchSelection()" style="padding: 4px 8px; font-size: 12px;">取消</button>';
html += '</div></div>';
container.innerHTML = html;
}
function renderBranchNodes(nodes, level) {
let html = '';
nodes.forEach(node => {
const indent = level * 20;
const nodeId = node.fullName.replace(/[^a-zA-Z0-9-]/g, '-');
if (node.isLeaf) {
html += '<div class="branch-node branch-leaf" style="margin-left: ' + indent + 'px;">';
html += '<input type="checkbox" id="branch-' + nodeId + '" class="branch-checkbox" ';
html += (node.branch && node.branch.selected ? 'checked' : '') + ' onchange="toggleBranch(\\'' + node.fullName + '\\')">';
html += '<span class="branch-icon">🌿</span>';
html += '<span class="branch-name ' + (node.branch && node.branch.isCurrent ? 'current-branch' : '') + '">';
html += node.name;
html += (node.branch && node.branch.isCurrent ? ' ⭐' : '');
html += '</span>';
html += '</div>';
} else {
html += '<div class="branch-node" style="margin-left: ' + indent + 'px;">';
html += '<span class="branch-expand" onclick="toggleBranchNode(\\'' + node.fullName + '\\')">';
html += (node.expanded ? '🪵' : '🪵');
html += '</span>';
html += '<span class="branch-icon">🪵</span>';
html += '<span class="branch-name">' + node.name + '</span>';
html += '</div>';
if (node.expanded && node.children && node.children.length > 0) {
html += '<div class="branch-children">';
html += renderBranchNodes(node.children, level + 1);
html += '</div>';
}
}
});
return html;
}
window.addEventListener('message', event => {
const message = event.data;
if (message.type === 'branchesFetched') {
branchTreeData = message.branchTree || [];
renderBranchTree(branchTreeData);
}
});
</script>
</body>
</html>`;
}
}
}