355 lines
21 KiB
HTML
355 lines
21 KiB
HTML
<div class="layui-card" style="margin-bottom:5px;">
|
||
<div class="layui-card-header" style="display:flex;align-items:center;justify-content:space-between;">
|
||
<div>
|
||
<span class="title" style="font-size:18px;font-weight:700;">应用市场</span>
|
||
{if $is_client}
|
||
<span class="mode-badge client">客户端模式 · 连接:{$api_url}</span>
|
||
{else /}
|
||
<span class="mode-badge center">服务中心模式(本地市场)</span>
|
||
{/if}
|
||
</div>
|
||
<span>
|
||
<a href="{:url('addon/my')}" class="layui-btn layui-btn-normal layui-btn-sm">我的插件</a>
|
||
<a href="{:url('addon/index')}" class="layui-btn layui-btn-primary layui-btn-sm"><i class="layui-icon layui-icon-return"></i> 返回已安装插件</a>
|
||
</span>
|
||
</div>
|
||
|
||
{if $error}
|
||
<div class="store-banner err">{$error}</div>
|
||
{/if}
|
||
|
||
<div class="layui-card-body">
|
||
<form class="store-search" method="get" action="{:url('addon/market')}" style="margin-bottom:14px;display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
|
||
<input type="text" name="keyword" value="{$keyword|default=''}" placeholder="搜索插件名称/作者/描述" class="layui-input" style="width:240px;">
|
||
<select name="category" class="layui-input" style="width:140px;">
|
||
<option value="">全部分类</option>
|
||
{volist name="categories" id="c"}
|
||
<option value="{$c}" {eq name="category" value="$c"}selected{/eq}>{$c}</option>
|
||
{/volist}
|
||
</select>
|
||
<select name="type" class="layui-input" style="width:120px;">
|
||
<option value="">全部类型</option>
|
||
<option value="addon" {eq name="type" value="addon"}selected{/eq}>插件</option>
|
||
<option value="template" {eq name="type" value="template"}selected{/eq}>模板</option>
|
||
</select>
|
||
<select name="order" class="layui-input" style="width:130px;">
|
||
<option value="new" {eq name="order" value="new"}selected{/eq}>最新上架</option>
|
||
<option value="hot" {eq name="order" value="hot"}selected{/eq}>最热门</option>
|
||
<option value="price_asc" {eq name="order" value="price_asc"}selected{/eq}>价格升序</option>
|
||
<option value="price_desc" {eq name="order" value="price_desc"}selected{/eq}>价格降序</option>
|
||
</select>
|
||
<button type="submit" class="layui-btn layui-btn-sm">筛选</button>
|
||
{if $keyword || $category || $type}<a href="{:url('addon/market')}" class="layui-btn layui-btn-sm layui-btn-primary">清空</a>{/if}
|
||
</form>
|
||
<div class="store-wrap">
|
||
{if empty($list)}
|
||
<div class="empty">暂无可用插件</div>
|
||
{else /}
|
||
{volist name="list" id="it"}
|
||
<div class="layui-card plugin-card" data-logo="{$it.logo|htmlspecialchars}" data-title="{$it.title|htmlspecialchars}" data-version="{$it.version|htmlspecialchars}" data-author="{$it.author|htmlspecialchars}" data-price="{$it.price}" data-rating="{$it.rating|default=0}" data-intro="{$it.intro|htmlspecialchars}" data-screenshots="{$it.screenshots|default=''|htmlspecialchars}" data-tags="{$it.tags|default=''|htmlspecialchars}" data-name="{$it.name|htmlspecialchars}" data-installed="{$it.installed}" data-upgradable="{$it.upgradable}" data-localversion="{$it.local_version|default=''|htmlspecialchars}" data-category="{$it.category|default=''|htmlspecialchars}" data-download_count="{$it.download_count|default=0}" data-update_at="{$it.update_at|default=0}" data-changelog="{$it.changelog|default=''|htmlspecialchars}" data-require_framework="{$it.require_framework|default=''|htmlspecialchars}" data-type="{$it.type|default='addon'}" data-versions="{:htmlspecialchars(json_encode($it['versions'] ?? []))}">
|
||
<div class="layui-card-body">
|
||
{if $it.logo}
|
||
<img class="plugin-logo" src="{$it.logo}" alt="">
|
||
{/if}
|
||
<div class="plugin-title">{$it.title}<span class="ver">v{$it.version}</span>{eq name="it.type" value="template"}<span class="type-badge tpl">模板</span>{/eq}</div>
|
||
{if $it.category}<span class="plugin-cat">{$it.category}</span>{/if}
|
||
<div class="plugin-author">作者:{$it.author}</div>
|
||
{if $it.rating > 0}<div class="plugin-rate">★ <span>{$it.rating|default='0.0'}</span></div>{/if}
|
||
{if !empty($it.tag_list)}<div class="plugin-tags">{volist name="it.tag_list" id="t"}<span class="plugin-cat">{$t}</span> {/volist}</div>{/if}
|
||
<div class="plugin-intro">{$it.intro}</div>
|
||
<div class="plugin-foot">
|
||
{if $it.price > 0}
|
||
<span class="price">¥{$it.price}</span>
|
||
{else /}
|
||
<span class="price free">免费</span>
|
||
{/if}
|
||
{if $it.installed}
|
||
{if $it.upgradable}
|
||
<button class="layui-btn layui-btn-xs layui-btn-warm" data-act="install" data-name="{$it.name}" data-version="{$it.version}" data-type="{$it.type|default='addon'}">升级</button>
|
||
{else /}
|
||
<span class="layui-badge layui-bg-green">已安装 v{$it.local_version}</span>
|
||
{/if}
|
||
{else /}
|
||
<button class="layui-btn layui-btn-xs layui-btn-normal" data-act="install" data-name="{$it.name}" data-version="{$it.version}" data-type="{$it.type|default='addon'}">安装</button>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/volist}
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<style>
|
||
.store-wrap{max-width:1100px;margin:20px auto;padding:0 14px;display:flex;flex-wrap:wrap;gap:14px;}
|
||
.plugin-card{width:calc(33.333% - 10px);min-width:260px;flex:1 1 260px;cursor:pointer;transition:box-shadow .2s,transform .2s;}
|
||
.plugin-card:hover{box-shadow:0 6px 20px rgba(0,0,0,.12);transform:translateY(-2px);}
|
||
.plugin-card .layui-card-body{padding:18px;}
|
||
.plugin-logo{width:48px;height:48px;border-radius:8px;object-fit:cover;float:left;margin-right:12px;background:#eee;}
|
||
.plugin-title{font-size:16px;font-weight:700;line-height:24px;}
|
||
.plugin-title .ver{color:#999;font-size:12px;font-weight:400;margin-left:6px;}
|
||
.plugin-author{color:#999;font-size:12px;line-height:20px;}
|
||
.plugin-cat{display:inline-block;font-size:11px;color:#1e80ff;background:#e8f3ff;border-radius:10px;padding:1px 8px;margin:2px 4px 2px 0;vertical-align:middle;}
|
||
.type-badge{display:inline-block;font-size:11px;font-weight:400;border-radius:10px;padding:1px 8px;margin-left:6px;vertical-align:middle;}
|
||
.type-badge.tpl{color:#722ed1;background:#f4eefe;}
|
||
.plugin-rate{color:#ff9f00;font-size:12px;line-height:20px;}
|
||
.plugin-rate span{color:#ff9f00;font-weight:700;}
|
||
.plugin-tags{margin:4px 0;}
|
||
.store-search .layui-input{display:inline-block;}
|
||
.plugin-intro{color:#666;font-size:13px;line-height:22px;margin:12px 0;min-height:44px;}
|
||
.plugin-foot{display:flex;align-items:center;justify-content:space-between;}
|
||
.price{color:#e4393c;font-weight:700;font-size:16px;}
|
||
.price.free{color:#18a058;}
|
||
.empty{text-align:center;color:#999;padding:80px 0;width:100%;}
|
||
.mode-badge{display:inline-block;font-size:12px;font-weight:400;padding:2px 10px;border-radius:12px;margin-left:10px;}
|
||
.mode-badge.client{background:#e8f3ff;color:#1e80ff;}
|
||
.mode-badge.center{background:#e8f7ef;color:#18a058;}
|
||
.store-banner{padding:10px 20px;font-size:13px;}
|
||
.store-banner.err{background:#fde2e2;color:#d03050;}
|
||
|
||
/* 详情弹窗 */
|
||
.detail-pop{padding:20px;color:#333;}
|
||
.dp-head{display:flex;align-items:flex-start;gap:14px;}
|
||
.dp-logo{width:64px;height:64px;border-radius:10px;object-fit:cover;background:#eee;flex:0 0 auto;}
|
||
.dp-meta{flex:1 1 auto;min-width:0;}
|
||
.dp-title{font-size:18px;font-weight:700;}
|
||
.dp-title .ver{color:#999;font-size:12px;font-weight:400;margin-left:6px;}
|
||
.dp-author{color:#999;font-size:12px;line-height:22px;}
|
||
.dp-rate{color:#ff9f00;font-size:13px;line-height:22px;}
|
||
.dp-rate .rate-num{color:#999;margin-left:4px;}
|
||
.dp-tags{margin-top:4px;}
|
||
.dp-price{text-align:right;flex:0 0 auto;}
|
||
.dp-gal{margin:16px 0;}
|
||
.dp-main{width:100%;max-height:320px;object-fit:contain;background:#f5f5f5;border-radius:8px;}
|
||
.dp-thumbs{display:flex;gap:8px;margin-top:8px;flex-wrap:wrap;}
|
||
.shot-thumb{width:64px;height:64px;object-fit:cover;border-radius:6px;border:2px solid transparent;cursor:pointer;background:#eee;}
|
||
.shot-thumb.active{border-color:#1e80ff;}
|
||
.dp-intro{color:#555;font-size:14px;line-height:24px;margin:12px 0;}
|
||
.dp-changelog{color:#555;font-size:13px;line-height:1.8;margin:4px 0;white-space:normal;word-break:break-word;}
|
||
.dp-foot{text-align:right;margin-top:10px;}
|
||
.dp-ver-sel{font-size:13px;font-weight:400;color:#333;border:1px solid #d9d9d9;border-radius:4px;padding:2px 6px;margin-left:6px;vertical-align:middle;background:#fff;cursor:pointer;}
|
||
.dp-meta-extra{display:flex;flex-wrap:wrap;gap:16px;margin-top:10px;padding-top:10px;border-top:1px dashed #eee;color:#777;font-size:13px;}
|
||
.dp-meta-extra .mi b{color:#333;font-weight:600;}
|
||
.dp-section{margin-top:18px;}
|
||
.dp-section h4{font-size:14px;font-weight:700;color:#333;margin:0 0 10px;padding-left:8px;border-left:3px solid #1e80ff;}
|
||
.dp-versions{display:flex;flex-direction:column;gap:8px;}
|
||
.ver-item{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border:1px solid #eee;border-radius:8px;cursor:pointer;transition:border-color .15s,background .15s;}
|
||
.ver-item:hover{border-color:#1e80ff;}
|
||
.ver-item.active{background:#e8f3ff;border-color:#1e80ff;}
|
||
.ver-item .ver-name{font-weight:600;color:#333;}
|
||
.ver-item .ver-date{color:#999;font-size:12px;}
|
||
.ver-item .ver-price{color:#e4393c;font-weight:600;font-size:13px;}
|
||
.ver-item .ver-price.free{color:#18a058;}
|
||
@media (max-width:768px){.plugin-card{width:100%;}}
|
||
</style>
|
||
<script>
|
||
layui.use(['layer', 'http'], function () {
|
||
var layer = layui.layer, http = layui.http, $ = layui.$;
|
||
|
||
// 解析逗号分隔或 JSON 数组文本为列表
|
||
function parseList(s) {
|
||
s = (s || '').trim();
|
||
if (!s) return [];
|
||
if (s.charAt(0) === '[') {
|
||
try { return JSON.parse(s); } catch (e) {}
|
||
}
|
||
return s.split(',').map(function (x) { return x.trim(); }).filter(Boolean);
|
||
}
|
||
|
||
// 评分星级
|
||
function starsHtml(r) {
|
||
r = parseFloat(r) || 0;
|
||
var html = '';
|
||
for (var i = 1; i <= 5; i++) {
|
||
if (r >= i) html += '<i class="star on">★</i>';
|
||
else if (r >= i - 0.5) html += '<i class="star half">★</i>';
|
||
else html += '<i class="star">★</i>';
|
||
}
|
||
return html + ' <span class="rate-num">' + r.toFixed(1) + '</span>';
|
||
}
|
||
|
||
// 简单版本号比较:a>b 返回 1,a<b 返回 -1,相等 0
|
||
function cmpVer(a, b) {
|
||
var pa = String(a || '0').split('.'), pb = String(b || '0').split('.');
|
||
for (var i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||
var x = parseInt(pa[i] || 0, 10), y = parseInt(pb[i] || 0, 10);
|
||
if (x !== y) return x > y ? 1 : -1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
function priceHtmlOf(p) {
|
||
return (parseFloat(p) > 0)
|
||
? '<span class="price">¥' + p + '</span>'
|
||
: '<span class="price free">免费</span>';
|
||
}
|
||
|
||
// 按所选版本生成操作按钮(已装同版=徽章;已装低版=升级;未装=安装)
|
||
function actBtnOf(d, ver) {
|
||
var tAttr = ' data-type="' + (d.type || 'addon') + '"';
|
||
if (d.installed) {
|
||
if (cmpVer(ver, d.localVersion) > 0) {
|
||
return '<button class="layui-btn layui-btn-warm layui-btn-sm" data-act="install" data-name="' + d.name + '" data-version="' + ver + '"' + tAttr + '>升级到 v' + ver + '</button>';
|
||
}
|
||
if (cmpVer(ver, d.localVersion) === 0) {
|
||
return '<span class="layui-badge layui-bg-green">已安装 v' + d.localVersion + '</span>';
|
||
}
|
||
return '<button class="layui-btn layui-btn-primary layui-btn-sm" data-act="install" data-name="' + d.name + '" data-version="' + ver + '"' + tAttr + '>安装 v' + ver + '(低于本地 v' + d.localVersion + ')</button>';
|
||
}
|
||
return '<button class="layui-btn layui-btn-normal layui-btn-sm" data-act="install" data-name="' + d.name + '" data-version="' + ver + '"' + tAttr + '>安装 v' + ver + '</button>';
|
||
}
|
||
|
||
// 时间戳格式化为 YYYY-MM-DD
|
||
function fmtDate(ts) {
|
||
ts = parseInt(ts || 0, 10);
|
||
if (!ts) return '';
|
||
var d = new Date(ts * 1000);
|
||
var p = function (n) { return (n < 10 ? '0' : '') + n; };
|
||
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
|
||
}
|
||
|
||
// HTML 转义(防 XSS)
|
||
function escapeHtml(s) {
|
||
return String(s == null ? '' : s)
|
||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||
.replace(/"/g, '"').replace(/'/g, ''');
|
||
}
|
||
// 换行转 <br>
|
||
function nl2br(s) {
|
||
return String(s == null ? '' : s).replace(/\r\n|\r|\n/g, '<br>');
|
||
}
|
||
|
||
function openDetail(d) {
|
||
var shots = parseList(d.screenshots);
|
||
var mainShot = shots.length ? shots[0] : (d.logo || '');
|
||
var thumbs = shots.map(function (u, i) {
|
||
return '<img class="shot-thumb' + (i === 0 ? ' active' : '') + '" src="' + u + '" data-src="' + u + '">';
|
||
}).join('');
|
||
var tags = parseList(d.tags).map(function (t) {
|
||
return '<span class="plugin-cat">' + t + '</span>';
|
||
}).join('');
|
||
|
||
// 历史版本(服务端已按版本号降序返回);无 versions 时退化为当前版本单项
|
||
var versions = [];
|
||
try { versions = JSON.parse(d.versions || '[]'); } catch (e) {}
|
||
if (!versions || !versions.length) versions = [{ version: d.version, price: d.price, update_at: d.updateAt || 0 }];
|
||
|
||
// 版本选择:多版本时渲染下拉,否则只显示版本号
|
||
var verSelHtml;
|
||
if (versions.length > 1) {
|
||
var opts = versions.map(function (v, i) {
|
||
var tag = (parseFloat(v.price) > 0) ? '¥' + v.price : '免费';
|
||
return '<option value="' + i + '"' + (i === 0 ? ' selected' : '') + '>v' + v.version + '(' + tag + ')' + (i === 0 ? ' · 最新' : '') + '</option>';
|
||
}).join('');
|
||
verSelHtml = '<select class="dp-ver-sel" id="dpVerSel">' + opts + '</select>';
|
||
} else {
|
||
verSelHtml = '<span class="ver">v' + versions[0].version + '</span>';
|
||
}
|
||
|
||
// 版本历史区块(DZ 风格:列出全部版本,点击切换)
|
||
var verHistory = versions.map(function (v, i) {
|
||
var pt = (parseFloat(v.price) > 0) ? '¥' + v.price : '免费';
|
||
return '<div class="ver-item' + (i === 0 ? ' active' : '') + '" data-idx="' + i + '">'
|
||
+ '<span class="ver-name">v' + v.version + (i === 0 ? ' · 最新' : '') + '</span>'
|
||
+ '<span class="ver-date">' + (v.update_at ? fmtDate(v.update_at) : '—') + '</span>'
|
||
+ '<span class="ver-price' + (parseFloat(v.price) > 0 ? '' : ' free') + '">' + pt + '</span>'
|
||
+ '</div>';
|
||
}).join('');
|
||
|
||
var cur = versions[0];
|
||
// 信息栏:评分 / 下载量 / 更新时间 / 分类
|
||
var extra = ''
|
||
+ (d.rating > 0 ? '<span class="mi">评分:<b>' + (parseFloat(d.rating) || 0).toFixed(1) + '</b></span>' : '')
|
||
+ (d.downloadCount ? '<span class="mi">下载:<b>' + d.downloadCount + '</b></span>' : '')
|
||
+ (d.updateAt && fmtDate(d.updateAt) ? '<span class="mi">更新:<b>' + fmtDate(d.updateAt) + '</b></span>' : '')
|
||
+ (d.category ? '<span class="mi">分类:<b>' + d.category + '</b></span>' : '')
|
||
+ (d.requireFramework ? '<span class="mi">最低框架:<b>v' + d.requireFramework + '</b></span>' : '');
|
||
|
||
var html = ''
|
||
+ '<div class="detail-pop">'
|
||
+ '<div class="dp-head">'
|
||
+ (d.logo ? '<img class="dp-logo" src="' + d.logo + '">' : '')
|
||
+ '<div class="dp-meta">'
|
||
+ '<div class="dp-title">' + d.title + ' ' + verSelHtml + '</div>'
|
||
+ '<div class="dp-author">作者:' + d.author + '</div>'
|
||
+ '<div class="dp-rate">' + starsHtml(d.rating) + '</div>'
|
||
+ (tags ? '<div class="dp-tags">' + tags + '</div>' : '')
|
||
+ '</div>'
|
||
+ '<div class="dp-price" id="dpPrice">' + priceHtmlOf(cur.price) + '</div>'
|
||
+ '</div>'
|
||
+ (extra ? '<div class="dp-meta-extra">' + extra + '</div>' : '')
|
||
+ (shots.length ? '<div class="dp-gal"><img class="dp-main" src="' + mainShot + '"><div class="dp-thumbs">' + thumbs + '</div></div>' : '')
|
||
+ '<div class="dp-intro">' + (d.intro || '暂无简介') + '</div>'
|
||
+ (d.changelog ? '<div class="dp-section"><h4>更新日志</h4><div class="dp-changelog">' + nl2br(escapeHtml(d.changelog)) + '</div></div>' : '')
|
||
+ (versions.length > 1 ? '<div class="dp-section"><h4>历史版本</h4><div class="dp-versions" id="dpVers">' + verHistory + '</div></div>' : '')
|
||
+ '<div class="dp-foot" id="dpFoot">' + actBtnOf(d, cur.version) + '</div>'
|
||
+ '</div>';
|
||
layer.open({ type: 1, title: false, area: ['680px', 'auto'], shadeClose: true, content: html, success: function () {
|
||
bindDetailEvents(versions, d);
|
||
}});
|
||
}
|
||
|
||
// 绑定详情弹窗内交互:截略图切换、版本切换(下拉/历史列表联动价格与安装按钮)
|
||
function bindDetailEvents(versions, d) {
|
||
function apply(idx) {
|
||
var v = versions[idx] || versions[0];
|
||
$('#dpPrice').html(priceHtmlOf(v.price));
|
||
$('#dpFoot').html(actBtnOf(d, v.version));
|
||
$('#dpVerSel').val(String(idx));
|
||
$('#dpVers .ver-item').removeClass('active').filter('[data-idx="' + idx + '"]').addClass('active');
|
||
}
|
||
$('.dp-thumbs .shot-thumb').off('click').on('click', function () {
|
||
$('.dp-main').attr('src', $(this).data('src'));
|
||
$('.dp-thumbs .shot-thumb').removeClass('active');
|
||
$(this).addClass('active');
|
||
});
|
||
$('#dpVerSel').off('change').on('change', function () { apply(parseInt(this.value, 10)); });
|
||
$('#dpVers .ver-item').off('click').on('click', function () { apply(parseInt($(this).data('idx'), 10)); });
|
||
}
|
||
|
||
// 卡片点击(安装按钮除外)打开详情
|
||
$(document).on('click', '.plugin-card', function (e) {
|
||
if ($(e.target).closest('[data-act="install"]').length) return;
|
||
var $c = $(this);
|
||
openDetail({
|
||
logo: $c.data('logo'), title: $c.data('title'), version: $c.data('version'),
|
||
author: $c.data('author'), price: $c.data('price'), rating: $c.data('rating'),
|
||
intro: $c.data('intro'), screenshots: $c.data('screenshots'), tags: $c.data('tags'),
|
||
name: $c.data('name'), installed: $c.data('installed') === '1',
|
||
upgradable: $c.data('upgradable') === '1', localVersion: $c.data('localversion'),
|
||
category: $c.data('category'), downloadCount: $c.data('download_count'), updateAt: $c.data('update_at'),
|
||
changelog: $c.data('changelog'), requireFramework: $c.data('require_framework'),
|
||
type: $c.data('type') || 'addon',
|
||
versions: $c.attr('data-versions') || '[]'
|
||
});
|
||
});
|
||
|
||
// 安装 / 升级(卡片与弹窗内按钮共用)
|
||
$(document).on('click', '[data-act="install"]', function () {
|
||
var $btn = $(this);
|
||
var name = $btn.data('name');
|
||
var version = $btn.data('version');
|
||
var type = $btn.data('type') || 'addon';
|
||
var label = type === 'template' ? '模板' : '';
|
||
layer.confirm('确认安装' + label + '「' + name + '」' + (version ? ' v' + version : '') + '?', function (idx) {
|
||
layer.close(idx);
|
||
var loading = layer.load();
|
||
http.post('{:url("addon/downloadInstall")}', { name: name, version: version, type: type })
|
||
.then(function (res) {
|
||
layer.close(loading);
|
||
if (res.code === 0) {
|
||
layer.msg(res.message || '安装成功', { icon: 1 });
|
||
setTimeout(function () { location.reload(); }, 800);
|
||
} else {
|
||
layer.msg(res.message || '安装失败', { icon: 2 });
|
||
}
|
||
})
|
||
.catch(function () {
|
||
layer.close(loading);
|
||
layer.msg('请求失败', { icon: 2 });
|
||
});
|
||
});
|
||
});
|
||
});
|
||
</script>
|