Files
devstar-create-from-template/templates/user/settings/appstore.tmpl
2025-08-25 15:46:12 +08:00

1016 lines
30 KiB
Handlebars
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{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" .}}