1451 lines
48 KiB
Handlebars
1451 lines
48 KiB
Handlebars
{{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" .}} |