npm package: @newgudi/region-picker
Description: Global region picker component for Ant Design and Vben Admin
Author: Vben Team
Latest version: 1.1.2

# GlobalRegionPicker 全球地区选择器

一个支持全球地区选择的 Vue 3 组件，特别优化了中国地区的五级选择（大洲 → 国家 → 省份 → 城市 → 区县）。

## 功能特性

- ✅ 支持全球地区选择（大洲 → 国家 → 行政区）
- ✅ 中国地区五级选择（大洲 → 国家 → 省份 → 城市 → 区县）
- ✅ 智能搜索功能，支持拼音和中英文搜索
- ✅ 分组显示，按首字母分组
- ✅ 响应式设计，适配各种屏幕尺寸
- ✅ 支持 v-model 双向绑定
- ✅ 主题适配（支持亮色/暗色主题）
- ✅ **弹窗可拖动**，点击标题栏可拖动弹窗位置
- ✅ **直辖市智能处理**，自动跳过重复的市级选择

## 安装

```bash
# 已内置在项目中，无需额外安装
```

## 基本使用

### 1. 在表单中使用（推荐）

```vue
<template>
  <div class="form-group">
    <label>地区选择：</label>
    <GlobalRegionPicker
      v-model="formData.region"
      placeholder="请选择地区"
      @change="handleRegionChange"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { GlobalRegionPicker } from '@vben/region-picker';

const formData = ref({
  region: null
});

function handleRegionChange(value: any) {
  console.log('选择的地区:', value);
  // value 结构：
  // {
  //   continent: '亚洲代码',
  //   country: '国家代码',
  //   province: '省份代码',
  //   city: '城市代码',
  //   district: '区县代码',
  //   region: '其他国家行政区代码',
  //   path: ['亚洲', '中国', '广东省', '东莞市', '茶山镇']
  // }
}
</script>
```

### 2. 在 VbenForm 中使用

```vue
<template>
  <VbenForm :schema="formSchema" @submit="handleSubmit" />
</template>

<script setup lang="ts">
import { ref, markRaw } from 'vue';
import { VbenForm } from '@vben/common-ui';
import { GlobalRegionPicker } from '@vben/region-picker';

const formSchema = ref([
  {
    fieldName: 'region',
    label: '地区',
    component: markRaw(GlobalRegionPicker), // 使用 markRaw 避免响应式警告
    componentProps: {
      placeholder: '请选择地区'
    },
    rules: [
      { required: true, message: '请选择地区' }
    ]
  }
]);

function handleSubmit(values: any) {
  console.log('表单数据:', values);
}
</script>
```

### 3. 编辑模式（回显数据）

```vue
<template>
  <GlobalRegionPicker
    v-model="selectedRegion"
    placeholder="请选择地区"
  />
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { GlobalRegionPicker } from '@vben/region-picker';

const selectedRegion = ref(null);

// 从后端加载数据
onMounted(async () => {
  const data = await fetchCustomerData();
  
  // 设置地区数据
  selectedRegion.value = {
    continent: data.continent,
    country: data.country,
    province: data.province,
    city: data.city,
    district: data.district,
    region: data.region,
    path: [
      data.continent_name,
      data.country_name,
      data.province_name,
      data.city_name,
      data.district_name
    ].filter(Boolean) // 过滤空值
  };
});
</script>
```

## 数据结构

### 输入数据（v-model）

```typescript
interface RegionValue {
  continent: string;      // 大洲代码，如：'AS'（亚洲）
  country: string;        // 国家代码，如：'CN'（中国）
  province?: string;      // 省份代码，如：'440000'（广东省）
  city?: string;          // 城市代码，如：'441900'（东莞市）
  district?: string;      // 区县代码，如：'441932'（茶山镇）
  region?: string;        // 其他国家行政区代码
  path: string[];         // 显示路径，如：['亚洲', '中国', '广东省', '东莞市', '茶山镇']
}
```

### 输出数据（change 事件）

与输入数据结构相同。

## Props

| 参数 | 说明 | 类型 | 默认值 |
|------|------|------|--------|
| modelValue | 绑定值 | `RegionValue \| null` | `null` |
| placeholder | 输入框占位文本 | `string` | `'请选择地区'` |

