feat: 新增电话拨打功能及相关配置,完善农产品分类与地区数据

- 新增商家/信息详情页电话拨打接口,支持每日额度限制与拨打日志记录
- 新增平台免责声明内容及协议接口,完善用户服务协议相关配置
- 新增农产品分类图标与上市角标展示,优化分类管理界面样式
- 新增全国行政区划工具类与城市选择组件,重构地区数据结构
- 新增SSH自检与部署脚本,完善项目部署流程
这个提交包含在:
2026-10-27 17:05:00 +08:00
父节点 b9fdb7cd77
当前提交 bb6418f51b
共有 86 个文件被更改,包括 5354 次插入 和 2061 次删除

查看文件

@@ -1,19 +1,26 @@
define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], function ($, undefined, Backend, Table, Form, Format) {
/**
* 解析分类图标地址
* 解析分类图标/实拍图地址
* @param {string} iconKey 标识或路径
* @param {number} id 分类ID
* @param {string} image 实拍图路径(可选)
* @returns {string} 完整的静态图片URL
*/
function resolveIconUrl(iconKey, id) {
function resolveIconUrl(iconKey, id, image) {
if (image && /^(https?:\/\/|\/uploads\/|\/static\/)/i.test(image)) {
return image;
}
if (!iconKey) {
iconKey = 'cat-job';
iconKey = 'cat-veg';
}
if (/^(https?:\/\/|\/uploads\/|\/static\/)/i.test(iconKey)) {
return iconKey;
}
var file = (iconKey.indexOf('.') !== -1) ? iconKey : iconKey + '.png';
if (iconKey.indexOf('sub-') === 0 || iconKey === 'cat-veg' || iconKey === 'cat-fruit') {
return '/static/categories/' + file + '?v=2';
}
return '/static/icons/' + file + '?v=2';
}
@@ -56,23 +63,26 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
formatter: Format.serial
},
// 1. 图标预览(微质感微卡片)
// 1. 实拍图预览(大图圆角卡,object-fit:cover 保证「看图识物」效果)
{
field: 'icon_preview',
title: __('Icon'),
field: 'image',
title: '实拍图',
operate: false,
align: 'center',
width: '80px',
width: '90px',
formatter: function (value, row, index) {
var url = resolveIconUrl(row.icon_key, row.id);
var url = resolveIconUrl(row.icon_key, row.id, row.image);
var color = row.color || '#00E070';
return '<div style="display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:10px;background:' + color + '18;border:1px solid ' + color + '40;padding:2px;">' +
'<img src="' + url + '" style="width:28px;height:28px;object-fit:contain;" onerror="this.src=\'/static/icons/cat-job.png\'" />' +
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>' +
'<div class="fc-img-zoom"><img src="' + url + '" onerror="this.src=\'/static/categories/cat-veg.png\'" /></div>' +
'</div>';
}
},
// 2. 分类名称(留足宽度,带主题色小圆点)
// 2. 分类名称(含层级缩进,带主题色小圆点)
{
field: 'name',
title: __('Name'),
@@ -81,21 +91,42 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
width: '200px',
formatter: function (value, row, index) {
var color = row.color || '#00E070';
var dot = '<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' + color + ';margin-right:10px;vertical-align:middle;box-shadow:0 0 6px ' + color + '99;"></span>';
return '<div style="display:flex;align-items:center;">' + dot + '<span style="font-weight:600;font-size:14px;color:#2c3e50;">' + value + '</span></div>';
var isSub = Number(row.pid || 0) > 0;
var dot = '<span style="flex:0 0 auto;display:inline-block;width:10px;height:10px;border-radius:50%;background:' + color + ';margin-right:10px;vertical-align:middle;box-shadow:0 0 6px ' + color + '99;"></span>';
var nameStyle = isSub
? 'font-weight:500;font-size:13.5px;color:#475569;'
: 'font-weight:700;font-size:14px;color:#1e293b;';
return '<div class="' + (isSub ? 'fc-tree-sub' : '') + '" style="display:flex;align-items:center;">' + dot + '<span style="' + nameStyle + '">' + value + '</span></div>';
}
},
// 3. 主题色
// 2.5 所属大类
{
field: 'color',
title: __('Color'),
operate: 'LIKE',
field: 'parent_name',
title: '上级分类',
operate: false,
align: 'center',
width: '130px',
width: '120px',
formatter: function (value, row, index) {
var color = value || '#00E070';
return '<span class="badge" style="background:' + color + ';color:#ffffff;font-size:12px;font-family:monospace;padding:4px 10px;border-radius:6px;text-shadow:0 1px 2px rgba(0,0,0,0.3);">' + color + '</span>';
if (!row.pid || Number(row.pid) === 0) {
return '<span class="label" style="background:#ecfdf5;color:#059669;border:1px solid #a7f3d0;border-radius:6px;padding:3px 8px;font-weight:600;">一级大类</span>';
}
return '<span class="label" style="background:#f1f5f9;color:#475569;border-radius:6px;padding:3px 8px;">' + (value || ('#' + row.pid)) + '</span>';
}
},
// 2.6 分类类型(决定发布页是否显示产地规格字段)
{
field: 'kind',
title: '类型',
operate: false,
align: 'center',
width: '100px',
formatter: function (value, row, index) {
if (value === 'farm') {
return '<span class="label" style="background:#ecfdf5;color:#059669;border:1px solid #a7f3d0;border-radius:6px;padding:3px 8px;font-weight:600;">农产品</span>';
}
return '<span class="label" style="background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe;border-radius:6px;padding:3px 8px;">通用信息</span>';
}
},
@@ -188,6 +219,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
var $iconInput = $('#c-icon_key');
var $nameInput = $('#c-name');
var $colorInput = $('#c-color');
var $colorNative = $('#c-color-native');
var $colorAddon = $('#color-preview-addon');
var $previewBadge = $('#preview-badge');
var $previewIcon = $('#preview-icon');
@@ -198,7 +230,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
// 刷新界面预览元素(纯视觉图形,无死板业务文字)
function updateLivePreview() {
var name = $nameInput.val() || '未命名分类';
var key = $iconInput.val() || 'cat-job';
var key = $iconInput.val() || 'cat-veg';
var color = $colorInput.val() || '#00E070';
var iconUrl = resolveIconUrl(key);
@@ -213,6 +245,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
});
}
if ($colorAddon.length) $colorAddon.css('background', color);
if ($colorNative.length) $colorNative.val(color);
}
// 确保初始状态完全隐藏
@@ -263,6 +296,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
updateLivePreview();
});
// 5.1 原生取色器 → 十六进制输入框
$colorNative.on('input change', function () {
$colorInput.val($(this).val()).trigger('change');
});
$(document).on('click', '.fc-color-presets .fc-color-dot', function () {
var color = $(this).data('color');
$colorInput.val(color).trigger('change');
@@ -270,7 +308,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
});
// 初始化回显
var initKey = $iconInput.val() || 'cat-job';
var initKey = $iconInput.val() || 'cat-veg';
$('.fc-icon-chip[data-key="' + initKey + '"]').addClass('active');
updateLivePreview();
}

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 5.9 KiB

二进制
public/static/categories/cat-veg.png 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 6.6 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 3.4 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 4.0 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 6.7 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 4.1 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 4.3 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 4.5 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 3.4 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 4.0 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 4.0 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 4.4 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 3.9 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 4.4 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 4.9 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 3.4 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 4.7 KiB

二进制
public/static/icons/cat-fruit.png 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 5.9 KiB

二进制
public/static/icons/cat-veg.png 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 6.6 KiB