You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

301 lines
8.4 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<script setup lang="ts">
import { ref } from 'vue'
import { usePageLoad } from '@/composables/usePageLoad'
import {
createUniversity,
deleteUniversity,
fetchUniversitiesList,
updateUniversity,
type UniversityRow,
} from '@/api/admin/assets'
import { Location } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import TiandituPickMap from '@/components/TiandituPickMap.vue'
const loading = ref(false)
const items = ref<UniversityRow[]>([])
const meta = ref({ current_page: 1, per_page: 20, total: 0 })
const page = ref(1)
const keyword = ref('')
const filterRegion = ref('')
const dialog = ref(false)
const mapPickVisible = ref(false)
const mapPickReady = ref(false)
const editing = ref<UniversityRow | null>(null)
const pickDraft = ref({ longitude: '', latitude: '' })
const form = ref({
name: '',
longitude: '',
latitude: '',
city: '',
province: '',
})
const regionOptions = [
{ label: '上海高校', value: '上海' },
{ label: '苏州高校', value: '苏州' },
{ label: '浙江高校', value: '浙江' },
]
async function load() {
loading.value = true
try {
const params: Record<string, unknown> = {
page: page.value,
page_size: meta.value.per_page,
}
if (keyword.value) params.keyword = keyword.value
if (filterRegion.value) params.region = filterRegion.value
const res = await fetchUniversitiesList(params)
items.value = res.items
meta.value = res.meta
} finally {
loading.value = false
}
}
function resetFilters() {
keyword.value = ''
filterRegion.value = ''
page.value = 1
load()
}
function searchUniversities() {
page.value = 1
load()
}
function openCreate() {
editing.value = null
form.value = { name: '', longitude: '', latitude: '', city: '', province: '' }
dialog.value = true
}
function openEdit(row: UniversityRow) {
editing.value = row
form.value = {
name: row.name,
longitude: row.longitude != null ? String(row.longitude) : '',
latitude: row.latitude != null ? String(row.latitude) : '',
city: row.city || '',
province: row.province || '',
}
dialog.value = true
}
function openMapPick() {
pickDraft.value = {
longitude: form.value.longitude,
latitude: form.value.latitude,
}
mapPickReady.value = false
mapPickVisible.value = true
}
function onMapPickOpened() {
mapPickReady.value = true
}
function onMapPickClosed() {
mapPickReady.value = false
}
function confirmMapPick() {
if (!pickDraft.value.longitude || !pickDraft.value.latitude) {
ElMessage.warning('请先在地图上选点')
return
}
form.value.longitude = pickDraft.value.longitude
form.value.latitude = pickDraft.value.latitude
mapPickVisible.value = false
}
async function save() {
if (!form.value.name || !form.value.longitude || !form.value.latitude) {
ElMessage.warning('请填写高校名称与经纬度')
return
}
const payload = {
name: form.value.name.trim(),
longitude: Number(form.value.longitude),
latitude: Number(form.value.latitude),
city: form.value.city || null,
province: form.value.province || null,
}
if (editing.value) {
await updateUniversity(editing.value.id, payload)
} else {
await createUniversity(payload)
}
ElMessage.success('已保存')
dialog.value = false
await load()
}
async function remove(row: UniversityRow) {
await ElMessageBox.confirm(`确定要删除高校「${row.name}」?`, '确认删除', { type: 'warning' })
await deleteUniversity(row.id)
ElMessage.success('已删除')
await load()
}
usePageLoad(load)
</script>
<template>
<div class="list-page">
<div class="page-header">
<h1 class="page-title">高校坐标库</h1>
<el-button type="primary" size="small" class="btn-create" @click="openCreate"></el-button>
</div>
<el-card shadow="never" class="admin-list-card">
<p class="list-page-hint">长三角主要高校经纬度预置录入供雷达地图映射使用</p>
<div class="list-filter-bar">
<el-input
v-model="keyword"
placeholder="搜索高校名称、经纬度…"
clearable
class="filter-search"
@keyup.enter="searchUniversities"
/>
<el-select v-model="filterRegion" placeholder="地区" clearable class="filter-select-wide">
<el-option v-for="r in regionOptions" :key="r.value" :label="r.label" :value="r.value" />
</el-select>
<el-button type="primary" @click="searchUniversities">搜索</el-button>
<el-button @click="resetFilters">重置</el-button>
</div>
<el-table v-loading="loading" :data="items" row-key="id">
<el-table-column prop="name" label="高校名称" min-width="180" />
<el-table-column label="经度" width="120">
<template #default="{ row }">{{ row.longitude ?? '—' }}</template>
</el-table-column>
<el-table-column label="纬度" width="120">
<template #default="{ row }">{{ row.latitude ?? '—' }}</template>
</el-table-column>
<el-table-column prop="city" label="城市" width="100" />
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<div class="table-row-actions">
<el-button class="btn-action-secondary" @click="openEdit(row)">编辑</el-button>
<el-button class="btn-action-secondary" @click="remove(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div v-if="meta.total > 0" class="list-pager">
<el-pagination
layout="total, prev, pager, next"
:total="meta.total"
:page-size="meta.per_page"
:current-page="page"
@current-change="(p: number) => { page = p; load() }"
/>
</div>
</el-card>
</div>
<el-dialog
v-model="dialog"
:title="editing ? '编辑高校' : '新增高校'"
width="480px"
destroy-on-close
>
<el-form label-position="top">
<el-form-item label="高校名称" required>
<el-input v-model="form.name" placeholder="如:复旦大学" />
</el-form-item>
<el-form-item label="经纬度" required>
<div class="coord-inline">
<el-input v-model="form.longitude" placeholder="经度" class="coord-input" />
<el-input v-model="form.latitude" placeholder="纬度" class="coord-input" />
<el-button class="pick-map-btn" @click="openMapPick">
<el-icon><Location /></el-icon>
地图选点
</el-button>
</div>
</el-form-item>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="省份">
<el-input v-model="form.province" placeholder="如:上海" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="城市">
<el-input v-model="form.city" placeholder="如:上海" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="dialog = false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
<el-dialog
v-model="mapPickVisible"
class="map-pick-dialog"
title="地图选点"
width="720px"
destroy-on-close
append-to-body
align-center
@opened="onMapPickOpened"
@closed="onMapPickClosed"
>
<p v-if="pickDraft.longitude && pickDraft.latitude" class="pick-coord-preview">
当前选点:{{ pickDraft.longitude }}{{ pickDraft.latitude }}
</p>
<TiandituPickMap
v-if="mapPickReady"
v-model:longitude="pickDraft.longitude"
v-model:latitude="pickDraft.latitude"
:default-keyword="form.name"
:height="400"
/>
<template #footer>
<el-button @click="mapPickVisible = false">取消</el-button>
<el-button type="primary" @click="confirmMapPick"></el-button>
</template>
</el-dialog>
</template>
<style scoped>
.coord-inline {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.coord-input {
flex: 1;
min-width: 0;
}
.pick-map-btn {
flex-shrink: 0;
}
.pick-coord-preview {
margin: 0 0 10px;
color: var(--el-text-color-secondary);
font-size: 13px;
}
</style>
<style>
/* 弹窗动画 transform 会导致天地图瓦片错位,打开后取消 transform */
.map-pick-dialog.el-dialog {
transform: none !important;
}
</style>