文件还在测试中

This commit is contained in:
2026-08-08 15:53:53 +08:00
parent 5f1e7adb64
commit 8101177cb5
241 changed files with 18074 additions and 22 deletions
+30
View File
@@ -0,0 +1,30 @@
<?php
$v = function ($k, $d = '') use ($b) { return $b ? ($b[$k] ?? $d) : $d; };
$isEdit = !empty($b);
?>
<div class="page-head">
<div><h1><?php echo $isEdit ? '编辑轮播' : '新增轮播'; ?></h1><div class="desc">背景可填 CSS 渐变或图片地址</div></div>
<a class="btn-ghost" href="<?php echo site_url('admin/banners'); ?>">← 返回</a>
</div>
<div class="admin-card">
<form method="post" action="<?php echo site_url($isEdit ? 'admin/banners/update/' . $b['id'] : 'admin/banners/store'); ?>" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<div class="form-grid">
<div class="field"><label>标题 *</label><input name="title" value="<?php echo e($v('title')); ?>" required></div>
<div class="field"><label>副标题</label><input name="subtitle" value="<?php echo e($v('subtitle')); ?>"></div>
</div>
<div class="field"><label>链接地址(如 /products</label><input name="link" value="<?php echo e($v('link')); ?>"></div>
<div class="form-grid">
<div class="field"><label>背景图(上传,可选)</label><input type="file" name="image" accept="image/*"></div>
<div class="field"><label>或背景(图片地址 / CSS 渐变)</label><input name="image" value="<?php echo e($v('image')); ?>" placeholder="linear-gradient(135deg,#0ea5e9,#14b8a6)"></div>
</div>
<div class="field"><label>排序</label><input name="sort_order" type="number" value="<?php echo e($v('sort_order', 0)); ?>"></div>
<div class="field" style="display:flex;align-items:center;gap:8px">
<input type="checkbox" name="status" value="1" <?php echo $v('status', 1) ? 'checked' : ''; ?> id="st"> <label for="st" style="margin:0">显示</label>
</div>
<div class="form-actions">
<button class="btn-primary" type="submit">保存</button>
<a class="btn-ghost" href="<?php echo site_url('admin/banners'); ?>">取消</a>
</div>
</form>
</div>
+23
View File
@@ -0,0 +1,23 @@
<div class="page-head">
<div><h1>轮播管理</h1><div class="desc">首页轮播图(背景支持渐变或图片)</div></div>
<a class="btn-primary" href="<?php echo site_url('admin/banners/create'); ?>">+ 新增轮播</a>
</div>
<div class="admin-card">
<table class="admin-table">
<tr><th>背景</th><th>标题</th><th>副标题</th><th>链接</th><th>操作</th></tr>
<?php foreach ($banners as $b): ?>
<tr>
<td><div class="thum" style="background:<?php echo e($b['image']); ?>">🖼</div></td>
<td><b><?php echo e($b['title']); ?></b></td>
<td class="muted"><?php echo e(mb_substr($b['subtitle'] ?? '', 0, 20)); ?></td>
<td class="muted"><?php echo e($b['link']); ?></td>
<td>
<div class="row-actions">
<a class="btn-soft btn-sm" href="<?php echo site_url('admin/banners/edit/' . $b['id']); ?>">编辑</a>
<form class="del-form" method="post" action="<?php echo site_url('admin/banners/delete/' . $b['id']); ?>"><?php echo csrf_field(); ?><button class="btn-danger btn-sm" type="submit">删除</button></form>
</div>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
+32
View File
@@ -0,0 +1,32 @@
<div class="page-head">
<div><h1>客户案例管理</h1><div class="desc">客户案例展示,可像新闻一样持续新增</div></div>
<a class="btn-primary" href="<?php echo site_url('admin/cases/create'); ?>">+ 新增案例</a>
</div>
<div class="admin-card">
<table class="admin-table">
<tr><th>封面</th><th>案例标题</th><th>模式</th><th>客户</th><th>行业</th><th>日期</th><th>状态</th><th>操作</th></tr>
<?php foreach ($cases as $c): ?>
<?php $cm = empty($c['mode']) ? 'fixed' : $c['mode']; ?>
<tr>
<td><div class="thum" style="background:<?php echo gradient($c['id']); ?>">🤝</div></td>
<td><b><?php echo e($c['title']); ?></b></td>
<td><span class="mode-badge <?php echo $cm === 'builder' ? 'builder' : 'fixed'; ?>"><?php echo $cm === 'builder' ? '可视化' : '固定'; ?></span></td>
<td class="muted"><?php echo e($c['customer'] ?? ''); ?></td>
<td class="muted"><?php echo e($c['industry'] ?? ''); ?></td>
<td class="muted"><?php echo e(format_date($c['published_at'])); ?></td>
<td><?php echo ($c['status'] ?? 1) ? '<span class="tag-mini">展示中</span>' : '<span class="muted">隐藏</span>'; ?></td>
<td>
<div class="row-actions">
<a class="btn-soft btn-sm" href="<?php echo site_url('admin/cases/edit/' . $c['id']); ?>">编辑</a>
<form class="del-form" method="post" action="<?php echo site_url('admin/cases/delete/' . $c['id']); ?>"><?php echo csrf_field(); ?><button class="btn-danger btn-sm" type="submit">删除</button></form>
</div>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
<style>
.mode-badge{display:inline-block;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:600}
.mode-badge.fixed{background:#f1f5f9;color:#475569}
.mode-badge.builder{background:#ede9fe;color:#6d28d9}
</style>
+92
View File
@@ -0,0 +1,92 @@
<?php
/** @var array $c */
/** @var array $layout */
/** @var string $mode */
$v = function ($k, $d = '') use ($c) { return $c ? ($c[$k] ?? $d) : $d; };
$isEdit = !empty($c);
$switchUrl = $isEdit ? site_url('admin/cases/switchMode/' . (int)$c['id']) : '';
$initial = json_encode($layout ?: [], JSON_UNESCAPED_UNICODE);
$coverVal = $v('cover');
?>
<div class="page-head">
<div>
<h1><?php echo $isEdit ? '编辑客户案例' : '新增客户案例'; ?><span class="mode-badge mode-builder">可视化编辑</span></h1>
<div class="desc">可视化编辑:拖拽文字 / 图片 / 按钮自由排版,保存后前台按此布局整页展示。</div>
</div>
<?php if ($isEdit): ?>
<div class="mode-switch-sel">
<label for="modeSel">切换模式</label>
<select id="modeSel" class="mode-sel" onchange="location.href='<?php echo $switchUrl; ?>?mode='+this.value">
<option value="fixed" <?php echo $mode === 'fixed' ? 'selected' : ''; ?>>固定版面</option>
<option value="builder" <?php echo $mode === 'builder' ? 'selected' : ''; ?>>可视化编辑</option>
</select>
</div>
<?php else: ?>
<div class="mode-switch-sel">
<label for="modeSel">排版方式</label>
<select id="modeSel" class="mode-sel">
<option value="fixed" <?php echo $mode === 'fixed' ? 'selected' : ''; ?>>固定版面</option>
<option value="builder" <?php echo $mode === 'builder' ? 'selected' : ''; ?>>可视化编辑</option>
</select>
</div>
<?php endif; ?>
</div>
<div class="admin-card">
<form id="pbForm" method="post" action="<?php echo site_url($isEdit ? 'admin/cases/update/' . $c['id'] : 'admin/cases/store'); ?>" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<input type="hidden" name="mode" value="builder">
<input type="hidden" name="layout" id="pbLayout">
<div class="field"><label>案例标题 *</label><input name="title" value="<?php echo e($v('title')); ?>" required></div>
<div class="form-grid">
<div class="field"><label>客户名称</label><input name="customer" value="<?php echo e($v('customer')); ?>"></div>
<div class="field"><label>所属行业</label><input name="industry" value="<?php echo e($v('industry')); ?>"></div>
</div>
<div class="form-grid">
<div class="field"><label>服务时间</label><input type="date" name="published_at" value="<?php echo e($v('published_at', date('Y-m-d'))); ?>"></div>
<div class="field"><label>排序权重</label><input type="number" name="sort_order" value="<?php echo e($v('sort_order', 0)); ?>" placeholder="越大越靠前"></div>
</div>
<div class="field"><label>摘要</label><input name="summary" value="<?php echo e($v('summary')); ?>"></div>
<div class="field"><label>URL 标识(留空按序号生成)</label><input name="slug" value="<?php echo e($v('slug')); ?>" placeholder="留空则自动生成如 12"></div>
<div class="form-grid">
<div class="field"><label>封面图(上传,可选)</label><input type="file" name="cover" accept="image/*"></div>
<div class="field"><label>或填写图片地址/路径</label><input name="cover_url" value="<?php echo e($coverVal); ?>" placeholder="assets/uploads/xxx.jpg 或 http(s)://"></div>
</div>
<div class="field">
<label>可视化画布(拖拽排版案例详情)</label>
<?php echo \Core\View::buffer('admin/parts/builder', ['module' => 'case', 'layout' => $layout]); ?>
</div>
<div class="field" style="display:flex;align-items:center;gap:8px">
<input type="checkbox" name="status" value="1" <?php echo $v('status', 1) ? 'checked' : ''; ?> id="st"> <label for="st" style="margin:0">在前台展示</label>
</div>
<div class="form-actions">
<button class="btn-primary" type="submit">保存</button>
<a class="btn-ghost" href="<?php echo site_url('admin/cases'); ?>">取消</a>
</div>
</form>
</div>
<style>
.mode-switch-sel{display:inline-flex;align-items:center;gap:8px}
.mode-switch-sel label{font-size:13px;color:#64748b}
.mode-sel{border:1px solid #e2e8f0;border-radius:10px;padding:8px 12px;font-size:14px;background:#fff;color:#334155;cursor:pointer;min-width:140px}
.mode-sel:focus{border-color:#0ea5e9;box-shadow:0 0 0 3px rgba(14,165,233,.12)}
.mode-badge{display:inline-block;margin-left:10px;padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600;vertical-align:middle;line-height:1.7}
.mode-badge.mode-fixed{background:#e0f2fe;color:#0369a1}
.mode-badge.mode-builder{background:#ede9fe;color:#6d28d9}
</style>
<script>
window.__PB_INIT__ = <?php echo $initial; ?>;
window.__PB_MEDIA__ = '<?php echo site_url('admin/media'); ?>';
window.__PB_UPLOAD__ = '<?php echo site_url('admin/media/upload'); ?>';
window.__PB_CSRF__ = '<?php echo csrf_token(); ?>';
window.__PB_MODULE__ = 'case';
window.__PB_DELETE__ = '<?php echo site_url('admin/media/delete/'); ?>';
</script>
<script src="<?php echo asset('js/page-builder.js'); ?>"></script>
+167
View File
@@ -0,0 +1,167 @@
<?php
/** @var array $c */
/** @var string $mode */
$v = function ($k, $d = '') use ($c) { return $c ? ($c[$k] ?? $d) : $d; };
$isEdit = !empty($c);
$mode = $mode ?? 'fixed';
$switchUrl = $isEdit ? site_url('admin/cases/switchMode/' . (int)$c['id']) : '';
$content = $v('content');
$coverVal = $v('cover');
?>
<div class="page-head">
<div>
<h1><?php echo $isEdit ? '编辑客户案例' : '新增客户案例'; ?><span class="mode-badge mode-fixed">固定版面</span></h1>
<div class="desc">固定版面:填写案例信息 / 封面,详情使用富文本编辑器排版<?php echo $isEdit ? ';可切换为可视化自由排版。' : '。'; ?></div>
</div>
<?php if ($isEdit): ?>
<div class="mode-switch-sel">
<label for="modeSel">切换模式</label>
<select id="modeSel" class="mode-sel" onchange="location.href='<?php echo $switchUrl; ?>?mode='+this.value">
<option value="fixed" <?php echo $mode === 'fixed' ? 'selected' : ''; ?>>固定版面</option>
<option value="builder" <?php echo $mode === 'builder' ? 'selected' : ''; ?>>可视化编辑</option>
</select>
</div>
<?php else: ?>
<div class="mode-switch-sel">
<label for="modeSel">排版方式</label>
<select id="modeSel" class="mode-sel">
<option value="fixed" <?php echo $mode === 'fixed' ? 'selected' : ''; ?>>固定版面</option>
<option value="builder" <?php echo $mode === 'builder' ? 'selected' : ''; ?>>可视化编辑</option>
</select>
</div>
<?php endif; ?>
</div>
<div class="admin-card">
<form id="fxForm" method="post" action="<?php echo site_url($isEdit ? 'admin/cases/update/' . $c['id'] : 'admin/cases/store'); ?>" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<input type="hidden" name="mode" value="fixed">
<div class="field"><label>案例标题 *</label><input name="title" value="<?php echo e($v('title')); ?>" required></div>
<div class="form-grid">
<div class="field"><label>客户名称</label><input name="customer" value="<?php echo e($v('customer')); ?>"></div>
<div class="field"><label>所属行业</label><input name="industry" value="<?php echo e($v('industry')); ?>"></div>
</div>
<div class="form-grid">
<div class="field"><label>服务时间</label><input type="date" name="published_at" value="<?php echo e($v('published_at', date('Y-m-d'))); ?>"></div>
<div class="field"><label>排序权重</label><input type="number" name="sort_order" value="<?php echo e($v('sort_order', 0)); ?>" placeholder="越大越靠前"></div>
</div>
<div class="field"><label>摘要</label><input name="summary" value="<?php echo e($v('summary')); ?>"></div>
<div class="field"><label>URL 标识(留空按序号生成,短而稳定)</label><input name="slug" value="<?php echo e($v('slug')); ?>" placeholder="留空则自动生成如 12"></div>
<div class="field">
<label>封面图(上传,可选)</label>
<input type="file" name="cover" accept="image/*">
<?php if ($coverVal): ?>
<div class="img-prev"><img src="<?php echo e(site_url($coverVal)); ?>" alt=""><span class="img-prev-path"><?php echo e($coverVal); ?></span></div>
<p class="fx-hint">已上传封面;重新选择文件将替换,留空则保留。</p>
<?php endif; ?>
<input type="text" name="cover_url" value="<?php echo e($coverVal); ?>" placeholder="或填写图片地址 assets/uploads/xxx.jpg 或 http(s)://" style="margin-top:8px">
</div>
<div class="field fx-fixed-only">
<label>案例详情(富文本编辑器)</label>
<div class="fx-toolbar">
<button type="button" data-cmd="bold" title="加粗"><b>B</b></button>
<button type="button" data-cmd="italic" title="斜体"><i>I</i></button>
<button type="button" data-cmd="formatBlock" data-val="H2">H2</button>
<button type="button" data-cmd="formatBlock" data-val="H3">H3</button>
<button type="button" data-cmd="insertUnorderedList" title="无序列表">• 列表</button>
<button type="button" data-cmd="insertOrderedList" title="有序列表">1. 列表</button>
<button type="button" data-cmd="formatBlock" data-val="BLOCKQUOTE" title="引用">引用</button>
<button type="button" id="fxLink" title="插入链接">链接</button>
<button type="button" id="fxImg" title="插入图片">图片</button>
</div>
<div class="fx-editor" id="fxEditor" contenteditable="true"><?php echo $content; ?></div>
<textarea name="content" id="fxContent" hidden><?php echo e($content); ?></textarea>
<p class="fx-hint">直接排版正文即可;点击「图片」可从素材库选择或上传新图。保存后前台固定版式展示。</p>
</div>
<div class="field" style="display:flex;align-items:center;gap:8px">
<input type="checkbox" name="status" value="1" <?php echo $v('status', 1) ? 'checked' : ''; ?> id="st"> <label for="st" style="margin:0">在前台展示</label>
</div>
<div class="form-actions">
<button class="btn-primary" type="submit">保存</button>
<a class="btn-ghost" href="<?php echo site_url('admin/cases'); ?>">取消</a>
</div>
</form>
</div>
<!-- 图片选择弹层 -->
<div class="fx-modal" id="fxModal" hidden>
<div class="fx-modal-box">
<div class="fx-modal-head"><span>选择图片</span><button type="button" id="fxModalClose">×</button></div>
<div class="fx-modal-body">
<label class="fx-upload"> 上传图片<input type="file" id="fxUp" accept="image/*" hidden></label>
<div class="fx-lib" id="fxLib"></div>
</div>
</div>
</div>
<style>
.mode-switch-sel{display:inline-flex;align-items:center;gap:8px}
.mode-switch-sel label{font-size:13px;color:#64748b}
.mode-sel{border:1px solid #e2e8f0;border-radius:10px;padding:8px 12px;font-size:14px;background:#fff;color:#334155;cursor:pointer;min-width:140px}
.mode-sel:focus{border-color:#0ea5e9;box-shadow:0 0 0 3px rgba(14,165,233,.12)}
.mode-badge{display:inline-block;margin-left:10px;padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600;vertical-align:middle;line-height:1.7}
.mode-badge.mode-fixed{background:#e0f2fe;color:#0369a1}
.mode-badge.mode-builder{background:#ede9fe;color:#6d28d9}
.img-prev{display:flex;align-items:center;gap:10px;margin-top:10px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:8px 10px}
.img-prev img{width:72px;height:54px;object-fit:cover;border-radius:8px;display:block}
.img-prev-path{font-size:12px;color:#94a3b8;word-break:break-all}
.fx-toolbar{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}
.fx-toolbar button{width:auto;min-width:38px;height:34px;padding:0 10px;border:1px solid #e2e8f0;background:#fff;border-radius:8px;cursor:pointer;font-size:14px;color:#334155}
.fx-toolbar button:hover{border-color:#0ea5e9;color:#0ea5e9}
.fx-editor{min-height:320px;border:1px solid #e2e8f0;border-radius:10px;padding:16px 18px;font-size:16px;line-height:1.9;outline:none;background:#fff;overflow:auto}
.fx-editor:focus{border-color:#0ea5e9;box-shadow:0 0 0 3px rgba(14,165,233,.12)}
.fx-editor h2{font-size:24px;margin:.6em 0 .4em}
.fx-editor h3{font-size:20px;margin:.6em 0 .4em}
.fx-editor blockquote{margin:.6em 0;padding:8px 14px;border-left:4px solid #0ea5e9;color:#475569;background:#f8fafc}
.fx-editor img{max-width:100%;height:auto;border-radius:8px;display:block;margin:8px 0}
.fx-editor a{color:#0ea5e9}
.fx-hint{font-size:12px;color:#94a3b8;margin:8px 2px 0}
.fx-modal{position:fixed;inset:0;background:rgba(15,23,42,.45);display:flex;align-items:center;justify-content:center;z-index:80}
.fx-modal[hidden]{display:none}
.fx-modal-box{background:#fff;border-radius:14px;width:420px;max-width:92vw;overflow:hidden}
.fx-modal-head{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #e2e8f0;font-weight:700}
.fx-modal-head button{border:none;background:none;font-size:22px;cursor:pointer;color:#64748b;line-height:1}
.fx-modal-body{padding:16px;max-height:60vh;overflow:auto}
.fx-upload{display:block;text-align:center;background:#0ea5e9;color:#fff;border-radius:10px;padding:10px;cursor:pointer;font-weight:600;margin-bottom:12px}
.fx-lib{display:flex;flex-wrap:wrap;gap:8px}
.fx-lib img{width:72px;height:72px;object-fit:cover;border-radius:8px;cursor:pointer;border:2px solid transparent}
.fx-lib img:hover{border-color:#0ea5e9}
</style>
<script>
window.__PB_MEDIA__ = '<?php echo site_url('admin/media'); ?>';
window.__PB_UPLOAD__ = '<?php echo site_url('admin/media/upload'); ?>';
window.__PB_CSRF__ = '<?php echo csrf_token(); ?>';
</script>
<script src="<?php echo asset('js/fixed-editor.js'); ?>"></script>
<script>
// 新增案例时按「排版方式」选择器联动:选可视化编辑则隐藏正文编辑器,并提示保存后进入可视化编辑器
(function(){
var sel = document.getElementById('modeSel');
if(!sel) return;
var isCreate = <?php echo $isEdit ? 'false' : 'true'; ?>;
var modeField = document.querySelector('#fxForm input[name="mode"]');
var fixedOnly = document.querySelectorAll('.fx-fixed-only');
var note = document.createElement('p');
note.className = 'fx-hint fx-builder-note';
note.style.cssText = 'margin-top:10px;color:#6d28d9;background:#f5f3ff;border:1px solid #ddd6fe;border-radius:8px;padding:8px 12px;display:none';
note.textContent = '已选择「可视化编辑」:案例详情无需填写,保存后将进入可视化编辑器排版。';
var firstFixed = fixedOnly[0];
if(firstFixed && firstFixed.parentNode){ firstFixed.parentNode.insertBefore(note, firstFixed); }
function apply(){
var m = sel.value;
if(modeField) modeField.value = m;
if(!isCreate) return;
var isBuilder = (m === 'builder');
for(var i=0;i<fixedOnly.length;i++){ fixedOnly[i].style.display = isBuilder ? 'none' : ''; }
note.style.display = isBuilder ? 'block' : 'none';
}
sel.addEventListener('change', apply);
apply();
})();
</script>
+30
View File
@@ -0,0 +1,30 @@
<div class="page-head">
<div><h1>分类管理</h1><div class="desc">产品系列分类</div></div>
<a class="btn-primary" href="<?php echo site_url('admin/categories/create'); ?>">+ 新增分类</a>
</div>
<div class="admin-card">
<table class="admin-table">
<tr><th>图标</th><th>名称</th><th>标识</th><th>模式</th><th>描述</th><th>操作</th></tr>
<?php foreach ($cats as $c): ?>
<?php $cm = empty($c['mode']) ? 'fixed' : $c['mode']; ?>
<tr>
<td style="font-size:24px"><?php echo e($c['icon']); ?></td>
<td><b><?php echo e($c['name']); ?></b></td>
<td class="muted"><?php echo e($c['slug']); ?></td>
<td><span class="mode-badge <?php echo $cm === 'builder' ? 'builder' : 'fixed'; ?>"><?php echo $cm === 'builder' ? '可视化' : '固定'; ?></span></td>
<td class="muted"><?php echo e(mb_substr($c['description'] ?? '', 0, 24)); ?></td>
<td>
<div class="row-actions">
<a class="btn-soft btn-sm" href="<?php echo site_url('admin/categories/edit/' . $c['id']); ?>">编辑</a>
<form class="del-form" method="post" action="<?php echo site_url('admin/categories/delete/' . $c['id']); ?>"><?php echo csrf_field(); ?><button class="btn-danger btn-sm" type="submit">删除</button></form>
</div>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
<style>
.mode-badge{display:inline-block;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:600}
.mode-badge.fixed{background:#f1f5f9;color:#475569}
.mode-badge.builder{background:#ede9fe;color:#6d28d9}
</style>
+83
View File
@@ -0,0 +1,83 @@
<?php
/** @var array $c */
/** @var array $layout */
/** @var string $mode */
$v = function ($k, $d = '') use ($c) { return $c ? ($c[$k] ?? $d) : $d; };
$isEdit = !empty($c);
$switchUrl = $isEdit ? site_url('admin/categories/switchMode/' . (int)$c['id']) : '';
$initial = json_encode($layout ?: [], JSON_UNESCAPED_UNICODE);
?>
<div class="page-head">
<div>
<h1><?php echo $isEdit ? '编辑分类' : '新增分类'; ?><span class="mode-badge mode-builder">可视化编辑</span></h1>
<div class="desc">可视化编辑:拖拽文字 / 图片 / 按钮自由排版分类内容,保存后可用于前台展示或后续扩展。</div>
</div>
<?php if ($isEdit): ?>
<div class="mode-switch-sel">
<label for="modeSel">切换模式</label>
<select id="modeSel" class="mode-sel" onchange="location.href='<?php echo $switchUrl; ?>?mode='+this.value">
<option value="fixed" <?php echo $mode === 'fixed' ? 'selected' : ''; ?>>固定版面</option>
<option value="builder" <?php echo $mode === 'builder' ? 'selected' : ''; ?>>可视化编辑</option>
</select>
</div>
<?php else: ?>
<div class="mode-switch-sel">
<label for="modeSel">排版方式</label>
<select id="modeSel" class="mode-sel">
<option value="fixed" <?php echo $mode === 'fixed' ? 'selected' : ''; ?>>固定版面</option>
<option value="builder" <?php echo $mode === 'builder' ? 'selected' : ''; ?>>可视化编辑</option>
</select>
</div>
<?php endif; ?>
</div>
<div class="admin-card">
<form id="pbForm" method="post" action="<?php echo site_url($isEdit ? 'admin/categories/update/' . $c['id'] : 'admin/categories/store'); ?>">
<?php echo csrf_field(); ?>
<input type="hidden" name="mode" value="builder">
<input type="hidden" name="layout" id="pbLayout">
<div class="form-grid">
<div class="field"><label>分类名称 *</label><input name="name" value="<?php echo e($v('name')); ?>" required></div>
<div class="field"><label>图标(emoji</label><input name="icon" value="<?php echo e($v('icon', '❄')); ?>"></div>
</div>
<div class="form-grid">
<div class="field"><label>标识(留空按序号生成)</label><input name="slug" value="<?php echo e($v('slug')); ?>"></div>
<div class="field"><label>排序</label><input name="sort_order" type="number" value="<?php echo e($v('sort_order', 0)); ?>"></div>
</div>
<div class="field">
<label>可视化画布(拖拽排版分类内容)</label>
<?php echo \Core\View::buffer('admin/parts/builder', ['module' => 'category', 'layout' => $layout]); ?>
</div>
<div class="field" style="display:flex;align-items:center;gap:8px">
<input type="checkbox" name="status" value="1" <?php echo $v('status', 1) ? 'checked' : ''; ?> id="st"> <label for="st" style="margin:0">显示</label>
</div>
<div class="form-actions">
<button class="btn-primary" type="submit">保存</button>
<a class="btn-ghost" href="<?php echo site_url('admin/categories'); ?>">取消</a>
</div>
</form>
</div>
<style>
.mode-switch-sel{display:inline-flex;align-items:center;gap:8px}
.mode-switch-sel label{font-size:13px;color:#64748b}
.mode-sel{border:1px solid #e2e8f0;border-radius:10px;padding:8px 12px;font-size:14px;background:#fff;color:#334155;cursor:pointer;min-width:140px}
.mode-sel:focus{border-color:#0ea5e9;box-shadow:0 0 0 3px rgba(14,165,233,.12)}
.mode-badge{display:inline-block;margin-left:10px;padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600;vertical-align:middle;line-height:1.7}
.mode-badge.mode-fixed{background:#e0f2fe;color:#0369a1}
.mode-badge.mode-builder{background:#ede9fe;color:#6d28d9}
</style>
<script>
window.__PB_INIT__ = <?php echo $initial; ?>;
window.__PB_MEDIA__ = '<?php echo site_url('admin/media'); ?>';
window.__PB_UPLOAD__ = '<?php echo site_url('admin/media/upload'); ?>';
window.__PB_CSRF__ = '<?php echo csrf_token(); ?>';
window.__PB_MODULE__ = 'category';
window.__PB_DELETE__ = '<?php echo site_url('admin/media/delete/'); ?>';
</script>
<script src="<?php echo asset('js/page-builder.js'); ?>"></script>
+101
View File
@@ -0,0 +1,101 @@
<?php
/** @var array $c */
/** @var string $mode */
$v = function ($k, $d = '') use ($c) { return $c ? ($c[$k] ?? $d) : $d; };
$isEdit = !empty($c);
$mode = $mode ?? 'fixed';
$switchUrl = $isEdit ? site_url('admin/categories/switchMode/' . (int)$c['id']) : '';
$desc = $v('description');
?>
<div class="page-head">
<div>
<h1><?php echo $isEdit ? '编辑分类' : '新增分类'; ?><span class="mode-badge mode-fixed">固定版面</span></h1>
<div class="desc">固定版面:填写分类名称 / 图标 / 描述等基本信息<?php echo $isEdit ? ';可切换为可视化自由排版。' : '。'; ?></div>
</div>
<?php if ($isEdit): ?>
<div class="mode-switch-sel">
<label for="modeSel">切换模式</label>
<select id="modeSel" class="mode-sel" onchange="location.href='<?php echo $switchUrl; ?>?mode='+this.value">
<option value="fixed" <?php echo $mode === 'fixed' ? 'selected' : ''; ?>>固定版面</option>
<option value="builder" <?php echo $mode === 'builder' ? 'selected' : ''; ?>>可视化编辑</option>
</select>
</div>
<?php else: ?>
<div class="mode-switch-sel">
<label for="modeSel">排版方式</label>
<select id="modeSel" class="mode-sel">
<option value="fixed" <?php echo $mode === 'fixed' ? 'selected' : ''; ?>>固定版面</option>
<option value="builder" <?php echo $mode === 'builder' ? 'selected' : ''; ?>>可视化编辑</option>
</select>
</div>
<?php endif; ?>
</div>
<div class="admin-card">
<form id="fxForm" method="post" action="<?php echo site_url($isEdit ? 'admin/categories/update/' . $c['id'] : 'admin/categories/store'); ?>">
<?php echo csrf_field(); ?>
<input type="hidden" name="mode" value="fixed">
<div class="form-grid">
<div class="field"><label>分类名称 *</label><input name="name" value="<?php echo e($v('name')); ?>" required></div>
<div class="field"><label>图标(emoji</label><input name="icon" value="<?php echo e($v('icon', '❄')); ?>"></div>
</div>
<div class="form-grid">
<div class="field"><label>标识(留空按序号生成)</label><input name="slug" value="<?php echo e($v('slug')); ?>"></div>
<div class="field"><label>排序</label><input name="sort_order" type="number" value="<?php echo e($v('sort_order', 0)); ?>"></div>
</div>
<div class="field fx-fixed-only">
<label>分类描述</label>
<textarea name="description" rows="4" placeholder="分类简介,用于前台分类展示区域"><?php echo e($desc); ?></textarea>
<p class="fx-hint">纯文本描述,保存后前台分类列表展示。如需复杂排版可切换为可视化编辑。</p>
</div>
<div class="field" style="display:flex;align-items:center;gap:8px">
<input type="checkbox" name="status" value="1" <?php echo $v('status', 1) ? 'checked' : ''; ?> id="st"> <label for="st" style="margin:0">显示</label>
</div>
<div class="form-actions">
<button class="btn-primary" type="submit">保存</button>
<a class="btn-ghost" href="<?php echo site_url('admin/categories'); ?>">取消</a>
</div>
</form>
</div>
<style>
.mode-switch-sel{display:inline-flex;align-items:center;gap:8px}
.mode-switch-sel label{font-size:13px;color:#64748b}
.mode-sel{border:1px solid #e2e8f0;border-radius:10px;padding:8px 12px;font-size:14px;background:#fff;color:#334155;cursor:pointer;min-width:140px}
.mode-sel:focus{border-color:#0ea5e9;box-shadow:0 0 0 3px rgba(14,165,233,.12)}
.mode-badge{display:inline-block;margin-left:10px;padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600;vertical-align:middle;line-height:1.7}
.mode-badge.mode-fixed{background:#e0f2fe;color:#0369a1}
.mode-badge.mode-builder{background:#ede9fe;color:#6d28d9}
.fx-hint{font-size:12px;color:#94a3b8;margin:8px 2px 0}
</style>
<script>
// 新增分类时按「排版方式」选择器联动:选可视化编辑则隐藏描述,并提示保存后进入可视化编辑器
(function(){
var sel = document.getElementById('modeSel');
if(!sel) return;
var isCreate = <?php echo $isEdit ? 'false' : 'true'; ?>;
var modeField = document.querySelector('#fxForm input[name="mode"]');
var fixedOnly = document.querySelectorAll('.fx-fixed-only');
var note = document.createElement('p');
note.className = 'fx-hint fx-builder-note';
note.style.cssText = 'margin-top:10px;color:#6d28d9;background:#f5f3ff;border:1px solid #ddd6fe;border-radius:8px;padding:8px 12px;display:none';
note.textContent = '已选择「可视化编辑」:描述无需填写,保存后将进入可视化编辑器排版分类内容。';
var firstFixed = fixedOnly[0];
if(firstFixed && firstFixed.parentNode){ firstFixed.parentNode.insertBefore(note, firstFixed); }
function apply(){
var m = sel.value;
if(modeField) modeField.value = m;
if(!isCreate) return;
var isBuilder = (m === 'builder');
for(var i=0;i<fixedOnly.length;i++){ fixedOnly[i].style.display = isBuilder ? 'none' : ''; }
note.style.display = isBuilder ? 'block' : 'none';
}
sel.addEventListener('change', apply);
apply();
})();
</script>
+42
View File
@@ -0,0 +1,42 @@
<div class="page-head">
<div><h1>仪表盘</h1><div class="desc">网站数据概览与快捷操作</div></div>
<a class="btn-primary" href="<?php echo site_url('admin/products/create'); ?>"><?php echo admin_icon('plus', 18); ?> 新增产品</a>
</div>
<div class="stat-row">
<div class="stat-box c1"><span class="stat-ic"><?php echo admin_icon('snowflake', 22); ?></span><b><?php echo e($counts['products']); ?></b><span>产品数量</span></div>
<div class="stat-box c2"><span class="stat-ic"><?php echo admin_icon('folders', 22); ?></span><b><?php echo e($counts['categories']); ?></b><span>分类数量</span></div>
<div class="stat-box c3"><span class="stat-ic"><?php echo admin_icon('newspaper', 22); ?></span><b><?php echo e($counts['news']); ?></b><span>新闻数量</span></div>
<div class="stat-box c4"><span class="stat-ic"><?php echo admin_icon('images', 22); ?></span><b><?php echo e($counts['banners']); ?></b><span>轮播数量</span></div>
</div>
<div class="admin-card">
<h3 style="margin:0 0 14px;display:flex;align-items:center;gap:8px"><?php echo admin_icon('newspaper', 18); ?> 最近新闻</h3>
<?php if (empty($news)): ?><p class="muted">暂无新闻</p><?php endif; ?>
<?php if (!empty($news)): ?>
<table class="admin-table">
<tr><th>标题</th><th>发布日期</th><th>操作</th></tr>
<?php foreach ($news as $n): ?>
<tr>
<td><?php echo e($n['title']); ?></td>
<td class="muted"><?php echo e(format_date($n['published_at'])); ?></td>
<td><a class="btn-soft btn-sm" href="<?php echo site_url('admin/news/edit/' . $n['id']); ?>"><?php echo admin_icon('pencil', 14); ?> 编辑</a></td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
</div>
<div class="admin-card" style="margin-top:22px">
<h3 style="margin:0 0 16px;display:flex;align-items:center;gap:8px"><?php echo admin_icon('dashboard', 18); ?> 快捷入口</h3>
<div class="quick-actions">
<?php if (admin_role() === 'super_admin'): ?>
<a class="qa-card" href="<?php echo site_url('admin/users'); ?>"><span class="qa-ico"><?php echo admin_icon('users', 22); ?></span><span class="qa-t">用户管理</span><span class="qa-d">账号与权限分配</span></a>
<?php endif; ?>
<a class="qa-card" href="<?php echo site_url('admin/theme'); ?>"><span class="qa-ico"><?php echo admin_icon('palette', 22); ?></span><span class="qa-t">风格设置</span><span class="qa-d">主题配色与外观</span></a>
<a class="qa-card" href="<?php echo site_url('admin/settings'); ?>"><span class="qa-ico"><?php echo admin_icon('settings', 22); ?></span><span class="qa-t">站点设置</span><span class="qa-d">基础信息与 SEO</span></a>
<a class="qa-card" href="<?php echo site_url('admin/banners'); ?>"><span class="qa-ico"><?php echo admin_icon('images', 22); ?></span><span class="qa-t">轮播管理</span><span class="qa-d">首页横幅图</span></a>
<a class="qa-card" href="<?php echo site_url('admin/password'); ?>"><span class="qa-ico"><?php echo admin_icon('key', 22); ?></span><span class="qa-t">修改密码</span><span class="qa-d">账户安全</span></a>
<a class="qa-card" href="<?php echo site_url(); ?>" target="_blank"><span class="qa-ico"><?php echo admin_icon('globe', 22); ?></span><span class="qa-t">查看前台</span><span class="qa-d">在新窗口打开</span></a>
</div>
</div>
+59
View File
@@ -0,0 +1,59 @@
<?php
/** @var string $table @var array $cols @var array $names @var string $pk
* @var array $rows @var int $page @var int $totalPages @var int $total @var string $q */
?>
<div class="page-head">
<div>
<h1>表:<?php echo e($table); ?></h1>
<div class="desc">共 <b><?php echo number_format($total); ?></b> 条记录,主键:<code><?php echo e($pk); ?></code></div>
</div>
<div class="head-actions">
<a class="btn-soft" href="<?php echo site_url('admin/db'); ?>">← 所有表</a>
<a class="btn-primary" href="<?php echo site_url('admin/db/create/' . $table); ?>"><?php echo admin_icon('plus', 16); ?> 新增记录</a>
</div>
</div>
<div class="admin-card">
<form class="db-search" method="get" action="<?php echo site_url('admin/db/browse/' . $table); ?>">
<input type="text" name="q" value="<?php echo e($q); ?>" placeholder="跨字段搜索…" />
<button class="btn-soft" type="submit">搜索</button>
<?php if ($q !== ''): ?><a class="btn-link" href="<?php echo site_url('admin/db/browse/' . $table); ?>">清除</a><?php endif; ?>
</form>
<div class="tbl-wrap">
<table class="tbl tbl-data">
<thead>
<tr>
<?php foreach ($names as $c): ?><th class="mono"><?php echo e($c); ?></th><?php endforeach; ?>
<th class="num">操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<?php foreach ($names as $c): ?>
<td class="cell" title="<?php echo e((string)($r[$c] ?? '')); ?>"><?php echo e(mb_substr((string)($r[$c] ?? ''), 0, 120)); ?></td>
<?php endforeach; ?>
<td class="num nowrap">
<a class="link-ok" href="<?php echo site_url('admin/db/edit/' . $table . '/' . rawurlencode($r[$pk])); ?>">编辑</a>
<a class="link-del" href="<?php echo site_url('admin/db/delete/' . $table . '/' . rawurlencode($r[$pk])); ?>"
data-confirm="确认删除该记录?此操作不可撤销。">删除</a>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?><tr><td class="muted" colspan="<?php echo count($names) + 1; ?>">无记录</td></tr><?php endif; ?>
</tbody>
</table>
</div>
<?php if ($totalPages > 1): ?>
<div class="pager">
<?php
$base = 'admin/db/browse/' . $table . '?q=' . urlencode($q) . '&page=';
if ($page > 1): ?><a class="btn-soft btn-sm" href="<?php echo site_url($base . ($page - 1)); ?>">← 上一页</a><?php endif;
echo '<span class="pager-info">第 ' . $page . ' / ' . $totalPages . ' 页</span>';
if ($page < $totalPages): ?><a class="btn-soft btn-sm" href="<?php echo site_url($base . ($page + 1)); ?>">下一页 →</a><?php endif;
?>
</div>
<?php endif; ?>
</div>
+52
View File
@@ -0,0 +1,52 @@
<?php
/** @var string $table @var array $cols @var string $pk @var array|null $row @var string $mode */
$isEdit = $mode === 'edit';
$action = $isEdit
? site_url('admin/db/update/' . $table . '/' . rawurlencode($row[$pk]))
: site_url('admin/db/store/' . $table);
?>
<div class="page-head">
<div>
<h1><?php echo $isEdit ? '编辑记录' : '新增记录'; ?></h1>
<div class="desc">表:<code><?php echo e($table); ?></code> 主键:<code><?php echo e($pk); ?></code></div>
</div>
<a class="btn-soft" href="<?php echo site_url('admin/db/browse/' . $table); ?>">← 返回列表</a>
</div>
<div class="admin-card db-form-card">
<form method="post" action="<?php echo $action; ?>">
<?php foreach ($cols as $c): ?>
<?php
$f = $c['Field'];
$autoInc = ($c['Extra'] ?? '') === 'auto_increment';
if ($autoInc && !$isEdit) continue; // 自增主键在新增时由数据库生成
$val = $row !== null ? ($row[$f] ?? '') : '';
$isPk = $f === $pk;
$long = (bool)preg_match('/(text|blob|json)/i', $c['Type']);
$required = $c['Null'] === 'NO' && $c['Default'] === null && !$autoInc;
?>
<div class="form-row">
<label>
<span class="form-label"><?php echo e($f); ?>
<?php if ($isPk): ?><span class="tag-mini tag-amber">PK</span><?php endif; ?>
<?php if ($c['Null'] === 'YES'): ?><span class="tag-mini tag-gray">可空</span><?php endif; ?>
<em class="form-type"><?php echo e($c['Type']); ?></em>
</span>
<?php if ($isPk && $isEdit): ?>
<input type="hidden" name="<?php echo e($f); ?>" value="<?php echo e($val); ?>" />
<input class="input" type="text" value="<?php echo e($val); ?>" disabled />
<?php elseif ($long): ?>
<textarea class="input" name="<?php echo e($f); ?>" rows="4" <?php echo $required ? 'required' : ''; ?>><?php echo e($val); ?></textarea>
<?php else: ?>
<input class="input" type="text" name="<?php echo e($f); ?>" value="<?php echo e($val); ?>" <?php echo $required ? 'required' : ''; ?> />
<?php endif; ?>
</label>
</div>
<?php endforeach; ?>
<div class="form-actions">
<button class="btn-primary" type="submit"><?php echo $isEdit ? '保存修改' : '创建记录'; ?></button>
<a class="btn-soft" href="<?php echo site_url('admin/db/browse/' . $table); ?>">取消</a>
</div>
</form>
</div>
+49
View File
@@ -0,0 +1,49 @@
<?php /** @var array $tables @var int $total @var bool $fileMode @var array $dataFiles */ ?>
<div class="page-head">
<div>
<h1>数据库管理</h1>
<div class="desc">查看并维护数据库中所有表的数据,可进行浏览、编辑、新增与删除。</div>
</div>
<a class="btn-soft" href="<?php echo site_url('admin/upgrade'); ?>"><?php echo admin_icon('upload', 16); ?> 数据库升级</a>
</div>
<?php if (!empty($fileMode)): ?>
<div class="admin-card">
<div class="alert alert-warn">当前为 <b>文件存储模式</b>,不支持在线数据库管理。可用数据文件如下(只读):</div>
<table class="tbl">
<thead><tr><th>数据文件</th><th>大小</th></tr></thead>
<tbody>
<?php foreach (($dataFiles ?? []) as $f): ?>
<tr><td><?php echo e($f['name']); ?></td><td><?php echo number_format($f['size']); ?> B</td></tr>
<?php endforeach; ?>
<?php if (empty($dataFiles)): ?><tr><td colspan="2" class="muted">无数据文件</td></tr><?php endif; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="admin-card">
<div class="db-stat">共 <b><?php echo (int)($total ?? 0); ?></b> 张表</div>
<div class="tbl-wrap">
<table class="tbl">
<thead>
<tr>
<th>表名</th><th>引擎</th><th class="num">记录数</th><th class="num">大小</th><th class="num">操作</th>
</tr>
</thead>
<tbody>
<?php foreach (($tables ?? []) as $t): ?>
<tr>
<td class="mono"><?php echo e($t['name']); ?></td>
<td class="muted"><?php echo e($t['engine']); ?></td>
<td class="num"><?php echo number_format($t['rows']); ?></td>
<td class="num"><?php echo human_size($t['size']); ?></td>
<td class="num">
<a class="link-ok" href="<?php echo site_url('admin/db/browse/' . $t['name']); ?>">浏览数据</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
+25
View File
@@ -0,0 +1,25 @@
<div class="login-card">
<div class="login-brand"><span class="brand-mark"><?php echo admin_icon('snowflake', 20); ?></span> 酷冰甲 · 降温服</div>
<h1>后台登录</h1>
<p class="sub">请输入管理员账号密码</p>
<?php if ($error): ?><div class="alert alert-err"><?php echo e($error); ?></div><?php endif; ?>
<form method="post" action="<?php echo site_url('admin/login'); ?>" autocomplete="on">
<?php echo csrf_field(); ?>
<div class="field"><label>账号</label><input name="username" required autofocus autocomplete="username" maxlength="32" placeholder="请输入账号"></div>
<div class="field"><label>密码</label><input type="password" name="password" required autocomplete="current-password" maxlength="64" placeholder="请输入密码"></div>
<div class="field">
<label>验证码</label>
<div style="display:flex;align-items:center;gap:12px">
<input name="captcha" required inputmode="text" autocomplete="off" maxlength="4" placeholder="请输入验证码" style="flex:1;text-transform:uppercase">
<img id="captchaImg" src="<?php echo e($captcha['url']); ?>" alt="验证码" title="点击刷新" style="width:130px;height:44px;border:1px solid #d1d5db;border-radius:6px;cursor:pointer;vertical-align:middle">
</div>
</div>
<button class="btn-primary" style="width:100%" type="submit">登 录</button>
</form>
<script>
document.getElementById('captchaImg').addEventListener('click', function() {
this.src = '<?php echo e($captcha['url']); ?>'.replace(/_t=\w+/, '_t=' + Date.now().toString(16));
});
</script>
<p class="muted" style="margin-top:16px;font-size:13px;display:flex;align-items:center;gap:6px"><?php echo admin_icon('shield', 15); ?> 请使用分配的账号登录;忘记密码请联系超级管理员重置</p>
</div>
+31
View File
@@ -0,0 +1,31 @@
<div class="page-head">
<div><h1>新闻管理</h1><div class="desc">新闻动态与案例</div></div>
<a class="btn-primary" href="<?php echo site_url('admin/news/create'); ?>">+ 写新闻</a>
</div>
<div class="admin-card">
<table class="admin-table">
<tr><th>封面</th><th>标题</th><th>模式</th><th>作者</th><th>日期</th><th>状态</th><th>操作</th></tr>
<?php foreach ($news as $n): ?>
<?php $nm = empty($n['mode']) ? 'fixed' : $n['mode']; ?>
<tr>
<td><div class="thum" style="background:<?php echo gradient($n['id']); ?>">📰</div></td>
<td><b><?php echo e($n['title']); ?></b></td>
<td><span class="mode-badge <?php echo $nm === 'builder' ? 'builder' : 'fixed'; ?>"><?php echo $nm === 'builder' ? '可视化' : '固定'; ?></span></td>
<td class="muted"><?php echo e($n['author']); ?></td>
<td class="muted"><?php echo e(format_date($n['published_at'])); ?></td>
<td><?php echo ($n['status'] ?? 1) ? '<span class="tag-mini">已发布</span>' : '<span class="muted">草稿</span>'; ?></td>
<td>
<div class="row-actions">
<a class="btn-soft btn-sm" href="<?php echo site_url('admin/news/edit/' . $n['id']); ?>">编辑</a>
<form class="del-form" method="post" action="<?php echo site_url('admin/news/delete/' . $n['id']); ?>"><?php echo csrf_field(); ?><button class="btn-danger btn-sm" type="submit">删除</button></form>
</div>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
<style>
.mode-badge{display:inline-block;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:600}
.mode-badge.fixed{background:#f1f5f9;color:#475569}
.mode-badge.builder{background:#ede9fe;color:#6d28d9}
</style>
+88
View File
@@ -0,0 +1,88 @@
<?php
/** @var array $n */
/** @var array $layout */
/** @var string $mode */
$v = function ($k, $d = '') use ($n) { return $n ? ($n[$k] ?? $d) : $d; };
$isEdit = !empty($n);
$switchUrl = $isEdit ? site_url('admin/news/switchMode/' . (int)$n['id']) : '';
$initial = json_encode($layout ?: [], JSON_UNESCAPED_UNICODE);
$coverVal = $v('cover');
?>
<div class="page-head">
<div>
<h1><?php echo $isEdit ? '编辑新闻' : '写新闻'; ?><span class="mode-badge mode-builder">可视化编辑</span></h1>
<div class="desc">可视化编辑:拖拽文字 / 图片 / 按钮自由排版,保存后前台按此布局整页展示。</div>
</div>
<?php if ($isEdit): ?>
<div class="mode-switch-sel">
<label for="modeSel">切换模式</label>
<select id="modeSel" class="mode-sel" onchange="location.href='<?php echo $switchUrl; ?>?mode='+this.value">
<option value="fixed" <?php echo $mode === 'fixed' ? 'selected' : ''; ?>>固定版面</option>
<option value="builder" <?php echo $mode === 'builder' ? 'selected' : ''; ?>>可视化编辑</option>
</select>
</div>
<?php else: ?>
<div class="mode-switch-sel">
<label for="modeSel">排版方式</label>
<select id="modeSel" class="mode-sel">
<option value="fixed" <?php echo $mode === 'fixed' ? 'selected' : ''; ?>>固定版面</option>
<option value="builder" <?php echo $mode === 'builder' ? 'selected' : ''; ?>>可视化编辑</option>
</select>
</div>
<?php endif; ?>
</div>
<div class="admin-card">
<form id="pbForm" method="post" action="<?php echo site_url($isEdit ? 'admin/news/update/' . $n['id'] : 'admin/news/store'); ?>" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<input type="hidden" name="mode" value="builder">
<input type="hidden" name="layout" id="pbLayout">
<div class="field"><label>标题 *</label><input name="title" value="<?php echo e($v('title')); ?>" required></div>
<div class="form-grid">
<div class="field"><label>作者</label><input name="author" value="<?php echo e($v('author', '酷冰甲')); ?>"></div>
<div class="field"><label>发布日期</label><input type="date" name="published_at" value="<?php echo e($v('published_at', date('Y-m-d'))); ?>"></div>
</div>
<div class="field"><label>摘要</label><input name="summary" value="<?php echo e($v('summary')); ?>"></div>
<div class="field"><label>URL 标识(留空按序号生成)</label><input name="slug" value="<?php echo e($v('slug')); ?>" placeholder="留空则自动生成如 12"></div>
<div class="form-grid">
<div class="field"><label>封面图(上传,可选)</label><input type="file" name="cover" accept="image/*"></div>
<div class="field"><label>或填写图片地址/路径</label><input name="cover_url" value="<?php echo e($coverVal); ?>" placeholder="assets/uploads/xxx.jpg 或 http(s)://"></div>
</div>
<div class="field">
<label>可视化画布(拖拽排版新闻详情)</label>
<?php echo \Core\View::buffer('admin/parts/builder', ['module' => 'news', 'layout' => $layout]); ?>
</div>
<div class="field" style="display:flex;align-items:center;gap:8px">
<input type="checkbox" name="status" value="1" <?php echo $v('status', 1) ? 'checked' : ''; ?> id="st"> <label for="st" style="margin:0">立即发布</label>
</div>
<div class="form-actions">
<button class="btn-primary" type="submit">保存</button>
<a class="btn-ghost" href="<?php echo site_url('admin/news'); ?>">取消</a>
</div>
</form>
</div>
<style>
.mode-switch-sel{display:inline-flex;align-items:center;gap:8px}
.mode-switch-sel label{font-size:13px;color:#64748b}
.mode-sel{border:1px solid #e2e8f0;border-radius:10px;padding:8px 12px;font-size:14px;background:#fff;color:#334155;cursor:pointer;min-width:140px}
.mode-sel:focus{border-color:#0ea5e9;box-shadow:0 0 0 3px rgba(14,165,233,.12)}
.mode-badge{display:inline-block;margin-left:10px;padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600;vertical-align:middle;line-height:1.7}
.mode-badge.mode-fixed{background:#e0f2fe;color:#0369a1}
.mode-badge.mode-builder{background:#ede9fe;color:#6d28d9}
</style>
<script>
window.__PB_INIT__ = <?php echo $initial; ?>;
window.__PB_MEDIA__ = '<?php echo site_url('admin/media'); ?>';
window.__PB_UPLOAD__ = '<?php echo site_url('admin/media/upload'); ?>';
window.__PB_CSRF__ = '<?php echo csrf_token(); ?>';
window.__PB_MODULE__ = 'news';
window.__PB_DELETE__ = '<?php echo site_url('admin/media/delete/'); ?>';
</script>
<script src="<?php echo asset('js/page-builder.js'); ?>"></script>
+163
View File
@@ -0,0 +1,163 @@
<?php
/** @var array $n */
/** @var string $mode */
$v = function ($k, $d = '') use ($n) { return $n ? ($n[$k] ?? $d) : $d; };
$isEdit = !empty($n);
$mode = $mode ?? 'fixed';
$switchUrl = $isEdit ? site_url('admin/news/switchMode/' . (int)$n['id']) : '';
$content = $v('content');
$coverVal = $v('cover');
?>
<div class="page-head">
<div>
<h1><?php echo $isEdit ? '编辑新闻' : '写新闻'; ?><span class="mode-badge mode-fixed">固定版面</span></h1>
<div class="desc">固定版面:填写标题 / 摘要 / 封面,正文使用富文本编辑器排版<?php echo $isEdit ? ';可切换为可视化自由排版。' : '。'; ?></div>
</div>
<?php if ($isEdit): ?>
<div class="mode-switch-sel">
<label for="modeSel">切换模式</label>
<select id="modeSel" class="mode-sel" onchange="location.href='<?php echo $switchUrl; ?>?mode='+this.value">
<option value="fixed" <?php echo $mode === 'fixed' ? 'selected' : ''; ?>>固定版面</option>
<option value="builder" <?php echo $mode === 'builder' ? 'selected' : ''; ?>>可视化编辑</option>
</select>
</div>
<?php else: ?>
<div class="mode-switch-sel">
<label for="modeSel">排版方式</label>
<select id="modeSel" class="mode-sel">
<option value="fixed" <?php echo $mode === 'fixed' ? 'selected' : ''; ?>>固定版面</option>
<option value="builder" <?php echo $mode === 'builder' ? 'selected' : ''; ?>>可视化编辑</option>
</select>
</div>
<?php endif; ?>
</div>
<div class="admin-card">
<form id="fxForm" method="post" action="<?php echo site_url($isEdit ? 'admin/news/update/' . $n['id'] : 'admin/news/store'); ?>" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<input type="hidden" name="mode" value="fixed">
<div class="field"><label>标题 *</label><input name="title" value="<?php echo e($v('title')); ?>" required></div>
<div class="form-grid">
<div class="field"><label>作者</label><input name="author" value="<?php echo e($v('author', '酷冰甲')); ?>"></div>
<div class="field"><label>发布日期</label><input type="date" name="published_at" value="<?php echo e($v('published_at', date('Y-m-d'))); ?>"></div>
</div>
<div class="field"><label>摘要</label><input name="summary" value="<?php echo e($v('summary')); ?>"></div>
<div class="field"><label>URL 标识(留空按序号生成,短而稳定)</label><input name="slug" value="<?php echo e($v('slug')); ?>" placeholder="留空则自动生成如 12"></div>
<div class="field">
<label>封面图(上传,可选)</label>
<input type="file" name="cover" accept="image/*">
<?php if ($coverVal): ?>
<div class="img-prev"><img src="<?php echo e(site_url($coverVal)); ?>" alt=""><span class="img-prev-path"><?php echo e($coverVal); ?></span></div>
<p class="fx-hint">已上传封面;重新选择文件将替换,留空则保留。</p>
<?php endif; ?>
<input type="text" name="cover_url" value="<?php echo e($coverVal); ?>" placeholder="或填写图片地址 assets/uploads/xxx.jpg 或 http(s)://" style="margin-top:8px">
</div>
<div class="field fx-fixed-only">
<label>正文内容(富文本编辑器)</label>
<div class="fx-toolbar">
<button type="button" data-cmd="bold" title="加粗"><b>B</b></button>
<button type="button" data-cmd="italic" title="斜体"><i>I</i></button>
<button type="button" data-cmd="formatBlock" data-val="H2">H2</button>
<button type="button" data-cmd="formatBlock" data-val="H3">H3</button>
<button type="button" data-cmd="insertUnorderedList" title="无序列表">• 列表</button>
<button type="button" data-cmd="insertOrderedList" title="有序列表">1. 列表</button>
<button type="button" data-cmd="formatBlock" data-val="BLOCKQUOTE" title="引用">引用</button>
<button type="button" id="fxLink" title="插入链接">链接</button>
<button type="button" id="fxImg" title="插入图片">图片</button>
</div>
<div class="fx-editor" id="fxEditor" contenteditable="true"><?php echo $content; ?></div>
<textarea name="content" id="fxContent" hidden><?php echo e($content); ?></textarea>
<p class="fx-hint">直接排版正文即可;点击「图片」可从素材库选择或上传新图。保存后前台固定版式展示。</p>
</div>
<div class="field" style="display:flex;align-items:center;gap:8px">
<input type="checkbox" name="status" value="1" <?php echo $v('status', 1) ? 'checked' : ''; ?> id="st"> <label for="st" style="margin:0">立即发布</label>
</div>
<div class="form-actions">
<button class="btn-primary" type="submit">保存</button>
<a class="btn-ghost" href="<?php echo site_url('admin/news'); ?>">取消</a>
</div>
</form>
</div>
<!-- 图片选择弹层 -->
<div class="fx-modal" id="fxModal" hidden>
<div class="fx-modal-box">
<div class="fx-modal-head"><span>选择图片</span><button type="button" id="fxModalClose">×</button></div>
<div class="fx-modal-body">
<label class="fx-upload"> 上传图片<input type="file" id="fxUp" accept="image/*" hidden></label>
<div class="fx-lib" id="fxLib"></div>
</div>
</div>
</div>
<style>
.mode-switch-sel{display:inline-flex;align-items:center;gap:8px}
.mode-switch-sel label{font-size:13px;color:#64748b}
.mode-sel{border:1px solid #e2e8f0;border-radius:10px;padding:8px 12px;font-size:14px;background:#fff;color:#334155;cursor:pointer;min-width:140px}
.mode-sel:focus{border-color:#0ea5e9;box-shadow:0 0 0 3px rgba(14,165,233,.12)}
.mode-badge{display:inline-block;margin-left:10px;padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600;vertical-align:middle;line-height:1.7}
.mode-badge.mode-fixed{background:#e0f2fe;color:#0369a1}
.mode-badge.mode-builder{background:#ede9fe;color:#6d28d9}
.img-prev{display:flex;align-items:center;gap:10px;margin-top:10px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:8px 10px}
.img-prev img{width:72px;height:54px;object-fit:cover;border-radius:8px;display:block}
.img-prev-path{font-size:12px;color:#94a3b8;word-break:break-all}
.fx-toolbar{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}
.fx-toolbar button{width:auto;min-width:38px;height:34px;padding:0 10px;border:1px solid #e2e8f0;background:#fff;border-radius:8px;cursor:pointer;font-size:14px;color:#334155}
.fx-toolbar button:hover{border-color:#0ea5e9;color:#0ea5e9}
.fx-editor{min-height:320px;border:1px solid #e2e8f0;border-radius:10px;padding:16px 18px;font-size:16px;line-height:1.9;outline:none;background:#fff;overflow:auto}
.fx-editor:focus{border-color:#0ea5e9;box-shadow:0 0 0 3px rgba(14,165,233,.12)}
.fx-editor h2{font-size:24px;margin:.6em 0 .4em}
.fx-editor h3{font-size:20px;margin:.6em 0 .4em}
.fx-editor blockquote{margin:.6em 0;padding:8px 14px;border-left:4px solid #0ea5e9;color:#475569;background:#f8fafc}
.fx-editor img{max-width:100%;height:auto;border-radius:8px;display:block;margin:8px 0}
.fx-editor a{color:#0ea5e9}
.fx-hint{font-size:12px;color:#94a3b8;margin:8px 2px 0}
.fx-modal{position:fixed;inset:0;background:rgba(15,23,42,.45);display:flex;align-items:center;justify-content:center;z-index:80}
.fx-modal[hidden]{display:none}
.fx-modal-box{background:#fff;border-radius:14px;width:420px;max-width:92vw;overflow:hidden}
.fx-modal-head{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #e2e8f0;font-weight:700}
.fx-modal-head button{border:none;background:none;font-size:22px;cursor:pointer;color:#64748b;line-height:1}
.fx-modal-body{padding:16px;max-height:60vh;overflow:auto}
.fx-upload{display:block;text-align:center;background:#0ea5e9;color:#fff;border-radius:10px;padding:10px;cursor:pointer;font-weight:600;margin-bottom:12px}
.fx-lib{display:flex;flex-wrap:wrap;gap:8px}
.fx-lib img{width:72px;height:72px;object-fit:cover;border-radius:8px;cursor:pointer;border:2px solid transparent}
.fx-lib img:hover{border-color:#0ea5e9}
</style>
<script>
window.__PB_MEDIA__ = '<?php echo site_url('admin/media'); ?>';
window.__PB_UPLOAD__ = '<?php echo site_url('admin/media/upload'); ?>';
window.__PB_CSRF__ = '<?php echo csrf_token(); ?>';
</script>
<script src="<?php echo asset('js/fixed-editor.js'); ?>"></script>
<script>
// 新增新闻时按「排版方式」选择器联动:选可视化编辑则隐藏正文编辑器,并提示保存后进入可视化编辑器
(function(){
var sel = document.getElementById('modeSel');
if(!sel) return;
var isCreate = <?php echo $isEdit ? 'false' : 'true'; ?>;
var modeField = document.querySelector('#fxForm input[name="mode"]');
var fixedOnly = document.querySelectorAll('.fx-fixed-only');
var note = document.createElement('p');
note.className = 'fx-hint fx-builder-note';
note.style.cssText = 'margin-top:10px;color:#6d28d9;background:#f5f3ff;border:1px solid #ddd6fe;border-radius:8px;padding:8px 12px;display:none';
note.textContent = '已选择「可视化编辑」:正文无需填写,保存后将进入可视化编辑器排版新闻详情。';
var firstFixed = fixedOnly[0];
if(firstFixed && firstFixed.parentNode){ firstFixed.parentNode.insertBefore(note, firstFixed); }
function apply(){
var m = sel.value;
if(modeField) modeField.value = m;
if(!isCreate) return;
var isBuilder = (m === 'builder');
for(var i=0;i<fixedOnly.length;i++){ fixedOnly[i].style.display = isBuilder ? 'none' : ''; }
note.style.display = isBuilder ? 'block' : 'none';
}
sel.addEventListener('change', apply);
apply();
})();
</script>
+33
View File
@@ -0,0 +1,33 @@
<?php /** @var array $o */ /** @var array $payments */ /** @var string $seg */ ?>
<div class="page-head">
<div><h1>订单详情</h1><div class="desc">订单号 <?php echo e($o['order_no']); ?></div></div>
<a class="btn-ghost" href="<?php echo site_url('admin/orders'); ?>">← 返回列表</a>
</div>
<div class="admin-card">
<table class="admin-table">
<tr><th>商品</th><td><?php echo e($o['product_name']); ?></td></tr>
<tr><th>客户</th><td><?php echo e($o['customer_name']); ?><?php echo e($o['phone']); ?><?php if (!empty($o['email'])): ?>/ <?php echo e($o['email']); ?><?php endif; ?></td></tr>
<tr><th>数量</th><td><?php echo e($o['qty']); ?> 套</td></tr>
<tr><th>金额</th><td>¥<?php echo e($o['amount']); ?></td></tr>
<tr><th>支付通道</th><td><?php echo $o['channel'] === 'wechat' ? '微信' : ($o['channel'] === 'alipay' ? '支付宝' : '—'); ?></td></tr>
<tr><th>状态</th><td><?php echo $o['status'] === 'paid' ? '已支付' : '待支付'; ?><?php if (!empty($o['paid_at'])): echo '' . e($o['paid_at']) . ''; ?><?php endif; ?></td></tr>
<tr><th>网关交易号</th><td><?php echo e($o['gateway_trade_no'] ?? '—'); ?></td></tr>
<tr><th>下单时间</th><td><?php echo e($o['created_at']); ?></td></tr>
</table>
<?php if ($o['status'] !== 'paid'): ?>
<form method="post" action="<?php echo site_url('admin/orders/markPaid/' . $o['id']); ?>" style="margin-top:16px">
<?php echo csrf_field(); ?><button class="btn-primary" type="submit" data-confirm="标记为已支付?">标记为已支付</button>
</form>
<?php endif; ?>
<?php if ($payments): ?>
<h3 style="margin:24px 0 10px">付款记录</h3>
<table class="admin-table">
<thead><tr><th>通道</th><th>交易号</th><th>金额</th><th>状态</th><th>付款时间</th></tr></thead>
<tbody>
<?php foreach ($payments as $p): ?>
<tr><td><?php echo $p['channel'] === 'wechat' ? '微信' : '支付宝'; ?></td><td><?php echo e($p['trade_no']); ?></td><td>¥<?php echo e($p['amount']); ?></td><td><?php echo e($p['status']); ?></td><td><?php echo e($p['paid_at']); ?></td></tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
+37
View File
@@ -0,0 +1,37 @@
<?php /** @var array $orders */ /** @var string $seg */ ?>
<div class="page-head"><div><h1>订单管理</h1><div class="desc">查看客户下单与付款状态(超级管理员 / 管理员可见)</div></div></div>
<div class="admin-card">
<table class="admin-table">
<thead><tr><th>订单号</th><th>商品</th><th>客户</th><th>数量</th><th>金额</th><th>通道</th><th>状态</th><th>下单时间</th><th>操作</th></tr></thead>
<tbody>
<?php foreach ($orders as $o): ?>
<tr>
<td><?php echo e($o['order_no']); ?></td>
<td><?php echo e($o['product_name']); ?></td>
<td><?php echo e($o['customer_name']); ?><br><span class="muted"><?php echo e($o['phone']); ?></span></td>
<td><?php echo e($o['qty']); ?></td>
<td>¥<?php echo e($o['amount']); ?></td>
<td><?php echo $o['channel'] === 'wechat' ? '微信' : ($o['channel'] === 'alipay' ? '支付宝' : '—'); ?></td>
<td><?php echo $o['status'] === 'paid'
? '<span class="tag-mini" style="background:#dcfce7;color:#15803d">已支付</span>'
: '<span class="tag-mini" style="background:#fef3c7;color:#b45309">待支付</span>'; ?></td>
<td class="muted"><?php echo e($o['created_at']); ?></td>
<td>
<div class="row-actions">
<a class="btn-soft btn-sm" href="<?php echo site_url('admin/orders/show/' . $o['id']); ?>">详情</a>
<?php if ($o['status'] !== 'paid'): ?>
<form method="post" action="<?php echo site_url('admin/orders/markPaid/' . $o['id']); ?>" style="display:inline">
<?php echo csrf_field(); ?><button class="btn-primary btn-sm" type="submit" data-confirm="标记为已支付?">标记支付</button>
</form>
<?php endif; ?>
<form method="post" action="<?php echo site_url('admin/orders/delete/' . $o['id']); ?>" style="display:inline">
<?php echo csrf_field(); ?><button class="btn-danger btn-sm" type="submit" data-confirm="确认删除该订单?">删除</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if (empty($orders)): ?><p class="muted" style="padding:20px">暂无订单</p><?php endif; ?>
</div>
+120
View File
@@ -0,0 +1,120 @@
<?php
/** @var array $p */
/** @var array $layout */
/** @var string $mode */
$v = function ($k, $d = '') use ($p) { return $p ? ($p[$k] ?? $d) : $d; };
$isEdit = !empty($p);
$switchUrl = $isEdit ? site_url('admin/pages/switchMode/' . (int)$p['id']) : '';
$initial = json_encode($layout ?: []);
?>
<div class="pb-root">
<div class="pb-topbar">
<div class="pb-top-left">
<a class="btn-ghost" href="<?php echo site_url('admin/pages'); ?>">← 返回</a>
<span class="pb-t">可视化编辑:<?php echo e($p['title']); ?></span>
<span class="mode-badge mode-builder">可视化编辑</span>
<?php if ($isEdit): ?>
<div class="mode-switch-sel">
<label for="modeSel">切换模式</label>
<select id="modeSel" class="mode-sel" onchange="location.href='<?php echo $switchUrl; ?>?mode='+this.value">
<option value="fixed" <?php echo $mode === 'fixed' ? 'selected' : ''; ?>>固定版面</option>
<option value="builder" <?php echo $mode === 'builder' ? 'selected' : ''; ?>>可视化编辑</option>
</select>
</div>
<?php endif; ?>
</div>
<form id="pbForm" method="post" action="<?php echo site_url('admin/pages/update/' . $p['id']); ?>" class="pb-top-form">
<?php echo csrf_field(); ?>
<input type="hidden" name="layout" id="pbLayout">
<input class="pb-title" name="title" value="<?php echo e($p['title']); ?>" placeholder="页面标题">
<button class="btn-primary" type="submit">保存页面</button>
</form>
</div>
<div class="pb-workspace">
<!-- 左侧:素材 + 添加 -->
<aside class="pb-left">
<div class="pb-box">
<h4>素材库</h4>
<label class="pb-upload"> 上传图片
<input type="file" id="pbFile" accept="image/*" hidden>
</label>
<div class="pb-lib" id="pbLib"></div>
</div>
<div class="pb-box">
<h4>添加元素</h4>
<button class="pb-add" id="pbAddText" type="button"> 文字</button>
<button class="pb-add" id="pbAddImg" type="button"> 图片(从素材库)</button>
</div>
<p class="pb-hint">· 拖动元素移动位置<br>· 拖动右下角缩放<br>· 双击文字可直接编辑<br>· 选中后在右侧改样式</p>
</aside>
<!-- 中间:画布 -->
<main class="pb-canvas-wrap">
<div class="pb-stage" id="pbStage"></div>
</main>
<!-- 右侧:属性 -->
<aside class="pb-right">
<div class="pb-box">
<h4>属性</h4>
<div id="pbPropsBody" class="pb-empty">在画布中选择一个元素以编辑属性</div>
</div>
</aside>
</div>
</div>
<style>
.pb-root{display:flex;flex-direction:column}
.pb-topbar{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:14px 18px;background:#fff;border-bottom:1px solid var(--admin-border,#e2e8f0);position:sticky;top:0;z-index:30}
.pb-top-left{display:flex;align-items:center;gap:12px}
.pb-t{font-weight:700;color:#0f172a}
.mode-switch-sel{display:inline-flex;align-items:center;gap:8px}
.mode-switch-sel label{font-size:13px;color:#64748b}
.mode-sel{border:1px solid #e2e8f0;border-radius:10px;padding:6px 12px;font-size:13px;background:#fff;color:#334155;cursor:pointer;min-width:140px}
.mode-sel:focus{border-color:#0ea5e9;box-shadow:0 0 0 3px rgba(14,165,233,.12)}
.mode-badge{display:inline-block;margin-left:6px;padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600;vertical-align:middle;line-height:1.7}
.mode-badge.mode-fixed{background:#e0f2fe;color:#0369a1}
.mode-badge.mode-builder{background:#ede9fe;color:#6d28d9}
.pb-top-form{display:flex;align-items:center;gap:10px}
.pb-title{border:1px solid #e2e8f0;border-radius:10px;padding:8px 12px;font-size:14px;min-width:200px}
.pb-workspace{display:flex;gap:0;min-height:72vh}
.pb-left,.pb-right{width:248px;flex:0 0 248px;background:#f8fafc;border-left:1px solid #e2e8f0;border-right:1px solid #e2e8f0;padding:14px;overflow:auto}
.pb-right{border-left:1px solid #e2e8f0}
.pb-box{margin-bottom:18px}
.pb-box h4{margin:0 0 10px;font-size:13px;color:#475569;letter-spacing:.04em;text-transform:uppercase}
.pb-upload{display:block;text-align:center;background:#0ea5e9;color:#fff;border-radius:10px;padding:10px;cursor:pointer;font-size:14px;font-weight:600;margin-bottom:10px}
.pb-upload:hover{background:#0284c7}
.pb-lib{display:flex;flex-wrap:wrap;gap:8px}
.pb-lib img{width:64px;height:64px;object-fit:cover;border-radius:8px;border:2px solid transparent;cursor:pointer}
.pb-lib img:hover{border-color:#0ea5e9}
.pb-add{display:block;width:100%;margin-bottom:8px;background:#fff;border:1px dashed #94a3b8;color:#334155;border-radius:10px;padding:10px;cursor:pointer;font-size:14px}
.pb-add:hover{border-color:#0ea5e9;color:#0ea5e9}
.pb-hint{font-size:12px;color:#94a3b8;line-height:1.8}
.pb-canvas-wrap{flex:1;overflow:auto;background:#eef2f7;background-image:radial-gradient(#dbe3ec 1px,transparent 1px);background-size:18px 18px;padding:26px;display:flex;justify-content:center;align-items:flex-start}
.pb-stage{position:relative;width:720px;min-height:480px;background:#fff;box-shadow:0 10px 40px rgba(15,23,42,.12);border-radius:6px}
.pb-el{position:absolute;box-sizing:border-box;cursor:move;user-select:none}
.pb-text{width:100%;min-height:1.4em;white-space:pre-wrap;word-break:break-word;padding:2px 4px}
.pb-img{width:100%;display:block;object-fit:cover;border-radius:4px}
.pb-text[contenteditable=true]{user-select:text;cursor:text;outline:none}
.pb-el.sel{outline:2px dashed #0ea5e9;outline-offset:2px}
.pb-handle{position:absolute;right:-7px;bottom:-7px;width:14px;height:14px;background:#0ea5e9;border:2px solid #fff;border-radius:50%;cursor:nwse-resize;display:none}
.pb-el.sel .pb-handle{display:block}
.pb-empty{font-size:13px;color:#94a3b8}
#pbPropsBody label{display:block;font-size:12px;color:#64748b;margin:10px 0 4px}
#pbPropsBody input[type=text],#pbPropsBody input[type=number],#pbPropsBody textarea,#pbPropsBody select{width:100%;border:1px solid #e2e8f0;border-radius:8px;padding:7px 9px;font-size:13px;font-family:inherit}
#pbPropsBody textarea{min-height:64px;resize:vertical}
.pb-del{margin-top:14px;width:100%;background:#fef2f2;color:#dc2626;border:1px solid #fecaca;border-radius:8px;padding:8px;cursor:pointer;font-size:13px}
.pb-del:hover{background:#fee2e2}
@media(max-width:980px){.pb-left,.pb-right{width:180px;flex-basis:180px}}
</style>
<script>
window.__PB_INIT__ = <?php echo $initial; ?>;
window.__PB_MEDIA__ = '<?php echo site_url('admin/media'); ?>';
window.__PB_UPLOAD__ = '<?php echo site_url('admin/media/upload'); ?>';
window.__PB_CSRF__ = '<?php echo csrf_token(); ?>';
window.__PB_MODULE__ = 'page';
window.__PB_DELETE__ = '<?php echo site_url('admin/media/delete/'); ?>';
</script>
<script src="<?php echo asset('js/page-builder.js'); ?>"></script>
+103
View File
@@ -0,0 +1,103 @@
<?php
/** @var array $p */
/** @var string $mode */
$v = function ($k, $d = '') use ($p) { return $p ? ($p[$k] ?? $d) : $d; };
$isEdit = !empty($p);
$mode = $mode ?? 'fixed';
$switchUrl = $isEdit ? site_url('admin/pages/switchMode/' . (int)$p['id']) : '';
?>
<div class="page-head">
<div>
<h1>编辑单页:<?php echo e($p['title']); ?><span class="mode-badge mode-fixed">固定版面</span></h1>
<div class="desc">固定版面:页面套用统一模板(标题区 + 正文 + 联系按钮),您只需编辑正文内容。</div>
</div>
<?php if ($isEdit): ?>
<div class="mode-switch-sel">
<label for="modeSel">切换模式</label>
<select id="modeSel" class="mode-sel" onchange="location.href='<?php echo $switchUrl; ?>?mode='+this.value">
<option value="fixed" <?php echo $mode === 'fixed' ? 'selected' : ''; ?>>固定版面</option>
<option value="builder" <?php echo $mode === 'builder' ? 'selected' : ''; ?>>可视化编辑</option>
</select>
</div>
<?php endif; ?>
</div>
<div class="admin-card">
<form method="post" action="<?php echo site_url('admin/pages/update/' . $p['id']); ?>" id="fxForm">
<?php echo csrf_field(); ?>
<input type="hidden" name="mode" value="fixed">
<div class="field"><label>标题</label><input name="title" value="<?php echo e($p['title']); ?>"></div>
<div class="field">
<label>正文内容</label>
<div class="fx-toolbar">
<button type="button" data-cmd="bold" title="加粗"><b>B</b></button>
<button type="button" data-cmd="italic" title="斜体"><i>I</i></button>
<button type="button" data-cmd="formatBlock" data-val="H2">H2</button>
<button type="button" data-cmd="formatBlock" data-val="H3">H3</button>
<button type="button" data-cmd="insertUnorderedList" title="无序列表">• 列表</button>
<button type="button" data-cmd="insertOrderedList" title="有序列表">1. 列表</button>
<button type="button" data-cmd="formatBlock" data-val="BLOCKQUOTE" title="引用">引用</button>
<button type="button" id="fxLink" title="插入链接">链接</button>
<button type="button" id="fxImg" title="插入图片">图片</button>
</div>
<div class="fx-editor" id="fxEditor" contenteditable="true"><?php echo $p['content']; ?></div>
<textarea name="content" id="fxContent" hidden><?php echo e($p['content']); ?></textarea>
<p class="fx-hint">直接排版正文即可;切换「可视化编辑」可自由拖拽布局。保存后页面以统一版式展示。</p>
</div>
<div class="form-actions">
<button class="btn-primary" type="submit">保存</button>
<a class="btn-ghost" href="<?php echo site_url('admin/pages'); ?>">取消</a>
</div>
</form>
</div>
<!-- 图片选择弹层 -->
<div class="fx-modal" id="fxModal" hidden>
<div class="fx-modal-box">
<div class="fx-modal-head"><span>选择图片</span><button type="button" id="fxModalClose">×</button></div>
<div class="fx-modal-body">
<label class="fx-upload"> 上传图片<input type="file" id="fxUp" accept="image/*" hidden></label>
<div class="fx-lib" id="fxLib"></div>
</div>
</div>
</div>
<style>
.mode-switch-sel{display:inline-flex;align-items:center;gap:8px}
.mode-switch-sel label{font-size:13px;color:#64748b}
.mode-sel{border:1px solid #e2e8f0;border-radius:10px;padding:8px 12px;font-size:14px;background:#fff;color:#334155;cursor:pointer;min-width:140px}
.mode-sel:focus{border-color:#0ea5e9;box-shadow:0 0 0 3px rgba(14,165,233,.12)}
.mode-badge{display:inline-block;margin-left:10px;padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600;vertical-align:middle;line-height:1.7}
.mode-badge.mode-fixed{background:#e0f2fe;color:#0369a1}
.mode-badge.mode-builder{background:#ede9fe;color:#6d28d9}
.fx-toolbar{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}
.fx-toolbar button{width:auto;min-width:38px;height:34px;padding:0 10px;border:1px solid #e2e8f0;background:#fff;border-radius:8px;cursor:pointer;font-size:14px;color:#334155}
.fx-toolbar button:hover{border-color:#0ea5e9;color:#0ea5e9}
.fx-editor{min-height:320px;border:1px solid #e2e8f0;border-radius:10px;padding:16px 18px;font-size:16px;line-height:1.9;outline:none;background:#fff;overflow:auto}
.fx-editor:focus{border-color:#0ea5e9;box-shadow:0 0 0 3px rgba(14,165,233,.12)}
.fx-editor h2{font-size:24px;margin:.6em 0 .4em}
.fx-editor h3{font-size:20px;margin:.6em 0 .4em}
.fx-editor blockquote{margin:.6em 0;padding:8px 14px;border-left:4px solid #0ea5e9;color:#475569;background:#f8fafc}
.fx-editor img{max-width:100%;border-radius:8px;display:block;margin:8px 0}
.fx-editor a{color:#0ea5e9}
.fx-hint{font-size:12px;color:#94a3b8;margin:8px 2px 0}
.fx-modal{position:fixed;inset:0;background:rgba(15,23,42,.45);display:flex;align-items:center;justify-content:center;z-index:80}
.fx-modal[hidden]{display:none}
.fx-modal-box{background:#fff;border-radius:14px;width:420px;max-width:92vw;overflow:hidden}
.fx-modal-head{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #e2e8f0;font-weight:700}
.fx-modal-head button{border:none;background:none;font-size:22px;cursor:pointer;color:#64748b;line-height:1}
.fx-modal-body{padding:16px;max-height:60vh;overflow:auto}
.fx-upload{display:block;text-align:center;background:#0ea5e9;color:#fff;border-radius:10px;padding:10px;cursor:pointer;font-weight:600;margin-bottom:12px}
.fx-lib{display:flex;flex-wrap:wrap;gap:8px}
.fx-lib img{width:72px;height:72px;object-fit:cover;border-radius:8px;cursor:pointer;border:2px solid transparent}
.fx-lib img:hover{border-color:#0ea5e9}
</style>
<script>
window.__PB_MEDIA__ = '<?php echo site_url('admin/media'); ?>';
window.__PB_UPLOAD__ = '<?php echo site_url('admin/media/upload'); ?>';
window.__PB_CSRF__ = '<?php echo csrf_token(); ?>';
</script>
<script src="<?php echo asset('js/fixed-editor.js'); ?>"></script>
+23
View File
@@ -0,0 +1,23 @@
<div class="page-head">
<div><h1>单页管理</h1><div class="desc">关于我们 / 服务优势 / 客户案例 / 荣誉资质</div></div>
</div>
<div class="admin-card">
<table class="admin-table">
<tr><th>标识</th><th>标题</th><th>模式</th><th>更新时间</th><th>操作</th></tr>
<?php foreach ($pages as $p): ?>
<?php $m = empty($p['mode']) ? 'fixed' : $p['mode']; ?>
<tr>
<td class="muted"><?php echo e($p['slug']); ?></td>
<td><b><?php echo e($p['title']); ?></b></td>
<td><span class="mode-badge <?php echo $m === 'builder' ? 'builder' : 'fixed'; ?>"><?php echo $m === 'builder' ? '可视化编辑' : '固定版面'; ?></span></td>
<td class="muted"><?php echo e(format_date($p['updated_at'])); ?></td>
<td><a class="btn-soft btn-sm" href="<?php echo site_url('admin/pages/edit/' . $p['id']); ?>">编辑</a></td>
</tr>
<?php endforeach; ?>
</table>
<style>
.mode-badge{display:inline-block;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:600}
.mode-badge.fixed{background:#f1f5f9;color:#475569}
.mode-badge.builder{background:#e0f2fe;color:#0369a1}
</style>
</div>
+60
View File
@@ -0,0 +1,60 @@
<?php
/** @var string $module */
/** @var array $layout */
$module = $module ?? 'page';
$initial = json_encode($layout ?: [], JSON_UNESCAPED_UNICODE);
$isProduct = $module === 'product';
?>
<div class="pb-workspace">
<!-- 最左:窄图标栏(仅图标) -->
<aside class="pb-rail" title="工具栏">
<button type="button" class="pb-rail-btn" data-act="material" title="素材库">🖼️</button>
<button type="button" class="pb-rail-btn" data-act="text" title="添加文字">T</button>
<button type="button" class="pb-rail-btn" data-act="image" title="添加图片">🏞️</button>
<button type="button" class="pb-rail-btn" data-act="button" title="添加按钮">🔗</button>
<?php if ($isProduct): ?>
<button type="button" class="pb-rail-btn" data-act="buy" title="购买按钮">🛒</button>
<button type="button" class="pb-rail-btn" data-act="price" title="价格">¥</button>
<button type="button" class="pb-rail-btn" data-act="specs" title="规格参数">▤</button>
<?php endif; ?>
</aside>
<!-- 中间:可编辑画布 -->
<main class="pb-canvas-wrap">
<div class="pb-stage" id="pbStage"></div>
</main>
<!-- 右栏:标签页 预览 / 属性 / 素材 -->
<aside class="pb-side">
<div class="pb-tabs">
<button type="button" class="pb-tab active" data-tab="preview">预览</button>
<button type="button" class="pb-tab" data-tab="props">属性</button>
<button type="button" class="pb-tab" data-tab="material">素材</button>
</div>
<div class="pb-panes">
<div class="pb-pane" id="panePreview">
<div class="pb-preview" id="pbPreviewWrap"><div class="pb-stage pb-front" id="pbPreview"></div></div>
<p class="pb-hint" style="padding:8px 12px;margin:0">实时模拟(只读)。在画布选中元素即跳到「属性」。</p>
</div>
<div class="pb-pane hidden" id="paneProps">
<div id="pbPropsBody" class="pb-empty">在画布中选择一个元素以编辑属性</div>
</div>
<div class="pb-pane hidden" id="paneMaterial">
<label class="pb-upload"> 上传图片
<input type="file" id="pbFile" accept="image/*" hidden>
</label>
<div class="pb-lib" id="pbLib"></div>
<p class="pb-hint">点击图片加入画布;悬停图片点 × 可删除素材。</p>
</div>
</div>
</aside>
</div>
<script>
window.__PB_INIT__ = <?php echo $initial; ?>;
window.__PB_MODULE__ = '<?php echo $module; ?>';
window.__PB_MEDIA__ = '<?php echo site_url('admin/media'); ?>';
window.__PB_UPLOAD__ = '<?php echo site_url('admin/media/upload'); ?>';
window.__PB_DELETE__ = '<?php echo site_url('admin/media/delete/'); ?>';
window.__PB_CSRF__ = '<?php echo csrf_token(); ?>';
</script>
<script src="<?php echo asset('js/page-builder.js'); ?>"></script>
+17
View File
@@ -0,0 +1,17 @@
<?php /** @var string $error */ /** @var string $ok */ ?>
<div class="page-head">
<div><h1>修改密码</h1><div class="desc">修改当前登录账号(<?php echo e($_SESSION['admin_name'] ?? ''); ?>)的密码</div></div>
</div>
<?php if ($error): ?><div class="alert alert-err"><?php echo e($error); ?></div><?php endif; ?>
<?php if ($ok): ?><div class="alert alert-ok"><?php echo e($ok); ?></div><?php endif; ?>
<div class="admin-card" style="max-width:480px">
<form method="post" action="<?php echo site_url('admin/password'); ?>">
<?php echo csrf_field(); ?>
<div class="field"><label>当前密码</label><input type="password" name="current_password" required></div>
<div class="field"><label>新密码(至少 6 位)</label><input type="password" name="new_password" required></div>
<div class="field"><label>确认新密码</label><input type="password" name="confirm_password" required></div>
<div class="form-actions">
<button class="btn-primary" type="submit">保存修改</button>
</div>
</form>
</div>
+34
View File
@@ -0,0 +1,34 @@
<?php /** @var array $v */ /** @var string $seg */ ?>
<div class="page-head"><div><h1>支付设置</h1><div class="desc">配置支付宝 / 微信支付通道;未配置商户号时默认「演示模式」,整条支付链路可正常走通。</div></div></div>
<div class="admin-card">
<form method="post" action="<?php echo site_url('admin/payments'); ?>">
<?php echo csrf_field(); ?>
<div class="form-grid">
<div class="field"><label>支付开关</label>
<select name="pay_enabled">
<option value="1" <?php echo $v['pay_enabled'] === '1' ? 'selected' : ''; ?>>开启</option>
<option value="0" <?php echo $v['pay_enabled'] === '0' ? 'selected' : ''; ?>>关闭</option>
</select>
</div>
<div class="field"><label>运行模式</label>
<select name="pay_mode">
<option value="demo" <?php echo $v['pay_mode'] === 'demo' ? 'selected' : ''; ?>>演示模式(模拟支付)</option>
<option value="live" <?php echo $v['pay_mode'] === 'live' ? 'selected' : ''; ?>>真实收款(需填下方商户号)</option>
</select>
</div>
</div>
<h3 style="margin:18px 0 8px">支付宝(电脑网站支付)</h3>
<div class="field"><label>AppID</label><input name="pay_alipay_appid" value="<?php echo e($v['pay_alipay_appid']); ?>"></div>
<div class="field"><label>应用私钥 private_key</label><textarea name="pay_alipay_private_key"><?php echo e($v['pay_alipay_private_key']); ?></textarea></div>
<div class="field"><label>支付宝公钥 public_key</label><textarea name="pay_alipay_public_key"><?php echo e($v['pay_alipay_public_key']); ?></textarea></div>
<div class="field"><label>网关地址</label><input name="pay_alipay_gateway" value="<?php echo e($v['pay_alipay_gateway']); ?>"></div>
<h3 style="margin:18px 0 8px">微信支付(NATIVE 扫码)</h3>
<div class="field"><label>商户号 mch_id</label><input name="pay_wechat_mchid" value="<?php echo e($v['pay_wechat_mchid']); ?>"></div>
<div class="field"><label>AppID</label><input name="pay_wechat_appid" value="<?php echo e($v['pay_wechat_appid']); ?>"></div>
<div class="field"><label>API 密钥 key</label><input name="pay_wechat_key" value="<?php echo e($v['pay_wechat_key']); ?>"></div>
<div style="margin-top:8px"><button class="btn-primary" type="submit">保存支付设置</button></div>
</form>
</div>
+93
View File
@@ -0,0 +1,93 @@
<?php
/** @var array $p */
/** @var array $layout */
/** @var string $mode */
$v = function ($k, $d = '') use ($p) { return $p ? ($p[$k] ?? $d) : $d; };
$isEdit = !empty($p);
$switchUrl = $isEdit ? site_url('admin/products/switchMode/' . (int)$p['id']) : '';
$initial = json_encode($layout ?: [], JSON_UNESCAPED_UNICODE);
?>
<div class="page-head">
<div>
<h1><?php echo $isEdit ? '编辑产品' : '新增产品'; ?><span class="mode-badge mode-builder">可视化编辑</span></h1>
<div class="desc">可视化编辑:拖拽文字 / 图片 / 按钮自由排版,可放置「购买 / 价格 / 规格」专用块(自动读取本产品数据)。</div>
</div>
<div class="mode-switch-sel">
<label for="modeSel">编辑模式</label>
<select id="modeSel" class="mode-sel" onchange="location.href='<?php echo $switchUrl; ?>?mode='+this.value">
<option value="fixed" <?php echo $mode === 'fixed' ? 'selected' : ''; ?>>固定版面</option>
<option value="builder" <?php echo $mode === 'builder' ? 'selected' : ''; ?>>可视化编辑</option>
</select>
</div>
</div>
<div class="admin-card">
<form id="pbForm" method="post" action="<?php echo site_url($isEdit ? 'admin/products/update/' . $p['id'] : 'admin/products/store'); ?>" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<input type="hidden" name="mode" value="builder">
<input type="hidden" name="layout" id="pbLayout">
<div class="form-grid">
<div class="field">
<label>所属分类 *</label>
<select name="category_id" required>
<?php foreach ($cats as $c): ?><option value="<?php echo e($c['id']); ?>" <?php echo $v('category_id') == $c['id'] ? 'selected' : ''; ?>><?php echo e($c['name']); ?></option><?php endforeach; ?>
</select>
</div>
<div class="field"><label>产品名称 *</label><input name="name" value="<?php echo e($v('name')); ?>" required></div>
<div class="field"><label>URL 标识(留空按序号生成)</label><input name="slug" value="<?php echo e($v('slug')); ?>" placeholder="留空则自动生成如 12"></div>
<div class="field"><label>价格(元,起)</label><input name="price" type="number" step="0.01" value="<?php echo e($v('price')); ?>"></div>
</div>
<div class="field"><label>一句话简介</label><input name="summary" value="<?php echo e($v('summary')); ?>"></div>
<div class="form-grid">
<div class="field"><label>封面图(上传,可选)</label><input type="file" name="cover" accept="image/*"></div>
<div class="field"><label>或填写图片地址/路径</label><input name="cover_url" value="<?php echo e($v('cover')); ?>" placeholder="assets/uploads/xxx.jpg 或 http(s)://"></div>
</div>
<div class="field">
<label>可视化画布(拖拽排版产品详情)</label>
<?php echo \Core\View::buffer('admin/parts/builder', ['module' => 'product', 'layout' => $layout]); ?>
</div>
<div class="field">
<label>规格参数(每行一项,格式:参数名|参数值)</label>
<textarea name="specs" placeholder="降温方式|水冷循环&#10;续航|6-8 小时"><?php echo e($specText ?? ''); ?></textarea>
</div>
<div class="form-grid">
<div class="field"><label>标签(逗号分隔)</label><input name="tags" value="<?php echo e($v('tags')); ?>"></div>
<div class="field"><label>排序(越小越靠前)</label><input name="sort_order" type="number" value="<?php echo e($v('sort_order', 0)); ?>"></div>
</div>
<div class="field" style="display:flex;align-items:center;gap:8px">
<input type="checkbox" name="status" value="1" <?php echo $v('status', 1) ? 'checked' : ''; ?> id="st"> <label for="st" style="margin:0">立即上线</label>
</div>
<div class="form-actions">
<button class="btn-primary" type="submit">保存</button>
<a class="btn-ghost" href="<?php echo site_url('admin/products'); ?>">取消</a>
</div>
</form>
</div>
<style>
.mode-switch-sel{display:inline-flex;align-items:center;gap:8px}
.mode-switch-sel label{font-size:13px;color:#64748b}
.mode-sel{border:1px solid #e2e8f0;border-radius:10px;padding:8px 12px;font-size:14px;background:#fff;color:#334155;cursor:pointer;min-width:140px}
.mode-sel:focus{border-color:#0ea5e9;box-shadow:0 0 0 3px rgba(14,165,233,.12)}
.mode-badge{display:inline-block;margin-left:10px;padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600;vertical-align:middle;line-height:1.7}
.mode-badge.mode-fixed{background:#e0f2fe;color:#0369a1}
.mode-badge.mode-builder{background:#ede9fe;color:#6d28d9}
</style>
<script>
window.__PB_INIT__ = <?php echo $initial; ?>;
window.__PB_MEDIA__ = '<?php echo site_url('admin/media'); ?>';
window.__PB_UPLOAD__ = '<?php echo site_url('admin/media/upload'); ?>';
window.__PB_CSRF__ = '<?php echo csrf_token(); ?>';
window.__PB_MODULE__ = 'product';
window.__PB_DELETE__ = '<?php echo site_url('admin/media/delete/'); ?>';
</script>
<script src="<?php echo asset('js/page-builder.js'); ?>"></script>
+199
View File
@@ -0,0 +1,199 @@
<?php
/** @var array $p */
/** @var string $mode */
$v = function ($k, $d = '') use ($p) { return $p ? ($p[$k] ?? $d) : $d; };
$isEdit = !empty($p);
$mode = $mode ?? 'fixed';
$switchUrl = $isEdit ? site_url('admin/products/switchMode/' . (int)$p['id']) : '';
$coverVal = $v('cover');
$galleryArr = [];
$g = $v('gallery');
if ($g) { $d = @json_decode($g, true); if (is_array($d)) $galleryArr = $d; }
$desc = $v('description');
?>
<div class="page-head">
<div>
<h1><?php echo $isEdit ? '编辑产品' : '新增产品'; ?><span class="mode-badge mode-fixed">固定版面</span></h1>
<div class="desc">固定版面:填写资料并上传封面 / 图集,图片自适应展示<?php echo $isEdit ? ';可切换为可视化自由排版。' : '。'; ?></div>
</div>
<?php if ($isEdit): ?>
<div class="mode-switch-sel">
<label for="modeSel">切换模式</label>
<select id="modeSel" class="mode-sel" onchange="location.href='<?php echo $switchUrl; ?>?mode='+this.value">
<option value="fixed" <?php echo $mode === 'fixed' ? 'selected' : ''; ?>>固定版面</option>
<option value="builder" <?php echo $mode === 'builder' ? 'selected' : ''; ?>>可视化编辑</option>
</select>
</div>
<?php else: ?>
<div class="mode-switch-sel">
<label for="modeSel">排版方式</label>
<select id="modeSel" class="mode-sel">
<option value="fixed" <?php echo $mode === 'fixed' ? 'selected' : ''; ?>>固定版面</option>
<option value="builder" <?php echo $mode === 'builder' ? 'selected' : ''; ?>>可视化编辑</option>
</select>
</div>
<?php endif; ?>
</div>
<div class="admin-card">
<form id="fxForm" method="post" action="<?php echo site_url($isEdit ? 'admin/products/update/' . $p['id'] : 'admin/products/store'); ?>" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<input type="hidden" name="mode" value="fixed">
<div class="form-grid">
<div class="field">
<label>所属分类 *</label>
<select name="category_id" required>
<?php foreach ($cats as $c): ?><option value="<?php echo e($c['id']); ?>" <?php echo $v('category_id') == $c['id'] ? 'selected' : ''; ?>><?php echo e($c['name']); ?></option><?php endforeach; ?>
</select>
</div>
<div class="field"><label>产品名称 *</label><input name="name" value="<?php echo e($v('name')); ?>" required></div>
<div class="field"><label>URL 标识(留空按序号生成,短而稳定)</label><input name="slug" value="<?php echo e($v('slug')); ?>" placeholder="留空则自动生成如 12"></div>
<div class="field"><label>价格(元,起)</label><input name="price" type="number" step="0.01" value="<?php echo e($v('price')); ?>"></div>
</div>
<div class="field"><label>一句话简介</label><input name="summary" value="<?php echo e($v('summary')); ?>"></div>
<div class="field">
<label>封面图(上传,建议 4:3,将作为列表/详情主图)</label>
<input type="file" name="cover" accept="image/*">
<?php if ($coverVal): ?>
<div class="img-prev"><img src="<?php echo e(site_url($coverVal)); ?>" alt=""><span class="img-prev-path"><?php echo e($coverVal); ?></span></div>
<p class="fx-hint">已上传封面;重新选择文件将替换,留空则保留。</p>
<?php endif; ?>
<input type="text" name="cover_url" value="<?php echo e($coverVal); ?>" placeholder="或填写图片地址/路径 assets/uploads/xxx.jpg 或 http(s)://" style="margin-top:8px">
</div>
<div class="field fx-fixed-only">
<label>图集(可一次选择多张,每张都会单独上传)</label>
<input type="file" name="gallery[]" accept="image/*" multiple>
<?php if (!empty($galleryArr)): ?>
<div class="gallery-prev">
<?php foreach ($galleryArr as $gp): ?><div class="gp-item"><img src="<?php echo e(site_url($gp)); ?>" alt=""><span><?php echo e($gp); ?></span></div><?php endforeach; ?>
</div>
<label class="chk-inline"><input type="checkbox" name="clear_gallery" value="1"> 清空现有图集(重新上传)</label>
<?php endif; ?>
</div>
<div class="field fx-fixed-only">
<label>详细描述(富文本,每个图片均可上传,前台自适应展示)</label>
<div class="fx-toolbar">
<button type="button" data-cmd="bold" title="加粗"><b>B</b></button>
<button type="button" data-cmd="italic" title="斜体"><i>I</i></button>
<button type="button" data-cmd="formatBlock" data-val="H2">H2</button>
<button type="button" data-cmd="formatBlock" data-val="H3">H3</button>
<button type="button" data-cmd="insertUnorderedList" title="无序列表">• 列表</button>
<button type="button" data-cmd="insertOrderedList" title="有序列表">1. 列表</button>
<button type="button" data-cmd="formatBlock" data-val="BLOCKQUOTE" title="引用">引用</button>
<button type="button" id="fxLink" title="插入链接">链接</button>
<button type="button" id="fxImg" title="插入图片">图片</button>
</div>
<div class="fx-editor" id="fxEditor" contenteditable="true"><?php echo $desc; ?></div>
<textarea name="description" id="fxContent" hidden><?php echo e($desc); ?></textarea>
<p class="fx-hint">直接排版正文即可;点击「图片」可从素材库选择或上传新图。保存后前台固定版式展示。</p>
</div>
<div class="field">
<label>规格参数(每行一项,格式:参数名|参数值)</label>
<textarea name="specs" placeholder="降温方式|水冷循环&#10;续航|6-8 小时"><?php echo e($specText ?? ''); ?></textarea>
</div>
<div class="form-grid">
<div class="field"><label>标签(逗号分隔)</label><input name="tags" value="<?php echo e($v('tags')); ?>"></div>
<div class="field"><label>排序(越小越靠前)</label><input name="sort_order" type="number" value="<?php echo e($v('sort_order', 0)); ?>"></div>
</div>
<div class="field" style="display:flex;align-items:center;gap:8px">
<input type="checkbox" name="status" value="1" <?php echo $v('status', 1) ? 'checked' : ''; ?> id="st"> <label for="st" style="margin:0">立即上线</label>
</div>
<div class="form-actions">
<button class="btn-primary" type="submit">保存</button>
<a class="btn-ghost" href="<?php echo site_url('admin/products'); ?>">取消</a>
</div>
</form>
</div>
<!-- 图片选择弹层 -->
<div class="fx-modal" id="fxModal" hidden>
<div class="fx-modal-box">
<div class="fx-modal-head"><span>选择图片</span><button type="button" id="fxModalClose">×</button></div>
<div class="fx-modal-body">
<label class="fx-upload"> 上传图片<input type="file" id="fxUp" accept="image/*" hidden></label>
<div class="fx-lib" id="fxLib"></div>
</div>
</div>
</div>
<style>
.mode-switch-sel{display:inline-flex;align-items:center;gap:8px}
.mode-switch-sel label{font-size:13px;color:#64748b}
.mode-sel{border:1px solid #e2e8f0;border-radius:10px;padding:8px 12px;font-size:14px;background:#fff;color:#334155;cursor:pointer;min-width:140px}
.mode-sel:focus{border-color:#0ea5e9;box-shadow:0 0 0 3px rgba(14,165,233,.12)}
.mode-badge{display:inline-block;margin-left:10px;padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600;vertical-align:middle;line-height:1.7}
.mode-badge.mode-fixed{background:#e0f2fe;color:#0369a1}
.mode-badge.mode-builder{background:#ede9fe;color:#6d28d9}
.img-prev{display:flex;align-items:center;gap:10px;margin-top:10px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:8px 10px}
.img-prev img{width:72px;height:54px;object-fit:cover;border-radius:8px;display:block}
.img-prev-path{font-size:12px;color:#94a3b8;word-break:break-all}
.gallery-prev{display:flex;flex-wrap:wrap;gap:10px;margin-top:10px}
.gallery-prev .gp-item{width:104px;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden;background:#fff}
.gallery-prev .gp-item img{width:100%;height:74px;object-fit:cover;display:block}
.gallery-prev .gp-item span{display:block;font-size:11px;color:#94a3b8;padding:4px 6px;word-break:break-all;line-height:1.3}
.chk-inline{display:inline-flex;align-items:center;gap:6px;margin-top:10px;font-size:13px;color:#64748b;cursor:pointer}
.fx-toolbar{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}
.fx-toolbar button{width:auto;min-width:38px;height:34px;padding:0 10px;border:1px solid #e2e8f0;background:#fff;border-radius:8px;cursor:pointer;font-size:14px;color:#334155}
.fx-toolbar button:hover{border-color:#0ea5e9;color:#0ea5e9}
.fx-editor{min-height:260px;border:1px solid #e2e8f0;border-radius:10px;padding:16px 18px;font-size:16px;line-height:1.9;outline:none;background:#fff;overflow:auto}
.fx-editor:focus{border-color:#0ea5e9;box-shadow:0 0 0 3px rgba(14,165,233,.12)}
.fx-editor h2{font-size:24px;margin:.6em 0 .4em}
.fx-editor h3{font-size:20px;margin:.6em 0 .4em}
.fx-editor blockquote{margin:.6em 0;padding:8px 14px;border-left:4px solid #0ea5e9;color:#475569;background:#f8fafc}
.fx-editor img{max-width:100%;height:auto;border-radius:8px;display:block;margin:8px 0}
.fx-editor a{color:#0ea5e9}
.fx-hint{font-size:12px;color:#94a3b8;margin:8px 2px 0}
.fx-modal{position:fixed;inset:0;background:rgba(15,23,42,.45);display:flex;align-items:center;justify-content:center;z-index:80}
.fx-modal[hidden]{display:none}
.fx-modal-box{background:#fff;border-radius:14px;width:420px;max-width:92vw;overflow:hidden}
.fx-modal-head{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #e2e8f0;font-weight:700}
.fx-modal-head button{border:none;background:none;font-size:22px;cursor:pointer;color:#64748b;line-height:1}
.fx-modal-body{padding:16px;max-height:60vh;overflow:auto}
.fx-upload{display:block;text-align:center;background:#0ea5e9;color:#fff;border-radius:10px;padding:10px;cursor:pointer;font-weight:600;margin-bottom:12px}
.fx-lib{display:flex;flex-wrap:wrap;gap:8px}
.fx-lib img{width:72px;height:72px;object-fit:cover;border-radius:8px;cursor:pointer;border:2px solid transparent}
.fx-lib img:hover{border-color:#0ea5e9}
</style>
<script>
window.__PB_MEDIA__ = '<?php echo site_url('admin/media'); ?>';
window.__PB_UPLOAD__ = '<?php echo site_url('admin/media/upload'); ?>';
window.__PB_CSRF__ = '<?php echo csrf_token(); ?>';
</script>
<script src="<?php echo asset('js/fixed-editor.js'); ?>"></script>
<script>
// 新增产品时按「排版方式」选择器联动:选可视化编辑则隐藏图集/详细描述,并提示保存后进入可视化编辑器
(function(){
var sel = document.getElementById('modeSel');
if(!sel) return;
var isCreate = <?php echo $isEdit ? 'false' : 'true'; ?>;
var modeField = document.querySelector('#fxForm input[name="mode"]');
var fixedOnly = document.querySelectorAll('.fx-fixed-only');
var note = document.createElement('p');
note.className = 'fx-hint fx-builder-note';
note.style.cssText = 'margin-top:10px;color:#6d28d9;background:#f5f3ff;border:1px solid #ddd6fe;border-radius:8px;padding:8px 12px;display:none';
note.textContent = '已选择「可视化编辑」:图集与详细描述无需填写,保存后将进入可视化编辑器排版产品详情。';
var firstFixed = fixedOnly[0];
if(firstFixed && firstFixed.parentNode){ firstFixed.parentNode.insertBefore(note, firstFixed); }
function apply(){
var m = sel.value;
if(modeField) modeField.value = m;
if(!isCreate) return; // 编辑页选择器仅用于切换页面,不在此隐藏字段
var isBuilder = (m === 'builder');
for(var i=0;i<fixedOnly.length;i++){ fixedOnly[i].style.display = isBuilder ? 'none' : ''; }
note.style.display = isBuilder ? 'block' : 'none';
}
sel.addEventListener('change', apply);
apply();
})();
</script>
+36
View File
@@ -0,0 +1,36 @@
<?php
$cm = new \App\Models\Category();
$cmap = []; foreach ($cm->all() as $c) $cmap[$c['id']] = $c['name'];
?>
<div class="page-head">
<div><h1>产品管理</h1><div class="desc">管理降温服产品与上下架</div></div>
<a class="btn-primary" href="<?php echo site_url('admin/products/create'); ?>">+ 新增产品</a>
</div>
<div class="admin-card">
<table class="admin-table">
<tr><th>封面</th><th>名称</th><th>分类</th><th>价格</th><th>模式</th><th>状态</th><th>操作</th></tr>
<?php foreach ($products as $p): ?>
<?php $pm = empty($p['mode']) ? 'fixed' : $p['mode']; ?>
<tr>
<td><?php if (!empty($p['cover'])): ?><div class="thum" style="background:#fff"><img src="<?php echo e(site_url($p['cover'])); ?>" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:10px"></div><?php else: ?><div class="thum" style="background:<?php echo gradient($p['id']); ?>">❄</div><?php endif; ?></td>
<td><b><?php echo e($p['name']); ?></b><br><span class="muted" style="font-size:12px"><?php echo e($p['slug']); ?></span></td>
<td><?php echo e($cmap[$p['category_id']] ?? '—'); ?></td>
<td>¥<?php echo e($p['price']); ?></td>
<td><span class="mode-badge <?php echo $pm === 'builder' ? 'builder' : 'fixed'; ?>"><?php echo $pm === 'builder' ? '可视化编辑' : '固定版面'; ?></span></td>
<td><?php echo ($p['status'] ?? 1) ? '<span class="tag-mini">已上线</span>' : '<span class="muted">草稿</span>'; ?></td>
<td>
<div class="row-actions">
<a class="btn-soft btn-sm" href="<?php echo site_url('admin/products/edit/' . $p['id']); ?>">编辑</a>
<form class="del-form" method="post" action="<?php echo site_url('admin/products/delete/' . $p['id']); ?>"><?php echo csrf_field(); ?><button class="btn-danger btn-sm" type="submit">删除</button></form>
</div>
</td>
</tr>
<?php endforeach; ?>
</table>
<style>
.mode-badge{display:inline-block;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:600}
.mode-badge.fixed{background:#f1f5f9;color:#475569}
.mode-badge.builder{background:#e0f2fe;color:#0369a1}
</style>
</div>
+76
View File
@@ -0,0 +1,76 @@
<?php
/**
* 后台:SEO 设置 —— 页面清单
* 每个页面独立编辑,点击卡片进入 admin/seo/edit/{key}。
*/
$pages = $pages ?? [];
$rows = $rows ?? [];
function seo_len(string $s): int { return mb_strlen($s, 'UTF-8'); }
function seo_kwcount(string $s): int {
if ($s === '') return 0;
return count(array_filter(array_map('trim', explode(',', $s)), function ($x) { return $x !== ''; }));
}
function seo_badge(int $n, int $min, string $emptyTxt): string {
if ($n === 0) return '<span class="badge badge-none">' . $emptyTxt . '</span>';
$cls = $n >= $min ? 'badge-ok' : 'badge-warn';
return '<span class="badge ' . $cls . '">' . $n . ($min > 0 ? '(≥' . $min . '' : '') . '</span>';
}
?>
<div class="page-head">
<div>
<h1>SEO 设置</h1>
<p class="muted">逐页维护搜索结果展示信息。点击任一页面进入独立编辑页,带实时字数提示。</p>
</div>
<div class="page-head-actions">
<a class="btn btn-ghost" href="<?= e(site_url('admin/seo/edit/home')) ?>">编辑首页 SEO</a>
<a class="btn btn-ghost" href="<?= e(site_url('sitemap.xml')) ?>" target="_blank" rel="noopener">查看 sitemap.xml</a>
</div>
</div>
<?= flash_html() ?>
<div class="seo-grid">
<?php foreach ($pages as $key => $meta):
$v = $rows[$key] ?? [];
$title = $v['title'] ?? '';
$desc = $v['description'] ?? '';
$tLen = seo_len($title);
$dLen = seo_len($desc);
$kwN = seo_kwcount($v['keywords'] ?? '');
$noindex = !empty($v['noindex']);
?>
<a class="seo-card" href="<?= e(site_url('admin/seo/edit/' . $key)) ?>">
<div class="seo-card-head">
<span class="seo-card-title"><?= e($meta['label']) ?></span>
<code class="mono seo-card-key"><?= e($key) ?></code>
</div>
<div class="seo-card-badges">
<?= seo_badge($tLen, $meta['title_min'], '标题未设置') ?>
<?= seo_badge($dLen, $meta['desc_min'], '描述未设置') ?>
<?= seo_badge($kwN, $meta['kw_min'], '无关键词') ?>
<?php if ($noindex): ?><span class="badge badge-noidx">noindex</span><?php endif; ?>
</div>
<div class="seo-card-preview"><?= e($title ?: '(未设置标题)') ?></div>
<div class="seo-card-desc"><?= e($desc ?: '(未设置描述)') ?></div>
<div class="seo-card-go">编辑此页 </div>
</a>
<?php endforeach; ?>
</div>
<style>
.seo-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
@media (max-width: 820px) { .seo-grid { grid-template-columns: 1fr; } }
.seo-card { display: block; background: #fff; border: 1px solid #e6eaf0; border-radius: 14px; padding: 16px 18px; text-decoration: none; color: inherit; transition: border-color .15s, box-shadow .2s, transform .12s; }
.seo-card:hover { border-color: #7dd3fc; box-shadow: 0 12px 28px -18px rgba(14,165,233,.7); transform: translateY(-2px); }
.seo-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
.seo-card-title { font-size: 16px; font-weight: 800; }
.seo-card-key { background: #f1f5f9; color: #64748b; padding: 2px 8px; border-radius: 7px; font-size: 12px; }
.seo-card-badges { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
.seo-card-preview { font-size: 14px; font-weight: 700; color: #0f172a; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.seo-card-desc { font-size: 13px; color: #64748b; margin-top: 4px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.seo-card-go { margin-top: 12px; font-size: 13px; font-weight: 700; color: #0ea5e9; }
.badge-ok { background: #dcfce7; color: #15803d; }
.badge-warn { background: #fef3c7; color: #b45309; }
.badge-none { background: #f1f5f9; color: #94a3b8; }
.badge-noidx { background: #fee2e2; color: #dc2626; }
</style>
+128
View File
@@ -0,0 +1,128 @@
<?php
/**
* 后台:SEO 单页编辑(admin/seo/edit/{key}
* 仅编辑一个页面的 标题/描述/关键词/Open Graph/规范链接/收录开关,带实时字数提示。
*/
$key = $key ?? '';
$meta = $meta ?? [];
$v = $v ?? [];
$ogTypes = ['website' => 'website(普通页)', 'article' => 'article(文章/资讯)', 'product' => 'product(商品)'];
$idp = preg_replace('/[^a-z0-9]/', '', $key);
?>
<div class="page-head">
<div>
<h1>SEO 设置 · <?= e($meta['label'] ?? $key) ?></h1>
<p class="muted">
页面标识 <code class="mono"><?= e($key) ?></code>
<?php if (!empty($meta['note'])): ?>· <b style="color:#b45309"><?= e($meta['note']) ?></b><?php endif; ?>
</p>
</div>
<div class="page-head-actions">
<a class="btn btn-ghost" href="<?= e(site_url('admin/seo')) ?>"> 返回列表</a>
</div>
</div>
<?= flash_html() ?>
<form method="POST" action="<?= e(site_url('admin/seo/edit/' . $key)) ?>" class="seo-edit-form">
<?= csrf_field() ?>
<div class="card">
<div class="card-head"><h3>搜索结果展示(SERP</h3></div>
<div class="form-grid">
<div class="form-group" style="grid-column:1 / -1;">
<label class="label" for="t_<?= $idp ?>">页面标题(Title</label>
<input class="form-control" id="t_<?= $idp ?>" name="title"
value="<?= e($v['title'] ?? '') ?>" data-count data-min="<?= $meta['title_min'] ?? 0 ?>">
<div class="field-hint">
建议 35–60 字符(含品牌名更好);当前 <span class="counter" id="c_t_<?= $idp ?>"></span>
</div>
</div>
<div class="form-group" style="grid-column:1 / -1;">
<label class="label" for="d_<?= $idp ?>">页面描述(Description</label>
<textarea class="form-control" id="d_<?= $idp ?>" name="description" rows="3"
data-count data-min="<?= $meta['desc_min'] ?? 0 ?>"><?= e($v['description'] ?? '') ?></textarea>
<div class="field-hint">
建议 120160 字符;当前 <span class="counter" id="c_d_<?= $idp ?>"></span>
</div>
</div>
<div class="form-group" style="grid-column:1 / -1;">
<label class="label" for="k_<?= $idp ?>">关键词(Keywords,英文逗号分隔)</label>
<input class="form-control" id="k_<?= $idp ?>" name="keywords"
value="<?= e($v['keywords'] ?? '') ?>" data-kw data-min="<?= $meta['kw_min'] ?? 0 ?>">
<div class="field-hint">
建议 <?= $meta['kw_min'] ?? 0 ?>+ 个;当前 <span class="counter" id="c_k_<?= $idp ?>"></span>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-head"><h3>社交分享(Open Graph / Twitter</h3></div>
<div class="form-grid">
<div class="form-group">
<label class="label" for="ot_<?= $idp ?>">分享标题(OG Title</label>
<input class="form-control" id="ot_<?= $idp ?>" name="og_title"
value="<?= e($v['og_title'] ?? '') ?>" placeholder="留空则用页面标题">
</div>
<div class="form-group">
<label class="label" for="oty_<?= $idp ?>">分享类型(OG Type</label>
<select class="form-control" id="oty_<?= $idp ?>" name="og_type">
<?php foreach ($ogTypes as $tv => $tl): ?>
<option value="<?= e($tv) ?>" <?= ($v['og_type'] ?? 'website') === $tv ? 'selected' : '' ?>><?= e($tl) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="grid-column:1 / -1;">
<label class="label" for="od_<?= $idp ?>">分享描述(OG Description</label>
<textarea class="form-control" id="od_<?= $idp ?>" name="og_description" rows="2"
placeholder="留空则用页面描述"><?= e($v['og_description'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label class="label" for="oi_<?= $idp ?>">分享图(OG Image</label>
<input class="form-control" id="oi_<?= $idp ?>" name="og_image"
value="<?= e($v['og_image'] ?? '') ?>" placeholder="留空则用站点默认分享图">
</div>
<div class="form-group">
<label class="label" for="ca_<?= $idp ?>">规范链接(Canonical</label>
<input class="form-control" id="ca_<?= $idp ?>" name="canonical"
value="<?= e($v['canonical'] ?? '') ?>" placeholder="留空则自动取当前 URL">
</div>
<div class="form-group" style="grid-column:1 / -1;">
<label class="checkbox">
<input type="checkbox" name="noindex" value="1" <?= !empty($v['noindex']) ? 'checked' : '' ?>>
<span>禁止搜索引擎收录此页面(输出 noindex 标签)</span>
</label>
</div>
</div>
</div>
<div class="form-actions">
<button class="btn btn-primary" type="submit">保存「<?= e($meta['label'] ?? $key) ?>」SEO 设置</button>
<a class="btn btn-ghost" href="<?= e(site_url('admin/seo')) ?>">返回列表</a>
</div>
</form>
<script>
document.querySelectorAll('[data-count],[data-kw]').forEach(function (el) {
var min = parseInt(el.dataset.min || '0', 10);
var out = document.getElementById('c_' + el.id);
if (!out) return;
function upd() {
var val = el.value;
var n, unit;
if (el.dataset.kw !== undefined) {
var arr = val.split(',').map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; });
n = arr.length; unit = '个关键词';
} else {
n = Array.from(val).length; unit = '字符';
}
out.textContent = n + ' ' + unit + (min > 0 ? '(建议≥' + min + '' : '');
out.style.color = n === 0 ? '#dc2626' : (n >= min ? '#16a34a' : '#d97706');
}
el.addEventListener('input', upd);
upd();
});
</script>
+30
View File
@@ -0,0 +1,30 @@
<div class="page-head">
<div><h1>站点设置</h1><div class="desc">网站名称、联系方式与 SEO 信息</div></div>
</div>
<div class="admin-card">
<form method="post" action="<?php echo site_url('admin/settings'); ?>" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<div class="form-grid">
<div class="field"><label>网站名称</label><input name="site_name" value="<?php echo e($v['site_name']); ?>"></div>
<div class="field"><label>网站标语</label><input name="site_slogan" value="<?php echo e($v['site_slogan']); ?>"></div>
<div class="field"><label>联系电话</label><input name="contact_phone" value="<?php echo e($v['contact_phone']); ?>"></div>
<div class="field"><label>联系邮箱</label><input name="contact_email" value="<?php echo e($v['contact_email']); ?>"></div>
</div>
<div class="field"><label>联系地址</label><input name="contact_address" value="<?php echo e($v['contact_address']); ?>"></div>
<div class="field" style="grid-column:1/-1">
<label>网站 Logo(酷冰甲商标)</label>
<input type="file" name="logo" accept="image/*">
<small style="color:#888;display:block;margin-top:4px">选择图片上传将替换当前 Logo;也可在下方直接填写图片路径或网址(留空则保留现有)。当前:<?php echo e($v['site_logo'] ?: '默认商标'); ?></small>
<input type="text" name="logo_url" value="<?php echo e($v['site_logo']); ?>" placeholder="图片路径或网址,如 assets/img/logo.png 或 https://...">
</div>
<div class="field"><label>备案号(ICP</label><input name="icp" value="<?php echo e($v['icp']); ?>" placeholder="如 苏ICP备XXXXXXXX号"></div>
<div class="field"><label>公安备案号(网安备)</label><input name="gongan" value="<?php echo e($v['gongan'] ?? ''); ?>" placeholder="如 京公网安备11010802012345号"></div>
<hr style="border:none;border-top:1px solid #eef2f7;margin:18px 0">
<div class="field"><label>SEO 标题</label><input name="seo_title" value="<?php echo e($v['seo_title']); ?>"></div>
<div class="field"><label>SEO 关键词</label><input name="seo_keywords" value="<?php echo e($v['seo_keywords']); ?>"></div>
<div class="field"><label>SEO 描述</label><textarea name="seo_description"><?php echo e($v['seo_description']); ?></textarea></div>
<div class="form-actions">
<button class="btn-primary" type="submit">保存设置</button>
</div>
</form>
</div>
+48
View File
@@ -0,0 +1,48 @@
<?php /** @var string $seg */
$phpVer = phpversion();
$sapi = php_sapi_name();
$dbDriver = \Core\Db::driver();
$dbVer = $dbDriver === 'mysql' ? \Core\Db::pdo()->query("SELECT VERSION()")->fetchColumn() : '—';
$tz = date_default_timezone_get();
$storageWritable = is_writable(BASE_PATH . '/storage');
$uploadWritable = is_writable(BASE_PATH . '/public/uploads');
$installed = \Core\Installer::isInstalled();
$info = [
'PHP 版本' => $phpVer,
'运行模式' => $sapi,
'数据库驱动' => $dbDriver === 'mysql' ? 'MySQL' : $dbDriver,
'数据库版本' => $dbVer,
'系统时区' => $tz,
'站点已安装' => $installed ? '是' : '否',
'根目录' => BASE_PATH,
'存储目录可写' => $storageWritable ? '✓ 可写' : '✗ 不可写',
'上传目录可写' => $uploadWritable ? '✓ 可写' : '✗ 不可写',
];
?>
<div class="page-head">
<div><h1>系统设置</h1><div class="desc">查看运行环境与数据库维护入口。</div></div>
</div>
<div class="admin-card">
<h3 class="card-title">运行环境</h3>
<table class="tbl tbl-info">
<tbody>
<?php foreach ($info as $k => $v): ?>
<tr><th><?php echo e($k); ?></th><td class="mono"><?php echo e($v); ?></td></tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="shortcut-grid">
<a class="shortcut-card" href="<?php echo site_url('admin/db'); ?>">
<span class="shortcut-ic"><?php echo admin_icon('database', 26); ?></span>
<span class="shortcut-title">数据库管理</span>
<span class="shortcut-desc">查看并编辑所有表的数据</span>
</a>
<a class="shortcut-card" href="<?php echo site_url('admin/upgrade'); ?>">
<span class="shortcut-ic"><?php echo admin_icon('upload', 26); ?></span>
<span class="shortcut-title">数据库升级</span>
<span class="shortcut-desc">执行升级包并留痕记录</span>
</a>
</div>
+90
View File
@@ -0,0 +1,90 @@
<div class="page-head">
<div><h1>🎨 风格设置</h1><div class="desc">一键设置任意网页风格:配色 / 字体 / 圆角 / 导航 / 明暗 / 自定义 CSS,保存后立即生效</div></div>
</div>
<div class="admin-card">
<form method="post" action="<?php echo site_url('admin/theme'); ?>">
<?php echo csrf_field(); ?>
<div class="form-grid">
<div class="field">
<label>预设风格(选择后自动填充下方配色)</label>
<select id="preset" name="preset">
<?php foreach ($presets as $key => $p): ?>
<option value="<?php echo e($key); ?>" <?php echo $v['preset'] == $key ? 'selected' : ''; ?>><?php echo e($p['label']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="field">
<label>导航栏样式</label>
<select name="header">
<option value="center" <?php echo $v['header'] == 'center' ? 'selected' : ''; ?>>居中Logo</option>
<option value="left" <?php echo $v['header'] == 'left' ? 'selected' : ''; ?>>左对齐</option>
<option value="transparent" <?php echo $v['header'] == 'transparent' ? 'selected' : ''; ?>>透明浮层</option>
</select>
</div>
</div>
<h3 style="margin:8px 0 12px">品牌配色</h3>
<div class="form-grid">
<div class="field"><label>主色 Primary</label><input type="color" id="f_primary" name="primary" value="<?php echo e($v['primary']); ?>"></div>
<div class="field"><label>主色深 Primary-600</label><input type="color" id="f_primary_600" name="primary_600" value="<?php echo e($v['primary_600']); ?>"></div>
<div class="field"><label>辅色 Secondary</label><input type="color" id="f_secondary" name="secondary" value="<?php echo e($v['secondary']); ?>"></div>
<div class="field"><label>强调色 Accent</label><input type="color" id="f_accent" name="accent" value="<?php echo e($v['accent']); ?>"></div>
</div>
<h3 style="margin:8px 0 12px">界面底色(浅色模式)</h3>
<div class="form-grid">
<div class="field"><label>背景 Background</label><input type="color" id="f_bg" name="bg" value="<?php echo e($v['bg']); ?>"></div>
<div class="field"><label>卡片 Surface</label><input type="color" id="f_surface" name="surface" value="<?php echo e($v['surface']); ?>"></div>
<div class="field"><label>正文文字 Text</label><input type="color" id="f_text" name="text" value="<?php echo e($v['text']); ?>"></div>
<div class="field"><label>次要文字 Muted</label><input type="color" id="f_muted" name="muted" value="<?php echo e($v['muted']); ?>"></div>
<div class="field"><label>边框 Border</label><input type="color" id="f_border" name="border" value="<?php echo e($v['border']); ?>"></div>
<div class="field"><label>导航背景(RGBA</label><input id="f_nav_bg" name="nav_bg" value="<?php echo e($v['nav_bg']); ?>"></div>
</div>
<h3 style="margin:8px 0 12px">排版与布局</h3>
<div class="form-grid">
<div class="field">
<label>字体</label>
<select id="f_font" name="font">
<option value="'Noto Sans SC', system-ui, sans-serif" <?php echo strpos($v['font'], 'Noto Sans SC') !== false ? 'selected' : ''; ?>>思源黑体(默认)</option>
<option value="system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif" <?php echo strpos($v['font'], 'Segoe UI') !== false ? 'selected' : ''; ?>>系统无衬线</option>
<option value="'Microsoft YaHei', system-ui, sans-serif" <?php echo strpos($v['font'], 'YaHei') !== false ? 'selected' : ''; ?>>微软雅黑</option>
<option value="Georgia, 'Times New Roman', serif" <?php echo strpos($v['font'], 'Georgia') !== false ? 'selected' : ''; ?>>衬线 Serif</option>
</select>
</div>
<div class="field"><label>圆角(px</label><input type="number" id="f_radius" name="radius" value="<?php echo e($v['radius']); ?>"></div>
<div class="field"><label>容器宽度(px</label><input type="number" id="f_container" name="container" value="<?php echo e($v['container']); ?>"></div>
<div class="field">
<label>默认明暗</label>
<select id="f_default_mode" name="default_mode">
<option value="light" <?php echo $v['default_mode'] == 'light' ? 'selected' : ''; ?>>浅色</option>
<option value="dark" <?php echo $v['default_mode'] == 'dark' ? 'selected' : ''; ?>>深色</option>
</select>
</div>
</div>
<h3 style="margin:8px 0 12px">自定义 CSS(高级)</h3>
<div class="field">
<label>将追加到主题样式末尾,可覆盖任意样式</label>
<textarea name="custom_css" style="font-family:monospace;min-height:120px" placeholder=".site-header{box-shadow:none;}"><?php echo e($v['custom_css']); ?></textarea>
</div>
<div class="preview-box" id="themePreview">
<div class="pv-title" style="font-size:20px;font-weight:800"></div>
<div class="pv-sub" style="opacity:.9;margin-top:6px"></div>
<div style="margin-top:16px;display:flex;gap:10px">
<span style="background:#fff;color:#0ea5e9;padding:8px 16px;border-radius:10px;font-weight:700">主按钮</span>
<span style="background:rgba(255,255,255,.25);padding:8px 16px;border-radius:10px;font-weight:700">次按钮</span>
</div>
</div>
<div class="form-actions" style="margin-top:18px">
<button class="btn-primary" type="submit">保存并应用风格</button>
<a class="btn-ghost" href="<?php echo site_url(); ?>" target="_blank">查看前台效果</a>
</div>
</form>
</div>
<script>window.__presets__ = <?php echo json_encode($presets, JSON_UNESCAPED_UNICODE); ?>;</script>
+105
View File
@@ -0,0 +1,105 @@
<?php /** @var string $driver @var array $list @var int $pending @var array $history */ ?>
<div class="page-head">
<div>
<h1>数据库升级</h1>
<div class="desc">将升级包(<code>*.sql</code>)放入 <code>install/upgrades/</code> 目录后,系统自动提示可升级;每次升级均留痕记录。</div>
</div>
<a class="btn-soft" href="<?php echo site_url('admin/db'); ?>"><?php echo admin_icon('database', 16); ?> 数据库管理</a>
</div>
<?php if (($driver ?? '') === 'file'): ?>
<div class="admin-card">
<div class="alert alert-warn">当前为文件存储模式,不支持 SQL 升级。请切换到 MySQL 后使用本功能。</div>
</div>
<?php else: ?>
<?php if ($pending > 0): ?>
<div class="alert alert-upgrade">
<div class="alert-upgrade-main">
<span class="alert-upgrade-ic"><?php echo admin_icon('upload', 22); ?></span>
<div>
<b>发现 <?php echo (int)$pending; ?> 个可升级数据库脚本</b>
<div class="muted">执行后将在「升级记录」中留痕,可随时追溯。</div>
</div>
</div>
<a class="btn-primary" href="<?php echo site_url('admin/upgrade/applyall'); ?>"
data-confirm="确认执行全部待升级脚本?建议升级前先备份数据库。">⬆️ 立即全部升级</a>
</div>
<?php else: ?>
<div class="alert alert-ok">✓ 数据库已是最新,没有待升级的脚本。</div>
<?php endif; ?>
<div class="admin-card">
<h3 class="card-title">升级包(install/upgrades/</h3>
<?php if (empty($list)): ?>
<div class="muted" style="padding:12px 0">暂无升级包。将 <code>*.sql</code> 文件放入 <code>install/upgrades/</code> 目录即可在此处升级。</div>
<?php else: ?>
<div class="tbl-wrap">
<table class="tbl">
<thead>
<tr><th>文件名</th><th class="num">大小</th><th>修改时间</th><th>状态</th><th class="num">操作</th></tr>
</thead>
<tbody>
<?php foreach ($list as $it): ?>
<tr>
<td class="mono"><?php echo e($it['name']); ?></td>
<td class="num"><?php echo number_format($it['size']); ?> B</td>
<td class="muted"><?php echo date('Y-m-d H:i', $it['mtime']); ?></td>
<td>
<?php if ($it['state'] === 'done'): ?><span class="tag-mini tag-green">已应用</span>
<?php elseif ($it['state'] === 'changed'): ?><span class="tag-mini tag-amber">内容已变更</span>
<?php else: ?><span class="tag-mini tag-blue">待升级</span><?php endif; ?>
</td>
<td class="num">
<?php if ($it['state'] !== 'done'): ?>
<a class="link-ok" href="<?php echo site_url('admin/upgrade/apply/' . rawurlencode($it['name'])); ?>"
data-confirm="确认执行该升级包?建议先备份数据库。">升级</a>
<?php else: ?><span class="muted">—</span><?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<div class="admin-card">
<h3 class="card-title">升级记录(db_upgrades</h3>
<?php if (empty($history)): ?>
<div class="muted" style="padding:12px 0">暂无升级记录。</div>
<?php else: ?>
<div class="tbl-wrap">
<table class="tbl">
<thead>
<tr><th>文件名</th><th>操作人</th><th>执行时间</th><th>文件指纹(MD5)</th></tr>
</thead>
<tbody>
<?php foreach ($history as $h): ?>
<tr>
<td class="mono"><?php echo e($h['file']); ?></td>
<td><?php echo e($h['applied_by'] ?? ''); ?></td>
<td class="muted"><?php echo e($h['applied_at'] ?? ''); ?></td>
<td class="mono muted" title="<?php echo e($h['hash']); ?>"><?php echo e(substr($h['hash'], 0, 12)); ?>…</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<div class="admin-card">
<h3 class="card-title">基础初始化工具</h3>
<p class="muted" style="line-height:1.8">
重新执行基础表结构与种子数据补齐(<code>install/schema.sql</code> / <code>install/seed.php</code>)。
该操作会补齐缺失的新模块表 / 列,并保留已有订单与管理员账号,但不会覆盖客户已编辑的内容数据。
</p>
<form method="post" action="<?php echo site_url('admin/upgrade/init'); ?>">
<?php echo csrf_field(); ?>
<button class="btn-soft" type="submit"
data-confirm="确认执行基础数据升级?">⬆️ 执行基础数据升级</button>
</form>
</div>
<?php endif; ?>
+88
View File
@@ -0,0 +1,88 @@
<?php /** @var array|null $user */ /** @var string $error */ /** @var string $ok */ /** @var array $crmPerms */ /** @var array $psiPerms */ ?>
<div class="page-head">
<div><h1><?php echo $user ? '编辑用户' : '新增用户'; ?></h1></div>
<a class="btn-ghost" href="<?php echo site_url('admin/users'); ?>">← 返回</a>
</div>
<?php if ($error): ?><div class="alert alert-err"><?php echo e($error); ?></div><?php endif; ?>
<?php if ($ok): ?><div class="alert alert-ok"><?php echo $ok; ?></div><?php endif; ?>
<?php
$showCrm = $user ? (($user['crm_role'] ?? 'none') !== 'none') : true;
$showPsi = $user ? (($user['psi_role'] ?? 'none') !== 'none') : true;
$crmPages = ['customers' => '客户管理', 'leads' => '商机线索', 'followups' => '跟进记录'];
$psiPages = ['materials' => '物料管理', 'products' => '成品管理', 'suppliers' => '供应商', 'purchases' => '采购入库', 'sales' => '销售出库', 'stock' => '库存流水'];
?>
<div class="admin-card" style="max-width:640px">
<form method="post" action="<?php echo site_url($user ? 'admin/users/update/' . $user['id'] : 'admin/users/store'); ?>">
<?php echo csrf_field(); ?>
<div class="field">
<label>登录账号</label>
<input name="username" value="<?php echo e($user['username'] ?? ''); ?>" <?php echo $user ? 'readonly' : 'required'; ?>>
<?php if ($user): ?><small class="muted">账号创建后不可修改</small><?php endif; ?>
</div>
<div class="field">
<label>姓名 / 昵称</label>
<input name="name" value="<?php echo e($user['name'] ?? ''); ?>">
</div>
<div class="field">
<label>主角色(后台)</label>
<select name="role">
<option value="super_admin" <?php echo ($user['role'] ?? '') === 'super_admin' ? 'selected' : ''; ?>>超级管理员(全部权限 + 用户管理)</option>
<option value="admin" <?php echo ($user['role'] ?? '') === 'admin' ? 'selected' : ''; ?>>管理员(内容管理)</option>
<option value="user" <?php echo ($user['role'] ?? '') === 'user' ? 'selected' : ''; ?>>用户(受限后台权限)</option>
<option value="none" <?php echo ($user['role'] ?? '') === 'none' ? 'selected' : ''; ?>>无(无后台权限,仅作为 CRM / PSI 子系统账号)</option>
</select>
<small class="muted">「无」用于纯子系统账号:该账号只通过下方 CRM / PSI 角色进入对应系统,不显示任何后台模块,角色分配更清晰。</small>
</div>
<div class="field">
<label>CRM 系统角色</label>
<select name="crm_role">
<option value="none" <?php echo ($user['crm_role'] ?? 'none') === 'none' ? 'selected' : ''; ?>>无(不可进入 CRM</option>
<option value="user" <?php echo ($user['crm_role'] ?? 'none') === 'user' ? 'selected' : ''; ?>>用户(仅查看 CRM</option>
<option value="admin" <?php echo ($user['crm_role'] ?? 'none') === 'admin' ? 'selected' : ''; ?>>管理员(可增删改 CRM</option>
</select>
</div>
<div class="field">
<label>PSI 系统角色</label>
<select name="psi_role">
<option value="none" <?php echo ($user['psi_role'] ?? 'none') === 'none' ? 'selected' : ''; ?>>无(不可进入 PSI</option>
<option value="user" <?php echo ($user['psi_role'] ?? 'none') === 'user' ? 'selected' : ''; ?>>用户(仅查看 PSI</option>
<option value="admin" <?php echo ($user['psi_role'] ?? 'none') === 'admin' ? 'selected' : ''; ?>>管理员(可增删改 PSI</option>
</select>
</div>
<?php if ($showCrm): ?>
<div class="field">
<label>CRM 页面权限(勾选可进入的页面,仪表盘始终可见)</label>
<div class="chk-grid">
<?php foreach ($crmPages as $k => $l): ?>
<label class="chk"><input type="checkbox" name="crm_pages[]" value="<?php echo $k; ?>" <?php echo !empty($crmPerms[$k]) ? 'checked' : ''; ?>> <?php echo $l; ?></label>
<?php endforeach; ?>
</div>
<small class="muted">未勾选的页面将在侧边栏隐藏,且直接访问会被拦截。</small>
</div>
<?php endif; ?>
<?php if ($showPsi): ?>
<div class="field">
<label>PSI 页面权限(勾选可进入的页面,仪表盘始终可见)</label>
<div class="chk-grid">
<?php foreach ($psiPages as $k => $l): ?>
<label class="chk"><input type="checkbox" name="psi_pages[]" value="<?php echo $k; ?>" <?php echo !empty($psiPerms[$k]) ? 'checked' : ''; ?>> <?php echo $l; ?></label>
<?php endforeach; ?>
</div>
<small class="muted">未勾选的页面将在侧边栏隐藏,且直接访问会被拦截。</small>
</div>
<?php endif; ?>
<div class="field">
<label><?php echo $user ? '重置密码(留空则不修改)' : '登录密码'; ?></label>
<input type="password" name="password" <?php echo $user ? '' : 'required'; ?>>
</div>
<div class="field" style="display:flex;align-items:center;gap:8px">
<input type="checkbox" name="status" value="1" id="st" <?php echo ($user['status'] ?? 1) ? 'checked' : ''; ?>>
<label for="st" style="margin:0">启用该账号</label>
</div>
<div class="form-actions">
<button class="btn-primary" type="submit">保存</button>
<a class="btn-ghost" href="<?php echo site_url('admin/users'); ?>">取消</a>
</div>
</form>
</div>
+31
View File
@@ -0,0 +1,31 @@
<?php /** @var array $users */ /** @var string $error */ /** @var string $ok */ ?>
<div class="page-head">
<div><h1>用户管理</h1><div class="desc">管理后台账号与权限:超级管理员 / 管理员 / 用户 / 无(纯子系统账号),支持多人协同编辑</div></div>
<a class="btn-primary" href="<?php echo site_url('admin/users/create'); ?>">+ 新增用户</a>
</div>
<?php if ($ok): ?><div class="alert alert-ok"><?php echo $ok; ?></div><?php endif; ?>
<?php if ($error): ?><div class="alert alert-err"><?php echo e($error); ?></div><?php endif; ?>
<div class="admin-card">
<table class="admin-table">
<tr><th>账号</th><th>姓名</th><th>角色</th><th>状态</th><th>操作</th></tr>
<?php foreach ($users as $u): ?>
<tr>
<td><?php echo e($u['username']); ?></td>
<td><?php echo e($u['name'] ?? ''); ?></td>
<td><span class="role-badge role-<?php echo e($u['role'] ?? 'super_admin'); ?>"><?php echo e(admin_role_label($u['role'] ?? 'super_admin')); ?></span></td>
<td><?php echo (($u['status'] ?? 1) ? '启用' : '停用'); ?></td>
<td>
<div class="row-actions">
<a class="btn-soft btn-sm" href="<?php echo site_url('admin/users/edit/' . $u['id']); ?>">编辑</a>
<a class="btn-ghost btn-sm" href="<?php echo site_url('admin/users/reset/' . $u['id']); ?>" data-confirm="确定重置该用户密码?">重置密码</a>
<?php if (($u['id'] ?? 0) != admin_uid()): ?>
<a class="btn-danger btn-sm" href="<?php echo site_url('admin/users/destroy/' . $u['id']); ?>" data-confirm="确定删除该用户?">删除</a>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>