## Events

| 事件名 | 说明 | 回调参数 |
|--------|------|----------|
| update:modelValue | 值变化时触发 | `(value: RegionValue) => void` |
| change | 值变化时触发 | `(value: RegionValue) => void` |

## 数据库字段设计

建议使用以下字段存储地区信息：

```sql
-- 地区代码字段
continent VARCHAR(10) COMMENT '大洲代码',
country VARCHAR(10) COMMENT '国家代码',
province VARCHAR(20) COMMENT '省份代码',
city VARCHAR(20) COMMENT '城市代码',
district VARCHAR(20) COMMENT '区县代码',
region VARCHAR(50) COMMENT '其他国家行政区代码',

-- 地区名称字段（用于显示）
continent_name VARCHAR(50) COMMENT '大洲名称',
country_name VARCHAR(100) COMMENT '国家名称',
province_name VARCHAR(100) COMMENT '省份名称',
city_name VARCHAR(100) COMMENT '城市名称',
district_name VARCHAR(100) COMMENT '区县名称',
region_name VARCHAR(100) COMMENT '其他国家行政区名称',

-- 完整地址路径（可选，用于快速显示）
full_address VARCHAR(500) COMMENT '完整地址路径'
```

## 完整示例

### 客户管理表单示例

```vue
<template>
  <VbenModal
    v-model:open="visible"
    :title="isEdit ? '编辑客户' : '新增客户'"
    @ok="handleSubmit"
  >
    <VbenForm ref="formRef" :schema="formSchema" />
  </VbenModal>
</template>

<script setup lang="ts">
import { ref, computed, watch, nextTick, markRaw } from 'vue';
import { VbenModal, VbenForm } from '@vben/common-ui';
import { GlobalRegionPicker } from '@vben/region-picker';
import { message } from 'ant-design-vue';

const props = defineProps<{
  open: boolean;
  record?: any;
}>();

const emit = defineEmits<{
  (e: 'update:open', value: boolean): void;
  (e: 'success'): void;
}>();

const visible = computed({
  get: () => props.open,
  set: (val) => emit('update:open', val)
});

const formRef = ref();
const isEdit = computed(() => !!props.record?.id);
const selectedRegion = ref(null);

// 表单配置
const formSchema = computed(() => [
  {
    fieldName: 'name',
    label: '客户名称',
    component: 'Input',
    rules: [{ required: true, message: '请输入客户名称' }]
  },
  {
    fieldName: 'contact',
    label: '联系人',
    component: 'Input'
  },
  {
    fieldName: 'phone',
    label: '联系电话',
    component: 'Input'
  },
  {
    fieldName: 'email',
    label: '电子邮箱',
    component: 'Input'
  }
]);

// 监听弹窗打开，加载数据
watch(() => props.open, async (newVal) => {
  if (newVal && props.record) {
    await nextTick();
    
    // 设置表单数据
    formRef.value?.setValues({
      name: props.record.name,
      contact: props.record.contact,
      phone: props.record.phone,
      email: props.record.email
    });
    
    // 设置地区数据
    if (props.record.country) {
      selectedRegion.value = {
        continent: props.record.continent || '',
        country: props.record.country || '',
        province: props.record.province || '',
        city: props.record.city || '',
        district: props.record.district || '',
        region: props.record.region || '',
        path: [
          props.record.continent_name,
          props.record.country_name,
          props.record.province_name,
          props.record.city_name,
          props.record.district_name
        ].filter(Boolean)
      };
    }
  }
});

// 提交表单
async function handleSubmit() {
  try {
    const values = await formRef.value?.validate();
    
    // 合并地区数据
    const submitData = {
      ...values,
      continent: selectedRegion.value?.continent || '',
      country: selectedRegion.value?.country || '',
      province: selectedRegion.value?.province || '',
      city: selectedRegion.value?.city || '',
      district: selectedRegion.value?.district || '',
      region: selectedRegion.value?.region || '',
      continent_name: selectedRegion.value?.path[0] || '',
      country_name: selectedRegion.value?.path[1] || '',
      province_name: selectedRegion.value?.path[2] || '',
      city_name: selectedRegion.value?.path[3] || '',
      district_name: selectedRegion.value?.path[4] || '',
      full_address: selectedRegion.value?.path.join(' / ') || ''
    };
    
    // 调用 API
    if (isEdit.value) {
      await updateCustomer(props.record.id, submitData);
      message.success('更新成功');
    } else {
      await createCustomer(submitData);
      message.success('创建成功');
    }
    
    visible.value = false;
    emit('success');
  } catch (error) {
    console.error('提交失败:', error);
  }
}
</script>
```

