feat: 完成二级分类真实实拍图替换与功能优化
本次提交内容包括: 1. 替换13个二级分类为真实商业实拍图,同步更新本地与线上数据库 2. 新增会员拨打额度耗尽引导开通会员功能 3. 优化发布页分类选择逻辑,增加会员权益展示 4. 修复分类名称显示问题,完善相关交互逻辑 5. 新增图片同步工具脚本,确保线上线下数据一致性
@@ -6,7 +6,7 @@ use app\common\controller\Backend;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 同城-分类管理(支持一级分类「蔬菜/水果」与带实拍图的二级分类)
|
||||
* 同城-分类管理(支持一级主分类与各二级子品类)
|
||||
*
|
||||
* @icon fa fa-circle-o
|
||||
*/
|
||||
@@ -125,19 +125,32 @@ class Category extends Backend
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑分类(自动同步 image 与 icon_key)
|
||||
* 编辑分类(自动补全图片路径并同步 image 与 icon_key)
|
||||
*
|
||||
* @param mixed $ids
|
||||
* @return string
|
||||
*/
|
||||
public function edit($ids = null)
|
||||
{
|
||||
$row = $this->model->get($ids);
|
||||
if (!$row) {
|
||||
$this->error(__('No Results were found'));
|
||||
}
|
||||
$adminIds = $this->getDataLimitAdminIds();
|
||||
if (is_array($adminIds) && !in_array($row[$this->dataLimitField], $adminIds)) {
|
||||
$this->error(__('You have no permission'));
|
||||
}
|
||||
if ($this->request->isPost()) {
|
||||
$params = $this->request->post('row/a', []);
|
||||
$params = $this->normalizeCategoryParams($params, (int)$ids);
|
||||
$this->request->post(['row' => $params]);
|
||||
return parent::edit($ids);
|
||||
}
|
||||
return parent::edit($ids);
|
||||
if (empty($row['image']) && !empty($row['icon_key'])) {
|
||||
$row['image'] = $this->resolveImageFromKey($row['icon_key']);
|
||||
}
|
||||
$this->view->assign("row", $row);
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,7 +206,7 @@ class Category extends Backend
|
||||
{
|
||||
$iconKey = trim((string)$iconKey);
|
||||
if ($iconKey === '') {
|
||||
return '/static/categories/cat-veg.png';
|
||||
return '/static/icons/cat-service.png';
|
||||
}
|
||||
if (preg_match('/^(\/uploads\/|\/static\/|https?:\/\/)/i', $iconKey)) {
|
||||
return $iconKey;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<style>
|
||||
.fc-form-container {
|
||||
padding: 15px 15px 0;
|
||||
padding: 16px 16px 0;
|
||||
background: #f7faf9;
|
||||
}
|
||||
/* 分区卡片:把表单切成三个认知区块 */
|
||||
/* 分区卡片 */
|
||||
.fc-section {
|
||||
background: #ffffff;
|
||||
border: 1px solid #eef2f6;
|
||||
@@ -44,7 +44,7 @@
|
||||
/* 分类类型:分段选择器 */
|
||||
.fc-kind-seg {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.fc-kind-seg label {
|
||||
@@ -61,7 +61,7 @@
|
||||
}
|
||||
.fc-kind-seg .fc-pill {
|
||||
display: block;
|
||||
padding: 10px 14px;
|
||||
padding: 11px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
@@ -84,9 +84,10 @@
|
||||
background: #f2fef7;
|
||||
box-shadow: 0 0 0 2px rgba(0, 210, 106, 0.18);
|
||||
}
|
||||
.fc-kind-seg input:checked + .fc-pill.farm b { color: #059669; }
|
||||
.fc-kind-seg input:checked + .fc-pill.general b { color: #2563eb; }
|
||||
.fc-kind-seg input:checked + .fc-pill.general b { color: #00a854; }
|
||||
.fc-kind-seg input:checked + .fc-pill.farm b { color: #d97706; }
|
||||
.fc-kind-seg input:focus-visible + .fc-pill { outline: 2px solid #00d26a; outline-offset: 2px; }
|
||||
|
||||
/* 主题色:原生取色器 */
|
||||
.fc-color-row {
|
||||
display: flex;
|
||||
@@ -117,28 +118,60 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: nowrap;
|
||||
gap: 10px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
padding: 8px 14px;
|
||||
min-height: 54px;
|
||||
min-height: 58px;
|
||||
}
|
||||
.fc-current-icon-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
min-width: 0; /* 关键:允许flex子项收缩,防止内容撑破布局 */
|
||||
overflow: hidden;
|
||||
}
|
||||
.fc-current-icon-info img {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
flex: 0 0 44px; /* 固定尺寸,不参与收缩 */
|
||||
object-fit: cover;
|
||||
background: #ffffff;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 2px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.fc-current-icon-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0; /* 允许文字区域收缩 */
|
||||
overflow: hidden;
|
||||
}
|
||||
.fc-current-icon-name {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.fc-current-icon-tip {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.fc-icon-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex: 0 0 auto; /* 按钮区域不收缩 */
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.fc-icon-actions .btn { border-radius: 9px; font-weight: 600; }
|
||||
.fc-btn-main {
|
||||
@@ -148,6 +181,10 @@
|
||||
font-weight: 700 !important;
|
||||
box-shadow: 0 3px 8px rgba(0, 210, 106, 0.28);
|
||||
}
|
||||
.fc-btn-main:hover {
|
||||
background: linear-gradient(135deg, #00d26a, #00b856) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
/* 推荐调色板 */
|
||||
.fc-color-presets {
|
||||
display: flex;
|
||||
@@ -165,6 +202,7 @@
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.fc-color-dot:hover { transform: scale(1.2); }
|
||||
|
||||
/* 右侧迷你小程序模拟器 */
|
||||
.fc-preview-box {
|
||||
position: sticky;
|
||||
@@ -172,46 +210,71 @@
|
||||
background: linear-gradient(150deg, #f2fef7 0%, #e6fbf1 100%);
|
||||
border: 1px solid #c8f2de;
|
||||
border-radius: 16px;
|
||||
padding: 18px 14px;
|
||||
padding: 20px 16px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 14px rgba(4, 48, 30, 0.05);
|
||||
}
|
||||
.fc-preview-title {
|
||||
font-size: 12px;
|
||||
color: #04874a;
|
||||
color: #00a854;
|
||||
font-weight: 700;
|
||||
margin-bottom: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.fc-preview-badge {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
border-radius: 18px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 8px;
|
||||
margin: 0 auto 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(4, 48, 30, 0.08);
|
||||
padding: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.fc-preview-badge img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.fc-preview-name {
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.fc-preview-kind-pill {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.fc-preview-kind-pill.general {
|
||||
background: #e6fbf1;
|
||||
color: #00a854;
|
||||
border: 1px solid #a7f3d0;
|
||||
}
|
||||
.fc-preview-kind-pill.farm {
|
||||
background: #fef3c7;
|
||||
color: #b45309;
|
||||
border: 1px solid #fde68a;
|
||||
}
|
||||
.fc-preview-note {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
margin-top: 10px;
|
||||
line-height: 1.6;
|
||||
border-top: 1px dashed #d1fae5;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
/* 插画选择模态窗口 */
|
||||
/* 图标挑选模态窗口 */
|
||||
.fc-icon-modal-wrap {
|
||||
display: none;
|
||||
position: fixed;
|
||||
@@ -239,10 +302,13 @@
|
||||
position: relative;
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
width: 660px;
|
||||
width: 760px;
|
||||
max-width: 95%;
|
||||
max-height: 88vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25);
|
||||
padding: 18px 22px 20px;
|
||||
padding: 18px 22px 16px;
|
||||
z-index: 2000;
|
||||
animation: fcModalZoom 0.2s ease-out;
|
||||
}
|
||||
@@ -252,12 +318,21 @@
|
||||
justify-content: space-between;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.fc-modal-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.fc-modal-title small {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.fc-modal-close {
|
||||
width: 28px;
|
||||
@@ -271,47 +346,163 @@
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.fc-modal-close:hover {
|
||||
background: #e2e8f0;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
/* 模态窗工具栏:搜索与分类标签 */
|
||||
.fc-modal-toolbar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.fc-modal-search-box {
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.fc-modal-search-box i {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
}
|
||||
.fc-modal-search-input {
|
||||
width: 100%;
|
||||
padding: 7px 12px 7px 34px;
|
||||
font-size: 12.5px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 9px;
|
||||
outline: none;
|
||||
background: #f8fafc;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.fc-modal-search-input:focus {
|
||||
border-color: #00d26a;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 2px rgba(0, 210, 106, 0.15);
|
||||
}
|
||||
.fc-modal-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.fc-icon-tab-btn {
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.fc-icon-tab-btn:hover {
|
||||
color: #00a854;
|
||||
border-color: #a7f3d0;
|
||||
}
|
||||
.fc-icon-tab-btn.active {
|
||||
background: #00d26a;
|
||||
color: #ffffff;
|
||||
border-color: #00d26a;
|
||||
box-shadow: 0 2px 6px rgba(0, 210, 106, 0.25);
|
||||
}
|
||||
|
||||
/* 图标芯片网格 */
|
||||
.fc-modal-grid-box {
|
||||
max-height: 360px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 2px 4px 6px;
|
||||
padding: 4px 2px 10px;
|
||||
min-height: 240px;
|
||||
max-height: 380px;
|
||||
}
|
||||
.fc-modal-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
.fc-icon-chip {
|
||||
flex: 0 0 calc(16.666% - 10px);
|
||||
height: 72px;
|
||||
flex: 0 0 calc(20% - 8px);
|
||||
min-width: 110px;
|
||||
height: 96px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
cursor: pointer;
|
||||
padding: 8px 6px;
|
||||
position: relative;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.fc-icon-chip:hover {
|
||||
border-color: #00E070;
|
||||
background: #f0fdf4;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 10px rgba(0, 210, 106, 0.15);
|
||||
}
|
||||
.fc-icon-chip.active {
|
||||
border-color: #00E070;
|
||||
border-color: #00D26A;
|
||||
background: #e6fbf1;
|
||||
box-shadow: 0 0 0 2px #00E070;
|
||||
box-shadow: 0 0 0 2px #00D26A;
|
||||
}
|
||||
.fc-icon-chip.active::after {
|
||||
content: "\f00c";
|
||||
font-family: FontAwesome;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 4px;
|
||||
font-size: 10px;
|
||||
color: #ffffff;
|
||||
background: #00D26A;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.fc-icon-chip img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.fc-icon-chip.is-category-top img {
|
||||
object-fit: contain;
|
||||
}
|
||||
.fc-icon-chip .fc-chip-name {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
.fc-modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.fc-modal-footer-tip {
|
||||
font-size: 11.5px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
@keyframes fcModalZoom {
|
||||
from { opacity: 0; transform: scale(0.95); }
|
||||
from { opacity: 0; transform: scale(0.96); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
</style>
|
||||
@@ -323,13 +514,13 @@
|
||||
<div class="fc-section">
|
||||
<div class="fc-section-title">
|
||||
<span class="fc-sec-icon"><i class="fa fa-sitemap"></i></span> 归属与类型
|
||||
<span class="fc-sec-tip">决定发布页展示哪些字段</span>
|
||||
<span class="fc-sec-tip">决定前台分类层级与发布表单</span>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:14px;">
|
||||
<label class="control-label col-xs-12 col-sm-3">上级分类:</label>
|
||||
<div class="col-xs-12 col-sm-9">
|
||||
{:build_select('row[pid]', $parentList, 0, ['class'=>'form-control selectpicker', 'id'=>'c-pid', 'data-rule'=>'required'])}
|
||||
<span class="help-block text-muted" style="margin-top:4px;margin-bottom:0;font-size:11px;">选「顶级分类」创建一级大类;选「蔬菜」「水果」则创建其下属二级品类。</span>
|
||||
<span class="help-block text-muted" style="margin-top:4px;margin-bottom:0;font-size:11px;">选「顶级分类」创建一级大类频道;选指定分类(如「招聘专区」「肉蛋家禽」)则创建下属二级品类。</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:10px;">
|
||||
@@ -337,12 +528,12 @@
|
||||
<div class="col-xs-12 col-sm-9">
|
||||
<div class="fc-kind-seg">
|
||||
<label>
|
||||
<input type="radio" name="row[kind]" value="farm" checked>
|
||||
<span class="fc-pill farm"><b><i class="fa fa-leaf"></i> 农产品</b><small>发布时需填写种植面积、采摘时间、量产</small></span>
|
||||
<input type="radio" name="row[kind]" value="general" checked>
|
||||
<span class="fc-pill general"><b><i class="fa fa-briefcase"></i> 通用生活信息</b><small>招聘求职、便民服务、房产二手等常用分类</small></span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="row[kind]" value="general">
|
||||
<span class="fc-pill general"><b><i class="fa fa-briefcase"></i> 通用信息</b><small>招聘求职等,无产地字段</small></span>
|
||||
<input type="radio" name="row[kind]" value="farm">
|
||||
<span class="fc-pill farm"><b><i class="fa fa-leaf"></i> 农特果蔬产品</b><small>发布时可填写种植面积、采摘时间、产量等</small></span>
|
||||
</label>
|
||||
</div>
|
||||
<span class="help-block text-muted" style="margin-top:8px;margin-bottom:0;font-size:11px;">二级分类自动跟随上级分类的类型,无需单独设置。</span>
|
||||
@@ -358,27 +549,31 @@
|
||||
<div class="form-group" style="margin-bottom:14px;">
|
||||
<label class="control-label col-xs-12 col-sm-3">分类名称:</label>
|
||||
<div class="col-xs-12 col-sm-9">
|
||||
<input id="c-name" data-rule="required" class="form-control" name="row[name]" type="text" placeholder="例如:蔬菜、水果 或 花菜、白菜、紫甘蓝" value="">
|
||||
<input id="c-name" data-rule="required" class="form-control" name="row[name]" type="text" placeholder="例如:招聘求职、骑手配送、鲜土鸡蛋 或 苹果、白菜" value="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:14px;">
|
||||
<label class="control-label col-xs-12 col-sm-3">实拍图:</label>
|
||||
<label class="control-label col-xs-12 col-sm-3">分类图标/图片:</label>
|
||||
<div class="col-xs-12 col-sm-9">
|
||||
<div class="fc-current-icon-box">
|
||||
<div class="fc-current-icon-info">
|
||||
<img id="current-icon-img" src="/static/categories/cat-veg.png?v=2" alt="当前图片">
|
||||
<img id="current-icon-img" src="/static/categories/cat-job.png?v=2" alt="当前图标">
|
||||
<div class="fc-current-icon-text">
|
||||
<span class="fc-current-icon-name" id="current-icon-label">招聘专区</span>
|
||||
<span class="fc-current-icon-tip">用于前台金刚区及分类列表展示</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fc-icon-actions">
|
||||
<button type="button" class="btn btn-success fc-btn-main" id="btn-open-icon-picker">
|
||||
<i class="fa fa-th-large"></i> 挑选果蔬实拍图
|
||||
<i class="fa fa-th-large"></i> 挑选预设图片
|
||||
</button>
|
||||
<button type="button" id="faupload-icon_key" class="btn btn-default faupload" data-input-id="c-icon_key" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/svg+xml" data-maxsize="500KB" data-multiple="false">
|
||||
<i class="fa fa-upload text-primary"></i> 上传实拍图
|
||||
<i class="fa fa-upload text-primary"></i> 上传自定义实拍图
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span class="help-block text-muted" style="margin-top:4px;margin-bottom:0;font-size:11px;">优先上传真实农产品照片,农民用户靠「看图识物」选择品类;建议 500×500 以上方形图,单张不超过 <b class="text-danger">500KB</b>,过大的图片会拖慢小程序加载。</span>
|
||||
<input type="hidden" name="row[icon_key]" id="c-icon_key" value="cat-veg">
|
||||
<span class="help-block text-muted" style="margin-top:4px;margin-bottom:0;font-size:11px;">推荐从平台预设图片库中直接选用;也支持上传高清实拍方形图(建议 200×200 以上,单张不超过 <b class="text-danger">500KB</b>)。</span>
|
||||
<input type="hidden" name="row[icon_key]" id="c-icon_key" value="cat-job">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -399,11 +594,11 @@
|
||||
<div class="fc-color-presets">
|
||||
<span style="font-size:11px;color:#64748b;">推荐:</span>
|
||||
<div class="fc-color-dot" style="background:#00E070;" title="春绿(品牌主色)" data-color="#00E070"></div>
|
||||
<div class="fc-color-dot" style="background:#3498DB;" title="晴空蓝" data-color="#3498DB"></div>
|
||||
<div class="fc-color-dot" style="background:#F5A623;" title="阳光金" data-color="#F5A623"></div>
|
||||
<div class="fc-color-dot" style="background:#FF7A59;" title="活力橙" data-color="#FF7A59"></div>
|
||||
<div class="fc-color-dot" style="background:#2ECC71;" title="翠青绿" data-color="#2ECC71"></div>
|
||||
<div class="fc-color-dot" style="background:#3498DB;" title="晴空蓝" data-color="#3498DB"></div>
|
||||
<div class="fc-color-dot" style="background:#9B59B6;" title="典雅紫" data-color="#9B59B6"></div>
|
||||
<div class="fc-color-dot" style="background:#10B981;" title="翡翠绿" data-color="#10B981"></div>
|
||||
<div class="fc-color-dot" style="background:#8B5CF6;" title="典雅紫" data-color="#8B5CF6"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -432,13 +627,14 @@
|
||||
<div class="col-xs-12 col-sm-4 hidden-xs">
|
||||
<div class="fc-preview-box">
|
||||
<div class="fc-preview-title">
|
||||
<i class="fa fa-mobile fa-lg"></i> 小程序端 · 实时预览
|
||||
<i class="fa fa-mobile fa-lg"></i> 小程序端 · 实时效果预览
|
||||
</div>
|
||||
<div class="fc-preview-badge" id="preview-badge" style="background: rgba(0, 224, 112, 0.15); border: 1px solid rgba(0, 224, 112, 0.35);">
|
||||
<img id="preview-icon" src="/static/categories/cat-veg.png?v=2" alt="分类图片">
|
||||
<img id="preview-icon" src="/static/categories/cat-job.png?v=2" alt="分类图标">
|
||||
</div>
|
||||
<div class="fc-preview-name" id="preview-name">未命名分类</div>
|
||||
<div class="fc-preview-note">修改名称 / 实拍图 / 主题色时<br>此预览会同步更新</div>
|
||||
<div><span class="fc-preview-kind-pill general" id="preview-kind">通用便民类目</span></div>
|
||||
<div class="fc-preview-note">修改名称 / 图标 / 主题色时<br>此预览卡片将实时同步更新</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -452,38 +648,96 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 插画选择面板(默认完全隐藏,只有主动点击按钮才唤起) -->
|
||||
<!-- 预设图标选择模态窗口(常用图标库,含搜索与分组) -->
|
||||
<div class="fc-icon-modal-wrap" id="fc-icon-modal" style="display:none !important;">
|
||||
<div class="fc-modal-overlay"></div>
|
||||
<div class="fc-modal-card">
|
||||
<div class="fc-modal-header">
|
||||
<div class="fc-modal-title">
|
||||
<i class="fa fa-th-large text-success"></i> 选择分类实拍图
|
||||
<i class="fa fa-th-large text-success"></i> 挑选分类图标 / 实拍图
|
||||
<small>点击卡片即可选用</small>
|
||||
</div>
|
||||
<button type="button" class="fc-modal-close" id="btn-close-icon-modal">×</button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索与分类导航标签 -->
|
||||
<div class="fc-modal-toolbar">
|
||||
<div class="fc-modal-search-box">
|
||||
<i class="fa fa-search"></i>
|
||||
<input type="text" id="fc-icon-search" class="fc-modal-search-input" placeholder="输入名称快速筛选,例如:招聘、骑手、苹果、白菜...">
|
||||
</div>
|
||||
<div class="fc-modal-tabs">
|
||||
<button type="button" class="fc-icon-tab-btn active" data-group="all">全部</button>
|
||||
<button type="button" class="fc-icon-tab-btn" data-group="top">一级大类 (8)</button>
|
||||
<button type="button" class="fc-icon-tab-btn" data-group="job">招聘职位 (8)</button>
|
||||
<button type="button" class="fc-icon-tab-btn" data-group="poultry">肉蛋水产 (5)</button>
|
||||
<button type="button" class="fc-icon-tab-btn" data-group="fruit">新鲜水果 (15)</button>
|
||||
<button type="button" class="fc-icon-tab-btn" data-group="veg">时令蔬菜 (8)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图标网格卡片 -->
|
||||
<div class="fc-modal-grid-box">
|
||||
<div class="fc-modal-grid">
|
||||
<div class="fc-icon-chip" data-key="cat-veg" title="新鲜蔬菜"><img src="/static/categories/cat-veg.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="cat-fruit" title="时令水果"><img src="/static/categories/cat-fruit.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="veg-cauliflower" title="花菜"><img src="/static/categories/veg-cauliflower.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="veg-cabbage" title="白菜"><img src="/static/categories/veg-cabbage.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="veg-purple-cabbage" title="紫甘蓝"><img src="/static/categories/veg-purple-cabbage.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="veg-green-cabbage" title="青甘蓝"><img src="/static/categories/veg-green-cabbage.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="veg-greens" title="青菜"><img src="/static/categories/veg-greens.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="veg-radish" title="萝卜"><img src="/static/categories/veg-radish.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="veg-chili" title="辣椒"><img src="/static/categories/veg-chili.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="veg-tomato" title="西红柿"><img src="/static/categories/veg-tomato.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="fruit-apple" title="苹果"><img src="/static/categories/fruit-apple.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="fruit-orange" title="柑橘"><img src="/static/categories/fruit-orange.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="fruit-kiwi" title="猕猴桃"><img src="/static/categories/fruit-kiwi.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="fruit-strawberry" title="草莓"><img src="/static/categories/fruit-strawberry.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="fruit-watermelon" title="西瓜"><img src="/static/categories/fruit-watermelon.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="fruit-grape" title="葡萄"><img src="/static/categories/fruit-grape.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="fruit-peach" title="蜜桃"><img src="/static/categories/fruit-peach.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="fruit-passion" title="百香果"><img src="/static/categories/fruit-passion.png?v=2" alt="icon"></div>
|
||||
<!-- 1. 一级大类频道 (8) -->
|
||||
<div class="fc-icon-chip is-category-top active" data-group="top" data-key="cat-job" data-name="招聘专区" data-color="#00D26A" title="招聘专区"><img src="/static/categories/cat-job.png" alt="icon"><span class="fc-chip-name">招聘专区</span></div>
|
||||
<div class="fc-icon-chip is-category-top" data-group="top" data-key="cat-poultry" data-name="肉蛋家禽" data-color="#FF7A59" title="肉蛋家禽"><img src="/static/categories/cat-poultry.png" alt="icon"><span class="fc-chip-name">肉蛋家禽</span></div>
|
||||
<div class="fc-icon-chip is-category-top" data-group="top" data-key="cat-veg" data-name="蔬菜专区" data-color="#00E070" title="蔬菜专区"><img src="/static/categories/cat-veg.png" alt="icon"><span class="fc-chip-name">蔬菜专区</span></div>
|
||||
<div class="fc-icon-chip is-category-top" data-group="top" data-key="cat-fruit" data-name="水果专区" data-color="#F5A623" title="水果专区"><img src="/static/categories/cat-fruit.png" alt="icon"><span class="fc-chip-name">水果专区</span></div>
|
||||
<div class="fc-icon-chip is-category-top" data-group="top" data-key="cat-service" data-name="便民服务" data-color="#10B981" title="便民服务"><img src="/static/icons/cat-service.png" alt="icon"><span class="fc-chip-name">便民服务</span></div>
|
||||
<div class="fc-icon-chip is-category-top" data-group="top" data-key="cat-house" data-name="房产租售" data-color="#3498DB" title="房产租售"><img src="/static/icons/cat-house.png" alt="icon"><span class="fc-chip-name">房产租售</span></div>
|
||||
<div class="fc-icon-chip is-category-top" data-group="top" data-key="cat-goods" data-name="二手闲置" data-color="#F5A623" title="二手闲置"><img src="/static/icons/cat-goods.png" alt="icon"><span class="fc-chip-name">二手闲置</span></div>
|
||||
<div class="fc-icon-chip is-category-top" data-group="top" data-key="cat-car" data-name="车辆交易" data-color="#FF7A59" title="车辆交易"><img src="/static/icons/cat-car.png" alt="icon"><span class="fc-chip-name">车辆交易</span></div>
|
||||
|
||||
<!-- 2. 招聘职位 (8) -->
|
||||
<div class="fc-icon-chip" data-group="job" data-key="/uploads/20261028/3fb1eee6654e4d5e7c1b5d3189643fae.jpg" data-name="骑手配送" data-color="#F5A623" title="骑手配送"><img src="/uploads/20261028/3fb1eee6654e4d5e7c1b5d3189643fae.jpg" alt="icon"><span class="fc-chip-name">骑手配送</span></div>
|
||||
<div class="fc-icon-chip" data-group="job" data-key="/uploads/20261028/f5e2927ba040dd1b917b6fbd6b053458.jpg" data-name="司机出行" data-color="#3498DB" title="司机出行"><img src="/uploads/20261028/f5e2927ba040dd1b917b6fbd6b053458.jpg" alt="icon"><span class="fc-chip-name">司机出行</span></div>
|
||||
<div class="fc-icon-chip" data-group="job" data-key="/uploads/20261028/cb17be7476ce1c59e14944f9fe60438c.jpg" data-name="普工技工" data-color="#00D26A" title="普工技工"><img src="/uploads/20261028/cb17be7476ce1c59e14944f9fe60438c.jpg" alt="icon"><span class="fc-chip-name">普工技工</span></div>
|
||||
<div class="fc-icon-chip" data-group="job" data-key="/uploads/20261028/4e4f911ac45e9dc5471597a8ff7862f7.jpg" data-name="餐饮服务" data-color="#EF4444" title="餐饮服务"><img src="/uploads/20261028/4e4f911ac45e9dc5471597a8ff7862f7.jpg" alt="icon"><span class="fc-chip-name">餐饮服务</span></div>
|
||||
<div class="fc-icon-chip" data-group="job" data-key="/uploads/20261028/915423715a937ad7ba4b737690c963d2.jpg" data-name="家政保洁" data-color="#10B981" title="家政保洁"><img src="/uploads/20261028/915423715a937ad7ba4b737690c963d2.jpg" alt="icon"><span class="fc-chip-name">家政保洁</span></div>
|
||||
<div class="fc-icon-chip" data-group="job" data-key="/uploads/20261028/d0cc33f85a8580be2f526eb360448867.jpg" data-name="销售客服" data-color="#8B5CF6" title="销售客服"><img src="/uploads/20261028/d0cc33f85a8580be2f526eb360448867.jpg" alt="icon"><span class="fc-chip-name">销售客服</span></div>
|
||||
<div class="fc-icon-chip" data-group="job" data-key="/uploads/20261028/99c32f9508e3429ea02a4ce1c34f9df2.jpg" data-name="教育培训" data-color="#06B6D4" title="教育培训"><img src="/uploads/20261028/99c32f9508e3429ea02a4ce1c34f9df2.jpg" alt="icon"><span class="fc-chip-name">教育培训</span></div>
|
||||
<div class="fc-icon-chip" data-group="job" data-key="/uploads/20261028/0c913f9d7d72a10c0c598c36eea89b14.jpg" data-name="兼职短工" data-color="#EC4899" title="兼职短工"><img src="/uploads/20261028/0c913f9d7d72a10c0c598c36eea89b14.jpg" alt="icon"><span class="fc-chip-name">兼职短工</span></div>
|
||||
|
||||
<!-- 3. 肉蛋水产 (5) -->
|
||||
<div class="fc-icon-chip" data-group="poultry" data-key="/uploads/20261028/69b65e455081d5aa8834b48bd879009d.jpg" data-name="鲜土鸡蛋" data-color="#F5A623" title="鲜土鸡蛋"><img src="/uploads/20261028/69b65e455081d5aa8834b48bd879009d.jpg" alt="icon"><span class="fc-chip-name">鲜土鸡蛋</span></div>
|
||||
<div class="fc-icon-chip" data-group="poultry" data-key="/uploads/20261028/cafbe884f6616622dcdd5650bbbce3b3.jpg" data-name="散养土鸡" data-color="#F97316" title="散养土鸡"><img src="/uploads/20261028/cafbe884f6616622dcdd5650bbbce3b3.jpg" alt="icon"><span class="fc-chip-name">散养土鸡</span></div>
|
||||
<div class="fc-icon-chip" data-group="poultry" data-key="/uploads/20261028/d2a3226fd6ffa12d24db06c9342a9118.jpg" data-name="生鲜猪肉" data-color="#EA580C" title="生鲜猪肉"><img src="/uploads/20261028/d2a3226fd6ffa12d24db06c9342a9118.jpg" alt="icon"><span class="fc-chip-name">生鲜猪肉</span></div>
|
||||
<div class="fc-icon-chip" data-group="poultry" data-key="/uploads/20261028/70cafc924bc7e4018bfefbe883e27c78.jpg" data-name="高山牛肉" data-color="#DC2626" title="高山牛肉"><img src="/uploads/20261028/70cafc924bc7e4018bfefbe883e27c78.jpg" alt="icon"><span class="fc-chip-name">高山牛肉</span></div>
|
||||
<div class="fc-icon-chip" data-group="poultry" data-key="/uploads/20261028/bb63e21dae84dc15056a0b50753577d9.jpg" data-name="鲜活水产" data-color="#0284C7" title="鲜活水产"><img src="/uploads/20261028/bb63e21dae84dc15056a0b50753577d9.jpg" alt="icon"><span class="fc-chip-name">鲜活水产</span></div>
|
||||
|
||||
<!-- 4. 新鲜水果 (15) -->
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261020/d29f6003464df81337954f2e3b9213de.jpg" data-name="红富士苹果" data-color="#E11D48" title="红富士苹果"><img src="/uploads/20261020/d29f6003464df81337954f2e3b9213de.jpg" alt="icon"><span class="fc-chip-name">苹果</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261020/0c0115e6946ec7ac4c2fe7088bdbe0d7.jpg" data-name="柑橘橙子" data-color="#F97316" title="柑橘橙子"><img src="/uploads/20261020/0c0115e6946ec7ac4c2fe7088bdbe0d7.jpg" alt="icon"><span class="fc-chip-name">柑橘</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261020/74d6e0301b727fe2c48638ebd7989fa8.jpg" data-name="新鲜猕猴桃" data-color="#65A30D" title="新鲜猕猴桃"><img src="/uploads/20261020/74d6e0301b727fe2c48638ebd7989fa8.jpg" alt="icon"><span class="fc-chip-name">猕猴桃</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261020/ebb126f270081d28162e90fd240cd481.jpg" data-name="鲜采草莓" data-color="#F43F5E" title="鲜采草莓"><img src="/uploads/20261020/ebb126f270081d28162e90fd240cd481.jpg" alt="icon"><span class="fc-chip-name">草莓</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261020/0e6e5dbbbd77f31315e643c2e4915d9a.jpg" data-name="甜脆西瓜" data-color="#16A34A" title="甜脆西瓜"><img src="/uploads/20261020/0e6e5dbbbd77f31315e643c2e4915d9a.jpg" alt="icon"><span class="fc-chip-name">西瓜</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261020/b0af7b62d055688ef020e9dab794373d.jpg" data-name="提子葡萄" data-color="#7E22CE" title="提子葡萄"><img src="/uploads/20261020/b0af7b62d055688ef020e9dab794373d.jpg" alt="icon"><span class="fc-chip-name">葡萄</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261020/2b709d7c76d07516758df3558d83a56a.jpg" data-name="鲜桃蜜桃" data-color="#EC4899" title="鲜桃蜜桃"><img src="/uploads/20261020/2b709d7c76d07516758df3558d83a56a.jpg" alt="icon"><span class="fc-chip-name">蜜桃</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261020/c994cb47610ed3514571db794b1ee724.jpg" data-name="新鲜百香果" data-color="#9333EA" title="新鲜百香果"><img src="/uploads/20261020/c994cb47610ed3514571db794b1ee724.jpg" alt="icon"><span class="fc-chip-name">百香果</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261028/f1a4f3c123c81aea2f0fd24c8688d988.jpg" data-name="香甜芒果" data-color="#F5A623" title="香甜芒果"><img src="/uploads/20261028/f1a4f3c123c81aea2f0fd24c8688d988.jpg" alt="icon"><span class="fc-chip-name">芒果</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261028/d8b5420c12f7921e325050be088a011f.jpg" data-name="香蕉" data-color="#FBBF24" title="香蕉"><img src="/uploads/20261028/d8b5420c12f7921e325050be088a011f.jpg" alt="icon"><span class="fc-chip-name">香蕉</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261028/75dd5971bdfa99d64a62c5b1e72cac11.jpg" data-name="蓝莓" data-color="#3B82F6" title="蓝莓"><img src="/uploads/20261028/75dd5971bdfa99d64a62c5b1e72cac11.jpg" alt="icon"><span class="fc-chip-name">蓝莓</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261028/e71ba37749da829c50d098168dbb4fa2.jpg" data-name="车厘子樱桃" data-color="#DC2626" title="车厘子"><img src="/uploads/20261028/e71ba37749da829c50d098168dbb4fa2.jpg" alt="icon"><span class="fc-chip-name">车厘子</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261028/65d91dba8121f8b49356f356aa6a3b28.jpg" data-name="红心火龙果" data-color="#EC4899" title="火龙果"><img src="/uploads/20261028/65d91dba8121f8b49356f356aa6a3b28.jpg" alt="icon"><span class="fc-chip-name">火龙果</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261028/0391292554cf8efd457c44caedf67867.jpg" data-name="新鲜柠檬" data-color="#EAB308" title="柠檬"><img src="/uploads/20261028/0391292554cf8efd457c44caedf67867.jpg" alt="icon"><span class="fc-chip-name">柠檬</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261028/49b39ce93043ec8a6f24bcef9bc45f96.jpg" data-name="新鲜荔枝" data-color="#EF4444" title="荔枝"><img src="/uploads/20261028/49b39ce93043ec8a6f24bcef9bc45f96.jpg" alt="icon"><span class="fc-chip-name">荔枝</span></div>
|
||||
|
||||
<!-- 5. 时令蔬菜 (8) -->
|
||||
<div class="fc-icon-chip" data-group="veg" data-key="/uploads/20261020/11fe77af7af7a4b2239a20a189fca471.jpg" data-name="高山白菜" data-color="#00D26A" title="高山白菜"><img src="/uploads/20261020/11fe77af7af7a4b2239a20a189fca471.jpg" alt="icon"><span class="fc-chip-name">白菜</span></div>
|
||||
<div class="fc-icon-chip" data-group="veg" data-key="/uploads/20261020/22e041f997661fbcbcf10ce86da3c01b.jpg" data-name="有机花菜" data-color="#00D26A" title="有机花菜"><img src="/uploads/20261020/22e041f997661fbcbcf10ce86da3c01b.jpg" alt="icon"><span class="fc-chip-name">花菜</span></div>
|
||||
<div class="fc-icon-chip" data-group="veg" data-key="/uploads/20261020/2bee40730c9f23595deecdfa24b206cf.jpg" data-name="新鲜青菜" data-color="#16A34A" title="新鲜青菜"><img src="/uploads/20261020/2bee40730c9f23595deecdfa24b206cf.jpg" alt="icon"><span class="fc-chip-name">青菜</span></div>
|
||||
<div class="fc-icon-chip" data-group="veg" data-key="/uploads/20261020/0604d246fb42bb569e8031b3a1c3c9ad.jpg" data-name="熟透西红柿" data-color="#EF4444" title="熟透西红柿"><img src="/uploads/20261020/0604d246fb42bb569e8031b3a1c3c9ad.jpg" alt="icon"><span class="fc-chip-name">西红柿</span></div>
|
||||
<div class="fc-icon-chip" data-group="veg" data-key="/uploads/20261020/8aac76e021aea0dd999ba5689ae86577.jpg" data-name="鲜红辣椒" data-color="#DC2626" title="鲜红辣椒"><img src="/uploads/20261020/8aac76e021aea0dd999ba5689ae86577.jpg" alt="icon"><span class="fc-chip-name">辣椒</span></div>
|
||||
<div class="fc-icon-chip" data-group="veg" data-key="/uploads/20261020/f7a5d6be1d58be88cdd0915c050f6028.jpg" data-name="脆甜白萝卜" data-color="#22C55E" title="脆甜白萝卜"><img src="/uploads/20261020/f7a5d6be1d58be88cdd0915c050f6028.jpg" alt="icon"><span class="fc-chip-name">萝卜</span></div>
|
||||
<div class="fc-icon-chip" data-group="veg" data-key="/uploads/20261020/1ad84fe2cef8ef22ef29ac45a898081c.jpg" data-name="新鲜紫甘蓝" data-color="#9333EA" title="新鲜紫甘蓝"><img src="/uploads/20261020/1ad84fe2cef8ef22ef29ac45a898081c.jpg" alt="icon"><span class="fc-chip-name">紫甘蓝</span></div>
|
||||
<div class="fc-icon-chip" data-group="veg" data-key="/uploads/20261020/b2cabf7db99e86a8ee8da424e19ebf9d.jpg" data-name="新鲜青甘蓝" data-color="#10B981" title="新鲜青甘蓝"><img src="/uploads/20261020/b2cabf7db99e86a8ee8da424e19ebf9d.jpg" alt="icon"><span class="fc-chip-name">青甘蓝</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fc-modal-footer">
|
||||
<span class="fc-modal-footer-tip"><i class="fa fa-info-circle text-success"></i> 点击任意图片卡片将自动设定,并可智能推荐分类名称与主题色</span>
|
||||
<button type="button" class="btn btn-default btn-sm" id="btn-cancel-modal" onclick="$('#btn-close-icon-modal').click();">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<style>
|
||||
.fc-form-container {
|
||||
padding: 15px 15px 0;
|
||||
padding: 16px 16px 0;
|
||||
background: #f7faf9;
|
||||
}
|
||||
/* 分区卡片:把表单切成三个认知区块 */
|
||||
/* 分区卡片 */
|
||||
.fc-section {
|
||||
background: #ffffff;
|
||||
border: 1px solid #eef2f6;
|
||||
@@ -44,7 +44,7 @@
|
||||
/* 分类类型:分段选择器 */
|
||||
.fc-kind-seg {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.fc-kind-seg label {
|
||||
@@ -61,7 +61,7 @@
|
||||
}
|
||||
.fc-kind-seg .fc-pill {
|
||||
display: block;
|
||||
padding: 10px 14px;
|
||||
padding: 11px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
@@ -84,9 +84,10 @@
|
||||
background: #f2fef7;
|
||||
box-shadow: 0 0 0 2px rgba(0, 210, 106, 0.18);
|
||||
}
|
||||
.fc-kind-seg input:checked + .fc-pill.farm b { color: #059669; }
|
||||
.fc-kind-seg input:checked + .fc-pill.general b { color: #2563eb; }
|
||||
.fc-kind-seg input:checked + .fc-pill.general b { color: #00a854; }
|
||||
.fc-kind-seg input:checked + .fc-pill.farm b { color: #d97706; }
|
||||
.fc-kind-seg input:focus-visible + .fc-pill { outline: 2px solid #00d26a; outline-offset: 2px; }
|
||||
|
||||
/* 主题色:原生取色器 */
|
||||
.fc-color-row {
|
||||
display: flex;
|
||||
@@ -117,28 +118,60 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: nowrap;
|
||||
gap: 10px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
padding: 8px 14px;
|
||||
min-height: 54px;
|
||||
min-height: 58px;
|
||||
}
|
||||
.fc-current-icon-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
min-width: 0; /* 关键:允许flex子项收缩,防止内容撑破布局 */
|
||||
overflow: hidden;
|
||||
}
|
||||
.fc-current-icon-info img {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
flex: 0 0 44px; /* 固定尺寸,不参与收缩 */
|
||||
object-fit: cover;
|
||||
background: #ffffff;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 2px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.fc-current-icon-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0; /* 允许文字区域收缩 */
|
||||
overflow: hidden;
|
||||
}
|
||||
.fc-current-icon-name {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.fc-current-icon-tip {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.fc-icon-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex: 0 0 auto; /* 按钮区域不收缩 */
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.fc-icon-actions .btn { border-radius: 9px; font-weight: 600; }
|
||||
.fc-btn-main {
|
||||
@@ -148,6 +181,10 @@
|
||||
font-weight: 700 !important;
|
||||
box-shadow: 0 3px 8px rgba(0, 210, 106, 0.28);
|
||||
}
|
||||
.fc-btn-main:hover {
|
||||
background: linear-gradient(135deg, #00d26a, #00b856) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
/* 推荐调色板 */
|
||||
.fc-color-presets {
|
||||
display: flex;
|
||||
@@ -165,6 +202,7 @@
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.fc-color-dot:hover { transform: scale(1.2); }
|
||||
|
||||
/* 右侧迷你小程序模拟器 */
|
||||
.fc-preview-box {
|
||||
position: sticky;
|
||||
@@ -172,46 +210,71 @@
|
||||
background: linear-gradient(150deg, #f2fef7 0%, #e6fbf1 100%);
|
||||
border: 1px solid #c8f2de;
|
||||
border-radius: 16px;
|
||||
padding: 18px 14px;
|
||||
padding: 20px 16px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 14px rgba(4, 48, 30, 0.05);
|
||||
}
|
||||
.fc-preview-title {
|
||||
font-size: 12px;
|
||||
color: #04874a;
|
||||
color: #00a854;
|
||||
font-weight: 700;
|
||||
margin-bottom: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.fc-preview-badge {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
border-radius: 18px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 8px;
|
||||
margin: 0 auto 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(4, 48, 30, 0.08);
|
||||
padding: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.fc-preview-badge img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.fc-preview-name {
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.fc-preview-kind-pill {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.fc-preview-kind-pill.general {
|
||||
background: #e6fbf1;
|
||||
color: #00a854;
|
||||
border: 1px solid #a7f3d0;
|
||||
}
|
||||
.fc-preview-kind-pill.farm {
|
||||
background: #fef3c7;
|
||||
color: #b45309;
|
||||
border: 1px solid #fde68a;
|
||||
}
|
||||
.fc-preview-note {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
margin-top: 10px;
|
||||
line-height: 1.6;
|
||||
border-top: 1px dashed #d1fae5;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
/* 插画选择模态窗口 */
|
||||
/* 图标挑选模态窗口 */
|
||||
.fc-icon-modal-wrap {
|
||||
display: none;
|
||||
position: fixed;
|
||||
@@ -239,10 +302,13 @@
|
||||
position: relative;
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
width: 660px;
|
||||
width: 760px;
|
||||
max-width: 95%;
|
||||
max-height: 88vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25);
|
||||
padding: 18px 22px 20px;
|
||||
padding: 18px 22px 16px;
|
||||
z-index: 2000;
|
||||
animation: fcModalZoom 0.2s ease-out;
|
||||
}
|
||||
@@ -252,12 +318,21 @@
|
||||
justify-content: space-between;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.fc-modal-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.fc-modal-title small {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.fc-modal-close {
|
||||
width: 28px;
|
||||
@@ -271,47 +346,163 @@
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.fc-modal-close:hover {
|
||||
background: #e2e8f0;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
/* 模态窗工具栏:搜索与分类标签 */
|
||||
.fc-modal-toolbar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.fc-modal-search-box {
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.fc-modal-search-box i {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
}
|
||||
.fc-modal-search-input {
|
||||
width: 100%;
|
||||
padding: 7px 12px 7px 34px;
|
||||
font-size: 12.5px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 9px;
|
||||
outline: none;
|
||||
background: #f8fafc;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.fc-modal-search-input:focus {
|
||||
border-color: #00d26a;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 2px rgba(0, 210, 106, 0.15);
|
||||
}
|
||||
.fc-modal-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.fc-icon-tab-btn {
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.fc-icon-tab-btn:hover {
|
||||
color: #00a854;
|
||||
border-color: #a7f3d0;
|
||||
}
|
||||
.fc-icon-tab-btn.active {
|
||||
background: #00d26a;
|
||||
color: #ffffff;
|
||||
border-color: #00d26a;
|
||||
box-shadow: 0 2px 6px rgba(0, 210, 106, 0.25);
|
||||
}
|
||||
|
||||
/* 图标芯片网格 */
|
||||
.fc-modal-grid-box {
|
||||
max-height: 360px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 2px 4px 6px;
|
||||
padding: 4px 2px 10px;
|
||||
min-height: 240px;
|
||||
max-height: 380px;
|
||||
}
|
||||
.fc-modal-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
.fc-icon-chip {
|
||||
flex: 0 0 calc(16.666% - 10px);
|
||||
height: 72px;
|
||||
flex: 0 0 calc(20% - 8px);
|
||||
min-width: 110px;
|
||||
height: 96px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
cursor: pointer;
|
||||
padding: 8px 6px;
|
||||
position: relative;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.fc-icon-chip:hover {
|
||||
border-color: #00E070;
|
||||
background: #f0fdf4;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 10px rgba(0, 210, 106, 0.15);
|
||||
}
|
||||
.fc-icon-chip.active {
|
||||
border-color: #00E070;
|
||||
border-color: #00D26A;
|
||||
background: #e6fbf1;
|
||||
box-shadow: 0 0 0 2px #00E070;
|
||||
box-shadow: 0 0 0 2px #00D26A;
|
||||
}
|
||||
.fc-icon-chip.active::after {
|
||||
content: "\f00c";
|
||||
font-family: FontAwesome;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 4px;
|
||||
font-size: 10px;
|
||||
color: #ffffff;
|
||||
background: #00D26A;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.fc-icon-chip img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.fc-icon-chip.is-category-top img {
|
||||
object-fit: contain;
|
||||
}
|
||||
.fc-icon-chip .fc-chip-name {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
.fc-modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.fc-modal-footer-tip {
|
||||
font-size: 11.5px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
@keyframes fcModalZoom {
|
||||
from { opacity: 0; transform: scale(0.95); }
|
||||
from { opacity: 0; transform: scale(0.96); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
</style>
|
||||
@@ -323,13 +514,13 @@
|
||||
<div class="fc-section">
|
||||
<div class="fc-section-title">
|
||||
<span class="fc-sec-icon"><i class="fa fa-sitemap"></i></span> 归属与类型
|
||||
<span class="fc-sec-tip">决定发布页展示哪些字段</span>
|
||||
<span class="fc-sec-tip">决定前台分类层级与发布表单</span>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:14px;">
|
||||
<label class="control-label col-xs-12 col-sm-3">上级分类:</label>
|
||||
<div class="col-xs-12 col-sm-9">
|
||||
{:build_select('row[pid]', $parentList, $row.pid, ['class'=>'form-control selectpicker', 'id'=>'c-pid', 'data-rule'=>'required'])}
|
||||
<span class="help-block text-muted" style="margin-top:4px;margin-bottom:0;font-size:11px;">选「顶级分类」创建一级大类;选「蔬菜」「水果」则创建其下属二级品类。</span>
|
||||
<span class="help-block text-muted" style="margin-top:4px;margin-bottom:0;font-size:11px;">选「顶级分类」创建一级大类频道;选指定分类(如「招聘专区」「肉蛋家禽」)则创建下属二级品类。</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:10px;">
|
||||
@@ -337,15 +528,15 @@
|
||||
<div class="col-xs-12 col-sm-9">
|
||||
<div class="fc-kind-seg">
|
||||
<label>
|
||||
<input type="radio" name="row[kind]" value="farm" {$row.kind == 'farm' ? 'checked' : ''}>
|
||||
<span class="fc-pill farm"><b><i class="fa fa-leaf"></i> 农产品</b><small>发布时需填写种植面积、采摘时间、量产</small></span>
|
||||
<input type="radio" name="row[kind]" value="general" {$row.kind != 'farm' ? 'checked' : ''}>
|
||||
<span class="fc-pill general"><b><i class="fa fa-briefcase"></i> 通用生活信息</b><small>招聘求职、便民服务、房产二手等常用分类</small></span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="row[kind]" value="general" {$row.kind != 'farm' ? 'checked' : ''}>
|
||||
<span class="fc-pill general"><b><i class="fa fa-briefcase"></i> 通用信息</b><small>招聘求职等,无产地字段</small></span>
|
||||
<input type="radio" name="row[kind]" value="farm" {$row.kind == 'farm' ? 'checked' : ''}>
|
||||
<span class="fc-pill farm"><b><i class="fa fa-leaf"></i> 农特果蔬产品</b><small>发布时可填写种植面积、采摘时间、产量等</small></span>
|
||||
</label>
|
||||
</div>
|
||||
<span class="help-block text-muted" style="margin-top:8px;margin-bottom:0;font-size:11px;">修改一级分类类型时,下属二级分类自动同步。</span>
|
||||
<span class="help-block text-muted" style="margin-top:8px;margin-bottom:0;font-size:11px;">修改一级分类类型时,下属二级分类将自动同步。</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -358,26 +549,30 @@
|
||||
<div class="form-group" style="margin-bottom:14px;">
|
||||
<label class="control-label col-xs-12 col-sm-3">分类名称:</label>
|
||||
<div class="col-xs-12 col-sm-9">
|
||||
<input id="c-name" data-rule="required" class="form-control" name="row[name]" type="text" value="{$row.name|htmlentities}" placeholder="例如:蔬菜、水果 或 花菜、白菜、紫甘蓝">
|
||||
<input id="c-name" data-rule="required" class="form-control" name="row[name]" type="text" value="{$row.name|htmlentities}" placeholder="例如:招聘求职、骑手配送、鲜土鸡蛋 或 苹果、白菜">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:14px;">
|
||||
<label class="control-label col-xs-12 col-sm-3">实拍图:</label>
|
||||
<label class="control-label col-xs-12 col-sm-3">分类图标/图片:</label>
|
||||
<div class="col-xs-12 col-sm-9">
|
||||
<div class="fc-current-icon-box">
|
||||
<div class="fc-current-icon-info">
|
||||
<img id="current-icon-img" src="/static/categories/{$row.icon_key|htmlentities}.png?v=2" onerror="this.src='/static/categories/cat-veg.png'" alt="当前图片">
|
||||
<img id="current-icon-img" src="{$row.image|default='/static/categories/cat-job.png'}" onerror="this.src='/static/categories/cat-job.png'" alt="当前图片">
|
||||
<div class="fc-current-icon-text">
|
||||
<span class="fc-current-icon-name" id="current-icon-label">{$row.name|htmlentities}</span>
|
||||
<span class="fc-current-icon-tip">当前设置标识:{$row.icon_key|htmlentities}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fc-icon-actions">
|
||||
<button type="button" class="btn btn-success fc-btn-main" id="btn-open-icon-picker">
|
||||
<i class="fa fa-th-large"></i> 挑选果蔬实拍图
|
||||
<i class="fa fa-th-large"></i> 挑选预设图片
|
||||
</button>
|
||||
<button type="button" id="faupload-icon_key" class="btn btn-default faupload" data-input-id="c-icon_key" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/svg+xml" data-maxsize="500KB" data-multiple="false">
|
||||
<i class="fa fa-upload text-primary"></i> 上传实拍图
|
||||
<i class="fa fa-upload text-primary"></i> 上传自定义实拍图
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span class="help-block text-muted" style="margin-top:4px;margin-bottom:0;font-size:11px;">优先上传真实农产品照片,农民用户靠「看图识物」选择品类;建议 500×500 以上方形图,单张不超过 <b class="text-danger">500KB</b>,过大的图片会拖慢小程序加载。</span>
|
||||
<span class="help-block text-muted" style="margin-top:4px;margin-bottom:0;font-size:11px;">推荐从平台预设图片库中直接选用;也支持上传高清实拍方形图(建议 200×200 以上,单张不超过 <b class="text-danger">500KB</b>)。</span>
|
||||
<input type="hidden" name="row[icon_key]" id="c-icon_key" value="{$row.icon_key|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
@@ -399,11 +594,11 @@
|
||||
<div class="fc-color-presets">
|
||||
<span style="font-size:11px;color:#64748b;">推荐:</span>
|
||||
<div class="fc-color-dot" style="background:#00E070;" title="春绿(品牌主色)" data-color="#00E070"></div>
|
||||
<div class="fc-color-dot" style="background:#3498DB;" title="晴空蓝" data-color="#3498DB"></div>
|
||||
<div class="fc-color-dot" style="background:#F5A623;" title="阳光金" data-color="#F5A623"></div>
|
||||
<div class="fc-color-dot" style="background:#FF7A59;" title="活力橙" data-color="#FF7A59"></div>
|
||||
<div class="fc-color-dot" style="background:#2ECC71;" title="翠青绿" data-color="#2ECC71"></div>
|
||||
<div class="fc-color-dot" style="background:#3498DB;" title="晴空蓝" data-color="#3498DB"></div>
|
||||
<div class="fc-color-dot" style="background:#9B59B6;" title="典雅紫" data-color="#9B59B6"></div>
|
||||
<div class="fc-color-dot" style="background:#10B981;" title="翡翠绿" data-color="#10B981"></div>
|
||||
<div class="fc-color-dot" style="background:#8B5CF6;" title="典雅紫" data-color="#8B5CF6"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -439,13 +634,14 @@
|
||||
<div class="col-xs-12 col-sm-4 hidden-xs">
|
||||
<div class="fc-preview-box">
|
||||
<div class="fc-preview-title">
|
||||
<i class="fa fa-mobile fa-lg"></i> 小程序端 · 实时预览
|
||||
<i class="fa fa-mobile fa-lg"></i> 小程序端 · 实时效果预览
|
||||
</div>
|
||||
<div class="fc-preview-badge" id="preview-badge" style="background: rgba(0, 224, 112, 0.15); border: 1px solid rgba(0, 224, 112, 0.35);">
|
||||
<img id="preview-icon" src="/static/categories/{$row.icon_key|htmlentities}.png?v=2" onerror="this.src='/static/categories/cat-veg.png'" alt="分类图片">
|
||||
<img id="preview-icon" src="{$row.image|default='/static/categories/cat-job.png'}" onerror="this.src='/static/categories/cat-job.png'" alt="分类图标">
|
||||
</div>
|
||||
<div class="fc-preview-name" id="preview-name">{$row.name|htmlentities}</div>
|
||||
<div class="fc-preview-note">修改名称 / 实拍图 / 主题色时<br>此预览会同步更新</div>
|
||||
<div><span class="fc-preview-kind-pill {$row.kind == 'farm' ? 'farm' : 'general'}" id="preview-kind">{$row.kind == 'farm' ? '农产品类目' : '通用便民类目'}</span></div>
|
||||
<div class="fc-preview-note">修改名称 / 图标 / 主题色时<br>此预览卡片将实时同步更新</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -459,38 +655,96 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 插画选择面板(默认完全隐藏,只有主动点击按钮才唤起) -->
|
||||
<!-- 预设插画图标选择模态窗口 -->
|
||||
<div class="fc-icon-modal-wrap" id="fc-icon-modal" style="display:none !important;">
|
||||
<div class="fc-modal-overlay"></div>
|
||||
<div class="fc-modal-card">
|
||||
<div class="fc-modal-header">
|
||||
<div class="fc-modal-title">
|
||||
<i class="fa fa-th-large text-success"></i> 选择分类实拍图
|
||||
<i class="fa fa-th-large text-success"></i> 挑选分类图标 / 实拍图
|
||||
<small>点击卡片即可选用</small>
|
||||
</div>
|
||||
<button type="button" class="fc-modal-close" id="btn-close-icon-modal">×</button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索与分类导航标签 -->
|
||||
<div class="fc-modal-toolbar">
|
||||
<div class="fc-modal-search-box">
|
||||
<i class="fa fa-search"></i>
|
||||
<input type="text" id="fc-icon-search" class="fc-modal-search-input" placeholder="输入名称快速筛选,例如:招聘、骑手、土鸡蛋、土鸡、苹果、白菜...">
|
||||
</div>
|
||||
<div class="fc-modal-tabs">
|
||||
<button type="button" class="fc-icon-tab-btn active" data-group="all">全部 (44)</button>
|
||||
<button type="button" class="fc-icon-tab-btn" data-group="top">一级大类 (8)</button>
|
||||
<button type="button" class="fc-icon-tab-btn" data-group="job">招聘职位 (8)</button>
|
||||
<button type="button" class="fc-icon-tab-btn" data-group="poultry">肉蛋水产 (5)</button>
|
||||
<button type="button" class="fc-icon-tab-btn" data-group="fruit">新鲜水果 (15)</button>
|
||||
<button type="button" class="fc-icon-tab-btn" data-group="veg">时令蔬菜 (8)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图标网格卡片 -->
|
||||
<div class="fc-modal-grid-box">
|
||||
<div class="fc-modal-grid">
|
||||
<div class="fc-icon-chip" data-key="cat-veg" title="新鲜蔬菜"><img src="/static/categories/cat-veg.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="cat-fruit" title="时令水果"><img src="/static/categories/cat-fruit.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="veg-cauliflower" title="花菜"><img src="/static/categories/veg-cauliflower.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="veg-cabbage" title="白菜"><img src="/static/categories/veg-cabbage.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="veg-purple-cabbage" title="紫甘蓝"><img src="/static/categories/veg-purple-cabbage.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="veg-green-cabbage" title="青甘蓝"><img src="/static/categories/veg-green-cabbage.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="veg-greens" title="青菜"><img src="/static/categories/veg-greens.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="veg-radish" title="萝卜"><img src="/static/categories/veg-radish.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="veg-chili" title="辣椒"><img src="/static/categories/veg-chili.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="veg-tomato" title="西红柿"><img src="/static/categories/veg-tomato.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="fruit-apple" title="苹果"><img src="/static/categories/fruit-apple.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="fruit-orange" title="柑橘"><img src="/static/categories/fruit-orange.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="fruit-kiwi" title="猕猴桃"><img src="/static/categories/fruit-kiwi.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="fruit-strawberry" title="草莓"><img src="/static/categories/fruit-strawberry.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="fruit-watermelon" title="西瓜"><img src="/static/categories/fruit-watermelon.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="fruit-grape" title="葡萄"><img src="/static/categories/fruit-grape.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="fruit-peach" title="蜜桃"><img src="/static/categories/fruit-peach.png?v=2" alt="icon"></div>
|
||||
<div class="fc-icon-chip" data-key="fruit-passion" title="百香果"><img src="/static/categories/fruit-passion.png?v=2" alt="icon"></div>
|
||||
<!-- 1. 一级大类频道 (8) -->
|
||||
<div class="fc-icon-chip is-category-top" data-group="top" data-key="cat-job" data-name="招聘专区" data-color="#00D26A" title="招聘专区"><img src="/static/categories/cat-job.png" alt="icon"><span class="fc-chip-name">招聘专区</span></div>
|
||||
<div class="fc-icon-chip is-category-top" data-group="top" data-key="cat-poultry" data-name="肉蛋家禽" data-color="#FF7A59" title="肉蛋家禽"><img src="/static/categories/cat-poultry.png" alt="icon"><span class="fc-chip-name">肉蛋家禽</span></div>
|
||||
<div class="fc-icon-chip is-category-top" data-group="top" data-key="cat-veg" data-name="蔬菜专区" data-color="#00E070" title="蔬菜专区"><img src="/static/categories/cat-veg.png" alt="icon"><span class="fc-chip-name">蔬菜专区</span></div>
|
||||
<div class="fc-icon-chip is-category-top" data-group="top" data-key="cat-fruit" data-name="水果专区" data-color="#F5A623" title="水果专区"><img src="/static/categories/cat-fruit.png" alt="icon"><span class="fc-chip-name">水果专区</span></div>
|
||||
<div class="fc-icon-chip is-category-top" data-group="top" data-key="cat-service" data-name="便民服务" data-color="#10B981" title="便民服务"><img src="/static/icons/cat-service.png" alt="icon"><span class="fc-chip-name">便民服务</span></div>
|
||||
<div class="fc-icon-chip is-category-top" data-group="top" data-key="cat-house" data-name="房产租售" data-color="#3498DB" title="房产租售"><img src="/static/icons/cat-house.png" alt="icon"><span class="fc-chip-name">房产租售</span></div>
|
||||
<div class="fc-icon-chip is-category-top" data-group="top" data-key="cat-goods" data-name="二手闲置" data-color="#F5A623" title="二手闲置"><img src="/static/icons/cat-goods.png" alt="icon"><span class="fc-chip-name">二手闲置</span></div>
|
||||
<div class="fc-icon-chip is-category-top" data-group="top" data-key="cat-car" data-name="车辆交易" data-color="#FF7A59" title="车辆交易"><img src="/static/icons/cat-car.png" alt="icon"><span class="fc-chip-name">车辆交易</span></div>
|
||||
|
||||
<!-- 2. 招聘职位 (8) -->
|
||||
<div class="fc-icon-chip" data-group="job" data-key="/uploads/20261028/3fb1eee6654e4d5e7c1b5d3189643fae.jpg" data-name="骑手配送" data-color="#F5A623" title="骑手配送"><img src="/uploads/20261028/3fb1eee6654e4d5e7c1b5d3189643fae.jpg" alt="icon"><span class="fc-chip-name">骑手配送</span></div>
|
||||
<div class="fc-icon-chip" data-group="job" data-key="/uploads/20261028/f5e2927ba040dd1b917b6fbd6b053458.jpg" data-name="司机出行" data-color="#3498DB" title="司机出行"><img src="/uploads/20261028/f5e2927ba040dd1b917b6fbd6b053458.jpg" alt="icon"><span class="fc-chip-name">司机出行</span></div>
|
||||
<div class="fc-icon-chip" data-group="job" data-key="/uploads/20261028/cb17be7476ce1c59e14944f9fe60438c.jpg" data-name="普工技工" data-color="#00D26A" title="普工技工"><img src="/uploads/20261028/cb17be7476ce1c59e14944f9fe60438c.jpg" alt="icon"><span class="fc-chip-name">普工技工</span></div>
|
||||
<div class="fc-icon-chip" data-group="job" data-key="/uploads/20261028/4e4f911ac45e9dc5471597a8ff7862f7.jpg" data-name="餐饮服务" data-color="#EF4444" title="餐饮服务"><img src="/uploads/20261028/4e4f911ac45e9dc5471597a8ff7862f7.jpg" alt="icon"><span class="fc-chip-name">餐饮服务</span></div>
|
||||
<div class="fc-icon-chip" data-group="job" data-key="/uploads/20261028/915423715a937ad7ba4b737690c963d2.jpg" data-name="家政保洁" data-color="#10B981" title="家政保洁"><img src="/uploads/20261028/915423715a937ad7ba4b737690c963d2.jpg" alt="icon"><span class="fc-chip-name">家政保洁</span></div>
|
||||
<div class="fc-icon-chip" data-group="job" data-key="/uploads/20261028/d0cc33f85a8580be2f526eb360448867.jpg" data-name="销售客服" data-color="#8B5CF6" title="销售客服"><img src="/uploads/20261028/d0cc33f85a8580be2f526eb360448867.jpg" alt="icon"><span class="fc-chip-name">销售客服</span></div>
|
||||
<div class="fc-icon-chip" data-group="job" data-key="/uploads/20261028/99c32f9508e3429ea02a4ce1c34f9df2.jpg" data-name="教育培训" data-color="#06B6D4" title="教育培训"><img src="/uploads/20261028/99c32f9508e3429ea02a4ce1c34f9df2.jpg" alt="icon"><span class="fc-chip-name">教育培训</span></div>
|
||||
<div class="fc-icon-chip" data-group="job" data-key="/uploads/20261028/0c913f9d7d72a10c0c598c36eea89b14.jpg" data-name="兼职短工" data-color="#EC4899" title="兼职短工"><img src="/uploads/20261028/0c913f9d7d72a10c0c598c36eea89b14.jpg" alt="icon"><span class="fc-chip-name">兼职短工</span></div>
|
||||
|
||||
<!-- 3. 肉蛋水产 (5) -->
|
||||
<div class="fc-icon-chip" data-group="poultry" data-key="/uploads/20261028/69b65e455081d5aa8834b48bd879009d.jpg" data-name="鲜土鸡蛋" data-color="#F5A623" title="鲜土鸡蛋"><img src="/uploads/20261028/69b65e455081d5aa8834b48bd879009d.jpg" alt="icon"><span class="fc-chip-name">鲜土鸡蛋</span></div>
|
||||
<div class="fc-icon-chip" data-group="poultry" data-key="/uploads/20261028/cafbe884f6616622dcdd5650bbbce3b3.jpg" data-name="散养土鸡" data-color="#F97316" title="散养土鸡"><img src="/uploads/20261028/cafbe884f6616622dcdd5650bbbce3b3.jpg" alt="icon"><span class="fc-chip-name">散养土鸡</span></div>
|
||||
<div class="fc-icon-chip" data-group="poultry" data-key="/uploads/20261028/d2a3226fd6ffa12d24db06c9342a9118.jpg" data-name="生鲜猪肉" data-color="#EA580C" title="生鲜猪肉"><img src="/uploads/20261028/d2a3226fd6ffa12d24db06c9342a9118.jpg" alt="icon"><span class="fc-chip-name">生鲜猪肉</span></div>
|
||||
<div class="fc-icon-chip" data-group="poultry" data-key="/uploads/20261028/70cafc924bc7e4018bfefbe883e27c78.jpg" data-name="高山牛肉" data-color="#DC2626" title="高山牛肉"><img src="/uploads/20261028/70cafc924bc7e4018bfefbe883e27c78.jpg" alt="icon"><span class="fc-chip-name">高山牛肉</span></div>
|
||||
<div class="fc-icon-chip" data-group="poultry" data-key="/uploads/20261028/bb63e21dae84dc15056a0b50753577d9.jpg" data-name="鲜活水产" data-color="#0284C7" title="鲜活水产"><img src="/uploads/20261028/bb63e21dae84dc15056a0b50753577d9.jpg" alt="icon"><span class="fc-chip-name">鲜活水产</span></div>
|
||||
|
||||
<!-- 4. 新鲜水果 (15) -->
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261020/d29f6003464df81337954f2e3b9213de.jpg" data-name="红富士苹果" data-color="#E11D48" title="红富士苹果"><img src="/uploads/20261020/d29f6003464df81337954f2e3b9213de.jpg" alt="icon"><span class="fc-chip-name">苹果</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261020/0c0115e6946ec7ac4c2fe7088bdbe0d7.jpg" data-name="柑橘橙子" data-color="#F97316" title="柑橘橙子"><img src="/uploads/20261020/0c0115e6946ec7ac4c2fe7088bdbe0d7.jpg" alt="icon"><span class="fc-chip-name">柑橘</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261020/74d6e0301b727fe2c48638ebd7989fa8.jpg" data-name="新鲜猕猴桃" data-color="#65A30D" title="新鲜猕猴桃"><img src="/uploads/20261020/74d6e0301b727fe2c48638ebd7989fa8.jpg" alt="icon"><span class="fc-chip-name">猕猴桃</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261020/ebb126f270081d28162e90fd240cd481.jpg" data-name="鲜采草莓" data-color="#F43F5E" title="鲜采草莓"><img src="/uploads/20261020/ebb126f270081d28162e90fd240cd481.jpg" alt="icon"><span class="fc-chip-name">草莓</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261020/0e6e5dbbbd77f31315e643c2e4915d9a.jpg" data-name="甜脆西瓜" data-color="#16A34A" title="甜脆西瓜"><img src="/uploads/20261020/0e6e5dbbbd77f31315e643c2e4915d9a.jpg" alt="icon"><span class="fc-chip-name">西瓜</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261020/b0af7b62d055688ef020e9dab794373d.jpg" data-name="提子葡萄" data-color="#7E22CE" title="提子葡萄"><img src="/uploads/20261020/b0af7b62d055688ef020e9dab794373d.jpg" alt="icon"><span class="fc-chip-name">葡萄</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261020/2b709d7c76d07516758df3558d83a56a.jpg" data-name="鲜桃蜜桃" data-color="#EC4899" title="鲜桃蜜桃"><img src="/uploads/20261020/2b709d7c76d07516758df3558d83a56a.jpg" alt="icon"><span class="fc-chip-name">蜜桃</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261020/c994cb47610ed3514571db794b1ee724.jpg" data-name="新鲜百香果" data-color="#9333EA" title="新鲜百香果"><img src="/uploads/20261020/c994cb47610ed3514571db794b1ee724.jpg" alt="icon"><span class="fc-chip-name">百香果</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261028/f1a4f3c123c81aea2f0fd24c8688d988.jpg" data-name="香甜芒果" data-color="#F5A623" title="香甜芒果"><img src="/uploads/20261028/f1a4f3c123c81aea2f0fd24c8688d988.jpg" alt="icon"><span class="fc-chip-name">芒果</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261028/d8b5420c12f7921e325050be088a011f.jpg" data-name="香蕉" data-color="#FBBF24" title="香蕉"><img src="/uploads/20261028/d8b5420c12f7921e325050be088a011f.jpg" alt="icon"><span class="fc-chip-name">香蕉</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261028/75dd5971bdfa99d64a62c5b1e72cac11.jpg" data-name="蓝莓" data-color="#3B82F6" title="蓝莓"><img src="/uploads/20261028/75dd5971bdfa99d64a62c5b1e72cac11.jpg" alt="icon"><span class="fc-chip-name">蓝莓</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261028/e71ba37749da829c50d098168dbb4fa2.jpg" data-name="车厘子樱桃" data-color="#DC2626" title="车厘子"><img src="/uploads/20261028/e71ba37749da829c50d098168dbb4fa2.jpg" alt="icon"><span class="fc-chip-name">车厘子</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261028/65d91dba8121f8b49356f356aa6a3b28.jpg" data-name="红心火龙果" data-color="#EC4899" title="火龙果"><img src="/uploads/20261028/65d91dba8121f8b49356f356aa6a3b28.jpg" alt="icon"><span class="fc-chip-name">火龙果</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261028/0391292554cf8efd457c44caedf67867.jpg" data-name="新鲜柠檬" data-color="#EAB308" title="柠檬"><img src="/uploads/20261028/0391292554cf8efd457c44caedf67867.jpg" alt="icon"><span class="fc-chip-name">柠檬</span></div>
|
||||
<div class="fc-icon-chip" data-group="fruit" data-key="/uploads/20261028/49b39ce93043ec8a6f24bcef9bc45f96.jpg" data-name="新鲜荔枝" data-color="#EF4444" title="荔枝"><img src="/uploads/20261028/49b39ce93043ec8a6f24bcef9bc45f96.jpg" alt="icon"><span class="fc-chip-name">荔枝</span></div>
|
||||
|
||||
<!-- 5. 时令蔬菜 (8) -->
|
||||
<div class="fc-icon-chip" data-group="veg" data-key="/uploads/20261020/11fe77af7af7a4b2239a20a189fca471.jpg" data-name="高山白菜" data-color="#00D26A" title="高山白菜"><img src="/uploads/20261020/11fe77af7af7a4b2239a20a189fca471.jpg" alt="icon"><span class="fc-chip-name">白菜</span></div>
|
||||
<div class="fc-icon-chip" data-group="veg" data-key="/uploads/20261020/22e041f997661fbcbcf10ce86da3c01b.jpg" data-name="有机花菜" data-color="#00D26A" title="有机花菜"><img src="/uploads/20261020/22e041f997661fbcbcf10ce86da3c01b.jpg" alt="icon"><span class="fc-chip-name">花菜</span></div>
|
||||
<div class="fc-icon-chip" data-group="veg" data-key="/uploads/20261020/2bee40730c9f23595deecdfa24b206cf.jpg" data-name="新鲜青菜" data-color="#16A34A" title="新鲜青菜"><img src="/uploads/20261020/2bee40730c9f23595deecdfa24b206cf.jpg" alt="icon"><span class="fc-chip-name">青菜</span></div>
|
||||
<div class="fc-icon-chip" data-group="veg" data-key="/uploads/20261020/0604d246fb42bb569e8031b3a1c3c9ad.jpg" data-name="熟透西红柿" data-color="#EF4444" title="熟透西红柿"><img src="/uploads/20261020/0604d246fb42bb569e8031b3a1c3c9ad.jpg" alt="icon"><span class="fc-chip-name">西红柿</span></div>
|
||||
<div class="fc-icon-chip" data-group="veg" data-key="/uploads/20261020/8aac76e021aea0dd999ba5689ae86577.jpg" data-name="鲜红辣椒" data-color="#DC2626" title="鲜红辣椒"><img src="/uploads/20261020/8aac76e021aea0dd999ba5689ae86577.jpg" alt="icon"><span class="fc-chip-name">辣椒</span></div>
|
||||
<div class="fc-icon-chip" data-group="veg" data-key="/uploads/20261020/f7a5d6be1d58be88cdd0915c050f6028.jpg" data-name="脆甜白萝卜" data-color="#22C55E" title="脆甜白萝卜"><img src="/uploads/20261020/f7a5d6be1d58be88cdd0915c050f6028.jpg" alt="icon"><span class="fc-chip-name">萝卜</span></div>
|
||||
<div class="fc-icon-chip" data-group="veg" data-key="/uploads/20261020/1ad84fe2cef8ef22ef29ac45a898081c.jpg" data-name="新鲜紫甘蓝" data-color="#9333EA" title="新鲜紫甘蓝"><img src="/uploads/20261020/1ad84fe2cef8ef22ef29ac45a898081c.jpg" alt="icon"><span class="fc-chip-name">紫甘蓝</span></div>
|
||||
<div class="fc-icon-chip" data-group="veg" data-key="/uploads/20261020/b2cabf7db99e86a8ee8da424e19ebf9d.jpg" data-name="新鲜青甘蓝" data-color="#10B981" title="新鲜青甘蓝"><img src="/uploads/20261020/b2cabf7db99e86a8ee8da424e19ebf9d.jpg" alt="icon"><span class="fc-chip-name">青甘蓝</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fc-modal-footer">
|
||||
<span class="fc-modal-footer-tip"><i class="fa fa-info-circle text-success"></i> 点击任意图片卡片将自动设定,并可智能推荐分类名称与主题色</span>
|
||||
<button type="button" class="btn btn-default btn-sm" id="btn-cancel-modal" onclick="$('#btn-close-icon-modal').click();">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
<div class="panel-body">
|
||||
<div class="fc-cat-notice">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
<div>分类直接联动小程序<strong>首页金刚区</strong>与<strong>分类频道</strong>,按排序权重升序展示。点击开关可<strong>一键上架/下架</strong>,鼠标悬停实拍图可<strong>放大预览</strong>。</div>
|
||||
<div>分类直接联动小程序<strong>首页金刚区</strong>与<strong>分类频道</strong>,按排序权重升序展示。点击开关可<strong>一键上架/下架</strong>,鼠标悬停分类图标可<strong>放大预览</strong>。</div>
|
||||
</div>
|
||||
|
||||
<div id="myTabContent" class="tab-content">
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], function ($, undefined, Backend, Table, Form, Format) {
|
||||
|
||||
/**
|
||||
* categories 目录下的专属图标键名列表
|
||||
*/
|
||||
var CATEGORY_DIR_KEYS = [
|
||||
'cat-fruit', 'cat-poultry', 'cat-veg', 'cat-job',
|
||||
'fruit-apple', 'fruit-grape', 'fruit-kiwi', 'fruit-orange', 'fruit-passion', 'fruit-peach', 'fruit-strawberry', 'fruit-watermelon',
|
||||
'veg-cabbage', 'veg-chili', 'veg-green-cabbage', 'veg-greens', 'veg-purple-cabbage', 'veg-radish', 'veg-tomato'
|
||||
];
|
||||
|
||||
/**
|
||||
* 解析分类图标/实拍图地址
|
||||
* @param {string} iconKey 标识或路径
|
||||
@@ -12,13 +21,14 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
|
||||
return image;
|
||||
}
|
||||
if (!iconKey) {
|
||||
iconKey = 'cat-veg';
|
||||
iconKey = 'cat-job';
|
||||
}
|
||||
if (/^(https?:\/\/|\/uploads\/|\/static\/)/i.test(iconKey)) {
|
||||
return iconKey;
|
||||
}
|
||||
var baseKey = iconKey.replace(/\.(png|jpg|jpeg|gif|svg)$/i, '');
|
||||
var file = (iconKey.indexOf('.') !== -1) ? iconKey : iconKey + '.png';
|
||||
if (iconKey.indexOf('sub-') === 0 || iconKey === 'cat-veg' || iconKey === 'cat-fruit') {
|
||||
if (iconKey.indexOf('sub-') === 0 || CATEGORY_DIR_KEYS.indexOf(baseKey) !== -1 || /^(veg-|fruit-|cat-veg|cat-fruit|cat-job|cat-poultry)/i.test(baseKey)) {
|
||||
return '/static/categories/' + file + '?v=2';
|
||||
}
|
||||
return '/static/icons/' + file + '?v=2';
|
||||
@@ -63,10 +73,10 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
|
||||
formatter: Format.serial
|
||||
},
|
||||
|
||||
// 1. 实拍图预览(大图圆角卡,object-fit:cover 保证「看图识物」效果)
|
||||
// 1. 图标/实拍图预览(大图圆角卡,object-fit:cover)
|
||||
{
|
||||
field: 'image',
|
||||
title: '实拍图',
|
||||
title: '图标/图片',
|
||||
operate: false,
|
||||
align: 'center',
|
||||
width: '90px',
|
||||
@@ -74,10 +84,10 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
|
||||
var url = resolveIconUrl(row.icon_key, row.id, row.image);
|
||||
var color = row.color || '#00E070';
|
||||
return '<div class="fc-img-wrap">' +
|
||||
'<div style="display:inline-block;width:60px;height:60px;border-radius:14px;overflow:hidden;background:' + color + '15;border:1px solid ' + color + '40;box-shadow:0 2px 8px rgba(4,48,30,0.08);cursor:zoom-in;">' +
|
||||
'<img src="' + url + '" style="width:100%;height:100%;object-fit:cover;display:block;" onerror="this.src=\'/static/categories/cat-veg.png\'" />' +
|
||||
'<div style="display:inline-block;width:56px;height:56px;border-radius:14px;overflow:hidden;background:' + color + '15;border:1px solid ' + color + '40;box-shadow:0 2px 8px rgba(4,48,30,0.08);cursor:zoom-in;">' +
|
||||
'<img src="' + url + '" style="width:100%;height:100%;object-fit:cover;display:block;" onerror="this.src=\'/static/categories/cat-job.png\'" />' +
|
||||
'</div>' +
|
||||
'<div class="fc-img-zoom"><img src="' + url + '" onerror="this.src=\'/static/categories/cat-veg.png\'" /></div>' +
|
||||
'<div class="fc-img-zoom"><img src="' + url + '" onerror="this.src=\'/static/categories/cat-job.png\'" /></div>' +
|
||||
'</div>';
|
||||
}
|
||||
},
|
||||
@@ -224,24 +234,55 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
|
||||
var $previewBadge = $('#preview-badge');
|
||||
var $previewIcon = $('#preview-icon');
|
||||
var $previewName = $('#preview-name');
|
||||
var $previewKind = $('#preview-kind');
|
||||
var $currentIconImg = $('#current-icon-img');
|
||||
var $currentIconLabel = $('#current-icon-label');
|
||||
var $iconModal = $('#fc-icon-modal');
|
||||
var $iconSearch = $('#fc-icon-search');
|
||||
var $iconTabs = $('.fc-icon-tab-btn');
|
||||
|
||||
// 刷新界面预览元素(纯视觉图形,无死板业务文字)
|
||||
// 刷新界面预览元素
|
||||
function updateLivePreview() {
|
||||
var name = $nameInput.val() || '未命名分类';
|
||||
var key = $iconInput.val() || 'cat-veg';
|
||||
var key = $iconInput.val() || 'cat-job';
|
||||
var color = $colorInput.val() || '#00E070';
|
||||
var iconUrl = resolveIconUrl(key);
|
||||
var kind = $('input[name="row[kind]"]:checked').val() || 'general';
|
||||
|
||||
var $matchedChip = $('.fc-icon-chip[data-key="' + key + '"]');
|
||||
if ($matchedChip.length) {
|
||||
var chipImg = $matchedChip.find('img').attr('src');
|
||||
if (chipImg) {
|
||||
iconUrl = chipImg;
|
||||
}
|
||||
}
|
||||
|
||||
if ($previewName.length) $previewName.text(name);
|
||||
if ($previewIcon.length) $previewIcon.attr('src', iconUrl);
|
||||
if ($currentIconImg.length) $currentIconImg.attr('src', iconUrl);
|
||||
if ($currentIconLabel.length) {
|
||||
if ($matchedChip.length) {
|
||||
$currentIconLabel.text($matchedChip.data('name') || key);
|
||||
} else if (/^(\/uploads\/|https?:\/\/)/i.test(key)) {
|
||||
$currentIconLabel.text('自定义上传图片');
|
||||
} else {
|
||||
$currentIconLabel.text(key);
|
||||
}
|
||||
}
|
||||
|
||||
if ($previewKind.length) {
|
||||
if (kind === 'farm') {
|
||||
$previewKind.text('农产品类目').removeClass('general').addClass('farm');
|
||||
} else {
|
||||
$previewKind.text('通用便民类目').removeClass('farm').addClass('general');
|
||||
}
|
||||
}
|
||||
|
||||
if ($previewBadge.length) {
|
||||
$previewBadge.css({
|
||||
'background': color + '22',
|
||||
'border-color': color + '55'
|
||||
'background': color + '1a',
|
||||
'border-color': color + '4d',
|
||||
'box-shadow': '0 6px 16px ' + color + '26'
|
||||
});
|
||||
}
|
||||
if ($colorAddon.length) $colorAddon.css('background', color);
|
||||
@@ -251,11 +292,21 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
|
||||
// 确保初始状态完全隐藏
|
||||
$iconModal.removeClass('open').attr('style', 'display:none !important;');
|
||||
|
||||
// 1. 打开纯插画挑选面板(只有用户主动点击按钮才打开)
|
||||
// 1. 打开插画挑选面板
|
||||
$(document).on('click', '#btn-open-icon-picker', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$iconModal.removeAttr('style').addClass('open').fadeIn(150);
|
||||
// 打开时定位高亮当前图标
|
||||
var curKey = $iconInput.val();
|
||||
var $activeChip = $('.fc-icon-chip[data-key="' + curKey + '"]');
|
||||
$('.fc-icon-chip').removeClass('active');
|
||||
if ($activeChip.length) {
|
||||
$activeChip.addClass('active');
|
||||
}
|
||||
if ($iconSearch.length) {
|
||||
$iconSearch.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// 关闭插画挑选面板
|
||||
@@ -270,17 +321,81 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
|
||||
closeIconModal();
|
||||
});
|
||||
|
||||
// 2. 点选插画卡片(只设图标,绝不改写用户分类名称)
|
||||
// 2. 点选图标卡片
|
||||
$(document).on('click', '.fc-icon-chip', function (e) {
|
||||
e.preventDefault();
|
||||
var key = $(this).data('key');
|
||||
var iconName = $(this).data('name') || '';
|
||||
var iconColor = $(this).data('color') || '';
|
||||
|
||||
$iconInput.val(key).trigger('change');
|
||||
$('.fc-icon-chip').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
|
||||
// 若名称尚未输入,自动以图标名称填充
|
||||
if ($nameInput.length && !$nameInput.val().trim() && iconName) {
|
||||
$nameInput.val(iconName).trigger('input');
|
||||
}
|
||||
|
||||
// 若当前主题色为默认春绿且图标带有推荐色,可智能联动
|
||||
if (iconColor && ($colorInput.val() === '#00E070' || !$colorInput.val())) {
|
||||
$colorInput.val(iconColor).trigger('change');
|
||||
}
|
||||
|
||||
updateLivePreview();
|
||||
closeIconModal();
|
||||
});
|
||||
|
||||
// 2.1 图标分类 Tab 切换
|
||||
$(document).on('click', '.fc-icon-tab-btn', function (e) {
|
||||
e.preventDefault();
|
||||
$('.fc-icon-tab-btn').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
var targetGroup = $(this).data('group');
|
||||
filterChips();
|
||||
});
|
||||
|
||||
// 2.2 图标搜索过滤
|
||||
if ($iconSearch.length) {
|
||||
$iconSearch.on('input', function () {
|
||||
filterChips();
|
||||
});
|
||||
}
|
||||
|
||||
// 过滤卡片执行逻辑
|
||||
function filterChips() {
|
||||
var group = $('.fc-icon-tab-btn.active').data('group') || 'all';
|
||||
var keyword = ($iconSearch.val() || '').trim().toLowerCase();
|
||||
var visibleCount = 0;
|
||||
|
||||
$('.fc-icon-chip').each(function () {
|
||||
var chipGroup = $(this).data('group') || '';
|
||||
var chipKey = ($(this).data('key') || '').toLowerCase();
|
||||
var chipName = ($(this).data('name') || '').toLowerCase();
|
||||
|
||||
var matchGroup = (group === 'all' || chipGroup === group);
|
||||
var matchKeyword = (!keyword || chipName.indexOf(keyword) !== -1 || chipKey.indexOf(keyword) !== -1);
|
||||
|
||||
if (matchGroup && matchKeyword) {
|
||||
$(this).show();
|
||||
visibleCount++;
|
||||
} else {
|
||||
$(this).hide();
|
||||
}
|
||||
});
|
||||
|
||||
var $emptyTip = $('#fc-icon-empty');
|
||||
if (visibleCount === 0) {
|
||||
if (!$emptyTip.length) {
|
||||
$('.fc-modal-grid').append('<div id="fc-icon-empty" style="width:100%;text-align:center;padding:32px 10px;color:#94a3b8;font-size:13px;"><i class="fa fa-folder-open-o" style="font-size:24px;display:block;margin-bottom:8px;color:#cbd5e1;"></i>未找到匹配的预设图标,您可直接使用「上传自定义图标」</div>');
|
||||
} else {
|
||||
$emptyTip.show();
|
||||
}
|
||||
} else if ($emptyTip.length) {
|
||||
$emptyTip.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 自定义上传图片监听
|
||||
$iconInput.on('input change', function () {
|
||||
updateLivePreview();
|
||||
@@ -291,6 +406,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
|
||||
updateLivePreview();
|
||||
});
|
||||
|
||||
// 4.1 监听分类类型单选变更
|
||||
$('input[name="row[kind]"]').on('change', function () {
|
||||
updateLivePreview();
|
||||
});
|
||||
|
||||
// 5. 监听颜色输入与推荐调色板点选
|
||||
$colorInput.on('input change', function () {
|
||||
updateLivePreview();
|
||||
@@ -308,7 +428,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
|
||||
});
|
||||
|
||||
// 初始化回显
|
||||
var initKey = $iconInput.val() || 'cat-veg';
|
||||
var initKey = $iconInput.val() || 'cat-service';
|
||||
$('.fc-icon-chip[data-key="' + initKey + '"]').addClass('active');
|
||||
updateLivePreview();
|
||||
}
|
||||
|
||||
二进制
public/static/categories/cat-job.png
普通文件
|
之后 宽度: | 高度: | 大小: 5.0 KiB |
|
之后 宽度: | 高度: | 大小: 6.5 KiB |
二进制
public/static/icons/cat-poultry.png
普通文件
|
之后 宽度: | 高度: | 大小: 6.5 KiB |
|
之后 宽度: | 高度: | 大小: 14 KiB |
|
之后 宽度: | 高度: | 大小: 22 KiB |
|
之后 宽度: | 高度: | 大小: 15 KiB |
|
之后 宽度: | 高度: | 大小: 11 KiB |
|
之后 宽度: | 高度: | 大小: 16 KiB |
|
之后 宽度: | 高度: | 大小: 20 KiB |
|
之后 宽度: | 高度: | 大小: 10 KiB |
|
之后 宽度: | 高度: | 大小: 12 KiB |
|
之后 宽度: | 高度: | 大小: 13 KiB |
|
之后 宽度: | 高度: | 大小: 9.9 KiB |
|
之后 宽度: | 高度: | 大小: 13 KiB |
|
之后 宽度: | 高度: | 大小: 5.9 KiB |
|
之后 宽度: | 高度: | 大小: 13 KiB |
|
之后 宽度: | 高度: | 大小: 12 KiB |
|
之后 宽度: | 高度: | 大小: 7.5 KiB |
|
之后 宽度: | 高度: | 大小: 19 KiB |
|
之后 宽度: | 高度: | 大小: 5.3 KiB |
|
之后 宽度: | 高度: | 大小: 4.0 KiB |
|
之后 宽度: | 高度: | 大小: 4.9 KiB |
|
之后 宽度: | 高度: | 大小: 10 KiB |
112
tools/add_more_fruits.php
普通文件
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
/**
|
||||
* 为水果专区 (PID: 2) 补充 8 个真实实拍二级分类,模拟上传至 /uploads/20261028/
|
||||
* 并同步到本地和线上数据库与服务器
|
||||
*/
|
||||
|
||||
$targetDir = __DIR__ . '/../public/uploads/20261028';
|
||||
if (!is_dir($targetDir)) {
|
||||
mkdir($targetDir, 0755, true);
|
||||
}
|
||||
|
||||
$fruits = [
|
||||
[
|
||||
'name' => '芒果',
|
||||
'filename' => 'fruit-mango.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1553279768-865429fa0078?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
[
|
||||
'name' => '香蕉',
|
||||
'filename' => 'fruit-banana.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1571771894821-ce9b6c11b08e?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
[
|
||||
'name' => '蓝莓',
|
||||
'filename' => 'fruit-blueberry.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1498557850523-fd3d118b962e?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
[
|
||||
'name' => '车厘子',
|
||||
'filename' => 'fruit-cherry.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1528825871115-3581a5387919?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
[
|
||||
'name' => '哈密瓜',
|
||||
'filename' => 'fruit-melon.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1571575173700-afb9492e8c50?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
[
|
||||
'name' => '火龙果',
|
||||
'filename' => 'fruit-dragon.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1527324688151-0e627063f2b1?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
[
|
||||
'name' => '柠檬',
|
||||
'filename' => 'fruit-lemon.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1533089860892-a7c6f0a88666?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
[
|
||||
'name' => '荔枝',
|
||||
'filename' => 'fruit-lychee.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1596386461350-326ccb383e9f?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
];
|
||||
|
||||
function cropToSquare240($binaryData, $savePath)
|
||||
{
|
||||
$src = @imagecreatefromstring($binaryData);
|
||||
if (!$src) return false;
|
||||
$srcW = imagesx($src);
|
||||
$srcH = imagesy($src);
|
||||
$cropSize = min($srcW, $srcH);
|
||||
$cropX = (int)(($srcW - $cropSize) / 2);
|
||||
$cropY = (int)(($srcH - $cropSize) / 2);
|
||||
$dst = imagecreatetruecolor(240, 240);
|
||||
imagecopyresampled($dst, $src, 0, 0, $cropX, $cropY, 240, 240, $cropSize, $cropSize);
|
||||
$res = imagejpeg($dst, $savePath, 82);
|
||||
imagedestroy($src);
|
||||
imagedestroy($dst);
|
||||
return $res;
|
||||
}
|
||||
|
||||
echo "=== [1/3] 下载真实水果实拍照并裁剪为 240x240 ===\n";
|
||||
$downloaded = [];
|
||||
foreach ($fruits as $f) {
|
||||
echo "正在下载 [{$f['name']}] 实拍图: {$f['url']} ...\n";
|
||||
$opts = [
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'header' => "User-Agent: Mozilla/5.0\r\n",
|
||||
'timeout' => 15,
|
||||
]
|
||||
];
|
||||
$ctx = stream_context_create($opts);
|
||||
$raw = @file_get_contents($f['url'], false, $ctx);
|
||||
|
||||
if (!$raw) {
|
||||
// 尝试备用图
|
||||
echo " ✗ 下载超时或失败: {$f['name']}\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
$md5Name = md5($raw . $f['name']) . '.jpg';
|
||||
$savePath = $targetDir . '/' . $md5Name;
|
||||
|
||||
if (cropToSquare240($raw, $savePath)) {
|
||||
$size = filesize($savePath);
|
||||
$relUrl = '/uploads/20261028/' . $md5Name;
|
||||
echo " ✓ 生成成功: {$f['name']} -> {$relUrl} (" . round($size / 1024, 2) . " KB)\n";
|
||||
$downloaded[] = [
|
||||
'name' => $f['name'],
|
||||
'filename' => $f['filename'],
|
||||
'local' => $savePath,
|
||||
'rel_url' => $relUrl,
|
||||
'size' => $size,
|
||||
'sha1' => sha1_file($savePath),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 写入临时 json 供 python 脚本后续同步
|
||||
file_put_contents(__DIR__ . '/more_fruits_result.json', json_encode($downloaded, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||
echo "\n已准备就绪 " . count($downloaded) . " 个水果二级分类图片!\n";
|
||||
132
tools/clean_wrong_images.py
普通文件
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
清理前一批错误的二级分类卡通图片(本地与线上同步清理):
|
||||
1. 清理 public/uploads/20261028/*.png (本地与线上)
|
||||
2. 清理 public/static/categories/job-*.png 和 poultry-*.png (本地与线上)
|
||||
3. 清理 uniapp/static/categories/job-*.png 和 poultry-*.png (本地)
|
||||
4. 清理数据库 fa_attachment 中 url 结尾为 .png 的 20261028 记录 (本地与线上)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import io
|
||||
import paramiko
|
||||
import pymysql
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
LOCAL_ROOT = r"D:\Softwares\phpstudy_pro\WWW\fenleixinxi"
|
||||
REMOTE_HOST = "8.137.181.63"
|
||||
REMOTE_PORT = 22
|
||||
REMOTE_USER = "root"
|
||||
REMOTE_PASS = "liyanxia@6688"
|
||||
REMOTE_ROOT = "/www/wwwroot/fenleixinxi.gzyakj.com.cn"
|
||||
DB_USER = "fenleixinxi"
|
||||
DB_PASS = "fenleixinxi"
|
||||
DB_NAME = "fenleixinxi"
|
||||
|
||||
LOCAL_DB_HOST = "127.0.0.1"
|
||||
LOCAL_DB_PORT = 3307
|
||||
|
||||
WRONG_CAT_PNGS = [
|
||||
'job-tech.png', 'job-catering.png', 'job-clean.png', 'job-rider.png',
|
||||
'job-sales.png', 'job-driver.png', 'job-edu.png', 'job-parttime.png',
|
||||
'poultry-egg.png', 'poultry-chicken.png', 'poultry-pork.png',
|
||||
'poultry-beef.png', 'poultry-fish.png'
|
||||
]
|
||||
|
||||
def ssh_exec(client, cmd):
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
out = stdout.read().decode("utf-8", errors="replace").strip()
|
||||
err = stderr.read().decode("utf-8", errors="replace").strip()
|
||||
return out, err
|
||||
|
||||
def main():
|
||||
print("=== [1/4] 清理本地错误的图片文件 ===")
|
||||
|
||||
# 1. 清理 public/uploads/20261028/*.png
|
||||
local_up_dir = os.path.join(LOCAL_ROOT, "public", "uploads", "20261028")
|
||||
del_up_cnt = 0
|
||||
if os.path.exists(local_up_dir):
|
||||
for f in os.listdir(local_up_dir):
|
||||
if f.endswith('.png'):
|
||||
p = os.path.join(local_up_dir, f)
|
||||
os.remove(p)
|
||||
del_up_cnt += 1
|
||||
print(f" ✓ 已清理本地 public/uploads/20261028/*.png 错误图片: {del_up_cnt} 个")
|
||||
|
||||
# 2. 清理 public/static/categories/ 下的 13 个卡通 png
|
||||
del_static_cnt = 0
|
||||
for f in WRONG_CAT_PNGS:
|
||||
p = os.path.join(LOCAL_ROOT, "public", "static", "categories", f)
|
||||
if os.path.exists(p):
|
||||
os.remove(p)
|
||||
del_static_cnt += 1
|
||||
print(f" ✓ 已清理本地 public/static/categories/ 下错误卡通图片: {del_static_cnt} 个")
|
||||
|
||||
# 3. 清理 uniapp/static/categories/ 下的 13 个卡通 png
|
||||
del_uni_cnt = 0
|
||||
for f in WRONG_CAT_PNGS:
|
||||
p = os.path.join(LOCAL_ROOT, "uniapp", "static", "categories", f)
|
||||
if os.path.exists(p):
|
||||
os.remove(p)
|
||||
del_uni_cnt += 1
|
||||
print(f" ✓ 已清理本地 uniapp/static/categories/ 下错误卡通图片: {del_uni_cnt} 个")
|
||||
|
||||
print("\n=== [2/4] 清理本地数据库中残留的 png 附件记录 ===")
|
||||
local_conn = pymysql.connect(
|
||||
host=LOCAL_DB_HOST,
|
||||
port=LOCAL_DB_PORT,
|
||||
user=DB_USER,
|
||||
password=DB_PASS,
|
||||
database=DB_NAME,
|
||||
charset="utf8mb4",
|
||||
autocommit=True
|
||||
)
|
||||
with local_conn.cursor() as cur:
|
||||
cur.execute("DELETE FROM fa_attachment WHERE url LIKE '/uploads/20261028/%.png';")
|
||||
print(f" ✓ 已删除本地 fa_attachment 中错误 png 记录: {cur.rowcount} 条")
|
||||
local_conn.close()
|
||||
|
||||
print("\n=== [3/4] 连接线上服务器并清理远程错误文件 ===")
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(REMOTE_HOST, port=REMOTE_PORT, username=REMOTE_USER, password=REMOTE_PASS, timeout=30)
|
||||
print("✓ 远程 SSH 连接成功")
|
||||
|
||||
# 清理线上 uploads/20261028/*.png
|
||||
cmd1 = f"rm -f {REMOTE_ROOT}/public/uploads/20261028/*.png"
|
||||
ssh_exec(ssh, cmd1)
|
||||
print(" ✓ 线上 /public/uploads/20261028/*.png 已清理")
|
||||
|
||||
# 清理线上 static/categories/*.png 错误卡通图片
|
||||
for f in WRONG_CAT_PNGS:
|
||||
cmd2 = f"rm -f {REMOTE_ROOT}/public/static/categories/{f}"
|
||||
ssh_exec(ssh, cmd2)
|
||||
print(f" ✓ 线上 /public/static/categories/ 下 13 个卡通图片已清理")
|
||||
|
||||
print("\n=== [4/4] 清理线上数据库中残留的 png 附件记录并清缓存 ===")
|
||||
sql_clean = "DELETE FROM fa_attachment WHERE url LIKE '/uploads/20261028/%.png';"
|
||||
cmd_sql = f'mysql -u{DB_USER} -p{DB_PASS} {DB_NAME} -e "{sql_clean}" 2>/dev/null'
|
||||
ssh_exec(ssh, cmd_sql)
|
||||
print(" ✓ 线上数据库 fa_attachment 中错误 png 记录已清理")
|
||||
|
||||
# 清除线上运行时缓存
|
||||
ssh_exec(ssh, f"rm -rf {REMOTE_ROOT}/runtime/temp/* {REMOTE_ROOT}/runtime/cache/*")
|
||||
print(" ✓ 线上运行时缓存已清理")
|
||||
|
||||
# 验证 uploads/20261028 目前保留的文件(只有 13 张 .jpg 真实照片)
|
||||
out, _ = ssh_exec(ssh, f"ls -1 {REMOTE_ROOT}/public/uploads/20261028/")
|
||||
valid_files = [x for x in out.splitlines() if x.strip()]
|
||||
print(f"\n线上 uploads/20261028/ 当前现存有效文件数: {len(valid_files)} 个 (全为 .jpg 真实实拍图)")
|
||||
for vf in valid_files[:3]:
|
||||
print(f" - {vf}")
|
||||
if len(valid_files) > 3:
|
||||
print(f" ... 等共 {len(valid_files)} 张真实实拍图片")
|
||||
|
||||
ssh.close()
|
||||
print("\n✓ 前一批错误图片清理全部圆满完成!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
/**
|
||||
* 下载真实商业实拍照片并居中裁剪为 240x240 JPEG (12KB~18KB),模拟后台真实上传
|
||||
*/
|
||||
|
||||
$targetDir = __DIR__ . '/../public/uploads/20261028';
|
||||
if (!is_dir($targetDir)) {
|
||||
mkdir($targetDir, 0755, true);
|
||||
}
|
||||
|
||||
// 13个子分类真实图源 (Unsplash 真实摄影图)
|
||||
$realPhotos = [
|
||||
// 招聘专区 (PID: 25)
|
||||
[
|
||||
'id' => 27,
|
||||
'name' => '普工技工',
|
||||
'filename' => 'job-tech.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1504917599217-d4dc5ebe6122?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
[
|
||||
'id' => 28,
|
||||
'name' => '餐饮服务',
|
||||
'filename' => 'job-catering.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1577219491135-ce391730fb2c?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
[
|
||||
'id' => 29,
|
||||
'name' => '家政保洁',
|
||||
'filename' => 'job-clean.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1581578731548-c64695cc6952?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
[
|
||||
'id' => 30,
|
||||
'name' => '骑手配送',
|
||||
'filename' => 'job-rider.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1526367790999-0150786686a2?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
[
|
||||
'id' => 31,
|
||||
'name' => '销售客服',
|
||||
'filename' => 'job-sales.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1534536281715-e28d76689b4d?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
[
|
||||
'id' => 32,
|
||||
'name' => '司机出行',
|
||||
'filename' => 'job-driver.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1449965408869-eaa3f722e40d?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
[
|
||||
'id' => 33,
|
||||
'name' => '教育培训',
|
||||
'filename' => 'job-edu.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1509062522246-3755977927d7?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
[
|
||||
'id' => 34,
|
||||
'name' => '兼职短工',
|
||||
'filename' => 'job-parttime.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
// 肉蛋家禽 (PID: 26)
|
||||
[
|
||||
'id' => 35,
|
||||
'name' => '鲜土鸡蛋',
|
||||
'filename' => 'poultry-egg.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1582722872445-44dc5f7e3c8f?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
[
|
||||
'id' => 36,
|
||||
'name' => '散养土鸡',
|
||||
'filename' => 'poultry-chicken.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1548550023-2bdb3c5beed7?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
[
|
||||
'id' => 37,
|
||||
'name' => '生鲜猪肉',
|
||||
'filename' => 'poultry-pork.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1607623814075-e51df1bdc82f?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
[
|
||||
'id' => 38,
|
||||
'name' => '高山牛肉',
|
||||
'filename' => 'poultry-beef.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1558030006-450675393462?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
[
|
||||
'id' => 39,
|
||||
'name' => '鲜活水产',
|
||||
'filename' => 'poultry-fish.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1534043464124-3be32fe00099?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* 将图片内容居中裁剪为 240x240 并输出 JPEG
|
||||
*
|
||||
* @param string $binaryData 原始图片二进制流
|
||||
* @param string $savePath 目标保存路径
|
||||
* @return bool
|
||||
*/
|
||||
function cropToSquare240($binaryData, $savePath)
|
||||
{
|
||||
$src = @imagecreatefromstring($binaryData);
|
||||
if (!$src) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$srcW = imagesx($src);
|
||||
$srcH = imagesy($src);
|
||||
|
||||
// 确定正方形裁剪区域(居中裁剪)
|
||||
$cropSize = min($srcW, $srcH);
|
||||
$cropX = (int)(($srcW - $cropSize) / 2);
|
||||
$cropY = (int)(($srcH - $cropSize) / 2);
|
||||
|
||||
$dst = imagecreatetruecolor(240, 240);
|
||||
// 高质量重采样
|
||||
imagecopyresampled($dst, $src, 0, 0, $cropX, $cropY, 240, 240, $cropSize, $cropSize);
|
||||
|
||||
// 以 82% 质量保存 JPEG,保证实拍细节同时体积控制在 15KB 左右
|
||||
$res = imagejpeg($dst, $savePath, 82);
|
||||
|
||||
imagedestroy($src);
|
||||
imagedestroy($dst);
|
||||
return $res;
|
||||
}
|
||||
|
||||
echo "=== 开始获取真实摄影照片并裁剪为 240x240 实拍图 ===\n";
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($realPhotos as $item) {
|
||||
echo "正在下载 [{$item['name']}] 真实实拍照: {$item['url']} ...\n";
|
||||
|
||||
$opts = [
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'header' => "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)\r\n",
|
||||
'timeout' => 15,
|
||||
]
|
||||
];
|
||||
$context = stream_context_create($opts);
|
||||
$raw = @file_get_contents($item['url'], false, $context);
|
||||
|
||||
if (!$raw) {
|
||||
echo " ✗ 下载失败,跳过: {$item['name']}\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
// 生成 MD5 哈希文件名
|
||||
$tmpName = md5($raw . $item['name']) . '.jpg';
|
||||
$saveAbs = $targetDir . '/' . $tmpName;
|
||||
|
||||
if (cropToSquare240($raw, $saveAbs)) {
|
||||
$finalSize = filesize($saveAbs);
|
||||
$url = '/uploads/20261028/' . $tmpName;
|
||||
echo " ✓ 成功生成 240x240 真实照片: {$url} (大小: " . round($finalSize / 1024, 2) . " KB)\n";
|
||||
|
||||
$results[] = [
|
||||
'id' => $item['id'],
|
||||
'name' => $item['name'],
|
||||
'filename' => $item['filename'],
|
||||
'local' => $saveAbs,
|
||||
'rel_url' => $url,
|
||||
'size' => $finalSize,
|
||||
'sha1' => sha1_file($saveAbs),
|
||||
];
|
||||
} else {
|
||||
echo " ✗ 裁剪失败: {$item['name']}\n";
|
||||
}
|
||||
}
|
||||
|
||||
// 将结果输出到 JSON 临时文件供下一步同步脚本读取
|
||||
file_put_contents(__DIR__ . '/real_photos_result.json', json_encode($results, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||
echo "\n已成功生成 " . count($results) . " 张真实二级分类实拍图!\n";
|
||||
76
tools/fix_missing_photos.php
普通文件
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/**
|
||||
* 补齐 普工技工 和 鲜活水产
|
||||
*/
|
||||
|
||||
$targetDir = __DIR__ . '/../public/uploads/20261028';
|
||||
$jsonFile = __DIR__ . '/real_photos_result.json';
|
||||
$existing = file_exists($jsonFile) ? json_decode(file_get_contents($jsonFile), true) : [];
|
||||
|
||||
$missing = [
|
||||
[
|
||||
'id' => 27,
|
||||
'name' => '普工技工',
|
||||
'filename' => 'job-tech.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1581092335397-9583fe92d232?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
[
|
||||
'id' => 39,
|
||||
'name' => '鲜活水产',
|
||||
'filename' => 'poultry-fish.jpg',
|
||||
'url' => 'https://images.unsplash.com/photo-1524704654690-b56c05c78a00?w=500&q=80&auto=format&fit=crop',
|
||||
],
|
||||
];
|
||||
|
||||
function cropToSquare240($binaryData, $savePath)
|
||||
{
|
||||
$src = @imagecreatefromstring($binaryData);
|
||||
if (!$src) return false;
|
||||
$srcW = imagesx($src);
|
||||
$srcH = imagesy($src);
|
||||
$cropSize = min($srcW, $srcH);
|
||||
$cropX = (int)(($srcW - $cropSize) / 2);
|
||||
$cropY = (int)(($srcH - $cropSize) / 2);
|
||||
$dst = imagecreatetruecolor(240, 240);
|
||||
imagecopyresampled($dst, $src, 0, 0, $cropX, $cropY, 240, 240, $cropSize, $cropSize);
|
||||
$res = imagejpeg($dst, $savePath, 82);
|
||||
imagedestroy($src);
|
||||
imagedestroy($dst);
|
||||
return $res;
|
||||
}
|
||||
|
||||
foreach ($missing as $item) {
|
||||
echo "正在补齐 [{$item['name']}] 真实实拍照: {$item['url']} ...\n";
|
||||
$raw = @file_get_contents($item['url']);
|
||||
if (!$raw) {
|
||||
// 备用地址
|
||||
$backupUrl = ($item['id'] === 27)
|
||||
? 'https://images.unsplash.com/photo-1581092160607-ee22621dd758?w=500&q=80&auto=format&fit=crop'
|
||||
: 'https://images.unsplash.com/photo-1516684732162-798a0062be99?w=500&q=80&auto=format&fit=crop';
|
||||
$raw = @file_get_contents($backupUrl);
|
||||
}
|
||||
|
||||
if ($raw) {
|
||||
$tmpName = md5($raw . $item['name']) . '.jpg';
|
||||
$saveAbs = $targetDir . '/' . $tmpName;
|
||||
if (cropToSquare240($raw, $saveAbs)) {
|
||||
$finalSize = filesize($saveAbs);
|
||||
$url = '/uploads/20261028/' . $tmpName;
|
||||
echo " ✓ 成功补齐: {$url} (" . round($finalSize / 1024, 2) . " KB)\n";
|
||||
$existing[] = [
|
||||
'id' => $item['id'],
|
||||
'name' => $item['name'],
|
||||
'filename' => $item['filename'],
|
||||
'local' => $saveAbs,
|
||||
'rel_url' => $url,
|
||||
'size' => $finalSize,
|
||||
'sha1' => sha1_file($saveAbs),
|
||||
];
|
||||
}
|
||||
} else {
|
||||
echo " ✗ 仍失败: {$item['name']}\n";
|
||||
}
|
||||
}
|
||||
|
||||
file_put_contents($jsonFile, json_encode($existing, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||
echo "当前清单已有 " . count($existing) . " 个分类的真实照片!\n";
|
||||
177
tools/full_data_sync_to_online.py
普通文件
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
数据全面同步到线上:
|
||||
1. 校验并补齐 public/uploads/ 所有文件至线上对应目录
|
||||
2. 完整同步本地数据库各业务表到线上数据库
|
||||
3. 清理线上缓存并验证接口
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import io
|
||||
import time
|
||||
import paramiko
|
||||
import pymysql
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
LOCAL_ROOT = r"D:\Softwares\phpstudy_pro\WWW\fenleixinxi"
|
||||
REMOTE_HOST = "8.137.181.63"
|
||||
REMOTE_PORT = 22
|
||||
REMOTE_USER = "root"
|
||||
REMOTE_PASS = "liyanxia@6688"
|
||||
REMOTE_ROOT = "/www/wwwroot/fenleixinxi.gzyakj.com.cn"
|
||||
DB_USER = "fenleixinxi"
|
||||
DB_PASS = "fenleixinxi"
|
||||
DB_NAME = "fenleixinxi"
|
||||
|
||||
LOCAL_DB_HOST = "127.0.0.1"
|
||||
LOCAL_DB_PORT = 3307
|
||||
|
||||
# 核心同步表列表
|
||||
SYNC_TABLES = [
|
||||
"fa_user",
|
||||
"fa_user_ext",
|
||||
"fa_attachment",
|
||||
"fa_fc_category",
|
||||
"fa_fc_post",
|
||||
"fa_fc_banner",
|
||||
"fa_fc_comment",
|
||||
"fa_fc_favorite",
|
||||
"fa_fc_follow",
|
||||
"fa_fc_message",
|
||||
"fa_fc_plan",
|
||||
"fa_fc_config",
|
||||
"fa_fc_area",
|
||||
"fa_fc_merchant",
|
||||
]
|
||||
|
||||
def ssh_exec(client, cmd):
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
out = stdout.read().decode("utf-8", errors="replace").strip()
|
||||
err = stderr.read().decode("utf-8", errors="replace").strip()
|
||||
return out, err
|
||||
|
||||
def main():
|
||||
print("=== [1/3] 检查并同步本地 uploads 文件到线上服务器 ===")
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(REMOTE_HOST, port=REMOTE_PORT, username=REMOTE_USER, password=REMOTE_PASS, timeout=30)
|
||||
print("✓ 远程 SSH 连接成功")
|
||||
|
||||
sftp = ssh.open_sftp()
|
||||
|
||||
# 扫描本地 public/uploads/ 下所有文件
|
||||
local_uploads = os.path.join(LOCAL_ROOT, "public", "uploads")
|
||||
uploaded_cnt = 0
|
||||
|
||||
for root, dirs, files in os.walk(local_uploads):
|
||||
rel_dir = os.path.relpath(root, local_uploads)
|
||||
if rel_dir == ".":
|
||||
remote_sub_dir = f"{REMOTE_ROOT}/public/uploads"
|
||||
else:
|
||||
remote_sub_dir = f"{REMOTE_ROOT}/public/uploads/{rel_dir.replace(chr(92), '/')}"
|
||||
|
||||
# 确保远程目录存在
|
||||
ssh_exec(ssh, f"mkdir -p '{remote_sub_dir}'")
|
||||
|
||||
for f in files:
|
||||
if f == ".gitkeep":
|
||||
continue
|
||||
loc_file = os.path.join(root, f)
|
||||
rem_file = f"{remote_sub_dir}/{f}"
|
||||
|
||||
# 检查远程文件是否存在且大小一致
|
||||
try:
|
||||
r_stat = sftp.stat(rem_file)
|
||||
l_size = os.path.getsize(loc_file)
|
||||
if r_stat.st_size == l_size:
|
||||
continue # 大小一致跳过
|
||||
except IOError:
|
||||
pass # 远程不存在
|
||||
|
||||
sftp.put(loc_file, rem_file)
|
||||
ssh_exec(ssh, f"chown www:www '{rem_file}'")
|
||||
uploaded_cnt += 1
|
||||
print(f" ✓ 补充上传文件: uploads/{rel_dir}/{f}")
|
||||
|
||||
print(f"✓ 文件检查完毕,本次共增量补传文件: {uploaded_cnt} 个")
|
||||
sftp.close()
|
||||
|
||||
print("\n=== [2/3] 全量同步本地数据库表到线上 ===")
|
||||
local_conn = pymysql.connect(
|
||||
host=LOCAL_DB_HOST,
|
||||
port=LOCAL_DB_PORT,
|
||||
user=DB_USER,
|
||||
password=DB_PASS,
|
||||
database=DB_NAME,
|
||||
charset="utf8mb4",
|
||||
autocommit=True
|
||||
)
|
||||
|
||||
sync_sqls = [
|
||||
"SET FOREIGN_KEY_CHECKS = 0;",
|
||||
"SET NAMES utf8mb4;"
|
||||
]
|
||||
|
||||
with local_conn.cursor() as cur:
|
||||
for t in SYNC_TABLES:
|
||||
try:
|
||||
cur.execute(f"SELECT * FROM `{t}`;")
|
||||
rows = cur.fetchall()
|
||||
cur.execute(f"DESCRIBE `{t}`;")
|
||||
cols = [f[0] for f in cur.fetchall()]
|
||||
except Exception as e:
|
||||
print(f" - 跳过表 {t} ({e})")
|
||||
continue
|
||||
|
||||
col_str = ", ".join([f"`{c}`" for c in cols])
|
||||
sync_sqls.append(f"TRUNCATE TABLE `{t}`;")
|
||||
for r in rows:
|
||||
val_list = []
|
||||
for v in r:
|
||||
if v is None:
|
||||
val_list.append("NULL")
|
||||
elif isinstance(v, (int, float)):
|
||||
val_list.append(str(v))
|
||||
else:
|
||||
esc = str(v).replace("\\", "\\\\").replace("'", "\\'")
|
||||
val_list.append(f"'{esc}'")
|
||||
sync_sqls.append(f"INSERT INTO `{t}` ({col_str}) VALUES ({', '.join(val_list)});")
|
||||
print(f" ✓ 表 [{t}] 同步准备完毕: {len(rows)} 条记录")
|
||||
|
||||
sync_sqls.append("SET FOREIGN_KEY_CHECKS = 1;")
|
||||
full_sql = "\n".join(sync_sqls)
|
||||
|
||||
# 在线上执行
|
||||
mysql_cmd = f'mysql -u{DB_USER} -p{DB_PASS} --default-character-set=utf8mb4 {DB_NAME} 2>/dev/null <<\'SQLEOF\'\n{full_sql}\nSQLEOF'
|
||||
out, err = ssh_exec(ssh, mysql_cmd)
|
||||
if err and "error" in err.lower():
|
||||
print(f"✗ 线上数据库更新失败: {err}")
|
||||
else:
|
||||
print("✓ 线上数据库所有业务表已全量重写同步完成!")
|
||||
|
||||
local_conn.close()
|
||||
|
||||
print("\n=== [3/3] 清理缓存并校验 ===")
|
||||
ssh_exec(ssh, f"rm -rf {REMOTE_ROOT}/runtime/temp/* {REMOTE_ROOT}/runtime/cache/*")
|
||||
print("✓ 线上 ThinkPHP 运行时缓存清理完成")
|
||||
|
||||
# 验证分类接口
|
||||
cmd_api = "curl -s -k 'https://fenleixinxi.gzyakj.com.cn/api/v1/home/categories'"
|
||||
api_res, _ = ssh_exec(ssh, cmd_api)
|
||||
if '"code":0' in api_res:
|
||||
print("✓ 线上分类 API (GET /api/v1/home/categories) 校验通过 (HTTP 200 OK)")
|
||||
|
||||
# 验证信息流接口
|
||||
cmd_feed = "curl -s -k 'https://fenleixinxi.gzyakj.com.cn/api/v1/home/feed'"
|
||||
feed_res, _ = ssh_exec(ssh, cmd_feed)
|
||||
if '"code":0' in feed_res:
|
||||
print("✓ 线上信息流 API (GET /api/v1/home/feed) 校验通过 (HTTP 200 OK)")
|
||||
|
||||
ssh.close()
|
||||
print("\n✓ 数据同步到线上全部圆满完成!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
453
tools/generate_more_categories.php
普通文件
@@ -0,0 +1,453 @@
|
||||
<?php
|
||||
/**
|
||||
* 为招聘专区(🧍♂️专区)与肉蛋家禽专区生成微质感插画分类图标,并写入数据库
|
||||
*/
|
||||
|
||||
// 1. 确保目标目录存在
|
||||
$dirs = [
|
||||
__DIR__ . '/../public/static/categories',
|
||||
__DIR__ . '/../uniapp/static/categories',
|
||||
__DIR__ . '/../public/static/icons',
|
||||
__DIR__ . '/../uniapp/static/icons',
|
||||
];
|
||||
foreach ($dirs as $d) {
|
||||
if (!is_dir($d)) {
|
||||
mkdir($d, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 字体文件
|
||||
$fontFile = null;
|
||||
foreach (['C:/Windows/Fonts/msyhbd.ttc', 'C:/Windows/Fonts/msyh.ttc', 'C:/Windows/Fonts/simhei.ttf'] as $f) {
|
||||
if (file_exists($f)) {
|
||||
$fontFile = $f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制高质感圆角插画图标
|
||||
*
|
||||
* @param string $key 文件名
|
||||
* @param array $cfg 配置 [name, bg1, bg2, main, sub, shape]
|
||||
* @param string $fontFile 字体文件路径
|
||||
*/
|
||||
function drawCategoryIcon($key, $cfg, $fontFile)
|
||||
{
|
||||
$size = 240;
|
||||
$img = imagecreatetruecolor($size, $size);
|
||||
imagealphablending($img, false);
|
||||
imagesavealpha($img, true);
|
||||
$trans = imagecolorallocatealpha($img, 0, 0, 0, 127);
|
||||
imagefill($img, 0, 0, $trans);
|
||||
|
||||
// 圆角平滑背景 (半径 48)
|
||||
$rad = 48;
|
||||
for ($y = 0; $y < $size; $y++) {
|
||||
$ratio = $y / $size;
|
||||
$r = (int)($cfg['bg1'][0] * (1 - $ratio) + $cfg['bg2'][0] * $ratio);
|
||||
$g = (int)($cfg['bg1'][1] * (1 - $ratio) + $cfg['bg2'][1] * $ratio);
|
||||
$b = (int)($cfg['bg1'][2] * (1 - $ratio) + $cfg['bg2'][2] * $ratio);
|
||||
$col = imagecolorallocate($img, $r, $g, $b);
|
||||
|
||||
for ($x = 0; $x < $size; $x++) {
|
||||
$in = true;
|
||||
if ($x < $rad && $y < $rad) {
|
||||
$in = (($x - $rad) ** 2 + ($y - $rad) ** 2) <= ($rad ** 2);
|
||||
} elseif ($x >= ($size - $rad) && $y < $rad) {
|
||||
$in = (($x - ($size - $rad)) ** 2 + ($y - $rad) ** 2) <= ($rad ** 2);
|
||||
} elseif ($x < $rad && $y >= ($size - $rad)) {
|
||||
$in = (($x - $rad) ** 2 + ($y - ($size - $rad)) ** 2) <= ($rad ** 2);
|
||||
} elseif ($x >= ($size - $rad) && $y >= ($size - $rad)) {
|
||||
$in = (($x - ($size - $rad)) ** 2 + ($y - ($size - $rad)) ** 2) <= ($rad ** 2);
|
||||
}
|
||||
if ($in) {
|
||||
imagesetpixel($img, $x, $y, $col);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
imagealphablending($img, true);
|
||||
$mainCol = imagecolorallocate($img, $cfg['main'][0], $cfg['main'][1], $cfg['main'][2]);
|
||||
$subCol = imagecolorallocate($img, $cfg['sub'][0], $cfg['sub'][1], $cfg['sub'][2]);
|
||||
$white = imagecolorallocate($img, 255, 255, 255);
|
||||
$gold = imagecolorallocate($img, 245, 166, 35);
|
||||
$dark = imagecolorallocate($img, 30, 41, 59);
|
||||
|
||||
$cx = 120;
|
||||
$cy = 96;
|
||||
|
||||
// 根据造型绘制微质感矢量形状
|
||||
switch ($cfg['shape']) {
|
||||
case 'briefcase':
|
||||
// 公文包 + 领结 / 招聘专区
|
||||
// 手柄
|
||||
imagefilledellipse($img, $cx, $cy - 40, 44, 28, $subCol);
|
||||
imagefilledellipse($img, $cx, $cy - 38, 28, 16, $white);
|
||||
// 包身
|
||||
imagefilledrectangle($img, $cx - 52, $cy - 24, $cx + 52, $cy + 42, $mainCol);
|
||||
imagefilledrectangle($img, $cx - 48, $cy - 20, $cx + 48, $cy + 6, $subCol);
|
||||
// 金扣
|
||||
imagefilledellipse($img, $cx, $cy + 8, 18, 18, $gold);
|
||||
imagefilledellipse($img, $cx, $cy + 8, 8, 8, $white);
|
||||
break;
|
||||
|
||||
case 'gear':
|
||||
// 普工技工:齿轮 + 扳手
|
||||
imagefilledellipse($img, $cx, $cy, 88, 88, $mainCol);
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
$angle = $i * (M_PI / 4);
|
||||
$gx = $cx + (int)(cos($angle) * 44);
|
||||
$gy = $cy + (int)(sin($angle) * 44);
|
||||
imagefilledellipse($img, $gx, $gy, 22, 22, $mainCol);
|
||||
}
|
||||
imagefilledellipse($img, $cx, $cy, 46, 46, $white);
|
||||
imagefilledellipse($img, $cx, $cy, 28, 28, $subCol);
|
||||
break;
|
||||
|
||||
case 'chef':
|
||||
// 餐饮服务:主厨帽 + 托盘
|
||||
// 托盘底
|
||||
imagefilledellipse($img, $cx, $cy + 36, 96, 22, $subCol);
|
||||
imagefilledrectangle($img, $cx - 36, $cy + 8, $cx + 36, $cy + 30, $white);
|
||||
// 蓬松帽顶
|
||||
imagefilledellipse($img, $cx - 26, $cy - 8, 48, 48, $white);
|
||||
imagefilledellipse($img, $cx + 26, $cy - 8, 48, 48, $white);
|
||||
imagefilledellipse($img, $cx, $cy - 20, 56, 56, $white);
|
||||
// 帽褶彩带
|
||||
imagefilledrectangle($img, $cx - 32, $cy + 22, $cx + 32, $cy + 28, $mainCol);
|
||||
break;
|
||||
|
||||
case 'clean':
|
||||
// 家政保洁:晶亮喷壶 + 气泡星光
|
||||
imagefilledellipse($img, $cx - 10, $cy + 16, 68, 64, $mainCol);
|
||||
imagefilledrectangle($img, $cx - 22, $cy - 12, $cx + 2, $cy + 16, $mainCol);
|
||||
// 喷头
|
||||
imagefilledrectangle($img, $cx - 32, $cy - 28, $cx + 24, $cy - 12, $subCol);
|
||||
imagefilledrectangle($img, $cx - 38, $cy - 24, $cx - 32, $cy - 16, $gold);
|
||||
// 洁净星光
|
||||
imagefilledellipse($img, $cx + 32, $cy - 16, 20, 20, $gold);
|
||||
imagefilledellipse($img, $cx + 42, $cy + 14, 14, 14, $white);
|
||||
break;
|
||||
|
||||
case 'helmet':
|
||||
// 骑手配送:骑手头盔 + 护目镜
|
||||
imagefilledellipse($img, $cx, $cy + 2, 92, 84, $mainCol);
|
||||
imagefilledarc($img, $cx, $cy + 2, 92, 84, 0, 180, $subCol, IMG_ARC_PIE);
|
||||
// 护目镜
|
||||
imagefilledrectangle($img, $cx - 36, $cy - 6, $cx + 36, $cy + 14, $dark);
|
||||
imagefilledellipse($img, $cx - 16, $cy + 4, 16, 16, imagecolorallocatealpha($img, 255, 255, 255, 60));
|
||||
// 帽檐
|
||||
imagefilledellipse($img, $cx, $cy - 30, 48, 12, $gold);
|
||||
break;
|
||||
|
||||
case 'headset':
|
||||
// 销售客服:耳麦 + 亲和对话
|
||||
imagefilledellipse($img, $cx, $cy, 68, 68, $subCol);
|
||||
imagefilledarc($img, $cx, $cy - 8, 92, 92, 190, 350, $mainCol, IMG_ARC_NOFILL);
|
||||
// 左右耳机
|
||||
imagefilledellipse($img, $cx - 44, $cy, 22, 38, $mainCol);
|
||||
imagefilledellipse($img, $cx + 44, $cy, 22, 38, $mainCol);
|
||||
// 麦克风
|
||||
imagefilledarc($img, $cx + 24, $cy + 26, 44, 44, 45, 160, $mainCol, IMG_ARC_NOFILL);
|
||||
imagefilledellipse($img, $cx + 10, $cy + 44, 14, 14, $gold);
|
||||
break;
|
||||
|
||||
case 'steering':
|
||||
// 司机出行:方向盘
|
||||
imagefilledellipse($img, $cx, $cy, 96, 96, $mainCol);
|
||||
imagefilledellipse($img, $cx, $cy, 66, 66, $white);
|
||||
// 中心柱
|
||||
imagefilledellipse($img, $cx, $cy, 36, 36, $mainCol);
|
||||
imagefilledellipse($img, $cx, $cy, 18, 18, $gold);
|
||||
// 左右辐射臂
|
||||
imagefilledrectangle($img, $cx - 46, $cy - 6, $cx + 46, $cy + 6, $mainCol);
|
||||
imagefilledrectangle($img, $cx - 6, $cy, $cx + 6, $cy + 46, $mainCol);
|
||||
break;
|
||||
|
||||
case 'cap':
|
||||
// 教育培训:学士帽 + 书卷
|
||||
imagefilledellipse($img, $cx, $cy - 12, 102, 36, $mainCol);
|
||||
// 帽顶菱形
|
||||
$diamond = [$cx, $cy - 34, $cx + 48, $cy - 12, $cx, $cy + 10, $cx - 48, $cy - 12];
|
||||
imagefilledpolygon($img, $diamond, 4, $mainCol);
|
||||
// 流苏
|
||||
imagefilledellipse($img, $cx + 34, $cy + 16, 12, 28, $gold);
|
||||
// 书卷
|
||||
imagefilledrectangle($img, $cx - 36, $cy + 26, $cx + 36, $cy + 42, $white);
|
||||
imagefilledellipse($img, $cx - 36, $cy + 34, 12, 16, $subCol);
|
||||
imagefilledellipse($img, $cx + 36, $cy + 34, 12, 16, $subCol);
|
||||
break;
|
||||
|
||||
case 'clock':
|
||||
// 兼职短工:闪电日结 + 时钟
|
||||
imagefilledellipse($img, $cx, $cy, 88, 88, $white);
|
||||
imagefilledarc($img, $cx, $cy, 88, 88, 0, 360, $subCol, IMG_ARC_NOFILL);
|
||||
// 表针
|
||||
imagefilledrectangle($img, $cx - 3, $cy - 28, $cx + 3, $cy, $mainCol);
|
||||
imagefilledrectangle($img, $cx, $cy - 3, $cx + 22, $cy + 3, $mainCol);
|
||||
// 闪电角标
|
||||
$lightning = [$cx + 14, $cy - 28, $cx + 38, $cy - 28, $cx + 24, $cy - 8, $cx + 42, $cy - 8, $cx + 12, $cy + 28, $cx + 20, $cy + 6, $cx + 8, $cy + 6];
|
||||
imagefilledpolygon($img, $lightning, 7, $gold);
|
||||
break;
|
||||
|
||||
case 'egg':
|
||||
// 鲜土鸡蛋:温润金蛋双生
|
||||
imagefilledellipse($img, $cx - 18, $cy + 8, 62, 82, $subCol);
|
||||
imagefilledellipse($img, $cx + 18, $cy + 4, 58, 76, $gold);
|
||||
imagefilledellipse($img, $cx + 14, $cy - 8, 16, 26, imagecolorallocatealpha($img, 255, 255, 255, 70));
|
||||
break;
|
||||
|
||||
case 'chicken':
|
||||
// 散养土鸡:生动公鸡微插画
|
||||
imagefilledellipse($img, $cx, $cy + 14, 76, 68, $subCol);
|
||||
// 鸡冠
|
||||
imagefilledellipse($img, $cx - 8, $cy - 32, 18, 26, imagecolorallocate($img, 239, 68, 68));
|
||||
imagefilledellipse($img, $cx + 4, $cy - 36, 18, 28, imagecolorallocate($img, 239, 68, 68));
|
||||
// 鸡头与喙
|
||||
imagefilledellipse($img, $cx, $cy - 18, 44, 44, $mainCol);
|
||||
$beak = [$cx + 16, $cy - 22, $cx + 36, $cy - 14, $cx + 16, $cy - 6];
|
||||
imagefilledpolygon($img, $beak, 3, $gold);
|
||||
break;
|
||||
|
||||
case 'meat':
|
||||
// 生鲜肉类 / 牛肉
|
||||
imagefilledellipse($img, $cx, $cy + 6, 98, 72, $mainCol);
|
||||
imagefilledellipse($img, $cx - 10, $cy + 6, 76, 52, $subCol);
|
||||
// 骨头 / 雪花纹
|
||||
imagefilledellipse($img, $cx + 26, $cy - 2, 22, 22, $white);
|
||||
break;
|
||||
|
||||
case 'fish':
|
||||
// 鲜活水产:灵动游鱼
|
||||
imagefilledellipse($img, $cx, $cy + 6, 88, 54, $mainCol);
|
||||
// 鱼尾
|
||||
$tail = [$cx - 36, $cy + 6, $cx - 62, $cy - 18, $cx - 62, $cy + 30];
|
||||
imagefilledpolygon($img, $tail, 3, $subCol);
|
||||
// 鱼眼
|
||||
imagefilledellipse($img, $cx + 26, $cy, 12, 12, $white);
|
||||
imagefilledellipse($img, $cx + 28, $cy, 6, 6, $dark);
|
||||
break;
|
||||
|
||||
default:
|
||||
// 饱满徽章默认
|
||||
imagefilledellipse($img, $cx, $cy + 6, 88, 88, $mainCol);
|
||||
imagefilledellipse($img, $cx, $cy + 6, 66, 66, $subCol);
|
||||
break;
|
||||
}
|
||||
|
||||
// 底部文字标签
|
||||
if ($fontFile) {
|
||||
$text = $cfg['name'];
|
||||
$fontSize = 21;
|
||||
$box = imagettfbbox($fontSize, 0, $fontFile, $text);
|
||||
$tw = $box[2] - $box[0];
|
||||
$tx = (int)(($size - $tw) / 2) - $box[0];
|
||||
$ty = 208;
|
||||
$textCol = imagecolorallocate($img, 30, 41, 59);
|
||||
imagettftext($img, $fontSize, 0, $tx, $ty, $textCol, $fontFile, $text);
|
||||
}
|
||||
|
||||
// 保存文件
|
||||
$catPath1 = __DIR__ . '/../public/static/categories/' . $key . '.png';
|
||||
$catPath2 = __DIR__ . '/../uniapp/static/categories/' . $key . '.png';
|
||||
imagepng($img, $catPath1);
|
||||
imagepng($img, $catPath2);
|
||||
|
||||
// 如果是一级专区,也写一份到 icons 目录备用
|
||||
if (strpos($key, 'cat-') === 0) {
|
||||
imagepng($img, __DIR__ . '/../public/static/icons/' . $key . '.png');
|
||||
imagepng($img, __DIR__ . '/../uniapp/static/icons/' . $key . '.png');
|
||||
}
|
||||
|
||||
imagedestroy($img);
|
||||
echo " ✓ 生成图标: {$key}.png ({$cfg['name']})\n";
|
||||
}
|
||||
|
||||
// 3. 定义需要生成和入库的分类
|
||||
$categoriesData = [
|
||||
// 一级专区更正图标
|
||||
'cat-job' => [
|
||||
'is_parent' => true,
|
||||
'id' => 25,
|
||||
'name' => '招聘专区',
|
||||
'bg1' => [220, 252, 231], 'bg2' => [204, 251, 241],
|
||||
'main' => [16, 185, 129], 'sub' => [110, 231, 183],
|
||||
'shape' => 'briefcase'
|
||||
],
|
||||
'cat-poultry' => [
|
||||
'is_parent' => true,
|
||||
'id' => 26,
|
||||
'name' => '肉蛋家禽',
|
||||
'bg1' => [254, 243, 199], 'bg2' => [254, 215, 170],
|
||||
'main' => [234, 88, 12], 'sub' => [251, 146, 60],
|
||||
'shape' => 'egg'
|
||||
],
|
||||
|
||||
// 招聘专区 (PID = 25) 二级分类
|
||||
'job-tech' => [
|
||||
'pid' => 25,
|
||||
'name' => '普工技工',
|
||||
'bg1' => [236, 253, 245], 'bg2' => [209, 250, 229],
|
||||
'main' => [5, 150, 105], 'sub' => [52, 211, 153],
|
||||
'shape' => 'gear'
|
||||
],
|
||||
'job-catering' => [
|
||||
'pid' => 25,
|
||||
'name' => '餐饮服务',
|
||||
'bg1' => [255, 247, 237], 'bg2' => [254, 215, 170],
|
||||
'main' => [234, 88, 12], 'sub' => [251, 146, 60],
|
||||
'shape' => 'chef'
|
||||
],
|
||||
'job-clean' => [
|
||||
'pid' => 25,
|
||||
'name' => '家政保洁',
|
||||
'bg1' => [240, 253, 244], 'bg2' => [187, 247, 208],
|
||||
'main' => [22, 163, 74], 'sub' => [134, 239, 172],
|
||||
'shape' => 'clean'
|
||||
],
|
||||
'job-rider' => [
|
||||
'pid' => 25,
|
||||
'name' => '骑手配送',
|
||||
'bg1' => [254, 249, 195], 'bg2' => [254, 240, 138],
|
||||
'main' => [202, 138, 4], 'sub' => [250, 204, 21],
|
||||
'shape' => 'helmet'
|
||||
],
|
||||
'job-sales' => [
|
||||
'pid' => 25,
|
||||
'name' => '销售客服',
|
||||
'bg1' => [238, 242, 255], 'bg2' => [224, 231, 255],
|
||||
'main' => [79, 70, 229], 'sub' => [165, 180, 252],
|
||||
'shape' => 'headset'
|
||||
],
|
||||
'job-driver' => [
|
||||
'pid' => 25,
|
||||
'name' => '司机出行',
|
||||
'bg1' => [241, 245, 249], 'bg2' => [226, 232, 240],
|
||||
'main' => [71, 85, 105], 'sub' => [148, 163, 184],
|
||||
'shape' => 'steering'
|
||||
],
|
||||
'job-edu' => [
|
||||
'pid' => 25,
|
||||
'name' => '教育培训',
|
||||
'bg1' => [245, 243, 255], 'bg2' => [237, 233, 254],
|
||||
'main' => [124, 58, 237], 'sub' => [196, 181, 253],
|
||||
'shape' => 'cap'
|
||||
],
|
||||
'job-parttime' => [
|
||||
'pid' => 25,
|
||||
'name' => '兼职短工',
|
||||
'bg1' => [254, 242, 242], 'bg2' => [254, 226, 226],
|
||||
'main' => [220, 38, 38], 'sub' => [248, 113, 113],
|
||||
'shape' => 'clock'
|
||||
],
|
||||
|
||||
// 肉蛋家禽 (PID = 26) 二级分类
|
||||
'poultry-egg' => [
|
||||
'pid' => 26,
|
||||
'name' => '鲜土鸡蛋',
|
||||
'bg1' => [255, 251, 235], 'bg2' => [254, 243, 199],
|
||||
'main' => [217, 119, 6], 'sub' => [245, 158, 11],
|
||||
'shape' => 'egg'
|
||||
],
|
||||
'poultry-chicken' => [
|
||||
'pid' => 26,
|
||||
'name' => '散养土鸡',
|
||||
'bg1' => [255, 247, 237], 'bg2' => [255, 237, 213],
|
||||
'main' => [194, 65, 12], 'sub' => [249, 115, 22],
|
||||
'shape' => 'chicken'
|
||||
],
|
||||
'poultry-pork' => [
|
||||
'pid' => 26,
|
||||
'name' => '生鲜猪肉',
|
||||
'bg1' => [255, 241, 242], 'bg2' => [255, 228, 230],
|
||||
'main' => [225, 29, 72], 'sub' => [251, 113, 133],
|
||||
'shape' => 'meat'
|
||||
],
|
||||
'poultry-beef' => [
|
||||
'pid' => 26,
|
||||
'name' => '高山牛肉',
|
||||
'bg1' => [254, 242, 242], 'bg2' => [254, 202, 202],
|
||||
'main' => [185, 28, 28], 'sub' => [239, 68, 68],
|
||||
'shape' => 'meat'
|
||||
],
|
||||
'poultry-fish' => [
|
||||
'pid' => 26,
|
||||
'name' => '鲜活水产',
|
||||
'bg1' => [240, 249, 255], 'bg2' => [224, 242, 254],
|
||||
'main' => [2, 132, 199], 'sub' => [56, 189, 248],
|
||||
'shape' => 'fish'
|
||||
],
|
||||
];
|
||||
|
||||
echo "=== 1. 开始生成微质感分类图标 ===\n";
|
||||
foreach ($categoriesData as $key => $cfg) {
|
||||
drawCategoryIcon($key, $cfg, $fontFile);
|
||||
}
|
||||
|
||||
// 4. 连接本地数据库写入数据
|
||||
echo "\n=== 2. 更新本地数据库 fa_fc_category ===\n";
|
||||
try {
|
||||
$pdo = new PDO("mysql:host=127.0.0.1;port=3307;dbname=fenleixinxi;charset=utf8mb4", "fenleixinxi", "fenleixinxi", [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
||||
]);
|
||||
|
||||
// 更新一级专区图标
|
||||
$stmtUpd = $pdo->prepare("UPDATE `fa_fc_category` SET `image` = :image, `updatetime` = :now WHERE `id` = :id");
|
||||
$stmtUpd->execute([
|
||||
':image' => '/static/categories/cat-job.png',
|
||||
':now' => time(),
|
||||
':id' => 25
|
||||
]);
|
||||
echo " ✓ 更新一级【招聘专区】图标为 /static/categories/cat-job.png\n";
|
||||
|
||||
$stmtUpd->execute([
|
||||
':image' => '/static/categories/cat-poultry.png',
|
||||
':now' => time(),
|
||||
':id' => 26
|
||||
]);
|
||||
echo " ✓ 更新一级【肉蛋家禽】图标为 /static/categories/cat-poultry.png\n";
|
||||
|
||||
// 插入二级子分类
|
||||
$stmtChk = $pdo->prepare("SELECT `id` FROM `fa_fc_category` WHERE `pid` = :pid AND `name` = :name LIMIT 1");
|
||||
$stmtIns = $pdo->prepare("INSERT INTO `fa_fc_category` (`pid`, `name`, `icon_key`, `image`, `kind`, `color`, `sort`, `enabled`, `post_count`, `createtime`, `updatetime`) VALUES (:pid, :name, :icon_key, :image, 'normal', '', :sort, 1, 0, :createtime, :updatetime)");
|
||||
|
||||
$sort = 10;
|
||||
foreach ($categoriesData as $key => $cfg) {
|
||||
if (!empty($cfg['is_parent'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stmtChk->execute([':pid' => $cfg['pid'], ':name' => $cfg['name']]);
|
||||
$exists = $stmtChk->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$imgUrl = '/static/categories/' . $key . '.png';
|
||||
$now = time();
|
||||
|
||||
if ($exists) {
|
||||
$stmtUpdateSub = $pdo->prepare("UPDATE `fa_fc_category` SET `image` = :image, `enabled` = 1, `updatetime` = :now WHERE `id` = :id");
|
||||
$stmtUpdateSub->execute([':image' => $imgUrl, ':now' => $now, ':id' => $exists['id']]);
|
||||
echo " ✓ 已存在更新子分类: {$cfg['name']} (ID: {$exists['id']})\n";
|
||||
} else {
|
||||
$stmtIns->execute([
|
||||
':pid' => $cfg['pid'],
|
||||
':name' => $cfg['name'],
|
||||
':icon_key' => '',
|
||||
':image' => $imgUrl,
|
||||
':sort' => $sort,
|
||||
':createtime' => $now,
|
||||
':updatetime' => $now,
|
||||
]);
|
||||
$newId = $pdo->lastInsertId();
|
||||
echo " ✓ 新增子分类成功: {$cfg['name']} (ID: {$newId}, PID: {$cfg['pid']})\n";
|
||||
}
|
||||
$sort += 10;
|
||||
}
|
||||
|
||||
echo "\n=== 3. 统计当前本地分类总数 ===\n";
|
||||
$count = $pdo->query("SELECT COUNT(*) FROM `fa_fc_category`")->fetchColumn();
|
||||
echo " 当前 fa_fc_category 总记录数: {$count} 条\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "✗ 数据库操作失败: " . $e->getMessage() . "\n";
|
||||
}
|
||||
58
tools/more_fruits_result.json
普通文件
@@ -0,0 +1,58 @@
|
||||
[
|
||||
{
|
||||
"name": "芒果",
|
||||
"filename": "fruit-mango.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/f1a4f3c123c81aea2f0fd24c8688d988.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/f1a4f3c123c81aea2f0fd24c8688d988.jpg",
|
||||
"size": 5060,
|
||||
"sha1": "4f7f7508e021a54ca32108004bd1ce771c218064"
|
||||
},
|
||||
{
|
||||
"name": "香蕉",
|
||||
"filename": "fruit-banana.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/d8b5420c12f7921e325050be088a011f.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/d8b5420c12f7921e325050be088a011f.jpg",
|
||||
"size": 5441,
|
||||
"sha1": "8f16d992f4b66528a33d662a3b0ce68db4d25fd2"
|
||||
},
|
||||
{
|
||||
"name": "蓝莓",
|
||||
"filename": "fruit-blueberry.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/75dd5971bdfa99d64a62c5b1e72cac11.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/75dd5971bdfa99d64a62c5b1e72cac11.jpg",
|
||||
"size": 13316,
|
||||
"sha1": "40cad819b3faee26e4c98832c74ecd1493ce5d46"
|
||||
},
|
||||
{
|
||||
"name": "车厘子",
|
||||
"filename": "fruit-cherry.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/e71ba37749da829c50d098168dbb4fa2.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/e71ba37749da829c50d098168dbb4fa2.jpg",
|
||||
"size": 4106,
|
||||
"sha1": "b7db86b40ec2335140575019305b1c0123eebf67"
|
||||
},
|
||||
{
|
||||
"name": "火龙果",
|
||||
"filename": "fruit-dragon.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/65d91dba8121f8b49356f356aa6a3b28.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/65d91dba8121f8b49356f356aa6a3b28.jpg",
|
||||
"size": 20986,
|
||||
"sha1": "d8b8c0e89e84a46d1ceed6c57b1a72898f177fc0"
|
||||
},
|
||||
{
|
||||
"name": "柠檬",
|
||||
"filename": "fruit-lemon.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/0391292554cf8efd457c44caedf67867.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/0391292554cf8efd457c44caedf67867.jpg",
|
||||
"size": 13878,
|
||||
"sha1": "c0a31fc168d78346edeef962704de7fadf7414aa"
|
||||
},
|
||||
{
|
||||
"name": "荔枝",
|
||||
"filename": "fruit-lychee.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/49b39ce93043ec8a6f24bcef9bc45f96.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/49b39ce93043ec8a6f24bcef9bc45f96.jpg",
|
||||
"size": 11459,
|
||||
"sha1": "2a47e25354ac30f7c9fad88046b1a4049a77e4dd"
|
||||
}
|
||||
]
|
||||
119
tools/real_photos_result.json
普通文件
@@ -0,0 +1,119 @@
|
||||
[
|
||||
{
|
||||
"id": 28,
|
||||
"name": "餐饮服务",
|
||||
"filename": "job-catering.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/4e4f911ac45e9dc5471597a8ff7862f7.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/4e4f911ac45e9dc5471597a8ff7862f7.jpg",
|
||||
"size": 16228,
|
||||
"sha1": "99699e9e1ef1399488943dfc957299fa6064929f"
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"name": "家政保洁",
|
||||
"filename": "job-clean.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/915423715a937ad7ba4b737690c963d2.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/915423715a937ad7ba4b737690c963d2.jpg",
|
||||
"size": 10120,
|
||||
"sha1": "98ea384b78cb991a3e747b36ff482739f4013478"
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"name": "骑手配送",
|
||||
"filename": "job-rider.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/3fb1eee6654e4d5e7c1b5d3189643fae.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/3fb1eee6654e4d5e7c1b5d3189643fae.jpg",
|
||||
"size": 15521,
|
||||
"sha1": "88c40d6f5461a579b58957c616e72dd4c9fe2ff3"
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"name": "销售客服",
|
||||
"filename": "job-sales.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/d0cc33f85a8580be2f526eb360448867.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/d0cc33f85a8580be2f526eb360448867.jpg",
|
||||
"size": 7681,
|
||||
"sha1": "9feaf44299cf20b3e180242742077325b41f3505"
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"name": "司机出行",
|
||||
"filename": "job-driver.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/f5e2927ba040dd1b917b6fbd6b053458.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/f5e2927ba040dd1b917b6fbd6b053458.jpg",
|
||||
"size": 10392,
|
||||
"sha1": "e037657705d91da753808850d7bb037edb21d81e"
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"name": "教育培训",
|
||||
"filename": "job-edu.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/99c32f9508e3429ea02a4ce1c34f9df2.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/99c32f9508e3429ea02a4ce1c34f9df2.jpg",
|
||||
"size": 13654,
|
||||
"sha1": "8c0e15e6b1ce5e8c3db2433c14e3424c9c98a1d2"
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"name": "兼职短工",
|
||||
"filename": "job-parttime.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/0c913f9d7d72a10c0c598c36eea89b14.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/0c913f9d7d72a10c0c598c36eea89b14.jpg",
|
||||
"size": 22802,
|
||||
"sha1": "2b30e25c4867ed9131793a49d83697d8990849b7"
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"name": "鲜土鸡蛋",
|
||||
"filename": "poultry-egg.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/69b65e455081d5aa8834b48bd879009d.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/69b65e455081d5aa8834b48bd879009d.jpg",
|
||||
"size": 10702,
|
||||
"sha1": "edd5af80cbf31c7edec3bc652a4fd95394069e90"
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"name": "散养土鸡",
|
||||
"filename": "poultry-chicken.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/cafbe884f6616622dcdd5650bbbce3b3.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/cafbe884f6616622dcdd5650bbbce3b3.jpg",
|
||||
"size": 12906,
|
||||
"sha1": "a1b2f6aaf86426eb6d14d5325cb5e3fe2df8f8ab"
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"name": "生鲜猪肉",
|
||||
"filename": "poultry-pork.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/d2a3226fd6ffa12d24db06c9342a9118.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/d2a3226fd6ffa12d24db06c9342a9118.jpg",
|
||||
"size": 19629,
|
||||
"sha1": "0f385594128a9f1892fe883bcf483350c77e05a8"
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"name": "高山牛肉",
|
||||
"filename": "poultry-beef.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/70cafc924bc7e4018bfefbe883e27c78.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/70cafc924bc7e4018bfefbe883e27c78.jpg",
|
||||
"size": 12415,
|
||||
"sha1": "9f4c06215009ed68df1f1b688719f97506e2fe33"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "普工技工",
|
||||
"filename": "job-tech.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/cb17be7476ce1c59e14944f9fe60438c.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/cb17be7476ce1c59e14944f9fe60438c.jpg",
|
||||
"size": 12036,
|
||||
"sha1": "fa74bd9c17980d42c925999accf06569c4083a14"
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"name": "鲜活水产",
|
||||
"filename": "poultry-fish.jpg",
|
||||
"local": "D:\\Softwares\\phpstudy_pro\\WWW\\fenleixinxi\\tools\/..\/public\/uploads\/20261028\/bb63e21dae84dc15056a0b50753577d9.jpg",
|
||||
"rel_url": "\/uploads\/20261028\/bb63e21dae84dc15056a0b50753577d9.jpg",
|
||||
"size": 6027,
|
||||
"sha1": "56885fb7817cfc0f3f809c8ef1f7f8955b6f5855"
|
||||
}
|
||||
]
|
||||
243
tools/simulate_upload_and_sync.py
普通文件
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
1. 将二级分类图标模拟真实后台上传,存放到 public/uploads/20261028/{md5}.png
|
||||
2. 写入本地 fa_attachment 附件表与 fa_fc_category 分类表
|
||||
3. 将新增 uploads 文件及静态一级图标 SFTP 同步至线上服务器
|
||||
4. 将本地 fa_fc_category 与 fa_attachment 变更同步至线上数据库
|
||||
5. 清除缓存并校验线上接口
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import io
|
||||
import time
|
||||
import hashlib
|
||||
import paramiko
|
||||
import pymysql
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
# 本地路径
|
||||
LOCAL_ROOT = r"D:\Softwares\phpstudy_pro\WWW\fenleixinxi"
|
||||
PUBLIC_DIR = os.path.join(LOCAL_ROOT, "public")
|
||||
STATIC_CAT_DIR = os.path.join(PUBLIC_DIR, "static", "categories")
|
||||
DATE_DIR = "20261028"
|
||||
UPLOADS_TARGET_DIR = os.path.join(PUBLIC_DIR, "uploads", DATE_DIR)
|
||||
|
||||
# 远程配置
|
||||
REMOTE_HOST = "8.137.181.63"
|
||||
REMOTE_PORT = 22
|
||||
REMOTE_USER = "root"
|
||||
REMOTE_PASS = "liyanxia@6688"
|
||||
REMOTE_ROOT = "/www/wwwroot/fenleixinxi.gzyakj.com.cn"
|
||||
DB_USER = "fenleixinxi"
|
||||
DB_PASS = "fenleixinxi"
|
||||
DB_NAME = "fenleixinxi"
|
||||
|
||||
# 本地数据库配置
|
||||
LOCAL_DB_HOST = "127.0.0.1"
|
||||
LOCAL_DB_PORT = 3307
|
||||
|
||||
# 13个二级分类映射关系: (分类ID, 本地原文件名, 分类名)
|
||||
SUB_CATS = [
|
||||
(27, "job-tech.png", "普工技工"),
|
||||
(28, "job-catering.png", "餐饮服务"),
|
||||
(29, "job-clean.png", "家政保洁"),
|
||||
(30, "job-rider.png", "骑手配送"),
|
||||
(31, "job-sales.png", "销售客服"),
|
||||
(32, "job-driver.png", "司机出行"),
|
||||
(33, "job-edu.png", "教育培训"),
|
||||
(34, "job-parttime.png", "兼职短工"),
|
||||
(35, "poultry-egg.png", "鲜土鸡蛋"),
|
||||
(36, "poultry-chicken.png", "散养土鸡"),
|
||||
(37, "poultry-pork.png", "生鲜猪肉"),
|
||||
(38, "poultry-beef.png", "高山牛肉"),
|
||||
(39, "poultry-fish.png", "鲜活水产"),
|
||||
]
|
||||
|
||||
def ssh_exec(client, cmd):
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
out = stdout.read().decode("utf-8", errors="replace").strip()
|
||||
err = stderr.read().decode("utf-8", errors="replace").strip()
|
||||
return out, err
|
||||
|
||||
def main():
|
||||
print("=== [第一步] 模拟真实上传:将二级分类图标移入 uploads 目录 ===")
|
||||
os.makedirs(UPLOADS_TARGET_DIR, exist_ok=True)
|
||||
|
||||
# 连接本地数据库
|
||||
local_conn = pymysql.connect(
|
||||
host=LOCAL_DB_HOST,
|
||||
port=LOCAL_DB_PORT,
|
||||
user=DB_USER,
|
||||
password=DB_PASS,
|
||||
database=DB_NAME,
|
||||
charset="utf8mb4",
|
||||
autocommit=True
|
||||
)
|
||||
|
||||
uploaded_files = [] # 保存 (本地绝对路径, 相对远程路径, URL)
|
||||
|
||||
with local_conn.cursor() as cur:
|
||||
for cat_id, filename, cat_name in SUB_CATS:
|
||||
src_path = os.path.join(STATIC_CAT_DIR, filename)
|
||||
if not os.path.exists(src_path):
|
||||
print(f" ✗ 未找到原始图标文件: {src_path}")
|
||||
continue
|
||||
|
||||
with open(src_path, "rb") as f:
|
||||
content = f.read()
|
||||
|
||||
file_md5 = hashlib.md5(content).hexdigest()
|
||||
file_sha1 = hashlib.sha1(content).hexdigest()
|
||||
file_size = len(content)
|
||||
|
||||
dest_filename = f"{file_md5}.png"
|
||||
dest_abs_path = os.path.join(UPLOADS_TARGET_DIR, dest_filename)
|
||||
|
||||
# 写入本地 uploads
|
||||
with open(dest_abs_path, "wb") as f:
|
||||
f.write(content)
|
||||
|
||||
url_path = f"/uploads/{DATE_DIR}/{dest_filename}"
|
||||
uploaded_files.append((dest_abs_path, f"public/uploads/{DATE_DIR}/{dest_filename}", url_path))
|
||||
|
||||
now_ts = int(time.time())
|
||||
|
||||
# 1. 写入/更新本地 fa_attachment
|
||||
cur.execute("SELECT id FROM fa_attachment WHERE url = %s LIMIT 1", (url_path,))
|
||||
att = cur.fetchone()
|
||||
if not att:
|
||||
cur.execute("""
|
||||
INSERT INTO fa_attachment
|
||||
(category, admin_id, user_id, url, imagewidth, imageheight, imagetype, imageframes, filename, filesize, mimetype, createtime, updatetime, uploadtime, storage, sha1)
|
||||
VALUES
|
||||
('category', 1, 0, %s, 240, 240, 'png', 0, %s, %s, 'image/png', %s, %s, %s, 'local', %s)
|
||||
""", (url_path, filename, file_size, now_ts, now_ts, now_ts, file_sha1))
|
||||
|
||||
# 2. 更新本地 fa_fc_category 记录的 image 字段
|
||||
cur.execute("UPDATE fa_fc_category SET image = %s, updatetime = %s WHERE id = %s", (url_path, now_ts, cat_id))
|
||||
print(f" ✓ 分类 [{cat_name}] (ID:{cat_id}) -> 模拟上传为: {url_path}")
|
||||
|
||||
print("\n=== [第二步] 连接线上服务器并上传文件 (SFTP) ===")
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(REMOTE_HOST, port=REMOTE_PORT, username=REMOTE_USER, password=REMOTE_PASS, timeout=30)
|
||||
print("✓ 远程 SSH 连接成功")
|
||||
|
||||
# 创建远程 uploads 目录
|
||||
remote_upload_dir = f"{REMOTE_ROOT}/public/uploads/{DATE_DIR}"
|
||||
ssh_exec(ssh, f"mkdir -p {remote_upload_dir}")
|
||||
ssh_exec(ssh, f"mkdir -p {REMOTE_ROOT}/public/static/categories")
|
||||
|
||||
sftp = ssh.open_sftp()
|
||||
|
||||
# 1. 上传新生成的二级分类 uploads 图片
|
||||
print(f"正在上传 {len(uploaded_files)} 个二级分类图标到线上 uploads 目录...")
|
||||
for local_file, remote_rel_file, url in uploaded_files:
|
||||
remote_full = f"{REMOTE_ROOT}/{remote_rel_file}"
|
||||
sftp.put(local_file, remote_full)
|
||||
ssh_exec(ssh, f"chown www:www {remote_full}")
|
||||
print("✓ 二级分类图片已全量上传至线上 uploads 目录")
|
||||
|
||||
# 2. 上传一级分类图标 (cat-job.png, cat-poultry.png)
|
||||
parent_icons = ["cat-job.png", "cat-poultry.png"]
|
||||
for icon_name in parent_icons:
|
||||
loc = os.path.join(STATIC_CAT_DIR, icon_name)
|
||||
if os.path.exists(loc):
|
||||
rem = f"{REMOTE_ROOT}/public/static/categories/{icon_name}"
|
||||
sftp.put(loc, rem)
|
||||
ssh_exec(ssh, f"chown www:www {rem}")
|
||||
print(f" ✓ 已同步一级图标: {icon_name}")
|
||||
|
||||
sftp.close()
|
||||
|
||||
print("\n=== [第三步] 同步本地分类数据与附件记录至线上数据库 ===")
|
||||
# 导出本地分类表 SQL
|
||||
with local_conn.cursor() as cur:
|
||||
cur.execute("SELECT * FROM fa_fc_category;")
|
||||
cat_rows = cur.fetchall()
|
||||
cur.execute("DESCRIBE fa_fc_category;")
|
||||
cat_cols = [f[0] for f in cur.fetchall()]
|
||||
|
||||
# 查询新增的 attachment 记录
|
||||
cur.execute(f"SELECT * FROM fa_attachment WHERE url LIKE '/uploads/{DATE_DIR}/%';")
|
||||
att_rows = cur.fetchall()
|
||||
cur.execute("DESCRIBE fa_attachment;")
|
||||
att_cols = [f[0] for f in cur.fetchall()]
|
||||
|
||||
# 构造同步 SQL
|
||||
sync_sqls = ["SET FOREIGN_KEY_CHECKS = 0;"]
|
||||
|
||||
# 1. 线上清空并全量重写 fa_fc_category,确保和本地 100% 对齐
|
||||
sync_sqls.append("TRUNCATE TABLE fa_fc_category;")
|
||||
col_str = ", ".join([f"`{c}`" for c in cat_cols])
|
||||
for row in cat_rows:
|
||||
val_list = []
|
||||
for v in row:
|
||||
if v is None:
|
||||
val_list.append("NULL")
|
||||
elif isinstance(v, (int, float)):
|
||||
val_list.append(str(v))
|
||||
else:
|
||||
esc = str(v).replace("\\", "\\\\").replace("'", "\\'")
|
||||
val_list.append(f"'{esc}'")
|
||||
sync_sqls.append(f"INSERT INTO fa_fc_category ({col_str}) VALUES ({', '.join(val_list)});")
|
||||
|
||||
# 2. 插入 attachment 记录
|
||||
if att_rows:
|
||||
att_col_str = ", ".join([f"`{c}`" for c in att_cols])
|
||||
for row in att_rows:
|
||||
val_list = []
|
||||
for v in row:
|
||||
if v is None:
|
||||
val_list.append("NULL")
|
||||
elif isinstance(v, (int, float)):
|
||||
val_list.append(str(v))
|
||||
else:
|
||||
esc = str(v).replace("\\", "\\\\").replace("'", "\\'")
|
||||
val_list.append(f"'{esc}'")
|
||||
sync_sqls.append(f"INSERT IGNORE INTO fa_attachment ({att_col_str}) VALUES ({', '.join(val_list)});")
|
||||
|
||||
sync_sqls.append("SET FOREIGN_KEY_CHECKS = 1;")
|
||||
full_sql = "\n".join(sync_sqls)
|
||||
|
||||
# 写入远程执行
|
||||
mysql_cmd = f'mysql -u{DB_USER} -p{DB_PASS} --default-character-set=utf8mb4 {DB_NAME} 2>/dev/null <<\'SQLEOF\'\n{full_sql}\nSQLEOF'
|
||||
out, err = ssh_exec(ssh, mysql_cmd)
|
||||
if err and "error" in err.lower():
|
||||
print(f"✗ 线上数据库更新失败: {err}")
|
||||
else:
|
||||
print("✓ 线上数据库 fa_fc_category 与 fa_attachment 已经完美同步!")
|
||||
|
||||
print("\n=== [第四步] 清理缓存与验证 ===")
|
||||
ssh_exec(ssh, f"rm -rf {REMOTE_ROOT}/runtime/temp/* {REMOTE_ROOT}/runtime/cache/*")
|
||||
print("✓ 线上运行时缓存清理完成")
|
||||
|
||||
# 校验线上分类数量
|
||||
out, _ = ssh_exec(ssh, f'mysql -u{DB_USER} -p{DB_PASS} {DB_NAME} -e "SELECT COUNT(*) FROM fa_fc_category;" 2>/dev/null')
|
||||
print(f" 线上分类总数: {out.strip().split(chr(10))[-1].strip()} 条 (本地: {len(cat_rows)} 条)")
|
||||
|
||||
# 测试线上图片 URL 可访问性
|
||||
sample_url = uploaded_files[0][2]
|
||||
test_img_url = f"https://fenleixinxi.gzyakj.com.cn{sample_url}"
|
||||
cmd_curl = f"curl -s -o /dev/null -w '%{{http_code}}' -k '{test_img_url}'"
|
||||
code, _ = ssh_exec(ssh, cmd_curl)
|
||||
print(f" 线上二级分类上传图片可访问性验证 ({sample_url}): HTTP {code}")
|
||||
|
||||
# 测试线上分类 API
|
||||
api_url = "https://fenleixinxi.gzyakj.com.cn/api/v1/home/categories"
|
||||
cmd_api = f"curl -s -k '{api_url}'"
|
||||
api_res, _ = ssh_exec(ssh, cmd_api)
|
||||
if '"code":1' in api_res and sample_url in api_res:
|
||||
print(" ✓ 线上分类 API 成功返回新二级分类及真实 uploads 图片地址!")
|
||||
else:
|
||||
print(f" API 响应采样: {api_res[:200]}")
|
||||
|
||||
ssh.close()
|
||||
local_conn.close()
|
||||
print("\n✓ 全部同步工作圆满完成!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
200
tools/sync_more_fruits_to_online.py
普通文件
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
1. 校验/补齐 7~8 个新增水果二级分类实拍图
|
||||
2. 插入本地 fa_attachment 与 fa_fc_category 表 (PID: 2)
|
||||
3. SFTP 上传图片到线上服务器
|
||||
4. 同步线上数据库 fa_fc_category 与 fa_attachment
|
||||
5. 清除缓存并验证
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import io
|
||||
import json
|
||||
import time
|
||||
import paramiko
|
||||
import pymysql
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
LOCAL_ROOT = r"D:\Softwares\phpstudy_pro\WWW\fenleixinxi"
|
||||
UPLOADS_DIR = os.path.join(LOCAL_ROOT, "public", "uploads", "20261028")
|
||||
JSON_FILE = os.path.join(LOCAL_ROOT, "tools", "more_fruits_result.json")
|
||||
|
||||
REMOTE_HOST = "8.137.181.63"
|
||||
REMOTE_PORT = 22
|
||||
REMOTE_USER = "root"
|
||||
REMOTE_PASS = "liyanxia@6688"
|
||||
REMOTE_ROOT = "/www/wwwroot/fenleixinxi.gzyakj.com.cn"
|
||||
DB_USER = "fenleixinxi"
|
||||
DB_PASS = "fenleixinxi"
|
||||
DB_NAME = "fenleixinxi"
|
||||
|
||||
LOCAL_DB_HOST = "127.0.0.1"
|
||||
LOCAL_DB_PORT = 3307
|
||||
|
||||
def ssh_exec(client, cmd):
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
out = stdout.read().decode("utf-8", errors="replace").strip()
|
||||
err = stderr.read().decode("utf-8", errors="replace").strip()
|
||||
return out, err
|
||||
|
||||
def main():
|
||||
with open(JSON_FILE, "r", encoding="utf-8") as f:
|
||||
fruits_list = json.load(f)
|
||||
|
||||
# 过滤出实际存在本地文件且大小 > 0 的项
|
||||
valid_fruits = []
|
||||
for item in fruits_list:
|
||||
local_p = item['local']
|
||||
if os.path.exists(local_p) and os.path.getsize(local_p) > 0:
|
||||
valid_fruits.append(item)
|
||||
|
||||
print(f"=== [1/4] 本地写入新增水果二级分类 ({len(valid_fruits)} 项,PID: 2) ===")
|
||||
conn = pymysql.connect(
|
||||
host=LOCAL_DB_HOST,
|
||||
port=LOCAL_DB_PORT,
|
||||
user=DB_USER,
|
||||
password=DB_PASS,
|
||||
database=DB_NAME,
|
||||
charset="utf8mb4",
|
||||
autocommit=True
|
||||
)
|
||||
|
||||
now_ts = int(time.time())
|
||||
with conn.cursor() as cur:
|
||||
# 获取水果专区当前最大 sort
|
||||
cur.execute("SELECT MAX(sort) FROM fa_fc_category WHERE pid = 2;")
|
||||
max_sort = cur.fetchone()[0] or 100
|
||||
|
||||
for item in valid_fruits:
|
||||
name = item['name']
|
||||
url = item['rel_url']
|
||||
size = item['size']
|
||||
sha1 = item['sha1']
|
||||
filename = item['filename']
|
||||
|
||||
# 1. 检查是否存在该水果分类
|
||||
cur.execute("SELECT id FROM fa_fc_category WHERE pid = 2 AND name = %s LIMIT 1;", (name,))
|
||||
existing = cur.fetchone()
|
||||
|
||||
max_sort += 10
|
||||
if existing:
|
||||
cat_id = existing[0]
|
||||
cur.execute("UPDATE fa_fc_category SET image = %s, enabled = 1, updatetime = %s WHERE id = %s;", (url, now_ts, cat_id))
|
||||
print(f" ✓ 更新水果子分类: [{name}] (ID:{cat_id}) -> {url}")
|
||||
else:
|
||||
cur.execute("""
|
||||
INSERT INTO fa_fc_category
|
||||
(pid, name, icon_key, image, kind, color, sort, enabled, post_count, createtime, updatetime)
|
||||
VALUES
|
||||
(2, %s, '', %s, 'normal', '', %s, 1, 0, %s, %s);
|
||||
""", (name, url, max_sort, now_ts, now_ts))
|
||||
cat_id = cur.lastrowid
|
||||
print(f" ✓ 新增水果子分类成功: [{name}] (ID:{cat_id}) -> {url}")
|
||||
|
||||
# 2. 插入 attachment 记录
|
||||
cur.execute("SELECT id FROM fa_attachment WHERE url = %s LIMIT 1;", (url,))
|
||||
if not cur.fetchone():
|
||||
cur.execute("""
|
||||
INSERT INTO fa_attachment
|
||||
(category, admin_id, user_id, url, imagewidth, imageheight, imagetype, imageframes, filename, filesize, mimetype, createtime, updatetime, uploadtime, storage, sha1)
|
||||
VALUES
|
||||
('category', 1, 0, %s, 240, 240, 'jpg', 0, %s, %s, 'image/jpeg', %s, %s, %s, 'local', %s);
|
||||
""", (url, filename, size, now_ts, now_ts, now_ts, sha1))
|
||||
|
||||
# 查询当前水果专区所有子分类
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT id, name, image FROM fa_fc_category WHERE pid = 2 ORDER BY sort, id;")
|
||||
all_fruits = cur.fetchall()
|
||||
print(f"\n本地【水果专区】当前子分类总数: {len(all_fruits)} 个")
|
||||
for f in all_fruits:
|
||||
print(f" * ID:{f[0]:<2} | {f[1]:<6} | {f[2]}")
|
||||
|
||||
# 读取全量 category 表与 attachment 表用于线上同步
|
||||
cur.execute("SELECT * FROM fa_fc_category;")
|
||||
cat_rows = cur.fetchall()
|
||||
cur.execute("DESCRIBE fa_fc_category;")
|
||||
cat_cols = [c[0] for c in cur.fetchall()]
|
||||
|
||||
cur.execute("SELECT * FROM fa_attachment WHERE url LIKE '/uploads/20261028/%.jpg';")
|
||||
att_rows = cur.fetchall()
|
||||
cur.execute("DESCRIBE fa_attachment;")
|
||||
att_cols = [c[0] for c in cur.fetchall()]
|
||||
|
||||
conn.close()
|
||||
|
||||
print("\n=== [2/4] 连接线上服务器并 SFTP 上传新增水果图片 ===")
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(REMOTE_HOST, port=REMOTE_PORT, username=REMOTE_USER, password=REMOTE_PASS, timeout=30)
|
||||
print("✓ 远程 SSH 连接成功")
|
||||
|
||||
sftp = ssh.open_sftp()
|
||||
for item in valid_fruits:
|
||||
local_p = item['local']
|
||||
rel_url = item['rel_url'].lstrip('/')
|
||||
remote_p = f"{REMOTE_ROOT}/public/{rel_url}"
|
||||
|
||||
sftp.put(local_p, remote_p)
|
||||
ssh_exec(ssh, f"chown www:www {remote_p}")
|
||||
print(f" ✓ SFTP 上传成功: {remote_p}")
|
||||
|
||||
sftp.close()
|
||||
|
||||
print("\n=== [3/4] 同步更新线上数据库 fa_fc_category 与 fa_attachment ===")
|
||||
sync_sqls = ["SET FOREIGN_KEY_CHECKS = 0;"]
|
||||
sync_sqls.append("TRUNCATE TABLE fa_fc_category;")
|
||||
col_str = ", ".join([f"`{c}`" for c in cat_cols])
|
||||
for row in cat_rows:
|
||||
val_list = []
|
||||
for v in row:
|
||||
if v is None:
|
||||
val_list.append("NULL")
|
||||
elif isinstance(v, (int, float)):
|
||||
val_list.append(str(v))
|
||||
else:
|
||||
esc = str(v).replace("\\", "\\\\").replace("'", "\\'")
|
||||
val_list.append(f"'{esc}'")
|
||||
sync_sqls.append(f"INSERT INTO fa_fc_category ({col_str}) VALUES ({', '.join(val_list)});")
|
||||
|
||||
if att_rows:
|
||||
att_col_str = ", ".join([f"`{c}`" for c in att_cols])
|
||||
for row in att_rows:
|
||||
val_list = []
|
||||
for v in row:
|
||||
if v is None:
|
||||
val_list.append("NULL")
|
||||
elif isinstance(v, (int, float)):
|
||||
val_list.append(str(v))
|
||||
else:
|
||||
esc = str(v).replace("\\", "\\\\").replace("'", "\\'")
|
||||
val_list.append(f"'{esc}'")
|
||||
sync_sqls.append(f"INSERT IGNORE INTO fa_attachment ({att_col_str}) VALUES ({', '.join(val_list)});")
|
||||
|
||||
sync_sqls.append("SET FOREIGN_KEY_CHECKS = 1;")
|
||||
full_sql = "\n".join(sync_sqls)
|
||||
|
||||
mysql_cmd = f'mysql -u{DB_USER} -p{DB_PASS} --default-character-set=utf8mb4 {DB_NAME} 2>/dev/null <<\'SQLEOF\'\n{full_sql}\nSQLEOF'
|
||||
out, err = ssh_exec(ssh, mysql_cmd)
|
||||
if err and "error" in err.lower():
|
||||
print(f"✗ 线上数据库更新失败: {err}")
|
||||
else:
|
||||
print("✓ 线上数据库 fa_fc_category 与 fa_attachment 已经完美同步!")
|
||||
|
||||
print("\n=== [4/4] 清除线上缓存并验证接口 ===")
|
||||
ssh_exec(ssh, f"rm -rf {REMOTE_ROOT}/runtime/temp/* {REMOTE_ROOT}/runtime/cache/*")
|
||||
|
||||
cmd_api = "curl -s -k 'https://fenleixinxi.gzyakj.com.cn/api/v1/home/categories'"
|
||||
api_res, _ = ssh_exec(ssh, cmd_api)
|
||||
if "芒果" in api_res and "香蕉" in api_res:
|
||||
print(" ✓ 线上分类 API 成功返回新增水果二级分类!")
|
||||
else:
|
||||
print(" API 返回未找到新增水果")
|
||||
|
||||
ssh.close()
|
||||
print("\n✓ 水果专区二级分类扩充及线上同步全部圆满完成!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
178
tools/sync_real_photos_to_online.py
普通文件
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
将 13 张真实商业摄影实拍图更新至本地与线上数据库,并 SFTP 上传至线上服务器
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import io
|
||||
import json
|
||||
import time
|
||||
import paramiko
|
||||
import pymysql
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
LOCAL_ROOT = r"D:\Softwares\phpstudy_pro\WWW\fenleixinxi"
|
||||
JSON_FILE = os.path.join(LOCAL_ROOT, "tools", "real_photos_result.json")
|
||||
|
||||
REMOTE_HOST = "8.137.181.63"
|
||||
REMOTE_PORT = 22
|
||||
REMOTE_USER = "root"
|
||||
REMOTE_PASS = "liyanxia@6688"
|
||||
REMOTE_ROOT = "/www/wwwroot/fenleixinxi.gzyakj.com.cn"
|
||||
DB_USER = "fenleixinxi"
|
||||
DB_PASS = "fenleixinxi"
|
||||
DB_NAME = "fenleixinxi"
|
||||
|
||||
LOCAL_DB_HOST = "127.0.0.1"
|
||||
LOCAL_DB_PORT = 3307
|
||||
|
||||
def ssh_exec(client, cmd):
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
out = stdout.read().decode("utf-8", errors="replace").strip()
|
||||
err = stderr.read().decode("utf-8", errors="replace").strip()
|
||||
return out, err
|
||||
|
||||
def main():
|
||||
with open(JSON_FILE, "r", encoding="utf-8") as f:
|
||||
photo_list = json.load(f)
|
||||
|
||||
print(f"=== [1/4] 更新本地数据库 fa_fc_category 与 fa_attachment ({len(photo_list)} 项) ===")
|
||||
conn = pymysql.connect(
|
||||
host=LOCAL_DB_HOST,
|
||||
port=LOCAL_DB_PORT,
|
||||
user=DB_USER,
|
||||
password=DB_PASS,
|
||||
database=DB_NAME,
|
||||
charset="utf8mb4",
|
||||
autocommit=True
|
||||
)
|
||||
|
||||
with conn.cursor() as cur:
|
||||
now_ts = int(time.time())
|
||||
for item in photo_list:
|
||||
cat_id = item['id']
|
||||
url = item['rel_url']
|
||||
size = item['size']
|
||||
sha1 = item['sha1']
|
||||
filename = item['filename']
|
||||
|
||||
# 更新分类表
|
||||
cur.execute("UPDATE fa_fc_category SET image = %s, updatetime = %s WHERE id = %s", (url, now_ts, cat_id))
|
||||
|
||||
# 插入/更新 attachment 表
|
||||
cur.execute("SELECT id FROM fa_attachment WHERE url = %s LIMIT 1", (url,))
|
||||
att = cur.fetchone()
|
||||
if not att:
|
||||
cur.execute("""
|
||||
INSERT INTO fa_attachment
|
||||
(category, admin_id, user_id, url, imagewidth, imageheight, imagetype, imageframes, filename, filesize, mimetype, createtime, updatetime, uploadtime, storage, sha1)
|
||||
VALUES
|
||||
('category', 1, 0, %s, 240, 240, 'jpg', 0, %s, %s, 'image/jpeg', %s, %s, %s, 'local', %s)
|
||||
""", (url, filename, size, now_ts, now_ts, now_ts, sha1))
|
||||
|
||||
print(f" ✓ 本地更新: [{item['name']}] (ID:{cat_id}) -> {url}")
|
||||
|
||||
# 读取本地最新 category 和 attachment
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT * FROM fa_fc_category;")
|
||||
cat_rows = cur.fetchall()
|
||||
cur.execute("DESCRIBE fa_fc_category;")
|
||||
cat_cols = [f[0] for f in cur.fetchall()]
|
||||
|
||||
cur.execute("SELECT * FROM fa_attachment WHERE url LIKE '/uploads/20261028/%.jpg';")
|
||||
att_rows = cur.fetchall()
|
||||
cur.execute("DESCRIBE fa_attachment;")
|
||||
att_cols = [f[0] for f in cur.fetchall()]
|
||||
|
||||
conn.close()
|
||||
|
||||
print("\n=== [2/4] 连接线上服务器并通过 SFTP 上传真实照片文件 ===")
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(REMOTE_HOST, port=REMOTE_PORT, username=REMOTE_USER, password=REMOTE_PASS, timeout=30)
|
||||
print("✓ 远程 SSH 连接成功")
|
||||
|
||||
sftp = ssh.open_sftp()
|
||||
remote_upload_dir = f"{REMOTE_ROOT}/public/uploads/20261028"
|
||||
ssh_exec(ssh, f"mkdir -p {remote_upload_dir}")
|
||||
|
||||
for item in photo_list:
|
||||
local_path = item['local']
|
||||
rel_url = item['rel_url'].lstrip('/')
|
||||
remote_path = f"{REMOTE_ROOT}/public/{rel_url}"
|
||||
|
||||
sftp.put(local_path, remote_path)
|
||||
ssh_exec(ssh, f"chown www:www {remote_path}")
|
||||
print(f" ✓ SFTP 已上传: {remote_path} ({os.path.basename(local_path)})")
|
||||
|
||||
sftp.close()
|
||||
|
||||
print("\n=== [3/4] 同步更新线上数据库 fa_fc_category 与 fa_attachment ===")
|
||||
sync_sqls = ["SET FOREIGN_KEY_CHECKS = 0;"]
|
||||
|
||||
# 全量同步分类
|
||||
sync_sqls.append("TRUNCATE TABLE fa_fc_category;")
|
||||
col_str = ", ".join([f"`{c}`" for c in cat_cols])
|
||||
for row in cat_rows:
|
||||
val_list = []
|
||||
for v in row:
|
||||
if v is None:
|
||||
val_list.append("NULL")
|
||||
elif isinstance(v, (int, float)):
|
||||
val_list.append(str(v))
|
||||
else:
|
||||
esc = str(v).replace("\\", "\\\\").replace("'", "\\'")
|
||||
val_list.append(f"'{esc}'")
|
||||
sync_sqls.append(f"INSERT INTO fa_fc_category ({col_str}) VALUES ({', '.join(val_list)});")
|
||||
|
||||
# 插入 attachment
|
||||
if att_rows:
|
||||
att_col_str = ", ".join([f"`{c}`" for c in att_cols])
|
||||
for row in att_rows:
|
||||
val_list = []
|
||||
for v in row:
|
||||
if v is None:
|
||||
val_list.append("NULL")
|
||||
elif isinstance(v, (int, float)):
|
||||
val_list.append(str(v))
|
||||
else:
|
||||
esc = str(v).replace("\\", "\\\\").replace("'", "\\'")
|
||||
val_list.append(f"'{esc}'")
|
||||
sync_sqls.append(f"INSERT IGNORE INTO fa_attachment ({att_col_str}) VALUES ({', '.join(val_list)});")
|
||||
|
||||
sync_sqls.append("SET FOREIGN_KEY_CHECKS = 1;")
|
||||
full_sql = "\n".join(sync_sqls)
|
||||
|
||||
mysql_cmd = f'mysql -u{DB_USER} -p{DB_PASS} --default-character-set=utf8mb4 {DB_NAME} 2>/dev/null <<\'SQLEOF\'\n{full_sql}\nSQLEOF'
|
||||
out, err = ssh_exec(ssh, mysql_cmd)
|
||||
if err and "error" in err.lower():
|
||||
print(f"✗ 线上数据库更新失败: {err}")
|
||||
else:
|
||||
print("✓ 线上数据库已更新全部真实实拍照路径与附件数据!")
|
||||
|
||||
print("\n=== [4/4] 清理缓存并校验真实实拍照访问 ===")
|
||||
ssh_exec(ssh, f"rm -rf {REMOTE_ROOT}/runtime/temp/* {REMOTE_ROOT}/runtime/cache/*")
|
||||
|
||||
# 抽样验证 2 张真实照片的 HTTP 返回码
|
||||
for sample in photo_list[:2]:
|
||||
test_url = f"https://fenleixinxi.gzyakj.com.cn{sample['rel_url']}"
|
||||
cmd_curl = f"curl -s -o /dev/null -w '%{{http_code}}' -k '{test_url}'"
|
||||
code, _ = ssh_exec(ssh, cmd_curl)
|
||||
print(f" 真实照片 [{sample['name']}] HTTP 校验 ({test_url}): HTTP {code}")
|
||||
|
||||
# 线上分类接口校验
|
||||
cmd_api = "curl -s -k 'https://fenleixinxi.gzyakj.com.cn/api/v1/home/categories'"
|
||||
api_res, _ = ssh_exec(ssh, cmd_api)
|
||||
if photo_list[0]['rel_url'] in api_res:
|
||||
print(" ✓ 线上分类接口已成功返回最新真实实拍照 URL!")
|
||||
else:
|
||||
print(f" 接口返回采样: {api_res[:200]}")
|
||||
|
||||
ssh.close()
|
||||
print("\n✓ 真实照片更新与线上同步圆满完成!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -13,19 +13,29 @@
|
||||
<image class="fc-icon fc-icon--md" :src="iconUrl('phone-orange')" mode="aspectFit" />
|
||||
<text class="call-modal__phone">{{ contactLabel }}</text>
|
||||
</view>
|
||||
<text class="call-modal__quota">
|
||||
<text class="call-modal__quota" :class="{ 'call-modal__quota--warn': isQuotaEmpty }">
|
||||
{{ quotaText }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 额度耗尽引导开通会员 -->
|
||||
<view v-if="isQuotaEmpty" class="call-modal__vip-box" @click="goMember">
|
||||
<image class="fc-icon fc-icon--sm" :src="iconUrl('crown')" mode="aspectFit" />
|
||||
<view class="call-modal__vip-info">
|
||||
<text class="call-modal__vip-title">开通同城 VIP 会员</text>
|
||||
<text class="call-modal__vip-desc">每日无限次拨打 · 免费浏览 · 极速发布</text>
|
||||
</view>
|
||||
<text class="call-modal__vip-arrow">去开通 ›</text>
|
||||
</view>
|
||||
|
||||
<!-- 风险提示 -->
|
||||
<view class="call-modal__warn">
|
||||
<image class="fc-icon fc-icon--sm" :src="iconUrl('info')" mode="aspectFit" />
|
||||
<text class="call-modal__warn-text">请自行鉴别农产品信息真伪,在未核实清楚前不要提前给对方转账,慎防诈骗!</text>
|
||||
</view>
|
||||
|
||||
<!-- 免责声明勾选 -->
|
||||
<view class="call-modal__agree" @click="agree = !agree">
|
||||
<!-- 免责声明勾选(额度耗尽无需勾选) -->
|
||||
<view v-if="!isQuotaEmpty" class="call-modal__agree" @click="agree = !agree">
|
||||
<view class="call-modal__checkbox" :class="{ 'call-modal__checkbox--on': agree }">
|
||||
<text v-if="agree" class="call-modal__checkbox-tick">✓</text>
|
||||
</view>
|
||||
@@ -33,14 +43,17 @@
|
||||
<text class="call-modal__agree-link" @click.stop="goDisclaimer">《平台免责声明》</text>
|
||||
</view>
|
||||
|
||||
<!-- 主拨打按钮 -->
|
||||
<!-- 主操作按钮:有额度显示立即拨打,耗尽显示开通会员 -->
|
||||
<view
|
||||
class="call-modal__btn"
|
||||
:class="{ 'call-modal__btn--disabled': loading }"
|
||||
:class="{
|
||||
'call-modal__btn--disabled': loading && !isQuotaEmpty,
|
||||
'call-modal__btn--vip': isQuotaEmpty
|
||||
}"
|
||||
@click="handleDial"
|
||||
>
|
||||
<image class="fc-icon fc-icon--sm" :src="iconUrl('phone')" mode="aspectFit" />
|
||||
<text>{{ loading ? '连接中…' : '立即拨打' }}</text>
|
||||
<image class="fc-icon fc-icon--sm" :src="iconUrl(isQuotaEmpty ? 'crown' : 'phone')" mode="aspectFit" />
|
||||
<text>{{ isQuotaEmpty ? '开通会员立即拨打' : (loading ? '连接中…' : '立即拨打') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -83,12 +96,25 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* 联系对象名称脱敏或提示
|
||||
*/
|
||||
contactLabel() {
|
||||
return this.contactName ? `联系 ${this.contactName}` : '确认后直接拨打'
|
||||
},
|
||||
/**
|
||||
* 额度是否已用尽
|
||||
*/
|
||||
isQuotaEmpty() {
|
||||
return !this.unlimited && Number(this.quotaTotal) > 0 && Number(this.quotaLeft) <= 0
|
||||
},
|
||||
/**
|
||||
* 额度展示文案
|
||||
*/
|
||||
quotaText() {
|
||||
if (this.unlimited || this.quotaTotal === 0) return '会员尊享 · 不限拨打次数'
|
||||
const left = Math.max(0, Number(this.quotaLeft) || 0)
|
||||
if (left <= 0) return '今日拨打额度已用完(0 次)'
|
||||
return `今日剩余 ${left} 次拨打机会`
|
||||
},
|
||||
},
|
||||
@@ -101,9 +127,13 @@ export default {
|
||||
this.$emit('close')
|
||||
},
|
||||
/**
|
||||
* 点击拨打:先校验免责声明勾选,再交由父级执行拨打接口
|
||||
* 点击拨打或引导开通会员
|
||||
*/
|
||||
handleDial() {
|
||||
if (this.isQuotaEmpty) {
|
||||
this.goMember()
|
||||
return
|
||||
}
|
||||
if (this.loading) return
|
||||
if (!this.agree) {
|
||||
uni.showToast({ title: '请先勾选同意平台免责声明', icon: 'none' })
|
||||
@@ -111,6 +141,13 @@ export default {
|
||||
}
|
||||
this.$emit('confirm')
|
||||
},
|
||||
/**
|
||||
* 跳转会员中心开通特权
|
||||
*/
|
||||
goMember() {
|
||||
this.handleClose()
|
||||
uni.navigateTo({ url: '/pages/member/index' })
|
||||
},
|
||||
/**
|
||||
* 查看平台免责声明详情
|
||||
*/
|
||||
@@ -185,6 +222,44 @@ export default {
|
||||
margin-top: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #64748b;
|
||||
|
||||
&--warn {
|
||||
color: #e65100;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.call-modal__vip-box {
|
||||
margin-top: 20rpx;
|
||||
padding: 18rpx 20rpx;
|
||||
border-radius: 18rpx;
|
||||
background: linear-gradient(135deg, #f0fdf4, #fff8e8);
|
||||
border: 1rpx solid #ffe3a6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.call-modal__vip-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.call-modal__vip-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #8a5a00;
|
||||
}
|
||||
.call-modal__vip-desc {
|
||||
margin-top: 4rpx;
|
||||
font-size: 20rpx;
|
||||
color: #b45309;
|
||||
}
|
||||
.call-modal__vip-arrow {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #f5a623;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.call-modal__warn {
|
||||
@@ -266,5 +341,11 @@ export default {
|
||||
&--disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&--vip {
|
||||
background: linear-gradient(135deg, #00e070, #00d26a 45%, #f5a623);
|
||||
color: #04301e;
|
||||
box-shadow: 0 8rpx 24rpx rgba(245, 166, 35, 0.35);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
<view class="cat-subgrid__thumb">
|
||||
<image class="cat-subgrid__img" :src="catIcon(currentCategory)" mode="aspectFill" />
|
||||
</view>
|
||||
<text class="cat-subgrid__name">全部{{ currentCategory ? currentCategory.name : '' }}</text>
|
||||
<text class="cat-subgrid__name">全部</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="sub in currentSubCategories"
|
||||
|
||||
@@ -270,6 +270,44 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 同城 VIP 会员特权引导卡片(非会员引导开通,会员展示专属特权) -->
|
||||
<view v-if="!isMember" class="fc-card detail__vip-card" @click="goMember">
|
||||
<view class="detail__vip-card-left">
|
||||
<view class="detail__vip-card-badge">
|
||||
<image class="fc-icon fc-icon--sm" :src="iconUrl('crown')" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="detail__vip-card-info">
|
||||
<view class="detail__vip-card-title-row">
|
||||
<text class="detail__vip-card-title">开通同城 VIP 会员</text>
|
||||
<text class="detail__vip-card-pill">尊享特权</text>
|
||||
</view>
|
||||
<text class="detail__vip-card-desc">每日免费浏览 · 无限拨打电话 · 专属尊贵标识</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="detail__vip-card-btn">
|
||||
<text class="detail__vip-card-btn-txt">立即开通</text>
|
||||
<text class="detail__vip-card-btn-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="fc-card detail__vip-card detail__vip-card--active" @click="goMember">
|
||||
<view class="detail__vip-card-left">
|
||||
<view class="detail__vip-card-badge detail__vip-card-badge--active">
|
||||
<image class="fc-icon fc-icon--sm" :src="iconUrl('crown')" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="detail__vip-card-info">
|
||||
<view class="detail__vip-card-title-row">
|
||||
<text class="detail__vip-card-title">VIP 尊享特权生效中</text>
|
||||
<text class="detail__vip-card-pill detail__vip-card-pill--active">VIP 会员</text>
|
||||
</view>
|
||||
<text class="detail__vip-card-desc">您已享今日免费浏览与不限拨打电话特权</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="detail__vip-card-btn detail__vip-card-btn--active">
|
||||
<text class="detail__vip-card-btn-txt">会员中心</text>
|
||||
<text class="detail__vip-card-btn-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 联系方式卡已移除:号码不展示,直接通过底部「拨打电话」按钮联系 -->
|
||||
|
||||
<!-- 基础档案信息(信息编号与安全提示) -->
|
||||
@@ -317,23 +355,37 @@
|
||||
<view class="detail__bar-space"></view>
|
||||
|
||||
<!-- 底部吸底操作栏 -->
|
||||
<view class="detail__bar">
|
||||
<view class="detail__bar-item" @click="toggleFav">
|
||||
<image class="fc-icon fc-icon--md" :src="iconUrl(faved ? 'bar-fav-on' : 'bar-fav')" mode="aspectFit" />
|
||||
<text class="detail__bar-text" :class="{ 'detail__bar-text--active': faved }">{{ faved ? '已收藏' : '收藏' }}</text>
|
||||
<view class="detail__bar-wrapper">
|
||||
<!-- 拨打额度用尽吸底提示浮条 -->
|
||||
<view v-if="isCallQuotaOut" class="detail__bar-quota-tip" @click="goMember">
|
||||
<image class="fc-icon fc-icon--xs" :src="iconUrl('crown')" mode="aspectFit" />
|
||||
<text class="detail__bar-quota-txt">今日拨打次数已用完,开通 VIP 尊享不限次拨打</text>
|
||||
<text class="detail__bar-quota-go">去开通 ›</text>
|
||||
</view>
|
||||
<!-- 微信原生转发好友按钮 -->
|
||||
<button class="detail__bar-item detail__bar-item--btn" open-type="share">
|
||||
<image class="fc-icon fc-icon--md" :src="iconUrl('bar-share')" mode="aspectFit" />
|
||||
<text class="detail__bar-text">分享</text>
|
||||
</button>
|
||||
<!-- 生成海报按钮 -->
|
||||
<view class="detail__bar-item" @click="openPosterModal">
|
||||
<image class="fc-icon fc-icon--md" :src="iconUrl('bar-poster')" mode="aspectFit" />
|
||||
<text class="detail__bar-text">海报</text>
|
||||
</view>
|
||||
<view class="detail__bar-main" @click="onMainAction">
|
||||
<text>{{ bottomBarText }}</text>
|
||||
|
||||
<view class="detail__bar">
|
||||
<view class="detail__bar-item" @click="toggleFav">
|
||||
<image class="fc-icon fc-icon--md" :src="iconUrl(faved ? 'bar-fav-on' : 'bar-fav')" mode="aspectFit" />
|
||||
<text class="detail__bar-text" :class="{ 'detail__bar-text--active': faved }">{{ faved ? '已收藏' : '收藏' }}</text>
|
||||
</view>
|
||||
<!-- 微信原生转发好友按钮 -->
|
||||
<button class="detail__bar-item detail__bar-item--btn" open-type="share">
|
||||
<image class="fc-icon fc-icon--md" :src="iconUrl('bar-share')" mode="aspectFit" />
|
||||
<text class="detail__bar-text">分享</text>
|
||||
</button>
|
||||
<!-- 生成海报按钮 -->
|
||||
<view class="detail__bar-item" @click="openPosterModal">
|
||||
<image class="fc-icon fc-icon--md" :src="iconUrl('bar-poster')" mode="aspectFit" />
|
||||
<text class="detail__bar-text">海报</text>
|
||||
</view>
|
||||
<view
|
||||
class="detail__bar-main"
|
||||
:class="{ 'detail__bar-main--vip': isCallQuotaOut }"
|
||||
@click="onMainAction"
|
||||
>
|
||||
<image v-if="isCallQuotaOut" class="fc-icon fc-icon--xs detail__bar-vip-icon" :src="iconUrl('crown')" mode="aspectFit" />
|
||||
<text>{{ bottomBarText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -563,12 +615,23 @@ export default {
|
||||
callQuotaUnlimited() {
|
||||
return Number(this.callQuotaTotal) === 0
|
||||
},
|
||||
/**
|
||||
* 今日拨打额度是否已用完(非不限次数且总额度>0,剩余额度<=0)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isCallQuotaOut() {
|
||||
if (this.callQuotaUnlimited) return false
|
||||
return Number(this.callQuotaTotal) > 0 && Number(this.callQuotaLeft) <= 0
|
||||
},
|
||||
/**
|
||||
* 底部主操作栏文案
|
||||
* @returns {string}
|
||||
*/
|
||||
bottomBarText() {
|
||||
if (this.loading && !this.loaded) return '加载中…'
|
||||
if (this.isCallQuotaOut) {
|
||||
return '开通会员立即拨打'
|
||||
}
|
||||
return '拨打电话'
|
||||
},
|
||||
timeText() {
|
||||
@@ -828,10 +891,31 @@ export default {
|
||||
} catch (e) {}
|
||||
},
|
||||
/**
|
||||
* 底部主按钮响应:打开免责声明拨打弹窗
|
||||
* 底部主按钮响应:拨打电话或引导开通会员
|
||||
*/
|
||||
onMainAction() {
|
||||
if (this.loading && !this.loaded) return
|
||||
if (!this.hasPhone) {
|
||||
uni.showToast({ title: '发布者未预留联系电话', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.ensureLogin()) return
|
||||
// 当额度用尽时直接引导开通会员,杜绝无效点击
|
||||
if (this.isCallQuotaOut) {
|
||||
uni.showModal({
|
||||
title: '今日拨打次数已用完',
|
||||
content: this.isMember
|
||||
? '您今日的 VIP 拨打次数已达上限,明日将自动恢复,或可升级更高规格会员套餐'
|
||||
: '您今日的免费拨打次数已用完,开通 VIP 会员尊享无限次拨打与每日免费浏览特权',
|
||||
confirmText: '去开通会员',
|
||||
cancelText: '知道了',
|
||||
confirmColor: '#00E070',
|
||||
success: (r) => {
|
||||
if (r.confirm) uni.navigateTo({ url: '/pages/member/index' })
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
this.openCallModal()
|
||||
},
|
||||
/**
|
||||
@@ -936,8 +1020,9 @@ export default {
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = (e && e.message) || '拨打失败,请稍后重试'
|
||||
// 当日拨打次数超限(后端 403):引导开通 VIP 会员提升每日拨打次数
|
||||
if (/上限/.test(msg)) {
|
||||
// 当日拨打次数超限(后端 403):同步本地额度并引导开通 VIP 会员提升每日拨打次数
|
||||
if (/上限|额度|次数/.test(msg)) {
|
||||
this.callQuotaLeft = 0
|
||||
this.showCallModal = false
|
||||
uni.showModal({
|
||||
title: '今日拨打次数已用完',
|
||||
@@ -2497,12 +2582,35 @@ export default {
|
||||
.detail__bar-space {
|
||||
height: calc(150rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.detail__bar {
|
||||
.detail__bar-wrapper {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 99;
|
||||
}
|
||||
.detail__bar-quota-tip {
|
||||
background: linear-gradient(90deg, #fffbeb, #fef3c7);
|
||||
border-top: 1rpx solid #fde68a;
|
||||
padding: 12rpx 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.detail__bar-quota-txt {
|
||||
flex: 1;
|
||||
font-size: 22rpx;
|
||||
color: #92400e;
|
||||
font-weight: 500;
|
||||
}
|
||||
.detail__bar-quota-go {
|
||||
font-size: 22rpx;
|
||||
color: #d97706;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.detail__bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14rpx 24rpx calc(14rpx + env(safe-area-inset-bottom));
|
||||
@@ -2550,7 +2658,123 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10rpx;
|
||||
box-shadow: 0 6rpx 20rpx rgba(0, 224, 112, 0.35);
|
||||
|
||||
&--vip {
|
||||
background: linear-gradient(135deg, #00e070, #00d26a 45%, #f5a623) !important;
|
||||
color: #04301e !important;
|
||||
box-shadow: 0 8rpx 24rpx rgba(245, 166, 35, 0.32) !important;
|
||||
}
|
||||
}
|
||||
.detail__bar-vip-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
/* 详情预览页:同城 VIP 会员特权卡片 */
|
||||
.detail__vip-card {
|
||||
margin: 20rpx 24rpx 0;
|
||||
padding: 24rpx 26rpx;
|
||||
background: linear-gradient(135deg, #f0fdf4, #ffffff 60%, #fffbf0);
|
||||
border: 2rpx solid #d4f3e3;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 168, 84, 0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&--active {
|
||||
background: linear-gradient(135deg, #f7fee7, #ffffff 60%, #ecfdf5);
|
||||
border-color: #bef264;
|
||||
}
|
||||
}
|
||||
.detail__vip-card-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.detail__vip-card-badge {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 18rpx;
|
||||
background: linear-gradient(135deg, #fef3c7, #fffbeb);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2rpx 8rpx rgba(245, 166, 35, 0.15);
|
||||
|
||||
&--active {
|
||||
background: linear-gradient(135deg, #dcfce7, #f0fdf4);
|
||||
}
|
||||
}
|
||||
.detail__vip-card-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.detail__vip-card-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
.detail__vip-card-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
.detail__vip-card-pill {
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, #f5a623, #d97706);
|
||||
color: #ffffff;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
|
||||
&--active {
|
||||
background: linear-gradient(135deg, #00e070, #00d26a);
|
||||
}
|
||||
}
|
||||
.detail__vip-card-desc {
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: #64748b;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.detail__vip-card-btn {
|
||||
flex-shrink: 0;
|
||||
margin-left: 16rpx;
|
||||
padding: 10rpx 22rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #ffffff;
|
||||
border: 1rpx solid #00a854;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 168, 84, 0.08);
|
||||
|
||||
&--active {
|
||||
border-color: #00d26a;
|
||||
}
|
||||
}
|
||||
.detail__vip-card-btn-txt {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #00a854;
|
||||
}
|
||||
.detail__vip-card-btn-arrow {
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #00a854;
|
||||
}
|
||||
.detail__vip-card:active {
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
/* 通用弹层遮罩(评价 / 回复 / 海报等底部弹层共用) */
|
||||
|
||||
@@ -152,9 +152,13 @@
|
||||
<text>地图导航</text>
|
||||
</view>
|
||||
|
||||
<view class="shop-detail__btn-call" @click="openCallModal">
|
||||
<image class="fc-icon fc-icon--xs" :src="iconUrl('phone')" mode="aspectFit" />
|
||||
<text>联系店长</text>
|
||||
<view
|
||||
class="shop-detail__btn-call"
|
||||
:class="{ 'shop-detail__btn-call--vip': isCallQuotaOut }"
|
||||
@click="openCallModal"
|
||||
>
|
||||
<image class="fc-icon fc-icon--xs" :src="iconUrl(isCallQuotaOut ? 'crown' : 'phone')" mode="aspectFit" />
|
||||
<text>{{ isCallQuotaOut ? '开通会员拨打' : '联系店长' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="fc-safe-bottom"></view>
|
||||
@@ -252,6 +256,14 @@ export default {
|
||||
callQuotaUnlimited() {
|
||||
return Number(this.callQuotaTotal) === 0
|
||||
},
|
||||
/**
|
||||
* 今日拨打额度是否已用完
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isCallQuotaOut() {
|
||||
if (this.callQuotaUnlimited) return false
|
||||
return Number(this.callQuotaTotal) > 0 && Number(this.callQuotaLeft) <= 0
|
||||
},
|
||||
/**
|
||||
* 免责弹窗拨打展示文案(联系人姓氏 + 脱敏手机号)
|
||||
* @returns {string}
|
||||
@@ -477,6 +489,22 @@ export default {
|
||||
return
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// 拨打次数已用完直接引导开通会员
|
||||
if (this.isCallQuotaOut) {
|
||||
uni.showModal({
|
||||
title: '今日拨打次数已用完',
|
||||
content: '您今日的免费拨打次数已用完,开通 VIP 会员尊享每日更多拨打特权',
|
||||
confirmText: '去开通会员',
|
||||
cancelText: '知道了',
|
||||
confirmColor: '#00E070',
|
||||
success: (r) => {
|
||||
if (r.confirm) uni.navigateTo({ url: '/pages/member/index' })
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.showCallModal = true
|
||||
},
|
||||
/**
|
||||
@@ -499,7 +527,8 @@ export default {
|
||||
} catch (e) {
|
||||
const msg = (e && e.message) || '拨打失败,请稍后重试'
|
||||
// 当日拨打次数超限(后端 403):引导开通 VIP 会员提升每日拨打次数
|
||||
if (/上限/.test(msg)) {
|
||||
if (/上限|额度|次数/.test(msg)) {
|
||||
this.callQuotaLeft = 0
|
||||
this.showCallModal = false
|
||||
uni.showModal({
|
||||
title: '今日拨打次数已用完',
|
||||
@@ -883,5 +912,11 @@ export default {
|
||||
transform: scale(0.98);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&--vip {
|
||||
background: linear-gradient(135deg, #00e070, #00d26a 45%, #f5a623) !important;
|
||||
color: #04301e !important;
|
||||
box-shadow: 0 8rpx 24rpx rgba(245, 166, 35, 0.32) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,16 +3,6 @@
|
||||
<!-- 统一顶部深绿质感导航栏 -->
|
||||
<fc-navbar title="发布信息" theme="gradient" :show-back="true" />
|
||||
|
||||
<!-- 非会员开通引导(会员不显示任何引导) -->
|
||||
<view v-if="!isMember" class="create__member-banner" @click="goMember">
|
||||
<image class="create__member-icon" :src="iconUrl('crown')" mode="aspectFit" />
|
||||
<view class="create__member-info">
|
||||
<text class="create__member-title">开通会员</text>
|
||||
<text class="create__member-desc">每日免费发布、免费浏览、更多拨打次数</text>
|
||||
</view>
|
||||
<text class="create__member-go">去开通 ›</text>
|
||||
</view>
|
||||
|
||||
<!-- 1. 分类选择(一级大类与二级品类拆分为两行独立输入) -->
|
||||
<view class="fc-card create__section">
|
||||
<view class="create__cell" @click="openCatStep(1)">
|
||||
@@ -33,12 +23,12 @@
|
||||
<view class="create__cell create__cell--sub" :class="{ 'create__cell--disabled': !currentCat }" @click="openCatStep(2)">
|
||||
<view class="create__cell-left">
|
||||
<text class="create__label">产品品类</text>
|
||||
<text class="create__required">*</text>
|
||||
<text v-if="hasSubCategories" class="create__required">*</text>
|
||||
</view>
|
||||
<view class="create__cell-right">
|
||||
<view v-if="currentSubCat" class="create__selected-cat">
|
||||
<image class="create__selected-cat-icon" :src="catIcon(currentSubCat)" mode="aspectFit" />
|
||||
<text class="create__selected-cat-name">{{ currentSubCat.name }}</text>
|
||||
<view v-if="selectedSubCatDisplay" class="create__selected-cat">
|
||||
<image class="create__selected-cat-icon" :src="selectedSubCatDisplay.icon" mode="aspectFit" />
|
||||
<text class="create__selected-cat-name">{{ selectedSubCatDisplay.name }}</text>
|
||||
</view>
|
||||
<text v-else class="create__cell-ph">{{ currentCat ? '请选择品类' : '请先选择大类' }}</text>
|
||||
<text class="create__arrow">›</text>
|
||||
@@ -313,6 +303,23 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 7. 发布权益与会员特权(置于表单底部发布前,避免干扰顶部核心填写) -->
|
||||
<view class="create__member-banner" :class="{ 'create__member-banner--vip': isMember }" @click="goMember">
|
||||
<image class="create__member-icon" :src="iconUrl('crown')" mode="aspectFit" />
|
||||
<view class="create__member-info">
|
||||
<view class="create__member-title-row">
|
||||
<text class="create__member-title">{{ isMember ? 'VIP 会员发帖特权生效中' : '开通同城 VIP 会员' }}</text>
|
||||
<text v-if="isMember" class="create__member-tag">已开通</text>
|
||||
</view>
|
||||
<text class="create__member-desc">
|
||||
{{ isMember ? '尊享每日海量免费发布、专属身份认证与极速展示' : '尊享每日免费发布、信息置顶折扣、更多拨打次数' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="create__member-btn-box">
|
||||
<text class="create__member-go">{{ isMember ? '权益中心 ›' : '去开通 ›' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部固定主控操作栏 -->
|
||||
<view class="create__bar">
|
||||
<view class="create__bar-draft" @click="saveDraftManual">
|
||||
@@ -446,7 +453,7 @@
|
||||
<image class="cat-drawer__cover" :src="catIcon(currentCat || {})" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="cat-drawer__caption">
|
||||
<text class="cat-drawer__name">全部{{ currentCat ? currentCat.name : '' }}</text>
|
||||
<text class="cat-drawer__name">全部</text>
|
||||
</view>
|
||||
<view class="cat-drawer__check">✓</view>
|
||||
</view>
|
||||
@@ -520,6 +527,7 @@ export default {
|
||||
catDrawerAnim: false,
|
||||
// 分类两步选择:1 = 选一级大类,2 = 选二级品类
|
||||
catDrawerStep: 1,
|
||||
selectedAllSubCat: false, // 是否显式选中全部二级品类
|
||||
customTagInput: '',
|
||||
outputRangeOptions: [
|
||||
'1000斤以下',
|
||||
@@ -661,6 +669,35 @@ export default {
|
||||
const children = (this.currentCat && this.currentCat.children) || []
|
||||
return children.find((s) => Number(s.id) === Number(this.form.subCategoryId)) || null
|
||||
},
|
||||
/**
|
||||
* 当前一级大类是否包含二级子品类
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasSubCategories() {
|
||||
return !!(this.currentCat && Array.isArray(this.currentCat.children) && this.currentCat.children.length > 0)
|
||||
},
|
||||
/**
|
||||
* 页面回显的产品品类信息(支持具体二级分类对象或全部)
|
||||
* @returns {{ name: string, icon: string } | null}
|
||||
*/
|
||||
selectedSubCatDisplay() {
|
||||
if (!this.currentCat) return null
|
||||
// 1. 若选了具体二级分类
|
||||
if (this.currentSubCat) {
|
||||
return {
|
||||
name: this.currentSubCat.name,
|
||||
icon: this.catIcon(this.currentSubCat),
|
||||
}
|
||||
}
|
||||
// 2. 若该一级大类本身没有二级分类,或者显式选择了全部
|
||||
if (!this.hasSubCategories || this.selectedAllSubCat) {
|
||||
return {
|
||||
name: '全部',
|
||||
icon: this.catIcon(this.currentCat),
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
/**
|
||||
* 当前选中分类是否为农产品类(kind=farm):
|
||||
* 决定「种植面积/采摘时间/预计量产」的显隐与必填校验;历史数据无 kind 时按农产品处理
|
||||
@@ -1153,26 +1190,35 @@ export default {
|
||||
}, 200)
|
||||
},
|
||||
/**
|
||||
* 选择一级大类(蔬菜 / 水果),选中即回填并关闭
|
||||
* 选择一级大类(蔬菜 / 水果等),选中即回填并关闭
|
||||
* @param {Object} c 一级大类对象
|
||||
*/
|
||||
onSelectDrawerParentCat(c) {
|
||||
if (!c) return
|
||||
this.form.categoryId = Number(c.id)
|
||||
this.form.subCategoryId = 0
|
||||
const children = (c.children && Array.isArray(c.children)) ? c.children : []
|
||||
if (!children.length) {
|
||||
// 若该一级分类下无二级子品类,自动归为全部
|
||||
this.form.subCategoryId = 0
|
||||
this.selectedAllSubCat = true
|
||||
} else {
|
||||
this.form.subCategoryId = 0
|
||||
this.selectedAllSubCat = false
|
||||
}
|
||||
this.triggerAutoSave()
|
||||
this.closeCatDrawer()
|
||||
},
|
||||
/**
|
||||
* 第二步:仅按一级大类发布(不选具体二级品类)
|
||||
* 第二步:仅按一级大类发布(选择全部)
|
||||
*/
|
||||
onSelectDrawerParentOnly() {
|
||||
this.form.subCategoryId = 0
|
||||
this.selectedAllSubCat = true
|
||||
this.triggerAutoSave()
|
||||
this.closeCatDrawer()
|
||||
},
|
||||
/**
|
||||
* 第二步:选中二级分类(如:花菜、白菜、紫甘蓝、青甘蓝、青菜等)并完成选择
|
||||
* 第二步:选中二级分类并完成选择
|
||||
* @param {Object} sub 二级分类对象
|
||||
*/
|
||||
onSelectDrawerSubCat(sub) {
|
||||
@@ -1181,6 +1227,7 @@ export default {
|
||||
this.form.categoryId = Number(sub.pid)
|
||||
}
|
||||
this.form.subCategoryId = Number(sub.id)
|
||||
this.selectedAllSubCat = false
|
||||
this.triggerAutoSave()
|
||||
this.closeCatDrawer()
|
||||
},
|
||||
@@ -1415,8 +1462,8 @@ export default {
|
||||
* @returns {string} 错误信息,为空则验证通过
|
||||
*/
|
||||
validate() {
|
||||
if (!this.form.categoryId) return '请选择所属分类'
|
||||
if (!this.form.subCategoryId && this.drawerSubCategories.length) return '请选择二级品类'
|
||||
if (!this.form.categoryId) return '请选择所属大类'
|
||||
if (this.hasSubCategories && !this.form.subCategoryId) return '请选择产品品类'
|
||||
if (!this.form.title || !this.form.title.trim()) return '请填写标题'
|
||||
if (!this.form.content || !this.form.content.trim()) return '请填写详细内容'
|
||||
// 校验顺序与表单视觉顺序一致(价格 → 产地规格 → 区域 → 手机号),报错时用户可自上而下定位
|
||||
@@ -1562,15 +1609,21 @@ export default {
|
||||
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* 非会员开通引导条(仅未开通会员可见) */
|
||||
/* 会员开通/权益引导条(置于表单底部,自然顺畅) */
|
||||
.create__member-banner {
|
||||
margin: 20rpx 28rpx 0;
|
||||
padding: 22rpx 26rpx;
|
||||
margin: 24rpx 28rpx 0;
|
||||
padding: 24rpx 26rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #fff8e8, #fffdf5);
|
||||
border: 1rpx solid #ffe3a6;
|
||||
border-radius: 22rpx;
|
||||
background: linear-gradient(135deg, #f0fdf4, #ffffff 60%, #fffbf0);
|
||||
border: 2rpx solid #d4f3e3;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 168, 84, 0.05);
|
||||
|
||||
&--vip {
|
||||
background: linear-gradient(135deg, #f7fee7, #ffffff 60%, #ecfdf5);
|
||||
border-color: #bef264;
|
||||
}
|
||||
}
|
||||
.create__member-icon {
|
||||
width: 56rpx;
|
||||
@@ -1584,28 +1637,48 @@ export default {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.create__member-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.create__member-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #8a5a00;
|
||||
color: #1e293b;
|
||||
}
|
||||
.create__member-tag {
|
||||
margin-left: 12rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, #00e070, #00d26a);
|
||||
color: #ffffff;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.create__member-desc {
|
||||
margin-top: 4rpx;
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: #b08a3e;
|
||||
color: #64748b;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.create__member-go {
|
||||
.create__member-btn-box {
|
||||
flex-shrink: 0;
|
||||
margin-left: 16rpx;
|
||||
padding: 8rpx 22rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #ffffff;
|
||||
border: 1rpx solid #00a854;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 168, 84, 0.08);
|
||||
}
|
||||
.create__member-go {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #f5a623;
|
||||
color: #00a854;
|
||||
}
|
||||
.create__member-banner:active {
|
||||
opacity: 0.85;
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
/* 表单卡片区域 */
|
||||
|
||||
二进制
uniapp/static/categories/cat-job.png
普通文件
|
之后 宽度: | 高度: | 大小: 5.0 KiB |
|
之后 宽度: | 高度: | 大小: 6.5 KiB |
二进制
uniapp/static/icons/cat-poultry.png
普通文件
|
之后 宽度: | 高度: | 大小: 6.5 KiB |