chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
@@ -0,0 +1,93 @@
<div class="layui-card" style="margin:20px;">
<div class="layui-card-header">插件审核(开发者上架申请)</div>
<div class="layui-card-body">
<div class="layui-form layui-row" style="margin-bottom:12px;">
<div class="layui-inline">
<select id="statusFilter">
<option value="">全部状态</option>
<option value="0">待审核</option>
<option value="1">已发布</option>
<option value="2">已驳回</option>
</select>
</div>
<div class="layui-inline">
<button class="layui-btn" id="searchBtn">搜索</button>
</div>
</div>
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
</div>
</div>
<script type="text/html" id="toolTpl">
{{# if(d.status == 0){ }}
<a class="layui-btn layui-btn-xs" lay-event="audit">通过</a>
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="reject">驳回</a>
{{# } else { }}
<span class="layui-badge {{ d.status == 1 ? 'layui-bg-green' : '' }}">
{{ d.status == 1 ? '已发布' : '已驳回' }}
</span>
{{# } }}
</script>
<script>
layui.use(['table', 'jquery', 'layer'], function () {
var table = layui.table, $ = layui.jquery, layer = layui.layer;
table.render({
elem: '#dataTable',
url: '{:url("/appmall/backend/addonreview/index")}',
page: true,
response: { statusName: 'code', msgName: 'message', countName: 'count', dataName: 'data' },
parseData: function (res) {
return {
code: res.code,
msg: res.message,
count: res.count || 0,
data: res.data || []
};
},
where: { status: $('#statusFilter').val() },
cols: [[
{field: 'id', title: 'ID', width: 70},
{field: 'title', title: '插件名称', width: 160},
{field: 'name', title: '标识', width: 140},
{field: 'version', title: '版本', width: 90},
{field: 'developer_id', title: '开发者ID', width: 100},
{field: 'price', title: '价格', width: 90, templet: function(d){return d.price ? d.price : '免费';}},
{field: 'status', title: '状态', width: 90, templet: function(d){
if(d.status==0) return '<span class="layui-badge">待审核</span>';
if(d.status==1) return '<span class="layui-badge layui-bg-green">已发布</span>';
return '<span class="layui-badge layui-bg-red">已驳回</span>';
}},
{field: 'reject_reason', title: '驳回原因', width: 160},
{field: 'create_at', title: '提交时间', width: 170, templet: function(d){return layui.util.toDateString(d.create_at*1000);}},
{title: '操作', width: 160, templet: '#toolTpl', fixed: 'right'}
]]
});
$('#searchBtn').on('click', function () {
table.reload('dataTable', { where: { status: $('#statusFilter').val() }, page: { curr: 1 } });
});
table.on('tool(dataTable)', function (obj) {
var d = obj.data;
if (obj.event === 'audit') {
layer.confirm('确认通过该插件上架申请?', function (index) {
$.post('{:url("/appmall/backend/addonreview/audit")}', { id: d.id }, function (r) {
layer.msg(r.msg || '操作完成');
if (r.code === 0) { table.reload('dataTable'); }
layer.close(index);
}, 'json');
});
} else if (obj.event === 'reject') {
layer.prompt({ title: '填写驳回原因', formType: 2 }, function (val, index) {
$.post('{:url("/appmall/backend/addonreview/reject")}', { id: d.id, reason: val }, function (r) {
layer.msg(r.msg || '操作完成');
if (r.code === 0) { table.reload('dataTable'); }
layer.close(index);
}, 'json');
});
}
});
});
</script>
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
{block name="title"}<title>{$title | default='YwxApp'}</title>{/block}
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link href="/assets/layui/css/layui.css" rel="stylesheet">
<link href="/assets/ywxapp/css/wxapp.css" rel="stylesheet">
<style>
html,
body {
height: 100%;
display: flex;
flex-direction: column;
}
body {
padding-bottom: 64px;
}
.layui-card {
flex: 1;
display: flex;
flex-direction: column;
}
.layui-card-body {
display: flex;
flex-direction: column;
flex: 1;
}
.layui-table-view {
display: flex;
flex-direction: column;
flex: 1;
}
.layui-table-box {
display: flex;
flex-direction: column;
flex: 1;
}
.layui-table-body {
display: flex;
flex-direction: column;
flex: 1;
}
</style>
<script src="/assets/layui/layui.js"></script>
<script type="text/javascript">
// 全局路径配置:仅注入部署参数,路由方法来自 layui 的 route 模块。
// app 为 ThinkPHP 真实应用目录名(backend/member/frontend/home),对应 public/static/<app> 目录。
layui.app= window.APP = {
root: "{$site.root|default=''}",
assetUrl: "/assets",
module: "{$site.module}",
realModule: "{$site.app}",
routeBase: "{$route_base}",
controller: "{$site.controller}",
action: "{$site.action}"
};
</script>
<script src="/assets/ywxapp/ywxapp.js" module="{$site.app}"></script>
</head>
<body>
{block name="body"}{/block}
{block name="script"}{/block}
</body>
</html>
@@ -0,0 +1,112 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>开发者管理 - 应用市场管理</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/assets/layui/css/layui.css" rel="stylesheet">
<link href="/assets/ywxapp/css/wxapp.css" rel="stylesheet">
<style>body{background:#f2f3f5;padding:15px;}</style>
</head>
<body>
<form class="layui-form" lay-filter="filterForm">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">状态</label>
<div class="layui-input-inline" style="width:140px;">
<select name="status">
<option value="">全部</option>
<option value="0">待审核</option>
<option value="1">正常</option>
<option value="-1">已禁用</option>
</select>
</div>
</div>
<button class="layui-btn" lay-submit lay-filter="search">搜索</button>
</div>
</form>
<table id="dataTable" lay-filter="dataTable"></table>
<script type="text/html" id="statusTpl">
{{# if(d.status == 0){ }}<span class="layui-badge layui-bg-orange">待审核</span>
{{# } else if(d.status == 1){ }}<span class="layui-badge layui-bg-green">正常</span>
{{# } else { }}<span class="layui-badge">已禁用</span>{{# } }}
</script>
<script type="text/html" id="operateTpl">
{{# if(d.status == 0){ }}
<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="approve">通过</a>
{{# } }}
{{# if(d.status == 1){ }}
<a class="layui-btn layui-btn-xs" lay-event="resend">重发令牌</a>
<a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="reset">重置令牌</a>
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="disable">禁用</a>
{{# } }}
{{# if(d.status == -1){ }}
<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="approve">启用</a>
{{# } }}
</script>
<script src="/assets/layui/layui.js"></script>
<script src="/assets/ywxapp/ywxapp.js" module="{$site.app}"></script>
<script>
layui.use(['table', 'form', 'layer', 'jquery', 'http'], function () {
var table = layui.table, form = layui.form, layer = layui.layer, $ = layui.jquery, http = layui.http;
table.render({
elem: '#dataTable',
url: 'index',
page: true,
limits: [10, 20, 50],
limit: 10,
cols: [[
{ field: 'id', title: 'ID', width: 70 },
{ field: 'username', title: '开发者', minWidth: 140 },
{ field: 'email', title: '邮箱', minWidth: 200 },
{ field: 'token', title: '令牌', minWidth: 320, templet: function (d) { return '<span style="word-break:break-all;">' + d.token + '</span>'; } },
{ field: 'status', title: '状态', width: 90, templet: '#statusTpl' },
{ field: 'created_at', title: '创建时间', width: 170, templet: function (d) { return d.created_at ? new Date(d.created_at * 1000).toLocaleString() : ''; } },
{ title: '操作', width: 260, toolbar: '#operateTpl', fixed: 'right' }
]],
response: { statusName: 'code', statusCode: 0, msgName: 'message', countName: 'count', dataName: 'data' }
});
form.on('submit(search)', function (data) {
table.reload('dataTable', { where: data.field, page: { curr: 1 } });
return false;
});
table.on('tool(dataTable)', function (elem) {
var d = elem.data, id = d.id;
if (elem.event === 'approve') {
layer.confirm('确认' + (d.status == 0 ? '通过该开发者并邮件下发令牌?' : '启用该开发者?'), function () {
http.post('approve', { id: id }).then(function (r) {
if (r.code === 0) { layer.msg('操作成功', { icon: 1 }); table.reload('dataTable'); }
else { layer.msg(r.message || '操作失败', { icon: 2 }); }
}).catch(function () { layer.msg('请求失败', { icon: 2 }); });
});
} else if (elem.event === 'disable') {
layer.confirm('确认禁用该开发者?其令牌将立即失效。', function () {
http.post('disable', { id: id }).then(function (r) {
if (r.code === 0) { layer.msg('已禁用', { icon: 1 }); table.reload('dataTable'); }
else { layer.msg(r.message || '操作失败', { icon: 2 }); }
}).catch(function () { layer.msg('请求失败', { icon: 2 }); });
});
} else if (elem.event === 'resend') {
http.post('resend', { id: id }).then(function (r) {
layer.msg(r.message || (r.code === 0 ? '已重发' : '失败'), { icon: r.code === 0 ? 1 : 2 });
}).catch(function () { layer.msg('请求失败', { icon: 2 }); });
} else if (elem.event === 'reset') {
layer.confirm('确认重置令牌?旧令牌将失效。', function () {
http.post('reset', { id: id }).then(function (r) {
if (r.code === 0) { layer.msg('已重置' + (r.data && r.data.token ? ',新令牌:' + r.data.token : ''), { icon: 1, time: 4000 }); table.reload('dataTable'); }
else { layer.msg(r.message || '操作失败', { icon: 2 }); }
}).catch(function () { layer.msg('请求失败', { icon: 2 }); });
});
}
});
});
</script>
</body>
</html>
@@ -0,0 +1,144 @@
<div class="layui-card">
<div class="layui-card-header">主框架版本管理(升级服务器)</div>
<div class="layui-card-body">
<blockquote class="layui-elem-quote layui-quote-nm" style="margin-bottom:12px;">
同一版本号可先后上传「整包」「增量补丁」「完整安装包」,三者共存于同一条版本记录;
客户端升级时自动择优(正好停在补丁基础版本走补丁,否则走整包),完整安装包供全新部署下载。重新上传任一包后需再次「发布」。
</blockquote>
<form class="layui-form" id="uploadForm">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">版本号</label>
<div class="layui-input-inline">
<input type="text" name="version" placeholder="x.y.z" class="layui-input" required>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">标题</label>
<div class="layui-input-inline">
<input type="text" name="title" placeholder="可选,如 安全修复" class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">类型</label>
<div class="layui-input-inline">
<select name="type" id="pkgType" class="layui-input">
<option value="0">整包(full)</option>
<option value="1">增量补丁(patch)</option>
<option value="2">完整安装包(install)</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">基础版本</label>
<div class="layui-input-inline">
<input type="text" name="from_version" id="fromVersion" placeholder="仅增量补丁需要,如 1.2.0" class="layui-input">
</div>
</div>
<div class="layui-inline">
<button type="button" class="layui-btn" id="btnUpload">
<i class="layui-icon layui-icon-upload"></i> 上传核心包
</button>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">更新日志</label>
<div class="layui-input-block">
<textarea name="changelog" class="layui-textarea" placeholder="更新内容..."></textarea>
</div>
</div>
</form>
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
</div>
</div>
<script>
layui.use(['table', 'upload', 'jquery', 'layer'], function () {
var table = layui.table, upload = layui.upload, $ = layui.jquery, layer = layui.layer;
table.render({
elem: '#dataTable',
url: '{:url("/appmall/backend/framework/index")}',
page: true,
cols: [[
{field: 'id', title: 'ID', width: 70},
{field: 'version', title: '版本号', width: 120},
{title: '包', width: 200, templet: function (d) {
var h = '';
h += d.file_path ? '<span class="layui-badge layui-bg-blue">整包</span> '
: '<span class="layui-badge layui-bg-gray">无整包</span> ';
h += d.patch_path ? '<span class="layui-badge layui-bg-orange">补丁</span> ' : '';
h += d.install_path ? '<span class="layui-badge layui-bg-cyan">安装包</span>' : '';
return h;
}},
{field: 'patch_from', title: '补丁基础版本', width: 110, templet: function (d) {
return d.patch_from || d.from_version || '';
}},
{field: 'title', title: '标题'},
{field: 'changelog', title: '更新日志'},
{field: 'status', title: '状态', width: 90, templet: function (d) {
return d.status == 1
? '<span class="layui-badge layui-bg-green">已发布</span>'
: '<span class="layui-badge">草稿</span>';
}},
{field: 'download_count', title: '下载数', width: 90},
{title: '操作', width: 360, templet: function (d) {
var h = '';
if (d.status != 1) h += '<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="publish">发布</a>';
if (d.file_path) h += '<a class="layui-btn layui-btn-xs" lay-event="download">下载整包</a>';
if (d.patch_path) h += '<a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="downloadPatch">下载补丁</a>';
if (d.install_path) h += '<a class="layui-btn layui-btn-xs layui-bg-cyan" lay-event="downloadInstall">下载安装包</a>';
h += '<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="del">删除</a>';
return h;
}}
]]
});
upload.render({
elem: '#btnUpload',
url: '{:url("/appmall/backend/framework/upload")}',
accept: 'file',
exts: 'zip',
before: function () {
if (!$('input[name="version"]').val()) {
layer.msg('请先填写版本号'); return false;
}
if ($('#pkgType').val() == '1' && !$('#fromVersion').val()) {
layer.msg('增量补丁必须填写基础版本'); return false;
}
},
data: {
version: function () { return $('input[name="version"]').val(); },
title: function () { return $('input[name="title"]').val(); },
changelog: function () { return $('textarea[name="changelog"]').val(); },
type: function () { return $('#pkgType').val(); },
from_version: function () { return $('#fromVersion').val(); }
},
done: function (res) {
layer.msg(res.message || '完成');
if (res.code === 0) table.reload('dataTable');
},
error: function () { layer.msg('上传失败'); }
});
table.on('tool(dataTable)', function (obj) {
var d = obj.data;
if (obj.event === 'publish') {
$.post('{:url("/appmall/backend/framework/publish")}', {id: d.id}, function (r) {
layer.msg(r.message); if (r.code === 0) table.reload('dataTable');
});
} else if (obj.event === 'download') {
location.href = '{:url("/appmall/backend/framework/download")}?id=' + d.id + '&type=full';
} else if (obj.event === 'downloadPatch') {
location.href = '{:url("/appmall/backend/framework/download")}?id=' + d.id + '&type=patch';
} else if (obj.event === 'downloadInstall') {
location.href = '{:url("/appmall/backend/framework/download")}?id=' + d.id + '&type=install';
} else if (obj.event === 'del') {
layer.confirm('确定删除该版本?', function () {
$.post('{:url("/appmall/backend/framework/delete")}', {id: d.id}, function (r) {
layer.msg(r.message); if (r.code === 0) table.reload('dataTable');
});
});
}
});
});
</script>
@@ -0,0 +1,51 @@
<div class="layui-card" style="margin:20px;">
<div class="layui-card-header">收益账本(开发者分红)</div>
<div class="layui-card-body">
<div class="layui-row" id="summary" style="margin-bottom:12px;"></div>
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
</div>
</div>
<script>
layui.use(['table', 'jquery'], function () {
var table = layui.table, $ = layui.jquery;
var summaryData = null;
table.render({
elem: '#dataTable',
url: '{:url("/appmall/backend/revenue/index")}',
page: true,
// Result::success 返回 code:0 / message / data:{list,summary} / count
response: { statusName: 'code', msgName: 'message', countName: 'count', dataName: 'data' },
parseData: function (res) {
summaryData = (res.data && res.data.summary) ? res.data.summary : null;
return {
code: res.code,
msg: res.message,
count: res.count || 0,
data: (res.data && res.data.list) ? res.data.list : []
};
},
cols: [[
{field: 'id', title: 'ID', width: 70},
{field: 'developer_id', title: '开发者ID', width: 100},
{field: 'aid', title: '商品ID', width: 90},
{field: 'uid', title: '购买用户', width: 90},
{field: 'amount', title: '订单金额', width: 100},
{field: 'commission', title: '平台抽成', width: 100},
{field: 'income', title: '开发者收益', width: 110, templet: function(d){return '<b>'+d.income+'</b>';}},
{field: 'status', title: '结算', width: 90, templet: function(d){return d.status==1?'<span class="layui-badge layui-bg-green">已结算</span>':'<span class="layui-badge">待结算</span>';}},
{field: 'create_at', title: '时间', width: 170, templet: function(d){return layui.util.toDateString(d.create_at*1000);}}
]],
done: function () {
if (summaryData) {
$('#summary').html(
'总销售额:<b>' + summaryData.total_amount + '</b> &nbsp;|&nbsp; ' +
'平台抽成:<b>' + summaryData.total_commission + '</b> &nbsp;|&nbsp; ' +
'开发者收益:<b style="color:#16a34a;">' + summaryData.total_income + '</b>'
);
}
}
});
});
</script>
@@ -0,0 +1,48 @@
<div class="layui-card" style="margin:20px;">
<div class="layui-card-header">提现管理(开发者分红结算)</div>
<div class="layui-card-body">
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
</div>
</div>
<script>
layui.use(['table', 'jquery', 'layer'], function () {
var table = layui.table, $ = layui.jquery, layer = layui.layer;
table.render({
elem: '#dataTable',
url: '{:url("/appmall/backend/revenue/withdrawals")}',
page: true,
cols: [[
{field: 'id', title: 'ID', width: 70},
{field: 'developer_id', title: '开发者ID', width: 100},
{field: 'amount', title: '提现金额', width: 110},
{field: 'channel', title: '渠道', width: 90},
{field: 'account', title: '收款账号', width: 180},
{field: 'status', title: '状态', width: 100, templet: function(d){
return d.status==1?'<span class="layui-badge layui-bg-green">已打款</span>'
: (d.status==-1?'<span class="layui-badge layui-bg-orange">已驳回</span>'
:'<span class="layui-badge">待处理</span>');
}},
{field: 'create_at', title: '申请时间', width: 170, templet: function(d){return layui.util.toDateString(d.create_at*1000);}},
{title: '操作', width: 160, templet: function(d){
if(d.status!=0) return '-';
return '<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="settle">打款</a>'
+ '<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="reject">驳回</a>';
}}
]]
});
table.on('tool(dataTable)', function(obj){
var d = obj.data;
if(obj.event==='settle'){
layer.confirm('确认已向开发者打款?', function(){
$.post('{:url("revenue/settle")}', {id:d.id}, function(r){layer.msg(r.message||r.msg);if(r.code===0)table.reload('dataTable');});
});
} else if(obj.event==='reject'){
layer.prompt({title:'驳回原因'}, function(val, index){
$.post('{:url("revenue/reject")}', {id:d.id, reason:val}, function(r){layer.msg(r.message||r.msg);if(r.code===0)table.reload('dataTable');layer.close(index);});
});
}
});
});
</script>
@@ -0,0 +1,157 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>开发者控制台 - YwxApp 应用市场</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/assets/layui/css/layui.css" rel="stylesheet">
<style>
body { background: #f2f3f5; color: #333; }
.dev-box { max-width: 720px; margin: 40px auto; background: #fff; padding: 28px 32px; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,.08); }
.dev-box h2 { margin-bottom: 4px; }
.dev-tip { color: #999; font-size: 13px; margin-bottom: 18px; }
.balance-row { display: flex; gap: 16px; margin: 18px 0; }
.balance-card { flex: 1; background: #f7f9fc; border-radius: 8px; padding: 16px; text-align: center; }
.balance-card .label { color: #888; font-size: 13px; }
.balance-card .val { font-size: 24px; font-weight: 700; margin-top: 6px; }
.balance-card .val.ok { color: #18a058; }
.balance-card .val.free { color: #d48806; }
.rev-table { margin-top: 10px; }
.hidden { display: none; }
</style>
</head>
<body>
<div class="dev-box">
<h2>开发者控制台</h2>
<p class="dev-tip">使用开发者令牌查看收益并发起提现(令牌可在「我的令牌」页查询)</p>
<form class="layui-form" id="authForm">
<div class="layui-form-item">
<label class="layui-form-label">开发者令牌</label>
<div class="layui-input-block">
<input type="text" name="token" id="tokenInput" value="{$token|default=''}" required placeholder="粘贴你的开发者令牌" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="load">加载我的收益</button>
<a href="{:url('index')}" class="layui-btn layui-btn-primary">返回注册</a>
</div>
</div>
</form>
<div id="panel" class="hidden">
<div class="balance-row">
<div class="balance-card">
<div class="label">可提现余额</div>
<div class="val ok" id="balance">¥0.00</div>
</div>
<div class="balance-card">
<div class="label">冻结中(提现待打款)</div>
<div class="val free" id="frozen">¥0.00</div>
</div>
</div>
<fieldset class="layui-elem-field layui-field-title" style="margin-top:18px;"><legend>发起提现</legend></fieldset>
<form class="layui-form" id="withdrawForm">
<input type="hidden" name="token" id="wToken">
<div class="layui-form-item">
<label class="layui-form-label">提现金额</label>
<div class="layui-input-inline">
<input type="number" name="amount" step="0.01" min="0.01" required placeholder="0.00" class="layui-input">
</div>
<div class="layui-form-mid layui-word-aux">不可超过可提现余额</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">提现渠道</label>
<div class="layui-input-inline">
<select name="channel">
<option value="alipay">支付宝</option>
<option value="wechat">微信</option>
<option value="bank">银行卡</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">收款账号</label>
<div class="layui-input-block">
<input type="text" name="account" required placeholder="支付宝/微信账号或银行卡号" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="doWithdraw">提交提现申请</button>
</div>
</div>
</form>
<fieldset class="layui-elem-field layui-field-title" style="margin-top:24px;"><legend>近期收益</legend></fieldset>
<table class="layui-table rev-table">
<thead>
<tr><th>订单号</th><th>销售额</th><th>抽成</th><th>应得</th><th>状态</th><th>时间</th></tr>
</thead>
<tbody id="revBody">
<tr><td colspan="6" style="text-align:center;color:#999;">暂无收益记录</td></tr>
</tbody>
</table>
</div>
</div>
<script src="/assets/layui/layui.js"></script>
<script>
layui.use(['form', 'jquery', 'layer', 'http'], function () {
var form = layui.form, $ = layui.jquery, layer = layui.layer, http = layui.http;
form.on('submit(load)', function (data) {
http.post('{:url("/appmall/developer/earnings")}', data.field).then(function (r) {
if (r.code !== 0) { layer.msg(r.message || '加载失败', { icon: 2 }); return; }
var d = r.data || {};
$('#balance').text('¥' + parseFloat(d.balance || 0).toFixed(2));
$('#frozen').text('¥' + parseFloat(d.frozen_balance || 0).toFixed(2));
$('#wToken').val(data.field.token);
renderRevenues(d.revenues || []);
$('#panel').removeClass('hidden');
}).catch(function () { layer.msg('请求失败', { icon: 2 }); });
return false;
});
form.on('submit(doWithdraw)', function (data) {
var amount = parseFloat(data.field.amount);
if (!(amount > 0)) { layer.msg('请输入正确的提现金额', { icon: 2 }); return false; }
var avail = parseFloat($('#balance').text().replace(/[^\d.]/g, '')) || 0;
if (amount > avail) { layer.msg('提现金额超过可提现余额', { icon: 2 }); return false; }
http.post('{:url("/appmall/developer/withdraw")}', data.field).then(function (r) {
layer.msg(r.message || '提交完成', { icon: r.code === 0 ? 1 : 2 });
if (r.code === 0) { $('#withdrawForm')[0].reset(); form.render(); }
}).catch(function () { layer.msg('请求失败', { icon: 2 }); });
return false;
});
function renderRevenues(list) {
var body = $('#revBody');
if (!list.length) {
body.html('<tr><td colspan="6" style="text-align:center;color:#999;">暂无收益记录</td></tr>');
return;
}
var html = '';
list.forEach(function (it) {
var st = it.status == 1 ? '<span style="color:#18a058;">已结算</span>' : '<span style="color:#d48806;">待结算</span>';
html += '<tr>'
+ '<td>' + (it.order_id || '-') + '</td>'
+ '<td>¥' + parseFloat(it.amount || 0).toFixed(2) + '</td>'
+ '<td>¥' + parseFloat(it.commission || 0).toFixed(2) + '</td>'
+ '<td>¥' + parseFloat(it.income || 0).toFixed(2) + '</td>'
+ '<td>' + st + '</td>'
+ '<td>' + (it.create_at ? new Date(it.create_at * 1000).toLocaleString() : '-') + '</td>'
+ '</tr>';
});
body.html(html);
}
// 若 URL 携带 token,自动加载
var tok = $('#tokenInput').val();
if (tok) { $('#authForm').find('button[lay-filter="load"]').click(); }
});
</script>
</body>
</html>
@@ -0,0 +1,137 @@
<!--
* @Author: YwxApp <ywx@ywxapp.cn>
* @Description: 主框架发布/下载展示页(公开,仅中心站)
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>主框架下载 - {$market_name}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/assets/layui/css/layui.css" rel="stylesheet">
<style>
:root {
--brand: #4f46e5; --brand-2: #7c3aed; --ink: #1f2329;
--muted: #8a8f99; --line: #eceef2; --bg: #f5f6fa;
}
* { box-sizing: border-box; }
body {
margin: 0; background: var(--bg); color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
-webkit-font-smoothing: antialiased;
}
.brandbar {
height: 56px; display: flex; align-items: center; gap: 10px;
padding: 0 24px; background: #fff; border-bottom: 1px solid var(--line);
position: sticky; top: 0; z-index: 20;
}
.brandbar .logo {
width: 30px; height: 30px; border-radius: 8px;
background: linear-gradient(135deg, var(--brand), var(--brand-2));
display: grid; place-items: center; color: #fff; font-weight: 700;
}
.brandbar .name { font-weight: 600; font-size: 16px; }
.brandbar .nav { margin-left: auto; display: flex; gap: 22px; font-size: 14px; }
.brandbar .nav a { color: var(--muted); text-decoration: none; transition: color .2s; }
.brandbar .nav a:hover, .brandbar .nav a.on { color: var(--brand); }
.hero {
padding: 54px 24px 46px; text-align: center; color: #fff;
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 55%, #9333ea 100%);
position: relative; overflow: hidden;
}
.hero::after {
content: ""; position: absolute; inset: 0;
background:
radial-gradient(420px 220px at 12% 0%, rgba(255,255,255,.18), transparent 60%),
radial-gradient(380px 200px at 88% 100%, rgba(255,255,255,.12), transparent 60%);
}
.hero h1 { margin: 0 0 10px; font-size: 32px; position: relative; }
.hero p { margin: 0; opacity: .92; font-size: 15px; position: relative; }
.wrap { max-width: 960px; margin: 36px auto 60px; padding: 0 24px; }
.ver-card {
background: #fff; border: 1px solid var(--line); border-radius: 16px;
padding: 22px 26px; margin-bottom: 18px;
transition: transform .22s, box-shadow .22s, border-color .22s;
}
.ver-card:hover { transform: translateY(-4px); box-shadow: 0 16px 36px rgba(79,70,229,.14); border-color: transparent; }
.ver-top { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; }
.ver-title { font-size: 20px; font-weight: 600; }
.ver-title .v { color: #16a34a; }
.ver-date { color: var(--muted); font-size: 13px; }
.ver-sub { color: #6b7280; font-size: 14px; margin: 10px 0 14px; }
.ver-sub b { color: var(--ink); }
.ver-log {
background: #f7f8fa; border: 1px solid var(--line); border-radius: 10px;
padding: 12px 14px; font-size: 13px; color: #555; white-space: pre-wrap;
margin-bottom: 16px; max-height: 160px; overflow: auto;
}
.dl-btns { display: flex; gap: 10px; flex-wrap: wrap; }
.dl-btns a {
text-decoration: none; font-size: 13px; color: #fff; padding: 9px 16px; border-radius: 9px;
background: linear-gradient(135deg, var(--brand), var(--brand-2)); transition: opacity .2s, transform .1s;
}
.dl-btns a:hover { opacity: .9; }
.dl-btns a:active { transform: scale(.97); }
.dl-btns a.install { background: linear-gradient(135deg, #722ed1, #9254de); }
.empty { text-align: center; color: var(--muted); padding: 60px 0; }
footer { text-align: center; color: var(--muted); font-size: 12px; padding: 26px; }
</style>
</head>
<body>
<div class="brandbar">
<div class="logo">Y</div>
<div class="name">{$market_name}</div>
<div class="nav">
<a href="/appmall/store">应用市场</a>
<a href="/appmall/framework" class="on">主框架</a>
</div>
</div>
<div class="hero">
<h1>YwxApp 主框架下载</h1>
<p>完整安装包用于全新部署,整包 / 补丁用于已安装站点的在线升级。</p>
</div>
<div class="wrap">
{if empty($list)}
<div class="empty">暂未发布任何框架版本。</div>
{else}
{foreach $list as $item}
<div class="ver-card">
<div class="ver-top">
<div class="ver-title">{$item.title|default='主框架'} <span class="v">v{$item.version}</span></div>
<div class="ver-date">发布于 {$item.create_at_fmt} · 下载 {$item.download_count} 次</div>
</div>
{if $item.patch_from}
<div class="ver-sub">增量补丁适用基础版本:<b>{$item.patch_from}</b></div>
{/if}
{if $item.changelog}
<div class="ver-log">{$item.changelog}</div>
{/if}
<div class="dl-btns">
{if $item.has_full}
<a href="/appmall/api/framework/download?version={$item.version}&type=full">
<i class="layui-icon layui-icon-download-circle"></i> 下载整包(升级)
</a>
{/if}
{if $item.has_patch}
<a href="/appmall/api/framework/download?version={$item.version}&type=patch">
<i class="layui-icon layui-icon-download-circle"></i> 下载补丁(升级)
</a>
{/if}
{if $item.has_install}
<a class="install" href="/appmall/api/framework/download?version={$item.version}&type=install">
<i class="layui-icon layui-icon-download-circle"></i> 下载完整安装包(全新部署)
</a>
{/if}
</div>
</div>
{/foreach}
{/if}
</div>
<footer>© {:date('Y')} {$market_name} · 由 YwxApp 框架强力驱动</footer>
</body>
</html>
@@ -0,0 +1,257 @@
<!--
* @Author: YwxApp <ywx@ywxapp.cn>
* @Description: 插件详情页(公开,仅中心站)· 参考 FastAdmin 插件市场详情版式
* 布局:Hero 标题区 → 左主内容(功能介绍/预览截图/更新日志) + 右 sticky 信息栏(价格/目录)
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>{$addon.title} - 应用市场</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/assets/layui/css/layui.css" rel="stylesheet">
<style>
:root {
--brand: #4f46e5; --brand-2: #7c3aed; --ink: #1f2329;
--muted: #8a8f99; --line: #eceef2; --bg: #f5f6fa;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
-webkit-font-smoothing: antialiased; }
a { text-decoration: none; }
.brandbar { height: 56px; display: flex; align-items: center; gap: 10px;
padding: 0 24px; background: #fff; border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 30; }
.brandbar .logo { width: 30px; height: 30px; border-radius: 8px;
background: linear-gradient(135deg, var(--brand), var(--brand-2)); display: grid; place-items: center; color: #fff; font-weight: 700; }
.brandbar .name { font-weight: 600; font-size: 16px; }
.brandbar .nav { margin-left: auto; display: flex; gap: 22px; font-size: 14px; }
.brandbar .nav a { color: var(--muted); transition: color .2s; }
.brandbar .nav a:hover, .brandbar .nav a.on { color: var(--brand); }
.crumb { max-width: 1120px; margin: 16px auto 0; padding: 0 24px; font-size: 13px; color: var(--muted); }
.crumb a { color: var(--muted); }
.crumb a:hover { color: var(--brand); }
/* Hero */
.hero { max-width: 1120px; margin: 14px auto 0; padding: 30px 36px; background: #fff; border: 1px solid var(--line); border-radius: 18px;
display: flex; gap: 24px; align-items: center; position: relative; overflow: hidden; }
.hero::before { content: ""; position: absolute; right: -60px; top: -60px; width: 240px; height: 240px; border-radius: 50%;
background: radial-gradient(circle, rgba(124,58,237,.10), transparent 70%); }
.hero .ava { width: 88px; height: 88px; border-radius: 20px; flex: none; overflow: hidden; background: #f0f1f5;
display: grid; place-items: center; color: var(--brand); font-size: 34px; font-weight: 700; box-shadow: 0 8px 22px rgba(79,70,229,.18); }
.hero .ava img { width: 100%; height: 100%; object-fit: cover; }
.hero .h-main { min-width: 0; flex: 1; }
.hero .h-title { font-size: 26px; font-weight: 700; display: flex; align-items: center; gap: 12px; }
.hero .h-title .ver { font-size: 13px; color: #16a34a; background: rgba(22,163,74,.1); border-radius: 6px; padding: 3px 9px; font-weight: 500; }
.hero .h-desc { color: #5b606b; font-size: 14px; margin-top: 8px; line-height: 1.7; }
.hero .h-actions { display: flex; gap: 12px; margin-top: 16px; flex-wrap: wrap; }
.btn-primary { display: inline-flex; align-items: center; gap: 6px; color: #fff; font-size: 14px; font-weight: 600;
padding: 11px 22px; border-radius: 11px; background: linear-gradient(135deg, var(--brand), var(--brand-2)); transition: .2s, transform .1s; }
.btn-primary:hover { opacity: .92; }
.btn-primary:active { transform: scale(.98); }
.btn-ghost { display: inline-flex; align-items: center; gap: 6px; color: var(--brand); font-size: 14px; font-weight: 600;
padding: 11px 22px; border-radius: 11px; background: #fff; border: 1px solid var(--brand); transition: .2s; }
.btn-ghost:hover { background: rgba(79,70,229,.06); }
/* 主体双栏 */
.main { max-width: 1120px; margin: 24px auto 60px; padding: 0 24px; display: grid; gap: 24px;
grid-template-columns: 1fr 320px; align-items: start; }
.col-right { position: sticky; top: 76px; display: flex; flex-direction: column; gap: 18px; }
.panel { background: #fff; border: 1px solid var(--line); border-radius: 16px; padding: 24px; }
.panel + .panel { margin-top: 24px; }
.panel h3 { margin: 0 0 16px; font-size: 17px; display: flex; align-items: center; gap: 9px; }
.panel h3::before { content: ""; width: 4px; height: 17px; border-radius: 2px; background: linear-gradient(var(--brand), var(--brand-2)); }
.prose { color: #4b515c; font-size: 14px; line-height: 1.9; white-space: pre-wrap; }
/* 截图 */
.cover { width: 100%; aspect-ratio: 16/9; border-radius: 14px; overflow: hidden; background: #f0f1f5;
display: grid; place-items: center; color: var(--brand); font-size: 36px; font-weight: 700; }
.cover img { width: 100%; height: 100%; object-fit: cover; }
.thumbs { display: flex; gap: 10px; margin-top: 12px; flex-wrap: wrap; }
.thumbs img { width: 104px; height: 64px; object-fit: cover; border-radius: 9px; border: 2px solid transparent; cursor: pointer; transition: .18s; }
.thumbs img:hover, .thumbs img.on { border-color: var(--brand); }
/* 更新日志时间线 */
.timeline { position: relative; padding-left: 22px; }
.timeline::before { content: ""; position: absolute; left: 5px; top: 6px; bottom: 6px; width: 2px; background: var(--line); }
.tl-item { position: relative; padding-bottom: 22px; }
.tl-item:last-child { padding-bottom: 0; }
.tl-item::before { content: ""; position: absolute; left: -21px; top: 5px; width: 12px; height: 12px; border-radius: 50%;
background: #fff; border: 3px solid var(--brand); }
.tl-item.first::before { background: var(--brand); box-shadow: 0 0 0 4px rgba(79,70,229,.18); }
.tl-head { display: flex; align-items: center; gap: 10px; }
.tl-ver { font-weight: 700; font-size: 14px; }
.tl-badge { font-size: 11px; color: #16a34a; background: rgba(22,163,74,.1); border-radius: 6px; padding: 2px 8px; }
.tl-date { color: var(--muted); font-size: 12px; margin-left: auto; }
.tl-note { color: #5b606b; font-size: 13px; line-height: 1.7; margin-top: 6px; white-space: pre-wrap; }
/* 右栏:价格卡 + 信息 + 目录 */
.price-card { background: linear-gradient(135deg, rgba(79,70,229,.07), rgba(124,58,237,.07));
border: 1px solid rgba(124,58,237,.18); border-radius: 14px; padding: 18px 20px; }
.price-card .price { font-size: 30px; font-weight: 800; color: var(--brand); }
.price-card .price.free { color: #16a34a; }
.price-card .sub { color: var(--muted); font-size: 12px; margin-top: 2px; }
.buy-btn { display: block; text-align: center; color: #fff; font-weight: 600; font-size: 15px; padding: 13px;
border-radius: 11px; background: linear-gradient(135deg, var(--brand), var(--brand-2)); margin-top: 14px; transition: .2s, transform .1s; }
.buy-btn:hover { opacity: .92; }
.buy-btn:active { transform: scale(.98); }
.buy-btn.ghost { background: #fff; color: var(--brand); border: 1px solid var(--brand); margin-top: 10px; }
.kv { font-size: 13px; }
.kv .row { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px dashed var(--line); }
.kv .row:last-child { border-bottom: 0; }
.kv .k { color: var(--muted); }
.kv .v { color: var(--ink); text-align: right; }
.stars { color: #f5a623; letter-spacing: 1px; }
.stars .off { color: #e2e4ea; }
.tags { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 4px; }
.tags span { font-size: 12px; color: var(--brand); background: rgba(79,70,229,.1); border-radius: 6px; padding: 4px 10px; }
.toc { font-size: 13px; }
.toc .t { color: var(--muted); font-size: 12px; margin-bottom: 8px; }
.toc a { display: block; color: #5b606b; padding: 7px 0; border-left: 2px solid transparent; padding-left: 12px; transition: .18s; }
.toc a:hover { color: var(--brand); border-left-color: var(--brand); }
footer { text-align: center; color: var(--muted); font-size: 12px; padding: 26px; }
@media (max-width: 900px) {
.main { grid-template-columns: 1fr; }
.col-right { position: static; }
}
</style>
</head>
<body>
<div class="brandbar">
<div class="logo">Y</div>
<div class="name">{$market_name}</div>
<div class="nav">
<a href="/appmall/store" class="on">应用市场</a>
<a href="/appmall/framework">主框架</a>
</div>
</div>
<div class="crumb">
<a href="/appmall/store">应用市场</a> / <span>{$addon.title}</span>
</div>
<!-- Hero -->
<div class="hero">
<div class="ava">
{if $addon.logo}<img src="{$addon.logo}" alt="{$addon.title}">{else}{$addon.title|substr=0,1}{/if}
</div>
<div class="h-main">
<div class="h-title">{$addon.title}<span class="ver">v{$addon.version}</span></div>
<div class="h-desc">{if $addon.author}by {$addon.author} · {/if}{$addon.category|default='未分类'}</div>
<div class="h-actions">
<a class="btn-primary" href="#"><i class="layui-icon layui-icon-play"></i> 立即体验</a>
<a class="btn-ghost" href="#"><i class="layui-icon layui-icon-cart"></i> 立即购买</a>
</div>
</div>
</div>
<div class="main">
<!-- 左主内容 -->
<div class="col-left">
<div class="panel" id="intro">
<h3>功能介绍</h3>
<div class="prose">{$addon.intro|default='暂无介绍'}</div>
</div>
{if $addon.screenshots}
<div class="panel" id="shots">
<h3>预览截图</h3>
<div class="cover" id="cover">
<img src="{$addon.screenshots.0}" alt="{$addon.title}">
</div>
{if count($addon.screenshots) > 1}
<div class="thumbs" id="thumbs">
{foreach $addon.screenshots as $s}
<img src="{$s}" class="{if $s == $addon.screenshots.0}on{/if}" onclick="swap('{$s}', this)">
{/foreach}
</div>
{/if}
</div>
{/if}
<div class="panel" id="log">
<h3>更新日志</h3>
{if empty($addon.versions)}
<div class="prose" style="color:var(--muted)">暂无版本记录</div>
{else}
<div class="timeline">
{foreach $addon.versions as $k=>$v}
<div class="tl-item {if $k == 0}first{/if}">
<div class="tl-head">
<span class="tl-ver">v{$v.version}</span>
{if $k == 0}<span class="tl-badge">最新版本</span>{/if}
<span class="tl-date">{$v.update_fmt}</span>
</div>
<div class="tl-note">本版本定价:{$v.price_text}</div>
</div>
{/foreach}
</div>
{/if}
</div>
</div>
<!-- 右 sticky 信息栏 -->
<div class="col-right">
<div class="panel">
<div class="kv">
<div class="row"><span class="k">当前版本</span><span class="v">v{$addon.version}</span></div>
<div class="row"><span class="k">更新时间</span><span class="v">{$addon.update_fmt}</span></div>
<div class="row"><span class="k">下载量</span><span class="v">{$addon.downloads}</span></div>
<div class="row"><span class="k">评分</span>
<span class="v">
<span class="stars">
{for start="1" end="6" name="i"}
{if $addon.rating >= $i}<i class="layui-icon layui-icon-rate-solid"></i>{else/}<i class="layui-icon layui-icon-rate off"></i>{/if}
{/for}
</span>
{$addon.rating}
</span>
</div>
<div class="row"><span class="k">开发者</span><span class="v">{$addon.author|default='官方'}</span></div>
<div class="row"><span class="k">分类</span><span class="v">{$addon.category|default='未分类'}</span></div>
</div>
{if $addon.tags}
<div class="tags">
{foreach explode(',', $addon.tags) as $t}
<span>{$t}</span>
{/foreach}
</div>
{/if}
</div>
<div class="panel">
<div class="toc">
<div class="t">本页目录</div>
<a href="#intro">功能介绍</a>
{if $addon.screenshots}<a href="#shots">预览截图</a>{/if}
<a href="#log">更新日志</a>
</div>
</div>
</div>
</div>
<footer>© {:date('Y')} {$market_name} · 由 YwxApp 框架强力驱动</footer>
<script>
function swap(src, el) {
document.querySelector('#cover img').src = src;
document.querySelectorAll('#thumbs img').forEach(function (im) { im.classList.remove('on'); });
el.classList.add('on');
}
// 锚点平滑滚动 + 目录高亮
document.querySelectorAll('.toc a').forEach(function (a) {
a.addEventListener('click', function (e) {
var id = this.getAttribute('href');
var target = document.querySelector(id);
if (target) { e.preventDefault(); target.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
});
});
</script>
</body>
</html>
@@ -0,0 +1,230 @@
<!--
* @Author: YwxApp <ywx@ywxapp.cn>
* @Description: 应用商店前台(公开,仅中心站)
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>应用市场 - {$market_name}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/assets/layui/css/layui.css" rel="stylesheet">
<style>
:root {
--brand: #4f46e5;
--brand-2: #7c3aed;
--ink: #1f2329;
--muted: #8a8f99;
--line: #eceef2;
--bg: #f5f6fa;
}
* { box-sizing: border-box; }
body {
margin: 0; background: var(--bg); color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
-webkit-font-smoothing: antialiased;
}
.brandbar {
height: 56px; display: flex; align-items: center; gap: 10px;
padding: 0 24px; background: #fff; border-bottom: 1px solid var(--line);
position: sticky; top: 0; z-index: 20;
}
.brandbar .logo {
width: 30px; height: 30px; border-radius: 8px;
background: linear-gradient(135deg, var(--brand), var(--brand-2));
display: grid; place-items: center; color: #fff; font-weight: 700;
}
.brandbar .name { font-weight: 600; font-size: 16px; }
.brandbar .nav { margin-left: auto; display: flex; gap: 22px; font-size: 14px; }
.brandbar .nav a { color: var(--muted); text-decoration: none; transition: color .2s; }
.brandbar .nav a:hover, .brandbar .nav a.on { color: var(--brand); }
.hero {
padding: 54px 24px 46px; text-align: center; color: #fff;
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 55%, #9333ea 100%);
position: relative; overflow: hidden;
}
.hero::after {
content: ""; position: absolute; inset: 0;
background:
radial-gradient(420px 220px at 12% 0%, rgba(255,255,255,.18), transparent 60%),
radial-gradient(380px 200px at 88% 100%, rgba(255,255,255,.12), transparent 60%);
}
.hero h1 { margin: 0 0 10px; font-size: 32px; letter-spacing: .5px; position: relative; }
.hero p { margin: 0; opacity: .92; font-size: 15px; position: relative; }
.toolbar {
max-width: 1180px; margin: -26px auto 0; padding: 0 24px; position: relative; z-index: 5;
}
.toolbar .inner {
background: #fff; border-radius: 14px; box-shadow: 0 8px 30px rgba(31,35,41,.08);
padding: 14px 18px; display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
}
.search {
flex: 1; min-width: 220px; display: flex; align-items: center; gap: 8px;
background: #f5f6fa; border-radius: 10px; padding: 9px 14px; color: var(--muted);
}
.search input {
border: 0; background: transparent; outline: none; flex: 1; font-size: 14px; color: var(--ink);
}
.cats { display: flex; gap: 8px; flex-wrap: wrap; }
.cat {
border: 1px solid var(--line); background: #fff; color: var(--muted);
border-radius: 999px; padding: 6px 14px; font-size: 13px; cursor: pointer; transition: .18s;
}
.cat:hover { border-color: var(--brand); color: var(--brand); }
.cat.on { background: var(--brand); border-color: var(--brand); color: #fff; }
.grid {
max-width: 1180px; margin: 28px auto 60px; padding: 0 24px;
display: grid; gap: 20px;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}
.card {
background: #fff; border-radius: 16px; border: 1px solid var(--line);
padding: 20px; display: flex; flex-direction: column; gap: 12px;
transition: transform .22s ease, box-shadow .22s ease, border-color .22s;
}
.card:hover {
transform: translateY(-6px);
box-shadow: 0 18px 40px rgba(79,70,229,.16);
border-color: transparent;
}
.card .head { display: flex; align-items: center; gap: 12px; }
.card .ava {
width: 52px; height: 52px; border-radius: 14px; flex: none;
background: #f0f1f5; object-fit: cover; display: grid; place-items: center;
font-weight: 700; color: var(--brand); font-size: 20px; overflow: hidden;
}
.card .ava img { width: 100%; height: 100%; object-fit: cover; }
.card .titles { min-width: 0; }
.card .titles .t { font-weight: 600; font-size: 16px; line-height: 1.3; }
.card .titles .a { color: var(--muted); font-size: 12px; margin-top: 2px; }
.card .desc {
color: #5b606b; font-size: 13px; line-height: 1.6; min-height: 42px;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.card .meta { display: flex; align-items: center; gap: 14px; font-size: 12px; color: var(--muted); }
.stars { color: #f5a623; letter-spacing: 1px; }
.stars .off { color: #e2e4ea; }
.dl { display: flex; align-items: center; gap: 5px; }
.card .foot { display: flex; align-items: center; justify-content: space-between; margin-top: auto; }
.price {
font-weight: 700; font-size: 16px; color: var(--brand);
}
.price.free { color: #16a34a; }
.tag {
font-size: 11px; color: var(--brand); background: rgba(79,70,229,.1);
border-radius: 6px; padding: 3px 8px;
}
.card .btn {
text-decoration: none; font-size: 13px; color: #fff;
background: linear-gradient(135deg, var(--brand), var(--brand-2));
padding: 8px 16px; border-radius: 9px; transition: opacity .2s, transform .1s;
}
.card .btn:hover { opacity: .9; }
.card .btn:active { transform: scale(.97); }
.empty { grid-column: 1 / -1; text-align: center; padding: 70px 0; color: var(--muted); }
.empty .ic { font-size: 48px; opacity: .5; }
.empty p { margin-top: 12px; }
footer { text-align: center; color: var(--muted); font-size: 12px; padding: 26px; }
</style>
</head>
<body>
<div class="brandbar">
<div class="logo">Y</div>
<div class="name">{$market_name}</div>
<div class="nav">
<a href="/appmall/store" class="on">应用市场</a>
<a href="/appmall/framework">主框架</a>
</div>
</div>
<div class="hero">
<h1>发现优质扩展,一键点亮你的站点</h1>
<p>插件、模板、工具,覆盖建站全场景 · 由 {$market_name} 官方精选</p>
</div>
<div class="toolbar">
<div class="inner">
<div class="search">
<i class="layui-icon layui-icon-search"></i>
<input type="text" placeholder="搜索插件名称、作者或功能…" oninput="filterCards(this.value)">
</div>
<div class="cats" id="cats">
<span class="cat on" data-cat="">全部</span>
{foreach $categories as $c}
<span class="cat" data-cat="{$c}">{$c}</span>
{/foreach}
</div>
</div>
</div>
<div class="grid" id="grid">
{if empty($list)}
<div class="empty">
<div class="ic"><i class="layui-icon layui-icon-template-1"></i></div>
<p>市场暂无插件,敬请期待。</p>
</div>
{else}
{foreach $list as $p}
<div class="card" data-name="{$p.name|lower}" data-author="{$p.author|lower}" data-cat="{$p.category}">
<div class="head">
<div class="ava">
{if $p.logo}
<img src="{$p.logo}" alt="{$p.title}">
{else}
{$p.title|substr=0,1}
{/if}
</div>
<div class="titles">
<div class="t">{$p.title}{if $p.version} <span style="font-weight:400;color:#aab;font-size:12px;">v{$p.version}</span>{/if}</div>
<div class="a">by {$p.author|default='官方'}</div>
</div>
</div>
<div class="desc">{$p.desc|default='暂无描述'}</div>
<div class="meta">
<span class="stars" title="评分 {$p.rating}">
{for start="1" end="6" name="i"}
{if $p.rating >= $i}<i class="layui-icon layui-icon-rate-solid"></i>{else/}<i class="layui-icon layui-icon-rate off"></i>{/if}
{/for}
</span>
<span class="dl"><i class="layui-icon layui-icon-download-circle"></i>{$p.downloads} 下载</span>
</div>
<div class="foot">
<div>
<span class="price {if $p.price==0}free{/if}">{$p.price_text}</span>
{if $p.category}<span class="tag">{$p.category}</span>{/if}
</div>
<a class="btn" href="/appmall/store/detail/{$p.name}">查看详情</a>
</div>
</div>
{/foreach}
{/if}
</div>
<footer>© {:date('Y')} {$market_name} · 由 YwxApp 框架强力驱动</footer>
<script>
function filterCards(kw) {
kw = (kw || '').toLowerCase().trim();
document.querySelectorAll('#grid .card').forEach(function (el) {
var hit = !kw || el.dataset.name.indexOf(kw) > -1 || el.dataset.author.indexOf(kw) > -1;
el.style.display = hit ? '' : 'none';
});
}
document.getElementById('cats').addEventListener('click', function (e) {
var cat = e.target.closest('.cat');
if (!cat) return;
document.querySelectorAll('#cats .cat').forEach(function (c) { c.classList.remove('on'); });
cat.classList.add('on');
var v = cat.dataset.cat;
document.querySelectorAll('#grid .card').forEach(function (el) {
el.style.display = (!v || el.dataset.cat === v) ? '' : 'none';
});
});
</script>
</body>
</html>