master
lion 6 days ago
parent a093799b01
commit 655bb65d00

@ -1,4 +1,4 @@
# 生产环境请改为实际后端 API 根路径(需包含 /api 前缀,或与后端约定一致)
VITE_API_BASE_URL=https://your-api.example.com/api
VITE_API_BASE_URL=https://slake.ali251.langye.net/api
# 天地图 JavaScript API 4.0 Key
VITE_TIANDITU_TK=

@ -601,16 +601,33 @@
min-height: 460px;
}
.radar-map-container {
.radar-map-stage {
min-height: 460px;
position: relative;
background: #e8eef5;
}
/* 高校点位:圆点 + 校名横排(对齐原型 .dashboard-radar-dot */
.radar-map-container {
position: absolute;
inset: 0;
/* z-index:0 SDK panetile-pane z=200
map-pane z=400marker-pane z=600
z-index:20 */
z-index: 0;
background: #e8eef5;
cursor: grab;
}
.radar-map-container:active {
cursor: grabbing;
}
/* 高校点位:圆点 + 校名横排(天地图 SDK 覆盖物,整体可点击选中高校) */
.radar-map-container .slake-map-school-marker {
position: absolute;
z-index: 400;
/* 圆点与校名整体可点击,点名字也能加载老师 */
pointer-events: auto;
display: inline-flex;
align-items: center;
gap: 4px;
@ -644,6 +661,7 @@
.radar-map-container .slake-map-school-marker.is-active .slake-map-school-dot {
width: 13px;
height: 13px;
background: #8a1418;
box-shadow:
0 0 0 6px rgba(177, 30, 35, 0.18),
0 8px 18px rgba(15, 23, 42, 0.16);
@ -668,6 +686,12 @@
color: #1f2937;
}
.radar-map-container .slake-map-school-marker.is-active .slake-map-school-label {
border-color: rgba(177, 30, 35, 0.55);
color: #8a1418;
font-weight: 600;
}
.radar-map-placeholder {
position: absolute;
inset: 0;

@ -2,13 +2,16 @@
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import {
bindMapResizeObserver,
configureMapInteraction,
createTiandituMap,
getTiandituKey,
invalidateMapSize,
loadTianditu,
normalizeTiandituPois,
parseTiandituPoiLonlat,
refreshMapView,
waitForMapContainerReady,
SUZHOU_MAP_CENTER,
SUZHOU_MAP_ZOOM,
type TiandituLocalSearch,
type TiandituMap,
type TiandituMapClickEvent,
@ -24,6 +27,7 @@ const props = withDefaults(
const longitude = defineModel<string>('longitude', { default: '' })
const latitude = defineModel<string>('latitude', { default: '' })
const mapContainerId = `tianditu-pick-map-${Math.random().toString(36).slice(2)}`
const containerRef = ref<HTMLElement | null>(null)
const mapError = ref('')
const mapLoading = ref(false)
@ -35,32 +39,30 @@ let map: TiandituMap | null = null
let marker: TiandituMarker | null = null
let localSearch: TiandituLocalSearch | null = null
let tiandituApi: NonNullable<typeof window.T> | null = null
let resizeTimers: ReturnType<typeof setTimeout>[] = []
let unbindResizeObserver: (() => void) | null = null
let pendingDefaultSearch = false
function parseCoord(v: string) {
// Number('') === 0 0 (0,0)
// 0,0
if (v == null || String(v).trim() === '') return null
const n = Number(v)
return Number.isFinite(n) ? n : null
}
function clearResizeTimers() {
for (const timer of resizeTimers) clearTimeout(timer)
resizeTimers = []
}
function scheduleMapResize() {
clearResizeTimers()
const delays = [0, 80, 240, 480]
for (const delay of delays) {
resizeTimers.push(
setTimeout(() => {
if (map) invalidateMapSize(map)
}, delay),
)
function getMapCenter() {
const lng = parseCoord(longitude.value)
const lat = parseCoord(latitude.value)
if (lng != null && lat != null) {
return { lng, lat, zoom: 14 }
}
return { lng: SUZHOU_MAP_CENTER.lng, lat: SUZHOU_MAP_CENTER.lat, zoom: 14 }
}
function destroyMapInstance() {
clearResizeTimers()
unbindResizeObserver?.()
unbindResizeObserver = null
pendingDefaultSearch = false
if (map && marker) {
try {
map.removeOverLay(marker)
@ -79,7 +81,7 @@ function applyPick(lng: number, lat: number, zoom = 16) {
longitude.value = lng.toFixed(6)
latitude.value = lat.toFixed(6)
if (map && tiandituApi) {
map.centerAndZoom(new tiandituApi.LngLat(lng, lat), zoom)
refreshMapView(map, tiandituApi, { lng, lat }, zoom)
placeMarker(tiandituApi, lng, lat)
}
}
@ -142,9 +144,25 @@ function selectPoi(poi: TiandituSearchPoi) {
searchResults.value = []
}
function runPendingDefaultSearch() {
if (!pendingDefaultSearch) return
pendingDefaultSearch = false
if (props.defaultKeyword.trim()) {
searchKeyword.value = props.defaultKeyword.trim()
runSearch()
}
}
function forceDefaultMapRefresh() {
if (!map || !tiandituApi) return
const view = getMapCenter()
refreshMapView(map, tiandituApi, { lng: view.lng, lat: view.lat }, view.zoom)
}
async function initMap() {
await nextTick()
if (!containerRef.value) return
const container = containerRef.value
if (!container) return
mapError.value = ''
if (!getTiandituKey()) {
@ -156,22 +174,32 @@ async function initMap() {
try {
destroyMapInstance()
const ready = await waitForMapContainerReady(container)
if (!ready) {
mapError.value = '地图容器未就绪,请关闭后重试'
return
}
const T = await loadTianditu()
tiandituApi = T
const container = containerRef.value
container.innerHTML = ''
container.id = mapContainerId
container.style.width = '100%'
container.style.height = `${props.height}px`
map = createTiandituMap(T, mapContainerId)
configureMapInteraction(map)
map = new T.Map(container)
map.enableScrollWheelZoom()
const center = getMapCenter()
map.centerAndZoom(new T.LngLat(center.lng, center.lat), center.zoom)
const lng = parseCoord(longitude.value)
const lat = parseCoord(latitude.value)
if (lng != null && lat != null) {
map.centerAndZoom(new T.LngLat(lng, lat), 14)
placeMarker(T, lng, lat)
} else {
map.centerAndZoom(new T.LngLat(SUZHOU_MAP_CENTER.lng, SUZHOU_MAP_CENTER.lat), SUZHOU_MAP_ZOOM)
placeMarker(T, center.lng, center.lat)
}
initLocalSearch(T)
@ -184,17 +212,30 @@ async function initMap() {
searchResults.value = []
})
scheduleMapResize()
map.addEventListener?.('load', () => {
if (!map || !tiandituApi) return
const view = getMapCenter()
refreshMapView(map, tiandituApi, { lng: view.lng, lat: view.lat }, view.zoom)
runPendingDefaultSearch()
})
unbindResizeObserver = bindMapResizeObserver(container, () => map)
refreshMapView(map, T, { lng: center.lng, lat: center.lat }, center.zoom)
window.setTimeout(forceDefaultMapRefresh, 300)
window.setTimeout(forceDefaultMapRefresh, 800)
window.setTimeout(forceDefaultMapRefresh, 1500)
if (props.defaultKeyword.trim()) {
searchKeyword.value = props.defaultKeyword.trim()
runSearch()
pendingDefaultSearch = true
}
} catch (e) {
mapError.value = e instanceof Error ? e.message : '地图加载失败'
} finally {
mapLoading.value = false
scheduleMapResize()
if (map && tiandituApi) {
const view = getMapCenter()
refreshMapView(map, tiandituApi, { lng: view.lng, lat: view.lat }, view.zoom)
}
}
}
@ -207,11 +248,7 @@ watch([longitude, latitude], async () => {
})
onMounted(() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
void initMap()
})
})
void initMap()
})
onBeforeUnmount(() => {
@ -242,7 +279,8 @@ onBeforeUnmount(() => {
</ul>
<div v-if="mapError" class="pick-map-error">{{ mapError }}</div>
<div v-else class="pick-map-shell" v-loading="mapLoading">
<div v-else class="pick-map-shell">
<div v-if="mapLoading" class="pick-map-loading"></div>
<div
ref="containerRef"
class="pick-map"
@ -315,26 +353,49 @@ onBeforeUnmount(() => {
.pick-map-shell {
position: relative;
width: 100%;
min-height: 200px;
overflow: hidden;
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
background: #e8eef5;
}
.pick-map-loading {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
color: var(--el-text-color-secondary);
font-size: 13px;
background: rgba(255, 255, 255, 0.72);
pointer-events: none;
}
.pick-map {
position: relative;
z-index: 0;
width: 100%;
overflow: hidden;
cursor: crosshair;
isolation: isolate;
}
.pick-map :deep(.tdt-container),
.pick-map :deep(.tdt-map) {
width: 100% !important;
height: 100% !important;
}
.pick-map :deep(.tdt-overlay-pane),
.pick-map :deep(.tdt-marker-pane) {
pointer-events: none;
}
.pick-map :deep(.tdt-marker-pane img) {
pointer-events: auto;
}
.pick-map-error {
padding: 24px 12px;
text-align: center;

@ -1,7 +1,10 @@
declare global {
interface Window {
T?: {
Map: new (container: HTMLElement | string) => TiandituMap
Map: new (
container: HTMLElement | string,
options?: { layers?: TiandituTileLayer[] },
) => TiandituMap
LngLat: new (lng: number, lat: number) => TiandituLngLat
Marker: new (lnglat: TiandituLngLat, options?: { icon?: TiandituIcon }) => TiandituMarker
Icon: new (options: {
@ -10,6 +13,10 @@ declare global {
iconAnchor?: TiandituPoint
}) => TiandituIcon
Point: new (x: number, y: number) => TiandituPoint
TileLayer?: new (
url: string,
options?: { minZoom?: number; maxZoom?: number },
) => TiandituTileLayer
Label: new (options: {
text: string
position: TiandituLngLat
@ -81,6 +88,10 @@ export interface TiandituIcon {
// marker icon handle
}
export interface TiandituTileLayer {
// tile layer handle
}
export interface TiandituPoint {
x: number
y: number
@ -88,28 +99,254 @@ export interface TiandituPoint {
export interface TiandituMapClickEvent {
lnglat?: TiandituLngLat
containerPoint?: TiandituPoint
}
export interface TiandituMap {
centerAndZoom(lnglat: TiandituLngLat, zoom: number): void
enableScrollWheelZoom(): void
addOverLay(overlay: TiandituMarker | TiandituSchoolOverlay): void
removeOverLay(overlay: TiandituMarker | TiandituSchoolOverlay): void
enableDrag?(): void
disableDrag?(): void
enableAutoResize?(): void
panBy?(position: TiandituPoint): void
addOverLay(overlay: TiandituMarker | TiandituLabel | TiandituSchoolOverlay): void
removeOverLay(overlay: TiandituMarker | TiandituLabel | TiandituSchoolOverlay): void
setViewport?(points: TiandituLngLat[]): void
clearOverLays?(): void
addLayer?(layer: TiandituTileLayer): void
addEventListener?(type: string, handler: (e?: TiandituMapClickEvent) => void): void
removeEventListener?(type: string, handler: (e?: TiandituMapClickEvent) => void): void
lngLatToLayerPoint?(lnglat: TiandituLngLat): TiandituPoint
getPanes?(): { overlayPane: HTMLElement }
lngLatToContainerPoint?(lnglat: TiandituLngLat): TiandituPoint
getPanes?(): Record<string, HTMLElement>
getContainer?(): HTMLElement
checkResize?(): void
destroy?(): void
}
const MAP_INTERACTIVE_PANE_KEYS = new Set(['mapPane', 'tilePane', 'floatPane'])
const MAP_PASSTHROUGH_PANE_CLASS_RE =
/tdt-overlay-pane|tdt-marker-pane|tdt-tooltip-pane|tdt-popup-pane|tdt-shadow-pane/i
function invokeMapMethod(map: TiandituMap, method: string) {
const fn = (map as unknown as Record<string, unknown>)[method]
if (typeof fn === 'function') {
;(fn as () => void).call(map)
}
}
/** 创建天地图实例。使用 SDK 默认底图,避免自定义瓦片图层影响拖拽/初始化。 */
export function createTiandituMap(
T: NonNullable<typeof window.T>,
container: HTMLElement | string,
): TiandituMap {
return new T.Map(container)
}
/** 弹窗内地图在容器尺寸变化后需重算布局,避免瓦片错位到页面顶部 */
export function invalidateMapSize(map: TiandituMap) {
map.checkResize?.()
}
/** 等待地图容器具备有效宽高(弹窗动画期间常为 0 */
export async function waitForMapContainerReady(
container: HTMLElement,
timeoutMs = 5000,
): Promise<boolean> {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
const rect = container.getBoundingClientRect()
if (rect.width > 0 && rect.height > 0) return true
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
}
return false
}
/** 重新计算尺寸并刷新视野,修复弹窗内瓦片不渲染 */
export function refreshMapView(
map: TiandituMap,
T: NonNullable<typeof window.T>,
center: { lng: number; lat: number },
zoom: number,
) {
invalidateMapSize(map)
map.centerAndZoom(new T.LngLat(center.lng, center.lat), zoom)
scheduleMapResize(map, [50, 200, 500])
}
/** 容器尺寸变化时自动触发 checkResize弹窗、侧栏展开等场景 */
export function bindMapResizeObserver(
container: HTMLElement,
getMap: () => TiandituMap | null,
): () => void {
if (typeof ResizeObserver === 'undefined') {
return () => {}
}
const observer = new ResizeObserver(() => {
const map = getMap()
if (map) invalidateMapSize(map)
})
observer.observe(container)
return () => observer.disconnect()
}
/** 多次延迟触发 checkResize覆盖弹窗动画结束后的布局 */
export function scheduleMapResize(map: TiandituMap, delays = [0, 80, 240, 480, 800]) {
for (const delay of delays) {
window.setTimeout(() => invalidateMapSize(map), delay)
}
}
/**
*
* SDK getPanes DOM class
*/
export function applyOverlayPassthrough(map: TiandituMap) {
const panes = map.getPanes?.()
if (panes) {
for (const [key, pane] of Object.entries(panes)) {
if (!pane || MAP_INTERACTIVE_PANE_KEYS.has(key)) continue
pane.style.pointerEvents = 'none'
}
}
const root = map.getContainer?.()
if (root) {
root.querySelectorAll<HTMLElement>('[class*="tdt-"][class*="pane"]').forEach((pane) => {
if (MAP_PASSTHROUGH_PANE_CLASS_RE.test(pane.className)) {
pane.style.pointerEvents = 'none'
}
})
root.querySelectorAll<HTMLElement>('.slake-map-school-marker').forEach((marker) => {
marker.style.pointerEvents = 'auto'
})
}
}
/** 启用滚轮缩放与拖拽,并让覆盖物层不挡住地图 */
export function configureMapInteraction(map: TiandituMap) {
invokeMapMethod(map, 'enableScrollWheelZoom')
invokeMapMethod(map, 'enableDrag')
invokeMapMethod(map, 'enableInertia')
invokeMapMethod(map, 'enableAutoResize')
const setOptions = (map as TiandituMap & { setOptions?: (opts: { drag?: boolean }) => void })
.setOptions
setOptions?.({ drag: true })
}
/**
* HTML SDK panBy
* SDK
*/
export function bindMapDragPan(
map: TiandituMap,
T: NonNullable<typeof window.T>,
options: { ignoreSelector?: string } = {},
): () => void {
const container = map.getContainer?.()
if (!container || !map.panBy) return () => {}
const root: HTMLElement = container
invokeMapMethod(map, 'disableDrag')
const ignoreSelector =
options.ignoreSelector ?? '.slake-map-school-marker, .slake-map-school-marker *'
let panning = false
let lastX = 0
let lastY = 0
let pendingDx = 0
let pendingDy = 0
let panRaf = 0
function shouldIgnore(target: EventTarget | null) {
return target instanceof Element && Boolean(target.closest(ignoreSelector))
}
function startPan(clientX: number, clientY: number) {
panning = true
lastX = clientX
lastY = clientY
root.style.cursor = 'grabbing'
}
function movePan(clientX: number, clientY: number) {
if (!panning) return
const dx = clientX - lastX
const dy = clientY - lastY
if (!dx && !dy) return
lastX = clientX
lastY = clientY
pendingDx += dx
pendingDy += dy
if (panRaf) return
panRaf = requestAnimationFrame(() => {
panRaf = 0
if (!pendingDx && !pendingDy) return
map.panBy?.(new T.Point(-pendingDx, -pendingDy))
pendingDx = 0
pendingDy = 0
})
}
function endPan() {
if (!panning) return
panning = false
root.style.cursor = 'grab'
}
const onMouseDown = (e: MouseEvent) => {
if (e.button !== 0 || shouldIgnore(e.target)) return
startPan(e.clientX, e.clientY)
e.preventDefault()
}
const onMouseMove = (e: MouseEvent) => {
movePan(e.clientX, e.clientY)
}
const onMouseUp = () => endPan()
const onTouchStart = (e: TouchEvent) => {
if (shouldIgnore(e.target) || e.touches.length !== 1) return
startPan(e.touches[0].clientX, e.touches[0].clientY)
}
const onTouchMove = (e: TouchEvent) => {
if (!panning || e.touches.length !== 1) return
movePan(e.touches[0].clientX, e.touches[0].clientY)
e.preventDefault()
}
const onTouchEnd = () => endPan()
root.style.cursor = 'grab'
root.addEventListener('mousedown', onMouseDown)
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp)
root.addEventListener('touchstart', onTouchStart, { passive: false })
root.addEventListener('touchmove', onTouchMove, { passive: false })
root.addEventListener('touchend', onTouchEnd)
root.addEventListener('touchcancel', onTouchEnd)
return () => {
if (panRaf) {
cancelAnimationFrame(panRaf)
panRaf = 0
}
root.removeEventListener('mousedown', onMouseDown)
window.removeEventListener('mousemove', onMouseMove)
window.removeEventListener('mouseup', onMouseUp)
root.removeEventListener('touchstart', onTouchStart)
root.removeEventListener('touchmove', onTouchMove)
root.removeEventListener('touchend', onTouchEnd)
root.removeEventListener('touchcancel', onTouchEnd)
root.style.cursor = ''
}
}
interface SchoolMapOverlayInstance {
lnglat: TiandituLngLat
options: SchoolMapOverlayOptions
@ -204,13 +441,23 @@ function getSchoolMapOverlayClass(T: NonNullable<typeof window.T>) {
if (this.options.active) div.classList.add('is-active')
div.setAttribute('role', 'button')
div.setAttribute('tabindex', '0')
div.style.pointerEvents = 'auto'
div.innerHTML =
'<span class="slake-map-school-dot" aria-hidden="true"></span>' +
`<span class="slake-map-school-label">${escapeMapLabelHtml(this.options.name.trim() || '—')}</span>`
this._div = div
map.getPanes?.().overlayPane.appendChild(div)
this._onMapChange = () => this.update()
const panes = map.getPanes?.()
const overlayPane = panes?.overlayPane ?? panes?.markerPane
if (overlayPane) overlayPane.appendChild(div)
let moveRaf = 0
this._onMapChange = () => {
if (moveRaf) return
moveRaf = requestAnimationFrame(() => {
moveRaf = 0
this.update()
})
}
map.addEventListener?.('move', this._onMapChange)
map.addEventListener?.('zoomend', this._onMapChange)

@ -13,10 +13,15 @@ import TeacherDetailDialog from '@/views/teachers/components/TeacherDetailDialog
import { fetchRadarMap, type RadarMapData, type RadarSchool } from '@/api/admin/assets'
import { starDisplay } from '@/utils/teacherStar'
import {
applyOverlayPassthrough,
bindMapDragPan,
centerMapOnSuzhou,
configureMapInteraction,
createSchoolMapOverlay,
createTiandituMap,
getTiandituKey,
loadTianditu,
scheduleMapResize,
type TiandituMap,
type TiandituSchoolOverlay,
} from '@/utils/tiandituMap'
@ -32,7 +37,8 @@ const detailTeacherId = ref<number | null>(null)
const mapContainerRef = ref<HTMLElement | null>(null)
let mapInstance: TiandituMap | null = null
let schoolOverlays: { overlay: TiandituSchoolOverlay; schoolId: number }[] = []
let unbindDragPan: (() => void) | null = null
let schoolOverlays: { school: RadarSchool; overlay: TiandituSchoolOverlay }[] = []
const summary = computed(() => mapData.value?.summary)
const quality = computed(() => mapData.value?.quality || [])
@ -57,13 +63,8 @@ function openTeacher(id: number) {
function selectSchool(school: RadarSchool) {
activeSchool.value = school
refreshMarkerActiveState()
}
function refreshMarkerActiveState() {
const activeId = activeSchool.value?.id ?? null
for (const { overlay, schoolId } of schoolOverlays) {
overlay.setActive?.(schoolId === activeId)
for (const item of schoolOverlays) {
item.overlay.setActive?.(item.school.id === school.id)
}
}
@ -105,28 +106,38 @@ async function initMap() {
const container = mapContainerRef.value
container.innerHTML = ''
mapInstance = new T.Map(container)
mapInstance.enableScrollWheelZoom()
mapInstance = createTiandituMap(T, container)
configureMapInteraction(mapInstance)
centerMapOnSuzhou(mapInstance, T)
schoolOverlays = []
const activeId = activeSchool.value?.id ?? null
for (const school of schools) {
const overlay = createSchoolMapOverlay(
T,
{ name: school.name, longitude: school.longitude, latitude: school.latitude },
activeId === school.id,
activeSchool.value?.id === school.id,
)
mapInstance.addOverLay(overlay)
overlay.addEventListener('click', () => selectSchool(school))
schoolOverlays.push({ overlay, schoolId: school.id })
mapInstance.addOverLay(overlay)
schoolOverlays.push({ school, overlay })
}
centerMapOnSuzhou(mapInstance, T)
// / panBy
applyOverlayPassthrough(mapInstance)
unbindDragPan = bindMapDragPan(mapInstance, T)
scheduleMapResize(mapInstance)
const reapplyPassthrough = () => {
if (mapInstance) applyOverlayPassthrough(mapInstance)
}
mapInstance.addEventListener?.('load', reapplyPassthrough)
window.setTimeout(reapplyPassthrough, 300)
window.setTimeout(reapplyPassthrough, 800)
} catch (e) {
const msg = e instanceof Error ? e.message : '地图初始化失败'
mapError.value =
msg.includes('脚本') || msg.includes('SDK')
? `${msg}。若 Key 已配置域名白名单,请使用 https://slake.ali251.langye.net 访问(本地可在 hosts 绑定该域名后访问 http://slake.ali251.langye.net:5173`
? `${msg}。若 Key 已配置域名白名单,请使用 https://slake.ali251.langye.net 访问(本地开发可在 hosts 绑定该域名后访问 http://slake.ali251.langye.net:5173`
: msg
destroyMap()
} finally {
@ -135,15 +146,11 @@ async function initMap() {
}
function destroyMap() {
unbindDragPan?.()
unbindDragPan = null
if (mapInstance) {
for (const { overlay } of schoolOverlays) {
try {
mapInstance.removeOverLay(overlay)
} catch {
/* 覆盖物可能已被地图销毁 */
}
}
mapInstance.clearOverLays?.()
mapInstance.destroy?.()
}
schoolOverlays = []
mapInstance = null
@ -210,10 +217,11 @@ onBeforeUnmount(destroyMap)
</div>
<div
v-else
ref="mapContainerRef"
v-loading="mapLoading"
class="radar-map-container"
/>
class="radar-map-stage"
>
<div ref="mapContainerRef" class="radar-map-container" />
</div>
</div>
<aside class="radar-side">
<div class="radar-side-head">

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref } from 'vue'
import { nextTick, ref } from 'vue'
import { usePageLoad } from '@/composables/usePageLoad'
import {
createUniversity,
@ -96,7 +96,12 @@ function openMapPick() {
}
function onMapPickOpened() {
mapPickReady.value = true
mapPickReady.value = false
nextTick(() => {
window.setTimeout(() => {
mapPickReady.value = true
}, 800)
})
}
function onMapPickClosed() {
@ -255,7 +260,7 @@ usePageLoad(load)
当前选点{{ pickDraft.longitude }}{{ pickDraft.latitude }}
</p>
<TiandituPickMap
v-if="mapPickReady"
v-if="mapPickVisible && mapPickReady"
v-model:longitude="pickDraft.longitude"
v-model:latitude="pickDraft.latitude"
:default-keyword="form.name"

@ -14,8 +14,19 @@ import {
type DemandRow,
} from '@/api/admin/demands'
import { demandStatusClass, demandTypeClass } from '@/utils/admin-list'
import { useAuthStore } from '@/stores/auth'
import { ElMessage, ElMessageBox } from 'element-plus'
const auth = useAuthStore()
function defaultFollowAdminUserId() {
const currentId = auth.user?.id
if (currentId != null && adminOptions.value.some((a) => a.id === currentId)) {
return currentId
}
return adminOptions.value[0]?.id
}
const loading = ref(false)
const items = ref<DemandRow[]>([])
const meta = ref({ current_page: 1, per_page: 20, total: 0 })
@ -105,7 +116,7 @@ function openFollow(row: DemandRow) {
followRow.value = row
followForm.value = {
handled_at: new Date().toISOString().slice(0, 10),
admin_user_id: adminOptions.value[0]?.id,
admin_user_id: defaultFollowAdminUserId(),
status_dict_item_id: row.status_dict_item_id,
content: '',
next_plan: '',

@ -29,6 +29,14 @@ import { ElMessage } from 'element-plus'
const auth = useAuthStore()
const isGridMember = computed(() => auth.isGridMember)
function defaultFollowAdminUserId() {
const currentId = auth.user?.id
if (currentId != null && adminOptions.value.some((a) => a.id === currentId)) {
return currentId
}
return adminOptions.value[0]?.id
}
const loading = ref(false)
const detailVisible = ref(false)
const detailTeacherId = ref<number | null>(null)
@ -374,7 +382,7 @@ function openFollow(row: TeacherRow) {
followed_at: new Date().toISOString().slice(0, 10),
follow_method_dict_item_id: methodOptions.value[0]?.id,
urgency_dict_item_id: urgencyOptions.value.find((u) => u.value === 'normal')?.id,
admin_user_id: adminOptions.value[0]?.id,
admin_user_id: defaultFollowAdminUserId(),
next_follow_subject: '',
next_follow_date: previewNextFollowDate(row.star_level_item?.value) || '',
}
@ -565,7 +573,7 @@ usePageLoad(async () => {
<span v-else class="text-muted"></span>
</template>
</el-table-column>
<el-table-column label="来源" width="76" align="center">
<el-table-column label="来源" width="108" align="center">
<template #default="{ row }">
<el-tag
:type="sourceTagType(row.source_item?.value)"

Loading…
Cancel
Save