修复一些问题

This commit is contained in:
李琦
2026-07-08 08:18:02 +08:00
parent 41e56b3be6
commit 497d0a840f
43 changed files with 1078 additions and 554 deletions

View File

@@ -254,7 +254,7 @@ watch(() => props.remoteStream, (stream) => {
/* #endif */
/* #ifndef MP-WEIXIN */
top: calc(var(--status-bar-height, 0) + 240rpx);
top: calc(var(--status-bar-height, 20px) + 240rpx);
/* #endif */
}
}
@@ -367,7 +367,7 @@ watch(() => props.remoteStream, (stream) => {
/* #endif */
/* #ifndef MP-WEIXIN */
top: calc(28rpx + var(--status-bar-height, 0));
top: calc(28rpx + var(--status-bar-height, 20px));
/* #endif */
align-items: center;
justify-content: center;

View File

@@ -1,5 +1,5 @@
<template>
<!-- ========== H5/App 单聊通话组件 ========== -->
<!-- H5/App single call -->
<!-- #ifdef H5 || APP-PLUS -->
<call-window
v-if="h5CallActive"
@@ -16,19 +16,14 @@
/>
<!-- #endif -->
<!-- ========== 微信小程序单聊通话组件 ========== -->
<!-- WeChat mini program single call -->
<!-- #ifdef MP-WEIXIN -->
<mini-program-call-window />
<!-- #endif -->
<!-- ========== 群通话组件(所有平台共用) ========== -->
<!-- 全局群通话来电覆盖层 -->
<!-- Group call (all platforms) -->
<group-call-incoming />
<!-- 全局群通话窗口 -->
<group-call-window />
<!-- 群聊通话横幅 (有正在进行的通话但用户未加入) -->
<group-call-banner />
</template>
@@ -64,26 +59,20 @@ import MiniProgramCallWindow from './MiniProgramCallWindow.vue'
const mpCall = useMiniProgramCall()
// #endif
// 群通话
const { initListener: initGroupListener } = useGroupWebRTC()
onMounted(() => {
// #ifdef H5 || APP-PLUS
// 初始化单聊通话信令监听器 (H5/App)
webrtc.initListener()
console.log('✅ [GlobalCallProvider] H5/App WebRTC 信令监听器已初始化')
console.log('[GlobalCallProvider] H5/App WebRTC listener initialized')
// #endif
// #ifdef MP-WEIXIN
// 初始化小程序通话信令监听器
// 注意:initPusherContext 不在这里调用,因为此时 live-pusher 组件还未渲染
// live-pusher 的 Context 初始化由 MiniProgramCallWindow.vue 在 pushUrl 设置后处理
mpCall.initListener()
console.log('✅ [GlobalCallProvider] 微信小程序通话监听器已初始化')
console.log('[GlobalCallProvider] MP call listener initialized')
// #endif
// 初始化群通话信令监听器(所有平台)
initGroupListener()
console.log('✅ [GlobalCallProvider] 群通话信令监听器已初始化')
console.log('[GlobalCallProvider] Group call listener initialized')
})
</script>

View File

@@ -179,7 +179,7 @@ function handleClose() {
.modal-container {
position: fixed;
inset: 0;
z-index: 1000;
z-index: 2100;
--bg-surface: #fff;
--bg-hover: #f5f5f5;

View File

@@ -1,6 +1,5 @@
<template>
<view v-if="show" class="emoji-picker" :class="{ dark: isDark }">
<!-- 分类标签 -->
<view class="emoji-tabs">
<view
v-for="(cat, idx) in categories"
@@ -13,7 +12,6 @@
</view>
</view>
<!-- 表情列表 -->
<scroll-view scroll-y class="emoji-grid-container">
<view class="emoji-grid">
<view
@@ -33,7 +31,7 @@
import { ref, computed } from 'vue'
import { useTheme } from '@/composables/useTheme'
const props = defineProps<{
defineProps<{
show: boolean
}>()
@@ -49,87 +47,67 @@ const activeTab = ref(0)
const categories = [
{
key: 'face',
icon: '😀',
icon: '\u{1F600}',
emojis: [
'😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂',
'🙂', '🙃', '😉', '😊', '😇', '🥰', '😍', '🤩',
'😘', '😗', '☺️', '😚', '😙', '🥲', '😋', '😛',
'😜', '🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔',
'🤐', '🤨', '😐', '😑', '😶', '😏', '😒', '🙄',
'😬', '🤥', '😌', '😔', '😪', '🤤', '😴', '😷',
'🤒', '🤕', '🤢', '🤮', '🤧', '🥵', '🥶', '🥴',
'😵', '🤯', '🤠', '🥳', '🥸', '😎', '🤓', '🧐'
'\u{1F600}', '\u{1F603}', '\u{1F604}', '\u{1F601}', '\u{1F606}', '\u{1F605}', '\u{1F923}', '\u{1F602}',
'\u{1F642}', '\u{1F643}', '\u{1F609}', '\u{1F60A}', '\u{1F607}', '\u{1F970}', '\u{1F60D}', '\u{1F929}',
'\u{1F618}', '\u{1F617}', '\u{263A}\u{FE0F}', '\u{1F61A}', '\u{1F619}', '\u{1F972}', '\u{1F60B}', '\u{1F61B}',
'\u{1F61C}', '\u{1F92A}', '\u{1F61D}', '\u{1F911}', '\u{1F917}', '\u{1F92D}', '\u{1F92B}', '\u{1F914}',
'\u{1F910}', '\u{1F928}', '\u{1F610}', '\u{1F611}', '\u{1F636}', '\u{1F60F}', '\u{1F612}', '\u{1F644}',
'\u{1F62C}', '\u{1F925}', '\u{1F60C}', '\u{1F614}', '\u{1F62A}', '\u{1F924}', '\u{1F634}', '\u{1F637}',
'\u{1F912}', '\u{1F915}', '\u{1F922}', '\u{1F92E}', '\u{1F927}', '\u{1F975}', '\u{1F976}', '\u{1F974}',
'\u{1F635}', '\u{1F92F}', '\u{1F920}', '\u{1F973}', '\u{1F978}', '\u{1F60E}', '\u{1F913}', '\u{1F9D0}'
]
},
{
key: 'gesture',
icon: '👋',
icon: '\u{1F44B}',
emojis: [
'👋', '🤚', '🖐️', '✋', '🖖', '👌', '🤌', '🤏',
'✌️', '🤞', '🤟', '🤘', '🤙', '👈', '👉', '👆',
'🖕', '👇', '☝️', '👍', '👎', '✊', '👊', '🤛',
'🤜', '👏', '🙌', '👐', '🤲', '🤝', '🙏', '✍️',
'💪', '🦾', '🦿', '🦵', '🦶', '👂', '🦻', '👃'
'\u{1F44B}', '\u{1F91A}', '\u{1F590}\u{FE0F}', '\u{270B}', '\u{1F596}', '\u{1F44C}', '\u{1F90C}', '\u{1F90F}',
'\u{270C}\u{FE0F}', '\u{1F91E}', '\u{1F91F}', '\u{1F918}', '\u{1F919}', '\u{1F448}', '\u{1F449}', '\u{1F446}',
'\u{1F595}', '\u{1F447}', '\u{261D}\u{FE0F}', '\u{1F44D}', '\u{1F44E}', '\u{270A}', '\u{1F44A}', '\u{1F91B}',
'\u{1F91C}', '\u{1F44F}', '\u{1F64C}', '\u{1F450}', '\u{1F932}', '\u{1F91D}', '\u{1F64F}', '\u{270D}\u{FE0F}',
'\u{1F4AA}', '\u{1F9BE}', '\u{1F9BF}', '\u{1F9B5}', '\u{1F9B6}', '\u{1F442}', '\u{1F9BB}', '\u{1F443}'
]
},
{
key: 'heart',
icon: '❤️',
icon: '\u{2764}\u{FE0F}',
emojis: [
'❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍',
'🤎', '💔', '❣️', '💕', '💞', '💓', '💗', '💖',
'💘', '💝', '💟', '☮️', '✝️', '☪️', '🕉️', '☸️',
'✡️', '🔯', '🕎', '☯️', '☦️', '🛐', '⛎', '♈'
'\u{2764}\u{FE0F}', '\u{1F9E1}', '\u{1F49B}', '\u{1F49A}', '\u{1F499}', '\u{1F49C}', '\u{1F5A4}', '\u{1F90D}',
'\u{1F90E}', '\u{1F494}', '\u{2763}\u{FE0F}', '\u{1F495}', '\u{1F49E}', '\u{1F493}', '\u{1F497}', '\u{1F496}',
'\u{1F498}', '\u{1F49D}', '\u{1F49F}', '\u{262E}\u{FE0F}', '\u{271D}\u{FE0F}', '\u{262A}\u{FE0F}', '\u{1F549}\u{FE0F}', '\u{2638}\u{FE0F}',
'\u{2721}\u{FE0F}', '\u{1F52F}', '\u{1F54E}', '\u{262F}\u{FE0F}', '\u{2626}\u{FE0F}', '\u{1F6D0}', '\u{26CE}', '\u{2648}'
]
},
{
key: 'animal',
icon: '🐶',
icon: '\u{1F436}',
emojis: [
'🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼',
'🐻‍❄️', '🐨', '🐯', '🦁', '🐮', '🐷', '🐸', '🐵',
'🙈', '🙉', '🙊', '🐒', '🐔', '🐧', '🐦', '🐤',
'🐣', '🐥', '🦆', '🦅', '🦉', '🦇', '🐺', '🐗'
'\u{1F436}', '\u{1F431}', '\u{1F42D}', '\u{1F439}', '\u{1F430}', '\u{1F98A}', '\u{1F43B}', '\u{1F43C}',
'\u{1F428}', '\u{1F42F}', '\u{1F981}', '\u{1F42E}', '\u{1F437}', '\u{1F438}', '\u{1F435}', '\u{1F648}',
'\u{1F649}', '\u{1F64A}', '\u{1F412}', '\u{1F414}', '\u{1F427}', '\u{1F426}', '\u{1F424}', '\u{1F423}',
'\u{1F425}', '\u{1F986}', '\u{1F985}', '\u{1F989}', '\u{1F987}', '\u{1F41D}', '\u{1F41B}', '\u{1F98B}'
]
},
{
key: 'food',
icon: '🍔',
icon: '\u{1F354}',
emojis: [
'🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓',
'🫐', '🍈', '🍒', '🍑', '🥭', '🍍', '🥥', '🥝',
'🍔', '🍟', '🍕', '🌭', '🥪', '🌮', '🌯', '🥗',
'🍜', '🍝', '🍲', '🍱', '🍣', '🍤', '🍿', '🧁'
]
},
{
key: 'activity',
icon: '⚽',
emojis: [
'⚽', '🏀', '🏈', '⚾', '🥎', '🎾', '🏐', '🏉',
'🥏', '🎱', '🪀', '🏓', '🏸', '🏒', '🏑', '🥍',
'🏏', '🪃', '🥅', '⛳', '🪁', '🏹', '🎣', '🤿',
'🥊', '🥋', '🎽', '🛹', '🛼', '🛷', '⛸️', '🥌'
]
},
{
key: 'object',
icon: '💡',
emojis: [
'⌚', '📱', '💻', '⌨️', '🖥️', '🖨️', '🖱️', '🖲️',
'💽', '💾', '💿', '📀', '📼', '📷', '📸', '📹',
'🎥', '📽️', '🎞️', '📞', '☎️', '📟', '📠', '📺',
'📻', '🎙️', '🎚️', '🎛️', '🧭', '⏱️', '⏲️', '⏰'
'\u{1F34E}', '\u{1F34A}', '\u{1F34C}', '\u{1F347}', '\u{1F353}', '\u{1F349}', '\u{1F345}', '\u{1F346}',
'\u{1F955}', '\u{1F33D}', '\u{1F336}', '\u{1F952}', '\u{1F96C}', '\u{1F966}', '\u{1F9C4}', '\u{1F9C5}',
'\u{1F354}', '\u{1F35F}', '\u{1F355}', '\u{1F32D}', '\u{1F96A}', '\u{1F32E}', '\u{1F32F}', '\u{1F957}',
'\u{1F35D}', '\u{1F35C}', '\u{1F372}', '\u{1F35B}', '\u{1F363}', '\u{1F371}', '\u{1F35A}', '\u{1F359}'
]
},
{
key: 'symbol',
icon: '🔥',
icon: '\u{1F525}',
emojis: [
'🔥', '💥', '✨', '🌟', '⭐', '🌈', '☀️', '🌤️',
'⛅', '🌥️', '☁️', '🌦️', '🌧️', '⛈️', '🌩️', '🌨️',
'❄️', '☃️', '⛄', '🌬️', '💨', '💧', '💦', '☔',
'🎵', '🎶', '🎼', '🎹', '🥁', '🎷', '🎺', '🎸'
'\u{1F525}', '\u{1F4A5}', '\u{2728}', '\u{1F31F}', '\u{2B50}', '\u{1F308}', '\u{2600}\u{FE0F}', '\u{1F324}\u{FE0F}',
'\u{26C5}', '\u{1F325}', '\u{2601}\u{FE0F}', '\u{1F326}', '\u{1F327}', '\u{26C8}', '\u{1F329}', '\u{1F328}',
'\u{2744}\u{FE0F}', '\u{2603}\u{FE0F}', '\u{26C4}', '\u{1F32C}', '\u{1F4A8}', '\u{1F4A7}', '\u{1F4A6}', '\u{2614}',
'\u{1F3B5}', '\u{1F3B6}', '\u{1F3BC}', '\u{1F3B9}', '\u{1F941}', '\u{1F3A7}', '\u{1F3BA}', '\u{1F3B8}'
]
}
]
@@ -173,38 +151,22 @@ function selectEmoji(emoji: string) {
.emoji-tabs {
display: flex;
padding: 16rpx 24rpx;
border-bottom: 1rpx solid var(--border-color, #e5e7eb);
gap: 8rpx;
border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
overflow-x: auto;
.dark & {
border-bottom-color: #44403c;
}
.tab-item {
flex-shrink: 0;
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12rpx;
font-size: 36rpx;
&::-webkit-scrollbar {
display: none;
}
}
.tab-item {
min-width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 16rpx;
font-size: 40rpx;
transition: all 0.2s;
&:active {
transform: scale(0.95);
}
&.active {
background: rgba(79, 70, 229, 0.1);
.dark & {
background: rgba(249, 115, 22, 0.15);
&.active {
background: var(--bg-hover, #f3f4f6);
}
}
}
@@ -214,34 +176,22 @@ function selectEmoji(emoji: string) {
}
.emoji-grid {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 8rpx;
padding: 16rpx 24rpx;
display: flex;
flex-wrap: wrap;
padding: 16rpx;
}
.emoji-item {
aspect-ratio: 1;
width: 12.5%;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 48rpx;
border-radius: 16rpx;
cursor: pointer;
transition: all 0.15s;
&:hover {
background: rgba(0, 0, 0, 0.05);
}
font-size: 44rpx;
&:active {
transform: scale(0.9);
background: rgba(0, 0, 0, 0.1);
}
.dark &:hover {
background: rgba(255, 255, 255, 0.1);
background: var(--bg-hover, rgba(0, 0, 0, 0.05));
border-radius: 8rpx;
}
}
</style>

View File

@@ -10,12 +10,12 @@
/>
</view>
<!-- 内容列 -->
<!-- 消息内容列 -->
<view class="content-col" :class="{ 'items-end': isSelf }">
<!-- 群聊昵称 -->
<!-- 发送者昵称(群聊) -->
<text v-if="senderName" class="sender-name">{{ senderName }}</text>
<!-- 气泡实体 -->
<!-- 消息气泡 -->
<view
class="bubble"
:class="[
@@ -24,7 +24,7 @@
]"
@longpress="$emit('long-press', msg)"
>
<!-- 0. 纯文本 -->
<!-- 0. 文本消息 -->
<text v-if="msg.message_type === 0" class="msg-text" selectable>{{ msg.content }}</text>
<!-- 1. 图片 -->
@@ -73,8 +73,14 @@
</view>
</view>
<text v-else class="msg-text">[不支持的消息类型]</text>
<text v-else class="msg-text">[未知消息类型]</text>
</view>
<!-- 已读回执(仅自己发送的消息) -->
<text v-if="isSelf && msg.is_read" class="read-receipt">已读</text>
<!-- IP 归属地 -->
<text v-if="showIpLocation" class="ip-location">{{ msg.ip_location }}</text>
</view>
</view>
</template>
@@ -105,6 +111,12 @@ defineEmits<{
const avatarName = computed(() => props.senderName || 'U')
/** 是否展示 IP 归属地(过滤本地/未知) */
const showIpLocation = computed(() => {
const loc = props.msg.ip_location
return !!loc && loc !== '本地' && loc !== '未知'
})
const mediaTypeClass = computed(() => {
const type = props.msg.message_type
if (type === 1) return 'media-bubble image'
@@ -114,7 +126,7 @@ const mediaTypeClass = computed(() => {
return ''
})
// Helpers (复用原 index.vue 逻辑)
/** 解析媒体 URL(与 chat/index.vue 保持一致) */
function getMediaUrl(msg: ChatMessage): string {
let url = ''
if (typeof msg.extra === 'string') {
@@ -141,11 +153,8 @@ function getVideoThumb(msg: ChatMessage) {
</script>
<style lang="scss" scoped>
/* * 气泡组件样式
* 完全复用原 index.vue 的 .msg-row, .bubble 及其子元素样式
*/
/* 消息行布局,样式与 chat/index.vue 的 .msg-row、.bubble 保持一致 */
/* 布局 */
.msg-row {
display: flex; align-items: flex-start; gap: 20rpx;
&.is-self { flex-direction: row-reverse; }
@@ -157,35 +166,28 @@ function getVideoThumb(msg: ChatMessage) {
.sender-name { font-size: 20rpx; color: #9ca3af; margin-bottom: 8rpx; margin-left: 8rpx; .theme-dark & { color: #78716c; } }
}
/* 核心气泡 */
.bubble {
padding: 24rpx; border-radius: 32rpx; font-size: 30rpx; line-height: 1.6; position: relative; word-break: break-all; transition: all 0.3s ease;
/* 对方消息 */
&.bubble-other {
background: #ffffff; color: #1f2937; border-bottom-left-radius: 8rpx; box-shadow: 0 2rpx 4rpx rgba(0,0,0,0.05);
.theme-dark & { background: #262626; color: #e7e5e4; border: 2rpx solid #292524; box-shadow: none; }
}
/* 自己消息 */
&.bubble-self {
background: linear-gradient(135deg, #6366F1, #4F46E5); color: #ffffff; border-bottom-right-radius: 8rpx; box-shadow: 0 4rpx 16rpx rgba(79, 70, 229, 0.2);
.theme-dark & { background: linear-gradient(135deg, #fb923c, #ea580c); box-shadow: 0 4rpx 16rpx rgba(234, 88, 12, 0.3); }
}
/* 媒体容器 */
&.media-bubble { padding: 0; background: transparent !important; box-shadow: none !important; border: none !important; }
}
/* 媒体类型 */
.image-bubble .msg-image { width: 360rpx; border-radius: 32rpx; background: #f3f4f6; border: 2rpx solid #f3f4f6; .theme-dark & { background: #292524; border-color: #292524; } }
/* 语音胶囊 */
.audio-capsule {
display: flex; align-items: center; gap: 20rpx; min-width: 180rpx; padding: 24rpx; background: #ffffff; border-radius: 32rpx;
.theme-dark & { background: #262626; border: 2rpx solid #292524; }
/* 自己的语音 */
.is-self & { background: linear-gradient(135deg, #6366F1, #4F46E5) !important; border: none !important; .theme-dark & { background: linear-gradient(135deg, #fb923c, #ea580c) !important; } }
.play-icon { width: 32rpx; height: 32rpx; color: #6b7280; .theme-dark & { color: #a8a29e; } .is-self & { color: rgba(255,255,255,0.9); } }
@@ -202,7 +204,6 @@ function getVideoThumb(msg: ChatMessage) {
}
@keyframes wave { 0%, 100% { height: 8rpx; } 50% { height: 24rpx; } }
/* 视频预览 */
.video-preview {
position: relative; width: 360rpx; height: 200rpx; border-radius: 32rpx; overflow: hidden; background: #000;
.video-thumb { width: 100%; height: 100%; opacity: 0.9; }
@@ -210,7 +211,6 @@ function getVideoThumb(msg: ChatMessage) {
.play-btn-circle { width: 80rpx; height: 80rpx; background: rgba(0,0,0,0.3); backdrop-filter: blur(4px); border-radius: 50%; display: flex; align-items: center; justify-content: center; border: 2rpx solid rgba(255,255,255,0.3); color: #fff; svg { width: 32rpx; height: 32rpx; } }
}
/* 文件卡片 */
.file-card {
background: #ffffff; padding: 24rpx; border-radius: 32rpx; display: flex; align-items: center; gap: 24rpx; min-width: 400rpx; border: 2rpx solid #f3f4f6;
.theme-dark & { background: #262626; border-color: #292524; }
@@ -225,4 +225,18 @@ function getVideoThumb(msg: ChatMessage) {
}
.file-meta { flex: 1; display: flex; flex-direction: column; overflow: hidden; .f-name { font-size: 28rpx; font-weight: 500; color: #1f2937; .theme-dark & { color: #e7e5e4; } } .f-size { font-size: 22rpx; color: #9ca3af; .theme-dark & { color: #78716c; } } }
}
.read-receipt {
font-size: 20rpx;
color: var(--text-tertiary, #9ca3af);
margin-top: 4rpx;
padding: 0 8rpx;
}
.ip-location {
font-size: 20rpx;
color: var(--text-tertiary, #9ca3af);
margin-top: 4rpx;
padding: 0 8rpx;
}
</style>

View File

@@ -322,7 +322,7 @@ function formatRecordDuration(s: number) { return `${Math.floor(s/60).toString()
.input-floater {
position: fixed;
bottom: 0; left: 0; right: 0;
z-index: 200;
z-index: 1;
background: var(--bg-floater);
border-top: 2rpx solid var(--border-color);

View File

@@ -0,0 +1,58 @@
<template>
<wd-action-sheet
:model-value="modelValue"
:actions="actions"
:title="title"
:cancel-text="cancelText"
:z-index="Z.actionSheet"
v-bind="$attrs"
@update:model-value="onModelUpdate"
@select="emit('select', $event)"
@open="onOpen"
@close="emit('close')"
/>
</template>
<script setup lang="ts">
import { inject, watch } from 'vue'
import { Z } from '@/constants/zIndex'
const props = defineProps<{
modelValue: boolean
actions?: Array<{ name: string; value?: string; color?: string; disabled?: boolean }>
title?: string
cancelText?: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
select: [action: { item: { name: string; value?: string }; index: number }]
open: []
close: []
}>()
const tabPageLayout = inject<{ closeDrawer?: () => void } | null>('tabPageLayout', null)
function onModelUpdate(value: boolean) {
if (value && tabPageLayout?.closeDrawer) {
tabPageLayout.closeDrawer()
}
emit('update:modelValue', value)
}
function onOpen() {
if (tabPageLayout?.closeDrawer) {
tabPageLayout.closeDrawer()
}
emit('open')
}
watch(
() => props.modelValue,
(open) => {
if (open && tabPageLayout?.closeDrawer) {
tabPageLayout.closeDrawer()
}
}
)
</script>

View File

@@ -1,13 +1,14 @@
<template>
<view class="app-avatar" :style="avatarStyle" @click="onClick">
<wd-img
v-if="resolvedSrc"
v-if="showImage"
:src="resolvedSrc"
:width="sizeRpx"
:height="sizeRpx"
:round="round"
:radius="round ? '50%' : radius"
mode="aspectFill"
@error="onImageError"
/>
<view
v-else
@@ -16,8 +17,6 @@
>
{{ displayText }}
</view>
<!-- 徽标 -->
<wd-badge
v-if="badge && badge > 0"
:value="badge"
@@ -29,7 +28,7 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, ref, watch } from 'vue'
import { generateColor } from '@/utils/format'
import { resolveImageUrl } from '@/utils/image'
@@ -55,26 +54,24 @@ const props = withDefaults(defineProps<Props>(), {
bgColor: ''
})
// 判断是否为有效图片 URL
function isValidImageUrl(url?: string): boolean {
if (!url) return false
// 有效 URL: http/https/data URI 或以 /uploads/ 开头
return url.startsWith('http://') ||
url.startsWith('https://') ||
url.startsWith('data:') ||
url.startsWith('/uploads/')
}
const imageError = ref(false)
// 处理图片 URL,添加域名前缀,并验证是否为有效图片 URL
const resolvedSrc = computed(() => {
// 先检查原始 src 是否为有效图片路径
if (!isValidImageUrl(props.src)) return ''
return resolveImageUrl(props.src)
watch(() => props.src, () => {
imageError.value = false
})
const emit = defineEmits<{
(e: 'click'): void
}>()
const resolvedSrc = computed(() => {
if (!props.src?.trim()) return ''
return resolveImageUrl(props.src.trim())
})
const showImage = computed(() => !!resolvedSrc.value && !imageError.value)
function onImageError() {
imageError.value = true
}
const emit = defineEmits<{ (e: 'click'): void }>()
const sizeRpx = computed(() => `${props.size}rpx`)
@@ -91,9 +88,7 @@ const placeholderStyle = computed(() => ({
fontSize: `${props.size * 0.4}rpx`
}))
const displayText = computed(() => {
return props.name?.charAt(0) || '?'
})
const displayText = computed(() => props.name?.charAt(0) || '?')
function onClick() {
emit('click')
@@ -131,10 +126,3 @@ function onClick() {
border: 2rpx solid var(--bg-content);
}
</style>

View File

@@ -0,0 +1,8 @@
<template>
<wd-toast :z-index="Z.toast" />
<wd-message-box :z-index="Z.messageBox" />
</template>
<script setup lang="ts">
import { Z } from '@/constants/zIndex'
</script>

View File

@@ -23,7 +23,7 @@
/>
</view>
<text class="username">{{ user?.name || '请登录' }}</text>
<text class="bio">{{ user?.desc || '编辑个签,展示你的独特个性' }}</text>
<text class="bio">{{ user?.desc || '编辑个性签名,展示你的独特个性' }}</text>
</view>
<!-- 菜单列表 -->

View File

@@ -0,0 +1,65 @@
<template>
<wd-popup
:model-value="modelValue"
:position="position"
:z-index="Z.popup"
:safe-area-inset-bottom="safeAreaInsetBottom"
:custom-style="customStyle"
v-bind="$attrs"
@update:model-value="onModelUpdate"
@open="onOpen"
@close="emit('close')"
>
<slot />
</wd-popup>
</template>
<script setup lang="ts">
import { inject, watch } from 'vue'
import { Z } from '@/constants/zIndex'
const props = withDefaults(
defineProps<{
modelValue: boolean
position?: 'top' | 'right' | 'bottom' | 'left' | 'center'
safeAreaInsetBottom?: boolean
customStyle?: string
}>(),
{
position: 'center',
safeAreaInsetBottom: true,
customStyle: '',
}
)
const emit = defineEmits<{
'update:modelValue': [value: boolean]
open: []
close: []
}>()
const tabPageLayout = inject<{ closeDrawer?: () => void } | null>('tabPageLayout', null)
function onModelUpdate(value: boolean) {
if (value && tabPageLayout?.closeDrawer) {
tabPageLayout.closeDrawer()
}
emit('update:modelValue', value)
}
function onOpen() {
if (tabPageLayout?.closeDrawer) {
tabPageLayout.closeDrawer()
}
emit('open')
}
watch(
() => props.modelValue,
(open) => {
if (open && tabPageLayout?.closeDrawer) {
tabPageLayout.closeDrawer()
}
}
)
</script>

View File

@@ -261,8 +261,8 @@ function handleClose() {
/* #endif */
/* #ifndef MP-WEIXIN */
height: calc(88rpx + var(--status-bar-height, 0));
padding-top: var(--status-bar-height, 0);
height: calc(88rpx + var(--status-bar-height, 20px));
padding-top: var(--status-bar-height, 20px);
/* #endif */
}