feat: 完成二级分类真实实拍图替换与功能优化

本次提交内容包括:
1. 替换13个二级分类为真实商业实拍图,同步更新本地与线上数据库
2. 新增会员拨打额度耗尽引导开通会员功能
3. 优化发布页分类选择逻辑,增加会员权益展示
4. 修复分类名称显示问题,完善相关交互逻辑
5. 新增图片同步工具脚本,确保线上线下数据一致性
这个提交包含在:
2026-10-28 14:13:32 +08:00
父节点 111054d984
当前提交 f13fb6cdab
共有 47 个文件被更改,包括 3203 次插入 和 225 次删除

查看文件

@@ -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">&times;</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">&times;</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";

查看文件

@@ -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";

查看文件

@@ -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()

查看文件

@@ -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";
}

查看文件

@@ -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"
}
]

查看文件

@@ -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"
}
]

查看文件

@@ -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()

查看文件

@@ -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()

查看文件

@@ -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