Skip to content

Commit 469116f

Browse files
committed
feat(chat): 支持位置消息与小程序卡片解析展示
- 新增位置消息解析,补充经纬度、地点名和地址字段 - 修复小程序分享 type 识别,避免嵌套 type 干扰 - 聊天页新增位置卡片展示,并补充小程序卡片样式 - 导出、搜索和会话预览同步支持位置消息 - 补充位置导出与小程序解析测试
1 parent 3c5452f commit 469116f

9 files changed

Lines changed: 890 additions & 61 deletions

File tree

Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
<template>
2+
<div
3+
class="wechat-location-card-wrap"
4+
:class="isSent ? 'wechat-location-card-wrap--sent' : 'wechat-location-card-wrap--received'"
5+
>
6+
<div
7+
class="wechat-location-card"
8+
:class="{ 'wechat-location-card--sent': isSent }"
9+
role="button"
10+
tabindex="0"
11+
@click="openLocation"
12+
@keydown.enter.prevent="openLocation"
13+
@keydown.space.prevent="openLocation"
14+
>
15+
<div class="wechat-location-card__text">
16+
<div class="wechat-location-card__title">{{ primaryText }}</div>
17+
<div v-if="secondaryText" class="wechat-location-card__subtitle">{{ secondaryText }}</div>
18+
</div>
19+
20+
<div class="wechat-location-card__map" :class="{ 'wechat-location-card__map--placeholder': !mapTileUrl }">
21+
<img
22+
v-if="mapTileUrl"
23+
:src="mapTileUrl"
24+
alt="地图预览"
25+
class="wechat-location-card__map-image"
26+
loading="lazy"
27+
referrerpolicy="no-referrer"
28+
>
29+
<div class="wechat-location-card__map-overlay"></div>
30+
<div class="wechat-location-card__pin" :style="markerStyle" aria-hidden="true">
31+
<svg viewBox="0 0 24 24" fill="none">
32+
<path d="M12 22s7-5.82 7-12a7 7 0 1 0-14 0c0 6.18 7 12 7 12Z" fill="#22c55e" />
33+
<circle cx="12" cy="10" r="3.2" fill="#ffffff" />
34+
</svg>
35+
</div>
36+
</div>
37+
</div>
38+
</div>
39+
</template>
40+
41+
<script setup>
42+
const props = defineProps({
43+
message: { type: Object, default: () => ({}) },
44+
})
45+
46+
const TILE_SIZE = 256
47+
const MAP_ZOOM = 15
48+
49+
const cleanText = (value) => String(value || '').replace(/\s+/g, ' ').trim()
50+
51+
const toFiniteNumber = (value) => {
52+
const num = Number.parseFloat(String(value ?? '').trim())
53+
return Number.isFinite(num) ? num : null
54+
}
55+
56+
const latitude = computed(() => {
57+
const num = toFiniteNumber(props.message?.locationLat)
58+
return num != null && Math.abs(num) <= 90 ? num : null
59+
})
60+
61+
const longitude = computed(() => {
62+
const num = toFiniteNumber(props.message?.locationLng)
63+
return num != null && Math.abs(num) <= 180 ? num : null
64+
})
65+
66+
const primaryText = computed(() => {
67+
return cleanText(
68+
props.message?.locationPoiname
69+
|| props.message?.title
70+
|| props.message?.content
71+
|| '位置'
72+
) || '位置'
73+
})
74+
75+
const secondaryText = computed(() => {
76+
const label = cleanText(props.message?.locationLabel)
77+
return label && label !== primaryText.value ? label : ''
78+
})
79+
80+
const isSent = computed(() => !!props.message?.isSent)
81+
82+
const mapTileMeta = computed(() => {
83+
const lat = latitude.value
84+
const lng = longitude.value
85+
if (lat == null || lng == null) return null
86+
87+
const scale = Math.pow(2, MAP_ZOOM)
88+
const worldX = ((lng + 180) / 360) * scale * TILE_SIZE
89+
const latRad = (lat * Math.PI) / 180
90+
const worldY = ((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2) * scale * TILE_SIZE
91+
const tileX = Math.floor(worldX / TILE_SIZE)
92+
const tileY = Math.floor(worldY / TILE_SIZE)
93+
const offsetX = worldX - tileX * TILE_SIZE
94+
const offsetY = worldY - tileY * TILE_SIZE
95+
96+
return {
97+
tileX,
98+
tileY,
99+
left: `${(offsetX / TILE_SIZE) * 100}%`,
100+
top: `${(offsetY / TILE_SIZE) * 100}%`,
101+
}
102+
})
103+
104+
const mapTileUrl = computed(() => {
105+
const meta = mapTileMeta.value
106+
if (!meta) return ''
107+
return `https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x=${meta.tileX}&y=${meta.tileY}&z=${MAP_ZOOM}`
108+
})
109+
110+
const markerStyle = computed(() => {
111+
const meta = mapTileMeta.value
112+
return {
113+
left: meta?.left || '50%',
114+
top: meta?.top || '50%',
115+
}
116+
})
117+
118+
const mapLink = computed(() => {
119+
const name = encodeURIComponent(primaryText.value || secondaryText.value || '位置')
120+
const lat = latitude.value
121+
const lng = longitude.value
122+
if (lat != null && lng != null) {
123+
return `https://uri.amap.com/marker?position=${lng},${lat}&name=${name}`
124+
}
125+
if (name) return `https://uri.amap.com/search?keyword=${name}`
126+
return ''
127+
})
128+
129+
const openLocation = () => {
130+
if (!process.client) return
131+
const href = mapLink.value
132+
if (!href) return
133+
window.open(href, '_blank', 'noopener,noreferrer')
134+
}
135+
</script>
136+
137+
<style scoped>
138+
.wechat-location-card-wrap {
139+
position: relative;
140+
display: inline-block;
141+
}
142+
143+
.wechat-location-card-wrap--received::before,
144+
.wechat-location-card-wrap--sent::after {
145+
content: '';
146+
position: absolute;
147+
top: 12px;
148+
width: 12px;
149+
height: 12px;
150+
background: #fff;
151+
transform: rotate(45deg);
152+
border-radius: 2px;
153+
}
154+
155+
.wechat-location-card-wrap--received::before {
156+
left: -4px;
157+
}
158+
159+
.wechat-location-card-wrap--sent::after {
160+
right: -4px;
161+
}
162+
163+
.wechat-location-card {
164+
width: 208px;
165+
overflow: hidden;
166+
border-radius: var(--message-radius);
167+
border: none;
168+
background: #fff;
169+
box-shadow: none;
170+
cursor: pointer;
171+
transition: opacity 0.15s ease;
172+
}
173+
174+
.wechat-location-card--sent {
175+
background: #fff;
176+
}
177+
178+
.wechat-location-card__text {
179+
padding: 10px 12px 8px;
180+
background: #fff;
181+
}
182+
183+
.wechat-location-card--sent .wechat-location-card__text {
184+
background: #fff;
185+
}
186+
187+
.wechat-location-card__title {
188+
color: #111827;
189+
font-size: 13px;
190+
font-weight: 500;
191+
line-height: 1.4;
192+
display: -webkit-box;
193+
-webkit-box-orient: vertical;
194+
-webkit-line-clamp: 2;
195+
overflow: hidden;
196+
}
197+
198+
.wechat-location-card__subtitle {
199+
margin-top: 4px;
200+
color: #9ca3af;
201+
font-size: 11px;
202+
line-height: 1.4;
203+
white-space: nowrap;
204+
overflow: hidden;
205+
text-overflow: ellipsis;
206+
}
207+
208+
.wechat-location-card--sent .wechat-location-card__subtitle {
209+
color: #9ca3af;
210+
}
211+
212+
.wechat-location-card__map {
213+
position: relative;
214+
height: 98px;
215+
overflow: hidden;
216+
background:
217+
linear-gradient(0deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.3)),
218+
linear-gradient(135deg, #d7eef5 0%, #f6f8fb 45%, #ece7cf 100%);
219+
}
220+
221+
.wechat-location-card__map--placeholder::before {
222+
content: '';
223+
position: absolute;
224+
inset: 0;
225+
background-image:
226+
linear-gradient(90deg, rgba(255,255,255,0.65) 0 8%, transparent 8% 34%, rgba(255,255,255,0.65) 34% 42%, transparent 42% 100%),
227+
linear-gradient(0deg, rgba(255,255,255,0.7) 0 10%, transparent 10% 38%, rgba(255,255,255,0.7) 38% 46%, transparent 46% 100%);
228+
opacity: 0.65;
229+
}
230+
231+
.wechat-location-card__map-image,
232+
.wechat-location-card__map-overlay {
233+
position: absolute;
234+
inset: 0;
235+
}
236+
237+
.wechat-location-card__map-image {
238+
display: block;
239+
width: 100%;
240+
height: 100%;
241+
object-fit: cover;
242+
}
243+
244+
.wechat-location-card__map-overlay {
245+
background: linear-gradient(180deg, rgba(255,255,255,0.04) 0%, rgba(255,255,255,0) 38%, rgba(17,24,39,0.06) 100%);
246+
}
247+
248+
.wechat-location-card__pin {
249+
position: absolute;
250+
width: 22px;
251+
height: 22px;
252+
transform: translate(-50%, -92%);
253+
filter: drop-shadow(0 4px 8px rgba(34, 197, 94, 0.28));
254+
}
255+
256+
.wechat-location-card__pin svg {
257+
display: block;
258+
width: 100%;
259+
height: 100%;
260+
}
261+
</style>

frontend/components/EditedMessagePreview.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@
8383
[语音]
8484
</div>
8585

86+
<div v-else-if="renderType === 'location'" class="max-w-sm">
87+
<ChatLocationCard :message="message" />
88+
</div>
89+
8690
<!-- 默认文本消息 -->
8791
<div
8892
v-else

0 commit comments

Comments
 (0)