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