Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

View 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>