初步给项目、飞行器、容器页面添加了git拉取功能
This commit is contained in:
File diff suppressed because it is too large
Load Diff
13
src/panels/services/GitService.ts
Normal file → Executable file
13
src/panels/services/GitService.ts
Normal file → Executable 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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从已存在的磁盘目录导入容器
|
||||
*
|
||||
* ✅ 你的最新需求:
|
||||
* - 不创建“默认两个配置”
|
||||
* - 自动扫描容器目录下的『子文件夹』
|
||||
* - 每个子文件夹创建一个 ModuleFolder(type: '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);
|
||||
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user