feat: 优化腾讯地图地址选点插件,完善订单管理与分销服务

这个提交包含在:
2026-10-26 10:01:40 +08:00
父节点 d871e73d23
当前提交 7b58952cd4
共有 19 个文件被更改,包括 1045 次插入 和 201 次删除

查看文件

@@ -11,8 +11,19 @@ require([], function () {
var lat = lat_id ? $("#" + lat_id).val() : '';
var lng = lng_id ? $("#" + lng_id).val() : '';
var zoom = zoom_id ? $("#" + zoom_id).val() : '';
var url = "/addons/address/index/select";
url += (lat && lng) ? '?lat=' + lat + '&lng=' + lng + (input_id ? "&address=" + $("#" + input_id).val() : "") + (zoom ? "&zoom=" + zoom : "") : '';
var address = input_id ? $("#" + input_id).val() : ($(that).data("address") || '');
var query = [];
if (lat && lng) {
query.push('lat=' + encodeURIComponent(lat));
query.push('lng=' + encodeURIComponent(lng));
}
if (address) {
query.push('address=' + encodeURIComponent(address));
}
if (zoom) {
query.push('zoom=' + encodeURIComponent(zoom));
}
var url = "/addons/address/index/select" + (query.length ? '?' + query.join('&') : '');
Fast.api.open(url, '位置选择', {
callback: function (res) {
input_id && $("#" + input_id).val(res.address).trigger("change");

查看文件

@@ -9,6 +9,7 @@
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
}
#container {
@@ -19,72 +20,173 @@
bottom: 0;
}
.confirm {
position: absolute;
bottom: 30px;
right: 4%;
z-index: 99;
height: 50px;
width: 50px;
line-height: 50px;
font-size: 15px;
text-align: center;
background-color: white;
background: #1ABC9C;
color: white;
border: none;
cursor: pointer;
border-radius: 50%;
}
.search {
position: absolute;
width: 400px;
top: 0;
width: 440px;
max-width: 92%;
top: 12px;
left: 50%;
padding: 5px;
margin-left: -200px;
transform: translateX(-50%);
z-index: 100;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.14);
padding: 4px;
}
.search .form-control {
border: none;
box-shadow: none;
height: 36px;
font-size: 13px;
}
.search .btn-success {
background: #00d26a;
border-color: #00d26a;
height: 36px;
border-radius: 6px !important;
padding: 0 14px;
}
.search .btn-success:hover {
background: #00a854;
border-color: #00a854;
}
.map-tip-banner {
position: absolute;
top: 64px;
left: 50%;
transform: translateX(-50%);
z-index: 90;
background: rgba(15, 23, 42, 0.75);
color: #fff;
font-size: 12px;
padding: 5px 14px;
border-radius: 20px;
pointer-events: none;
white-space: nowrap;
}
.selected-panel {
position: absolute;
left: 16px;
right: 16px;
bottom: 16px;
z-index: 99;
background: #ffffff;
border-radius: 10px;
box-shadow: 0 6px 24px rgba(15, 23, 42, 0.16);
padding: 12px 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
}
.selected-panel__info {
flex: 1;
min-width: 0;
}
.selected-panel__addr {
font-size: 14px;
font-weight: 600;
color: #1e293b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 3px;
}
.selected-panel__coord {
font-size: 12px;
color: #64748b;
}
.confirm {
flex-shrink: 0;
height: 38px;
padding: 0 22px;
line-height: 38px;
font-size: 14px;
font-weight: 600;
text-align: center;
background: linear-gradient(135deg, #00e070, #00a854);
color: #ffffff;
border: none;
cursor: pointer;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 210, 106, 0.28);
transition: filter 0.15s ease;
}
.confirm:hover {
filter: brightness(0.96);
}
.autocomplete-search {
text-align: left;
cursor: default;
background: #fff;
border-radius: 2px;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
-moz-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
background-clip: padding-box;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.16);
position: absolute;
display: none;
z-index: 1036;
max-height: 254px;
max-height: 280px;
overflow: hidden;
overflow-y: auto;
box-sizing: border-box;
margin-top: 4px;
}
.autocomplete-search .autocomplete-suggestion {
padding: 5px;
padding: 8px 12px;
font-size: 13px;
color: #1e293b;
border-bottom: 1px solid #f1f5f9;
cursor: pointer;
}
.autocomplete-search .autocomplete-suggestion:hover {
background: #f0f0f0;
.autocomplete-search .autocomplete-suggestion:last-child {
border-bottom: none;
}
.autocomplete-search .autocomplete-suggestion:hover,
.autocomplete-search .autocomplete-suggestion.selected {
background: #f2fef7;
color: #00a854;
}
.autocomplete-suggestion__sub {
display: block;
font-size: 11px;
color: #94a3b8;
margin-top: 2px;
}
</style>
</head>
<body>
<div class="search">
<div class="input-group">
<input type="text" id="place" name="q" class="form-control" placeholder="输入地点"/>
<input type="text" id="place" name="q" class="form-control" autocomplete="off" placeholder="搜索全国城市、区县、街道或地标(回车搜索)"/>
<span class="input-group-btn">
<button type="button" name="search" id="search-btn" class="btn btn-success">
<i class="fa fa-search"></i>
<i class="fa fa-search"></i> 搜索
</button>
</span>
</div>
</div>
<div class="confirm">确定</div>
<div class="map-tip-banner"><i class="fa fa-hand-pointer-o"></i> 直接点击地图任意位置或拖动红标即可选点</div>
<div class="selected-panel">
<div class="selected-panel__info">
<div class="selected-panel__addr" id="selected-addr">点击地图选择位置...</div>
<div class="selected-panel__coord" id="selected-coord">坐标:--</div>
</div>
<button type="button" class="confirm"><i class="fa fa-check"></i> 确定选点</button>
</div>
<div id="container"></div>
<script charset="utf-8" src="//map.qq.com/api/js?v=2.exp&libraries=place&key={$config.tencentkey|default=''}"></script>
@@ -94,72 +196,274 @@
<script type="text/javascript">
$(function () {
var map, marker, geocoder, infoWin, searchService, keyword, address, fromtype, totype;
address = "{$address|htmlentities}";
var map, marker, geocoder, infoWin, xhr;
var initialAddress = "{$address|htmlentities}";
var hasUrlCoord = location.search.indexOf('lat=') !== -1 && location.search.indexOf('lng=') !== -1;
var lng = Number("{$lng}");
var lat = Number("{$lat}");
fromtype = "GCJ02";
totype = "{$config.coordtype|default='DEFAULT'}"
var fromtype = "GCJ02";
var totype = "{$config.coordtype|default='DEFAULT'}";
totype = totype === 'DEFAULT' ? "GCJ02" : totype;
if (lng && lat && fromtype !== totype) {
var result = gcoord.transform([lng, lat], gcoord[totype], gcoord[fromtype]);
lng = result[0] || lng;
lat = result[1] || lat;
var converted = gcoord.transform([lng, lat], gcoord[totype], gcoord[fromtype]);
lng = converted[0] || lng;
lat = converted[1] || lat;
}
// 当前选点状态(确保无论逆地址解析是否成功,随时可点击确定返回所选点位)
var selectedState = {
lat: lat,
lng: lng,
address: initialAddress || '',
province: '',
city: '',
district: '',
rawParts: []
};
/**
* 提取 LatLng 对象的数值坐标
* @param {Object} latLng 腾讯地图 LatLng 或普通 {lat, lng} 对象
* @return {{lat: number, lng: number}}
*/
var extractLatLng = function (latLng) {
var parsedLat = typeof latLng.getLat === 'function' ? latLng.getLat() : Number(latLng.lat);
var parsedLng = typeof latLng.getLng === 'function' ? latLng.getLng() : Number(latLng.lng);
return {
lat: parsedLat,
lng: parsedLng
};
};
/**
* 更新底部状态栏与地图信息窗体展示
* @param {{lat: number, lng: number}} pos 坐标
* @param {string} title 地址文本
*/
var updateSelectedDisplay = function (pos, title) {
var addrText = title || selectedState.address || ('已选坐标 (' + pos.lat.toFixed(5) + ', ' + pos.lng.toFixed(5) + ')');
$('#selected-addr').text(addrText);
$('#selected-coord').text('纬度:' + pos.lat.toFixed(6) + ' ,经度:' + pos.lng.toFixed(6));
showInfoWin(pos, addrText);
};
/**
* 解析 Nominatim 返回的中文地址层级
* @param {Object} ret Nominatim 响应对象
* @return {{address: string, province: string, city: string, district: string, rawParts: Array}}
*/
var parseNominatimResult = function (ret) {
var displayName = (ret && ret.display_name) ? String(ret.display_name) : '';
var rawParts = displayName.split(',').map(function (s) {
return $.trim(s);
}).filter(function (s) {
return s !== '' && s !== '中国' && !/^\d{5,6}$/.test(s);
}).reverse();
var addrObj = (ret && ret.address) ? ret.address : {};
var province = addrObj.state || rawParts[0] || '';
var city = '';
var district = '';
// 从反转后的层级中提取省、市、区县
for (var i = 0; i < rawParts.length; i++) {
var part = rawParts[i];
if (!province && /(省|自治区|特别行政区)$/.test(part)) {
province = part;
} else if (!city && /(市|自治州|地区|盟)$/.test(part) && part !== province) {
city = part;
} else if (!district && /(区|县|旗|市)$/.test(part) && part !== province && part !== city) {
district = part;
}
}
if (!city && /^(北京市|上海市|天津市|重庆市|北京|上海|天津|重庆)/.test(province)) {
city = province.indexOf('市') === -1 ? (province + '市') : province;
}
if (!district && addrObj.county) {
district = addrObj.county;
}
var fullAddress = rawParts.length ? rawParts.join('') : displayName;
return {
address: fullAddress,
province: province,
city: city,
district: district,
rawParts: rawParts
};
};
/**
* 选中地图上某个坐标点:立即移动图标并异步解析中文地址
* @param {Object} latLng 坐标点
* @param {string} [presetAddress] 预设地址名(如搜索结果标题)
* @param {boolean} [panCenter] 是否移动地图中心
*/
var selectPoint = function (latLng, presetAddress, panCenter) {
var pos = extractLatLng(latLng);
if (!isFinite(pos.lat) || !isFinite(pos.lng)) {
return;
}
selectedState.lat = pos.lat;
selectedState.lng = pos.lng;
if (presetAddress) {
selectedState.address = presetAddress;
}
// 1. 立即放置/移动红标并显示坐标,不等待接口返回
showMarker(pos, presetAddress || '已选位置');
updateSelectedDisplay(pos, presetAddress || '正在解析地点名称...');
if (panCenter && map) {
map.setCenter(new qq.maps.LatLng(pos.lat, pos.lng));
}
// 2. 异步逆地址解析
geocoder.getAddress(pos, presetAddress);
};
/**
* 通过关键词搜索地点并自动定位到第一个匹配结果
* @param {string} keyword 搜索关键词
*/
var searchAndLocate = function (keyword) {
var kw = $.trim(String(keyword || ''));
if (!kw) {
return;
}
var wgs2gcj = function (wgsLng, wgsLat) {
var res = gcoord.transform([Number(wgsLng), Number(wgsLat)], gcoord.WGS84, gcoord.GCJ02);
return {
lng: res[0] || Number(wgsLng),
lat: res[1] || Number(wgsLat)
};
};
$.ajax({
url: "https://nominatim.openstreetmap.org/search",
dataType: "jsonp",
jsonp: "json_callback",
data: {
format: "json",
q: kw,
addressdetails: 1,
limit: 5,
"accept-language": "zh-CN",
countrycodes: "cn"
},
success: function (list) {
if ($.isArray(list) && list.length > 0) {
var first = list[0];
var gcj = wgs2gcj(first.lon, first.lat);
var parsed = parseNominatimResult(first);
selectedState.province = parsed.province;
selectedState.city = parsed.city;
selectedState.district = parsed.district;
selectedState.rawParts = parsed.rawParts;
selectPoint(gcj, parsed.address || first.name || kw, true);
}
}
});
};
/**
* 初始化腾讯地图与交互事件
*/
var init = function () {
var center = new qq.maps.LatLng(lat, lng);
map = new qq.maps.Map(document.getElementById('container'), {
center: center,
zoom: parseInt("{$config.zoom}")
zoom: parseInt("{$config.zoom}") || 12
});
//实例化信息窗口
infoWin = new qq.maps.InfoWindow({
map: map
});
geocoder = {
getAddress: function (latLng) {
/**
* 逆地址解析:优先尝试腾讯接口,未授权时自动降级为 Nominatim
* @param {{lat: number, lng: number}} pos GCJ02坐标
* @param {string} [fallbackTitle] 预设标题
*/
getAddress: function (pos, fallbackTitle) {
var fallbackToNominatim = function () {
var wgs = gcoord.transform([pos.lng, pos.lat], gcoord.GCJ02, gcoord.WGS84);
$.ajax({
url: "https://apis.map.qq.com/ws/geocoder/v1/?location=" + latLng.lat + "," + latLng.lng + "&key={$config.tencentkey|default=''}&output=jsonp",
url: "https://nominatim.openstreetmap.org/reverse",
dataType: "jsonp",
jsonp: "json_callback",
data: {
format: "json",
lat: wgs[1],
lon: wgs[0],
zoom: 18,
addressdetails: 1,
"accept-language": "zh-CN"
},
success: function (ret) {
if (ret && ret.display_name) {
var parsed = parseNominatimResult(ret);
selectedState.address = parsed.address || fallbackTitle || '';
selectedState.province = parsed.province;
selectedState.city = parsed.city;
selectedState.district = parsed.district;
selectedState.rawParts = parsed.rawParts;
updateSelectedDisplay(pos, selectedState.address);
} else {
updateSelectedDisplay(pos, fallbackTitle || ('坐标 (' + pos.lat.toFixed(5) + ', ' + pos.lng.toFixed(5) + ')'));
}
},
error: function () {
updateSelectedDisplay(pos, fallbackTitle || ('坐标 (' + pos.lat.toFixed(5) + ', ' + pos.lng.toFixed(5) + ')'));
}
});
};
$.ajax({
url: "https://apis.map.qq.com/ws/geocoder/v1/?location=" + pos.lat + "," + pos.lng + "&key={$config.tencentkey|default=''}&output=jsonp",
dataType: "jsonp",
type: 'GET',
cache: true,
crossDomain: true,
success: function (ret) {
console.log("getAddress:", ret)
if (ret.status === 0) {
var component = ret.result.address_component;
if (ret && ret.status === 0 && ret.result) {
var component = ret.result.address_component || {};
var resolvedAddr = '';
if (ret.result.formatted_addresses && ret.result.formatted_addresses.recommend) {
var recommend = ret.result.formatted_addresses.recommend;
var standard_address = ret.result.formatted_addresses.standard_address;
var address = component.province !== component.city ? component.province + component.city : component.province;
address = address + (recommend.indexOf(component.district) === 0 ? '' : component.district) + recommend;
resolvedAddr = (component.province !== component.city ? (component.province + component.city) : component.province)
+ (recommend.indexOf(component.district) === 0 ? '' : component.district) + recommend;
} else {
address = ret.result.address;
resolvedAddr = ret.result.address || '';
}
showMarker(ret.result.location, address);
showInfoWin(ret.result.location, address);
selectedState.address = resolvedAddr || fallbackTitle || '';
selectedState.province = component.province || '';
selectedState.city = component.city || '';
selectedState.district = component.district || '';
updateSelectedDisplay(pos, selectedState.address);
} else {
fallbackToNominatim();
}
},
error: function (e) {
console.log(e, 'error')
error: function () {
fallbackToNominatim();
}
});
}
};
//初始化marker
showMarker(center);
if (address) {
showInfoWin(center, address);
} else {
geocoder.getAddress(center);
// 初始选点渲染
selectPoint({lat: lat, lng: lng}, initialAddress, false);
// 若打开时未传自定义坐标但带有区域地址,自动定位到该区域
if (!hasUrlCoord && initialAddress) {
$('#place').val(initialAddress);
searchAndLocate(initialAddress);
}
// 搜索框自动联想(腾讯 Suggestion + Nominatim 双通道自动降级)
var place = $("#place");
place.autoComplete({
minChars: 1,
@@ -167,121 +471,196 @@
menuClass: 'autocomplete-search',
source: function (term, response) {
try {
xhr.abort();
xhr && xhr.abort();
} catch (e) {
}
var searchByNominatim = function () {
xhr = $.ajax({
url: "https://apis.map.qq.com/ws/place/v1/suggestion?keyword=" + term + "&key={$config.tencentkey|default=''}&output=jsonp",
url: "https://nominatim.openstreetmap.org/search",
dataType: "jsonp",
jsonp: "json_callback",
data: {
format: "json",
q: term,
addressdetails: 1,
limit: 8,
"accept-language": "zh-CN",
countrycodes: "cn"
},
success: function (list) {
if (!$.isArray(list) || !list.length) {
$(".autocomplete-suggestions.autocomplete-search").html('');
response([]);
return;
}
var mapped = [];
for (var i = 0; i < list.length; i++) {
var row = list[i];
var gcj = gcoord.transform([Number(row.lon), Number(row.lat)], gcoord.WGS84, gcoord.GCJ02);
var parsed = parseNominatimResult(row);
mapped.push({
title: row.name || parsed.district || parsed.city || term,
address: parsed.address,
province: parsed.province,
city: parsed.city,
district: parsed.district,
rawParts: parsed.rawParts,
location: {
lat: gcj[1] || Number(row.lat),
lng: gcj[0] || Number(row.lon)
}
});
}
response(mapped);
}
});
};
xhr = $.ajax({
url: "https://apis.map.qq.com/ws/place/v1/suggestion?keyword=" + encodeURIComponent(term) + "&key={$config.tencentkey|default=''}&output=jsonp",
dataType: "jsonp",
type: 'GET',
cache: true,
success: function (ret) {
if (ret.status === 0) {
if(ret.data.length === 0){
if (ret && ret.status === 0 && $.isArray(ret.data)) {
if (ret.data.length === 0) {
$(".autocomplete-suggestions.autocomplete-search").html('');
}
response(ret.data);
} else {
console.log(ret);
searchByNominatim();
}
},
error: function () {
searchByNominatim();
}
});
},
renderItem: function (item, search) {
search = search.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
var regexp = new RegExp("(" + search.replace(/[\,|\u3000|\uff0c]/, ' ').split(' ').join('|') + ")", "gi");
return "<div class='autocomplete-suggestion' data-item='" + JSON.stringify(item) + "' data-title='" + item.title + "' data-val='" + item.title + "'>" + item.title.replace(regexp, "<b>$1</b>") + "</div>";
var safeSearch = search.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
var regexp = new RegExp("(" + safeSearch.replace(/[\,|\u3000|\uff0c]/, ' ').split(' ').join('|') + ")", "gi");
var sub = item.address ? ("<span class='autocomplete-suggestion__sub'>" + item.address + "</span>") : "";
var jsonStr = JSON.stringify(item).replace(/'/g, "&#39;");
return "<div class='autocomplete-suggestion' data-item='" + jsonStr + "' data-title='" + item.title + "' data-val='" + item.title + "'>" + item.title.replace(regexp, "<b>$1</b>") + sub + "</div>";
},
onSelect: function (e, term, sel) {
e.preventDefault();
var item = $(sel).data("item");
//调用获取位置方法
geocoder.getAddress(item.location);
var position = new qq.maps.LatLng(item.location.lat, item.location.lng);
map.setCenter(position);
if (item && item.location) {
if (item.province) selectedState.province = item.province;
if (item.city) selectedState.city = item.city;
if (item.district) selectedState.district = item.district;
if (item.rawParts) selectedState.rawParts = item.rawParts;
selectPoint(item.location, item.address || item.title, true);
}
}
});
//地图点击
// 点击地图任意地点立即选点
qq.maps.event.addListener(map, 'click', function (event) {
try {
//调用获取位置方法
geocoder.getAddress(event.latLng);
} catch (e) {
console.log(e);
if (event && event.latLng) {
selectPoint(event.latLng, '', false);
}
}
);
});
};
//显示info窗口
/**
* 显示地图信息气泡
* @param {{lat: number, lng: number}} latLng 坐标
* @param {string} title 显示文本
*/
var showInfoWin = function (latLng, title) {
var position = new qq.maps.LatLng(latLng.lat, latLng.lng);
infoWin.open();
infoWin.setContent(title);
infoWin.setContent('<div style="padding:2px 4px;font-size:12px;color:#1e293b;max-width:260px;">' + (title || '当前选点') + '</div>');
infoWin.setPosition(position);
};
//实例化marker和监听拖拽结束事件
/**
* 放置可拖拽红标并监听拖拽结束选点
* @param {{lat: number, lng: number}} latLng 坐标
* @param {string} title 提示文字
*/
var showMarker = function (latLng, title) {
console.log("showMarker", latLng, title)
var position = new qq.maps.LatLng(latLng.lat, latLng.lng);
marker && marker.setMap(null);
marker = new qq.maps.Marker({
map: map,
position: position,
draggable: true,
title: title || '拖动图标选择位置'
title: title || '点击地图或拖动图标选择位置'
});
//监听拖拽结束
qq.maps.event.addListener(marker, 'dragend', function (event) {
//调用获取位置方法
geocoder.getAddress(event.latLng);
if (event && event.latLng) {
selectPoint(event.latLng, '', false);
}
});
};
/**
* 关闭弹窗并向父窗口回传选点数据
* @param {Object} data 选点数据
*/
var close = function (data) {
var index = parent.Layer.getFrameIndex(window.name);
var callback = parent.$("#layui-layer" + index).data("callback");
//再执行关闭
parent.Layer.close(index);
//再调用回传函数
if (typeof callback === 'function') {
callback.call(undefined, data);
}
};
//点击确定后执行回调赋值
// 点击「确定选点」回传坐标与解析出的省市区信息
$(document).on('click', '.confirm', function () {
var zoom = map.getZoom();
var data = {lat: infoWin.position.lat.toFixed(5), lng: infoWin.position.lng.toFixed(5), zoom: zoom, address: infoWin.content};
var zoom = map ? map.getZoom() : 12;
var outLat = Number(selectedState.lat);
var outLng = Number(selectedState.lng);
if (fromtype !== totype) {
var result = gcoord.transform([data.lng, data.lat], gcoord[fromtype], gcoord[totype]);
data.lng = (result[0] || data.lng).toFixed(5);
data.lat = (result[1] || data.lat).toFixed(5);
console.log(data, result, fromtype, totype);
var result = gcoord.transform([outLng, outLat], gcoord[fromtype], gcoord[totype]);
outLng = Number(result[0] || outLng);
outLat = Number(result[1] || outLat);
}
var data = {
lat: outLat.toFixed(6),
lng: outLng.toFixed(6),
zoom: zoom,
address: selectedState.address || '',
province: selectedState.province || '',
city: selectedState.city || '',
district: selectedState.district || '',
rawParts: selectedState.rawParts || []
};
close(data);
});
//点击搜索按钮
// 点击搜索按钮或回车搜索
$(document).on('click', '#search-btn', function () {
if ($("#place").val() === '')
return;
var first = $(".autocomplete-search > .autocomplete-suggestion:first");
if (!first.length) {
var kw = $.trim($("#place").val());
if (!kw) {
return;
}
var first = $(".autocomplete-search > .autocomplete-suggestion:first");
if (first.length && first.is(':visible')) {
var item = first.data("item");
if (item && item.location) {
if (item.province) selectedState.province = item.province;
if (item.city) selectedState.city = item.city;
if (item.district) selectedState.district = item.district;
if (item.rawParts) selectedState.rawParts = item.rawParts;
selectPoint(item.location, item.address || item.title, true);
$(".autocomplete-search").hide();
return;
}
}
searchAndLocate(kw);
});
//调用获取位置方法
geocoder.getAddress(item.location);
var position = new qq.maps.LatLng(item.location.lat, item.location.lng);
map.setCenter(position);
$('#place').on('keydown', function (e) {
if (e.keyCode === 13) {
e.preventDefault();
$('#search-btn').trigger('click');
}
});
init();

查看文件

@@ -80,40 +80,70 @@ class Order extends Backend
}
/**
* 订单补单
* 订单补单(含权益发放与分销结算)
*
* @return void
*/
public function supplement()
{
$orderNo = $this->request->post('orderNo');
if (trim((string)$orderNo) === '') {
$orderNo = trim((string)$this->request->post('orderNo', ''));
if ($orderNo === '') {
$this->error('订单号不能为空');
}
$order = Db::name('fc_order')->where('order_no', $orderNo)->find();
if (!$order) {
$this->error('订单不存在');
}
if ($order['status'] === 'paid') {
if ((string)$order['status'] === 'paid') {
$this->success('订单已支付');
}
Db::name('fc_order')->where('id', $order['id'])->update([
'status' => 'paid',
'paytime' => time(),
'updatetime' => time(),
]);
Db::name('fc_finance_flow')->insert([
'user_id' => (int)$order['user_id'],
'flow_no' => 'SK' . date('YmdHis') . $order['id'],
'type' => 'income',
'scene' => '订单补单',
'amount' => $order['amount'],
'balance' => 0,
'related_id' => $order['id'],
'remark' => '订单补单',
'createtime' => time(),
]);
$this->success('补单成功');
if ((string)$order['status'] !== 'pending') {
$this->error('订单状态不支持补单(当前:' . $order['status'] . ')');
}
$now = time();
try {
Db::transaction(function () use ($order, $now) {
$locked = Db::name('fc_order')->where('id', (int)$order['id'])->lock(true)->find();
if (!$locked || (string)$locked['status'] !== 'pending') {
return;
}
$affected = Db::name('fc_order')
->where('id', (int)$order['id'])
->where('status', 'pending')
->update([
'status' => 'paid',
'paytime' => $now,
'updatetime' => $now,
]);
if (!$affected) {
return;
}
$locked['status'] = 'paid';
$locked['paytime'] = $now;
Db::name('fc_finance_flow')->insert([
'user_id' => (int)$locked['user_id'],
'flow_no' => 'SK' . date('YmdHis') . $locked['id'],
'type' => 'income',
'scene' => '订单补单',
'amount' => $locked['amount'],
'balance' => 0,
'related_id' => $locked['id'],
'remark' => '订单补单',
'createtime' => $now,
]);
// 统一发放订单权益(会员开通 / 余额充值入账 / 付费查看解锁 / 发帖激活)并触发分销佣金结算
\app\common\library\OrderFulfill::handle($locked);
});
} catch (\Throwable $e) {
\think\Log::record('后台订单补单失败:订单 ' . $orderNo . ',' . $e->getMessage(), 'error');
$this->error('补单失败:' . $e->getMessage());
}
$this->success('补单成功');
}
}

查看文件

@@ -110,7 +110,7 @@
<input type="hidden" id="area-options" value="{$areaOptionsJson|htmlentities}">
<div class="fc-area-actions">
<button type="submit" class="btn btn-primary btn-embossed disabled">保存</button>
<div class="fc-area-actions normal-footer">
<button type="submit" class="btn btn-success btn-embossed fc-area-submit-btn"><i class="fa fa-check"></i> 保存</button>
</div>
</form>

查看文件

@@ -110,7 +110,7 @@
<input type="hidden" id="area-options" data-current-id="{$row.id|htmlentities}" value="{$areaOptionsJson|htmlentities}">
<div class="fc-area-actions">
<button type="submit" class="btn btn-primary btn-embossed disabled">保存</button>
<div class="fc-area-actions normal-footer">
<button type="submit" class="btn btn-success btn-embossed fc-area-submit-btn"><i class="fa fa-check"></i> 保存</button>
</div>
</form>

查看文件

@@ -221,7 +221,7 @@ class Distribution extends V1
/**
* D4 分销海报生成要素
* GET /api/v1/distribution/poster
* 获取用于前端合成或分享专属推广海报的元数据
* 获取用于前端合成或分享专属推广海报的元数据(含官方小程序码 URL,支持传入 postId 生成详情页小程序码)
*
* @return void 输出海报配置与素材信息的JSON
*/
@@ -234,6 +234,16 @@ class Distribution extends V1
$siteName = (string)$this->cfg('app.name', '同城分类信息');
$slogan = (string)$this->cfg('dist.slogan', '同城便民生活 · 随时随地发布与求助');
$code = (string)$ext['invite_code'];
$postId = (int)$this->input('postId', 0);
if ($postId > 0) {
$scene = 'id_' . $postId . '_u_' . $uid;
$page = 'pages/detail/detail';
} else {
$scene = 'ic_' . $code;
$page = 'pages/index/index';
}
$wxacodeUrl = \app\common\library\WxMini::wxacodeUnlimit($scene, $page, 280);
$this->ok([
'inviteCode' => $code,
@@ -241,7 +251,8 @@ class Distribution extends V1
'slogan' => $slogan,
'nickname' => (string)(isset($user['nickname']) && $user['nickname'] !== '' ? $user['nickname'] : '同城合伙人'),
'avatar' => (string)(isset($user['avatar']) ? $user['avatar'] : ''),
'path' => 'pages/index/index?ic=' . $code,
'path' => $postId > 0 ? ('pages/detail/detail?id=' . $postId . '&ic=' . $code) : ('pages/index/index?ic=' . $code),
'wxacodeUrl' => $wxacodeUrl,
]);
}

查看文件

@@ -578,6 +578,7 @@ class V1 extends Api
'memberLevel' => $isMember ? (string)$ext['member_level'] : '',
'memberExpireAt' => $ext['member_expiretime'] ? $this->iso($ext['member_expiretime']) : null,
'balance' => (float)$user['money'],
'inviteCode' => isset($ext['invite_code']) ? (string)$ext['invite_code'] : '',
'realnameStatus' => (int)$ext['realname_status'],
'realname' => isset($ext['realname']) ? (string)$ext['realname'] : '',
'isMerchant' => (bool)$isMerchant,

查看文件

@@ -185,9 +185,12 @@ class DistributionService
'createtime' => $now,
]);
// 发送分销佣金入账站内信通知
// 发送分销佣金入账站内信通知(区分待结算周期与实时结算)
$levelText = ($level === 1) ? '直接下线' : '间接下线';
$noticeContent = '您的 ' . $levelText . ' 产生了一笔消费(¥' . $amount . '),为您带来分销佣金 ¥' . $commissionAmount . ',已计入可提现资产。';
$settleTip = ($settleDays > 0)
? '已计入待结算佣金(预计 T+' . $settleDays . ' 自动结算转入可提现资产)。'
: '已计入可提现资产。';
$noticeContent = '您的 ' . $levelText . ' 产生了一笔消费(¥' . $amount . '),为您带来分销佣金 ¥' . $commissionAmount . ',' . $settleTip;
Db::name('fc_message')->insert([
'user_id' => $parentId,
'type' => 'interact',

查看文件

@@ -207,6 +207,60 @@ class WxMini
return isset($token['access_token']) ? (string)$token['access_token'] : '';
}
/**
* 生成并缓存不限次数微信小程序码(wxacode.getUnlimited)
* 若未配置小程序或线上尚未发布正式版,静默返回空字符串以便前端自动回退至普通二维码
*
* @param string $scene 场景值(最长32个可见字符,如 ic_ABCD12)
* @param string $page 小程序页面路径(不带前导 /,如 pages/index/index)
* @param int $width 二维码宽度(px)
* @return string 成功返回相对 URL(如 /uploads/wxacode/xxx.png),失败返回空字符串
*/
public static function wxacodeUnlimit($scene, $page = 'pages/index/index', $width = 280)
{
$scene = substr(trim((string)$scene), 0, 32);
$page = ltrim(trim((string)$page), '/');
if ($scene === '' || !self::isConfigured()) {
return '';
}
$hash = md5($scene . '|' . $page . '|' . (int)$width);
$relDir = '/uploads/wxacode';
$absDir = ROOT_PATH . 'public' . DS . 'uploads' . DS . 'wxacode';
$relUrl = $relDir . '/' . $hash . '.png';
$absFile = $absDir . DS . $hash . '.png';
if (is_file($absFile) && filesize($absFile) > 200) {
return $relUrl;
}
try {
$response = self::app()->app_code->getUnlimit($scene, [
'page' => $page !== '' ? $page : 'pages/index/index',
'width' => max(280, min(1280, (int)$width)),
'check_path' => false,
]);
if ($response instanceof \EasyWeChat\Kernel\Http\StreamResponse) {
$body = $response->getBody()->getContents();
// 校验 PNG (\x89PNG) 或 JPEG (\xFF\xD8) 魔数,避免把微信返回的 JSON 错误当图片落盘
if (strlen($body) > 200 && (substr($body, 0, 4) === "\x89PNG" || substr($body, 0, 2) === "\xFF\xD8")) {
if (!is_dir($absDir)) {
@mkdir($absDir, 0755, true);
}
@file_put_contents($absFile, $body);
if (is_file($absFile) && filesize($absFile) > 200) {
return $relUrl;
}
}
}
} catch (\Throwable $e) {
// 静默降级:小程序未发布或网络受限时由前端自动回退绘制二维码
}
return '';
}
/* ==================== 内部工具 ==================== */
/** SDK 返回 Collection / array / object 统一转数组 */

查看文件

@@ -258,16 +258,17 @@
height: 40px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #00e070, #04874a);
color: #04301e;
font-weight: 700;
box-shadow: 0 4px 12px rgba(0, 224, 112, 0.2);
background: linear-gradient(135deg, #00d26a, #00a854);
color: #ffffff;
font-weight: 600;
box-shadow: 0 4px 12px rgba(0, 210, 106, 0.24);
transition: filter 0.15s ease, box-shadow 0.15s ease;
}
.fc-area-map-btn:hover,
.fc-area-map-btn:focus {
color: #04301e;
filter: brightness(0.98);
color: #ffffff;
filter: brightness(0.96);
}
.fc-area-actions {
@@ -276,11 +277,28 @@
padding: 4px 0 8px;
}
.fc-area-actions .btn {
min-width: 120px;
.fc-area-actions .btn,
.fc-area-actions .fc-area-submit-btn {
min-width: 128px;
height: 40px;
border: none;
border-radius: 8px;
font-weight: 700;
background: linear-gradient(135deg, #00d26a, #00a854) !important;
color: #ffffff !important;
font-size: 14px;
font-weight: 600;
box-shadow: 0 4px 14px rgba(0, 210, 106, 0.28);
opacity: 1 !important;
transition: filter 0.15s ease, transform 0.15s ease;
}
.fc-area-actions .btn:hover,
.fc-area-actions .btn:focus,
.fc-area-actions .fc-area-submit-btn:hover,
.fc-area-actions .fc-area-submit-btn:focus {
background: linear-gradient(135deg, #00c462, #00964b) !important;
color: #ffffff !important;
filter: brightness(0.98);
}
@media (max-width: 767px) {

查看文件

@@ -12,8 +12,19 @@ define([], function () {
var lat = lat_id ? $("#" + lat_id).val() : '';
var lng = lng_id ? $("#" + lng_id).val() : '';
var zoom = zoom_id ? $("#" + zoom_id).val() : '';
var url = "/addons/address/index/select";
url += (lat && lng) ? '?lat=' + lat + '&lng=' + lng + (input_id ? "&address=" + $("#" + input_id).val() : "") + (zoom ? "&zoom=" + zoom : "") : '';
var address = input_id ? $("#" + input_id).val() : ($(that).data("address") || '');
var query = [];
if (lat && lng) {
query.push('lat=' + encodeURIComponent(lat));
query.push('lng=' + encodeURIComponent(lng));
}
if (address) {
query.push('address=' + encodeURIComponent(address));
}
if (zoom) {
query.push('zoom=' + encodeURIComponent(zoom));
}
var url = "/addons/address/index/select" + (query.length ? '?' + query.join('&') : '');
Fast.api.open(url, '位置选择', {
callback: function (res) {
input_id && $("#" + input_id).val(res.address).trigger("change");

查看文件

@@ -321,7 +321,50 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
};
/**
* 刷新完整区域实时预览文案
* 将 WGS84 坐标转换为 GCJ02 坐标(中国大陆标准)
* @param {number} wgsLng WGS84经度
* @param {number} wgsLat WGS84纬度
* @return {{lat: number, lng: number}}
*/
var wgs84ToGcj02 = function (wgsLng, wgsLat) {
var lon = Number(wgsLng);
var lat = Number(wgsLat);
if (!isFinite(lon) || !isFinite(lat)) {
return {lat: lat, lng: lon};
}
var a = 6378245.0;
var ee = 0.00669342162296594323;
var pi = Math.PI;
var transformLat = function (x, y) {
var ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(y * pi) + 40.0 * Math.sin(y / 3.0 * pi)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(y / 12.0 * pi) + 320 * Math.sin(y * pi / 30.0)) * 2.0 / 3.0;
return ret;
};
var transformLon = function (x, y) {
var ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(x * pi) + 40.0 * Math.sin(x / 3.0 * pi)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(x / 12.0 * pi) + 300.0 * Math.sin(x / 30.0 * pi)) * 2.0 / 3.0;
return ret;
};
var dLat = transformLat(lon - 105.0, lat - 35.0);
var dLon = transformLon(lon - 105.0, lat - 35.0);
var radLat = lat / 180.0 * pi;
var magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
var sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
return {
lat: lat + dLat,
lng: lon + dLon
};
};
/**
* 刷新完整区域实时预览文案并同步地图选点按钮默认地址
*/
var refreshPreview = function () {
var parts = [
@@ -332,6 +375,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
return item !== '';
});
$('#area-full-name').text(parts.length ? parts.join(' / ') : '未选择');
$('.fc-area-map-btn').data('address', parts.join(''));
syncWrapState();
};
@@ -354,7 +398,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
};
/**
* 根据所选省市区自动解析中心经纬度(腾讯地图Place API JSONP)
* 根据所选省市区自动解析中心经纬度(腾讯Place接口 + Nominatim自动降级)
*/
var autoResolveCoordinates = function () {
var p = String($province.val() || '').trim();
@@ -375,13 +419,37 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
clearTimeout(geocodeTimer);
}
geocodeTimer = setTimeout(function () {
var queryAddress = (p === c ? p : (p + c)) + d;
var queryAddress = (p === c ? p : (p + ' ' + c)) + ' ' + d;
var resolveByNominatim = function () {
$.ajax({
url: 'https://nominatim.openstreetmap.org/search',
dataType: 'jsonp',
jsonp: 'json_callback',
data: {
format: 'json',
q: queryAddress,
limit: 1,
'accept-language': 'zh-CN',
countrycodes: 'cn'
},
success: function (list) {
if ($.isArray(list) && list.length > 0 && list[0].lat && list[0].lon) {
var gcj = wgs84ToGcj02(list[0].lon, list[0].lat);
coordAutoFilled = true;
$lat.val(Number(gcj.lat).toFixed(6));
$lng.val(Number(gcj.lng).toFixed(6));
refreshCoordTip(true);
}
}
});
};
var tencentKey = 'WM7BZ-ZAYLL-MZEPZ-MRZBT-MBYJE-ZAFIT';
$.ajax({
url: 'https://apis.map.qq.com/ws/place/v1/suggestion',
dataType: 'jsonp',
data: {
keyword: queryAddress,
keyword: (p === c ? p : (p + c)) + d,
region: c,
key: tencentKey,
output: 'jsonp'
@@ -394,8 +462,13 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
$lat.val(Number(loc.lat).toFixed(6));
$lng.val(Number(loc.lng).toFixed(6));
refreshCoordTip(true);
return;
}
}
resolveByNominatim();
},
error: function () {
resolveByNominatim();
}
});
}, 180);
@@ -847,6 +920,66 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
closeAllDropdowns();
}
});
// 地图选点完成回调:不仅填充坐标,还可根据地图所选位置自动反填省 / 市 / 区县
$('.fc-area-map-btn').data('callback', function (res) {
if (!res) {
return;
}
var fullText = String(res.address || '') + ' ' + ($.isArray(res.rawParts) ? res.rawParts.join(' ') : '');
var matchedProv = resolveProvinceKey(res.province || '');
if (!matchedProv && fullText) {
for (var pIdx = 0; pIdx < tree.provinces.length; pIdx++) {
var pCandidate = tree.provinces[pIdx];
var pShort = pCandidate.replace(/省|市|自治区|特别行政区|壮族|回族|维吾尔/g, '');
if (fullText.indexOf(pCandidate) !== -1 || (pShort.length >= 2 && fullText.indexOf(pShort) !== -1)) {
matchedProv = pCandidate;
break;
}
}
}
var matchedCity = String(res.city || '').trim();
if (matchedProv && tree.cityMap[matchedProv]) {
var cCandidates = tree.cityMap[matchedProv];
if (cCandidates.length === 1) {
matchedCity = cCandidates[0];
} else if (!matchedCity || cCandidates.indexOf(matchedCity) === -1) {
for (var cIdx = 0; cIdx < cCandidates.length; cIdx++) {
var cItem = cCandidates[cIdx];
var cShort = cItem.replace(/市|自治州|地区|盟/g, '');
if (fullText.indexOf(cItem) !== -1 || (cShort.length >= 2 && fullText.indexOf(cShort) !== -1)) {
matchedCity = cItem;
break;
}
}
}
}
var matchedDist = String(res.district || '').trim();
if (matchedProv && matchedCity && tree.districtMap[matchedProv + '|' + matchedCity]) {
var dCandidates = tree.districtMap[matchedProv + '|' + matchedCity];
if (!matchedDist || dCandidates.indexOf(matchedDist) === -1) {
for (var dIdx = 0; dIdx < dCandidates.length; dIdx++) {
if (fullText.indexOf(dCandidates[dIdx]) !== -1) {
matchedDist = dCandidates[dIdx];
break;
}
}
}
}
if (matchedProv) {
$province.val(matchedProv).trigger('change').trigger('validate');
}
if (matchedCity) {
$city.val(matchedCity).trigger('change').trigger('validate');
}
if (matchedDist) {
$district.val(matchedDist).trigger('change').trigger('validate');
}
refreshPreview();
});
});
}
}

查看文件

@@ -31,8 +31,14 @@ export default {
ic = sceneStr.slice(3)
} else if (sceneStr.indexOf('uid_') === 0) {
inviterId = sceneStr.slice(4)
} else if (/^id_\d+_u_(\d+)$/.test(sceneStr)) {
const m = sceneStr.match(/^id_\d+_u_(\d+)$/)
if (m && m[1]) inviterId = m[1]
} else if (sceneStr.indexOf('uid=') === 0) {
inviterId = sceneStr.slice(4)
} else if (/^id_\d+_u_(\d+)$/.test(sceneStr)) {
const m = sceneStr.match(/^id_\d+_u_(\d+)$/)
if (m && m[1]) inviterId = m[1]
} else if (/^[A-Za-z0-9]{4,16}$/.test(sceneStr)) {
ic = sceneStr
}

查看文件

@@ -11,8 +11,8 @@ export const distributionApi = {
team: (params = {}) => get('/distribution/team', params, { silent: true }),
/** 分销收益明细账单(佣金流水) */
commissions: (params = {}) => get('/distribution/commissions', params, { silent: true }),
/** 专属推广海报要素 */
poster: () => get('/distribution/poster', {}, { silent: true }),
/** 专属推广海报要素(支持传入 postId 获取详情页小程序码) */
poster: (params = {}) => get('/distribution/poster', params, { silent: true }),
/** 申请提现(提现至微信零钱) */
withdraw: (payload) => post('/distribution/withdraw', payload, { silent: true }),
/** 提现记录 */

查看文件

@@ -471,19 +471,16 @@
<text class="detail__poster-tip">点击大图可全屏预览,支持长按直接转发</text>
</view>
<!-- 底部三大快捷分享操作 -->
<!-- 底部快捷分享操作 -->
<view class="detail__poster-actions">
<view class="detail__poster-btn detail__poster-btn--primary" @click="savePoster">
<image class="fc-icon fc-icon--sm" :src="iconUrl('image-white')" mode="aspectFit" />
<text class="detail__poster-btn-text">保存到手机相册</text>
</view>
<button class="detail__poster-btn detail__poster-btn--share" open-type="share">
<button class="detail__poster-btn detail__poster-btn--share" @click="sharePosterImage">
<image class="fc-icon fc-icon--sm" :src="iconUrl('bar-share')" mode="aspectFit" />
<text class="detail__poster-btn-text">发给微信好友</text>
</button>
<view class="detail__poster-btn detail__poster-btn--ghost" @click="copyPosterText">
<text>复制文案</text>
</view>
</view>
</view>
</view>
@@ -503,6 +500,7 @@ import postApi from '@/common/api/post.js'
import favoriteApi from '@/common/api/favorite.js'
import reportApi from '@/common/api/report.js'
import memberApi from '@/common/api/member.js'
import distributionApi from '@/common/api/distribution.js'
import { REPORT_REASONS } from '@/common/constants/report.js'
import { iconUrl, categoryIconUrl, loadUiConfig, uiState } from '@/common/store/ui.js'
import { fromNow, assetUrl } from '@/common/utils/format.js'
@@ -691,8 +689,29 @@ export default {
}
},
},
/**
* 页面加载:提取帖子ID与分销裂变参数
* @param {Object} query 路由入参
*/
onLoad(query) {
this.id = Number(query.id || 101)
let postIdFromScene = 0
if (query) {
const ic = query.ic || query.inviteCode || query.invite_code
let inviterId = query.inviterId || query.inviter_id
if (query.scene) {
try {
const s = decodeURIComponent(query.scene)
const m = s.match(/^id_(\d+)(?:_u_(\d+))?$/)
if (m) {
postIdFromScene = Number(m[1] || 0)
if (m[2]) inviterId = m[2]
}
} catch (e) {}
}
if (ic) uni.setStorageSync('fc_invite_code', String(ic))
if (inviterId) uni.setStorageSync('fc_inviter_id', Number(inviterId))
}
this.id = Number((query && query.id) || postIdFromScene || 101)
this.posterImagePath = ''
loadUiConfig()
this.fetch()
@@ -701,19 +720,21 @@ export default {
if (this.loaded) this.refreshAccess()
},
/**
* 转发给微信好友(携带分销邀请人与精美封面图)
* 转发给微信好友(携带分销邀请码/邀请人与精美封面图)
*/
onShareAppMessage() {
let inviterId = 0
let inviteCode = ''
try {
const user = this.currentUser
if (user && user.id) inviterId = user.id
if (user && user.inviteCode) inviteCode = user.inviteCode
} catch (e) {}
const shareTitle = this.post.title
? `【${this.post.categoryName || '同城信息'}】${this.post.title}`
: (uiState.shareTitle || uiState.siteName || '同城便民信息')
const sharePath = `/pages/detail/detail?id=${this.id}${inviterId ? '&inviterId=' + inviterId : ''}`
const sharePath = `/pages/detail/detail?id=${this.id}${inviteCode ? '&ic=' + encodeURIComponent(inviteCode) : ''}${inviterId ? '&inviterId=' + inviterId : ''}`
const shareImg = this.posterImagePath || (this.imageUrls && this.imageUrls.length ? this.imageUrls[0] : (this.post.cover ? assetUrl(this.post.cover) : ''))
return {
@@ -723,13 +744,15 @@ export default {
}
},
/**
* 转发到微信朋友圈
* 转发到微信朋友圈(携带分销邀请码/邀请人)
*/
onShareTimeline() {
let inviterId = 0
let inviteCode = ''
try {
const user = this.currentUser
if (user && user.id) inviterId = user.id
if (user && user.inviteCode) inviteCode = user.inviteCode
} catch (e) {}
const shareTitle = this.post.title
@@ -739,7 +762,7 @@ export default {
return {
title: shareTitle,
query: `id=${this.id}${inviterId ? '&inviterId=' + inviterId : ''}`,
query: `id=${this.id}${inviteCode ? '&ic=' + encodeURIComponent(inviteCode) : ''}${inviterId ? '&inviterId=' + inviterId : ''}`,
imageUrl: shareImg || undefined,
}
},
@@ -1693,8 +1716,22 @@ export default {
} catch (e) {}
const qrUrl = `https://fenleixinxi.gzyakj.com.cn/pages/detail/detail?id=${this.id}${inviterId ? '&inviterId=' + inviterId : ''}`
// 绘制高识别度合规二维码(124 x 124)
// 优先拉取并绘制微信官方小程序码,未发布或受限时无缝回退至自绘二维码(124 x 124)
let wxacodeLocal = ''
if (this.currentUser) {
try {
const posterMeta = await distributionApi.poster({ postId: this.id })
if (posterMeta && posterMeta.wxacodeUrl) {
wxacodeLocal = await this.getImageLocalPath(assetUrl(posterMeta.wxacodeUrl))
}
} catch (e) {}
}
if (wxacodeLocal) {
this.drawRoundRect(ctx, 56, 830, 124, 124, 10, '#FFFFFF', '#DFF2E8', 1)
ctx.drawImage(wxacodeLocal, 60, 834, 116, 116)
} else {
this.drawRealQRCode(ctx, qrUrl, 56, 830, 124)
}
// 右侧指引文案(显式左对齐,彻底杜绝对齐错乱)
this.drawSafeText(ctx, '微信长按识别小程序', 198, 842, {
@@ -1743,6 +1780,29 @@ export default {
}, 300)
})
},
/**
* 唤起微信原生图片分享面板(一键发给好友/发朋友圈/收藏),不支持时回退全屏预览长按转发
*/
sharePosterImage() {
if (!this.posterImagePath) {
uni.showToast({ title: '海报准备中,请稍候', icon: 'none' })
return
}
// #ifdef MP-WEIXIN
if (typeof wx !== 'undefined' && typeof wx.showShareImageMenu === 'function') {
wx.showShareImageMenu({
path: this.posterImagePath,
fail: (err) => {
if (err && err.errMsg && !err.errMsg.includes('cancel')) {
this.previewPoster()
}
},
})
return
}
// #endif
this.previewPoster()
},
/**
* 预览海报大图
*/

查看文件

@@ -512,14 +512,11 @@
<text>保存到相册</text>
</view>
<!-- #ifdef MP-WEIXIN -->
<button class="dist__poster-act-btn dist__poster-act-btn--share" open-type="share">
<button class="dist__poster-act-btn dist__poster-act-btn--share" @click="sharePosterImage">
<image class="dist__btn-icon" :src="iconUrl('share')" mode="aspectFit" />
<text>发给好友</text>
</button>
<!-- #endif -->
<view class="dist__poster-act-btn dist__poster-act-btn--ghost" @click="copyPosterText">
<text>复制文案</text>
</view>
</view>
</view>
</view>
@@ -528,7 +525,7 @@
<canvas
canvas-id="distPosterCanvas"
id="distPosterCanvas"
style="width: 600px; height: 880px; position: fixed; left: -9999px; top: -9999px; z-index: -999;"
style="width: 600px; height: 740px; position: fixed; left: -9999px; top: -9999px; z-index: -999;"
/>
</view>
</template>
@@ -1134,7 +1131,7 @@ export default {
}, 6000)
const W = 600
const H = 880
const H = 740
const ctx = uni.createCanvasContext('distPosterCanvas', this)
try {
@@ -1158,7 +1155,7 @@ export default {
ctx.fillRect(0, 0, W, H)
// 2. 纯白立体高光主卡片(圆角 24px,细边框 #D1FAE5)
this.drawRoundRect(ctx, 20, 20, 560, 840, 24, '#FFFFFF', '#D1FAE5', 1.5)
this.drawRoundRect(ctx, 20, 20, 560, 700, 24, '#FFFFFF', '#D1FAE5', 1.5)
// 3. 顶部 Header 栏(y: 44 ~ 76)
// 左侧绿色品牌胶囊
@@ -1291,7 +1288,16 @@ export default {
const qrX = 48
const qrY = 546
const qrSize = 150
let wxacodeLocal = ''
if (meta && meta.wxacodeUrl) {
wxacodeLocal = await this.getImageLocalPath(meta.wxacodeUrl, 1500)
}
if (wxacodeLocal) {
this.drawRoundRect(ctx, qrX, qrY, qrSize, qrSize, 12, '#FFFFFF', '#DFF2E8', 1)
ctx.drawImage(wxacodeLocal, qrX + 6, qrY + 6, qrSize - 12, qrSize - 12)
} else {
this.drawRealQRCode(ctx, qrUrl, qrX, qrY, qrSize)
}
// 二维码右侧文字说明(从 x = 216 开始,宽度 344px 极为充裕,排版整洁绝不重叠)
const textX = 218
@@ -1362,6 +1368,33 @@ export default {
}
},
/**
* 全屏预览生成的海报大图
*/
/**
* 唤起微信原生图片转发菜单(一键发给好友/发朋友圈/收藏),不支持时回退全屏预览长按转发
*/
sharePosterImage() {
if (!this.posterImagePath) {
uni.showToast({ title: '海报正在生成,请稍候', icon: 'none' })
return
}
// #ifdef MP-WEIXIN
if (typeof wx !== 'undefined' && typeof wx.showShareImageMenu === 'function') {
wx.showShareImageMenu({
path: this.posterImagePath,
fail: (err) => {
if (err && err.errMsg && !err.errMsg.includes('cancel')) {
this.previewPoster()
}
},
})
return
}
// #endif
this.previewPoster()
},
/**
* 全屏预览生成的海报大图
*/
@@ -2422,7 +2455,7 @@ export default {
}
.dist__poster-img {
width: 500rpx;
height: 733rpx;
height: 616rpx;
border-radius: 20rpx;
box-shadow: 0 16rpx 40rpx rgba(4, 48, 30, 0.16);
background: #ffffff;

查看文件

@@ -356,12 +356,7 @@ export default {
*/
vipTitle() {
if (this.isMember) return '会员权益已生效'
const b = this.benefits || {}
if (b.postMode === 'free') {
return '开通会员,尊享 VIP 专属特权'
}
const discount = b.postDiscount
return discount ? `开通会员,享专属发帖与查看特权` : '开通会员,享专属特权'
return '开通会员 · 享免费发帖看号'
},
/**
* 顶栏显示的城市或区域文案(优先当前选择区县,其次城市,缺省回退到同城服务)
@@ -472,16 +467,44 @@ export default {
onUnload() {
uni.$off('refreshUnreadCount')
},
/**
* 微信好友转发(携带当前登录用户的分销邀请码/ID)
*/
onShareAppMessage() {
let queryStr = ''
try {
const store = useUserStore()
const user = (store.userInfo && store.userInfo.value) || store.userInfo || (store.state && store.state.userInfo)
if (user) {
const params = []
if (user.inviteCode) params.push('ic=' + encodeURIComponent(user.inviteCode))
if (user.id) params.push('inviterId=' + encodeURIComponent(user.id))
if (params.length) queryStr = '?' + params.join('&')
}
} catch (e) {}
return {
title: uiState.shareTitle || uiState.siteName || '同城分类信息',
path: '/pages/index/index',
path: '/pages/index/index' + queryStr,
}
},
/**
* 微信朋友圈转发(携带当前登录用户的分销邀请码/ID)
*/
onShareTimeline() {
let queryStr = ''
try {
const store = useUserStore()
const user = (store.userInfo && store.userInfo.value) || store.userInfo || (store.state && store.state.userInfo)
if (user) {
const params = []
if (user.inviteCode) params.push('ic=' + encodeURIComponent(user.inviteCode))
if (user.id) params.push('inviterId=' + encodeURIComponent(user.id))
if (params.length) queryStr = params.join('&')
}
} catch (e) {}
return {
title: uiState.shareTitle || uiState.siteName || '同城分类信息',
query: '',
query: queryStr,
}
},
/**
@@ -1208,35 +1231,52 @@ export default {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
box-shadow: $fc-shadow;
}
.home__vip-left {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
}
.home__vip-left .fc-icon {
flex-shrink: 0;
}
.home__vip-texts {
display: flex;
flex-direction: column;
margin-left: 12rpx;
margin-left: 14rpx;
flex: 1;
min-width: 0;
}
.home__vip-title {
color: #ffffff;
font-size: $fc-font-lg;
font-size: 30rpx;
font-weight: 700;
line-height: $fc-leading-tight;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.home__vip-sub {
color: rgba(255, 255, 255, 0.9);
font-size: $fc-font-xs;
margin-top: 8rpx;
color: rgba(255, 255, 255, 0.92);
font-size: 22rpx;
margin-top: 6rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.home__vip-btn {
flex-shrink: 0;
white-space: nowrap;
padding: 12rpx 28rpx;
border-radius: $fc-radius-pill;
background: #ffffff;
color: $fc-primary-text;
font-size: $fc-font-sm;
font-size: 24rpx;
font-weight: 700;
line-height: 1.2;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}

查看文件

@@ -179,6 +179,7 @@ import FcEmpty from '@/components/fc-empty/fc-empty.vue'
import { merchantApi } from '@/common/api/merchant.js'
import { iconUrl, loadUiConfig } from '@/common/store/ui.js'
import { assetUrl } from '@/common/utils/format.js'
import { useUserStore } from '@/common/store/index.js'
export default {
components: { FcNavbar, FcPostCard, FcEmpty },
@@ -232,7 +233,13 @@ export default {
* @param {Object} query 路由入参
*/
onLoad(query) {
this.merchantId = Number(query.id || query.userId || 0)
if (query) {
const ic = query.ic || query.inviteCode || query.invite_code
const inviterId = query.inviterId || query.inviter_id
if (ic) uni.setStorageSync('fc_invite_code', String(ic))
if (inviterId) uni.setStorageSync('fc_inviter_id', Number(inviterId))
}
this.merchantId = Number((query && (query.id || query.userId)) || 0)
this.postId = Number(query.postId || 0)
loadUiConfig()
this.fetchLocation()
@@ -260,11 +267,44 @@ export default {
onShareAppMessage() {
return {
title: `【认证商家】${this.merchant.name || '同城实体门店'}`,
path: `/pages/merchant/detail?id=${this.merchantId}`,
path: this.buildSharePath(),
imageUrl: this.merchant.logo ? assetUrl(this.merchant.logo) : undefined,
}
},
/**
* 微信朋友圈分享配置(携带当前登录用户的分销邀请码/ID)
*/
onShareTimeline() {
return {
title: `【认证商家】${this.merchant.name || '同城实体门店'}`,
query: this.buildShareQuery(),
imageUrl: this.merchant.logo ? assetUrl(this.merchant.logo) : undefined,
}
},
methods: {
/**
* 构造带分销参数的商家详情页分享 query
* @returns {string}
*/
buildShareQuery() {
const params = [`id=${this.merchantId}`]
try {
const store = useUserStore()
const user = (store.userInfo && store.userInfo.value) || store.userInfo || (store.state && store.state.userInfo)
if (user) {
if (user.inviteCode) params.push('ic=' + encodeURIComponent(user.inviteCode))
if (user.id) params.push('inviterId=' + encodeURIComponent(user.id))
}
} catch (e) {}
return params.join('&')
},
/**
* 构造带分销参数的商家详情页分享路径
* @returns {string}
*/
buildSharePath() {
return '/pages/merchant/detail?' + this.buildShareQuery()
},
iconUrl,
assetUrl,
/**

查看文件

@@ -766,23 +766,32 @@ export default {
.mine__vip-left {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
margin-right: 16rpx;
}
.mine__vip-icon {
width: 52rpx;
height: 52rpx;
flex-shrink: 0;
}
.mine__vip-texts {
margin-left: 18rpx;
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
}
.mine__vip-title {
color: #4a2600; /* 深褐黑金字 */
font-size: 30rpx;
font-weight: 800;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mine__vip-sub {
@@ -790,9 +799,14 @@ export default {
color: #7a4600; /* 高清晰深暖金副标题 */
font-size: 22rpx;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mine__vip-btn {
flex-shrink: 0;
white-space: nowrap;
padding: 10rpx 26rpx;
border-radius: $fc-radius-pill;
background: #ffffff;