Files
YwxAppThink/app/backend/view/addon/market.html
T

355 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 返回 1a<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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
// 换行转 <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>