jcq 1 giorno fa
parent
commit
fc35656428

+ 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",

+ 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 = '';
 };
 

+ 20 - 351
src/views/count/user/versionDistribution/index.vue

@@ -12,383 +12,52 @@
 					<el-row shadow="hover" class=" mt-2">
 						<el-form :inline="true" :model="formData" @keyup.enter="query" 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" 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 v-model="selectedChannelCompare" class="w-[140px]"
+									:placeholder="selectPlaceholder">
+									<el-option v-for="item in channelCompareOptions" :key="item.value"
+										:label="item.label" :value="item.value" />
 								</el-select>
 							</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="selectedChannelCompare"
+				:channel-options="channelCompareOptions" />
+			<version-source-card v-if="selectedChannelCompare !== ''" :selected-version="selectedChannelCompare" />
 		</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 { useRoute } from 'vue-router';
 import { useI18n } from 'vue-i18n';
-import { QuestionFilled } from '@element-plus/icons-vue';
+import VersionTrendCard from './components/version-trend-card.vue';
+import VersionSourceCard from './components/version-source-card.vue';
 
 const { t } = useI18n();
 
-interface TableRow {
-	date: string;
-	newUsers: number;
-	ratio: string;
-}
-
 const formData = ref<Record<string, any>>({});
+const route = useRoute();
+const showAllParam = route.query.showAll as string | undefined;
+const showAll = showAllParam === undefined || showAllParam === '' || showAllParam === '1' || showAllParam === 'true';
+const selectPlaceholder = showAll ? '全部版本' : '请选择版本';
 const query = () => {
 	console.log(formData.value);
 };
 
-const selectedChannelCompare = ref('');
-const channelCompareOptions = [
-	{ label: '全部版本', value: '' },
+const selectedChannelCompare = ref(showAll ? '' : '1.0');
+const baseOptions = [
 	{ 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 channelCompareOptions = showAll ? [{ label: '全部版本', value: '' }, ...baseOptions] : baseOptions;
 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);
-}
-
-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);
 </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>