Files
devstar/templates/user/settings/appstore.tmpl
2025-11-25 14:24:05 +08:00

1451 lines
48 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" (dict "ctxData" . "pageClass" "user settings appstore")}}
{{define "user/settings/appstore_body"}}
<meta name="_csrf" content="{{.CsrfToken}}">
<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;
}
}
/* 修复应用详情页面中网站和仓库按钮的文字对齐问题 */
#app-details-modal .ui.buttons .ui.button {
display: flex !important;
align-items: center !important;
justify-content: center !important;
height: 2.5em !important;
line-height: 1 !important;
}
#app-details-modal .ui.buttons .ui.button i.icon {
margin-right: 0.5em !important;
line-height: 1 !important;
}
#app-details-modal .ui.buttons .ui.button:not(.icon) {
padding-left: 1em !important;
padding-right: 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>
</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="kubernetes">
<i class="kubernetes icon"></i>
{{ctx.Locale.Tr "appstore.deployment_kubernetes"}}
</a>
<a class="item" data-deployment="docker">
<i class="docker icon"></i>
{{ctx.Locale.Tr "appstore.deployment_docker"}}
</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 -->
<div id="install-dynamic-fields"></div>
<h4 class="ui dividing header" style="margin-top:1em;">安装位置</h4>
<div id="install-target-inline">
<div class="grouped fields">
<label>选择安装位置</label>
{{if .IsAdmin}}
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="installTargetRadio" value="local" checked>
<label>默认位置</label>
</div>
</div>
{{end}}
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="installTargetRadio" value="kubeconfig" {{if not .IsAdmin}}checked{{end}}>
<label>自定义位置Kubernetes URL + Token</label>
</div>
</div>
</div>
<div id="k8s-credential-fields" style="display:none;">
<div class="field">
<label>Kubernetes API 地址</label>
<input type="text" id="k8s-url" placeholder="例如https://your-cluster:6443">
</div>
<div class="field">
<label>Bearer Token</label>
<textarea id="k8s-token" rows="4" placeholder="粘贴访问该集群的 Bearer Token"></textarea>
</div>
</div>
</div>
</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">运行状态</h4>
<div id="details-status">
<span class="ui grey text">正在获取状态...</span>
</div>
<div class="ui divider"></div>
<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="updateInstalledAppFromDetails()">更新应用</div>
<div class="ui orange button" id="pause-resume-btn" onclick="togglePauseResumeFromDetails()">暂停应用</div>
<div class="ui red button" onclick="uninstallCurrentAppFromDetails()">卸载应用</div>
<div class="ui red button" id="delete-app-btn" onclick="deleteAppFromDetails()">删除应用</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 -->
<!-- 已合并到安装窗口,移除原独立 Modal -->
<script>
let allApps = [];
let filteredApps = [];
let currentApp = null;
let currentAppStatus = null; // 存储当前应用的运行状态
let storeSource = 'local'; // local | devstar
// 安装位置local | remote
let installTarget = 'local';
let installK8sURL = '';
let installK8sToken = '';
// 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-modal input[name="installTargetRadio"]');
radioButtons.forEach(radio => {
radio.addEventListener('change', function() {
const kubeconfigFields = document.querySelector('#install-modal #k8s-credential-fields');
if (kubeconfigFields) {
kubeconfigFields.style.display = (this.value === 'kubeconfig') ? 'block' : 'none';
}
// 同步到原有全局变量,保持功能不变
installTarget = this.value;
});
});
// Kubernetes URL/Token 变化时,同步到全局变量
const kcContentEl = document.querySelector('#install-modal #k8s-url');
if (kcContentEl) {
kcContentEl.addEventListener('input', function() {
installK8sURL = this.value.trim();
});
}
const kcContextEl = document.querySelector('#install-modal #k8s-token');
if (kcContextEl) {
kcContextEl.addEventListener('input', function() {
installK8sToken = this.value.trim();
});
}
}
// 旧的安装位置弹窗相关函数已移除openInstallTargetModal、closeInstallTargetModal、saveInstallTarget
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 icon header">
<i class="search icon"></i>
{{ctx.Locale.Tr "appstore.no_apps"}}
</div>
</div>
`;
return;
}
filteredApps.forEach(app => {
const card = createAppCard(app);
grid.appendChild(card);
// 异步检查应用安装状态
checkAppInstallStatus(app);
});
}
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>';
}
// 初始显示安装按钮,后续异步检查状态
const actionButton = `<button class="ui primary compact button" onclick="showInstallModal('${app.id || app.app_id}')" id="install-btn-${app.id || app.app_id}">
<i class="download icon"></i>
安装
</button>`;
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>
${actionButton}
</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 (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';
}
// 初始化按钮显示状态(在状态加载完成前先隐藏所有操作按钮)
updateActionButtonsVisibility();
// 拉取并展示运行状态
loadAndRenderAppStatus(app);
}
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');
const dyn = document.getElementById('install-dynamic-fields');
dyn.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') {
dyn.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}
`;
dyn.appendChild(field);
});
setTimeout(() => {
const dropdowns = form.querySelectorAll('.ui.dropdown');
dropdowns.forEach(() => {});
const checkboxes = form.querySelectorAll('.ui.checkbox input[type="checkbox"]');
checkboxes.forEach(() => {});
}, 100);
}
// 追加安装位置区块已保留在模板中,这里只同步状态
const radios = document.querySelectorAll('#install-modal input[name="installTargetRadio"]');
radios.forEach(r => { r.checked = (r.value === installTarget); });
const kubeconfigFields = document.querySelector('#install-modal #k8s-credential-fields');
if (kubeconfigFields) {
kubeconfigFields.style.display = (installTarget === 'kubeconfig') ? 'block' : 'none';
}
const kcContent = document.querySelector('#install-modal #k8s-url');
const kcContext = document.querySelector('#install-modal #k8s-token');
if (kcContent) kcContent.value = installK8sURL || '';
if (kcContext) kcContext.value = installK8sToken || '';
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);
}
}
// 更新应用(从详情弹窗触发)
async function updateInstalledAppFromDetails() {
if (!currentApp) return;
// 关闭详情弹窗,打开更新弹窗(类似安装)
closeDetailsModal();
showUpdateModal(currentApp.id || currentApp.app_id);
}
// 显示更新弹窗
function showUpdateModal(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 + ' - 更新';
// 生成配置表单(重用安装的逻辑)
const form = document.getElementById('install-form');
const dyn = document.getElementById('install-dynamic-fields');
dyn.innerHTML = '';
// 配置表单生成逻辑(与 showInstallModal 相同)
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') {
dyn.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}
`;
dyn.appendChild(field);
});
}
// 设置安装位置表单
const radios = document.querySelectorAll('#install-modal input[name="installTargetRadio"]');
radios.forEach(r => { r.checked = (r.value === installTarget); });
const kubeconfigFields = document.querySelector('#install-modal #k8s-credential-fields');
if (kubeconfigFields) {
kubeconfigFields.style.display = (installTarget === 'kubeconfig') ? 'block' : 'none';
}
const kcContent = document.querySelector('#install-modal #k8s-url');
const kcContext = document.querySelector('#install-modal #k8s-token');
if (kcContent) kcContent.value = installK8sURL || '';
if (kcContext) kcContext.value = installK8sToken || '';
// 修改安装按钮文字为"更新"
const installBtn = document.querySelector('#install-modal .ui.primary.button[onclick="installApp()"]');
if (installBtn) {
installBtn.textContent = '更新应用';
installBtn.onclick = updateApp;
}
const modal = document.getElementById('install-modal');
if (modal) {
modal.style.display = 'block';
}
}
// 更新应用(表单提交)
function updateApp() {
if (!currentApp) return;
const form = document.getElementById('install-form');
const formData = new FormData(form);
const config = {};
for (let [key, value] of formData.entries()) {
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;
}
}
const appId = currentApp.id || currentApp.app_id;
const currentDeploymentFilter = document.querySelector('.deployment-filter .active');
const deploymentType = currentDeploymentFilter?.getAttribute('data-deployment');
// 从安装窗口读取安装位置
const selectedTarget = document.querySelector('#install-modal input[name="installTargetRadio"]:checked');
installTarget = selectedTarget ? selectedTarget.value : 'local';
if (installTarget === 'kubeconfig') {
const kcContentEl = document.querySelector('#install-modal #k8s-url');
const kcContextEl = document.querySelector('#install-modal #k8s-token');
installK8sURL = kcContentEl ? kcContentEl.value.trim() : '';
installK8sToken = kcContextEl ? kcContextEl.value.trim() : '';
if (!installK8sURL || !installK8sToken) {
alert('请输入 Kubernetes API 地址和 Token');
return;
}
config.deploy = { type: 'kubernetes' };
} else {
if (deploymentType === 'docker') {
config.deploy = { type: 'docker' };
} else if (deploymentType === 'kubernetes') {
config.deploy = { type: 'kubernetes' };
} else {
// 回退到 docker 作为默认选择
config.deploy = { type: 'docker' };
}
}
// 创建表单提交到更新路由
const updateForm = document.createElement('form');
updateForm.method = 'POST';
updateForm.action = `/user/settings/appstore/update/${appId}`;
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = '_csrf';
csrfInput.value = document.querySelector('meta[name="_csrf"]')?.content || '';
updateForm.appendChild(csrfInput);
const appIdInput = document.createElement('input');
appIdInput.type = 'hidden';
appIdInput.name = 'app_id';
appIdInput.value = appId;
updateForm.appendChild(appIdInput);
const configInput = document.createElement('input');
configInput.type = 'hidden';
configInput.name = 'config';
configInput.value = JSON.stringify(config);
updateForm.appendChild(configInput);
const targetInput = document.createElement('input');
targetInput.type = 'hidden';
targetInput.name = 'install_target';
targetInput.value = installTarget;
updateForm.appendChild(targetInput);
const redirectInput = document.createElement('input');
redirectInput.type = 'hidden';
redirectInput.name = 'redirect_to';
redirectInput.value = window.location.pathname;
updateForm.appendChild(redirectInput);
if (installTarget === 'kubeconfig') {
const kcInput = document.createElement('input');
kcInput.type = 'hidden';
kcInput.name = 'k8s_url';
kcInput.value = installK8sURL;
updateForm.appendChild(kcInput);
const kctxInput = document.createElement('input');
kctxInput.type = 'hidden';
kctxInput.name = 'k8s_token';
kctxInput.value = installK8sToken;
updateForm.appendChild(kctxInput);
}
document.body.appendChild(updateForm);
updateForm.submit();
closeInstallModal();
}
// 检查应用安装状态并更新按钮
async function checkAppInstallStatus(app) {
const appId = app.id || app.app_id;
const installBtn = document.getElementById(`install-btn-${appId}`);
if (!installBtn) return;
try {
const resp = await fetch(`/user/settings/appstore/api/status/${appId}`);
if (resp.ok) {
const data = await resp.json();
const phase = data.phase || data.status?.phase || 'Unknown';
const replicas = data.replicas ?? data.status?.replicas ?? 0;
const readyReplicas = data.readyReplicas ?? data.status?.readyReplicas ?? 0;
// 判断是否已安装(包括暂停状态)
const isInstalled = phase !== 'Not Installed' && phase !== 'Unknown';
if (isInstalled) {
installBtn.className = 'ui green compact button disabled';
installBtn.innerHTML = '<i class="check icon"></i>已安装';
installBtn.onclick = null;
}
}
} catch (e) {
// 状态检查失败,保持安装按钮不变
console.log('Failed to check app status:', e);
}
}
// 卸载应用(从详情弹窗触发)
function uninstallCurrentAppFromDetails() {
if (!currentApp) return;
const appId = currentApp.id || currentApp.app_id;
const uninstallForm = document.createElement('form');
uninstallForm.method = 'POST';
uninstallForm.action = `/user/settings/appstore/uninstall/${appId}`;
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = '_csrf';
csrfInput.value = document.querySelector('meta[name="_csrf"]')?.content || '';
uninstallForm.appendChild(csrfInput);
const appIdInput = document.createElement('input');
appIdInput.type = 'hidden';
appIdInput.name = 'app_id';
appIdInput.value = appId;
uninstallForm.appendChild(appIdInput);
const redirectInput = document.createElement('input');
redirectInput.type = 'hidden';
redirectInput.name = 'redirect_to';
redirectInput.value = window.location.pathname;
uninstallForm.appendChild(redirectInput);
document.body.appendChild(uninstallForm);
uninstallForm.submit();
}
// 删除应用(从详情弹窗触发)
function deleteAppFromDetails() {
if (!currentApp) return;
const appId = currentApp.id || currentApp.app_id;
const appName = currentApp.name || appId;
// 确认删除
if (!confirm(`确定要删除应用 "${appName}" 吗?\n\n注意这将从应用商店中删除该应用模板但不会影响已安装的应用实例。`)) {
return;
}
const deleteForm = document.createElement('form');
deleteForm.method = 'POST';
deleteForm.action = `/user/settings/appstore/delete/${appId}`;
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = '_csrf';
csrfInput.value = document.querySelector('meta[name="_csrf"]')?.content || '';
deleteForm.appendChild(csrfInput);
const redirectInput = document.createElement('input');
redirectInput.type = 'hidden';
redirectInput.name = 'redirect_to';
redirectInput.value = window.location.pathname;
deleteForm.appendChild(redirectInput);
document.body.appendChild(deleteForm);
deleteForm.submit();
}
// 暂停/恢复应用(从详情弹窗触发)
function togglePauseResumeFromDetails() {
if (!currentApp) return;
const appId = currentApp.id || currentApp.app_id;
// 根据当前状态决定操作
const isRunning = currentAppStatus && currentAppStatus.replicas > 0;
const action = isRunning ? 'stop' : 'resume';
const actionText = isRunning ? '暂停' : '恢复';
const confirmText = isRunning ?
'确定要暂停应用吗?暂停后应用将停止运行,但数据会保留。' :
'确定要恢复应用吗?应用将重新开始运行。';
// 显示确认对话框
if (!confirm(confirmText)) {
return;
}
// 创建暂停/恢复应用的请求
const form = document.createElement('form');
form.method = 'POST';
form.action = `/user/settings/appstore/${action}/${appId}`;
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = '_csrf';
csrfInput.value = document.querySelector('meta[name="_csrf"]')?.content || '';
form.appendChild(csrfInput);
const appIdInput = document.createElement('input');
appIdInput.type = 'hidden';
appIdInput.name = 'app_id';
appIdInput.value = appId;
form.appendChild(appIdInput);
const redirectInput = document.createElement('input');
redirectInput.type = 'hidden';
redirectInput.name = 'redirect_to';
redirectInput.value = window.location.pathname;
form.appendChild(redirectInput);
document.body.appendChild(form);
form.submit();
}
// 暂停应用(从详情弹窗触发)- 保留向后兼容
function stopCurrentAppFromDetails() {
togglePauseResumeFromDetails();
}
// 加载并渲染运行状态(基于 K8s Application CRD
async function loadAndRenderAppStatus(app) {
const statusEl = document.getElementById('details-status');
if (!statusEl || !app) return;
statusEl.innerHTML = '<span class="ui grey text">正在获取状态...</span>';
try {
const appId = app.id || app.app_id;
const resp = await fetch(`/user/settings/appstore/api/status/${appId}`);
if (!resp.ok) throw new Error(`status: ${resp.status}`);
const data = await resp.json();
// 期望后端返回 { phase, replicas, readyReplicas, ready } 或类似结构
const phase = data.phase || data.status?.phase || 'Unknown';
const replicas = data.replicas ?? data.status?.replicas ?? '-';
const readyReplicas = data.readyReplicas ?? data.status?.readyReplicas ?? '-';
const ready = (typeof data.ready === 'boolean')
? data.ready
: (phase !== 'Not Installed' && phase !== 'Unknown' && (replicas === 0 || readyReplicas > 0));
// 存储应用状态到全局变量
currentAppStatus = {
phase: phase,
replicas: replicas,
readyReplicas: readyReplicas,
ready: ready
};
statusEl.innerHTML = `
<div class="ui small statistic">
<div class="value">${ready ? '<span class="ui green text">Ready</span>' : '<span class="ui orange text">Not Ready</span>'}</div>
<div class="label">${phase} · ${readyReplicas}/${replicas}</div>
</div>
`;
// 根据状态动态控制按钮显示
updateActionButtonsVisibility();
} catch (e) {
statusEl.innerHTML = '<span class="ui red text">获取状态失败</span>';
currentAppStatus = null;
updateActionButtonsVisibility();
}
}
// 根据应用状态动态控制操作按钮的显示
function updateActionButtonsVisibility() {
const updateBtn = document.querySelector('#app-details-modal .ui.primary.button[onclick="updateInstalledAppFromDetails()"]');
const pauseResumeBtn = document.getElementById('pause-resume-btn');
const uninstallBtn = document.querySelector('#app-details-modal .ui.red.button[onclick="uninstallCurrentAppFromDetails()"]');
const deleteBtn = document.getElementById('delete-app-btn');
if (!currentAppStatus) {
// 状态未知,隐藏所有操作按钮(除了删除按钮,因为未安装时应该显示)
if (updateBtn) updateBtn.style.display = 'none';
if (pauseResumeBtn) pauseResumeBtn.style.display = 'none';
if (uninstallBtn) uninstallBtn.style.display = 'none';
// 状态未知时,默认显示删除按钮(假设未安装)
if (deleteBtn) deleteBtn.style.display = 'inline-block';
return;
}
const isInstalled = currentAppStatus.phase !== 'Not Installed' && currentAppStatus.phase !== 'Unknown';
const isRunning = isInstalled && currentAppStatus.replicas > 0;
const isStopped = isInstalled && currentAppStatus.replicas === 0;
// 更新按钮:只有已安装的应用才能更新
if (updateBtn) {
updateBtn.style.display = isInstalled ? 'inline-block' : 'none';
}
// 暂停/恢复按钮:已安装的应用可以暂停或恢复
if (pauseResumeBtn) {
if (isInstalled) {
pauseResumeBtn.style.display = 'inline-block';
// 根据应用状态更新按钮文本和样式
if (isRunning) {
pauseResumeBtn.textContent = '暂停应用';
pauseResumeBtn.className = 'ui orange button';
} else if (isStopped) {
pauseResumeBtn.textContent = '恢复应用';
pauseResumeBtn.className = 'ui green button';
}
} else {
pauseResumeBtn.style.display = 'none';
}
}
// 卸载按钮:只有已安装的应用才能卸载
if (uninstallBtn) {
uninstallBtn.style.display = isInstalled ? 'inline-block' : 'none';
}
// 删除按钮:只有未安装的应用才能删除(删除的是应用模板)
if (deleteBtn) {
deleteBtn.style.display = isInstalled ? 'none' : 'inline-block';
}
}
async function installApp() {
if (!currentApp) return;
const form = document.getElementById('install-form');
const formData = new FormData(form);
const config = {};
for (let [key, value] of formData.entries()) {
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;
}
}
const appId = currentApp.id || currentApp.app_id;
const currentDeploymentFilter = document.querySelector('.deployment-filter .active');
const deploymentType = currentDeploymentFilter?.getAttribute('data-deployment');
// 从安装窗口读取安装位置
const selectedTarget = document.querySelector('#install-modal input[name="installTargetRadio"]:checked');
installTarget = selectedTarget ? selectedTarget.value : 'local';
if (installTarget === 'kubeconfig') {
const kcContentEl = document.querySelector('#install-modal #k8s-url');
const kcContextEl = document.querySelector('#install-modal #k8s-token');
installK8sURL = kcContentEl ? kcContentEl.value.trim() : '';
installK8sToken = kcContextEl ? kcContextEl.value.trim() : '';
if (!installK8sURL || !installK8sToken) {
alert('请输入 Kubernetes API 地址和 Token');
return;
}
config.deploy = { type: 'kubernetes' };
} else {
if (deploymentType === 'docker') {
config.deploy = { type: 'docker' };
} else if (deploymentType === 'kubernetes') {
config.deploy = { type: 'kubernetes' };
} else {
// 回退到 docker 作为默认选择
config.deploy = { type: 'docker' };
}
}
// 避免全局 beforeunload 提示:标记当前表单忽略脏检测
try { form.classList.add('ignore-dirty'); form.classList.remove('dirty'); } catch(e) {}
const installForm = document.createElement('form');
installForm.method = 'POST';
installForm.action = `/user/settings/appstore/install/${appId}`;
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = '_csrf';
csrfInput.value = document.querySelector('meta[name="_csrf"]')?.content || '';
installForm.appendChild(csrfInput);
const appIdInput = document.createElement('input');
appIdInput.type = 'hidden';
appIdInput.name = 'app_id';
appIdInput.value = appId;
installForm.appendChild(appIdInput);
const configInput = document.createElement('input');
configInput.type = 'hidden';
configInput.name = 'config';
configInput.value = JSON.stringify(config);
installForm.appendChild(configInput);
const targetInput = document.createElement('input');
targetInput.type = 'hidden';
targetInput.name = 'install_target';
targetInput.value = installTarget;
installForm.appendChild(targetInput);
// 带上回跳地址,优先返回提交页面(后台或用户设置页)
const redirectInput = document.createElement('input');
redirectInput.type = 'hidden';
redirectInput.name = 'redirect_to';
redirectInput.value = window.location.pathname;
installForm.appendChild(redirectInput);
if (installTarget === 'kubeconfig') {
const kcInput = document.createElement('input');
kcInput.type = 'hidden';
kcInput.name = 'k8s_url';
kcInput.value = installK8sURL;
installForm.appendChild(kcInput);
const kctxInput = document.createElement('input');
kctxInput.type = 'hidden';
kctxInput.name = 'k8s_token';
kctxInput.value = installK8sToken;
installForm.appendChild(kctxInput);
}
// 提交安装表单
document.body.appendChild(installForm);
// 显示安装中状态
const installBtn = document.querySelector('#install-modal .ui.primary.button');
if (installBtn) {
installBtn.className = 'ui loading primary button';
installBtn.textContent = '安装中...';
installBtn.disabled = true;
}
// 与卸载一致:使用表单直接提交,后端重定向以显示绿色提示
installForm.submit();
return;
}
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() {
console.log('submitAddApp called'); // 调试信息
const jsonText = document.getElementById('add-app-json').value.trim();
console.log('JSON text:', jsonText); // 调试信息
let appData;
try {
appData = JSON.parse(jsonText);
console.log('Parsed app data:', appData); // 调试信息
} catch (e) {
console.error('JSON parse error:', e); // 调试信息
alert('JSON格式错误');
return;
}
// 获取 CSRF token
const csrfToken = document.querySelector('meta[name="_csrf"]')?.content || '';
console.log('CSRF token:', csrfToken); // 调试信息
try {
const resp = await fetch('/user/settings/appstore/api/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(appData)
});
console.log('Response status:', resp.status); // 调试信息
if (resp.ok) {
closeAddAppModal();
loadAppsFromAPI(); // 刷新应用列表
alert('添加成功');
} else {
const err = await resp.json();
console.error('API error:', err); // 调试信息
alert('添加失败: ' + (err.error || '未知错误'));
}
} catch (error) {
console.error('Fetch error:', error); // 调试信息
alert('网络错误: ' + error.message);
}
}
</script>
{{end}}
{{template "user/settings/appstore_body" .}}
{{template "user/settings/layout_footer" .}}