From 8ee98719ef5aec87f776aceea6f3ae0682f777d9 Mon Sep 17 00:00:00 2001 From: mei Date: Tue, 11 Aug 2026 21:30:05 +0800 Subject: [PATCH] update --- build-apk.ps1 | 57 ------ build-exe2.ps1 | 13 -- build-hap.ps1 | 105 ----------- client/src/App.jsx | 10 +- client/src/components/PushService.jsx | 73 ++++++++ client/src/pages/Messages.jsx | 178 ++++++++++++++++-- database.js | 35 ++++ package-lock.json | 10 +- server.js | 256 ++++++++++++++++++++++++-- 9 files changed, 518 insertions(+), 219 deletions(-) delete mode 100644 build-apk.ps1 delete mode 100644 build-exe2.ps1 delete mode 100644 build-hap.ps1 create mode 100644 client/src/components/PushService.jsx diff --git a/build-apk.ps1 b/build-apk.ps1 deleted file mode 100644 index 3e560e4..0000000 --- a/build-apk.ps1 +++ /dev/null @@ -1,57 +0,0 @@ -# Manual APK build pipeline: aapt2 -> javac -> d8 -> jar -> zipalign -> apksigner -$root = 'c:\Users\shz73\Documents\Qoder\2026-08-09\chat-1\device-management-system' -$jdk = "$root\android-tools\jdk\jdk-17.0.20+8" -$sdk = "$root\android-sdk" -$bt = "$sdk\build-tools\33.0.2" -$androidJar = "$sdk\platforms\android-33\android.jar" -$proj = "$root\android-app" -$build = "$proj\build" - -$env:JAVA_HOME = $jdk -$env:Path = "$jdk\bin;" + $env:Path - -if (Test-Path $build) { Remove-Item $build -Recurse -Force } -New-Item -ItemType Directory -Force -Path "$build\gen" | Out-Null -New-Item -ItemType Directory -Force -Path "$build\classes" | Out-Null -New-Item -ItemType Directory -Force -Path "$build\dex" | Out-Null - -Write-Host '=== 1. aapt2 compile ===' -& "$bt\aapt2.exe" compile --dir "$proj\res" -o "$build\res.zip" -if ($LASTEXITCODE -ne 0) { Write-Host 'AAPT2 COMPILE FAILED'; exit 1 } - -Write-Host '=== 2. aapt2 link ===' -& "$bt\aapt2.exe" link -o "$build\app.unsigned.apk" -I $androidJar --manifest "$proj\AndroidManifest.xml" --java "$build\gen" --auto-add-overlay "$build\res.zip" -if ($LASTEXITCODE -ne 0) { Write-Host 'AAPT2 LINK FAILED'; exit 1 } - -Write-Host '=== 3. javac ===' -$srcs = @(Get-ChildItem -Recurse -Filter *.java "$build\gen" | ForEach-Object { $_.FullName }) + @(Get-ChildItem -Recurse -Filter *.java "$proj\src" | ForEach-Object { $_.FullName }) -& "$jdk\bin\javac.exe" -classpath $androidJar -d "$build\classes" @srcs -if ($LASTEXITCODE -ne 0) { Write-Host 'JAVAC FAILED'; exit 1 } - -Write-Host '=== 4. d8 ===' -$classes = Get-ChildItem -Recurse -Filter *.class "$build\classes" | ForEach-Object { $_.FullName } -& "$jdk\bin\java.exe" -cp "$bt\lib\d8.jar" com.android.tools.r8.D8 --output "$build\dex" --lib $androidJar @classes -if ($LASTEXITCODE -ne 0) { Write-Host 'D8 FAILED'; exit 1 } - -Write-Host '=== 5. add classes.dex ===' -Push-Location "$build\dex" -& "$jdk\bin\jar.exe" uf "$build\app.unsigned.apk" classes.dex -Pop-Location -if ($LASTEXITCODE -ne 0) { Write-Host 'JAR ADD FAILED'; exit 1 } - -Write-Host '=== 6. zipalign ===' -& "$bt\zipalign.exe" -f 4 "$build\app.unsigned.apk" "$build\app-aligned.apk" -if ($LASTEXITCODE -ne 0) { Write-Host 'ZIPALIGN FAILED'; exit 1 } - -Write-Host '=== 7. sign ===' -$ks = "$build\debug.keystore" -if (-not (Test-Path $ks)) { - & "$jdk\bin\keytool.exe" -genkeypair -keystore $ks -storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname 'CN=Android Debug,O=Android' -} -& "$jdk\bin\java.exe" -jar "$bt\lib\apksigner.jar" sign --ks $ks --ks-pass pass:android --key-pass pass:android --ks-key-alias androiddebugkey --out "$build\yingshi-device-manager.apk" "$build\app-aligned.apk" -if ($LASTEXITCODE -ne 0) { Write-Host 'SIGN FAILED'; exit 1 } - -Write-Host '=== 8. verify ===' -& "$jdk\bin\java.exe" -jar "$bt\lib\apksigner.jar" verify --verbose "$build\yingshi-device-manager.apk" -Get-Item "$build\yingshi-device-manager.apk" | Select-Object Name, Length -Write-Host 'APK-BUILD-DONE' diff --git a/build-exe2.ps1 b/build-exe2.ps1 deleted file mode 100644 index a10bb1f..0000000 --- a/build-exe2.ps1 +++ /dev/null @@ -1,13 +0,0 @@ -# Build EXE with signing disabled -$env:Path = 'C:\Program Files\nodejs;' + $env:Path -$env:ELECTRON_MIRROR = 'https://npmmirror.com/mirrors/electron/' -$env:ELECTRON_BUILDER_BINARIES_MIRROR = 'https://npmmirror.com/mirrors/electron-builder-binaries/' -$env:CSC_IDENTITY_AUTO_DISCOVERY = 'false' -Set-Location 'c:\Users\shz73\Documents\Qoder\2026-08-09\chat-1\device-management-system' - -Write-Host '=== electron-builder --win ===' -& 'C:\Program Files\nodejs\node.exe' node_modules\electron-builder\out\cli\cli.js --win -if ($LASTEXITCODE -ne 0) { Write-Host 'electron-builder failed'; exit 1 } - -Write-Host '=== output files ===' -Get-ChildItem dist-electron -File | Select-Object Name, Length diff --git a/build-hap.ps1 b/build-hap.ps1 deleted file mode 100644 index 120c148..0000000 --- a/build-hap.ps1 +++ /dev/null @@ -1,105 +0,0 @@ -# HarmonyOS HAP build pipeline (OpenHarmony SDK, no DevEco required) -$root = 'c:\Users\shz73\Documents\Qoder\2026-08-09\chat-1\device-management-system' -$env:Path = 'C:\Program Files\nodejs;' + $env:Path -$ets = "$root\harmonyos-tools\sdk\windows\ets\ets\build-tools\ets-loader" -$proj = "$root\harmony-app\entry\src\main" -$build = "$root\harmony-app\build" -if (Test-Path "$build\cache") { Remove-Item -Recurse -Force "$build\cache" } -New-Item -ItemType Directory -Force -Path "$build", "$build\intermediates", "$build\cache" | Out-Null - -Write-Host '=== webpack ets compile ===' -$env:compileMode = 'esmodule' -$env:buildMode = 'debug' -$env:compilerType = 'ark' -$env:cachePath = "$build\cache" -$env:aceBuildJson = "$root\harmony-app\build.json" -$env:aceModuleJsonPath = "$proj\module.json" -$env:aceProfilePath = "$proj\resources\base\profile" -$env:aceModuleRoot = "$proj\ets" -$env:aceModuleBuild = "$build\intermediates" -Push-Location $ets -& 'C:\Program Files\nodejs\node.exe' "$ets\node_modules\webpack\bin\webpack.js" --config "$ets\webpack.debug.config.js" --env buildMode=debug --env compilerType=ark --env compileMode=esmodule --env aceModuleRoot=$proj\ets --env aceModuleBuild="$build\intermediates" --env aceModuleJsonPath="$proj\module.json" --env aceBuildJson="$root\harmony-app\build.json" --env aceProfilePath="$proj\resources\base\profile" --env cachePath="$build\cache" -Write-Host "webpack exit: $LASTEXITCODE" -Pop-Location -if ($LASTEXITCODE -ne 0) { Write-Host 'BUILD FAILED at webpack step'; exit 1 } - -$tc = "$root\harmonyos-tools\sdk\windows\toolchains\toolchains" -$lib = "$tc\lib" -$jdk = "$root\android-tools\jdk\jdk-17.0.20+8\bin" -$bundle = 'com.yingshi.devicemgr' -$finalHap = "$root\harmony-app\影视星河设备管理系统.hap" - -Write-Host '=== restool compile resources ===' -if (Test-Path "$build\res") { Remove-Item -Recurse -Force "$build\res" } -New-Item -ItemType Directory -Force -Path "$build\res" | Out-Null -& "$tc\restool.exe" -i $proj -o "$build\res" -p entry -r "$build\res\ResourceTable.txt" -Write-Host "restool exit: $LASTEXITCODE" -if ($LASTEXITCODE -ne 0) { exit 1 } - -Write-Host '=== assemble hap stage ===' -$stage = "$build\hap-stage" -if (Test-Path $stage) { Remove-Item -Recurse -Force $stage } -New-Item -ItemType Directory -Force -Path $stage | Out-Null -Copy-Item "$build\intermediates\modules.abc" "$stage\modules.abc" -$mj = Get-Content "$build\res\module.json" -Raw | ConvertFrom-Json -$mj.PSObject.Properties.Remove('app') -[System.IO.File]::WriteAllText("$stage\module.json", ($mj | ConvertTo-Json -Depth 32), [System.Text.UTF8Encoding]::new($false)) -Copy-Item "$build\res\resources.index" "$stage\resources.index" -Copy-Item "$build\res\resources" "$stage\resources" -Recurse - -$packInfo = @{ - summary = @{ - app = @{ - bundleName = $bundle - debug = $true - icon = '$media:icon' - label = '$string:app_name' - version = @{ code = 1000000; name = '1.0.0' } - minAPIVersion = 9 - targetAPIVersion = 12 - } - modules = @(@{ - name = 'entry' - type = 'entry' - deviceType = @('phone', 'tablet') - abilities = @(@{ name = 'EntryAbility'; label = '$string:EntryAbility_label'; visible = $true }) - distro = @{ moduleType = 'entry'; installationFree = $false; deliveryWithInstall = $true; moduleName = 'entry' } - }) - } - packages = @(@{ deviceType = @('phone', 'tablet'); moduleType = 'entry'; deliveryWithInstall = $true; name = 'entry' }) -} -[System.IO.File]::WriteAllText("$stage\pack.info", ($packInfo | ConvertTo-Json -Depth 32), [System.Text.UTF8Encoding]::new($false)) - -Write-Host '=== zip unsigned hap ===' -Push-Location $stage -& "$jdk\jar.exe" -cfM "$build\unsigned.hap" modules.abc module.json pack.info resources.index resources -Write-Host "jar exit: $LASTEXITCODE" -Pop-Location -if ($LASTEXITCODE -ne 0) { exit 1 } - -Write-Host '=== prepare debug profile ===' -$prof = Get-Content "$lib\UnsgnedDebugProfileTemplate.json" -Raw | ConvertFrom-Json -$prof.uuid = [guid]::NewGuid().ToString() -$prof.validity.'not-after' = 1893456000 -$prof.'bundle-info'.'bundle-name' = $bundle -[System.IO.File]::WriteAllText("$build\unsigned-profile.json", ($prof | ConvertTo-Json -Depth 32), [System.Text.UTF8Encoding]::new($false)) -& "$jdk\keytool.exe" -exportcert -rfc -keystore "$lib\OpenHarmony.p12" -storetype PKCS12 -storepass 123456 -alias 'openharmony application ca' -file "$build\app-ca.cer" -& "$jdk\keytool.exe" -exportcert -rfc -keystore "$lib\OpenHarmony.p12" -storetype PKCS12 -storepass 123456 -alias 'openharmony application root ca' -file "$build\app-root.cer" -$chain = $prof.'bundle-info'.'development-certificate' + (Get-Content "$build\app-ca.cer" -Raw) + (Get-Content "$build\app-root.cer" -Raw) -[System.IO.File]::WriteAllText("$build\app-cert.pem", $chain, [System.Text.UTF8Encoding]::new($false)) - -Write-Host '=== sign profile ===' -& "$jdk\java.exe" -jar "$lib\hap-sign-tool.jar" sign-profile -mode localSign -keyAlias 'openharmony application profile debug' -keyPwd 123456 -profileCertFile "$lib\OpenHarmonyProfileDebug.pem" -inFile "$build\unsigned-profile.json" -signAlg SHA256withECDSA -keystoreFile "$lib\OpenHarmony.p12" -keystorePwd 123456 -outFile "$build\debug-profile.p7b" -Write-Host "sign-profile exit: $LASTEXITCODE" -if ($LASTEXITCODE -ne 0) { exit 1 } - -Write-Host '=== sign hap ===' -& "$jdk\java.exe" -jar "$lib\hap-sign-tool.jar" sign-app -mode localSign -keyAlias 'openharmony application release' -keyPwd 123456 -appCertFile "$build\app-cert.pem" -profileFile "$build\debug-profile.p7b" -inFile "$build\unsigned.hap" -signAlg SHA256withECDSA -keystoreFile "$lib\OpenHarmony.p12" -keystorePwd 123456 -outFile $finalHap -compatibleVersion 9 -inForm zip -Write-Host "sign-app exit: $LASTEXITCODE" -if ($LASTEXITCODE -ne 0) { exit 1 } - -Write-Host '=== verify ===' -& "$jdk\java.exe" -jar "$lib\hap-sign-tool.jar" verify-app -inFile $finalHap -outCertChain "$build\verify-certchain.cer" -outProfile "$build\verify-profile.p7b" -Write-Host "verify exit: $LASTEXITCODE" -Write-Host 'DONE' -Get-Item $finalHap | Select-Object FullName, Length diff --git a/client/src/App.jsx b/client/src/App.jsx index eff22ff..19bc46a 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -26,7 +26,7 @@ import AccountManagement from './pages/AccountManagement' import Messages from './pages/Messages' import About from './pages/About' import Guide from './pages/Guide' -import Download from './pages/Download' +import PushService from './components/PushService' const { Header, Sider, Content } = Layout @@ -127,7 +127,8 @@ function App() { { key: 'personnel', icon: , label: '人员管理' }, ...(user?.role === 'admin' ? [{ key: 'accounts', icon: , label: '账户管理' }] : []), { key: 'notifications', icon: , label: '通知管理' }, - { key: 'download', icon: , label: '软件下载' } + { key: 'about', icon: , label: '软件简介' }, + { key: 'guide', icon: , label: '使用说明' } ] // 移动端底部导航(5个核心入口) @@ -149,14 +150,13 @@ function App() { case 'accounts': return user?.role === 'admin' ? : case 'notifications': return case 'messages': return - case 'download': return default: return } } const getPageTitle = () => { const titles = { - dashboard: '系统首页', about: '软件简介', guide: '使用说明',download: '软件下载', + dashboard: '系统首页', about: '软件简介', guide: '使用说明', devices: '设备管理', personnel: '人员管理', accounts: '账户管理', notifications: '通知管理', messages: '消息对话' } @@ -304,6 +304,7 @@ function App() { {pwdModal} + ) } @@ -365,6 +366,7 @@ function App() { {pwdModal} + ) } diff --git a/client/src/components/PushService.jsx b/client/src/components/PushService.jsx new file mode 100644 index 0000000..1a9b011 --- /dev/null +++ b/client/src/components/PushService.jsx @@ -0,0 +1,73 @@ +import { useEffect, useRef } from 'react' +import { notification } from 'antd' +import axios from 'axios' + +// 分发到原生推送通道: +// 1. 安卓 APK 的 JS 桥(window.AndroidNotify.push) +// 2. 鸿蒙 HAP 的 javaScriptProxy 桥(window.HmosNotify.push) +// 3. HTML5 Notification(Electron 桌面端与浏览器直接支持) +function nativeNotify(title, body) { + try { + if (window.AndroidNotify && typeof window.AndroidNotify.push === 'function') { + window.AndroidNotify.push(title, body) + return + } + if (window.HmosNotify && typeof window.HmosNotify.push === 'function') { + window.HmosNotify.push(title, body) + return + } + if ('Notification' in window && Notification.permission === 'granted') { + new Notification(title, { body }) + } + } catch (e) { /* 原生通道失败时已有应用内通知兜底 */ } +} + +// 全局推送服务:登录后挂载,轮询后端推送事件(聊天消息/设备报修/维修完成) +function PushService({ user }) { + const cursorRef = useRef(0) + const firstRef = useRef(true) + + useEffect(() => { + if (!user) return + // 游标按用户隔离存储,切换账号不串推送 + const key = 'push_cursor_' + user.id + cursorRef.current = parseInt(localStorage.getItem(key) || '0') + firstRef.current = true + + // HTML5 通知权限(桌面 Electron/浏览器有效;移动端 WebView 会静默忽略) + if ('Notification' in window && Notification.permission === 'default') { + try { Notification.requestPermission() } catch (e) { /* ignore */ } + } + + const poll = async () => { + try { + const res = await axios.get('/api/push/events', { params: { after: cursorRef.current } }) + if (!res.data.success || !res.data.data.length) return + const events = res.data.data + const isFirstSync = firstRef.current && cursorRef.current === 0 + firstRef.current = false + for (const ev of events) { + cursorRef.current = ev.id + // 首次使用只同步游标不弹通知,避免历史事件刷屏 + if (isFirstSync) continue + notification.open({ + message: ev.title, + description: ev.body, + placement: 'bottomRight', + duration: 5 + }) + nativeNotify(ev.title, ev.body) + } + localStorage.setItem(key, String(cursorRef.current)) + } catch (e) { /* 静默 */ } + } + + poll() + const timer = setInterval(poll, 8000) + return () => clearInterval(timer) + }, [user?.id]) + + return null +} + +export default PushService diff --git a/client/src/pages/Messages.jsx b/client/src/pages/Messages.jsx index ad51a1a..0b62d8b 100644 --- a/client/src/pages/Messages.jsx +++ b/client/src/pages/Messages.jsx @@ -1,10 +1,29 @@ import React, { useState, useEffect, useRef } from 'react' -import { Card, Button, Input, List, Badge, Modal, Form, Select, Tabs, Avatar, Empty, message, Popconfirm, Space, Tag, Tooltip } from 'antd' -import { PlusOutlined, SendOutlined, TeamOutlined, UserOutlined, ArrowLeftOutlined, DeleteOutlined, SettingOutlined, UserDeleteOutlined } from '@ant-design/icons' +import { Card, Button, Input, List, Badge, Modal, Form, Select, Tabs, Avatar, Empty, message, Popconfirm, Space, Tag, Tooltip, Popover, Image } from 'antd' +import { PlusOutlined, SendOutlined, TeamOutlined, UserOutlined, ArrowLeftOutlined, DeleteOutlined, SettingOutlined, UserDeleteOutlined, CheckCircleFilled, ClockCircleOutlined, SmileOutlined, PictureOutlined, VideoCameraOutlined, PaperClipOutlined, FileOutlined, DownloadOutlined } from '@ant-design/icons' import axios from 'axios' const { TextArea } = Input +// 常用表情面板 +const EMOJIS = ['😀', '😄', '😂', '🤣', '😊', '😍', '😘', '😎', '🤔', '😅', '😭', '😡', '👍', '👎', '👏', '🙏', '🤝', '💪', '❤️', '💔', '🎉', '🎂', '🌹', '☕', '🍺', '⚽', '🌙', '☀️', '⛅', '🌈', '🔥', '✨', '💯', '✅', '❌', '⚠️', '📷', '🎬', '🎤', '📱', '💻', '🚗', '✈️', '⏰', '📅', '💰', '🎁', '🏆'] + +// 文件大小格式化 +function formatSize(size) { + if (!size && size !== 0) return '' + if (size < 1024) return size + ' B' + if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB' + if (size < 1024 * 1024 * 1024) return (size / 1024 / 1024).toFixed(1) + ' MB' + return (size / 1024 / 1024 / 1024).toFixed(2) + ' GB' +} + +// 附件访问地址:拼上 baseURL 与 token(img/video/下载链接无法携带 Authorization 头) +function fileSrc(url) { + if (!url) return '' + const token = localStorage.getItem('token') || '' + return axios.getUri({ url }) + (url.includes('?') ? '&' : '?') + 'token=' + encodeURIComponent(token) +} + // 移动端检测 function useIsMobile() { const [isMobile, setIsMobile] = useState(window.innerWidth < 768) @@ -28,21 +47,28 @@ function Messages({ user }) { const [membersVisible, setMembersVisible] = useState(false) const [createForm] = Form.useForm() const [creating, setCreating] = useState(false) + const [uploading, setUploading] = useState(false) + const imgInputRef = useRef(null) + const videoInputRef = useRef(null) + const fileInputRef = useRef(null) const bottomRef = useRef(null) const activeConvRef = useRef(null) const lastMsgIdRef = useRef(0) + const readStatusRef = useRef({}) // 打开会话时同步到 ref(供轮询闭包使用) const openConversation = (conv) => { setActiveConv(conv) activeConvRef.current = conv lastMsgIdRef.current = 0 + readStatusRef.current = {} setMessages([]) } const closeConversation = () => { setActiveConv(null) activeConvRef.current = null + readStatusRef.current = {} setMessages([]) } @@ -76,13 +102,26 @@ function Messages({ user }) { } catch (err) { /* 静默 */ } } - // 会话列表 5 秒轮询;打开的会话 3 秒轮询消息 + // 拉取当前打开会话的已读回执,合并进消息列表 + const fetchReadStatus = async (conv) => { + if (!conv) return + try { + const res = await axios.get(`/api/chat/conversations/${conv.id}/read-status`) + if (res.data.success) { + readStatusRef.current = res.data.data + setMessages(prev => prev.map(m => res.data.data[m.id] ? { ...m, read_status: res.data.data[m.id] } : m)) + } + } catch (err) { /* 静默 */ } + } + + // 会话列表 5 秒轮询;打开的会话 3 秒轮询消息,4 秒轮询已读回执 useEffect(() => { fetchConversations() fetchUsers() const convTimer = setInterval(fetchConversations, 5000) const msgTimer = setInterval(() => fetchMessages(activeConvRef.current), 3000) - return () => { clearInterval(convTimer); clearInterval(msgTimer) } + const readTimer = setInterval(() => fetchReadStatus(activeConvRef.current), 4000) + return () => { clearInterval(convTimer); clearInterval(msgTimer); clearInterval(readTimer) } }, []) // 新消息自动滚动到底部 @@ -108,6 +147,35 @@ function Messages({ user }) { } } + // 选择并发送图片/视频/文件:先上传拿 fileId,再发媒体消息 + const handlePickMedia = async (type, file) => { + if (!file || !activeConv || uploading) return + if (file.size > 100 * 1024 * 1024) { + message.error('文件超过 100MB 限制') + return + } + setUploading(true) + const hide = message.loading(type === 'image' ? '图片上传中…' : type === 'video' ? '视频上传中…' : '文件上传中…', 0) + try { + const fd = new FormData() + fd.append('file', file) + const up = await axios.post('/api/chat/upload', fd) + if (!up.data.success) throw new Error(up.data.message) + const res = await axios.post(`/api/chat/conversations/${activeConv.id}/messages`, { + type, fileId: up.data.data.fileId, content: up.data.data.fileName + }) + if (res.data.success) { + await fetchMessages(activeConvRef.current) + fetchConversations() + } + } catch (err) { + message.error(err.response?.data?.message || err.message || '发送失败') + } finally { + hide() + setUploading(false) + } + } + const handleCreate = async (values) => { setCreating(true) try { @@ -270,26 +338,105 @@ function Messages({ user }) { {messages.length === 0 && } {messages.map(m => { const mine = m.sender_id === user?.id + const rs = m.read_status + const allRead = rs && rs.total_others > 0 && rs.read_count >= rs.total_others + let readEl = null + if (mine && rs && rs.total_others > 0) { + if (activeConv.type === 'group' && !allRead) { + const names = rs.unread_users.map(u => u.name).slice(0, 5).join('、') + const extra = rs.unread_users.length > 5 ? ` 等${rs.unread_users.length}人` : '' + readEl = ( + + + 未读({rs.read_count}/{rs.total_others}人已读) + + + ) + } else { + readEl = allRead + ? 已读 + : 未读 + } + } + // 按消息类型渲染内容:图片/视频/文件/文本 + let body + if (m.msg_type === 'image' && m.file_url) { + body = ( +
+ +
+ ) + } else if (m.msg_type === 'video' && m.file_url) { + body = ( +
+
+ ) + } else if (m.msg_type === 'file' && m.file_url) { + body = ( +
+ +
+
{m.file_name || m.content}
+
{formatSize(m.file_size)}
+
+ + + +
+ ) + } else { + body =
{m.content}
+ } return (
{!mine &&
{m.sender_name}
} -
{m.content}
-
{m.created_at?.slice(5, 16)}
+ {body} +
+ {m.created_at?.slice(5, 16)} + {readEl} +
) })}
-