Files
akmon/components/supadb/RegionSelector.uvue
2026-01-20 08:04:15 +08:00

533 lines
13 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>