989 行
49 KiB
JavaScript
989 行
49 KiB
JavaScript
define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], function ($, undefined, Backend, Table, Form, Format) {
|
||
|
||
var Controller = {
|
||
/**
|
||
* 服务区域列表页初始化
|
||
*/
|
||
index: function () {
|
||
// 初始化表格参数配置
|
||
Table.api.init({
|
||
extend: {
|
||
index_url: 'fc/area/index' + location.search,
|
||
add_url: 'fc/area/add',
|
||
edit_url: 'fc/area/edit',
|
||
del_url: 'fc/area/del',
|
||
multi_url: 'fc/area/multi',
|
||
import_url: 'fc/area/import',
|
||
table: 'fc_area',
|
||
}
|
||
});
|
||
|
||
var table = $("#table");
|
||
|
||
// 初始化表格
|
||
table.bootstrapTable({
|
||
url: $.fn.bootstrapTable.defaults.extend.index_url,
|
||
pk: 'id',
|
||
sortName: 'sort',
|
||
columns: [
|
||
[
|
||
{checkbox: true},
|
||
{field: 'id', title: __('SerialNo'), operate: false, sortable: false, align: 'center', width: '70px', class: 'fc-serial', table: table, formatter: Format.serial},
|
||
{field: 'province', title: __('Province'), operate: 'LIKE'},
|
||
{field: 'city', title: __('City'), operate: 'LIKE'},
|
||
{field: 'district', title: __('District'), operate: 'LIKE'},
|
||
{field: 'full_name', title: __('FullName'), operate: false, sortable: false},
|
||
{
|
||
field: 'enabled', title: __('Enabled'),
|
||
searchList: {0: '否', 1: '是'},
|
||
formatter: Format.statusFormatter({0: '否', 1: '是'}, {0: 'default', 1: 'success'})
|
||
},
|
||
{field: 'sort', title: __('Sort'), width: '80px'},
|
||
{field: 'lat', title: __('Lat'), operate: false, visible: false},
|
||
{field: 'lng', title: __('Lng'), operate: false, visible: false},
|
||
{field: 'createtime', title: __('Createtime'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, formatter: Table.api.formatter.datetime},
|
||
{field: 'updatetime', title: __('Updatetime'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, formatter: Table.api.formatter.datetime, visible: false},
|
||
{field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate}
|
||
]
|
||
]
|
||
});
|
||
|
||
// 为表格绑定事件
|
||
Table.api.bindevent(table);
|
||
},
|
||
|
||
/**
|
||
* 服务区域新增页初始化
|
||
*/
|
||
add: function () {
|
||
Controller.api.bindevent();
|
||
Controller.api.bindAreaOptions();
|
||
},
|
||
|
||
/**
|
||
* 服务区域编辑页初始化
|
||
*/
|
||
edit: function () {
|
||
Controller.api.bindevent();
|
||
Controller.api.bindAreaOptions();
|
||
},
|
||
|
||
api: {
|
||
/**
|
||
* 绑定表单通用事件
|
||
*/
|
||
bindevent: function () {
|
||
Form.api.bindevent($("form[role=form]"));
|
||
},
|
||
|
||
/**
|
||
* 规范化省级名称(将直辖市简称统一为标准全称)
|
||
* @param {string} name 原始省份名称
|
||
* @return {string}
|
||
*/
|
||
normalizeProvinceName: function (name) {
|
||
var val = String(name || '').trim();
|
||
var map = {
|
||
'北京': '北京市',
|
||
'天津': '天津市',
|
||
'上海': '上海市',
|
||
'重庆': '重庆市',
|
||
'台湾': '台湾省'
|
||
};
|
||
return map[val] || val;
|
||
},
|
||
|
||
/**
|
||
* 加载全国行政区划数据(ChineseDistricts)
|
||
* @param {function} callback 数据加载完成回调
|
||
*/
|
||
loadNationalDistricts: function (callback) {
|
||
if (typeof window !== 'undefined' && window.ChineseDistricts) {
|
||
callback(window.ChineseDistricts);
|
||
return;
|
||
}
|
||
require(['citypicker-data'], function () {
|
||
if (typeof window !== 'undefined' && window.ChineseDistricts) {
|
||
callback(window.ChineseDistricts);
|
||
return;
|
||
}
|
||
require(['ChineseDistricts'], function (ChineseDistricts) {
|
||
callback(ChineseDistricts || window.ChineseDistricts || {});
|
||
}, function () {
|
||
callback({});
|
||
});
|
||
}, function () {
|
||
callback({});
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 构建全国省 / 市 / 区县全量数据树并合并数据库已有记录
|
||
* @param {Object} chineseDistricts 全国行政区划原始字典
|
||
* @param {Array} dbRows 数据库已存服务区域记录
|
||
* @param {number} currentId 当前编辑记录ID(新增时为0)
|
||
* @return {Object} 包含 provinces, cityMap, districtMap, allCities, allDistricts, addedMap
|
||
*/
|
||
buildAreaDataTree: function (chineseDistricts, dbRows, currentId) {
|
||
var normalizeProvince = Controller.api.normalizeProvinceName;
|
||
var provinces = [];
|
||
var provinceSeen = {};
|
||
var cityMap = {};
|
||
var districtMap = {};
|
||
var addedMap = {};
|
||
|
||
/**
|
||
* 确保省份存在于数据树中
|
||
* @param {string} prov 省份名称
|
||
*/
|
||
var ensureProvince = function (prov) {
|
||
var p = normalizeProvince(prov);
|
||
if (!p) {
|
||
return '';
|
||
}
|
||
if (!provinceSeen[p]) {
|
||
provinceSeen[p] = true;
|
||
provinces.push(p);
|
||
}
|
||
if (!cityMap[p]) {
|
||
cityMap[p] = [];
|
||
}
|
||
return p;
|
||
};
|
||
|
||
/**
|
||
* 确保城市存在于指定省份下
|
||
* @param {string} prov 省份名称
|
||
* @param {string} city 城市名称
|
||
*/
|
||
var ensureCity = function (prov, city) {
|
||
var p = ensureProvince(prov);
|
||
var c = String(city || '').trim();
|
||
if (!p || !c) {
|
||
return;
|
||
}
|
||
if (cityMap[p].indexOf(c) === -1) {
|
||
cityMap[p].push(c);
|
||
}
|
||
var key = p + '|' + c;
|
||
if (!districtMap[key]) {
|
||
districtMap[key] = [];
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 确保区县存在于指定省市下
|
||
* @param {string} prov 省份名称
|
||
* @param {string} city 城市名称
|
||
* @param {string} dist 区县名称
|
||
*/
|
||
var ensureDistrict = function (prov, city, dist) {
|
||
var p = ensureProvince(prov);
|
||
var c = String(city || '').trim();
|
||
var d = String(dist || '').trim();
|
||
if (!p || !c || !d) {
|
||
return;
|
||
}
|
||
ensureCity(p, c);
|
||
var key = p + '|' + c;
|
||
if (districtMap[key].indexOf(d) === -1) {
|
||
districtMap[key].push(d);
|
||
}
|
||
};
|
||
|
||
// 1. 解析全国行政区划库 ChineseDistricts
|
||
if (chineseDistricts && chineseDistricts[86]) {
|
||
var groups = chineseDistricts[86];
|
||
var provEntries = [];
|
||
$.each(groups, function (groupKey, list) {
|
||
if ($.isArray(list)) {
|
||
for (var i = 0; i < list.length; i++) {
|
||
if (list[i] && list[i].code && list[i].address) {
|
||
provEntries.push(list[i]);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
// 按行政区划代码排序(北京、天津、河北...顺序更符合国标习惯)
|
||
provEntries.sort(function (a, b) {
|
||
return String(a.code).localeCompare(String(b.code));
|
||
});
|
||
|
||
for (var i = 0; i < provEntries.length; i++) {
|
||
var provCode = provEntries[i].code;
|
||
var provName = ensureProvince(provEntries[i].address);
|
||
var citiesObj = chineseDistricts[provCode];
|
||
if (citiesObj && $.isPlainObject(citiesObj)) {
|
||
$.each(citiesObj, function (cityCode, cityName) {
|
||
ensureCity(provName, cityName);
|
||
var districtsObj = chineseDistricts[cityCode];
|
||
if (districtsObj && $.isPlainObject(districtsObj)) {
|
||
$.each(districtsObj, function (distCode, distName) {
|
||
ensureDistrict(provName, cityName, distName);
|
||
});
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. 合并数据库已有的 fc_area 记录(兼顾自定义区域与已添加标记)
|
||
if ($.isArray(dbRows)) {
|
||
for (var j = 0; j < dbRows.length; j++) {
|
||
var row = dbRows[j] || {};
|
||
var pRaw = String(row.province || '').trim();
|
||
var pNorm = normalizeProvince(pRaw);
|
||
var cName = String(row.city || '').trim();
|
||
var dName = String(row.district || '').trim();
|
||
var rowId = parseInt(row.id || 0, 10);
|
||
|
||
if (pNorm && cName && dName) {
|
||
ensureDistrict(pNorm, cName, dName);
|
||
if (rowId !== currentId) {
|
||
addedMap[pNorm + '|' + cName + '|' + dName] = true;
|
||
addedMap[pRaw + '|' + cName + '|' + dName] = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. 构建全国扁平化城市索引与区县索引,支持跨省直搜城市/区县反填
|
||
var allCities = [];
|
||
var allDistricts = [];
|
||
for (var pIdx = 0; pIdx < provinces.length; pIdx++) {
|
||
var pItem = provinces[pIdx];
|
||
var cList = cityMap[pItem] || [];
|
||
for (var cIdx = 0; cIdx < cList.length; cIdx++) {
|
||
var cItem = cList[cIdx];
|
||
allCities.push({
|
||
province: pItem,
|
||
city: cItem
|
||
});
|
||
var dList = districtMap[pItem + '|' + cItem] || [];
|
||
for (var dIdx = 0; dIdx < dList.length; dIdx++) {
|
||
allDistricts.push({
|
||
province: pItem,
|
||
city: cItem,
|
||
district: dList[dIdx]
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
provinces: provinces,
|
||
cityMap: cityMap,
|
||
districtMap: districtMap,
|
||
allCities: allCities,
|
||
allDistricts: allDistricts,
|
||
addedMap: addedMap
|
||
};
|
||
},
|
||
|
||
/**
|
||
* 省 / 市 / 区县全量可搜索级联下拉与中心坐标联动
|
||
*/
|
||
bindAreaOptions: function () {
|
||
var $raw = $('#area-options');
|
||
var $province = $('#c-province');
|
||
var $city = $('#c-city');
|
||
var $district = $('#c-district');
|
||
var $lat = $('#c-lat');
|
||
var $lng = $('#c-lng');
|
||
if (!$raw.length || !$province.length) {
|
||
return;
|
||
}
|
||
|
||
var currentId = parseInt($raw.data('current-id') || 0, 10);
|
||
var dbRows = [];
|
||
try {
|
||
dbRows = JSON.parse($raw.val() || '[]');
|
||
} catch (e) {
|
||
dbRows = [];
|
||
}
|
||
if (!$.isArray(dbRows)) {
|
||
dbRows = [];
|
||
}
|
||
|
||
var normalizeProvince = Controller.api.normalizeProvinceName;
|
||
var coordAutoFilled = false;
|
||
var geocodeTimer = null;
|
||
|
||
/**
|
||
* 更新输入框包装容器的 has-value 状态(控制清空按钮显示)
|
||
*/
|
||
var syncWrapState = function () {
|
||
$('.fc-area-select-wrap').each(function () {
|
||
var $wrap = $(this);
|
||
var val = String($wrap.find('.fc-area-input').val() || '').trim();
|
||
$wrap.toggleClass('has-value', val !== '');
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 将 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 = [
|
||
String($province.val() || '').trim(),
|
||
String($city.val() || '').trim(),
|
||
String($district.val() || '').trim()
|
||
].filter(function (item) {
|
||
return item !== '';
|
||
});
|
||
$('#area-full-name').text(parts.length ? parts.join(' / ') : '未选择');
|
||
$('.fc-area-map-btn').data('address', parts.join(''));
|
||
syncWrapState();
|
||
};
|
||
|
||
/**
|
||
* 刷新中心坐标状态提示文案
|
||
* @param {boolean} isAuto 是否为自动匹配填充
|
||
*/
|
||
var refreshCoordTip = function (isAuto) {
|
||
var lat = String($lat.val() || '').trim();
|
||
var lng = String($lng.val() || '').trim();
|
||
var $tip = $('#area-coord-tip');
|
||
if (lat && lng && isFinite(lat) && isFinite(lng)) {
|
||
var prefix = isAuto ? '已根据所选区域自动匹配中心坐标:' : '已设置中心坐标:';
|
||
$tip.removeClass('is-missing').addClass('is-set')
|
||
.text(prefix + lat + ', ' + lng + '(可点击「地图选点」微调)');
|
||
} else {
|
||
$tip.removeClass('is-set').addClass('is-missing')
|
||
.text('未设置坐标:选择区域后将自动匹配中心坐标,也可点击「地图选点」手动指定');
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 根据所选省市区自动解析中心经纬度(腾讯Place接口 + Nominatim自动降级)
|
||
*/
|
||
var autoResolveCoordinates = function () {
|
||
var p = String($province.val() || '').trim();
|
||
var c = String($city.val() || '').trim();
|
||
var d = String($district.val() || '').trim();
|
||
if (!p || !c || !d) {
|
||
return;
|
||
}
|
||
|
||
var curLat = String($lat.val() || '').trim();
|
||
var curLng = String($lng.val() || '').trim();
|
||
// 若已有手动填写的坐标且非自动匹配产生,则不覆盖
|
||
if (curLat && curLng && !coordAutoFilled) {
|
||
return;
|
||
}
|
||
|
||
if (geocodeTimer) {
|
||
clearTimeout(geocodeTimer);
|
||
}
|
||
geocodeTimer = setTimeout(function () {
|
||
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: (p === c ? p : (p + c)) + d,
|
||
region: c,
|
||
key: tencentKey,
|
||
output: 'jsonp'
|
||
},
|
||
success: function (ret) {
|
||
if (ret && ret.status === 0 && $.isArray(ret.data) && ret.data.length > 0 && ret.data[0].location) {
|
||
var loc = ret.data[0].location;
|
||
if (loc.lat && loc.lng) {
|
||
coordAutoFilled = true;
|
||
$lat.val(Number(loc.lat).toFixed(6));
|
||
$lng.val(Number(loc.lng).toFixed(6));
|
||
refreshCoordTip(true);
|
||
return;
|
||
}
|
||
}
|
||
resolveByNominatim();
|
||
},
|
||
error: function () {
|
||
resolveByNominatim();
|
||
}
|
||
});
|
||
}, 180);
|
||
};
|
||
|
||
refreshPreview();
|
||
refreshCoordTip(false);
|
||
|
||
// 手动修改坐标时标记为非自动填充
|
||
$lat.add($lng).on('input change', function () {
|
||
coordAutoFilled = false;
|
||
refreshCoordTip(false);
|
||
});
|
||
|
||
// 加载全国省市区数据并绑定下拉交互
|
||
Controller.api.loadNationalDistricts(function (chineseDistricts) {
|
||
var tree = Controller.api.buildAreaDataTree(chineseDistricts, dbRows, currentId);
|
||
|
||
/**
|
||
* 根据输入省份名称匹配标准省份键名
|
||
* @param {string} inputProv 输入的省份名
|
||
* @return {string}
|
||
*/
|
||
var resolveProvinceKey = function (inputProv) {
|
||
var raw = String(inputProv || '').trim();
|
||
if (!raw) {
|
||
return '';
|
||
}
|
||
var norm = normalizeProvince(raw);
|
||
if (tree.cityMap[norm]) {
|
||
return norm;
|
||
}
|
||
for (var i = 0; i < tree.provinces.length; i++) {
|
||
if (tree.provinces[i].indexOf(raw) !== -1 || raw.indexOf(tree.provinces[i].replace(/省|市|自治区|特别行政区|壮族|回族|维吾尔/g, '')) !== -1) {
|
||
return tree.provinces[i];
|
||
}
|
||
}
|
||
return raw;
|
||
};
|
||
|
||
/**
|
||
* 关闭所有下拉面板
|
||
* @param {jQuery} [$exceptWrap] 排除不关闭的容器
|
||
*/
|
||
var closeAllDropdowns = function ($exceptWrap) {
|
||
$('.fc-area-select-wrap').each(function () {
|
||
if (!$exceptWrap || !$(this).is($exceptWrap)) {
|
||
$(this).removeClass('is-open');
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 滚动下拉框使当前选中项可见
|
||
* @param {jQuery} $dropdown 下拉面板元素
|
||
*/
|
||
var scrollActiveIntoView = function ($dropdown) {
|
||
var $active = $dropdown.find('.fc-area-dropdown__item.is-active').first();
|
||
if ($active.length) {
|
||
var top = $active.position().top + $dropdown.scrollTop() - 40;
|
||
$dropdown.scrollTop(Math.max(0, top));
|
||
} else {
|
||
$dropdown.scrollTop(0);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 渲染省份下拉列表
|
||
* @param {boolean} filterByInput 是否根据当前输入内容过滤
|
||
*/
|
||
var renderProvinceDropdown = function (filterByInput) {
|
||
var $dropdown = $('#province-dropdown');
|
||
var currentVal = String($province.val() || '').trim();
|
||
var currentNorm = normalizeProvince(currentVal);
|
||
var keyword = filterByInput ? currentVal.toLowerCase() : '';
|
||
|
||
var list = tree.provinces.filter(function (prov) {
|
||
if (!keyword) {
|
||
return true;
|
||
}
|
||
return prov.toLowerCase().indexOf(keyword) !== -1;
|
||
});
|
||
|
||
$dropdown.empty();
|
||
$('<div class="fc-area-dropdown__header">').text(
|
||
keyword ? ('搜索省份(匹配 ' + list.length + ' 项)') : ('全国省级行政区(共 ' + tree.provinces.length + ' 个,可输入搜索)')
|
||
).appendTo($dropdown);
|
||
|
||
if (!list.length) {
|
||
$('<div class="fc-area-dropdown__empty">').text(
|
||
'未找到匹配省份,可直接保存自定义省份「' + currentVal + '」'
|
||
).appendTo($dropdown);
|
||
return;
|
||
}
|
||
|
||
for (var i = 0; i < list.length; i++) {
|
||
var prov = list[i];
|
||
var cityCount = (tree.cityMap[prov] || []).length;
|
||
var $item = $('<div class="fc-area-dropdown__item">')
|
||
.toggleClass('is-active', prov === currentVal || prov === currentNorm)
|
||
.attr('data-province', prov);
|
||
$('<span class="fc-area-dropdown__main">').text(prov).appendTo($item);
|
||
if (cityCount > 0) {
|
||
$('<span class="fc-area-dropdown__sub">').text(cityCount + ' 个城市').appendTo($item);
|
||
}
|
||
$dropdown.append($item);
|
||
}
|
||
scrollActiveIntoView($dropdown);
|
||
};
|
||
|
||
/**
|
||
* 渲染城市下拉列表(支持按当前省份展示,或未选省份/跨省搜索时展示全国城市)
|
||
* @param {boolean} filterByInput 是否根据当前输入内容过滤
|
||
*/
|
||
var renderCityDropdown = function (filterByInput) {
|
||
var $dropdown = $('#city-dropdown');
|
||
var provVal = resolveProvinceKey($province.val());
|
||
var currentCity = String($city.val() || '').trim();
|
||
var keyword = filterByInput ? currentCity.toLowerCase() : '';
|
||
|
||
$dropdown.empty();
|
||
|
||
var items = [];
|
||
var isNationalMode = false;
|
||
|
||
if (provVal && tree.cityMap[provVal] && !keyword) {
|
||
// 已选省份且未输入搜索词:展示该省下全部城市
|
||
var provCities = tree.cityMap[provVal] || [];
|
||
for (var i = 0; i < provCities.length; i++) {
|
||
items.push({
|
||
province: provVal,
|
||
city: provCities[i]
|
||
});
|
||
}
|
||
} else if (keyword) {
|
||
// 输入了关键字:优先当前省份匹配,若无匹配或未选省份则检索全国城市
|
||
if (provVal && tree.cityMap[provVal]) {
|
||
var pCities = tree.cityMap[provVal] || [];
|
||
for (var j = 0; j < pCities.length; j++) {
|
||
if (pCities[j].toLowerCase().indexOf(keyword) !== -1) {
|
||
items.push({
|
||
province: provVal,
|
||
city: pCities[j]
|
||
});
|
||
}
|
||
}
|
||
}
|
||
// 同时补充全国其他省份匹配的城市
|
||
isNationalMode = true;
|
||
for (var k = 0; k < tree.allCities.length; k++) {
|
||
var item = tree.allCities[k];
|
||
if (item.city.toLowerCase().indexOf(keyword) !== -1 || item.province.toLowerCase().indexOf(keyword) !== -1) {
|
||
var exists = false;
|
||
for (var m = 0; m < items.length; m++) {
|
||
if (items[m].province === item.province && items[m].city === item.city) {
|
||
exists = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!exists) {
|
||
items.push(item);
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// 未选省份:直接展示全国全部城市,点击自动带出所属省份
|
||
isNationalMode = true;
|
||
items = tree.allCities.slice(0);
|
||
}
|
||
|
||
var headerText = '';
|
||
if (keyword) {
|
||
headerText = '全国城市搜索结果(共 ' + items.length + ' 项,点击自动关联省份)';
|
||
} else if (!isNationalMode && provVal) {
|
||
headerText = provVal + ' · 下属城市(共 ' + items.length + ' 个,也可直接输入搜索全国城市)';
|
||
} else {
|
||
headerText = '全国全部城市(共 ' + items.length + ' 个,点击自动填充省份,可输入搜索)';
|
||
}
|
||
$('<div class="fc-area-dropdown__header">').text(headerText).appendTo($dropdown);
|
||
|
||
if (!items.length) {
|
||
$('<div class="fc-area-dropdown__empty">').text(
|
||
'未找到匹配城市,可直接保存自定义城市「' + currentCity + '」'
|
||
).appendTo($dropdown);
|
||
return;
|
||
}
|
||
|
||
for (var idx = 0; idx < items.length; idx++) {
|
||
var cObj = items[idx];
|
||
var distCount = (tree.districtMap[cObj.province + '|' + cObj.city] || []).length;
|
||
var $item = $('<div class="fc-area-dropdown__item">')
|
||
.toggleClass('is-active', cObj.city === currentCity && (!provVal || cObj.province === provVal))
|
||
.attr('data-province', cObj.province)
|
||
.attr('data-city', cObj.city);
|
||
$('<span class="fc-area-dropdown__main">').text(cObj.city).appendTo($item);
|
||
var subText = isNationalMode ? cObj.province : (distCount ? (distCount + ' 个区县') : '');
|
||
if (subText) {
|
||
$('<span class="fc-area-dropdown__sub">').text(subText).appendTo($item);
|
||
}
|
||
$dropdown.append($item);
|
||
}
|
||
scrollActiveIntoView($dropdown);
|
||
};
|
||
|
||
/**
|
||
* 渲染区县下拉列表(支持按当前省市展示、已添加标记,以及全国区县关键字检索反填)
|
||
* @param {boolean} filterByInput 是否根据当前输入内容过滤
|
||
*/
|
||
var renderDistrictDropdown = function (filterByInput) {
|
||
var $dropdown = $('#district-dropdown');
|
||
var provVal = resolveProvinceKey($province.val());
|
||
var cityVal = String($city.val() || '').trim();
|
||
var currentDist = String($district.val() || '').trim();
|
||
var keyword = filterByInput ? currentDist.toLowerCase() : '';
|
||
|
||
$dropdown.empty();
|
||
|
||
// 如果还没选省份但已选城市,尝试从全国城市反查省份
|
||
if (!provVal && cityVal) {
|
||
for (var cIdx = 0; cIdx < tree.allCities.length; cIdx++) {
|
||
if (tree.allCities[cIdx].city === cityVal) {
|
||
provVal = tree.allCities[cIdx].province;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
var items = [];
|
||
var isNationalSearch = false;
|
||
var key = provVal + '|' + cityVal;
|
||
var baseDistricts = tree.districtMap[key] || [];
|
||
|
||
if (baseDistricts.length > 0) {
|
||
for (var i = 0; i < baseDistricts.length; i++) {
|
||
var dName = baseDistricts[i];
|
||
if (!keyword || dName.toLowerCase().indexOf(keyword) !== -1) {
|
||
items.push({
|
||
province: provVal,
|
||
city: cityVal,
|
||
district: dName
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// 若在当前城市下未搜到或尚未选择省市,且有搜索词,则跨全国区县搜索
|
||
if (keyword && items.length === 0) {
|
||
isNationalSearch = true;
|
||
for (var j = 0; j < tree.allDistricts.length; j++) {
|
||
var dObj = tree.allDistricts[j];
|
||
if (dObj.district.toLowerCase().indexOf(keyword) !== -1) {
|
||
items.push(dObj);
|
||
if (items.length >= 120) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!provVal && !cityVal && !keyword) {
|
||
$('<div class="fc-area-dropdown__header">').text('区县选择提示').appendTo($dropdown);
|
||
$('<div class="fc-area-dropdown__empty">').text(
|
||
'请先选择省份或城市,也可直接在此输入区县名称搜索全国区县'
|
||
).appendTo($dropdown);
|
||
return;
|
||
}
|
||
|
||
var headerText = isNationalSearch
|
||
? ('全国区县搜索结果(点击自动填充省/市/区县)')
|
||
: ((cityVal || '当前城市') + ' · 下属区县(共 ' + items.length + ' 个,支持自定义输入)');
|
||
$('<div class="fc-area-dropdown__header">').text(headerText).appendTo($dropdown);
|
||
|
||
if (!items.length) {
|
||
$('<div class="fc-area-dropdown__empty">').text(
|
||
currentDist
|
||
? ('可直接保存自定义区县:「' + currentDist + '」')
|
||
: '暂无预设区县,请直接输入区县名称'
|
||
).appendTo($dropdown);
|
||
return;
|
||
}
|
||
|
||
for (var k = 0; k < items.length; k++) {
|
||
var item = items[k];
|
||
var isAdded = !!tree.addedMap[item.province + '|' + item.city + '|' + item.district];
|
||
var $item = $('<div class="fc-area-dropdown__item">')
|
||
.toggleClass('is-active', item.district === currentDist && item.city === cityVal)
|
||
.attr('data-province', item.province)
|
||
.attr('data-city', item.city)
|
||
.attr('data-district', item.district);
|
||
$('<span class="fc-area-dropdown__main">').text(item.district).appendTo($item);
|
||
|
||
if (isNationalSearch) {
|
||
$('<span class="fc-area-dropdown__sub">').text(item.province + ' / ' + item.city).appendTo($item);
|
||
} else if (isAdded) {
|
||
$('<span class="fc-area-dropdown__tag">').text('已添加').appendTo($item);
|
||
}
|
||
$dropdown.append($item);
|
||
}
|
||
scrollActiveIntoView($dropdown);
|
||
};
|
||
|
||
/**
|
||
* 展开指定层级的下拉面板
|
||
* @param {string} level province | city | district
|
||
* @param {boolean} filterByInput 是否按输入关键字过滤
|
||
*/
|
||
var openDropdown = function (level, filterByInput) {
|
||
var $wrap = $('.fc-area-select-wrap[data-level="' + level + '"]');
|
||
closeAllDropdowns($wrap);
|
||
$wrap.addClass('is-open');
|
||
if (level === 'province') {
|
||
renderProvinceDropdown(!!filterByInput);
|
||
} else if (level === 'city') {
|
||
renderCityDropdown(!!filterByInput);
|
||
} else if (level === 'district') {
|
||
renderDistrictDropdown(!!filterByInput);
|
||
}
|
||
};
|
||
|
||
// 聚焦或点击输入框时展开完整候选列表(不因已有值而隐藏其他选项)
|
||
$province.on('focus click', function () {
|
||
openDropdown('province', false);
|
||
});
|
||
$city.on('focus click', function () {
|
||
openDropdown('city', false);
|
||
});
|
||
$district.on('focus click', function () {
|
||
openDropdown('district', false);
|
||
});
|
||
|
||
// 输入文字时按关键字实时过滤
|
||
$province.on('input', function () {
|
||
refreshPreview();
|
||
openDropdown('province', true);
|
||
});
|
||
$city.on('input', function () {
|
||
refreshPreview();
|
||
openDropdown('city', true);
|
||
});
|
||
$district.on('input', function () {
|
||
refreshPreview();
|
||
openDropdown('district', true);
|
||
autoResolveCoordinates();
|
||
});
|
||
|
||
// 选择省份项
|
||
$('#province-dropdown').on('mousedown', '.fc-area-dropdown__item', function (e) {
|
||
e.preventDefault();
|
||
var selectedProv = String($(this).attr('data-province') || '');
|
||
var prevProv = normalizeProvince($province.val());
|
||
$province.val(selectedProv).trigger('change').trigger('validate');
|
||
|
||
if (selectedProv !== prevProv) {
|
||
$city.val('');
|
||
$district.val('');
|
||
}
|
||
|
||
var cities = tree.cityMap[selectedProv] || [];
|
||
if (cities.length === 1) {
|
||
// 直辖市(如北京市 -> 北京市)自动填充城市并直接展开区县
|
||
$city.val(cities[0]).trigger('change').trigger('validate');
|
||
refreshPreview();
|
||
$district.trigger('focus');
|
||
openDropdown('district', false);
|
||
} else {
|
||
refreshPreview();
|
||
$city.trigger('focus');
|
||
openDropdown('city', false);
|
||
}
|
||
});
|
||
|
||
// 选择城市项(支持跨省选城自动反填省份)
|
||
$('#city-dropdown').on('mousedown', '.fc-area-dropdown__item', function (e) {
|
||
e.preventDefault();
|
||
var selectedProv = String($(this).attr('data-province') || '');
|
||
var selectedCity = String($(this).attr('data-city') || '');
|
||
var prevCity = String($city.val() || '').trim();
|
||
|
||
if (selectedProv && String($province.val() || '').trim() !== selectedProv) {
|
||
$province.val(selectedProv).trigger('change').trigger('validate');
|
||
}
|
||
$city.val(selectedCity).trigger('change').trigger('validate');
|
||
|
||
if (selectedCity !== prevCity) {
|
||
$district.val('');
|
||
}
|
||
refreshPreview();
|
||
$district.trigger('focus');
|
||
openDropdown('district', false);
|
||
});
|
||
|
||
// 选择区县项(支持全国搜索区县自动反填省份和城市)
|
||
$('#district-dropdown').on('mousedown', '.fc-area-dropdown__item', function (e) {
|
||
e.preventDefault();
|
||
var selectedProv = String($(this).attr('data-province') || '');
|
||
var selectedCity = String($(this).attr('data-city') || '');
|
||
var selectedDist = String($(this).attr('data-district') || '');
|
||
|
||
if (selectedProv && String($province.val() || '').trim() !== selectedProv) {
|
||
$province.val(selectedProv).trigger('change').trigger('validate');
|
||
}
|
||
if (selectedCity && String($city.val() || '').trim() !== selectedCity) {
|
||
$city.val(selectedCity).trigger('change').trigger('validate');
|
||
}
|
||
$district.val(selectedDist).trigger('change').trigger('validate');
|
||
|
||
closeAllDropdowns();
|
||
refreshPreview();
|
||
autoResolveCoordinates();
|
||
});
|
||
|
||
// 清空按钮事件(清空当前及下级,并重新展开当前下拉)
|
||
$('.fc-area-select-clear').on('mousedown', function (e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
var $wrap = $(this).closest('.fc-area-select-wrap');
|
||
var level = $wrap.attr('data-level');
|
||
if (level === 'province') {
|
||
$province.val('');
|
||
$city.val('');
|
||
$district.val('');
|
||
refreshPreview();
|
||
$province.trigger('focus');
|
||
openDropdown('province', false);
|
||
} else if (level === 'city') {
|
||
$city.val('');
|
||
$district.val('');
|
||
refreshPreview();
|
||
$city.trigger('focus');
|
||
openDropdown('city', false);
|
||
} else if (level === 'district') {
|
||
$district.val('');
|
||
refreshPreview();
|
||
$district.trigger('focus');
|
||
openDropdown('district', false);
|
||
}
|
||
});
|
||
|
||
// 点击组件外部时关闭下拉列表
|
||
$(document).on('mousedown.fcAreaDropdown', function (e) {
|
||
if (!$(e.target).closest('.fc-area-select-wrap').length) {
|
||
closeAllDropdowns();
|
||
}
|
||
});
|
||
|
||
// 按 ESC 关闭下拉列表
|
||
$(document).on('keydown.fcAreaDropdown', function (e) {
|
||
if (e.keyCode === 27) {
|
||
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();
|
||
});
|
||
});
|
||
}
|
||
}
|
||
};
|
||
return Controller;
|
||
});
|