Initial commit of akmon project
This commit is contained in:
533
components/supadb/RegionSelector.uvue
Normal file
533
components/supadb/RegionSelector.uvue
Normal file
@@ -0,0 +1,533 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user