Files
devstar-create-from-template/templates/user/settings/appstore.tmpl

1016 lines
30 KiB
Handlebars
Raw Normal View History

2025-08-25 15:46:12 +08:00
{{template "user/settings/layout_head" .}}
<style>
/* 应用商店卡片样式 - 强制覆盖 */
.app-store-grid .ui.card {
height: 380px !important;
display: flex !important;
flex-direction: column !important;
margin: 0.5em 0 !important;
}
.app-store-grid .ui.card .image {
height: 140px !important;
background: #f8f9fa !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
overflow: hidden !important;
flex-shrink: 0 !important;
}
.app-store-grid .ui.card .image img {
max-width: 90px !important;
max-height: 90px !important;
width: auto !important;
height: auto !important;
object-fit: contain !important;
}
.app-store-grid .ui.card .content {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
padding: 1em !important;
}
.app-store-grid .ui.card .content .header {
font-size: 1.1em !important;
margin-bottom: 0.5em !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
line-height: 1.3em !important;
}
.app-store-grid .ui.card .content .meta {
margin-bottom: 0.5em !important;
font-size: 0.9em !important;
}
.app-store-grid .ui.card .content .description {
flex: 1 !important;
overflow: hidden !important;
display: -webkit-box !important;
-webkit-line-clamp: 3 !important;
-webkit-box-orient: vertical !important;
text-overflow: ellipsis !important;
line-height: 1.4em !important;
max-height: 4.2em !important;
font-size: 0.9em !important;
color: #666 !important;
}
.app-store-grid .ui.card .content .extra {
border-top: 1px solid rgba(34,36,38,.1) !important;
margin-top: 0.5em !important;
padding-top: 0.5em !important;
flex-shrink: 0 !important;
}
.app-store-grid .ui.card .extra.content {
padding: 0.5em 1em !important;
flex-shrink: 0 !important;
border-top: 1px solid rgba(34,36,38,.1) !important;
}
.app-store-grid .ui.card .ui.mini.label {
font-size: 0.7em !important;
}
.app-store-grid .ui.card .ui.compact.button {
padding: 0.5em 0.8em !important;
font-size: 14px !important;
}
.app-store-grid .ui.card .ui.button,
.app-store-grid .ui.card .ui.buttons .button {
font-size: 14px !important;
}
.app-store-grid .ui.buttons {
font-size: 14px !important;
}
/* 确保列布局正确 */
.app-store-grid.ui.grid > .column {
padding: 0.5rem !important;
}
/* 基本的modal显示样式 */
.ui.modal {
display: none;
position: absolute !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
z-index: 1000 !important;
}
.ui.modal[style*="display: block"] {
display: block !important;
}
/* 响应式调整 */
@media (max-width: 768px) {
.app-store-grid .ui.card {
height: auto !important;
min-height: 300px !important;
}
.app-store-grid .ui.card .image {
height: 100px !important;
}
.app-store-grid .ui.card .content .header {
font-size: 1em !important;
}
}
</style>
<div class="ui stackable grid">
<div class="sixteen wide column">
<div class="ui segment">
<div class="ui stackable grid">
<div class="sixteen wide column">
<h2 class="ui header">
<i class="shopping cart icon"></i>
<div class="content" style="width:100%; display:flex; align-items:center; justify-content:space-between; gap:.5rem;">
<div>
<div class="ui buttons" style="margin-right: .5em;">
<button class="ui primary button" id="btn-source-local">本地应用商店</button>
<div class="or"></div>
<button class="ui button" id="btn-source-devstar">DevStar 应用商店</button>
</div>
</div>
<div>
<div class="ui basic buttons">
<button class="ui button" onclick="openInstallTargetModal()">
<i class="server icon"></i> 安装位置
</button>
</div>
</div>
</div>
</h2>
</div>
</div>
<!-- Search and Filter Bar -->
<div class="ui stackable grid">
<div class="sixteen wide column">
<div class="ui fluid action input">
<input type="text" placeholder="{{ctx.Locale.Tr "appstore.search_placeholder"}}" id="app-search">
<button class="ui button" onclick="searchApps()">
<i class="search icon"></i>
</button>
</div>
</div>
</div>
<!-- 添加应用按钮 -->
<button class="ui primary button" style="margin-bottom:1em;" onclick="showAddAppModal()">添加应用</button>
<div class="ui stackable grid" style="margin-top: 1rem;">
<div class="four wide column">
<!-- Category Filter -->
<div class="ui vertical fluid menu">
<div class="header item">{{ctx.Locale.Tr "appstore.category_all"}}</div>
<a class="item active" data-category="all">
<i class="grid layout icon"></i>
{{ctx.Locale.Tr "appstore.category_all"}}
</a>
<a class="item" data-category="web-server">
<i class="server icon"></i>
{{ctx.Locale.Tr "appstore.category_web_server"}}
</a>
<a class="item" data-category="database">
<i class="database icon"></i>
{{ctx.Locale.Tr "appstore.category_database"}}
</a>
<a class="item" data-category="development">
<i class="code icon"></i>
{{ctx.Locale.Tr "appstore.category_development"}}
</a>
<a class="item" data-category="monitoring">
<i class="chart line icon"></i>
{{ctx.Locale.Tr "appstore.category_monitoring"}}
</a>
<a class="item" data-category="other">
<i class="ellipsis horizontal icon"></i>
{{ctx.Locale.Tr "appstore.category_other"}}
</a>
</div>
<!-- Deployment Type Filter -->
<div class="ui vertical fluid menu deployment-filter" style="margin-top: 1rem;">
<div class="header item">{{ctx.Locale.Tr "appstore.deployment_type"}}</div>
<a class="item active" data-deployment="all">
<i class="grid layout icon"></i>
{{ctx.Locale.Tr "appstore.category_all"}}
</a>
<a class="item" data-deployment="docker">
<i class="docker icon"></i>
{{ctx.Locale.Tr "appstore.deployment_docker"}}
</a>
<a class="item" data-deployment="kubernetes">
<i class="kubernetes icon"></i>
{{ctx.Locale.Tr "appstore.deployment_kubernetes"}}
</a>
</div>
</div>
<div class="twelve wide column">
<!-- Apps Grid -->
<div class="ui stackable three column grid app-store-grid" id="apps-grid">
<!-- App cards will be loaded here -->
<div class="sixteen wide column">
<div class="ui placeholder segment">
<div class="ui icon header">
<i class="spinner loading icon"></i>
{{ctx.Locale.Tr "appstore.loading"}}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- App Installation Modal -->
<div class="ui modal" id="install-modal">
<div class="header">
<span id="install-app-name"></span>
</div>
<div class="content">
<div class="description">
<form class="ui form" id="install-form">
<!-- Configuration fields will be dynamically generated here -->
</form>
</div>
</div>
<div class="actions">
<div class="ui button" onclick="closeInstallModal()">{{ctx.Locale.Tr "cancel"}}</div>
<div class="ui primary button" onclick="installApp()">{{ctx.Locale.Tr "appstore.install"}}</div>
</div>
</div>
<!-- App Details Modal -->
<div class="ui modal" id="app-details-modal">
<div class="header">
<span id="details-app-name"></span>
</div>
<div class="content">
<div class="ui stackable grid">
<div class="eight wide column">
<div class="ui segment">
<h4 class="ui header">{{ctx.Locale.Tr "appstore.description"}}</h4>
<p id="details-description"></p>
<h4 class="ui header">{{ctx.Locale.Tr "appstore.requirements"}}</h4>
<div id="details-requirements"></div>
<h4 class="ui header">{{ctx.Locale.Tr "appstore.tags"}}</h4>
<div id="details-tags"></div>
</div>
</div>
<div class="eight wide column">
<div class="ui segment">
<h4 class="ui header">{{ctx.Locale.Tr "appstore.version"}}</h4>
<p id="details-version"></p>
<h4 class="ui header">{{ctx.Locale.Tr "appstore.author"}}</h4>
<p id="details-author"></p>
<h4 class="ui header">{{ctx.Locale.Tr "appstore.license"}}</h4>
<p id="details-license"></p>
<div class="ui buttons">
<a class="ui button" id="details-website" target="_blank">
<i class="external alternate icon"></i>
{{ctx.Locale.Tr "appstore.website"}}
</a>
<a class="ui button" id="details-repository" target="_blank">
<i class="github icon"></i>
{{ctx.Locale.Tr "appstore.repository"}}
</a>
</div>
</div>
</div>
</div>
</div>
<div class="actions">
<div class="ui button" onclick="closeDetailsModal()">{{ctx.Locale.Tr "cancel"}}</div>
<div class="ui primary button" onclick="showInstallModalFromDetails()">{{ctx.Locale.Tr "appstore.install"}}</div>
</div>
</div>
<!-- 添加应用模态框 -->
<div class="ui modal" id="add-app-modal">
<div class="header">添加应用</div>
<div class="content">
<textarea id="add-app-json" rows="12" style="width:100%;" placeholder="粘贴应用JSON内容"></textarea>
</div>
<div class="actions">
<div class="ui button" onclick="closeAddAppModal()">取消</div>
<div class="ui primary button" onclick="submitAddApp()">提交</div>
</div>
</div>
<!-- 安装位置设置 Modal -->
<div class="ui modal" id="install-target-modal">
<div class="header">安装位置</div>
<div class="content">
<div class="ui form">
<div class="grouped fields">
<label>选择安装位置</label>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="installTargetRadio" value="local" checked>
<label>本机(默认)</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="installTargetRadio" value="kubeconfig">
<label>外部集群Kubeconfig</label>
</div>
</div>
</div>
<div id="kubeconfig-fields" style="display:none;">
<div class="field">
<label>Kubeconfig粘贴内容</label>
<textarea id="kubeconfig-content" rows="8" placeholder="粘贴 kubeconfig 内容"></textarea>
</div>
<div class="field">
<label>Context 名称(可选)</label>
<input type="text" id="kubeconfig-context" placeholder="不填则使用 current-context">
</div>
</div>
</div>
</div>
<div class="actions">
<div class="ui button" onclick="closeInstallTargetModal()">取消</div>
<div class="ui primary button" onclick="saveInstallTarget()">保存</div>
</div>
</div>
<script>
let allApps = [];
let filteredApps = [];
let currentApp = null;
let storeSource = 'local'; // local | devstar
// 安装位置local | kubeconfig
let installTarget = 'local';
let installKubeconfigContent = '';
let installKubeconfigContext = '';
// Initialize the page
document.addEventListener('DOMContentLoaded', function() {
setupSourceToggle();
setupInstallTargetUI();
loadAppsFromAPI();
setupEventListeners();
});
function setupSourceToggle() {
const btnLocal = document.getElementById('btn-source-local');
const btnDev = document.getElementById('btn-source-devstar');
const applyActive = () => {
if (storeSource === 'local') {
btnLocal.classList.add('primary');
btnDev.classList.remove('primary');
} else {
btnDev.classList.add('primary');
btnLocal.classList.remove('primary');
}
};
btnLocal.addEventListener('click', () => {
storeSource = 'local';
applyActive();
loadAppsFromAPI();
});
btnDev.addEventListener('click', () => {
storeSource = 'devstar';
applyActive();
loadAppsFromAPI();
});
applyActive();
}
function setupInstallTargetUI() {
// 使用原生JavaScript初始化radio button事件
const radioButtons = document.querySelectorAll('#install-target-modal input[type="radio"]');
radioButtons.forEach(radio => {
radio.addEventListener('change', function() {
const kubeconfigFields = document.getElementById('kubeconfig-fields');
if (kubeconfigFields) {
kubeconfigFields.style.display = (this.value === 'kubeconfig') ? 'block' : 'none';
}
});
});
}
function openInstallTargetModal() {
// 预填当前状态
const radios = document.getElementsByName('installTargetRadio');
for (const r of radios) {
r.checked = (r.value === installTarget);
}
const kubeconfigFields = document.getElementById('kubeconfig-fields');
if (kubeconfigFields) {
kubeconfigFields.style.display = (installTarget === 'kubeconfig') ? 'block' : 'none';
}
const kubeconfigContent = document.getElementById('kubeconfig-content');
const kubeconfigContext = document.getElementById('kubeconfig-context');
if (kubeconfigContent) {
kubeconfigContent.value = installKubeconfigContent || '';
}
if (kubeconfigContext) {
kubeconfigContext.value = installKubeconfigContext || '';
}
// 显示modal
const modal = document.getElementById('install-target-modal');
if (modal) {
modal.style.display = 'block';
}
}
function closeInstallTargetModal() {
// 隐藏modal
const modal = document.getElementById('install-target-modal');
if (modal) {
modal.style.display = 'none';
}
}
function saveInstallTarget() {
const radios = document.getElementsByName('installTargetRadio');
let selected = 'local';
for (const r of radios) {
if (r.checked) { selected = r.value; break; }
}
installTarget = selected;
if (installTarget === 'kubeconfig') {
installKubeconfigContent = document.getElementById('kubeconfig-content').value.trim();
installKubeconfigContext = document.getElementById('kubeconfig-context').value.trim();
if (!installKubeconfigContent) {
alert('请输入 kubeconfig 内容');
return;
}
} else {
installKubeconfigContent = '';
installKubeconfigContext = '';
}
closeInstallTargetModal();
}
function setupEventListeners() {
// Category filter
document.querySelectorAll('[data-category]').forEach(item => {
item.addEventListener('click', function() {
document.querySelectorAll('[data-category]').forEach(i => i.classList.remove('active'));
this.classList.add('active');
filterApps();
});
});
// Deployment filter
document.querySelectorAll('[data-deployment]').forEach(item => {
item.addEventListener('click', function() {
document.querySelectorAll('[data-deployment]').forEach(i => i.classList.remove('active'));
this.classList.add('active');
filterApps();
});
});
// Search input
document.getElementById('app-search').addEventListener('input', function() {
filterApps();
});
}
async function loadAppsFromAPI() {
try {
// 构建查询参数
const params = new URLSearchParams();
if (storeSource === 'devstar') {
params.append('source', 'devstar');
}
// 添加过滤参数
const selectedCategory = document.querySelector('[data-category].active')?.dataset.category;
const selectedDeployment = document.querySelector('[data-deployment].active')?.dataset.deployment;
const searchTerm = document.getElementById('app-search').value;
if (selectedCategory && selectedCategory !== 'all') {
params.append('category', selectedCategory);
}
if (selectedDeployment && selectedDeployment !== 'all') {
params.append('deployment', selectedDeployment);
}
if (searchTerm.trim()) {
params.append('search', searchTerm);
}
const url = `/user/settings/appstore/api/apps?${params.toString()}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
allApps = data.apps || [];
filteredApps = [...allApps];
loadApps();
} catch (error) {
console.error('Error loading apps:', error);
showError('Failed to load apps');
}
}
function showError(message) {
const grid = document.getElementById('apps-grid');
grid.innerHTML = `
<div class="sixteen wide column">
<div class="ui negative message">
<div class="header">Error</div>
<p>${message}</p>
</div>
</div>
`;
}
function loadApps() {
const grid = document.getElementById('apps-grid');
grid.innerHTML = '';
if (filteredApps.length === 0) {
grid.innerHTML = `
<div class="sixteen wide column">
<div class="ui placeholder segment">
<div class="ui icon header">
<i class="search icon"></i>
{{ctx.Locale.Tr "appstore.no_apps"}}
</div>
</div>
</div>
`;
return;
}
filteredApps.forEach(app => {
const card = createAppCard(app);
grid.appendChild(card);
});
}
function createAppCard(app) {
const column = document.createElement('div');
column.className = 'column';
// Create badges for official and verified apps
let badges = '';
if (app.isOfficial || app.is_official) {
badges += '<span class="ui blue label">Official</span> ';
}
if (app.isVerified || app.is_verified) {
badges += '<span class="ui green label">Verified</span> ';
}
// Get deployment type, prioritize deployment_type field
let deploymentType = 'docker';
if (app.deployment_type) {
deploymentType = app.deployment_type;
} else if (app.deployment) {
deploymentType = app.deployment;
} else if (app.Deploy && app.Deploy.Type) {
deploymentType = app.Deploy.Type;
} else if (app.deploy && app.deploy.type) {
deploymentType = app.deploy.type;
}
// Get install count
const installCount = app.installCount || app.install_count || 0;
// Create deployment type label with appropriate styling
let deploymentLabel = '';
if (deploymentType === 'both') {
deploymentLabel = '<span class="ui mini orange label">Docker & K8s</span>';
} else if (deploymentType === 'kubernetes') {
deploymentLabel = '<span class="ui mini blue label">Kubernetes</span>';
} else {
deploymentLabel = '<span class="ui mini green label">Docker</span>';
}
column.innerHTML = `
<div class="ui fluid card">
<div class="image">
<img src="${app.icon || '/assets/img/logo.png'}" alt="${app.name}" onerror="this.src='/assets/img/logo.png'">
</div>
<div class="content">
<div class="header" title="${app.name}">${app.name}</div>
<div class="meta">
<span class="date">v${app.version}</span>
<span class="right floated">
${deploymentLabel}
</span>
</div>
<div class="description" title="${app.description || 'No description available'}">
${truncateText(app.description || 'No description available', 100)}
</div>
<div class="extra">
${badges}
<span class="ui mini label">
<i class="download icon"></i>
${installCount} installs
</span>
</div>
</div>
<div class="extra content">
<div class="ui two buttons">
<button class="ui basic compact button" onclick="showAppDetails('${app.id || app.app_id}')">
<i class="info circle icon"></i>
详情
</button>
<button class="ui primary compact button" onclick="showInstallModal('${app.id || app.app_id}')">
<i class="download icon"></i>
安装
</button>
</div>
</div>
</div>
`;
return column;
}
// 文本截断函数
function truncateText(text, maxLength) {
if (text.length <= maxLength) {
return text;
}
return text.substring(0, maxLength) + '...';
}
function filterApps() {
const selectedCategory = document.querySelector('[data-category].active').dataset.category;
const selectedDeployment = document.querySelector('[data-deployment].active').dataset.deployment;
const searchTerm = document.getElementById('app-search').value.toLowerCase();
filteredApps = allApps.filter(app => {
const categoryMatch = selectedCategory === 'all' || app.category === selectedCategory;
// Check deployment type from different sources, prioritize deployment_type field
let appDeployment = 'docker';
if (app.deployment_type) {
appDeployment = app.deployment_type;
} else if (app.deployment) {
appDeployment = app.deployment;
} else if (app.Deploy && app.Deploy.Type) {
appDeployment = app.Deploy.Type;
} else if (app.deploy && app.deploy.type) {
appDeployment = app.deploy.type;
}
// Handle deployment type matching, including 'both' type
let deploymentMatch = false;
if (selectedDeployment === 'all') {
deploymentMatch = true;
} else if (appDeployment === 'both') {
// 'both' type matches both 'docker' and 'kubernetes' filters
deploymentMatch = true;
} else {
deploymentMatch = appDeployment === selectedDeployment;
}
// Search in name, description and tags
const searchMatch = app.name.toLowerCase().includes(searchTerm) ||
(app.description && app.description.toLowerCase().includes(searchTerm)) ||
(app.tags && app.tags.some && app.tags.some(tag => tag.toLowerCase().includes(searchTerm))) ||
(app.Tags && app.Tags.some && app.Tags.some(tag => tag.toLowerCase().includes(searchTerm)));
return categoryMatch && deploymentMatch && searchMatch;
});
loadApps();
}
function searchApps() {
filterApps();
}
function showAppDetails(appId) {
const app = allApps.find(a => a.id === appId || a.app_id === appId);
if (!app) return;
currentApp = app;
document.getElementById('details-app-name').textContent = app.name;
document.getElementById('details-description').textContent = app.description || 'No description available';
document.getElementById('details-version').textContent = app.version;
document.getElementById('details-author').textContent = app.author || 'Unknown';
document.getElementById('details-license').textContent = app.license || 'Unknown';
// Requirements
const requirements = document.getElementById('details-requirements');
if (app.requirements) {
requirements.innerHTML = `
<p><strong>系统要求:</strong></p>
<ul>
<li>内存: ${app.requirements.min_memory || app.requirements.minMemory || 'Unknown'}</li>
<li>CPU: ${app.requirements.min_cpu || app.requirements.minCPU || 'Unknown'}</li>
<li>存储: ${app.requirements.min_storage || app.requirements.minStorage || 'Unknown'}</li>
</ul>
`;
} else {
requirements.innerHTML = '<p>No requirements specified</p>';
}
// Tags
const tags = document.getElementById('details-tags');
if (app.tags && app.tags.length > 0) {
tags.innerHTML = app.tags.map(tag => `<span class="ui label">${tag}</span>`).join('');
} else if (app.Tags && app.Tags.length > 0) {
tags.innerHTML = app.Tags.map(tag => `<span class="ui label">${tag}</span>`).join('');
} else {
tags.innerHTML = '<p>No tags specified</p>';
}
// Repository
if (app.repository) {
document.getElementById('details-repository').href = app.repository;
document.getElementById('details-repository').style.display = 'inline-block';
} else {
document.getElementById('details-repository').style.display = 'none';
}
// 显示modal
const modal = document.getElementById('app-details-modal');
if (modal) {
modal.style.display = 'block';
}
}
function showInstallModal(appId) {
const app = allApps.find(a => a.id === appId || a.app_id === appId);
if (!app) return;
currentApp = app;
document.getElementById('install-app-name').textContent = app.name;
// Generate configuration form
const form = document.getElementById('install-form');
form.innerHTML = '';
// 兼容后端 config/schema 为 null 的情况
let configSchema = null;
let configDefaults = null;
if (app.config && app.config.schema) {
configSchema = app.config.schema;
configDefaults = app.config.default || {};
} else if (app.Config && app.Config.Schema) {
configSchema = app.Config.Schema;
configDefaults = app.Config.Default || {};
}
// 新增健壮性处理
if (!configSchema || typeof configSchema !== 'object') {
// 默认渲染一个端口输入框
form.innerHTML = `
<div class="field">
<label>port <span style="color: red;">*</span></label>
<input type="number" name="port" value="80" required>
</div>
`;
} else {
Object.entries(configSchema).forEach(([key, config]) => {
if (!config) return; // 防御
let field = document.createElement('div');
field.className = 'field';
let inputHtml = '';
const defaultValue = configDefaults[key] || config.default || '';
if (config.type === 'int') {
const min = config.min || '';
const max = config.max || '';
inputHtml = `<input type="number" name="${key}" value="${defaultValue}" ${config.required ? 'required' : ''} ${min ? 'min="' + min + '"' : ''} ${max ? 'max="' + max + '"' : ''}>`;
} else if (config.type === 'string') {
inputHtml = `<input type="text" name="${key}" value="${defaultValue}" ${config.required ? 'required' : ''}>`;
} else if (config.type === 'bool' || config.type === 'boolean') {
inputHtml = `
<div class="ui checkbox">
<input type="checkbox" name="${key}" ${defaultValue ? 'checked' : ''}>
<label></label>
</div>
`;
} else if (config.type === 'select' && config.options) {
let optionsHtml = '';
config.options.forEach(option => {
const selected = option === defaultValue ? 'selected' : '';
optionsHtml += `<option value="${option}" ${selected}>${option}</option>`;
});
inputHtml = `<select name="${key}" class="ui dropdown" ${config.required ? 'required' : ''}>${optionsHtml}</select>`;
} else {
inputHtml = `<input type="text" name="${key}" value="${defaultValue}" ${config.required ? 'required' : ''}>`;
}
field.innerHTML = `
<label>${key} ${config.required ? '<span style="color: red;">*</span>' : ''}</label>
${config.description ? `<div class="ui small text">${config.description}</div>` : ''}
${inputHtml}
`;
form.appendChild(field);
});
// 使用原生JavaScript初始化组件
setTimeout(() => {
// 初始化dropdown
const dropdowns = form.querySelectorAll('.ui.dropdown');
dropdowns.forEach(dropdown => {
// 这里可以添加dropdown的初始化逻辑
});
// 初始化checkbox
const checkboxes = form.querySelectorAll('.ui.checkbox input[type="checkbox"]');
checkboxes.forEach(checkbox => {
// 这里可以添加checkbox的初始化逻辑
});
}, 100);
}
// 显示modal
const modal = document.getElementById('install-modal');
if (modal) {
modal.style.display = 'block';
}
}
function closeInstallModal() {
// 隐藏modal
const modal = document.getElementById('install-modal');
if (modal) {
modal.style.display = 'none';
}
}
function closeDetailsModal() {
// 隐藏modal
const modal = document.getElementById('app-details-modal');
if (modal) {
modal.style.display = 'none';
}
}
function showInstallModalFromDetails() {
if (currentApp) {
closeDetailsModal();
showInstallModal(currentApp.id || currentApp.app_id);
}
}
function installApp() {
if (!currentApp) return;
// Collect form data
const form = document.getElementById('install-form');
const formData = new FormData(form);
const config = {};
for (let [key, value] of formData.entries()) {
// Handle checkbox values
const input = form.querySelector(`[name="${key}"]`);
if (input && input.type === 'checkbox') {
config[key] = input.checked;
} else if (input && input.type === 'number') {
config[key] = parseInt(value) || 0;
} else {
config[key] = value;
}
}
// Send installation request
const appId = currentApp.id || currentApp.app_id;
// 根据用户选择的部署分类设置 Deploy.Type
const currentDeploymentFilter = document.querySelector('.deployment-filter .active');
const deploymentType = currentDeploymentFilter?.getAttribute('data-deployment');
if (installTarget === 'kubeconfig') {
// 外部集群安装:设置为 kubernetes
config.deploy = { type: 'kubernetes' };
} else {
// 本地安装:根据当前选择的部署分类设置
if (deploymentType === 'docker') {
config.deploy = { type: 'docker' };
} else if (deploymentType === 'kubernetes') {
config.deploy = { type: 'kubernetes' };
} else {
// 如果选择"all",根据应用自身类型决定
if (currentApp.deployment_type === 'both') {
// 对于支持两种部署方式的应用,默认选择 Docker
config.deploy = { type: 'docker' };
} else {
config.deploy = { type: currentApp.deployment_type || 'docker' };
}
}
}
console.log('Installing app:', appId, 'with config:', config, 'target:', installTarget);
// Create a form and submit it to the install endpoint
const installForm = document.createElement('form');
installForm.method = 'POST';
installForm.action = `/user/settings/appstore/install/${appId}`;
// Add CSRF token
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = '_csrf';
csrfInput.value = document.querySelector('meta[name="_csrf"]')?.content || '';
installForm.appendChild(csrfInput);
// Add app ID
const appIdInput = document.createElement('input');
appIdInput.type = 'hidden';
appIdInput.name = 'app_id';
appIdInput.value = appId;
installForm.appendChild(appIdInput);
// Add config as JSON
const configInput = document.createElement('input');
configInput.type = 'hidden';
configInput.name = 'config';
configInput.value = JSON.stringify(config);
installForm.appendChild(configInput);
// Add install target
const targetInput = document.createElement('input');
targetInput.type = 'hidden';
targetInput.name = 'install_target';
targetInput.value = installTarget;
installForm.appendChild(targetInput);
if (installTarget === 'kubeconfig') {
const kcInput = document.createElement('input');
kcInput.type = 'hidden';
kcInput.name = 'kubeconfig';
kcInput.value = installKubeconfigContent;
installForm.appendChild(kcInput);
const kctxInput = document.createElement('input');
kctxInput.type = 'hidden';
kctxInput.name = 'kubeconfig_context';
kctxInput.value = installKubeconfigContext;
installForm.appendChild(kctxInput);
}
document.body.appendChild(installForm);
installForm.submit();
closeInstallModal();
}
function showAddAppModal() {
document.getElementById('add-app-json').value = '';
// 显示modal
const modal = document.getElementById('add-app-modal');
if (modal) {
modal.style.display = 'block';
}
}
function closeAddAppModal() {
// 隐藏modal
const modal = document.getElementById('add-app-modal');
if (modal) {
modal.style.display = 'none';
}
}
async function submitAddApp() {
const jsonText = document.getElementById('add-app-json').value.trim();
let appData;
try {
appData = JSON.parse(jsonText);
} catch (e) {
alert('JSON格式错误');
return;
}
const resp = await fetch('/user/settings/appstore/api/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(appData)
});
if (resp.ok) {
closeAddAppModal();
loadAppsFromAPI(); // 刷新应用列表
alert('添加成功');
} else {
const err = await resp.json();
alert('添加失败: ' + (err.error || '未知错误'));
}
}
</script>
{{template "user/settings/layout_footer" .}}