添加工单附件 及样式

This commit is contained in:
浅念
2025-08-11 19:24:24 +08:00
parent 619311dfbe
commit e797cb24de
5 changed files with 520 additions and 19 deletions
+2 -2
View File
@@ -40,7 +40,7 @@
<span :class="['tag-item', { 'tag-active': tagActiveMap[item.id] === item1.id }]">
{{ item1.name }}
</span>
<el-badge :value="item1.count" :max="99" class="itemtag" type="primary"> </el-badge>
<!-- <el-badge :value="item1.count" :max="99" class="itemtag" type="primary"> </el-badge> -->
</span>
</div>
</div>
@@ -64,7 +64,7 @@
<span :class="['tag-item', { 'tag-active': tagActiveMap[item.id] === item1.id }]">
{{ item1.name }}
</span>
<el-badge :value="item1.count" :max="99" class="itemtag" type="primary"> </el-badge>
<!-- <el-badge :value="item1.count" :max="99" class="itemtag" type="primary"> </el-badge> -->
</span>
</div>
</div>
+4 -1
View File
@@ -91,7 +91,10 @@
<span>
{{scope.row.countryInfo? scope.row.countryInfo.cn_addr : ''}}
</span>
<el-tag class="steer" type="warning" v-if="scope.row.countryInfo">{{ scope.row.countryInfo.steering }}</el-tag>
<span v-if="scope.row.countryInfo">
<el-tag class="steer" type="warning" v-if="scope.row.countryInfo.steering=='左舵'">{{ scope.row.countryInfo.steering }}</el-tag>
<el-tag class="steer" type="success" v-else-if="scope.row.countryInfo.steering=='右舵'">{{ scope.row.countryInfo.steering }}</el-tag>
</span>
<!-- <span class="steer" v-if="scope.row.countryInfo">{{ scope.row.countryInfo.steering }}</span> -->
</template>
</el-table-column>
+4 -3
View File
@@ -30,7 +30,7 @@
lass="customer-table"
>
<template #default>
<el-table-column prop="id" label="用户ID" align="center" />
<!-- <el-table-column prop="id" label="用户ID" align="center" /> -->
<el-table-column prop="sn" label="SN" align="center" />
<el-table-column prop="follow" label="头像" align="center">
<template #default="scope">
@@ -39,13 +39,14 @@
alt="头像"
class="avatar"
@click="handleAvatarClick(scope.row.avatar)"
style="cursor: pointer;"
style="cursor: pointer"
/>
</template>
</el-table-column>
<el-table-column prop="name" label="用户名称" align="center" />
<el-table-column prop="name" label="名字" align="center" />
<el-table-column prop="phone" label="电话" align="center" />
<el-table-column prop="email" label="邮箱" align="center" />
<el-table-column prop="corp_name" label="所属公司" align="center" />
<el-table-column prop="follow" label="状态" align="center">
<template #default="scope">
<span :class="statusClass(scope.row.status)">
+248 -4
View File
@@ -183,18 +183,96 @@
<div class="chat-messages" ref="messageContainer">
<template v-for="(message, index) in messages" :key="index">
<div :class="['message-item', String(message.user_id) === String(userid) ? 'message-right' : 'message-left']">
<el-avatar :size="32" :src="String(message.user_id) === String(userid) ? meAvatar : aiAvatar" class="message-avatar" />
<el-avatar :size="32" :src="message.user_avatar" class="message-avatar" />
<div class="message-content">
<div class="message-info">
<span class="sender-name">{{ message.user_id }}</span>
<span class="sender-name">{{ message.user_name }}</span>
<span class="message-time">{{ message.updated_at }}</span>
</div>
<div class="message-text">{{ message.content }}</div>
<!-- 附件显示区域 -->
<div class="message-attachments" v-if="message.attachment && message.attachment.length > 0">
<div class="attachment-item" v-for="(attachment, attIndex) in message.attachment" :key="attIndex">
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(attachment.original_name)">
<img
:src="attachment.storage_path_full"
:alt="attachment.original_name"
class="attachment-thumb"
@click="handleChatImageClick(attachment.storage_path_full)"
/>
</template>
<template v-else-if="/\.(mp4|webm|ogg)$/i.test(attachment.original_name)">
<video
:src="attachment.storage_path_full"
controls
class="attachment-thumb"
@click.prevent="handleChatVideoClick(attachment.storage_path_full)"
></video>
</template>
<template v-else>
<div class="file-attachment">
<el-icon class="file-icon"><Document /></el-icon>
<span class="file-name">{{ attachment.original_name }}</span>
<el-button
type="primary"
size="small"
link
@click="downloadAttachment(attachment)"
>
下载
</el-button>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
</div>
<div class="chat-input" v-if="hasAuth('work_order_self@conversation_add')">
<!-- 附件上传区域 -->
<div class="chat-attachment-upload">
<el-upload
v-model:file-list="chatFileList"
class="chat-upload"
:action="actionImageUplode"
:on-success="handleChatUploadSuccess"
:headers="uploadHeaders"
multiple
:limit="5"
:on-exceed="handleChatExceed"
:show-file-list="false"
>
<el-button type="primary" link>
<el-icon><Paperclip /></el-icon>
添加附件
</el-button>
</el-upload>
<!-- 已上传附件预览 -->
<div class="uploaded-attachments" v-if="chatFileList.length > 0">
<div class="attachment-preview-item" v-for="(file, idx) in chatFileList" :key="idx">
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)">
<img
:src="file.file_url || file.response?.data?.file_url"
:alt="file.name"
class="preview-thumb"
/>
</template>
<template v-else>
<el-icon class="file-icon"><Document /></el-icon>
</template>
<span class="file-name">{{ file.name }}</span>
<el-button
type="danger"
size="small"
link
@click="removeChatFile(idx)"
>
<el-icon><Close /></el-icon>
</el-button>
</div>
</div>
</div>
<el-input v-model="messageText" type="textarea" :rows="3" placeholder="输入消息" resize="none"
@keyup.enter.prevent="sendMessage" />
<ElInput v-model="ticket_id" type="hidden" />
@@ -226,7 +304,7 @@
</ElDialog>
<!-- 图片预览弹窗 -->
<ElImageViewer v-if="imageViewerVisible" :url-list="imageList" :initial-index="imageViewerIndex" @close="imageViewerVisible = false" />
<ElImageViewer v-if="imageViewerVisible" :url-list="chatImageList.length > 0 ? chatImageList : imageList" :initial-index="imageViewerIndex" @close="closeImageViewer" />
<!-- 视频预览弹窗 -->
<ElDialog v-model="videoDialogVisible" width="60%" top="10vh" :show-close="true" append-to-body>
<video v-if="currentVideoUrl" :src="currentVideoUrl" controls autoplay style="width:100%;max-height:70vh;display:block;"></video>
@@ -247,6 +325,7 @@
import { useAuth } from "@/composables/useAuth";
import { useUserStore } from '@/store/modules/user'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
import { Paperclip, Document, Close } from '@element-plus/icons-vue'
const { uploadHeaders } = useUploadHeaders()
const { info } = useUserStore()
const userid = info.user.id
@@ -279,6 +358,10 @@
}
const openChat = (row: any) => {
isDrawerVisible.value = true
// 清空数据
messageText.value = ''
messages.value = []
chatFileList.value = []
getConversationList(row.id)
}
@@ -380,14 +463,28 @@
const sendMessage = async () => {
const text = messageText.value.trim()
const ids = ticket_id.value
if (!text) return
if (!text && chatFileList.value.length === 0) return
// 准备附件数据
const attachments = chatFileList.value.map(file => {
const data = file.response?.data
if (data) {
return {
original_name: data.original_name,
storage_path: data.storage_path || data.file_name
}
}
return null
}).filter(Boolean)
const { message } = await WorkService.conversationAdd({
content: text,
ticket_id: ids,
attachment: attachments
})
ElMessage.success(message)
messageText.value = ''
chatFileList.value = [] // 清空附件列表
getConversationList(ids)
scrollToBottom()
}
@@ -523,6 +620,8 @@
const imageViewerVisible = ref(false)
const imageViewerIndex = ref(0)
const imageList = computed(() => fileList.value.filter(file => /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)).map(file => file.file_url?file.file_url:file.response.data.file_url))
// 聊天图片预览列表
const chatImageList = ref<string[]>([])
function handleImageClick(idx: number) {
imageViewerIndex.value = idx
imageViewerVisible.value = true
@@ -533,6 +632,52 @@
function handleVideoClick(url: string) {
currentVideoUrl.value = url
videoDialogVisible.value = true
}
// 聊天相关
const chatFileList = ref<any[]>([])
const handleChatUploadSuccess = (response: any, uploadFile: any, uploadFiles: any[]) => {
chatFileList.value = uploadFiles.filter(item => item.status === 'success').map(item => {
const data = item.response?.data
if (data) {
return {
name: data.original_name,
file_url: data.storage_path_full,
status: 'success',
response: { data: data }
}
}
return null
}).filter(Boolean)
}
const handleChatExceed = () => {
ElMessage.warning('最多上传5个文件')
}
const removeChatFile = (index: number) => {
chatFileList.value.splice(index, 1)
}
const handleChatImageClick = (url: string) => {
// 设置聊天图片预览列表
chatImageList.value = [url]
imageViewerIndex.value = 0
imageViewerVisible.value = true
}
const closeImageViewer = () => {
imageViewerVisible.value = false
chatImageList.value = [] // 清空聊天图片列表
}
const handleChatVideoClick = (url: string) => {
currentVideoUrl.value = url
videoDialogVisible.value = true
}
const downloadAttachment = (attachment: any) => {
const link = document.createElement('a')
link.href = attachment.storage_path_full
link.download = attachment.original_name
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
</script>
@@ -618,6 +763,7 @@
.message-info {
flex-direction: row;
align-items: center;
}
.message-text {
@@ -634,6 +780,7 @@
.message-info {
flex-direction: row-reverse;
align-items: center;
}
.message-text {
@@ -757,4 +904,101 @@
margin-top: 2px;
}
}
// 聊天附件样式
.message-right .message-attachments{
text-align: right;
}
.message-left .message-attachments{
text-align: left;
}
.message-attachments {
margin-top: 8px;
.attachment-item {
margin-bottom: 8px;
.attachment-thumb {
max-width: 200px;
max-height: 150px;
border-radius: 6px;
cursor: pointer;
border: 1px solid var(--el-border-color-lighter);
transition: transform 0.2s;
&:hover {
transform: scale(1.05);
}
}
.file-attachment {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--el-fill-color-light);
border-radius: 6px;
border: 1px solid var(--el-border-color-lighter);
.file-icon {
font-size: 16px;
color: var(--el-text-color-regular);
}
.file-name {
flex: 1;
font-size: 14px;
color: var(--el-text-color-regular);
word-break: break-all;
}
}
}
}
.chat-attachment-upload {
margin-bottom: 12px;
.chat-upload {
margin-bottom: 8px;
}
.uploaded-attachments {
display: flex;
flex-wrap: wrap;
gap: 8px;
.attachment-preview-item {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: var(--el-fill-color-light);
border-radius: 6px;
border: 1px solid var(--el-border-color-lighter);
max-width: 200px;
.preview-thumb {
width: 40px;
height: 40px;
border-radius: 4px;
object-fit: cover;
}
.file-icon {
font-size: 16px;
color: var(--el-text-color-regular);
}
.file-name {
flex: 1;
font-size: 12px;
color: var(--el-text-color-regular);
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
</style>
+257 -4
View File
@@ -123,22 +123,101 @@
String(message.user_id) === String(userid) ? 'message-right' : 'message-left'
]"
>
<!-- :src="String(message.user_id) === String(userid) ? meAvatar : aiAvatar" -->
<el-avatar
:size="32"
:src="String(message.user_id) === String(userid) ? meAvatar : aiAvatar"
:src="message.user_avatar"
class="message-avatar"
/>
<div class="message-content">
<div class="message-info">
<span class="sender-name">{{ message.user_id }}</span>
<span class="sender-name">{{ message.user_name }}</span>
<span class="message-time">{{ message.updated_at }}</span>
</div>
<div class="message-text">{{ message.content }}</div>
<!-- 附件显示区域 -->
<div class="message-attachments" v-if="message.attachment && message.attachment.length > 0">
<div class="attachment-item" v-for="(attachment, attIndex) in message.attachment" :key="attIndex">
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(attachment.original_name)">
<img
:src="attachment.storage_path_full"
:alt="attachment.original_name"
class="attachment-thumb"
@click="handleChatImageClick(attachment.storage_path_full)"
/>
</template>
<template v-else-if="/\.(mp4|webm|ogg)$/i.test(attachment.original_name)">
<video
:src="attachment.storage_path_full"
controls
class="attachment-thumb"
@click.prevent="handleChatVideoClick(attachment.storage_path_full)"
></video>
</template>
<template v-else>
<div class="file-attachment">
<el-icon class="file-icon"><Document /></el-icon>
<span class="file-name">{{ attachment.original_name }}</span>
<el-button
type="primary"
size="small"
link
@click="downloadAttachment(attachment)"
>
下载
</el-button>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
</div>
<div class="chat-input" v-if="hasAuth('work_order@conversation_add')">
<!-- 附件上传区域 -->
<div class="chat-attachment-upload">
<el-upload
v-model:file-list="chatFileList"
class="chat-upload"
:action="actionImageUplode"
:on-success="handleChatUploadSuccess"
:headers="uploadHeaders"
multiple
:limit="5"
:on-exceed="handleChatExceed"
:show-file-list="false"
>
<el-button type="primary" link>
<el-icon><Paperclip /></el-icon>
添加附件
</el-button>
</el-upload>
<!-- 已上传附件预览 -->
<div class="uploaded-attachments" v-if="chatFileList.length > 0">
<div class="attachment-preview-item" v-for="(file, idx) in chatFileList" :key="idx">
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)">
<img
:src="file.file_url || file.response?.data?.file_url"
:alt="file.name"
class="preview-thumb"
/>
</template>
<template v-else>
<el-icon class="file-icon"><Document /></el-icon>
</template>
<span class="file-name">{{ file.name }}</span>
<el-button
type="danger"
size="small"
link
@click="removeChatFile(idx)"
>
<el-icon><Close /></el-icon>
</el-button>
</div>
</div>
</div>
<el-input
v-model="messageText"
type="textarea"
@@ -176,7 +255,7 @@
</ElDialog>
<!-- 图片预览弹窗 -->
<ElImageViewer v-if="imageViewerVisible" :url-list="imageList" :initial-index="imageViewerIndex" @close="imageViewerVisible = false" />
<ElImageViewer v-if="imageViewerVisible" :url-list="chatImageList.length > 0 ? chatImageList : imageList" :initial-index="imageViewerIndex" @close="closeImageViewer" />
<!-- 视频预览弹窗 -->
<ElDialog v-model="videoDialogVisible" width="60%" top="10vh" :show-close="true" append-to-body>
<video v-if="currentVideoUrl" :src="currentVideoUrl" controls autoplay style="width:100%;max-height:70vh;display:block;"></video>
@@ -197,6 +276,7 @@ import { ElImageViewer } from 'element-plus'
import { useAuth } from "@/composables/useAuth";
import { useUserStore } from '@/store/modules/user'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
import { Paperclip, Document, Close } from '@element-plus/icons-vue'
const { uploadHeaders } = useUploadHeaders()
const { hasAuth } = useAuth();
defineOptions({ name: 'Order' })
@@ -232,6 +312,10 @@ const scrollToBottom = () => {
}
const openChat = (row: any) => {
isDrawerVisible.value = true
// 清空数据
messageText.value = ''
messages.value = []
chatFileList.value = []
getConversationList(row.id)
}
@@ -333,14 +417,28 @@ const getConversationList = async (id:any) => {
const sendMessage = async () => {
const text = messageText.value.trim()
const ids = ticket_id.value
if (!text) return
if (!text && chatFileList.value.length === 0) return
// 准备附件数据
const attachments = chatFileList.value.map(file => {
const data = file.response?.data
if (data) {
return {
original_name: data.original_name,
storage_path: data.storage_path || data.file_name
}
}
return null
}).filter(Boolean)
const { message } = await WorkService.conversationAdd({
content: text,
ticket_id: ids,
attachment: attachments
})
ElMessage.success(message)
messageText.value = ''
chatFileList.value = [] // 清空附件列表
getConversationList(ids)
scrollToBottom()
}
@@ -473,6 +571,8 @@ const statusSubmit = async () => {
const imageViewerVisible = ref(false)
const imageViewerIndex = ref(0)
const imageList = computed(() => fileList.value.filter(file => /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)).map(file => file.file_url?file.file_url:file.response.data.file_url))
// 聊天图片预览列表
const chatImageList = ref<string[]>([])
function handleImageClick(idx: number) {
imageViewerIndex.value = idx
imageViewerVisible.value = true
@@ -484,6 +584,60 @@ function handleVideoClick(url: string) {
currentVideoUrl.value = url
videoDialogVisible.value = true
}
// 聊天相关
const chatFileList = ref<any[]>([])
const handleChatUploadSuccess = (response: any, uploadFile: any, uploadFiles: any[]) => {
chatFileList.value = uploadFiles.filter(item => item.status === 'success').map(item => {
const data = item.response?.data
if (data) {
return {
name: data.original_name,
file_url: data.storage_path_full,
status: 'success',
response: { data: data }
}
}
return null
}).filter(Boolean)
}
const handleChatExceed = () => {
ElMessage.warning('最多上传5个文件')
}
const removeChatFile = (index: number) => {
chatFileList.value.splice(index, 1)
}
const handleChatImageClick = (url: string) => {
// 设置聊天图片预览列表
chatImageList.value = [url]
imageViewerIndex.value = 0
imageViewerVisible.value = true
}
const closeImageViewer = () => {
imageViewerVisible.value = false
chatImageList.value = [] // 清空聊天图片列表
}
const handleChatVideoClick = (url: string) => {
currentVideoUrl.value = url
videoDialogVisible.value = true
}
const downloadAttachment = (attachment: any) => {
try {
const fileName = attachment.original_name || 'download'
const link = document.createElement('a')
link.href = attachment.storage_path_full
link.download = fileName
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
ElMessage.success('开始下载')
} catch (error) {
ElMessage.error('下载失败')
console.error('下载失败:', error)
}
}
</script>
<style lang="scss" scoped>
@@ -568,6 +722,7 @@ function handleVideoClick(url: string) {
.message-info {
flex-direction: row;
align-items: center;
}
.message-text {
@@ -584,6 +739,7 @@ function handleVideoClick(url: string) {
.message-info {
flex-direction: row-reverse;
align-items: center;
}
.message-text {
@@ -707,4 +863,101 @@ function handleVideoClick(url: string) {
margin-top: 2px;
}
}
// 聊天附件样式
.message-right .message-attachments{
text-align: right;
}
.message-left .message-attachments{
text-align: left;
}
.message-attachments {
margin-top: 8px;
.attachment-item {
margin-bottom: 8px;
.attachment-thumb {
max-width: 200px;
max-height: 150px;
border-radius: 6px;
cursor: pointer;
border: 1px solid var(--el-border-color-lighter);
transition: transform 0.2s;
&:hover {
transform: scale(1.05);
}
}
.file-attachment {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--el-fill-color-light);
border-radius: 6px;
border: 1px solid var(--el-border-color-lighter);
.file-icon {
font-size: 16px;
color: var(--el-text-color-regular);
}
.file-name {
flex: 1;
font-size: 14px;
color: var(--el-text-color-regular);
word-break: break-all;
}
}
}
}
.chat-attachment-upload {
margin-bottom: 12px;
.chat-upload {
margin-bottom: 8px;
}
.uploaded-attachments {
display: flex;
flex-wrap: wrap;
gap: 8px;
.attachment-preview-item {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: var(--el-fill-color-light);
border-radius: 6px;
border: 1px solid var(--el-border-color-lighter);
max-width: 200px;
.preview-thumb {
width: 40px;
height: 40px;
border-radius: 4px;
object-fit: cover;
}
.file-icon {
font-size: 16px;
color: var(--el-text-color-regular);
}
.file-name {
flex: 1;
font-size: 12px;
color: var(--el-text-color-regular);
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
</style>