## 注意事项

1. **使用 markRaw**：在 VbenForm 的 schema 中使用组件时，建议用 `markRaw()` 包裹组件，避免 Vue 响应式警告：
   ```typescript
   component: markRaw(GlobalRegionPicker)
   ```

2. **数据回显**：编辑模式下，需要同时提供代码和名称数据，组件会根据 `path` 数组显示完整路径。

3. **数据验证**：建议在表单提交前验证地区数据的完整性：
   ```typescript
   if (!selectedRegion.value?.country) {
     message.error('请选择地区');
     return;
   }
   ```

4. **中国地区**：对于中国地区，建议至少选择到城市级别，以保证地址的准确性。

5. **其他国家**：其他国家可能只有国家级别或国家+行政区两级，根据实际数据结构处理。

## 主题适配

组件已内置主题适配，会自动跟随系统主题切换：

```css
/* 亮色主题 */
body.light .global-region-picker {
  /* 自动应用亮色样式 */
}

/* 暗色主题 */
body.dark .global-region-picker {
  /* 自动应用暗色样式 */
}
```

## 常见问题

### Q1: 编辑时输入框不显示数据？

**A**: 确保传入的数据包含 `path` 数组，组件通过 `path` 数组生成显示文本：

```typescript
selectedRegion.value = {
  continent: 'AS',
  country: 'CN',
  province: '440000',
  city: '441900',
  district: '441932',
  path: ['亚洲', '中国', '广东省', '东莞市', '茶山镇'] // 必须提供
};
```

### Q2: 如何获取选中的地区名称？

**A**: 通过 `path` 数组获取：

```typescript
const regionNames = selectedRegion.value?.path || [];
const fullAddress = regionNames.join(' / '); // "亚洲 / 中国 / 广东省 / 东莞市 / 茶山镇"
```

### Q3: 如何只选择到省份或城市级别？

**A**: 组件支持任意级别的选择，用户可以在任意级别点击"确定"按钮完成选择。

### Q4: 如何自定义样式？

**A**: 可以通过 CSS 覆盖组件样式：

```css
.global-region-picker {
  /* 自定义样式 */
}

.global-region-picker .ant-input {
  /* 自定义输入框样式 */
}
```

### Q5: 弹窗如何拖动？

**A**: 点击弹窗的标题栏（"选择地区"文字区域）并按住鼠标左键，即可拖动弹窗到任意位置。松开鼠标后弹窗会固定在新位置。关闭弹窗后，位置会自动重置。

### Q6: 为什么直辖市只显示一次？

**A**: 组件已针对中国直辖市（北京、天津、上海、重庆）做了特殊处理，选择直辖市后会自动跳过城市选择，直接显示区县，避免出现"上海市 / 上海市"这样的重复。

## 更新日志

### v1.2.0 (2026-02-01)
- ✨ 新增弹窗拖动功能，点击标题栏可拖动弹窗
- ✨ 优化直辖市选择逻辑，自动跳过重复的市级选择
- 🐛 修复直辖市路径重复显示问题（如"上海市 / 上海市"）
- 🐛 修复文件上传字段类型错误
- 🐛 修复无障碍访问警告

### v1.1.0 (2026-01-31)
- ✨ 新增数据回显功能
- ✨ 支持编辑模式
- 🐛 修复组件响应式警告

### v1.0.0 (2024-01-31)
- ✨ 初始版本发布
- ✨ 支持全球地区选择
- ✨ 支持中国五级地区选择
- ✨ 支持智能搜索
- ✨ 支持主题切换
- ✨ 支持数据回显

## 技术支持

如有问题或建议，请联系开发团队。