Przeglądaj źródła

Merge branch 'dev-jcq' of https://s-20coaj910c.zht2.com/cmy/data-marketing-platform into dev-ly

luoy 22 godzin temu
rodzic
commit
c538e530cf

+ 2 - 2
.env

@@ -5,8 +5,8 @@ VITE_IS_MICRO= true
 VITE_PUBLIC_PATH = /
 
 # 后端请求前缀
-# VITE_API_URL = http://192.168.10.101:9999
-VITE_API_URL = http://192.168.3.118:9999
+VITE_API_URL = http://192.168.10.101:9999
+# VITE_API_URL = http://192.168.3.118:9999
 #  VITE_API_URL = http://192.168.3.17:9999
 
 # OAUTH2 密码模式客户端信息

+ 14 - 0
package-lock.json

@@ -10,6 +10,7 @@
 			"license": "Apache-2.0",
 			"dependencies": {
 				"@element-plus/icons-vue": "2.3.1",
+				"@vant/area-data": "^2.1.0",
 				"@vueuse/core": "10.4.1",
 				"@wangeditor-next/editor": "5.6.34",
 				"@wangeditor-next/editor-for-vue": "5.1.14",
@@ -18,6 +19,7 @@
 				"chalk": "^5.4.1",
 				"child_process": "^1.0.2",
 				"china-area-data": "^5.0.1",
+				"china-division": "^2.7.0",
 				"codemirror": "5.65.18",
 				"country-state-city": "^3.2.1",
 				"crypto-js": "4.2.0",
@@ -1771,6 +1773,12 @@
 				"@uppy/core": "^2.3.3"
 			}
 		},
+		"node_modules/@vant/area-data": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/@vant/area-data/-/area-data-2.1.0.tgz",
+			"integrity": "sha512-wx9PrUX7wSUJiFcz8UrcvZfTjV6sTc+7SHcbjGQQzEcv5y+EwOo5uV4ZKdfrR5Hzcw4MA08LQdvXPSEb4nWbug==",
+			"license": "MIT"
+		},
 		"node_modules/@vitejs/plugin-vue": {
 			"version": "5.2.1",
 			"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz",
@@ -2567,6 +2575,12 @@
 			"resolved": "https://registry.npmmirror.com/china-area-data/-/china-area-data-5.0.1.tgz",
 			"integrity": "sha512-BQDPpiv5Nn+018ekcJK2oSD9PAD+E1bvXB0wgabc//dFVS/KvRqCgg0QOEUt3vBkx9XzB5a9BmkJCEZDBxVjVw=="
 		},
