修复一些问题
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -179,7 +179,7 @@ function handleClose() {
|
||||
.modal-container {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
z-index: 2100;
|
||||
|
||||
--bg-surface: #fff;
|
||||
--bg-hover: #f5f5f5;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
58
src/components/common/AppActionSheet.vue
Normal file
58
src/components/common/AppActionSheet.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
8
src/components/common/AppConfirmHost.vue
Normal file
8
src/components/common/AppConfirmHost.vue
Normal 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>
|
||||
@@ -23,7 +23,7 @@
|
||||
/>
|
||||
</view>
|
||||
<text class="username">{{ user?.name || '请登录' }}</text>
|
||||
<text class="bio">{{ user?.desc || '编辑个签,展示你的独特个性' }}</text>
|
||||
<text class="bio">{{ user?.desc || '编辑个性签名,展示你的独特个性' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 菜单列表 -->
|
||||
|
||||
65
src/components/common/AppModal.vue
Normal file
65
src/components/common/AppModal.vue
Normal 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>
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user