533 lines
13 KiB
Plaintext
533 lines
13 KiB
Plaintext
<template>
|
||
<view class="region-selector">
|
||
<view class="region-filter">
|
||
<view class="level-tabs">
|
||
<view
|
||
v-for="(level, index) in availableLevels"
|
||
:key="level.value"
|
||
:class="['level-tab', { active: currentLevelIndex === index }]"
|
||
@click="selectLevel(level.value, index)"
|
||
>
|
||
<text>{{ level.label }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="selected-path" v-if="showPath && selectedPath.length > 0">
|
||
<view
|
||
v-for="(region, index) in selectedPath"
|
||
:key="region.id"
|
||
class="path-item"
|
||
>
|
||
<text
|
||
class="path-text"
|
||
@click="navigateToPathItem(index)"
|
||
>{{ region.name }}</text>
|
||
<text class="path-separator" v-if="index < selectedPath.length - 1"> / </text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="region-list" v-if="regions.length > 0">
|
||
<view class="region-items">
|
||
<view
|
||
v-for="region in regions"
|
||
:key="region.id"
|
||
class="region-item"
|
||
@click="selectRegion(region)"
|
||
>
|
||
<view class="region-info">
|
||
<text class="region-name">{{ region.name }}</text>
|
||
<view class="region-meta" v-if="showStats">
|
||
<text class="region-count">{{ region.children_count || 0 }} 个下级区域</text>
|
||
<text class="region-count">{{ region.school_count || 0 }} 所学校</text>
|
||
</view>
|
||
</view>
|
||
<text class="region-arrow">></text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="empty-state" v-else-if="!loading">
|
||
<text>当前没有{{ currentLevelLabel }}级区域数据</text>
|
||
<button
|
||
v-if="canCreate"
|
||
class="add-btn"
|
||
@click="$emit('create', { parentId: currentParentId, level: currentLevel })"
|
||
>
|
||
添加{{ currentLevelLabel }}
|
||
</button>
|
||
</view>
|
||
|
||
<view class="loading" v-if="loading">
|
||
<text>加载中...</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script lang="uts">
|
||
import { ref, computed, onMounted, watch } from 'vue'
|
||
import { SupaDB } from './supadb.uvue'
|
||
|
||
// 定义区域数据接口
|
||
interface Region {
|
||
id: string
|
||
name: string
|
||
level: number
|
||
parent_id?: string
|
||
children_count?: number
|
||
school_count?: number
|
||
}
|
||
|
||
// 定义级别选项接口
|
||
interface LevelOption {
|
||
value: number
|
||
label: string
|
||
}
|
||
|
||
export default {
|
||
name: 'RegionSelector',
|
||
props: {
|
||
// 初始选中的区域ID
|
||
initialRegionId: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
// 是否显示路径导航
|
||
showPath: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// 是否显示统计数据
|
||
showStats: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// 是否可以创建新区域
|
||
canCreate: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
// 可用的区域级别,如果为空则使用所有级别
|
||
allowedLevels: {
|
||
type: Array,
|
||
default: () => []
|
||
}
|
||
},
|
||
setup(props: any, { emit }: any) {
|
||
const db = new SupaDB()
|
||
|
||
// 状态
|
||
const loading = ref(false)
|
||
const regions = ref<Region[]>([])
|
||
const selectedPath = ref<Region[]>([])
|
||
const currentLevel = ref(1) // 默认省级
|
||
const currentLevelIndex = ref(0)
|
||
const currentParentId = ref('')
|
||
|
||
// 级别常量
|
||
const REGION_LEVELS: LevelOption[] = [
|
||
{ value: 1, label: '省/直辖市' },
|
||
{ value: 2, label: '市/区' },
|
||
{ value: 3, label: '县/区' },
|
||
{ value: 4, label: '乡镇/街道' }
|
||
]
|
||
// 计算属性:可用的级别
|
||
const availableLevels = computed(() => {
|
||
if (props.allowedLevels && props.allowedLevels.length > 0) {
|
||
// Replace filter with for loop for UTS compatibility
|
||
let filteredLevels = []
|
||
for (let i = 0; i < REGION_LEVELS.length; i++) {
|
||
const level = REGION_LEVELS[i]
|
||
if (props.allowedLevels.includes(level.value)) {
|
||
filteredLevels.push(level)
|
||
}
|
||
}
|
||
return filteredLevels
|
||
}
|
||
return REGION_LEVELS
|
||
})
|
||
|
||
// 当前级别标签
|
||
const currentLevelLabel = computed(() => {
|
||
const level = REGION_LEVELS.find(l => l.value === currentLevel.value)
|
||
return level ? level.label : ''
|
||
})
|
||
|
||
// 初始化
|
||
onMounted(async () => {
|
||
// 如果有初始区域ID,则加载该区域及其路径
|
||
if (props.initialRegionId) {
|
||
await loadRegionAndPath(props.initialRegionId)
|
||
} else {
|
||
// 否则加载顶级区域
|
||
await loadRegions()
|
||
}
|
||
})
|
||
|
||
// 监听初始区域ID变化
|
||
watch(() => props.initialRegionId, async (newVal) => {
|
||
if (newVal) {
|
||
await loadRegionAndPath(newVal)
|
||
}
|
||
})
|
||
|
||
// 加载区域数据
|
||
const loadRegions = async (parentId?: string) => {
|
||
loading.value = true
|
||
|
||
try {
|
||
let query = db.from('ak_regions')
|
||
.select('*, children:ak_regions!parent_id(count), schools:ak_schools(count)')
|
||
.eq('level', currentLevel.value)
|
||
.order('name')
|
||
|
||
if (parentId) {
|
||
query = query.eq('parent_id', parentId)
|
||
} else if (currentLevel.value !== 1) {
|
||
// 非顶级区域但无父ID,显示空数据
|
||
regions.value = []
|
||
loading.value = false
|
||
return
|
||
}
|
||
|
||
const { data, error } = await query
|
||
|
||
if (error) {
|
||
console.error('加载区域数据失败:', error)
|
||
regions.value = []
|
||
return
|
||
}
|
||
if (data) {
|
||
// Replace map with for loop for UTS compatibility
|
||
let mappedRegions : Region[] = []
|
||
for (let i = 0; i < data.length; i++) {
|
||
const item = data[i]
|
||
mappedRegions.push({
|
||
...item,
|
||
children_count: (item.children && item.children.length) ? item.children[0].count : 0,
|
||
school_count: (item.schools && item.schools.length) ? item.schools[0].count : 0
|
||
} as Region)
|
||
}
|
||
regions.value = mappedRegions
|
||
} else {
|
||
regions.value = []
|
||
}
|
||
} catch (e) {
|
||
console.error('加载区域数据异常:', e)
|
||
regions.value = []
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 加载区域及其路径
|
||
const loadRegionAndPath = async (regionId: string) => {
|
||
loading.value = true
|
||
|
||
try {
|
||
// 获取区域详情
|
||
const { data, error } = await db.from('ak_regions')
|
||
.select('*')
|
||
.eq('id', regionId)
|
||
.single()
|
||
|
||
if (error) {
|
||
console.error('获取区域详情失败:', error)
|
||
return
|
||
}
|
||
|
||
if (!data) return
|
||
|
||
const region = data as Region
|
||
|
||
// 设置当前级别
|
||
currentLevel.value = region.level
|
||
const levelIndex = availableLevels.value.findIndex(l => l.value === region.level)
|
||
if (levelIndex >= 0) {
|
||
currentLevelIndex.value = levelIndex
|
||
}
|
||
|
||
// 获取路径
|
||
await loadRegionPath(region)
|
||
|
||
// 加载同级区域
|
||
if (region.parent_id) {
|
||
currentParentId.value = region.parent_id
|
||
await loadRegions(region.parent_id)
|
||
} else {
|
||
currentParentId.value = ''
|
||
await loadRegions()
|
||
}
|
||
|
||
// 触发选择事件
|
||
emit('select', region)
|
||
} catch (e) {
|
||
console.error('加载区域及路径异常:', e)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 加载区域路径
|
||
const loadRegionPath = async (region: Region) => {
|
||
try {
|
||
// 先将当前区域添加到路径
|
||
selectedPath.value = [region]
|
||
|
||
let currentParent = region.parent_id
|
||
|
||
// 循环获取所有父区域
|
||
while (currentParent) {
|
||
const { data, error } = await db.from('ak_regions')
|
||
.select('*')
|
||
.eq('id', currentParent)
|
||
.single()
|
||
|
||
if (error || !data) break
|
||
|
||
const parentRegion = data as Region
|
||
// 将父区域添加到路径前面
|
||
selectedPath.value.unshift(parentRegion)
|
||
|
||
// 继续向上级查找
|
||
currentParent = parentRegion.parent_id
|
||
}
|
||
} catch (e) {
|
||
console.error('加载区域路径异常:', e)
|
||
}
|
||
}
|
||
|
||
// 选择区域级别
|
||
const selectLevel = async (level: number, index: number) => {
|
||
currentLevel.value = level
|
||
currentLevelIndex.value = index
|
||
|
||
// 根据当前路径确定父级ID
|
||
if (selectedPath.value.length > 0) {
|
||
// 找到合适的父级
|
||
const parent = selectedPath.value.find(r => r.level === level - 1)
|
||
|
||
if (parent) {
|
||
// 找到合适的父级,更新路径
|
||
const pathIndex = selectedPath.value.indexOf(parent)
|
||
selectedPath.value = selectedPath.value.slice(0, pathIndex + 1)
|
||
currentParentId.value = parent.id
|
||
} else {
|
||
// 未找到合适的父级,重置路径
|
||
selectedPath.value = []
|
||
currentParentId.value = ''
|
||
}
|
||
} else {
|
||
currentParentId.value = ''
|
||
}
|
||
|
||
await loadRegions(currentParentId.value || undefined)
|
||
}
|
||
|
||
// 选择区域
|
||
const selectRegion = async (region: Region) => {
|
||
// 如果已在路径中,则不重复添加
|
||
if (selectedPath.value.some(r => r.id === region.id)) {
|
||
return
|
||
}
|
||
|
||
// 更新路径
|
||
if (region.level > 1 && selectedPath.value.length === 0) {
|
||
// 如果是选择非顶级区域且路径为空,需要加载完整路径
|
||
await loadRegionAndPath(region.id)
|
||
} else {
|
||
// 否则直接添加到路径末尾
|
||
selectedPath.value.push(region)
|
||
|
||
// 如果有下级区域,自动切换到下级
|
||
if (region.children_count && region.children_count > 0) {
|
||
const nextLevel = region.level + 1
|
||
const nextLevelOption = availableLevels.value.find(l => l.value === nextLevel)
|
||
|
||
if (nextLevelOption) {
|
||
const nextLevelIndex = availableLevels.value.indexOf(nextLevelOption)
|
||
currentLevel.value = nextLevel
|
||
currentLevelIndex.value = nextLevelIndex
|
||
currentParentId.value = region.id
|
||
await loadRegions(region.id)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 触发选择事件
|
||
emit('select', region)
|
||
}
|
||
|
||
// 导航到路径项
|
||
const navigateToPathItem = async (index: number) => {
|
||
if (index >= selectedPath.value.length) return
|
||
|
||
const pathItem = selectedPath.value[index]
|
||
|
||
// 更新路径
|
||
selectedPath.value = selectedPath.value.slice(0, index + 1)
|
||
|
||
// 更新级别
|
||
currentLevel.value = pathItem.level
|
||
const levelIndex = availableLevels.value.findIndex(l => l.value === pathItem.level)
|
||
if (levelIndex >= 0) {
|
||
currentLevelIndex.value = levelIndex
|
||
}
|
||
|
||
// 更新父ID
|
||
if (index === 0) {
|
||
currentParentId.value = ''
|
||
} else {
|
||
currentParentId.value = selectedPath.value[index - 1].id
|
||
}
|
||
|
||
// 加载区域
|
||
await loadRegions(currentParentId.value || undefined)
|
||
|
||
// 触发选择事件
|
||
emit('select', pathItem)
|
||
}
|
||
|
||
return {
|
||
loading,
|
||
regions,
|
||
selectedPath,
|
||
currentLevel,
|
||
currentLevelIndex,
|
||
currentParentId,
|
||
availableLevels,
|
||
currentLevelLabel,
|
||
|
||
// 方法
|
||
selectLevel,
|
||
selectRegion,
|
||
navigateToPathItem
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.region-selector {
|
||
background-color: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.region-filter {
|
||
padding: 15px;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.level-tabs {
|
||
display: flex;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.level-tab {
|
||
padding: 6px 12px;
|
||
margin-right: 8px;
|
||
border-radius: 4px;
|
||
background-color: #f5f5f5;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.level-tab.active {
|
||
background-color: #1890ff;
|
||
color: #fff;
|
||
}
|
||
|
||
.selected-path {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.path-item {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-right: 5px;
|
||
}
|
||
|
||
.path-text {
|
||
color: #1890ff;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.path-separator {
|
||
color: #bbb;
|
||
margin: 0 5px;
|
||
}
|
||
|
||
.region-list {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.region-items {
|
||
padding: 0 15px;
|
||
}
|
||
|
||
.region-item {
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid #eee;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.region-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.region-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.region-name {
|
||
font-size: 16px;
|
||
color: #333;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.region-meta {
|
||
display: flex;
|
||
}
|
||
|
||
.region-count {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-right: 15px;
|
||
}
|
||
|
||
.region-arrow {
|
||
color: #bbb;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.empty-state {
|
||
padding: 30px 15px;
|
||
text-align: center;
|
||
color: #999;
|
||
}
|
||
|
||
.add-btn {
|
||
margin-top: 15px;
|
||
background-color: #1890ff;
|
||
color: #fff;
|
||
border: none;
|
||
padding: 6px 12px;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.loading {
|
||
padding: 20px;
|
||
text-align: center;
|
||
color: #999;
|
||
}
|
||
</style> |