+		"node_modules/china-division": {
+			"version": "2.7.0",
+			"resolved": "https://registry.npmjs.org/china-division/-/china-division-2.7.0.tgz",
+			"integrity": "sha512-4uUPAT+1WfqDh5jytq7omdCmHNk3j+k76zEG/2IqaGcYB90c2SwcixttcypdsZ3T/9tN1TTpBDoeZn+Yw/qBEA==",
+			"license": "MIT"
+		},
 		"node_modules/chokidar": {
 			"version": "3.6.0",
 			"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",

+ 2 - 0
package.json

@@ -17,6 +17,7 @@
 	},
 	"dependencies": {
 		"@element-plus/icons-vue": "2.3.1",
+		"@vant/area-data": "^2.1.0",
 		"@vueuse/core": "10.4.1",
 		"@wangeditor-next/editor": "5.6.34",
 		"@wangeditor-next/editor-for-vue": "5.1.14",
@@ -25,6 +26,7 @@
 		"chalk": "^5.4.1",
 		"child_process": "^1.0.2",
 		"china-area-data": "^5.0.1",
+		"china-division": "^2.7.0",
 		"codemirror": "5.65.18",
 		"country-state-city": "^3.2.1",
 		"crypto-js": "4.2.0",

+ 45 - 0
src/api/count/version.ts

@@ -0,0 +1,45 @@
+import request from '/@/utils/request';
+import { appID } from '/@/api/common/common';
+
+//查询全部版本详情
+export const getTrendDetailAll = (data?: Object) => {
+	return request({
+		url: '/stats/user/version/detail',
+		method: 'post',
+		data:{appId: appID, ...data},
+	});
+};
+//查询单个版本详情
+export const getTrendDetailOne = (data?: Object) => {
+	return request({
+		url: '/stats/user/version/single/detail',
+		method: 'post',
+		data:{appId: appID, ...data},
+	});
+};
+//查询单个版本用户来源详情
+export const getTrendDetailSource = (data?: Object) => {
+	return request({
+		url: '/stats/user/version/distribution/detail',
+		method: 'post',
+		data:{appId: appID, ...data},
+	});
+};
+
+//升级用户趋势图
+export const getTrendUpgrade = (data?: Object) => {
+	return request({
+		url: '/stats/user/upgrade/trend',
+		method: 'post',
+		data:{appId: appID, ...data},
+	});
+};
+
+//查询单个版本用户来源
+export const getTrendSource = (data?: Object) => {
+	return request({
+		url: '/stats/user/version/distribution',
+		method: 'post',
+		data:{appId: appID, ...data},
+	});
+};

+ 152 - 313
src/components/common/ChineseRegionSelector.vue

@@ -1,45 +1,33 @@
 <template>
 	<div class="chinese-region-selector">
-		<el-select
-			v-model="selectedCountry"
-			placeholder="选择国家"
-			filterable
-			class="!w-48"
-			@change="onCountryChange"
-		>
+		<el-select v-model="selectedCountry" placeholder="选择国家" filterable class="!w-48" @change="onCountryChange">
+			<el-option-group v-if="props.showAll !== false" label="全部">
+				<el-option :label="'全部国家'" :value="ALL_COUNTRY" />
+			</el-option-group>
 			<el-option-group v-for="continent in continentOptions" :key="continent.label" :label="continent.label">
-				<el-option 
-					v-for="country in continent.countries" 
-					:key="country.code" 
-					:label="country.name" 
-					:value="country.code" 
-				/>
+				<el-option v-for="country in continent.countries" :key="country.code" :label="country.name"
+					:value="country.code" />
+			</el-option-group>
+		</el-select>
+
+		<el-select v-if="selectedCountry && selectedCountry !== ALL_COUNTRY && states.length > 0"
+			v-model="selectedState" placeholder="选择省份/州" filterable class="!w-48 ml-2" @change="onStateChange">
+			<el-option-group v-if="props.showAll !== false" label="全部">
+				<el-option :label="'全部省/州'" :value="ALL_STATE" />
 			</el-option-group>
+			<el-option v-for="state in states" :key="state.code" :label="state.name" :value="state.code" />
 		</el-select>
 
-		<el-select
-			v-if="selectedCountry && states.length > 0"
-			v-model="selectedState"
-			placeholder="选择省份/州"
-			filterable
-			class="!w-48 ml-2"
-			@change="onStateChange"
-		>
-			<el-option 
-				v-for="state in states" 
-				:key="state.code" 
-				:label="state.name" 
-				:value="state.name" 
-			/>
+		<el-select v-if="selectedState && selectedState !== ALL_STATE && cities.length > 0" v-model="selectedCity"
+			placeholder="选择城市" filterable class="!w-48 ml-2">
+			<el-option-group v-if="props.showAll !== false" label="全部">
+				<el-option :label="'全部城市'" :value="ALL_CITY" />
+			</el-option-group>
+			<el-option v-for="city in cities" :key="city.code" :label="city.name" :value="city.name" />
 		</el-select>
 
-		<el-button 
-			v-if="selectedCountry" 
-			type="primary" 
-			size="small" 
-			class="ml-2"
-			@click="addRegion"
-		>
+		<el-button v-if="(props.showAll !== false) || selectedCountry" type="primary" size="small" class="ml-2"
+			@click="addRegion">
 			添加地区
 		</el-button>
 	</div>
@@ -47,15 +35,19 @@
 
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
+import { State, City } from 'country-state-city';
+import regionData from './region-data.json';
 
 interface RegionData {
 	country: string;
 	state?: string;
+	city?: string;
 	code: string;
 }
 
 const props = defineProps<{
 	modelValue?: RegionData[];
+	showAll?: boolean;
 }>();
 
 const emit = defineEmits<{
@@ -65,269 +57,36 @@ const emit = defineEmits<{
 const continentOptions = ref<any[]>([]);
 const selectedCountry = ref('');
 const selectedState = ref('');
+const selectedCity = ref('');
 const states = ref<any[]>([]);
+const cities = ref<any[]>([]);
 const countryCode = ref('');
+const ALL_COUNTRY = '__ALL_COUNTRY__';
+const ALL_STATE = '__ALL_STATE__';
+const ALL_CITY = '__ALL_CITY__';
 
 // 预定义的中文国家数据
-const countriesData = [
-	// 亚洲
-	{ name: '中国', code: 'CN', continent: '亚洲' },
-	{ name: '日本', code: 'JP', continent: '亚洲' },
-	{ name: '韩国', code: 'KR', continent: '亚洲' },
-	{ name: '印度', code: 'IN', continent: '亚洲' },
-	{ name: '新加坡', code: 'SG', continent: '亚洲' },
-	{ name: '泰国', code: 'TH', continent: '亚洲' },
-	{ name: '马来西亚', code: 'MY', continent: '亚洲' },
-	{ name: '印度尼西亚', code: 'ID', continent: '亚洲' },
-	{ name: '菲律宾', code: 'PH', continent: '亚洲' },
-	{ name: '越南', code: 'VN', continent: '亚洲' },
-	{ name: '台湾', code: 'TW', continent: '亚洲' },
-	{ name: '香港', code: 'HK', continent: '亚洲' },
-	{ name: '澳门', code: 'MO', continent: '亚洲' },
-	{ name: '巴基斯坦', code: 'PK', continent: '亚洲' },
-	{ name: '孟加拉国', code: 'BD', continent: '亚洲' },
-	{ name: '斯里兰卡', code: 'LK', continent: '亚洲' },
-	{ name: '缅甸', code: 'MM', continent: '亚洲' },
-	{ name: '柬埔寨', code: 'KH', continent: '亚洲' },
-	{ name: '老挝', code: 'LA', continent: '亚洲' },
-	{ name: '蒙古', code: 'MN', continent: '亚洲' },
-	{ name: '哈萨克斯坦', code: 'KZ', continent: '亚洲' },
-	{ name: '乌兹别克斯坦', code: 'UZ', continent: '亚洲' },
-	{ name: '吉尔吉斯斯坦', code: 'KG', continent: '亚洲' },
-	{ name: '塔吉克斯坦', code: 'TJ', continent: '亚洲' },
-	{ name: '土库曼斯坦', code: 'TM', continent: '亚洲' },
-	{ name: '阿富汗', code: 'AF', continent: '亚洲' },
-	{ name: '伊朗', code: 'IR', continent: '亚洲' },
-	{ name: '伊拉克', code: 'IQ', continent: '亚洲' },
-	{ name: '叙利亚', code: 'SY', continent: '亚洲' },
-	{ name: '黎巴嫩', code: 'LB', continent: '亚洲' },
-	{ name: '约旦', code: 'JO', continent: '亚洲' },
-	{ name: '以色列', code: 'IL', continent: '亚洲' },
-	{ name: '巴勒斯坦', code: 'PS', continent: '亚洲' },
-	{ name: '沙特阿拉伯', code: 'SA', continent: '亚洲' },
-	{ name: '阿联酋', code: 'AE', continent: '亚洲' },
-	{ name: '卡塔尔', code: 'QA', continent: '亚洲' },
-	{ name: '巴林', code: 'BH', continent: '亚洲' },
-	{ name: '科威特', code: 'KW', continent: '亚洲' },
-	{ name: '阿曼', code: 'OM', continent: '亚洲' },
-	{ name: '也门', code: 'YE', continent: '亚洲' },
-	{ name: '土耳其', code: 'TR', continent: '亚洲' },
-	{ name: '塞浦路斯', code: 'CY', continent: '亚洲' },
-	{ name: '格鲁吉亚', code: 'GE', continent: '亚洲' },
-	{ name: '亚美尼亚', code: 'AM', continent: '亚洲' },
-	{ name: '阿塞拜疆', code: 'AZ', continent: '亚洲' },
-
-	// 欧洲
-	{ name: '英国', code: 'GB', continent: '欧洲' },
-	{ name: '法国', code: 'FR', continent: '欧洲' },
-	{ name: '德国', code: 'DE', continent: '欧洲' },
-	{ name: '意大利', code: 'IT', continent: '欧洲' },
-	{ name: '西班牙', code: 'ES', continent: '欧洲' },
-	{ name: '荷兰', code: 'NL', continent: '欧洲' },
-	{ name: '瑞士', code: 'CH', continent: '欧洲' },
-	{ name: '瑞典', code: 'SE', continent: '欧洲' },
-	{ name: '挪威', code: 'NO', continent: '欧洲' },
-	{ name: '丹麦', code: 'DK', continent: '欧洲' },
-	{ name: '芬兰', code: 'FI', continent: '欧洲' },
-	{ name: '俄罗斯', code: 'RU', continent: '欧洲' },
-	{ name: '波兰', code: 'PL', continent: '欧洲' },
-	{ name: '捷克', code: 'CZ', continent: '欧洲' },
-	{ name: '匈牙利', code: 'HU', continent: '欧洲' },
-	{ name: '奥地利', code: 'AT', continent: '欧洲' },
-	{ name: '比利时', code: 'BE', continent: '欧洲' },
-	{ name: '卢森堡', code: 'LU', continent: '欧洲' },
-	{ name: '爱尔兰', code: 'IE', continent: '欧洲' },
-	{ name: '葡萄牙', code: 'PT', continent: '欧洲' },
-	{ name: '希腊', code: 'GR', continent: '欧洲' },
-	{ name: '保加利亚', code: 'BG', continent: '欧洲' },
-	{ name: '罗马尼亚', code: 'RO', continent: '欧洲' },
-	{ name: '克罗地亚', code: 'HR', continent: '欧洲' },
-	{ name: '斯洛文尼亚', code: 'SI', continent: '欧洲' },
-	{ name: '斯洛伐克', code: 'SK', continent: '欧洲' },
-	{ name: '立陶宛', code: 'LT', continent: '欧洲' },
-	{ name: '拉脱维亚', code: 'LV', continent: '欧洲' },
-	{ name: '爱沙尼亚', code: 'EE', continent: '欧洲' },
-	{ name: '马耳他', code: 'MT', continent: '欧洲' },
-	{ name: '冰岛', code: 'IS', continent: '欧洲' },
-	{ name: '列支敦士登', code: 'LI', continent: '欧洲' },
-	{ name: '摩纳哥', code: 'MC', continent: '欧洲' },
-	{ name: '圣马力诺', code: 'SM', continent: '欧洲' },
-	{ name: '梵蒂冈', code: 'VA', continent: '欧洲' },
-	{ name: '安道尔', code: 'AD', continent: '欧洲' },
-	{ name: '白俄罗斯', code: 'BY', continent: '欧洲' },
-	{ name: '乌克兰', code: 'UA', continent: '欧洲' },
-	{ name: '摩尔多瓦', code: 'MD', continent: '欧洲' },
-	{ name: '北马其顿', code: 'MK', continent: '欧洲' },
-	{ name: '阿尔巴尼亚', code: 'AL', continent: '欧洲' },
-	{ name: '波黑', code: 'BA', continent: '欧洲' },
-	{ name: '黑山', code: 'ME', continent: '欧洲' },
-	{ name: '塞尔维亚', code: 'RS', continent: '欧洲' },
-	{ name: '科索沃', code: 'XK', continent: '欧洲' },
-
-	// 北美洲
-	{ name: '美国', code: 'US', continent: '北美洲' },
-	{ name: '加拿大', code: 'CA', continent: '北美洲' },
-	{ name: '墨西哥', code: 'MX', continent: '北美洲' },
-	{ name: '危地马拉', code: 'GT', continent: '北美洲' },
-	{ name: '伯利兹', code: 'BZ', continent: '北美洲' },
-	{ name: '萨尔瓦多', code: 'SV', continent: '北美洲' },
-	{ name: '洪都拉斯', code: 'HN', continent: '北美洲' },
-	{ name: '尼加拉瓜', code: 'NI', continent: '北美洲' },
-	{ name: '哥斯达黎加', code: 'CR', continent: '北美洲' },
-	{ name: '巴拿马', code: 'PA', continent: '北美洲' },
-	{ name: '古巴', code: 'CU', continent: '北美洲' },
-	{ name: '牙买加', code: 'JM', continent: '北美洲' },
-	{ name: '海地', code: 'HT', continent: '北美洲' },
-	{ name: '多米尼加', code: 'DO', continent: '北美洲' },
-	{ name: '特立尼达和多巴哥', code: 'TT', continent: '北美洲' },
-	{ name: '巴巴多斯', code: 'BB', continent: '北美洲' },
-	{ name: '安提瓜和巴布达', code: 'AG', continent: '北美洲' },
-	{ name: '圣基茨和尼维斯', code: 'KN', continent: '北美洲' },
-	{ name: '圣卢西亚', code: 'LC', continent: '北美洲' },
-	{ name: '圣文森特和格林纳丁斯', code: 'VC', continent: '北美洲' },
-	{ name: '格林纳达', code: 'GD', continent: '北美洲' },
-	{ name: '巴哈马', code: 'BS', continent: '北美洲' },
-	{ name: '多米尼克', code: 'DM', continent: '北美洲' },
-
-	// 南美洲
-	{ name: '巴西', code: 'BR', continent: '南美洲' },
-	{ name: '阿根廷', code: 'AR', continent: '南美洲' },
-	{ name: '智利', code: 'CL', continent: '南美洲' },
-	{ name: '哥伦比亚', code: 'CO', continent: '南美洲' },
-	{ name: '秘鲁', code: 'PE', continent: '南美洲' },
-	{ name: '委内瑞拉', code: 'VE', continent: '南美洲' },
-	{ name: '厄瓜多尔', code: 'EC', continent: '南美洲' },
-	{ name: '玻利维亚', code: 'BO', continent: '南美洲' },
-	{ name: '巴拉圭', code: 'PY', continent: '南美洲' },
-	{ name: '乌拉圭', code: 'UY', continent: '南美洲' },
-	{ name: '圭亚那', code: 'GY', continent: '南美洲' },
-	{ name: '苏里南', code: 'SR', continent: '南美洲' },
-	{ name: '法属圭亚那', code: 'GF', continent: '南美洲' },
-	{ name: '福克兰群岛', code: 'FK', continent: '南美洲' },
-
-	// 大洋洲
-	{ name: '澳大利亚', code: 'AU', continent: '大洋洲' },
-	{ name: '新西兰', code: 'NZ', continent: '大洋洲' },
-	{ name: '斐济', code: 'FJ', continent: '大洋洲' },
-	{ name: '巴布亚新几内亚', code: 'PG', continent: '大洋洲' },
-	{ name: '所罗门群岛', code: 'SB', continent: '大洋洲' },
-	{ name: '瓦努阿图', code: 'VU', continent: '大洋洲' },
-	{ name: '新喀里多尼亚', code: 'NC', continent: '大洋洲' },
-	{ name: '法属波利尼西亚', code: 'PF', continent: '大洋洲' },
-	{ name: '萨摩亚', code: 'WS', continent: '大洋洲' },
-	{ name: '汤加', code: 'TO', continent: '大洋洲' },
-	{ name: '基里巴斯', code: 'KI', continent: '大洋洲' },
-	{ name: '图瓦卢', code: 'TV', continent: '大洋洲' },
-	{ name: '瑙鲁', code: 'NR', continent: '大洋洲' },
-	{ name: '密克罗尼西亚', code: 'FM', continent: '大洋洲' },
-	{ name: '马绍尔群岛', code: 'MH', continent: '大洋洲' },
-	{ name: '帕劳', code: 'PW', continent: '大洋洲' },
-	{ name: '库克群岛', code: 'CK', continent: '大洋洲' },
-	{ name: '纽埃', code: 'NU', continent: '大洋洲' },
-	{ name: '托克劳', code: 'TK', continent: '大洋洲' },
-	{ name: '瓦利斯和富图纳', code: 'WF', continent: '大洋洲' },
-	{ name: '美属萨摩亚', code: 'AS', continent: '大洋洲' },
-	{ name: '关岛', code: 'GU', continent: '大洋洲' },
-	{ name: '北马里亚纳群岛', code: 'MP', continent: '大洋洲' },
-	{ name: '美属维尔京群岛', code: 'VI', continent: '大洋洲' },
-	{ name: '波多黎各', code: 'PR', continent: '大洋洲' },
-
-	// 非洲
-	{ name: '南非', code: 'ZA', continent: '非洲' },
-	{ name: '埃及', code: 'EG', continent: '非洲' },
-	{ name: '尼日利亚', code: 'NG', continent: '非洲' },
-	{ name: '肯尼亚', code: 'KE', continent: '非洲' },
-	{ name: '摩洛哥', code: 'MA', continent: '非洲' },
-	{ name: '阿尔及利亚', code: 'DZ', continent: '非洲' },
-	{ name: '苏丹', code: 'SD', continent: '非洲' },
-	{ name: '坦桑尼亚', code: 'TZ', continent: '非洲' },
-	{ name: '乌干达', code: 'UG', continent: '非洲' },
-	{ name: '加纳', code: 'GH', continent: '非洲' },
-	{ name: '埃塞俄比亚', code: 'ET', continent: '非洲' },
-	{ name: '安哥拉', code: 'AO', continent: '非洲' },
-	{ name: '赞比亚', code: 'ZM', continent: '非洲' },
-	{ name: '津巴布韦', code: 'ZW', continent: '非洲' },
-	{ name: '马拉维', code: 'MW', continent: '非洲' },
-	{ name: '莫桑比克', code: 'MZ', continent: '非洲' },
-	{ name: '马达加斯加', code: 'MG', continent: '非洲' },
-	{ name: '喀麦隆', code: 'CM', continent: '非洲' },
-	{ name: '科特迪瓦', code: 'CI', continent: '非洲' },
-	{ name: '布基纳法索', code: 'BF', continent: '非洲' },
-	{ name: '马里', code: 'ML', continent: '非洲' },
-	{ name: '尼日尔', code: 'NE', continent: '非洲' },
-	{ name: '乍得', code: 'TD', continent: '非洲' },
-	{ name: '塞内加尔', code: 'SN', continent: '非洲' },
-	{ name: '几内亚', code: 'GN', continent: '非洲' },
-	{ name: '塞拉利昂', code: 'SL', continent: '非洲' },
-	{ name: '利比里亚', code: 'LR', continent: '非洲' },
-	{ name: '多哥', code: 'TG', continent: '非洲' },
-	{ name: '贝宁', code: 'BJ', continent: '非洲' },
-	{ name: '卢旺达', code: 'RW', continent: '非洲' },
-	{ name: '布隆迪', code: 'BI', continent: '非洲' },
-	{ name: '吉布提', code: 'DJ', continent: '非洲' },
-	{ name: '索马里', code: 'SO', continent: '非洲' },
-	{ name: '厄立特里亚', code: 'ER', continent: '非洲' },
-	{ name: '利比亚', code: 'LY', continent: '非洲' },
-	{ name: '突尼斯', code: 'TN', continent: '非洲' },
-	{ name: '南苏丹', code: 'SS', continent: '非洲' },
-	{ name: '中非', code: 'CF', continent: '非洲' },
-	{ name: '刚果', code: 'CG', continent: '非洲' },
-	{ name: '刚果民主共和国', code: 'CD', continent: '非洲' },
-	{ name: '加蓬', code: 'GA', continent: '非洲' },
-	{ name: '赤道几内亚', code: 'GQ', continent: '非洲' },
-	{ name: '圣多美和普林西比', code: 'ST', continent: '非洲' },
-	{ name: '佛得角', code: 'CV', continent: '非洲' },
-	{ name: '塞舌尔', code: 'SC', continent: '非洲' },
-	{ name: '毛里求斯', code: 'MU', continent: '非洲' },
-	{ name: '科摩罗', code: 'KM', continent: '非洲' },
-	{ name: '马约特', code: 'YT', continent: '非洲' },
-	{ name: '留尼汪', code: 'RE', continent: '非洲' },
-	{ name: '圣赫勒拿', code: 'SH', continent: '非洲' },
-	{ name: '西撒哈拉', code: 'EH', continent: '非洲' },
-	{ name: '博茨瓦纳', code: 'BW', continent: '非洲' },
-	{ name: '纳米比亚', code: 'NA', continent: '非洲' },
-	{ name: '斯威士兰', code: 'SZ', continent: '非洲' },
-	{ name: '莱索托', code: 'LS', continent: '非洲' }
-];
+const countriesData = (regionData as any).countriesData;
 
 // 中国省份数据
-const chinaProvinces = [
-	'北京市', '天津市', '河北省', '山西省', '内蒙古自治区', '辽宁省', '吉林省', '黑龙江省',
-	'上海市', '江苏省', '浙江省', '安徽省', '福建省', '江西省', '山东省', '河南省',
-	'湖北省', '湖南省', '广东省', '广西壮族自治区', '海南省', '重庆市', '四川省', '贵州省',
-	'云南省', '西藏自治区', '陕西省', '甘肃省', '青海省', '宁夏回族自治区', '新疆维吾尔自治区',
-	'香港特别行政区', '澳门特别行政区', '台湾省'
-];
-
-// 美国州数据
-const usaStates = [
-	'阿拉巴马州', '阿拉斯加州', '亚利桑那州', '阿肯色州', '加利福尼亚州', '科罗拉多州', '康涅狄格州', '特拉华州',
-	'佛罗里达州', '佐治亚州', '夏威夷州', '爱达荷州', '伊利诺伊州', '印第安纳州', '爱荷华州', '堪萨斯州',
-	'肯塔基州', '路易斯安那州', '缅因州', '马里兰州', '马萨诸塞州', '密歇根州', '明尼苏达州', '密西西比州',
-	'密苏里州', '蒙大拿州', '内布拉斯加州', '内华达州', '新罕布什尔州', '新泽西州', '新墨西哥州', '纽约州',
-	'北卡罗来纳州', '北达科他州', '俄亥俄州', '俄克拉荷马州', '俄勒冈州', '宾夕法尼亚州', '罗得岛州', '南卡罗来纳州',
-	'南达科他州', '田纳西州', '德克萨斯州', '犹他州', '佛蒙特州', '弗吉尼亚州', '华盛顿州', '西弗吉尼亚州',
-	'威斯康星州', '怀俄明州'
-];
+const chinaProvinces = (regionData as any).chinaProvinces;
+
+// 中国主要城市数据
+const chinaCities: { [key: string]: string[] } = (regionData as any).chinaCities;
+
+// (已移除未使用的美国州静态数组)
 
 // 其他国家的主要省份/州
-const otherStates: { [key: string]: string[] } = {
-	'CA': ['安大略省', '魁北克省', '不列颠哥伦比亚省', '阿尔伯塔省', '马尼托巴省', '萨斯喀彻温省', '新斯科舍省', '新不伦瑞克省', '纽芬兰与拉布拉多省', '爱德华王子岛省', '育空地区', '西北地区', '努纳武特地区'],
-	'AU': ['新南威尔士州', '维多利亚州', '昆士兰州', '西澳大利亚州', '南澳大利亚州', '塔斯马尼亚州', '澳大利亚首都领地', '北领地'],
-	'BR': ['圣保罗州', '里约热内卢州', '米纳斯吉拉斯州', '巴伊亚州', '巴拉那州', '南里奥格兰德州', '伯南布哥州', '塞阿拉州', '帕拉州', '马托格罗索州'],
-	'IN': ['马哈拉施特拉邦', '北方邦', '比哈尔邦', '西孟加拉邦', '安得拉邦', '泰米尔纳德邦', '中央邦', '拉贾斯坦邦', '卡纳塔克邦', '古吉拉特邦'],
-	'JP': ['东京都', '大阪府', '爱知县', '神奈川县', '埼玉县', '千叶县', '兵库县', '北海道', '福冈县', '静冈县'],
-	'KR': ['首尔特别市', '釜山广域市', '大邱广域市', '仁川广域市', '光州广域市', '大田广域市', '蔚山广域市', '京畿道', '江原道', '忠清北道'],
-	'DE': ['巴伐利亚州', '巴登-符腾堡州', '北莱茵-威斯特法伦州', '下萨克森州', '黑森州', '萨克森州', '莱茵兰-普法尔茨州', '柏林州', '石勒苏益格-荷尔斯泰因州', '勃兰登堡州'],
-	'FR': ['法兰西岛大区', '奥弗涅-罗讷-阿尔卑斯大区', '新阿基坦大区', '奥克西塔尼大区', '普罗旺斯-阿尔卑斯-蓝色海岸大区', '大东部大区', '上法兰西大区', '布列塔尼大区', '卢瓦尔河地区大区', '诺曼底大区'],
-	'GB': ['英格兰', '苏格兰', '威尔士', '北爱尔兰'],
-	'IT': ['伦巴第大区', '拉齐奥大区', '坎帕尼亚大区', '西西里大区', '威尼托大区', '艾米利亚-罗马涅大区', '皮埃蒙特大区', '普利亚大区', '托斯卡纳大区', '卡拉布里亚大区'],
-	'ES': ['安达卢西亚自治区', '加泰罗尼亚自治区', '马德里自治区', '瓦伦西亚自治区', '加利西亚自治区', '卡斯蒂利亚-莱昂自治区', '巴斯克自治区', '卡斯蒂利亚-拉曼恰自治区', '穆尔西亚自治区', '阿拉贡自治区'],
-	'RU': ['莫斯科州', '圣彼得堡州', '克拉斯诺达尔边疆区', '斯维尔德洛夫斯克州', '新西伯利亚州', '下诺夫哥罗德州', '萨马拉州', '车里雅宾斯克州', '鄂木斯克州', '鞑靼斯坦共和国'],
-	'MX': ['墨西哥州', '哈利斯科州', '新莱昂州', '普埃布拉州', '韦拉克鲁斯州', '瓜纳华托州', '恰帕斯州', '米却肯州', '瓦哈卡州', '奇瓦瓦州'],
-	'AR': ['布宜诺斯艾利斯省', '科尔多瓦省', '圣菲省', '门多萨省', '图库曼省', '恩特雷里奥斯省', '萨尔塔省', '查科省', '科连特斯省', '圣地亚哥-德尔埃斯特罗省'],
-	'ZA': ['豪登省', '夸祖鲁-纳塔尔省', '西开普省', '东开普省', '自由州省', '林波波省', '普马兰加省', '北开普省', '西北省']
-};
+const otherStates: { [key: string]: string[] } = (regionData as any).otherStates;
+
+// 其他国家的主要城市数据
+const otherCities: { [key: string]: { [key: string]: string[] } } = (regionData as any).otherCities;
+
+// 美国州中文映射(ISO/英文名 -> 中文名)
+const usStateZhMap: Record<string, string> = (regionData as any).usStateZhMap;
+
+// 美国常见城市中文映射(按州代码 -> 英文名到中文名)
+const usCityZhMap: Record<string, Record<string, string>> = (regionData as any).usCityZhMap;
 
 // 初始化地区数据
 const initRegionData = () => {
@@ -340,13 +99,13 @@ const initRegionData = () => {
 		'大洋洲': [],
 		'非洲': []
 	};
-	
-	countriesData.forEach(country => {
+
+	(regionData as any).countriesData.forEach((country: any) => {
 		if (continentMap[country.continent]) {
 			continentMap[country.continent].push(country);
 		}
 	});
-	
+
 	// 转换为数组格式
 	continentOptions.value = Object.keys(continentMap).map(continent => ({
 		label: continent,
@@ -357,50 +116,130 @@ const initRegionData = () => {
 // 国家变化处理
 const onCountryChange = (countryCodeValue: string) => {
 	selectedState.value = '';
-	countryCode.value = countryCodeValue;
-	
+	selectedCity.value = '';
+	cities.value = [];
+	countryCode.value = countryCodeValue === ALL_COUNTRY ? '' : countryCodeValue;
+
 	// 根据国家代码获取省份/州
+	if (countryCodeValue === ALL_COUNTRY && props.showAll !== false) {
+		states.value = [];
+		cities.value = [];
+		return;
+	}
+
 	if (countryCodeValue === 'CN') {
-		states.value = chinaProvinces.map(name => ({ name, code: name }));
+		states.value = ((regionData as any).chinaProvinces as string[]).map(name => ({ name, code: name }));
 	} else if (countryCodeValue === 'US') {
-		states.value = usaStates.map(name => ({ name, code: name }));
-	} else if (otherStates[countryCodeValue]) {
-		states.value = otherStates[countryCodeValue].map(name => ({ name, code: name }));
+		const usStates = State.getStatesOfCountry('US') || [];
+		states.value = usStates.map((s: any) => ({ name: ((regionData as any).usStateZhMap || {})[s.name] || s.name, code: s.isoCode, enName: s.name }));
+	} else if ((regionData as any).otherStates[countryCodeValue]) {
+		states.value = (regionData as any).otherStates[countryCodeValue].map((name: string) => ({ name, code: name }));
 	} else {
 		states.value = [];
 	}
 };
 
 // 省份变化处理
-const onStateChange = (stateName: string) => {
-	// 省份选择完成
+const onStateChange = (stateCodeOrName: string) => {
+	selectedCity.value = '';
+
+	// 根据国家和省份获取城市
+	if (countryCode.value === 'CN') {
+		const stateName = stateCodeOrName === ALL_STATE ? '' : stateCodeOrName;
+		if (stateCodeOrName === ALL_STATE && props.showAll !== false) {
+			cities.value = [];
+			return;
+		}
+		if ((regionData as any).chinaCities[stateName]) {
+			cities.value = (regionData as any).chinaCities[stateName].map((name: string) => ({ name, code: name }));
+		} else {
+			cities.value = [];
+		}
+		return;
+	}
+
+	if (countryCode.value === 'US') {
+		// 加载美国州城市
+		if (stateCodeOrName === ALL_STATE && props.showAll !== false) {
+			cities.value = [];
+			return;
+		}
+		// 优先使用内置中文城市表(按州中文名)
+		const st = states.value.find((s: any) => s.code === stateCodeOrName);
+		const zhStateName = st ? st.name : '';
+		if ((regionData as any).otherCities['US'] && (regionData as any).otherCities['US'][zhStateName]) {
+			cities.value = (regionData as any).otherCities['US'][zhStateName].map((name: string) => ({ name, code: name }));
+			return;
+		}
+
+		let usCities = City.getCitiesOfState('US', stateCodeOrName) || [];
+		if (!usCities.length) {
+			// 兜底:按国家取所有城市再按州代码过滤
+			const allUsCities = City.getCitiesOfCountry('US') || [];
+			usCities = allUsCities.filter((c: any) => c.stateCode === stateCodeOrName);
+		}
+		const cityMap = ((regionData as any).usCityZhMap || {})[stateCodeOrName] || {};
+		cities.value = usCities.map((c: any) => {
+			const zhName = cityMap[c.name] || c.name;
+			return { name: zhName, code: zhName };
+		});
+		return;
+	}
+
+	const stateName = stateCodeOrName;
+	if ((regionData as any).otherCities[countryCode.value] && (regionData as any).otherCities[countryCode.value][stateName]) {
+		cities.value = (regionData as any).otherCities[countryCode.value][stateName].map((name: string) => ({ name, code: name }));
+	} else {
+		cities.value = [];
+	}
 };
 
+// (移除空的城市变更处理函数)
+
 // 添加地区
 const addRegion = () => {
-	if (!selectedCountry.value) return;
-	
-	const country = countriesData.find(c => c.code === selectedCountry.value);
-	if (!country) return;
-	
-	const regionData: RegionData = {
-		country: country.name,
-		code: country.code
+	if (!selectedCountry.value && props.showAll === false) return;
+
+	let countryName = '全部国家';
+	let countryCodeFinal = '';
+	if (selectedCountry.value && selectedCountry.value !== ALL_COUNTRY) {
+		const country = ((regionData as any).countriesData as any[]).find((c: any) => c.code === countryCode.value);
+		if (!country) return;
+		countryName = country.name;
+		countryCodeFinal = country.code;
+	}
+
+	const newRegion: RegionData = {
+		country: countryName,
+		code: countryCodeFinal
 	};
-	
-	if (selectedState.value) {
-		regionData.state = selectedState.value;
+
+	if (selectedState.value || (props.showAll !== false && selectedCountry.value && selectedState.value === ALL_STATE)) {
+		if (countryCode.value === 'US') {
+			const st = states.value.find((s: any) => s.code === selectedState.value);
+			newRegion.state = selectedState.value === ALL_STATE ? '全部省/州' : (st ? st.name : selectedState.value);
+		} else {
+			newRegion.state = selectedState.value === ALL_STATE ? '全部省/州' : selectedState.value;
+		}
 	}
-	
+
+	if (selectedCity.value || (props.showAll !== false && selectedState.value && selectedCity.value === ALL_CITY)) {
+		newRegion.city = selectedCity.value === ALL_CITY ? '全部城市' : selectedCity.value;
+	}
+
+	console.log('添加的地区数据:', newRegion);
+
 	const currentRegions = props.modelValue || [];
-	const newRegions = [...currentRegions, regionData];
+	const newRegions = [...currentRegions, newRegion];
 	
 	emit('update:modelValue', newRegions);
 	
 	// 重置选择
 	selectedCountry.value = '';
 	selectedState.value = '';
+	selectedCity.value = '';
 	states.value = [];
+	cities.value = [];
 	countryCode.value = '';
 };
 

+ 21 - 2
src/components/common/filter-select.vue

@@ -38,6 +38,7 @@ interface Props {
   disabled?: boolean
   filterable?: boolean
   wrapperClass?: string
+  showAllVersions?: boolean
 }
 
 const props = withDefaults(defineProps<Props>(), {
@@ -48,6 +49,7 @@ const props = withDefaults(defineProps<Props>(), {
   disabled: false,
   filterable: false,
   wrapperClass: '',
+  showAllVersions: false,
 })
 
 const emit = defineEmits<{
@@ -74,14 +76,24 @@ const isRemote = computed(() => true)
 
 const placeholderText = computed(() => {
   if (props.placeholder) return props.placeholder
-  return props.type === 'version' ? '全部版本' : '全部渠道'
+  if (props.type === 'version') {
+    return props.showAllVersions ? '全部版本' : '请选择版本'
+  }
+  return '全部渠道'
 })
 
 async function requestOptions(query: string): Promise<SelectOption[]> {
   if (props.type === 'version') {
     const res = await getAppVersion()
     const list: Array<string> = (res as any)?.data || []
-    return list.map((name) => ({ label: String(name), value: name }))
+    const versionOptions = list.map((name) => ({ label: String(name), value: name }))
+
+    // 如果启用了showAllVersions,在列表开头添加"全部版本"选项
+    if (props.showAllVersions) {
+      return [{ label: '全部版本', value: '' }, ...versionOptions]
+    }
+
+    return versionOptions
   }
   if (props.type === 'channel') {
     const res = await getAppChannel()
@@ -102,6 +114,13 @@ function handleRemoteSearch(query: string) {
 
 // initial fetch for remote
 requestOptions('').then((list) => (optionList.value = list))
+
+// 监听showAllVersions变化,重新获取选项
+watch(() => props.showAllVersions, () => {
+  if (props.type === 'version') {
+    requestOptions('').then((list) => (optionList.value = list))
+  }
+})
 </script>
 
 <style scoped lang="scss">

+ 435 - 0
src/components/common/region-data.json

@@ -0,0 +1,435 @@
+{
+	"countriesData": [
+		{ "name": "中国", "code": "CN", "continent": "亚洲" },
+		{ "name": "日本", "code": "JP", "continent": "亚洲" },
+		{ "name": "韩国", "code": "KR", "continent": "亚洲" },
+		{ "name": "印度", "code": "IN", "continent": "亚洲" },
+		{ "name": "新加坡", "code": "SG", "continent": "亚洲" },
+		{ "name": "泰国", "code": "TH", "continent": "亚洲" },
+		{ "name": "马来西亚", "code": "MY", "continent": "亚洲" },
+		{ "name": "印度尼西亚", "code": "ID", "continent": "亚洲" },
+		{ "name": "菲律宾", "code": "PH", "continent": "亚洲" },
+		{ "name": "越南", "code": "VN", "continent": "亚洲" },
+		{ "name": "台湾", "code": "TW", "continent": "亚洲" },
+		{ "name": "香港", "code": "HK", "continent": "亚洲" },
+		{ "name": "澳门", "code": "MO", "continent": "亚洲" },
+		{ "name": "巴基斯坦", "code": "PK", "continent": "亚洲" },
+		{ "name": "孟加拉国", "code": "BD", "continent": "亚洲" },
+		{ "name": "斯里兰卡", "code": "LK", "continent": "亚洲" },
+		{ "name": "缅甸", "code": "MM", "continent": "亚洲" },
+		{ "name": "柬埔寨", "code": "KH", "continent": "亚洲" },
+		{ "name": "老挝", "code": "LA", "continent": "亚洲" },
+		{ "name": "蒙古", "code": "MN", "continent": "亚洲" },
+		{ "name": "哈萨克斯坦", "code": "KZ", "continent": "亚洲" },
+		{ "name": "乌兹别克斯坦", "code": "UZ", "continent": "亚洲" },
+		{ "name": "吉尔吉斯斯坦", "code": "KG", "continent": "亚洲" },
+		{ "name": "塔吉克斯坦", "code": "TJ", "continent": "亚洲" },
+		{ "name": "土库曼斯坦", "code": "TM", "continent": "亚洲" },
+		{ "name": "阿富汗", "code": "AF", "continent": "亚洲" },
+		{ "name": "伊朗", "code": "IR", "continent": "亚洲" },
+		{ "name": "伊拉克", "code": "IQ", "continent": "亚洲" },
+		{ "name": "叙利亚", "code": "SY", "continent": "亚洲" },
+		{ "name": "黎巴嫩", "code": "LB", "continent": "亚洲" },
+		{ "name": "约旦", "code": "JO", "continent": "亚洲" },
+		{ "name": "以色列", "code": "IL", "continent": "亚洲" },
+		{ "name": "巴勒斯坦", "code": "PS", "continent": "亚洲" },
+		{ "name": "沙特阿拉伯", "code": "SA", "continent": "亚洲" },
+		{ "name": "阿联酋", "code": "AE", "continent": "亚洲" },
+		{ "name": "卡塔尔", "code": "QA", "continent": "亚洲" },
+		{ "name": "巴林", "code": "BH", "continent": "亚洲" },
+		{ "name": "科威特", "code": "KW", "continent": "亚洲" },
+		{ "name": "阿曼", "code": "OM", "continent": "亚洲" },
+		{ "name": "也门", "code": "YE", "continent": "亚洲" },
+		{ "name": "土耳其", "code": "TR", "continent": "亚洲" },
+		{ "name": "塞浦路斯", "code": "CY", "continent": "亚洲" },
+		{ "name": "格鲁吉亚", "code": "GE", "continent": "亚洲" },
+		{ "name": "亚美尼亚", "code": "AM", "continent": "亚洲" },
+		{ "name": "阿塞拜疆", "code": "AZ", "continent": "亚洲" },
+		{ "name": "英国", "code": "GB", "continent": "欧洲" },
+		{ "name": "法国", "code": "FR", "continent": "欧洲" },
+		{ "name": "德国", "code": "DE", "continent": "欧洲" },
+		{ "name": "意大利", "code": "IT", "continent": "欧洲" },
+		{ "name": "西班牙", "code": "ES", "continent": "欧洲" },
+		{ "name": "荷兰", "code": "NL", "continent": "欧洲" },
+		{ "name": "瑞士", "code": "CH", "continent": "欧洲" },
+		{ "name": "瑞典", "code": "SE", "continent": "欧洲" },
+		{ "name": "挪威", "code": "NO", "continent": "欧洲" },
+		{ "name": "丹麦", "code": "DK", "continent": "欧洲" },
+		{ "name": "芬兰", "code": "FI", "continent": "欧洲" },
+		{ "name": "俄罗斯", "code": "RU", "continent": "欧洲" },
+		{ "name": "波兰", "code": "PL", "continent": "欧洲" },
+		{ "name": "捷克", "code": "CZ", "continent": "欧洲" },
+		{ "name": "匈牙利", "code": "HU", "continent": "欧洲" },
+		{ "name": "奥地利", "code": "AT", "continent": "欧洲" },
+		{ "name": "比利时", "code": "BE", "continent": "欧洲" },
+		{ "name": "卢森堡", "code": "LU", "continent": "欧洲" },
+		{ "name": "爱尔兰", "code": "IE", "continent": "欧洲" },
+		{ "name": "葡萄牙", "code": "PT", "continent": "欧洲" },
+		{ "name": "希腊", "code": "GR", "continent": "欧洲" },
+		{ "name": "保加利亚", "code": "BG", "continent": "欧洲" },
+		{ "name": "罗马尼亚", "code": "RO", "continent": "欧洲" },
+		{ "name": "克罗地亚", "code": "HR", "continent": "欧洲" },
+		{ "name": "斯洛文尼亚", "code": "SI", "continent": "欧洲" },
+		{ "name": "斯洛伐克", "code": "SK", "continent": "欧洲" },
+		{ "name": "立陶宛", "code": "LT", "continent": "欧洲" },
+		{ "name": "拉脱维亚", "code": "LV", "continent": "欧洲" },
+		{ "name": "爱沙尼亚", "code": "EE", "continent": "欧洲" },
+		{ "name": "马耳他", "code": "MT", "continent": "欧洲" },
+		{ "name": "冰岛", "code": "IS", "continent": "欧洲" },
+		{ "name": "列支敦士登", "code": "LI", "continent": "欧洲" },
+		{ "name": "摩纳哥", "code": "MC", "continent": "欧洲" },
+		{ "name": "圣马力诺", "code": "SM", "continent": "欧洲" },
+		{ "name": "梵蒂冈", "code": "VA", "continent": "欧洲" },
+		{ "name": "安道尔", "code": "AD", "continent": "欧洲" },
+		{ "name": "白俄罗斯", "code": "BY", "continent": "欧洲" },
+		{ "name": "乌克兰", "code": "UA", "continent": "欧洲" },
+		{ "name": "摩尔多瓦", "code": "MD", "continent": "欧洲" },
+		{ "name": "北马其顿", "code": "MK", "continent": "欧洲" },
+		{ "name": "阿尔巴尼亚", "code": "AL", "continent": "欧洲" },
+		{ "name": "波黑", "code": "BA", "continent": "欧洲" },
+		{ "name": "黑山", "code": "ME", "continent": "欧洲" },
+		{ "name": "塞尔维亚", "code": "RS", "continent": "欧洲" },
+		{ "name": "科索沃", "code": "XK", "continent": "欧洲" },
+		{ "name": "美国", "code": "US", "continent": "北美洲" },
+		{ "name": "加拿大", "code": "CA", "continent": "北美洲" },
+		{ "name": "墨西哥", "code": "MX", "continent": "北美洲" },
+		{ "name": "危地马拉", "code": "GT", "continent": "北美洲" },
+		{ "name": "伯利兹", "code": "BZ", "continent": "北美洲" },
+		{ "name": "萨尔瓦多", "code": "SV", "continent": "北美洲" },
+		{ "name": "洪都拉斯", "code": "HN", "continent": "北美洲" },
+		{ "name": "尼加拉瓜", "code": "NI", "continent": "北美洲" },
+		{ "name": "哥斯达黎加", "code": "CR", "continent": "北美洲" },
+		{ "name": "巴拿马", "code": "PA", "continent": "北美洲" },
+		{ "name": "古巴", "code": "CU", "continent": "北美洲" },
+		{ "name": "牙买加", "code": "JM", "continent": "北美洲" },
+		{ "name": "海地", "code": "HT", "continent": "北美洲" },
+		{ "name": "多米尼加", "code": "DO", "continent": "北美洲" },
+		{ "name": "特立尼达和多巴哥", "code": "TT", "continent": "北美洲" },
+		{ "name": "巴巴多斯", "code": "BB", "continent": "北美洲" },
+		{ "name": "安提瓜和巴布达", "code": "AG", "continent": "北美洲" },
+		{ "name": "圣基茨和尼维斯", "code": "KN", "continent": "北美洲" },
+		{ "name": "圣卢西亚", "code": "LC", "continent": "北美洲" },
+		{ "name": "圣文森特和格林纳丁斯", "code": "VC", "continent": "北美洲" },
+		{ "name": "格林纳达", "code": "GD", "continent": "北美洲" },
+		{ "name": "巴哈马", "code": "BS", "continent": "北美洲" },
+		{ "name": "多米尼克", "code": "DM", "continent": "北美洲" },
+		{ "name": "巴西", "code": "BR", "continent": "南美洲" },
+		{ "name": "阿根廷", "code": "AR", "continent": "南美洲" },
+		{ "name": "智利", "code": "CL", "continent": "南美洲" },
+		{ "name": "哥伦比亚", "code": "CO", "continent": "南美洲" },
+		{ "name": "秘鲁", "code": "PE", "continent": "南美洲" },
+		{ "name": "委内瑞拉", "code": "VE", "continent": "南美洲" },
+		{ "name": "厄瓜多尔", "code": "EC", "continent": "南美洲" },
+		{ "name": "玻利维亚", "code": "BO", "continent": "南美洲" },
+		{ "name": "巴拉圭", "code": "PY", "continent": "南美洲" },
+		{ "name": "乌拉圭", "code": "UY", "continent": "南美洲" },
+		{ "name": "圭亚那", "code": "GY", "continent": "南美洲" },
+		{ "name": "苏里南", "code": "SR", "continent": "南美洲" },
+		{ "name": "法属圭亚那", "code": "GF", "continent": "南美洲" },
+		{ "name": "福克兰群岛", "code": "FK", "continent": "南美洲" },
+		{ "name": "澳大利亚", "code": "AU", "continent": "大洋洲" },
+		{ "name": "新西兰", "code": "NZ", "continent": "大洋洲" },
+		{ "name": "斐济", "code": "FJ", "continent": "大洋洲" },
+		{ "name": "巴布亚新几内亚", "code": "PG", "continent": "大洋洲" },
+		{ "name": "所罗门群岛", "code": "SB", "continent": "大洋洲" },
+		{ "name": "瓦努阿图", "code": "VU", "continent": "大洋洲" },
+		{ "name": "新喀里多尼亚", "code": "NC", "continent": "大洋洲" },
+		{ "name": "法属波利尼西亚", "code": "PF", "continent": "大洋洲" },
+		{ "name": "萨摩亚", "code": "WS", "continent": "大洋洲" },
+		{ "name": "汤加", "code": "TO", "continent": "大洋洲" },
+		{ "name": "基里巴斯", "code": "KI", "continent": "大洋洲" },
+		{ "name": "图瓦卢", "code": "TV", "continent": "大洋洲" },
+		{ "name": "瑙鲁", "code": "NR", "continent": "大洋洲" },
+		{ "name": "密克罗尼西亚", "code": "FM", "continent": "大洋洲" },
+		{ "name": "马绍尔群岛", "code": "MH", "continent": "大洋洲" },
+		{ "name": "帕劳", "code": "PW", "continent": "大洋洲" },
+		{ "name": "库克群岛", "code": "CK", "continent": "大洋洲" },
+		{ "name": "纽埃", "code": "NU", "continent": "大洋洲" },
+		{ "name": "托克劳", "code": "TK", "continent": "大洋洲" },
+		{ "name": "瓦利斯和富图纳", "code": "WF", "continent": "大洋洲" },
+		{ "name": "美属萨摩亚", "code": "AS", "continent": "大洋洲" },
+		{ "name": "关岛", "code": "GU", "continent": "大洋洲" },
+		{ "name": "北马里亚纳群岛", "code": "MP", "continent": "大洋洲" },
+		{ "name": "美属维尔京群岛", "code": "VI", "continent": "大洋洲" },
+		{ "name": "波多黎各", "code": "PR", "continent": "大洋洲" },
+		{ "name": "南非", "code": "ZA", "continent": "非洲" },
+		{ "name": "埃及", "code": "EG", "continent": "非洲" },
+		{ "name": "尼日利亚", "code": "NG", "continent": "非洲" },
+		{ "name": "肯尼亚", "code": "KE", "continent": "非洲" },
+		{ "name": "摩洛哥", "code": "MA", "continent": "非洲" },
+		{ "name": "阿尔及利亚", "code": "DZ", "continent": "非洲" },
+		{ "name": "苏丹", "code": "SD", "continent": "非洲" },
+		{ "name": "坦桑尼亚", "code": "TZ", "continent": "非洲" },
+		{ "name": "乌干达", "code": "UG", "continent": "非洲" },
+		{ "name": "加纳", "code": "GH", "continent": "非洲" },
+		{ "name": "埃塞俄比亚", "code": "ET", "continent": "非洲" },
+		{ "name": "安哥拉", "code": "AO", "continent": "非洲" },
+		{ "name": "赞比亚", "code": "ZM", "continent": "非洲" },
+		{ "name": "津巴布韦", "code": "ZW", "continent": "非洲" },
+		{ "name": "马拉维", "code": "MW", "continent": "非洲" },
+		{ "name": "莫桑比克", "code": "MZ", "continent": "非洲" },
+		{ "name": "马达加斯加", "code": "MG", "continent": "非洲" },
+		{ "name": "喀麦隆", "code": "CM", "continent": "非洲" },
+		{ "name": "科特迪瓦", "code": "CI", "continent": "非洲" },
+		{ "name": "布基纳法索", "code": "BF", "continent": "非洲" },
+		{ "name": "马里", "code": "ML", "continent": "非洲" },
+		{ "name": "尼日尔", "code": "NE", "continent": "非洲" },
+		{ "name": "乍得", "code": "TD", "continent": "非洲" },
+		{ "name": "塞内加尔", "code": "SN", "continent": "非洲" },
+		{ "name": "几内亚", "code": "GN", "continent": "非洲" },
+		{ "name": "塞拉利昂", "code": "SL", "continent": "非洲" },
+		{ "name": "利比里亚", "code": "LR", "continent": "非洲" },
+		{ "name": "多哥", "code": "TG", "continent": "非洲" },
+		{ "name": "贝宁", "code": "BJ", "continent": "非洲" },
+		{ "name": "卢旺达", "code": "RW", "continent": "非洲" },
+		{ "name": "布隆迪", "code": "BI", "continent": "非洲" },
+		{ "name": "吉布提", "code": "DJ", "continent": "非洲" },
+		{ "name": "索马里", "code": "SO", "continent": "非洲" },
+		{ "name": "厄立特里亚", "code": "ER", "continent": "非洲" },
+		{ "name": "利比亚", "code": "LY", "continent": "非洲" },
+		{ "name": "突尼斯", "code": "TN", "continent": "非洲" },
+		{ "name": "南苏丹", "code": "SS", "continent": "非洲" },
+		{ "name": "中非", "code": "CF", "continent": "非洲" },
+		{ "name": "刚果", "code": "CG", "continent": "非洲" },
+		{ "name": "刚果民主共和国", "code": "CD", "continent": "非洲" },
+		{ "name": "加蓬", "code": "GA", "continent": "非洲" },
+		{ "name": "赤道几内亚", "code": "GQ", "continent": "非洲" },
+		{ "name": "圣多美和普林西比", "code": "ST", "continent": "非洲" },
+		{ "name": "佛得角", "code": "CV", "continent": "非洲" },
+		{ "name": "塞舌尔", "code": "SC", "continent": "非洲" },
+		{ "name": "毛里求斯", "code": "MU", "continent": "非洲" },
+		{ "name": "科摩罗", "code": "KM", "continent": "非洲" },
+		{ "name": "马约特", "code": "YT", "continent": "非洲" },
+		{ "name": "留尼汪", "code": "RE", "continent": "非洲" },
+		{ "name": "圣赫勒拿", "code": "SH", "continent": "非洲" },
+		{ "name": "西撒哈拉", "code": "EH", "continent": "非洲" },
+		{ "name": "博茨瓦纳", "code": "BW", "continent": "非洲" },
+		{ "name": "纳米比亚", "code": "NA", "continent": "非洲" },
+		{ "name": "斯威士兰", "code": "SZ", "continent": "非洲" },
+		{ "name": "莱索托", "code": "LS", "continent": "非洲" }
+	],
+	"chinaProvinces": [
+		"北京市", "天津市", "河北省", "山西省", "内蒙古自治区", "辽宁省", "吉林省", "黑龙江省",
+		"上海市", "江苏省", "浙江省", "安徽省", "福建省", "江西省", "山东省", "河南省",
+		"湖北省", "湖南省", "广东省", "广西壮族自治区", "海南省", "重庆市", "四川省", "贵州省",
+		"云南省", "西藏自治区", "陕西省", "甘肃省", "青海省", "宁夏回族自治区", "新疆维吾尔自治区",
+		"香港特别行政区", "澳门特别行政区", "台湾省"
+	],
+	"chinaCities": {
+		"北京市": ["东城区", "西城区", "朝阳区", "丰台区", "石景山区", "海淀区", "门头沟区", "房山区", "通州区", "顺义区", "昌平区", "大兴区", "怀柔区", "平谷区", "密云区", "延庆区"],
+		"天津市": ["和平区", "河东区", "河西区", "南开区", "河北区", "红桥区", "东丽区", "西青区", "津南区", "北辰区", "武清区", "宝坻区", "滨海新区", "宁河区", "静海区", "蓟州区"],
+		"上海市": ["黄浦区", "徐汇区", "长宁区", "静安区", "普陀区", "虹口区", "杨浦区", "闵行区", "宝山区", "嘉定区", "浦东新区", "金山区", "松江区", "青浦区", "奉贤区", "崇明区"],
+		"重庆市": ["万州区", "涪陵区", "渝中区", "大渡口区", "江北区", "沙坪坝区", "九龙坡区", "南岸区", "北碚区", "綦江区", "大足区", "渝北区", "巴南区", "黔江区", "长寿区", "江津区", "合川区", "永川区", "南川区", "璧山区", "铜梁区", "潼南区", "荣昌区", "开州区", "梁平区", "武隆区"],
+		"河北省": ["石家庄市", "唐山市", "秦皇岛市", "邯郸市", "邢台市", "保定市", "张家口市", "承德市", "沧州市", "廊坊市", "衡水市"],
+		"山西省": ["太原市", "大同市", "阳泉市", "长治市", "晋城市", "朔州市", "晋中市", "运城市", "忻州市", "临汾市", "吕梁市"],
+		"内蒙古自治区": ["呼和浩特市", "包头市", "乌海市", "赤峰市", "通辽市", "鄂尔多斯市", "呼伦贝尔市", "巴彦淖尔市", "乌兰察布市", "兴安盟", "锡林郭勒盟", "阿拉善盟"],
+		"辽宁省": ["沈阳市", "大连市", "鞍山市", "抚顺市", "本溪市", "丹东市", "锦州市", "营口市", "阜新市", "辽阳市", "盘锦市", "铁岭市", "朝阳市", "葫芦岛市"],
+		"吉林省": ["长春市", "吉林市", "四平市", "辽源市", "通化市", "白山市", "松原市", "白城市", "延边朝鲜族自治州"],
+		"黑龙江省": ["哈尔滨市", "齐齐哈尔市", "鸡西市", "鹤岗市", "双鸭山市", "大庆市", "伊春市", "佳木斯市", "七台河市", "牡丹江市", "黑河市", "绥化市", "大兴安岭地区"],
+		"江苏省": ["南京市", "无锡市", "徐州市", "常州市", "苏州市", "南通市", "连云港市", "淮安市", "盐城市", "扬州市", "镇江市", "泰州市", "宿迁市"],
+		"浙江省": ["杭州市", "宁波市", "温州市", "嘉兴市", "湖州市", "绍兴市", "金华市", "衢州市", "舟山市", "台州市", "丽水市"],
+		"安徽省": ["合肥市", "芜湖市", "蚌埠市", "淮南市", "马鞍山市", "淮北市", "铜陵市", "安庆市", "黄山市", "滁州市", "阜阳市", "宿州市", "六安市", "亳州市", "池州市", "宣城市"],
+		"福建省": ["福州市", "厦门市", "莆田市", "三明市", "泉州市", "漳州市", "南平市", "龙岩市", "宁德市"],
+		"江西省": ["南昌市", "景德镇市", "萍乡市", "九江市", "新余市", "鹰潭市", "赣州市", "吉安市", "宜春市", "抚州市", "上饶市"],
+		"山东省": ["济南市", "青岛市", "淄博市", "枣庄市", "东营市", "烟台市", "潍坊市", "济宁市", "泰安市", "威海市", "日照市", "临沂市", "德州市", "聊城市", "滨州市", "菏泽市"],
+		"河南省": ["郑州市", "开封市", "洛阳市", "平顶山市", "安阳市", "鹤壁市", "新乡市", "焦作市", "濮阳市", "许昌市", "漯河市", "三门峡市", "南阳市", "商丘市", "信阳市", "周口市", "驻马店市", "济源市"],
+		"湖北省": ["武汉市", "黄石市", "十堰市", "宜昌市", "襄阳市", "鄂州市", "荆门市", "孝感市", "荆州市", "黄冈市", "咸宁市", "随州市", "恩施土家族苗族自治州", "仙桃市", "潜江市", "天门市", "神农架林区"],
+		"湖南省": ["长沙市", "株洲市", "湘潭市", "衡阳市", "邵阳市", "岳阳市", "常德市", "张家界市", "益阳市", "郴州市", "永州市", "怀化市", "娄底市", "湘西土家族苗族自治州"],
+		"广东省": ["广州市", "韶关市", "深圳市", "珠海市", "汕头市", "佛山市", "江门市", "湛江市", "茂名市", "肇庆市", "惠州市", "梅州市", "汕尾市", "河源市", "阳江市", "清远市", "东莞市", "中山市", "潮州市", "揭阳市", "云浮市"],
+		"广西壮族自治区": ["南宁市", "柳州市", "桂林市", "梧州市", "北海市", "防城港市", "钦州市", "贵港市", "玉林市", "百色市", "贺州市", "河池市", "来宾市", "崇左市"],
+		"海南省": ["海口市", "三亚市", "三沙市", "儋州市", "五指山市", "琼海市", "文昌市", "万宁市", "东方市", "定安县", "屯昌县", "澄迈县", "临高县", "白沙黎族自治县", "昌江黎族自治县", "乐东黎族自治县", "陵水黎族自治县", "保亭黎族苗族自治县", "琼中黎族苗族自治县"],
+		"四川省": ["成都市", "自贡市", "攀枝花市", "泸州市", "德阳市", "绵阳市", "广元市", "遂宁市", "内江市", "乐山市", "南充市", "眉山市", "宜宾市", "广安市", "达州市", "雅安市", "巴中市", "资阳市", "阿坝藏族羌族自治州", "甘孜藏族自治州", "凉山彝族自治州"],
+		"贵州省": ["贵阳市", "六盘水市", "遵义市", "安顺市", "毕节市", "铜仁市", "黔西南布依族苗族自治州", "黔东南苗族侗族自治州", "黔南布依族苗族自治州"],
+		"云南省": ["昆明市", "曲靖市", "玉溪市", "保山市", "昭通市", "丽江市", "普洱市", "临沧市", "楚雄彝族自治州", "红河哈尼族彝族自治州", "文山壮族苗族自治州", "西双版纳傣族自治州", "大理白族自治州", "德宏傣族景颇族自治州", "怒江傈僳族自治州", "迪庆藏族自治州"],
+		"西藏自治区": ["拉萨市", "日喀则市", "昌都市", "林芝市", "山南市", "那曲市", "阿里地区"],
+		"陕西省": ["西安市", "铜川市", "宝鸡市", "咸阳市", "渭南市", "延安市", "汉中市", "榆林市", "安康市", "商洛市"],
+		"甘肃省": ["兰州市", "嘉峪关市", "金昌市", "白银市", "天水市", "武威市", "张掖市", "平凉市", "酒泉市", "庆阳市", "定西市", "陇南市", "临夏回族自治州", "甘南藏族自治州"],
+		"青海省": ["西宁市", "海东市", "海北藏族自治州", "黄南藏族自治州", "海南藏族自治州", "果洛藏族自治州", "玉树藏族自治州", "海西蒙古族藏族自治州"],
+		"宁夏回族自治区": ["银川市", "石嘴山市", "吴忠市", "固原市", "中卫市"],
+		"新疆维吾尔自治区": ["乌鲁木齐市", "克拉玛依市", "吐鲁番市", "哈密市", "昌吉回族自治州", "博尔塔拉蒙古自治州", "巴音郭楞蒙古自治州", "阿克苏地区", "克孜勒苏柯尔克孜自治州", "喀什地区", "和田地区", "伊犁哈萨克自治州", "塔城地区", "阿勒泰地区", "石河子市", "阿拉尔市", "图木舒克市", "五家渠市", "北屯市", "铁门关市", "双河市", "可克达拉市", "昆玉市", "胡杨河市"],
+		"香港特别行政区": ["中西区", "湾仔区", "东区", "南区", "油尖旺区", "深水埗区", "九龙城区", "黄大仙区", "观塘区", "荃湾区", "屯门区", "元朗区", "北区", "大埔区", "沙田区", "西贡区", "葵青区", "离岛区"],
+		"澳门特别行政区": ["花地玛堂区", "圣安多尼堂区", "大堂区", "望德堂区", "风顺堂区", "嘉模堂区", "圣方济各堂区", "路氹城"],
+		"台湾省": ["台北市", "新北市", "桃园市", "台中市", "台南市", "高雄市", "基隆市", "新竹市", "嘉义市", "新竹县", "苗栗县", "彰化县", "南投县", "云林县", "嘉义县", "屏东县", "宜兰县", "花莲县", "台东县", "澎湖县", "金门县", "连江县"]
+	},
+	"otherStates": {
+		"CA": ["安大略省", "魁北克省", "不列颠哥伦比亚省", "阿尔伯塔省", "马尼托巴省", "萨斯喀彻温省", "新斯科舍省", "新不伦瑞克省", "纽芬兰与拉布拉多省", "爱德华王子岛省", "育空地区", "西北地区", "努纳武特地区"],
+		"AU": ["新南威尔士州", "维多利亚州", "昆士兰州", "西澳大利亚州", "南澳大利亚州", "塔斯马尼亚州", "澳大利亚首都领地", "北领地"],
+		"BR": ["圣保罗州", "里约热内卢州", "米纳斯吉拉斯州", "巴伊亚州", "巴拉那州", "南里奥格兰德州", "伯南布哥州", "塞阿拉州", "帕拉州", "马托格罗索州"],
+		"IN": ["马哈拉施特拉邦", "北方邦", "比哈尔邦", "西孟加拉邦", "安得拉邦", "泰米尔纳德邦", "中央邦", "拉贾斯坦邦", "卡纳塔克邦", "古吉拉特邦"],
+		"JP": ["东京都", "大阪府", "爱知县", "神奈川县", "埼玉县", "千叶县", "兵库县", "北海道", "福冈县", "静冈县"],
+		"KR": ["首尔特别市", "釜山广域市", "大邱广域市", "仁川广域市", "光州广域市", "大田广域市", "蔚山广域市", "京畿道", "江原道", "忠清北道"],
+		"DE": ["巴伐利亚州", "巴登-符腾堡州", "北莱茵-威斯特法伦州", "下萨克森州", "黑森州", "萨克森州", "莱茵兰-普法尔茨州", "柏林州", "石勒苏益格-荷尔斯泰因州", "勃兰登堡州"],
+		"FR": ["法兰西岛大区", "奥弗涅-罗讷-阿尔卑斯大区", "新阿基坦大区", "奥克西塔尼大区", "普罗旺斯-阿尔卑斯-蓝色海岸大区", "大东部大区", "上法兰西大区", "布列塔尼大区", "卢瓦尔河地区大区", "诺曼底大区"],
+		"GB": ["英格兰", "苏格兰", "威尔士", "北爱尔兰"],
+		"IT": ["伦巴第大区", "拉齐奥大区", "坎帕尼亚大区", "西西里大区", "威尼托大区", "艾米利亚-罗马涅大区", "皮埃蒙特大区", "普利亚大区", "托斯卡纳大区", "卡拉布里亚大区"],
+		"ES": ["安达卢西亚自治区", "加泰罗尼亚自治区", "马德里自治区", "瓦伦西亚自治区", "加利西亚自治区", "卡斯蒂利亚-莱昂自治区", "巴斯克自治区", "卡斯蒂利亚-拉曼恰自治区", "穆尔西亚自治区", "阿拉贡自治区"],
+		"RU": ["莫斯科州", "圣彼得堡州", "克拉斯诺达尔边疆区", "斯维尔德洛夫斯克州", "新西伯利亚州", "下诺夫哥罗德州", "萨马拉州", "车里雅宾斯克州", "鄂木斯克州", "鞑靼斯坦共和国"],
+		"MX": ["墨西哥州", "哈利斯科州", "新莱昂州", "普埃布拉州", "韦拉克鲁斯州", "瓜纳华托州", "恰帕斯州", "米却肯州", "瓦哈卡州", "奇瓦瓦州"],
+		"AR": ["布宜诺斯艾利斯省", "科尔多瓦省", "圣菲省", "门多萨省", "图库曼省", "恩特雷里奥斯省", "萨尔塔省", "查科省", "科连特斯省", "圣地亚哥-德尔埃斯特罗省"],
+		"ZA": ["豪登省", "夸祖鲁-纳塔尔省", "西开普省", "东开普省", "自由州省", "林波波省", "普马兰加省", "北开普省", "西北省"]
+	},
+	"otherCities": {
+		"US": {
+			"加利福尼亚州": ["洛杉矶", "旧金山", "圣迭戈", "圣何塞", "奥克兰", "长滩", "弗雷斯诺", "萨克拉门托", "河滨", "斯托克顿"],
+			"纽约州": ["纽约市", "布法罗", "罗切斯特", "扬克斯", "锡拉丘兹", "奥尔巴尼", "新罗谢尔", "芒特弗农", "斯克内克塔迪", "尤蒂卡"],
+			"德克萨斯州": ["休斯顿", "圣安东尼奥", "达拉斯", "奥斯汀", "沃思堡", "埃尔帕索", "阿灵顿", "科珀斯克里斯蒂", "普莱诺", "拉雷多"],
+			"佛罗里达州": ["杰克逊维尔", "迈阿密", "坦帕", "奥兰多", "圣彼得堡", "海厄利亚", "塔拉哈西", "劳德代尔堡", "圣露西港", "彭布罗克派恩斯"],
+			"伊利诺伊州": ["芝加哥", "奥罗拉", "罗克福德", "乔利埃特", "内珀维尔", "斯普林菲尔德", "皮奥里亚", "埃尔金", "沃基根", "西塞罗"]
+		},
+		"CA": {
+			"安大略省": ["多伦多", "渥太华", "汉密尔顿", "伦敦", "基奇纳", "温莎", "奥沙瓦", "巴里", "金斯顿", "圭尔夫"],
+			"魁北克省": ["蒙特利尔", "魁北克市", "拉瓦尔", "加蒂诺", "朗格伊", "舍布鲁克", "萨格奈", "莱维", "特鲁瓦-里维耶尔", "三河市"],
+			"不列颠哥伦比亚省": ["温哥华", "萨里", "本那比", "里士满", "阿伯茨福德", "高贵林", "萨尼奇", "兰里", "三角洲", "基洛纳"]
+		},
+		"AU": {
+			"新南威尔士州": ["悉尼", "纽卡斯尔", "伍伦贡", "梅特兰", "奥尔伯里", "塔姆沃思", "巴拉瑞特", "本迪戈", "谢珀顿", "米尔杜拉"],
+			"维多利亚州": ["墨尔本", "吉朗", "巴拉瑞特", "本迪戈", "谢珀顿", "米尔杜拉", "沃东加", "特鲁拉尔贡", "弗兰克斯顿", "丹德农"],
+			"昆士兰州": ["布里斯班", "黄金海岸", "汤斯维尔", "凯恩斯", "图文巴", "罗克汉普顿", "麦凯", "班达伯格", "赫维湾", "格拉德斯通"]
+		},
+		"JP": {
+			"东京都": ["千代田区", "中央区", "港区", "新宿区", "文京区", "台东区", "墨田区", "江东区", "品川区", "目黑区", "大田区", "世田谷区", "涩谷区", "中野区", "杉并区", "丰岛区", "北区", "荒川区", "板桥区", "练马区", "足立区", "葛饰区", "江户川区"],
+			"大阪府": ["大阪市", "堺市", "东大阪市", "枚方市", "八尾市", "吹田市", "茨木市", "高槻市", "松原市", "大东市", "和泉市", "箕面市", "柏原市", "羽曳野市", "门真市", "摄津市", "高石市", "藤井寺市", "东大阪市", "泉佐野市"],
+			"爱知县": ["名古屋市", "丰田市", "冈崎市", "一宫市", "春日井市", "安城市", "丰桥市", "西尾市", "刈谷市", "小牧市", "稻泽市", "新城市", "大府市", "知多市", "知立市", "尾张旭市", "高滨市", "岩仓市", "丰明市", "日进市"]
+		},
+		"KR": {
+			"首尔特别市": ["钟路区", "中区", "龙山区", "城东区", "广津区", "东大门区", "中浪区", "城北区", "江北区", "道峰区", "芦原区", "恩平区", "西大门区", "麻浦区", "阳川区", "江西区", "九老区", "衿川区", "永登浦区", "铜雀区", "冠岳区", "瑞草区", "江南区", "松坡区", "江东区"],
+			"釜山广域市": ["中区", "西区", "东区", "影岛区", "釜山镇区", "东莱区", "南区", "北区", "海云台区", "机张郡", "沙下区", "金井区", "莲堤区", "水营区", "沙上区", "江西区", "甘川区", "东区", "南区", "北区"],
+			"京畿道": ["水原市", "城南市", "安养市", "富川市", "光明市", "平泽市", "东豆川市", "安山市", "高阳市", "果川市", "南杨州市", "乌山市", "华城市", "始兴市", "军浦市", "义王市", "河南市", "龙仁市", "坡州市", "利川市", "安城市", "金浦市", "加平郡", "涟川郡", "抱川市", "杨平郡", "骊州郡", "杨州市"]
+		},
+		"DE": {
+			"巴伐利亚州": ["慕尼黑", "纽伦堡", "奥格斯堡", "雷根斯堡", "因戈尔施塔特", "维尔茨堡", "福希海姆", "施韦因富特", "兰茨胡特", "埃尔朗根"],
+			"北莱茵-威斯特法伦州": ["科隆", "杜塞尔多夫", "多特蒙德", "埃森", "杜伊斯堡", "波鸿", "伍珀塔尔", "比勒费尔德", "波恩", "明斯特"],
+			"巴登-符腾堡州": ["斯图加特", "曼海姆", "卡尔斯鲁厄", "弗赖堡", "海德堡", "海尔布隆", "乌尔姆", "普福尔茨海姆", "罗伊特林根", "康斯坦茨"]
+		},
+		"FR": {
+			"法兰西岛大区": ["巴黎", "布洛涅-比扬古", "圣但尼", "阿让特伊", "蒙特勒伊", "伊夫里", "万塞讷", "维特里-塞纳河畔", "克雷泰伊", "圣莫里斯"],
+			"奥弗涅-罗讷-阿尔卑斯大区": ["里昂", "圣艾蒂安", "格勒诺布尔", "维勒班", "瓦朗斯", "尚贝里", "阿讷西", "布尔格-昂布雷斯", "罗阿讷", "圣普列斯特"],
+			"普罗旺斯-阿尔卑斯-蓝色海岸大区": ["马赛", "尼斯", "土伦", "艾克斯-普罗旺斯", "阿维尼翁", "安提布", "戛纳", "拉西约塔", "弗雷瑞斯", "德拉吉尼昂"]
+		},
+		"GB": {
+			"英格兰": ["伦敦", "伯明翰", "曼彻斯特", "利物浦", "利兹", "谢菲尔德", "布里斯托尔", "莱斯特", "考文垂", "诺丁汉", "布拉德福德", "赫尔", "纽卡斯尔", "斯托克", "南安普顿", "德比", "朴茨茅斯", "普利茅斯", "卢顿", "伍尔弗汉普顿"],
+			"苏格兰": ["爱丁堡", "格拉斯哥", "阿伯丁", "邓迪", "斯特灵", "因弗内斯", "珀斯", "埃尔金", "邓弗姆林", "基尔马诺克"],
+			"威尔士": ["卡迪夫", "斯旺西", "纽波特", "雷克瑟姆", "巴里", "卡马森", "阿伯里斯特威斯", "班戈", "康威", "兰迪德诺"]
+		},
+		"IT": {
+			"伦巴第大区": ["米兰", "布雷西亚", "贝加莫", "蒙扎", "科莫", "帕维亚", "瓦雷泽", "克雷莫纳", "曼图亚", "莱科"],
+			"拉齐奥大区": ["罗马", "拉蒂纳", "维泰博", "弗罗西诺内", "列蒂", "奇维塔韦基亚", "费伦蒂诺", "阿纳尼", "卡西诺", "泰拉奇纳"],
+			"坎帕尼亚大区": ["那不勒斯", "萨勒诺", "卡塞塔", "阿韦利诺", "贝内文托", "波佐利", "托雷德尔格雷科", "朱利亚诺", "阿切拉", "卡斯特拉马雷迪斯塔比亚"]
+		}
+	},
+	"usStateZhMap": {
+		"Alabama": "阿拉巴马州",
+		"Alaska": "阿拉斯加州",
+		"Arizona": "亚利桑那州",
+		"Arkansas": "阿肯色州",
+		"California": "加利福尼亚州",
+		"Colorado": "科罗拉多州",
+		"Connecticut": "康涅狄格州",
+		"Delaware": "特拉华州",
+		"District of Columbia": "哥伦比亚特区",
+		"Florida": "佛罗里达州",
+		"Georgia": "佐治亚州",
+		"Hawaii": "夏威夷州",
+		"Idaho": "爱达荷州",
+		"Illinois": "伊利诺伊州",
+		"Indiana": "印第安纳州",
+		"Iowa": "爱荷华州",
+		"Kansas": "堪萨斯州",
+		"Kentucky": "肯塔基州",
+		"Louisiana": "路易斯安那州",
+		"Maine": "缅因州",
+		"Maryland": "马里兰州",
+		"Massachusetts": "马萨诸塞州",
+		"Michigan": "密歇根州",
+		"Minnesota": "明尼苏达州",
+		"Mississippi": "密西西比州",
+		"Missouri": "密苏里州",
+		"Montana": "蒙大拿州",
+		"Nebraska": "内布拉斯加州",
+		"Nevada": "内华达州",
+		"New Hampshire": "新罕布什尔州",
+		"New Jersey": "新泽西州",
+		"New Mexico": "新墨西哥州",
+		"New York": "纽约州",
+		"North Carolina": "北卡罗来纳州",
+		"North Dakota": "北达科他州",
+		"Ohio": "俄亥俄州",
+		"Oklahoma": "俄克拉荷马州",
+		"Oregon": "俄勒冈州",
+		"Pennsylvania": "宾夕法尼亚州",
+		"Rhode Island": "罗得岛州",
+		"South Carolina": "南卡罗来纳州",
+		"South Dakota": "南达科他州",
+		"Tennessee": "田纳西州",
+		"Texas": "德克萨斯州",
+		"Utah": "犹他州",
+		"Vermont": "佛蒙特州",
+		"Virginia": "弗吉尼亚州",
+		"Washington": "华盛顿州",
+		"West Virginia": "西弗吉尼亚州",
+		"Wisconsin": "威斯康星州",
+		"Wyoming": "怀俄明州"
+	},
+	"usCityZhMap": {
+		"CA": {
+			"Los Angeles": "洛杉矶",
+			"San Francisco": "旧金山",
+			"San Diego": "圣迭戈",
+			"San Jose": "圣何塞",
+			"Oakland": "奥克兰",
+			"Long Beach": "长滩",
+			"Fresno": "弗雷斯诺",
+			"Sacramento": "萨克拉门托",
+			"Riverside": "河滨",
+			"Stockton": "斯托克顿"
+		},
+		"NY": {
+			"New York City": "纽约市",
+			"Buffalo": "布法罗",
+			"Rochester": "罗切斯特",
+			"Yonkers": "扬克斯",
+			"Syracuse": "锡拉丘兹",
+			"Albany": "奥尔巴尼",
+			"New Rochelle": "新罗谢尔",
+			"Mount Vernon": "芒特弗农",
+			"Schenectady": "斯克内克塔迪",
+			"Utica": "尤蒂卡"
+		},
+		"TX": {
+			"Houston": "休斯顿",
+			"San Antonio": "圣安东尼奥",
+			"Dallas": "达拉斯",
+			"Austin": "奥斯汀",
+			"Fort Worth": "沃思堡",
+			"El Paso": "埃尔帕索",
+			"Arlington": "阿灵顿",
+			"Corpus Christi": "科珀斯克里斯蒂",
+			"Plano": "普莱诺",
+			"Laredo": "拉雷多"
+		},
+		"FL": {
+			"Jacksonville": "杰克逊维尔",
+			"Miami": "迈阿密",
+			"Tampa": "坦帕",
+			"Orlando": "奥兰多",
+			"St. Petersburg": "圣彼得堡",
+			"Hialeah": "海厄利亚",
+			"Tallahassee": "塔拉哈西",
+			"Fort Lauderdale": "劳德代尔堡",
+			"Port St. Lucie": "圣露西港",
+			"Pembroke Pines": "彭布罗克派恩斯"
+		},
+		"IL": {
+			"Chicago": "芝加哥",
+			"Aurora": "奥罗拉",
+			"Rockford": "罗克福德",
+			"Joliet": "乔利埃特",
+			"Naperville": "内珀维尔",
+			"Springfield": "斯普林菲尔德",
+			"Peoria": "皮奥里亚",
+			"Elgin": "埃尔金",
+			"Waukegan": "沃基根",
+			"Cicero": "西塞罗"
+		}
+	}
+}

+ 1 - 1
src/views/count/user/activations/components/initiateNumber.vue

@@ -1,5 +1,5 @@
 <template>
-	<div class="el-card p-9 mt-3">
+	<div class="el-card p-14 mt-3">
 		<!-- 新增趋势 -->
 		<div class="">
 			<div class="flex items-center justify-between mb-2 mt-3">

+ 1 - 1
src/views/count/user/activeUser/components/AddTrend.vue

@@ -1,5 +1,5 @@
 <template>
-	<div class="el-card p-9">
+	<div class="el-card p-14">
 		<!-- 新增趋势 -->
 		<div class="">
 			<div class="flex items-center justify-between mb-2 mt-3">

+ 182 - 0
src/views/count/user/versionDistribution/components/version-source-card.vue

@@ -0,0 +1,182 @@
+<template>
+	<div class="mt-2 el-card p-14">
+		<div class="flex justify-between">
+			<Title left-line :title="'版本用户来源'">
+				<template #default>
+					<el-popover class="box-item" placement="right" trigger="hover" width="300">
+						<template #reference>
+							<el-icon class="ml-1" style="color: #a4b8cf"><QuestionFilled /></el-icon>
+						</template>
+						<template #default>
+							<div class="ant-popover-inner-content">
+								<div class="um-page-tips-content" style="line-height: 24px">
+									<p><span class="highlight">新增用户:</span><span>第一次启动应用的用户(以设备为判断标准)</span></p>
+									<p>
+										<span class="highlight">活跃用户:</span><span>启动过应用的用户(去重),启动过一次的用户即视为活跃用户,包括新用户与老用户</span>
+									</p>
+									<p>
+										<span class="highlight">启动次数:</span
+										><span
+											>打开应用视为启动。完全退出或后台运行超过30s后再次进入应用,视为一次新启动。开发过程中可以通过setSessionContinueMills来自定义两次启动的间隔,默认30s</span
+										>
+									</p>
+									<p><span class="highlight">升级用户:</span><span>从其他版本升级到该版本的用户(以设备为判断标准)</span></p>
+									<p>
+										<span
+											>如果当日用户先启动老版本然后升级到新版本,分版本查看数据时,此用户在新老版本都会被算为活跃用户(按总体查看数据时不受影响)</span
+										>
+									</p>
+								</div>
+							</div>
+						</template>
+					</el-popover>
+				</template>
+			</Title>
+		</div>
+		<div class="flex items-center justify-between mt-3">
+			<el-select v-model="channelDistribution" style="width: 180px" placeholder="新增用户渠道分布">
+				<el-option label="新增用户渠道分布" value="distribution" />
+			</el-select>
+			<div class="ml-4">
+				<el-radio-group v-model="timeRange">
+					<el-radio-button label="yesterday">昨天</el-radio-button>
+					<el-radio-button label="7days">过去7天</el-radio-button>
+					<el-radio-button label="30days">过去30天</el-radio-button>
+				</el-radio-group>
+			</div>
+		</div>
+
+		<!-- 水平柱状图 -->
+		<div class="mt-4">
+			<div class="relative">
+				<div ref="barChartRef" style="width: 100%; height: 200px"></div>
+			</div>
+		</div>
+
+		<!-- 明细表格 -->
+		<div class="mt-4">
+			<div class="flex items-center justify-between mb-2">
+				<div class="text-base font-medium cursor-pointer select-none ml-3 items-center flex text-[#167AF0]" @click="showDetail = !showDetail">
+					{{ showDetail ? '收起明细数据' : '展开明细数据' }} <el-icon class="ml-2"><ArrowDown v-if="showDetail" /> <ArrowUp v-else /> </el-icon>
+				</div>
+
+				<div>
+					<el-button>导出</el-button>
+				</div>
+			</div>
+			<el-table v-if="showDetail" :data="pagedSourceRows" border>
+				<el-table-column prop="channel" label="渠道" min-width="140" />
+				<el-table-column prop="newUsers" label="新增用户" min-width="140" />
+				<el-table-column prop="upgradeRatio" label="升级用户比例" min-width="140" />
+			</el-table>
+			<div v-if="showDetail" class="flex justify-end mt-3">
+				<el-pagination
+					v-model:current-page="sourcePage"
+					v-model:page-size="sourcePageSize"
+					background
+					layout="total, prev, pager, next, sizes"
+					:total="sourceRows.length"
+					:page-sizes="[5, 10, 20]"
+				/>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, watch, computed, defineAsyncComponent } from 'vue';
+import * as echarts from 'echarts';
+import { useI18n } from 'vue-i18n';
+import { QuestionFilled } from '@element-plus/icons-vue';
+
+const { t } = useI18n();
+
+const props = defineProps<{
+	selectedVersion?: string;
+}>();
+
+const channelDistribution = ref('distribution');
+const timeRange = ref('yesterday');
+const barChartRef = ref<HTMLDivElement | null>(null);
+let barChartInstance: echarts.ECharts | null = null;
+
+const sourceRows = ref<Array<{ channel: string; newUsers: number; upgradeRatio: string }>>([
+	{ channel: '渠道A', newUsers: 100, upgradeRatio: '10%' },
+	{ channel: '渠道B', newUsers: 80, upgradeRatio: '15%' },
+	{ channel: '渠道C', newUsers: 70, upgradeRatio: '20%' },
+	{ channel: '渠道D', newUsers: 60, upgradeRatio: '25%' },
+	{ channel: '渠道E', newUsers: 50, upgradeRatio: '30%' },
+	{ channel: '渠道F', newUsers: 40, upgradeRatio: '35%' },
+	{ channel: '渠道G', newUsers: 30, upgradeRatio: '40%' },
+	{ channel: '渠道H', newUsers: 20, upgradeRatio: '45%' },
+	{ channel: '渠道I', newUsers: 10, upgradeRatio: '50%' },
+	{ channel: '渠道J', newUsers: 5, upgradeRatio: '55%' },
+]);
+
+function initBarChart(): void {
+	if (!barChartRef.value) return;
+	if (barChartInstance) barChartInstance.dispose();
+	barChartInstance = echarts.init(barChartRef.value);
+	const option: echarts.EChartsOption = {
+		tooltip: { trigger: 'axis' },
+		grid: { left: 40, right: 20, top: 20, bottom: 30 },
+		xAxis: {
+			type: 'value',
+			axisLine: { show: false },
+			splitLine: { lineStyle: { color: '#f3f4f6' } },
+			axisLabel: { color: '#6b7280' },
+		},
+		yAxis: {
+			type: 'category',
+			data: sourceRows.value.map((d) => d.channel),
+			axisLine: { lineStyle: { color: '#e5e7eb' } },
+			axisLabel: { color: '#6b7280' },
+			axisTick: { alignWithLabel: true },
+		},
+		series: [
+			{
+				name: '新增用户',
+				type: 'bar',
+				barWidth: '60%',
+				itemStyle: { color: '#409EFF' },
+				data: sourceRows.value.map((d) => d.newUsers),
+			},
+		],
+	};
+	barChartInstance.setOption(option);
+}
+
+onMounted(() => {
+	initBarChart();
+});
+
+watch(timeRange, () => {
+	initBarChart();
+});
+
+watch(
+	() => props.selectedVersion,
+	() => {
+		initBarChart();
+	}
+);
+
+const sourcePage = ref(1);
+const sourcePageSize = ref(5);
+const pagedSourceRows = computed(() => {
+	const startIndex = (sourcePage.value - 1) * sourcePageSize.value;
+
+	return sourceRows.value.slice(startIndex, startIndex + sourcePageSize.value);
+});
+
+const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
+const showDetail = ref(true);
+</script>
+
+<style lang="scss" scoped>
+.highlight {
+	color: #2196f3;
+}
+</style>
+
+

+ 629 - 0
src/views/count/user/versionDistribution/components/version-trend-card.vue

@@ -0,0 +1,629 @@
+<template>
+	<div class="mt-2 el-card  p-4">
+		<Title left-line
+			:title="selectedVersionLocal === '' ? t('versionDistribution.allVersion') : selectedVersionLocal + t('versionDistribution.version')">
+			<template #default>
+				<el-popover class="box-item" placement="right" trigger="hover" width="300">
+					<template #reference>
+						<el-icon class="ml-1" style="color: #a4b8cf">
+							<QuestionFilled />
+						</el-icon>
+					</template>
+					<template #default>
+						<div class="ant-popover-inner-content">
+							<div class="um-page-tips-content" style="line-height: 24px">
+								<p><span>趋势图展示累计用户排名Top10版本的变化趋势</span></p>
+								<p><span class="highlight">新增用户:</span><span>第一次启动应用的用户(以设备为判断标准)</span></p>
+								<p>
+									<span
+										class="highlight">活跃用户:</span><span>启动过应用的用户(去重),启动过一次的用户即视为活跃用户,包括新用户与老用户</span>
+								</p>
+								<p>
+									<span
+										class="highlight">启动次数:</span><span>打开应用视为启动。完全退出或后台运行超过30s后再次进入应用,视为一次新启动。开发过程中可以通过setSessionContinueMills来自定义两次启动的间隔,默认30s</span>
+								</p>
+								<p>
+									<span
+										class="highlight">版本累计用户(%):</span><span>截止到现在,该版本的累计用户(占累计用户全体的比例);若该版本的用户升级到其他版本,则累计用户会减少</span>
+								</p>
+								<p><span class="highlight">升级用户:</span><span>从其他版本升级到该版本的用户(以设备为判断标准)</span></p>
+								<p>
+									<span>如果当日用户先启动老版本然后升级到新版本,分版本查看数据时,此用户在新老版本都会被算为活跃用户(按总体查看数据时不受影响)</span>
+								</p>
+							</div>
+						</div>
+					</template>
+				</el-popover>
+			</template>
+		</Title>
+		<div class=" p-10">
+			<div class="flex items-center justify-between mb-2 mt-3">
+				<div class="flex items-center">
+					<el-select v-if="selectedVersionLocal !== ''" v-model="selectedVersionLocal" class="w-[140px] ml-2"
+						style="width: 140px" placeholder="全部频道">
+						<el-option v-for="item in channelOptions" :key="item.value" :label="item.label"
+							:value="item.value" />
+					</el-select>
+					<el-select v-model="industryCompare" class="!w-[120px] ml-2" clearable
+						@change="handleCompareChange">
+						<el-option label="版本对比" value="version" />
+						<el-option v-if="selectedVersionLocal !== ''" label="渠道对比" value="channel" />
+						<el-option v-if="selectedVersionLocal !== ''" label="时段对比" value="time" />
+					</el-select>
+
+					<!-- 版本对比和渠道对比使用popover -->
+					<el-popover v-if="industryCompare !== 'time' && industryCompare" placement="bottom" trigger="click"
+						width="400">
+						<template #reference>
+							<el-button class="ml-2">{{ t('versionDistribution.version') }}</el-button>
+						</template>
+						<template #default>
+							<div class="p-3">
+								<div class="mb-3">
+									<label class="text-sm font-medium mb-2 block">{{ getCompareTitle() }}</label>
+									<el-input v-model="searchKeyword" :placeholder="`请搜索${getCompareTypeText()}`"
+										clearable @input="filterCompareOptions" size="small" />
+								</div>
+								<div class="max-h-60 overflow-y-auto">
+									<el-checkbox-group v-model="selectedCompareItems"
+										@change="handleCompareItemsChange">
+										<div v-for="item in filteredCompareOptions" :key="item" class="mb-2">
+											<el-checkbox :label="item" size="small">{{ item }}</el-checkbox>
+										</div>
+									</el-checkbox-group>
+								</div>
+							</div>
+						</template>
+					</el-popover>
+
+					<!-- 时段对比使用日期选择 -->
+					<el-popover v-if="industryCompare === 'time'" placement="bottom" trigger="click" width="300"
+						:visible="timeCompareVisible" :hide-after="0" :persistent="true">
+						<template #reference>
+							<el-button class="ml-2"
+								@click="timeCompareVisible = !timeCompareVisible">{{ t('versionDistribution.version') }}</el-button>
+						</template>
+						<template #default>
+							<div class="p-3">
+								<div class="mb-3">
+									<label class="text-sm font-medium mb-2 block">选择对比时段</label>
+									<el-date-picker v-model="timeCompareRange" type="date" format="YYYY-MM-DD"
+										value-format="YYYY-MM-DD" :disabled-date="disableAfterToday"
+										@change="handleTimeCompareChange" style="width: 100%" :clearable="false" />
+								</div>
+							</div>
+						</template>
+					</el-popover>
+
+					<el-button link class="ml-2" @click="clearCompare()">清除</el-button>
+				</div>
+
+				<div class="flex items-center">
+					<el-radio-group v-model="timeGranularity">
+						<el-radio-button label="addUser">新增用户</el-radio-button>
+						<el-radio-button label="activeUser">活跃用户</el-radio-button>
+						<el-radio-button label="launchUser">启动次数</el-radio-button>
+						<el-radio-button v-if="selectedVersionLocal !== ''" label="upgradeUser">升级用户</el-radio-button>
+					</el-radio-group>
+				</div>
+			</div>
+
+			<div class="relative">
+				<div ref="lineChartRef" style="width: 100%; height: 320px"></div>
+			</div>
+		</div>
+
+		<!-- 明细表格 -->
+		<div class="mt-3">
+			<div class="flex items-center justify-between mb-2">
+				<div class="flex">
+					<div v-if="selectedVersionLocal == ''" class="flex items-center">
+						<el-radio-group v-model="timeGranularity">
+							<el-radio-button label="hour">今日</el-radio-button>
+							<el-radio-button label="day">作日 </el-radio-button>
+						</el-radio-group>
+					</div>
+					<div class="text-base font-medium cursor-pointer select-none ml-3 items-center flex text-[#167AF0]"
+						@click="showDetail = !showDetail">
+						{{ showDetail ? '收起明细数据' : '展开明细数据' }}
+						<el-icon class="ml-2">
+							<ArrowDown v-if="showDetail" />
+							<ArrowUp v-else />
+						</el-icon>
+					</div>
+				</div>
+
+				<div>
+					<el-button>导出</el-button>
+				</div>
+			</div>
+			<el-table v-if="showDetail" :data="pagedTableRows" border>
+				<el-table-column prop="date" label="日期" align="center" min-width="140" />
+				<el-table-column prop="hyyh" label="启动次数" align="center" min-width="140" />
+				<el-table-column prop="ratio" label="启动次数(占比)" align="center" min-width="220"> </el-table-column>
+			</el-table>
+			<div v-if="showDetail" class="flex justify-end mt-2">
+				<el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" background
+					layout="total, prev, pager, next, sizes" :total="tableRows.length" :page-sizes="[5, 10, 20]" />
+			</div>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, watch, computed, defineAsyncComponent } from 'vue';
+import * as echarts from 'echarts';
+import { useI18n } from 'vue-i18n';
+import { QuestionFilled } from '@element-plus/icons-vue';
+import { getAppVersion, getAppChannel } from '/@/api/common/common';
+import dayjs from 'dayjs';
+//新增用户tab
+import { getTrend as getAddUserTrend } from '/@/api/count/addUser';
+//活跃用户tab
+import { getTrend as getActiveUserTrend } from '/@/api/count/activeUser';
+//启动次数tab
+import { getTrend as getLaunchTrend } from '/@/api/count/activations';
+
+import { getTrendUpgrade, getTrendDetailAll, getTrendDetailOne, getTrendDetailSource, getTrendSource } from '/@/api/count/version';
+
+
+const { t } = useI18n();
+
+interface ChannelOption {
+	label: string;
+	value: string;
+}
+
+interface TableRow {
+	date: string;
+	newUsers: number;
+	ratio: string;
+	hyyh?: string;
+}
+
+const props = defineProps<{
+	modelValue?: string;
+	selectedVersion?: string;
+	channelOptions: ChannelOption[];
+	showAllVersions?: boolean;
+	formData?: {
+		time?: string[];
+		version?: string;
+	};
+}>();
+
+const emit = defineEmits<{
+	(e: 'update:selectedVersion', v: string): void;
+}>();
+
+const selectedVersionLocal = ref<string>(props.selectedVersion ?? '');
+watch(
+	() => props.selectedVersion,
+	(v) => {
+		selectedVersionLocal.value = v ?? '';
+	}
+);
+watch(selectedVersionLocal, (v) => emit('update:selectedVersion', v));
+watch(() => props.formData, (v) => {
+	formData.value.fromDate = v?.time && v.time[0] ? v.time[0] : '';
+	formData.value.toDate = v?.time && v.time[1] ? v.time[1] : '';
+
+})
+
+// 版本、渠道和时段对比相关状态
+const industryCompare = ref('');
+const selectedCompareItems = ref<string[]>([]);
+const searchKeyword = ref('');
+const compareOptions = ref<string[]>([]);
+const filteredCompareOptions = ref<string[]>([]);
+const previousCompareItems = ref<string[]>([]);
+const timeCompareRange = ref<string>('');
+const timeCompareVisible = ref(false);
+
+const timeGranularity = ref<'addUser' | 'activeUser' | 'launchUser' | 'upgradeUser'>('addUser');
+const lineChartRef = ref<HTMLDivElement | null>(null);
+let chartInstance: echarts.ECharts | null = null;
+
+const masterData = ref<any>({
+	//图表主数据
+	dates: [],
+	items: [],
+});
+
+const lineChartData = ref<any>({
+	//图表数据//显示数据
+	dates: [],
+	items: [],
+});
+
+const colorSchemes = [
+	{ color: '#409EFF' }, // 蓝色
+	{ color: '#67C23A' }, // 绿色
+	{ color: '#E6A23C' }, // 黄色
+	{ color: '#F56C6C' }, // 红色
+];
+
+const chartTimes = ref<string[]>([]);
+
+// 表单数据,用于API调用
+const formData = ref({
+	version: [] as string[],
+	channel: [] as string[],
+	fromDate: '',
+	toDate: '',
+	timeUnit: "day",
+});
+
+const getData = async (type: string) => {
+	//上方图表
+	if (type === 'clearAll') {
+		lineChartData.value = { dates: [], items: [] };
+		industryCompare.value = '';
+	}
+
+	let res;
+	let data;
+	console.log(props.formData);
+	formData.value = {
+		...formData.value,
+		fromDate: props.formData?.time?.[0] || '',
+		toDate: props.formData?.time?.[1] || '',
+	};
+	console.log(formData.value);
+	
+
+	// 根据选择的类型调用不同的API
+	if (timeGranularity.value === 'addUser') {
+		res = await getAddUserTrend({ ...formData.value });
+	}
+	console.log(formData.value);
+	
+
+	// 根据选择的类型调用不同的API
+	if (timeGranularity.value === 'addUser') {
+		res = await getAddUserTrend({ ...formData.value });
+		data = res?.data || [];
+	} else if (timeGranularity.value === 'activeUser') {
+		res = await getActiveUserTrend({ ...formData.value });
+		data = res?.data || [];
+	} else if (timeGranularity.value === 'launchUser') {
+		res = await getLaunchTrend({ ...formData.value });
+		data = res?.data || [];
+	} else if (timeGranularity.value === 'upgradeUser') {
+		res = await getTrendUpgrade({ ...formData.value });
+		data = res?.data || [];
+	}
+
+	initChartData(data);
+};
+
+const initChartData = async (data: any) => {
+	if (!industryCompare.value) {
+		masterData.value = data;
+		lineChartData.value.items = data.items.map((item: any, index: number) => {
+			const randomColorIndex = Math.floor(Math.random() * colorSchemes.length);
+			return {
+				name: getName(item, index, data),
+				type: 'line',
+				smooth: true,
+				data: item.data,
+				itemStyle: {
+					color: colorSchemes[randomColorIndex].color,
+				},
+				lineStyle: {
+					color: colorSchemes[randomColorIndex].color,
+				},
+			};
+		});
+		chartTimes.value = [];
+	} else {
+		lineChartData.value.items.push(
+			data.items.map((item: any, index: number) => {
+				const randomColorIndex = Math.floor(Math.random() * colorSchemes.length);
+				return {
+					name: getName(item, index, data),
+					type: 'line',
+					smooth: true,
+					data: item.data,
+					itemStyle: {
+						color: colorSchemes[randomColorIndex].color,
+					},
+					lineStyle: {
+						color: colorSchemes[randomColorIndex].color,
+					},
+				};
+			})[0]
+		);
+		if (industryCompare.value == 'time') {
+			chartTimes.value.push(data.dates);
+			lineChartData.value.items[0].name = formData.value.fromDate + ' ~ ' + formData.value.toDate;
+		} else {
+			chartTimes.value = [];
+		}
+	}
+	lineChartData.value.dates = masterData.value.dates;
+	initLineChart();
+};
+
+const getName = (item: any, index: number, data: any) => {
+	if (industryCompare.value === 'time') {
+		//日期
+		return formData.value.fromDate + ' ~ ' + formData.value.toDate;
+	} else if (industryCompare.value === 'version') {
+		//版本
+		return item.version === 'All' ? item.name : item.version + ' ' + item.name;
+	} else if (industryCompare.value === 'channel') {
+		//渠道
+		return formData.value.channel[0] || '';
+	} else {
+		return item.name;
+	}
+};
+
+function formatNumber(value: number | string): string {
+	const num = typeof value === 'number' ? value : Number(value || 0);
+	return num.toLocaleString('zh-CN');
+}
+
+const formatterTips = (params: any) => {
+	if (!params || !params.length) return '';
+	const date = params[0]?.axisValue || '';
+	const rows = params
+		.map((p: any, index: number) => {
+			const name = p.seriesName || '';
+			const val = formatNumber(p.data);
+			if (industryCompare.value === 'time' && index != 0) {
+				return `<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;min-width:180px;">
+							<span style="display:flex;align-items:center;gap:6px;">
+								<span style="width:8px;height:8px;border-radius:50%;background:${p.color}"></span>
+								<span>${chartTimes.value[0][p.dataIndex]}</span>
+							</span>
+							<span style="font-variant-numeric: tabular-nums;">${val}</span>
+						</div>`;
+			} else {
+				return `
+			<div style="margin-bottom:6px;color:#93c5fd;">${industryCompare.value === 'time' ? '' : date}</div>
+				
+				<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;min-width:180px;">
+							<span style="display:flex;align-items:center;gap:6px;">
+								<span style="width:8px;height:8px;border-radius:50%;background:${p.color}"></span>
+								<span>${industryCompare.value === 'time' ? p.axisValue : name} </span>
+							</span>
+							<span style="font-variant-numeric: tabular-nums;">${val}</span>
+						</div>`;
+			}
+		})
+		.join('');
+	return `<div style="font-size:12px;">
+			${rows}
+		</div>`;
+};
+
+function initLineChart(): void {
+	if (!lineChartRef.value) return;
+	if (chartInstance) chartInstance.dispose();
+	chartInstance = echarts.init(lineChartRef.value);
+
+	const option: echarts.EChartsOption = {
+		tooltip: {
+			trigger: 'axis',
+			confine: true,
+			axisPointer: { type: 'line' },
+			borderWidth: 0,
+			backgroundColor: 'rgba(17,24,39,0.9)',
+			textStyle: { color: '#fff' },
+			formatter: (params: any) => {
+				return formatterTips(params);
+			},
+		},
+		legend: {
+			data: lineChartData.value.items.map((item: any) => item.name),
+			top: 'bottom',
+			type: 'scroll', // 支持图例滚动
+		},
+		grid: {
+			left: 40,
+			right: 20,
+			top: 20, // 为图例留出空间
+			bottom: 60,
+		},
+		xAxis: {
+			type: 'category',
+			data: lineChartData.value.dates,
+			axisLine: { lineStyle: { color: '#e5e7eb' } },
+			axisLabel: { color: '#6b7280' },
+			axisTick: { alignWithLabel: true },
+		},
+		yAxis: {
+			type: 'value',
+			axisLine: { show: false },
+			splitLine: { lineStyle: { color: '#f3f4f6' } },
+			axisLabel: { color: '#6b7280' },
+		},
+		series: lineChartData.value.items,
+	};
+	chartInstance.setOption(option);
+}
+
+onMounted(() => {
+	getData('');
+	initCompareOptions();
+});
+
+watch(timeGranularity, () => {
+	getData('');
+});
+
+watch(selectedVersionLocal, () => {
+	industryCompare.value = '';
+	getData('');
+});
+
+// 监听showAllVersions变化,重新初始化对比选项
+watch(() => props.showAllVersions, () => {
+	if (industryCompare.value === 'version') {
+		initCompareOptions();
+	}
+});
+
+const tableRows = ref<TableRow[]>(
+	Array.from({ length: 42 }).map((_, idx) => ({
+		date: `2025-08-${String(11).padStart(2, '0')}`,
+		newUsers: 727,
+		hyyh: '115',
+		ratio: '97.45%',
+	}))
+);
+
+const currentPage = ref(1);
+const pageSize = ref(5);
+const pagedTableRows = computed(() => {
+	const startIndex = (currentPage.value - 1) * pageSize.value;
+
+	return tableRows.value.slice(startIndex, startIndex + pageSize.value);
+});
+
+// 版本和渠道对比相关函数
+function handleCompareChange(value: string) {
+	selectedCompareItems.value = [];
+	initCompareOptions();
+	timeCompareVisible.value = value === 'time';
+}
+
+function clearCompare() {
+	selectedCompareItems.value = [];
+	timeCompareRange.value = '';
+	industryCompare.value = '';
+	getData('clearAll');
+}
+
+function getCompareTitle(): string {
+	const typeMap = {
+		version: '选择版本',
+		channel: '选择渠道',
+		time: '选择时段',
+	};
+	return typeMap[industryCompare.value as keyof typeof typeMap] || '选择对比项';
+}
+
+function getCompareTypeText(): string {
+	const typeMap = {
+		version: '版本',
+		channel: '渠道',
+		time: '时段',
+	};
+	return typeMap[industryCompare.value as keyof typeof typeMap] || '';
+}
+
+function filterCompareOptions() {
+	if (!searchKeyword.value) {
+		filteredCompareOptions.value = compareOptions.value;
+	} else {
+		filteredCompareOptions.value = compareOptions.value.filter((item) => item.toLowerCase().includes(searchKeyword.value.toLowerCase()));
+	}
+}
+
+function handleCompareItemsChange(items: string[]) {
+	selectedCompareItems.value = items;
+
+	// 对比当前值和之前的值
+	const currentItems = new Set(items);
+	const previousItems = new Set(previousCompareItems.value);
+
+	// 找出新增的项
+	const addedItems = items.filter((item) => !previousItems.has(item));
+
+	// 找出减少的项(在之前有但现在没有的项)
+	const removedItemIndices = previousCompareItems.value
+		.map((item, index) => ({ item, index }))
+		.filter(({ item }) => !currentItems.has(item))
+		.map(({ index }) => index);
+
+	// 更新 previousCompareItems 为当前值,用于下次对比
+	previousCompareItems.value = items;
+
+	// 先处理主数据项的name
+	if (industryCompare.value === 'version') {
+		// 更新图表数据的主线名称为"全部版本"
+		// 这里可以根据实际需要更新图表数据
+	} else if (industryCompare.value === 'channel') {
+		// 更新图表数据的主线名称为"全部渠道"
+		// 这里可以根据实际需要更新图表数据
+	}
+
+	// 处理全部版本选项的特殊逻辑
+	if (industryCompare.value === 'version' && items.includes('全部版本')) {
+		// 如果选择了"全部版本",可以在这里添加特殊处理逻辑
+		// 例如:清空其他版本选择,或者显示所有版本的数据
+		console.log('选择了全部版本选项');
+	}
+
+	// 清理被取消的对比项
+	if (removedItemIndices.length > 0) {
+		// 从后往前删除,避免索引变化影响
+		removedItemIndices
+			.sort((a, b) => b - a)
+			.forEach(index => {
+				// 清理对应的图表数据
+				// 这里可以根据实际需要清理图表数据
+			});
+	}
+
+	// 根据是否有新增项来决定是否重新获取数据
+	if (addedItems.length > 0) {
+		// 触发数据重新获取
+		getData('');
+	} else if (removedItemIndices.length > 0) {
+		// 如果只是移除了项,则重新初始化图表
+		initLineChart();
+	}
+}
+
+// 初始化对比选项
+async function initCompareOptions() {
+	if (industryCompare.value === 'version') {
+		const res = await getAppVersion();
+		const list: Array<string> = res?.data || [];
+		// 如果props.showAllVersions为true,在列表开头添加"全部版本"选项
+		if (props.showAllVersions) {
+			compareOptions.value = ['全部版本', ...list];
+		} else {
+			compareOptions.value = list;
+		}
+	} else if (industryCompare.value === 'channel') {
+		const res = await getAppChannel();
+		const list: Array<string> = res?.data || [];
+		compareOptions.value = list;
+	}
+	filteredCompareOptions.value = compareOptions.value;
+}
+
+// 时段对比相关函数
+function handleTimeCompareChange(value: string) {
+	timeCompareRange.value = value;
+	formData.value.toDate = timeCompareRange.value;
+	formData.value.fromDate = dayjs(timeCompareRange.value).subtract(7, 'day').format('YYYY-MM-DD');
+	formData.value.channel = props.channelOptions?.map(item => item.value) || [];
+	formData.value.version = selectedVersionLocal.value ? [selectedVersionLocal.value] : [];
+
+	getData('');
+
+	timeCompareVisible.value = false;
+	timeCompareRange.value = '';
+}
+
+function disableAfterToday(date: Date) {
+	const today = new Date();
+	today.setHours(0, 0, 0, 0);
+	return date.getTime() > today.getTime();
+}
+
+const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
+const showDetail = ref(true);
+</script>
+
+<style lang="scss" scoped>
+.highlight {
+	color: #2196f3;
+}
+</style>

+ 41 - 361
src/views/count/user/versionDistribution/index.vue

@@ -4,391 +4,71 @@
 			<div class="el-card  p-9">
 				<div class="flex justify-between">
 					<Title :title="t('versionDistribution.analytics')" />
-					<div class="">
-						<el-button type="primary">{{ t('versionDistribution.aijb') }}</el-button>
-					</div>
+
 				</div>
-				<div>
-					<el-row shadow="hover" class=" mt-2">
-						<el-form :inline="true" :model="formData" @keyup.enter="query" ref="queryRef">
+				<div class="mt-2">
+					<el-row shadow="hover" class="">
+						<el-form :inline="true" :model="formData" ref="queryRef">
 							<el-form-item>
-								<el-date-picker v-model="formData.time" type="daterange" start-placeholder="开始时间" end-placeholder="结束时间" />
+								<el-date-picker v-model="formData.time" type="daterange" value-format="YYYY-MM-DD"
+									:disabled-date="disableFuture" @change="handleRangeChange" class="!w-[250px]"
+									start-placeholder="开始时间" end-placeholder="结束时间" />
 							</el-form-item>
 							<el-form-item>
-								<el-select v-model="selectedChannelCompare" class="w-[140px]" placeholder="全部版本">
-									<el-option v-for="item in channelCompareOptions" :key="item.value" :label="item.label" :value="item.value" />
-								</el-select>
+								<FilterSelect v-model="formData.version" showAllVersions type="version"
+									class="!w-[180px] ml-2" />
 							</el-form-item>
 						</el-form>
 					</el-row>
 				</div>
 			</div>
-			<div class="mt-2 el-card  p-9">
-				<Title left-line :title="selectedChannelCompare === ''? t('versionDistribution.allVersion') : selectedChannelCompare+t('versionDistribution.version')" >
-					<template #default>
-						<el-popover class="box-item" placement="right" trigger="hover" width="300">
-							<template #reference>
-								<el-icon class="ml-1" style="color: #a4b8cf"><QuestionFilled /></el-icon>
-							</template>
-							<template #default>
-								<div class="ant-popover-inner-content">
-									<div class="um-page-tips-content" style="line-height: 24px">
-										<p><span>趋势图展示累计用户排名Top10版本的变化趋势</span></p>
-										<p><span class="highlight">新增用户:</span><span>第一次启动应用的用户(以设备为判断标准)</span></p>
-										<p>
-											<span class="highlight">活跃用户:</span
-											><span>启动过应用的用户(去重),启动过一次的用户即视为活跃用户,包括新用户与老用户</span>
-										</p>
-										<p>
-											<span class="highlight">启动次数:</span
-											><span
-												>打开应用视为启动。完全退出或后台运行超过30s后再次进入应用,视为一次新启动。开发过程中可以通过setSessionContinueMills来自定义两次启动的间隔,默认30s</span
-											>
-										</p>
-										<p>
-											<span class="highlight">版本累计用户(%):</span
-											><span>截止到现在,该版本的累计用户(占累计用户全体的比例);若该版本的用户升级到其他版本,则累计用户会减少</span>
-										</p>
-										<p><span class="highlight">升级用户:</span><span>从其他版本升级到该版本的用户(以设备为判断标准)</span></p>
-										<p>
-											<span
-												>如果当日用户先启动老版本然后升级到新版本,分版本查看数据时,此用户在新老版本都会被算为活跃用户(按总体查看数据时不受影响)</span
-											>
-										</p>
-									</div>
-								</div>
-							</template>
-						</el-popover>
-					</template>
-				</Title>
-				<div class="">
-					<div class="flex items-center justify-between mb-2 mt-3">
-						<div>
-							<el-select v-if="selectedChannelCompare !== ''" v-model="selectedChannelCompare" class="w-[140px] ml-2" style="width: 140px" placeholder="全部频道">
-								<el-option v-for="item in channelCompareOptions" :key="item.value" :label="item.label" :value="item.value" />
-							</el-select>
-							<el-select v-model="selectedChannelCompare" class="w-[140px] ml-2" style="width: 140px" placeholder="版本对比">
-								<el-option v-for="item in channelCompareOptions" :key="item.value" :label="item.label" :value="item.value" />
-							</el-select>
-							<el-button type="primary" class="ml-2">{{ t('versionDistribution.version') }}</el-button>
-						</div>
-
-						<div class="flex items-center">
-							<el-radio-group v-model="timeGranularity">
-								<el-radio-button label="hour">新增用户</el-radio-button>
-								<el-radio-button label="day">活跃用户</el-radio-button>
-								<el-radio-button label="week">启动次数</el-radio-button>
-								<el-radio-button v-if="selectedChannelCompare !== ''" label="sjcs">升级用户</el-radio-button>
-							</el-radio-group>
-						</div>
-					</div>
-
-					<div class="relative">
-						<div ref="lineChartRef" style="width: 100%; height: 320px"></div>
-					</div>
-				</div>
-
-				<!-- 明细表格 -->
-				<div class="mt-3">
-					<div class="flex items-center justify-between mb-2">
-						<div class="flex">
-							<div v-if="selectedChannelCompare == ''" class="flex items-center">
-								<el-radio-group v-model="timeGranularity">
-									<el-radio-button label="hour">今日</el-radio-button>
-									<el-radio-button label="day">作日 </el-radio-button>
-								</el-radio-group>
-							</div>
-							<div class="text-base font-medium cursor-pointer select-none ml-3 items-center flex text-[#167AF0]" @click="showDetail1 = !showDetail1">
-								{{ showDetail1 ? '收起明细数据' : '展开明细数据' }}
-								<el-icon class="ml-2"><ArrowDown v-if="showDetail1" /> <ArrowUp v-else /> </el-icon>
-							</div>
-						</div>
-
-						<div>
-							<el-button>导出</el-button>
-						</div>
-					</div>
-					<el-table v-if="showDetail1" :data="pagedTableRows" border>
-						<el-table-column prop="date" label="日期" align="center" min-width="140" />
-						<el-table-column prop="hyyh" label="启动次数" align="center" min-width="140" />
-						<el-table-column prop="ratio" label="启动次数(占比)" align="center" min-width="220"> </el-table-column>
-					</el-table>
-					<div v-if="showDetail1" class="flex justify-end mt-2">
-						<el-pagination
-							v-model:current-page="currentPage"
-							v-model:page-size="pageSize"
-							background
-							layout="total, prev, pager, next, sizes"
-							:total="tableRows.length"
-							:page-sizes="[5, 10, 20]"
-						/>
-					</div>
-				</div>
-			</div>
-			<div v-if="selectedChannelCompare !== ''" class="mt-2 el-card  p-9">
-				<div class="flex justify-between">
-					<Title left-line :title="'版本用户来源'">
-						<template #default>
-							<el-popover class="box-item" placement="right" trigger="hover" width="300">
-								<template #reference>
-									<el-icon class="ml-1" style="color: #a4b8cf"><QuestionFilled /></el-icon>
-								</template>
-								<template #default>
-									<div class="ant-popover-inner-content">
-										<div class="um-page-tips-content" style="line-height: 24px">
-											<p><span class="highlight">版本用户来源:</span><span>展示各版本用户的主要获取渠道分布</span></p>
-											<p><span class="highlight">新增用户:</span><span>第一次启动应用的用户(以设备为判断标准)</span></p>
-											<p><span class="highlight">升级用户比例:</span><span>从其他版本升级到该版本的用户占比</span></p>
-										</div>
-									</div>
-								</template>
-							</el-popover>
-						</template>
-					</Title>
-				</div>
-				<div class="flex items-center justify-between mt-3">
-					<el-select v-model="channelDistribution" style="width: 180px" placeholder="新增用户渠道分布">
-						<el-option label="新增用户渠道分布" value="distribution" />
-					</el-select>
-					<div class="ml-4">
-						<el-radio-group v-model="timeRange">
-							<el-radio-button label="yesterday">昨天</el-radio-button>
-							<el-radio-button label="7days">过去7天</el-radio-button>
-							<el-radio-button label="30days">过去30天</el-radio-button>
-						</el-radio-group>
-					</div>
-				</div>
-
-				<!-- 水平柱状图 -->
-				<div class="mt-4">
-					<div class="relative">
-						<div ref="barChartRef" style="width: 100%; height: 200px"></div>
-					</div>
-				</div>
-
-				<!-- 明细表格 -->
-				<div class="mt-4">
-					<div class="flex items-center justify-between mb-2">
-						<div class="text-base font-medium cursor-pointer select-none ml-3 items-center flex text-[#167AF0]" @click="showDetail2 = !showDetail2">
-							{{ showDetail2 ? '收起明细数据' : '展开明细数据' }} <el-icon class="ml-2"><ArrowDown v-if="showDetail2" /> <ArrowUp v-else /> </el-icon>
-						</div>
-
-						<div>
-							<el-button>导出</el-button>
-						</div>
-					</div>
-					<el-table v-if="showDetail2" :data="pagedSourceRows" border>
-						<el-table-column prop="channel" label="渠道" min-width="140" />
-						<el-table-column prop="newUsers" label="新增用户" min-width="140" />
-						<el-table-column prop="upgradeRatio" label="升级用户比例" min-width="140" />
-					</el-table>
-					<div v-if="showDetail2" class="flex justify-end mt-3">
-						<el-pagination
-							v-model:current-page="sourcePage"
-							v-model:page-size="sourcePageSize"
-							background
-							layout="total, prev, pager, next, sizes"
-							:total="sourceRows.length"
-							:page-sizes="[5, 10, 20]"
-						/>
-					</div>
-				</div>
-			</div>
+			<version-trend-card v-model:selectedVersion="formData.version" :form-data="formData" :channel-options="formData.version" />
+			<version-source-card v-if="formData.version !== ''" :form-data="formData" :selected-version="formData.version" />
 		</div>
 	</div>
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted, watch, computed, defineAsyncComponent } from 'vue';
-import * as echarts from 'echarts';
+import { ref, defineAsyncComponent } from 'vue';
 import { useI18n } from 'vue-i18n';
-import { QuestionFilled } from '@element-plus/icons-vue';
+import dayjs from 'dayjs';
 
-const { t } = useI18n();
+const VersionTrendCard = defineAsyncComponent(() => import('./components/version-trend-card.vue'));
+const VersionSourceCard = defineAsyncComponent(() => import('./components/version-source-card.vue'));
+const FilterSelect = defineAsyncComponent(() => import('/@/components/common/filter-select.vue'));
 
-interface TableRow {
-	date: string;
-	newUsers: number;
-	ratio: string;
-}
+const { t } = useI18n();
 
-const formData = ref<Record<string, any>>({});
-const query = () => {
-	console.log(formData.value);
+const getDefaultDateRange = () => {
+	const endDate = dayjs();
+	const startDate = endDate.subtract(15, 'day');
+	return [startDate.format('YYYY-MM-DD'), endDate.format('YYYY-MM-DD')];
 };
-
-const selectedChannelCompare = ref('');
-const channelCompareOptions = [
-	{ label: '全部版本', value: '' },
-	{ label: '1.0', value: '1.0' },
-	{ label: '2.0', value: '2.0' },
-];
-
-// 图表相关
-const timeGranularity = ref<'hour' | 'day' | 'week' | 'month'>('week');
-const lineChartRef = ref<HTMLDivElement | null>(null);
-let chartInstance: echarts.ECharts | null = null;
-
-const lineChartData = ref<Array<{ x: string; value: number }>>([
-	{ x: '2025-07-01', value: 900 },
-	{ x: '2025-07-08', value: 1000 },
-	{ x: '2025-07-15', value: 1100 },
-	{ x: '2025-07-22', value: 1000 },
-	{ x: '2025-07-29', value: 600 },
-	{ x: '2025-08-05', value: 300 },
-	{ x: '2025-08-12', value: 250 },
-	{ x: '2025-08-19', value: 200 },
-	{ x: '2025-08-26', value: 650 },
-	{ x: '2025-09-02', value: 950 },
-	{ x: '2025-09-09', value: 900 },
-	{ x: '2025-09-16', value: 120 },
-]);
-
-function initLineChart(): void {
-	if (!lineChartRef.value) return;
-	if (chartInstance) chartInstance.dispose();
-	chartInstance = echarts.init(lineChartRef.value);
-	const option: echarts.EChartsOption = {
-		tooltip: { trigger: 'axis' },
-		grid: { left: 40, right: 20, top: 20, bottom: 30 },
-		xAxis: {
-			type: 'category',
-			data: lineChartData.value.map((d) => d.x),
-			axisLine: { lineStyle: { color: '#e5e7eb' } },
-			axisLabel: { color: '#6b7280' },
-			axisTick: { alignWithLabel: true },
-		},
-		yAxis: {
-			type: 'value',
-			axisLine: { show: false },
-			splitLine: { lineStyle: { color: '#f3f4f6' } },
-			axisLabel: { color: '#6b7280' },
-		},
-		series: [
-			{
-				name: '新增人数',
-				type: 'line',
-				smooth: true,
-				showSymbol: true,
-				symbolSize: 6,
-				itemStyle: { color: '#409EFF' },
-				lineStyle: { color: '#409EFF' },
-				data: lineChartData.value.map((d) => d.value),
-			},
-		],
-	};
-	chartInstance.setOption(option);
-}
-
-onMounted(() => {
-	initLineChart();
-	initBarChart();
-});
-
-watch(timeGranularity, () => {
-	// 静态页面:仅重新渲染
-	initLineChart();
-});
-
-// 表格相关(静态数据)
-const tableRows = ref<TableRow[]>(
-	Array.from({ length: 42 }).map((_, idx) => ({
-		date: `2025-08-${String(11).padStart(2, '0')}`,
-		newUsers: 727,
-		hyyh: '115',
-		ratio: '97.45%',
-	}))
-);
-
-const currentPage = ref(1);
-const pageSize = ref(5);
-const pagedTableRows = computed(() => {
-	const startIndex = (currentPage.value - 1) * pageSize.value;
-
-	return tableRows.value.slice(startIndex, startIndex + pageSize.value);
+const formData = ref<Record<string, any>>({
+	time: getDefaultDateRange(), // 时间范围
 });
 
-const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
-
-// 展开/收起明细
-const showDetail1 = ref(true);
-
-// 版本用户来源相关
-const channelDistribution = ref('distribution');
-const timeRange = ref('yesterday');
-const barChartRef = ref<HTMLDivElement | null>(null);
-let barChartInstance: echarts.ECharts | null = null;
-
-const sourceRows = ref<Array<{ channel: string; newUsers: number; upgradeRatio: string }>>([
-	{ channel: '渠道A', newUsers: 100, upgradeRatio: '10%' },
-	{ channel: '渠道B', newUsers: 80, upgradeRatio: '15%' },
-	{ channel: '渠道C', newUsers: 70, upgradeRatio: '20%' },
-	{ channel: '渠道D', newUsers: 60, upgradeRatio: '25%' },
-	{ channel: '渠道E', newUsers: 50, upgradeRatio: '30%' },
-	{ channel: '渠道F', newUsers: 40, upgradeRatio: '35%' },
-	{ channel: '渠道G', newUsers: 30, upgradeRatio: '40%' },
-	{ channel: '渠道H', newUsers: 20, upgradeRatio: '45%' },
-	{ channel: '渠道I', newUsers: 10, upgradeRatio: '50%' },
-	{ channel: '渠道J', newUsers: 5, upgradeRatio: '55%' },
-]);
 
-function initBarChart(): void {
-	if (!barChartRef.value) return;
-	if (barChartInstance) barChartInstance.dispose();
-	barChartInstance = echarts.init(barChartRef.value);
-	const option: echarts.EChartsOption = {
-		tooltip: { trigger: 'axis' },
-		grid: { left: 40, right: 20, top: 20, bottom: 30 },
-		xAxis: {
-			type: 'value',
-			axisLine: { show: false },
-			splitLine: { lineStyle: { color: '#f3f4f6' } },
-			axisLabel: { color: '#6b7280' },
-		},
-		yAxis: {
-			type: 'category',
-			data: sourceRows.value.map((d) => d.channel),
-			axisLine: { lineStyle: { color: '#e5e7eb' } },
-			axisLabel: { color: '#6b7280' },
-			axisTick: { alignWithLabel: true },
-		},
-		series: [
-			{
-				name: '新增用户',
-				type: 'bar',
-				barWidth: '60%',
-				itemStyle: { color: '#409EFF' },
-				data: sourceRows.value.map((d) => d.newUsers),
-			},
-		],
-	};
-	barChartInstance.setOption(option);
+function disableFuture(date: Date) {
+	const today = new Date();
+	today.setHours(0, 0, 0, 0);
+	return date.getTime() > today.getTime();
 }
 
-onMounted(() => {
-	initBarChart();
-});
-
-watch(timeRange, () => {
-	// 静态页面:仅重新渲染
-	initBarChart();
-});
-
-watch(selectedChannelCompare, () => {
-	// 静态页面:仅重新渲染
-	initBarChart();
-	initLineChart();
-});
-
-const sourcePage = ref(1);
-const sourcePageSize = ref(5);
-const pagedSourceRows = computed(() => {
-	const startIndex = (sourcePage.value - 1) * sourcePageSize.value;
-
-	return sourceRows.value.slice(startIndex, startIndex + sourcePageSize.value);
-});
-
-const showDetail2 = ref(true);
+function handleRangeChange(val: [string, string] | null) {
+	if (!val) {
+		return;
+	}
+	const [startStr, endStr] = val;
+	const start = dayjs(startStr);
+	let end = dayjs(endStr);
+	// 限制最大跨度为两年(按天计算,不含边界再减一天)
+	if (end.diff(start, 'day') >= 365 * 2) {
+		end = start.add(2, 'year').subtract(1, 'day');
+		formData.value.time = [start.format('YYYY-MM-DD'), end.format('YYYY-MM-DD')];
+	}
+}
+const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
 </script>
  
 <style lang="scss" scoped>

+ 118 - 58
src/views/marketing/config/components/push.vue

@@ -4,17 +4,12 @@
 			<div class="flex items-start">
 				<label class="w-[65px] leading-8 text-right">地区</label>
 				<div class="flex gap-2 ml-2 flex-wrap w-57vw]">
-					<el-tag
-						v-for="(tag, index) in regionData"
-						:key="index"
-						size="large"
-						closable
-						:disable-transitions="false"
-						@close="handleClose(tag, 'region')"
-					>
-						{{ tag.state ? `${tag.country} - ${tag.state}` : tag.country }}
+					<el-tag v-for="(tag, index) in regionData" :key="index" size="large" closable
+						:disable-transitions="false" @close="handleClose(tag, 'region')">
+						{{ tag }}
 					</el-tag>
-					<ChineseRegionSelector v-if="regionInputVisible" v-model="regionData" @update:modelValue="handleRegionDataUpdate" />
+					<ChineseRegionSelector v-if="regionInputVisible" v-model="regionData" showAll
+						@update:modelValue="handleRegionDataUpdate" />
 					<el-button v-else class="button-new-tag" @click="showRegionInput"> 添加地区</el-button>
 					<!-- 注册隐藏的表单项以启用校验 -->
 					<el-form-item prop="region" style="display: none"></el-form-item>
@@ -42,61 +37,63 @@
 			</div>
 		</div> -->
 
-		<div class="px-4 rounded overflow-y-auto mt-4 w-full" style="max-height: calc(100vh - 350px)">
+		<!-- <div class="px-4 rounded overflow-y-auto mt-4 w-full" style="max-height: calc(100vh - 350px)">
 			<div class="flex items-start">
 				<label class="w-[66px] leading-8 text-right">添加域名</label>
 				<div class="flex gap-2 ml-2 flex-wrap w-57vw]">
-					<el-tag v-for="tag in domainData" :key="tag" size="large" closable :disable-transitions="false" @close="handleClose(tag, 'domain')">
+					<el-tag v-for="tag in domainData" :key="tag" size="large" closable :disable-transitions="false"
+						@close="handleClose(tag, 'domain')">
 						{{ tag }}
 					</el-tag>
-					<el-input
-						v-if="domainInputVisible"
-						ref="domainInputRef"
-						v-model="domainInputValue"
-						class="!w-32"
-						@keyup.enter="handleInputConfirm('domain')"
-						@blur="handleInputConfirm('domain')"
-						placeholder="如: example.com"
-					/>
+					<el-input v-if="domainInputVisible" ref="domainInputRef" v-model="domainInputValue" class="!w-32"
+						@keyup.enter="handleInputConfirm('domain')" @blur="handleInputConfirm('domain')"
+						placeholder="如: example.com" />
 					<el-button v-else class="button-new-tag" @click="showDomainInput"> 添加域名</el-button>
-					<!-- 注册隐藏的表单项以启用校验 -->
 					<el-form-item prop="domain" style="display: none"></el-form-item>
 				</div>
 			</div>
-		</div>
-		<el-form ref="ruleFormRef" :model="formData" :rules="dataRules" label-width="90px" class="flex flex-wrap mt-4 w-1/2">
-			<el-form-item label="主动推送" prop="autoPush" class="w-full">
-				<el-switch v-model="formData.autoPush" class="mr-2" />
-			</el-form-item>
-			<el-form-item label="推送应用" prop="appId" class="w-1/2">
-				<el-select v-model="formData.appId" placeholder="请选择推送方式" :props="{ multiple: true }" class="w-full">
-					<el-option v-for="item in appOptions" :key="item.value" :label="item.label" :value="item.value" />
+		</div> -->
+		<el-form ref="ruleFormRef" :model="formData" :rules="dataRules" label-width="90px"
+			class="flex flex-wrap mt-4 w-1/2">
+			<el-form-item label="推送应用" prop="pushApp" class="w-full">
+				<el-select v-model="formData.pushApp" placeholder="请选择推送方式" multiple collapse-tags collapse-tags-tooltip
+					:max-collapse-tags="4" filterable remote class="w-full" :value-key="'appId'">
+					<el-option v-for="item in appOptions" :key="item.value" :label="item.label"
+						:value="{ id: item.id, appId: item.value }" />
 				</el-select>
 			</el-form-item>
+			<el-form-item label="主动推送" prop="autoPush" class="w-1/2">
+				<el-switch v-model="formData.autoPush" class="mr-2" />
+			</el-form-item>
+
 			<el-form-item label="推送方式" prop="action" class="w-1/2">
-				<JDictSelect v-model:value="formData.action" placeholder="请选择推送方式" dictType="pushMode" :styleClass="'w-full'" />
+				<JDictSelect v-model:value="formData.action" placeholder="请选择推送方式" dictType="pushMode"
+					:styleClass="'w-full'" />
 			</el-form-item>
 
-			<el-form-item label="推送频率" prop="pushFrequency" class="w-1/2">
+			<!-- <el-form-item label="推送频率" prop="pushFrequency" class="w-1/2">
 				<el-input v-model="formData.pushFrequency" type="text" placeholder="请输入推送频率" />
-			</el-form-item>
+			</el-form-item> -->
 			<el-form-item label="推送延时" prop="delayPush" class="w-1/2">
 				<div class="flex items-start w-full">
 					<el-input v-model="formData.delayPush" type="text" placeholder="请输入推送延时" />
 				</div>
 			</el-form-item>
-			<el-form-item label="推送图片" prop="pushContent" class="w-full">
+			<!-- <el-form-item label="推送图片" prop="pushContent" class="w-full">
 				<div class="flex items-start">
-					<el-switch v-model="formData.pushType" class="mr-2" @change="(oldUrl = formData.pushContent), (formData.pushContent = '')" />
-					<Image v-if="formData.pushType" @change="success" :imageUrl="formData.pushContent" @update="success" />
+					<el-switch v-model="formData.pushType" class="mr-2"
+						@change="(oldUrl = formData.pushContent), (formData.pushContent = '')" />
+					<Image v-if="formData.pushType" @change="success" :imageUrl="formData.pushContent"
+						@update="success" />
 				</div>
-			</el-form-item>
+			</el-form-item> -->
 
-			<el-form-item v-if="!formData.pushType" label="推送内容" prop="pushContent" class="w-full">
+			<el-form-item label="推送内容" prop="pushContent" class="w-full">
 				<el-input v-model="formData.pushContent" type="text" placeholder="请输入推送内容" />
 			</el-form-item>
 		</el-form>
-		<el-button type="primary" class="ml-5 mt-4 w-[80px]" @click="onSubmit" :disabled="loading">{{ t('common.saveBtn') }}</el-button>
+		<el-button type="primary" class="ml-5 mt-4 w-[80px]" @click="onSubmit"
+			:disabled="loading">{{ t('common.saveBtn') }}</el-button>
 	</div>
 </template>
 
@@ -116,7 +113,7 @@ const props = defineProps({
 	},
 	rowData: {
 		type: Object,
-		default: () => {},
+		default: () => { },
 	},
 });
 
@@ -147,20 +144,23 @@ const formData = ref<any>({
 	pushType: false,
 	delayPush: '',
 	autoPush: false,
+	pushApp: [],
+	pushBundle: []
 });
 const appOptions = ref<any[]>([]);
-const getAppListData = async() => {
-   const res =  await getAppList()
-   const data = res.data
-   appOptions.value = data.map((item: any) => {
-      return {
-         label: item.appName,
-         value: item.appId,
-      };
-   });
+const getAppListData = async () => {
+	const res = await getAppList();
+	const data = res.data;
+	appOptions.value = data.map((item: any, index: number) => {
+		return {
+			label: item.appName,
+			value: item.appId,
+			id: item.id,
+			bundle: item.bundle || ''
+		};
+	});
 };
 
-
 // // 表单校验规则
 const dataRules = reactive({
 	ruleName: [{ required: true, message: '规则名称不能为空', trigger: 'blur' }],
@@ -453,9 +453,62 @@ const handleInputConfirm = (type: string) => {
 	}
 };
 
+// 规范化 pushApp 值为 { id, appId } 数组
+function normalizePushApp(input: any[]): Array<{ id: any; appId: any }> {
+	if (!Array.isArray(input)) return [];
+	return input
+		.map((item: any) => {
+			if (item && typeof item === 'object' && 'appId' in item && 'id' in item) {
+				return { id: item.id, appId: item.appId };
+			}
+			const appId = typeof item === 'object' && 'value' in item ? item.value : item;
+			const found = appOptions.value.find((opt: any) => opt.value === appId);
+			return found ? { id: found.id, appId: found.value } : null;
+		})
+		.filter((v: any): v is { id: any; appId: any } => v !== null);
+}
+
+// 根据选择的 pushApp 派生 pushBundle(去重)
+function derivePushBundle(selected: Array<{ id: any; appId: any }>): string[] {
+	const bundles = new Set<string>();
+	selected.forEach((sel) => {
+		const opt = appOptions.value.find((o: any) => o.value === sel.appId);
+		if (opt && opt.bundle !== undefined && opt.bundle !== null) {
+			bundles.add(String(opt.bundle));
+		}
+	});
+	return Array.from(bundles);
+}
+
 // 地区数据更新处理
 const handleRegionDataUpdate = (newRegionData: any[]) => {
-	regionData.value = newRegionData;
+	// 1) 规范化映射为字符串;2) 过滤无效项;3) 去重
+	const mapped = newRegionData
+		.map((item: any) => {
+			if (item || item == '全部国家') {
+				return (
+					(item?.city
+						? `${item.country} - ${item.state} - ${item.city}`
+						: item?.state
+							? `${item.country} - ${item.state}`
+							: item?.country) || item
+				);
+			}
+			return false;
+		})
+		.filter((v: any) => v !== undefined && v !== false && v !== null && v !== '');
+
+	const unique = Array.from(new Set(mapped)) as string[];
+	const hasNonAll = unique.some((v) => v !== '全部国家');
+
+	if (hasNonAll) {
+		// 有其它选项时,去掉“全部国家”
+		regionData.value = unique.filter((v) => v !== '全部国家');
+	} else {
+		// 都是“全部国家”时,仅保留一个
+		regionData.value = unique.length > 0 ? ['全部国家'] : [];
+	}
+
 	regionInputVisible.value = false;
 };
 const getpushContent = () => {
@@ -481,16 +534,22 @@ const onSubmit = async () => {
 			pushFrequency = num / 100;
 		}
 	}
+	if (regionData.value.length < 1) {
+		regionData.value = ['全部国家'];
+	}
 	formData.value = {
 		...formData.value,
 		ip: ipData.value,
 		domain: domainData.value,
-		region: regionData.value,
+		pushAddr: regionData.value,
 		pushContent: getpushContent(),
-		pushType: formData.value.pushType || false,
+		pushType: false,
 		pushFrequency: pushFrequency.toString(),
+		pushApp: normalizePushApp(formData.value.pushApp),
+		pushBundle: derivePushBundle(normalizePushApp(formData.value.pushApp)),
 	};
 
+
 	if (!formData.value.pushContent) {
 		return useMessage().error(formData.value.pushType ? '推送图片不能为空' : '推送内容不能为空');
 	}
@@ -517,7 +576,7 @@ const formatNum = (value: string | number = 0) => {
 	} else if (num >= 1 && num < 10000) {
 		return num;
 	}
-	return '--';
+	return '';
 };
 
 watch(
@@ -528,15 +587,16 @@ watch(
 				...props.rowData,
 				action: props.rowData?.action || '1',
 				pushFrequency: formatNum(props.rowData?.pushFrequency),
+				pushApp: normalizePushApp(props.rowData.pushApp || []),
+				pushBundle: props.rowData.pushBundle || [],
 			};
 			ipData.value = props.rowData.ip || [];
 			domainData.value = props.rowData.domain || [];
-			regionData.value = props.rowData.region || [];
+			regionData.value = props.rowData.pushAddr || ['全部国家'];
 			oldUrl.value = props.rowData.pushContent;
-			getAppListData()
+			getAppListData();
 		}
 	}
 );
 </script>
-<style lang="scss">
-</style>
+<style lang="scss"></style>