Django+Nginx+uwsgi网站Channels+redis+daphne多人在线聊天实现粘贴上传图片

 在Django+Nginx+uwsgi网站Channels+redis+daphne多人在线的基础上(详见Django+Nginx+uwsgi网站使用Channels+redis+daphne实现简单的多人在线聊天及消息存储功能-CSDN博客),实现在输入框粘贴或打开本地图片,上传到网站后返回图片路径,以链接的形式将图片插入到输入框显示,并实现异步发送消息。具体效果如下图所示:

一、实现图片上传

实现图片上传客户端和服务器两边都要配置。

1.  客户端使用fetch实现图片上传

使用嵌入页面的javascript脚本实现fetch上传图片,主要代码如下:

        const csrftoken = document.querySelector('[name="csrfmiddlewaretoken"]').value;

            fetch('/chatjson/upload_image/',{
                method: 'POST',
                headers: {'X-CSRFToken': csrftoken},
                body: imgformData
                })
                .then(response => response.json())
                .then(data => {
                    if (data.image_url) {
                        //console.log('data image path::',data.image_url);
                        const img = document.createElement('img');
                        img.src = data.image_url;
                        editor.appendChild(img);
                        img.style.width = '300px';
                        img.style.height = 'auto';
                        img.style.objectFit = 'contain';
                        img.style.float = 'none';
                    } else {
                    console.error('Error uploading image:', data.error);
                }})
                .catch((error) => {
                    console.error('Error:', error);
                    alert('Error uploading the image.');
                });
2. 服务器端配置
(1) urls.py设置

客户端fetch的路径为'/chatjson/upload_image/',需要在urls.py中配置路径解析,包括聊天页面的路径解析

from myapp import views as channelsview
urlpatterns = [
....
    path('chatexp/<str:room_name>/', channelsview.chatexp, name='chatexp'),
    path('chatjson/upload_image/', channelsview.upload_image_json, name='upload_json'),
]
(2) 视图设置 myapp/views.py

包括聊天页面视图响应函数chatexp和文件上传响应upload_image_json

from django.contrib.auth.decorators import login_required

@login_required(login_url='/login/')
def chatexp(request,room_name):
    username = request.session.get('username','游客')
    msgs = ChatMessage.objects.filter(room=room_name).order_by('-create_time')[0:20]
    if request.method == 'POST':
        form = chatimgsForm(request.POST, request.FILES)
        if form.is_valid():
            image = form.save()
            #图片路径
            image_path = image.image.url
            return render(request,"channels/chattingexp.html",{'room_name':room_name,'form':form, 'image_path':image_path, 'username':username, 'msgs':msgs})

    form = chatimgsForm()
    return render(request,"channels/chattingexp.html",{'room_name':room_name,'form':form, 'image_path':'未上传', 'username':username, 'msgs':msgs})


from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse
from django.conf import settings
import os

@require_POST
@csrf_exempt
def upload_image_json(request):
    image_file = request.FILES['image']
    if image_file :
        upimg = chatimgs(image=image_file)
        upimg.save()
        #返回图片的绝对路径/home/...
        #image_path = upimg.image.path
        # 返回图片的相对路径/media/...
        image_path = upimg.image.url
        return JsonResponse({'image_url': image_path})
    else:
        return JsonResponse({'error': 'No image received!!'}, status=400)

二、客户端配置

1. 聊天页面设置

chatexp视图函数调用聊天页面chattingexp.html,聊天页面输入框由可编辑的div实现,页面内javascript脚本监听输入框的粘贴事件,将其中的图片上传,返回路径,将图片以img元素的形式插入到输入框,字符串转换成文本插入。脚本还实现了打开本地图片文件,同样上传后返回路径,将图片以img元素的形式插入到输入框。然后发送消息,消息文本通过channels异步传输,因文本只有图片链接,提高了传输效率。主要代码如下:

    <script>
        const editor = document.getElementById('chat-message-input'); 
        editor.addEventListener('paste', function(event) {
        // 阻止默认粘贴操作
        event.preventDefault();

        const clipboardData = (event.clipboardData||window.clipboardData);
        let items = clipboardData.items;

        const csrftoken = document.querySelector('[name="csrfmiddlewaretoken"]').value;

        for (const item of items) {
        if  (item.kind === 'string') {
            item.getAsString((text) => {
            const regex = /<img src="(.*?)"/;
            const match = text.match(regex);
 
            if (match) {
                document.execCommand('insertText', false, "link:<img src='"+match[1]+"'/>");
                //网页图片复制粘贴除了图片还带有图片链接,如果识别img链接插入图片会出现图片插入两次的问题
            } else {
                document.execCommand('insertText', false, text);
            }})
        } else if (item.kind === 'file' && item.type.indexOf('image/') !== -1) {
            var imgfile = item.getAsFile();
            const imgformData = new FormData();
            imgformData.append('image',imgfile);
            imgformData.append('csrfmiddlewaretoken', csrftoken);
            fetch('/chatjson/upload_image/',{
                method: 'POST',
                headers: {'X-CSRFToken': csrftoken},
                body: imgformData
                })
                .then(response => response.json())
                .then(data => {
                    if (data.image_url) {
                        //console.log('data image path::',data.image_url);
                        const img = document.createElement('img');
                        img.src = data.image_url;
                        editor.appendChild(img);
                        img.style.width = '300px';
                        img.style.height = 'auto';
                        img.style.objectFit = 'contain';
                        img.style.float = 'none';
                    } else {
                    console.error('Error uploading image:', data.error);
                }})
                .catch((error) => {
                    console.error('Error:', error);
                    alert('Error uploading the image.');
                });
        }}})

               
        window.onload = function() {
        var scrollableDiv = document.getElementById('chat-record');
        // 设置scrollTop使得滚动条向下翻
        scrollableDiv.scrollTop = scrollableDiv.scrollHeight;
        };
        const roomName = JSON.parse(document.getElementById('room-name').textContent);
        const username = JSON.parse(document.getElementById('username').textContent);
 
        const chatSocket = new WebSocket(
            'wss://abc.com/ws/chat/' + roomName + '/'
            );
 
        chatSocket.onmessage = function(e) {
            const data = JSON.parse(e.data);
            //data为收到的后端发出来的数据
            //console.log(data);
            if (data['message']) {
                if(data['username'] == username){
                    document.querySelector('#chat-record').innerHTML += ('<div class="chat-message right"><div class="user-content">' + data['username'] + '</div><div class="message-content"><span>' +data['message'] + '</span></div></div><br>');
                }else{
                    document.querySelector('#chat-record').innerHTML += ('<div class="chat-message left"><div class="user-content">' + data['username'] + '</div><div class="message-content"><span>' + data['message'] + '</span></div></div><br>');
                }
            } else {
            alert('消息为空!')
            }
            var scrollableDiv = document.getElementById('chat-record');
            // 设置scrollTop使得滚动条向下翻
            scrollableDiv.scrollTop = scrollableDiv.scrollHeight;
        };

 
        chatSocket.onclose = function(e) {
            console.error('聊天端口非正常关闭!');
        };
 
        document.querySelector('#chat-message-input').focus();
        document.querySelector('#chat-message-input').onkeyup = function(e) {
            if (e.keyCode === 13) {  // enter, return
                document.querySelector('#chat-message-submit').click();
            }
        };
 
        document.querySelector('#chat-message-submit').onclick = function(e) {
            const messageDivDom = document.querySelector('#chat-message-input');
            const message = messageDivDom.innerHTML;
            chatSocket.send(JSON.stringify({
                'message': message,
                'username':username
            }));
            messageDivDom.innerHTML = '';
        };

        //打开并上传本地文件
        document.getElementById('upload-btn').addEventListener('click', function () {
        const editor = document.getElementById('chat-message-input'); 
        const fileInput = document.getElementById('file-input');
        const csrftoken = document.querySelector('[name="csrfmiddlewaretoken"]').value;
        const file = fileInput.files[0];
        const formData = new FormData();
        formData.append('csrfmiddlewaretoken', csrftoken);
        formData.append('image', file);
    
        fetch('/chatjson/upload_image/', {
            method: 'POST',
            headers: {'X-CSRFToken': csrftoken},
            body: formData
        })
        .then(response => response.json())
        .then(data => {
            if (data.image_url) {
                    //console.log('data image path::',data.image_url);
                    const img = document.createElement('img');
                    img.src = data.image_url;
                    editor.appendChild(img);
                    img.style.width = '300px';
                    img.style.height = 'auto';
                    img.style.objectFit = 'contain';
                    img.style.float = 'none';
        } else {
                    console.error('Error uploading image:', data.error);
        }})
        .catch((error) => {
            console.error('Error:', error);
            alert('Error uploading the image.');
        });
        });

    </script>

2. 网页内容的粘贴处理

在Windows下从网页复制粘贴涉及剪切和粘贴 HTML 文档的片段。Windows采用 CF_HTML 剪贴板格式,其将原始 HTML 文本及其上下文(即外部 HTML)片段作为 ASCII 存储在剪贴板上。 这允许应用程序检查 HTML 片段的上下文,该片段由前面所有的周围标记组成,以便可以使用其属性来记录 HTML 片段的周围标记。 CF_HTML 剪贴板的常规布局或语法,如下所示:

<cf-html>                ::= <description-header> <context>
<context>                ::= [<preceding-context>] <fragment> [<trailing-context>]

<description-header>     ::= "Version:" <version> <br> ( <header-offset-keyword> ":" <header-offset-value> <br> )*
<header-offset-keyword>  ::= "StartHTML" | "EndHTML" | "StartFragment" | "EndFragment" | "StartSelection" | "EndSelection"
<header-offset-value>    ::= { Base 10 (decimal) integer string with optional _multiple_ leading zero digits (see "Offset syntax" below) }
<version>                ::= "0.9" | "1.0"
<fragment>               ::= <fragment-start-comment> <fragment-text> <fragment-end-comment>
<fragment-start-comment> ::= "<!--StartFragment -->"
<fragment-end-comment>   ::= "<!--EndFragment -->"
<preceding-context>      ::= { Arbitrary HTML }
<trailing-context>       ::= { Arbitrary HTML }
<fragment-text>          ::= { Arbitrary HTML }
<br>                     ::= "\r" | "\n" | "\r\n"

所以从网页单独粘贴一张图片时,会带入上下文,譬如:

<html>
<body>
<!--StartFragment--><img src="https://pics6.baidu.com/feed/a2cc7cd98d1001e9472d4193277785e255e797a5.jpeg@f_auto?token=475ae4ac49d50e247ac05e958799fc88"/><!--EndFragment-->
</body>
</html>

 如果既上传照片,又解析其中的<img>链接,会出现从网页上拷贝的文字图片在粘贴时,图片会被识别两次。所以对网页拷贝的内容,解决办法有两种:

(1)将图片以链接形式插入到文本中

chattingexp.html中Javascript脚本的主要代码如下:

    <script>
        const editor = document.getElementById('chat-message-input'); 
        editor.addEventListener('paste', function(event) {
        // 阻止默认粘贴操作
        event.preventDefault();

        const clipboardData = (event.clipboardData||window.clipboardData);
        let items = clipboardData.items;

        const csrftoken = document.querySelector('[name="csrfmiddlewaretoken"]').value;

        for (const item of items) {
        let htmlimglink = false;
        if(item.kind === 'string'&& item.type === 'text/html') {
            item.getAsString((text) => {
            const regex = /<img src="(.*?)"/;
            const match = text.match(regex);
 
            if (match) {                
                //网页图片以链接的形式存在,不上传服务器
                //document.execCommand('insertText', false, "link:<img src='"+match[1]+"'/>");
                const img = document.createElement('img');
                img.src = match[1];
                editor.appendChild(img);
                img.style.width = '300px';
                img.style.height = 'auto';
                img.style.objectFit = 'contain';
                img.style.float = 'none';
                htmlimglink = true;
            } else {
                //console.log(text);
                //新建一个div
                var divElement = document.createElement( "div" );
                divElement.innerHTML = text;//获取文本内容

                //如果divElement是null或undefined,那么返回为""(一个空字符串)。
                //如果divElement非null且存在textContent或innerText属性,那么将会返回该属性的值。如果两者都不存在,将会返回""。
                document.execCommand('insertText', false, divElement.textContent || divElement.innerText || "");
            }})
        } else if (item.kind === 'string'&& item.type === 'text/plain'){
            item.getAsString((text) => {
                document.execCommand('insertText', false, text);
            })

        }else if(htmimglink=false && item.kind === 'file' && item.type.indexOf('image/') !== -1) {
            var imgfile = item.getAsFile();
            const imgformData = new FormData();
            imgformData.append('image',imgfile);
            imgformData.append('csrfmiddlewaretoken', csrftoken);
            fetch('/chatjson/upload_image/',{
                method: 'POST',
                headers: {'X-CSRFToken': csrftoken},
                body: imgformData
                })
                .then(response => response.json())
                .then(data => {
                    if (data.image_url) {
                        //console.log('data image path::',data.image_url);
                        const img = document.createElement('img');
                        img.src = data.image_url;
                        editor.appendChild(img);
                        img.style.width = '300px';
                        img.style.height = 'auto';
                        img.style.objectFit = 'contain';
                        img.style.float = 'none';
                    } else {
                    console.error('Error uploading image:', data.error);
                }})
                .catch((error) => {
                    console.error('Error:', error);
                    alert('Error uploading the image.');
                });
        }}})

               
        //本地文件上传按钮事件
        document.getElementById('upload-btn').addEventListener('click', function () {
        const editor = document.getElementById('chat-message-input'); 
        const fileInput = document.getElementById('file-input');
        const csrftoken = document.querySelector('[name="csrfmiddlewaretoken"]').value;
        const file = fileInput.files[0];
        const formData = new FormData();
        formData.append('csrfmiddlewaretoken', csrftoken);
        formData.append('image', file);
    
        fetch('/chatjson/upload_image/', {
            method: 'POST',
            headers: {'X-CSRFToken': csrftoken},
            body: formData
        })
        .then(response => response.json())
        .then(data => {
            if (data.image_url) {
                    //console.log('data image path::',data.image_url);
                    const img = document.createElement('img');
                    img.src = data.image_url;
                    editor.appendChild(img);
                    img.style.width = '300px';
                    img.style.height = 'auto';
                    img.style.objectFit = 'contain';
                    img.style.float = 'none';
        } else {
                    console.error('Error uploading image:', data.error);
        }})
        .catch((error) => {
            console.error('Error:', error);
            alert('Error uploading the image.');
        });
        });

    </script>

(2)使用fetch获取图片Blob数据后上传到服务器

在前面的基础上,得到图片的链接后,利用正则表达式得到图片名和图片的格式,然后fetch到图片Blob数据后,使用FormData创建一个新的File对象,再使用fetch上传到服务器,返回路径,插入到输入框中。使用效果如下:

3. 聊天页面设计及功能实现

至此,多人在线聊天页面基本实现了粘贴图片和文字混合内容的功能。汇总之前的代码,chattingexp.html的主要内容如下: 

{% extends "newdesign/newbase.html" %}

{# 自定义过滤器startswith #}
    {% load django_bootstrap5 %}
        {% block mytitle %}
        <title>{{room_name}}号聊天室</title>
            <style>
                .chat-window {
                    max-width: 900px;
                    height: 500px;
                    overflow-y: scroll; /* 添加垂直滚动条 */
                    margin: auto;
                    background-color: #f1f1f1;
                    border: 2px solid #09e3f7;
                    border-radius: 5px;
                    padding: 10px;
                }
                
                .chat-message {
                    clear: both;
                    overflow: hidden;
                    margin-bottom: 10px;
                    text-align: left;
                }
                
                .chat-message .message-content {
                    border-radius: 5px;
                    padding: 8px;
                    max-width: 500px;
                    float: left;
                    clear: both;
                }
                
                .chat-message.right .message-content {
                    background-color: #428bca;
                    color: white;
                    float: right;
                    width:420px;
                }
                .chat-message.right .user-content {
                    background-color: #f7e91d;
                    border-radius:4px;
                    color: black;
                    float: right;
                    width: auto;
                    text-align: right;
                    padding-left:10px;
                    padding-right:10px;
                }
                
                .chat-message.left .message-content {
                    background-color: #2ef3be;
                    border-color: #ddd;
                    float:left;
                    width:420px;
                }
                .chat-message.left .user-content {
                    background-color: #f7e91d;
                    border-radius:4px;
                    border-color: #ddd;
                    float: left;
                    width: auto;
                    text-align: left;
                    padding-left:8px;
                    padding-right:8px;
                }
                .inputarea {
                    display:flex;
                    flex-direction: column;
                    justify-content: center;
                    align-items: center;
                    width:900px;
                    margin: 0 auto;
                }
                .replyinput {
                    display: inline-block;
                    width: 900px;
                    min-height:120px;
                    background-color: rgb(169, 228, 250);
                    border:2px solid #09e3f7;
                    border-radius: 10px;
                    padding: 10px;
                    font-size: 14px;
                    text-align: left;
                }
                .replyarea {
                    width: 900px;
                    height:50px;
                    margin:0 auto;
                }
                .sendImg-btn {
                    float:left;
                    border: 0px;
                    background-color: transparent;
                }
                .reply-btn {
                    float:right;
                }

            </style>
        {% endblock %}
    {% block maincontent %} 
    <div class="container">
    <div id="chat-record" class="chat-window">
    {% for m in msgs reversed %}
        {% if m.username == request.user.username %}
        <div class="chat-message right"><div class="user-content">{{m.username}}</div><div class="message-content"><span>{{m.content|safe}}</span></div></div>
        <br>
        {% else %}
        <div class="chat-message left"><div class="user-content">{{m.username}}</div><div class="message-content"><span>{{m.content|safe}}</span></div></div>
        <br>
        {% endif %}
    {% endfor %}
    </div>
    </div>
    <br>
    <form method='post' enctype="multipart/form-data"></form>
    {% csrf_token %}
    <div class="inputarea">
        <div
            class="replyinput"
            contenteditable="true"
            id="chat-message-input"
            @focus="onFocusEditableDiv"
            >
        </div>
        <br>
        <div class="replyarea">
            &nbsp;&nbsp;<button id="upload-btn">上传本地图片</button>&nbsp;&nbsp;<input type="file" id="file-input" accept="image/*"/>
        <button class="reply-btn" id="chat-message-submit" type="primary" style="height:40px;background-color: #0d4de1;color:white;border-radius: 4px;">发送消息</button>
        </div>
      </div>
    </form>

    {{ room_name|json_script:"room-name" }}
    {{ username|json_script:"username" }}
<script>
    const editor = document.getElementById('chat-message-input'); 
    editor.addEventListener('paste', function(event) {
        // 阻止默认粘贴操作
        event.preventDefault();

        const clipboardData = (event.clipboardData||window.clipboardData);
        let items = clipboardData.items;

        const csrftoken = document.querySelector('[name="csrfmiddlewaretoken"]').value;

        var htmlimglink = false;
        for (const item of items) {
        if(item.kind === 'string'&& item.type === 'text/html'){
            //如果是网页内容,因粘贴板格式带有图片链接,直接根据链接下载图片上传,不需要再次上传照片文件
            htmlimglink = true;
        }};

        for (const item of items) {
        if(item.kind === 'string'&& item.type === 'text/html') {
            item.getAsString((text) => {
            const regex = /<img src="(.*?)"/g;
            let matches = '';
            matches = text.matchAll(regex);
            if (matches) {   
                for (const match of matches) {
                //网页图片以链接的形式存在,不上传服务器
                //match[0]为整个匹配组,match[1]为第一个捕获组
                //document.execCommand('insertHTML', false, "<img style='width:300px;height:auto;object-fit:contain;float:none;' src='"+match[1]+"'/>");
                
                fetch(match[1])
                .then(response =>{
                if (!response.ok) {
                    throw new Error('Network response was not ok ' + response.statusText);
                    }
                    return response.blob(); // 转换响应为Blob对象
                })
                .then(blob => {
                const fileNameextra = match[1].split('/').pop(); // URL的最后一部分包含文件名和查询参数
                const regex = /^(.*)\.(png|jpeg|jpg|gif|bmp|webp|svg|tiff|avif)(?:\?|\@|#|$)/i ;
                let filematches = '';
                filematches = fileNameextra.match(regex);
                if(filematches){
                const fileName = filematches[1]+'.'+ filematches[2];
                const contentType = 'image/' + filematches[2];
                //console.log("fileName:::",fileName);
                const imgformData = new FormData();
                imgformData.append('image',new File([blob], fileName, { type: contentType }));
                imgformData.append('csrfmiddlewaretoken', csrftoken);
                fetch('/chatjson/upload_image/',{
                    method: 'POST',
                    headers: {'X-CSRFToken': csrftoken},
                    body: imgformData
                    })
                    .then(response => response.json())
                    .then(data => {
                        if (data.image_url) {
                            //console.log('data image path::',data.image_url);
                            document.execCommand('insertHTML', false, "<img style='width:300px;height:auto;object-fit:contain;float:none;' src='"+data.image_url+"'/>");
                        } else {
                        console.error('Error uploading image:', data.error);
                    }})
                    .catch((error) => {
                        console.error('Error:', error);
                        alert('Error uploading the image.');
                    });
                }else{
                    throw new Error('图片格式不正确!');
                }}
                )
                .catch(error => console.error('Error fetching or processing the image:', error));
                };
            } else {
                //新建一个div
                var divElement = document.createElement( "div" );
                divElement.innerHTML = text;//获取文本内容

                //如果divElement是null或undefined,那么返回为""(一个空字符串)。
                //如果divElement非null且存在textContent或innerText属性,那么将会返回该属性的值。如果两者都不存在,将会返回""。
                document.execCommand('insertText', false, divElement.textContent || divElement.innerText || "");
            }})
        } else if (item.kind === 'string'&& item.type === 'text/plain'){
            item.getAsString((text) => {
                document.execCommand('insertText', false, text);
            })
        }else if(htmlimglink === false && item.kind === 'file' && item.type.indexOf('image/') !== -1) {
            var imgfile = item.getAsFile();
            const imgformData = new FormData();
            imgformData.append('image',imgfile);
            imgformData.append('csrfmiddlewaretoken', csrftoken);
            fetch('/chatjson/upload_image/',{
                method: 'POST',
                headers: {'X-CSRFToken': csrftoken},
                body: imgformData
                })
                .then(response => response.json())
                .then(data => {
                    if (data.image_url) {
                        //console.log('data image path::',data.image_url);
                        const img = document.createElement('img');
                        img.src = data.image_url;
                        editor.appendChild(img);
                        img.style.width = '300px';
                        img.style.height = 'auto';
                        img.style.objectFit = 'contain';
                        img.style.float = 'none';
                    } else {
                    console.error('Error uploading image:', data.error);
                }})
                .catch((error) => {
                    console.error('Error:', error);
                    alert('Error uploading the image.');
                });
        }}})

               
        window.onload = function() {
        var scrollableDiv = document.getElementById('chat-record');
        // 设置scrollTop使得滚动条向下翻
        scrollableDiv.scrollTop = scrollableDiv.scrollHeight;
        };
        const roomName = JSON.parse(document.getElementById('room-name').textContent);
        const username = JSON.parse(document.getElementById('username').textContent);
 
        const chatSocket = new WebSocket(
            'wss://abc.com/ws/chat/' + roomName + '/'
            );
 
        chatSocket.onmessage = function(e) {
            const data = JSON.parse(e.data);
            //data为收到的后端发出来的数据
            //console.log(data);
            if (data['message']) {
                if(data['username'] == username){
                    document.querySelector('#chat-record').innerHTML += ('<div class="chat-message right"><div class="user-content">' + data['username'] + '</div><div class="message-content"><span>' +data['message'] + '</span></div></div><br>');
                }else{
                    document.querySelector('#chat-record').innerHTML += ('<div class="chat-message left"><div class="user-content">' + data['username'] + '</div><div class="message-content"><span>' + data['message'] + '</span></div></div><br>');
                }
            } else {
            alert('消息为空!')
            }
            var scrollableDiv = document.getElementById('chat-record');
            // 设置scrollTop使得滚动条向下翻
            scrollableDiv.scrollTop = scrollableDiv.scrollHeight;
        };

 
        chatSocket.onclose = function(e) {
            console.error('聊天端口非正常关闭!');
        };
 
        document.querySelector('#chat-message-input').focus();
        document.querySelector('#chat-message-input').onkeyup = function(e) {
            if (e.keyCode === 13) {  // enter, return
                document.querySelector('#chat-message-submit').click();
            }
        };
 
        document.querySelector('#chat-message-submit').onclick = function(e) {
            const messageDivDom = document.querySelector('#chat-message-input');
            const message = messageDivDom.innerHTML;
            chatSocket.send(JSON.stringify({
                'message': message,
                'username':username
            }));
            messageDivDom.innerHTML = '';
        };


        document.getElementById('upload-btn').addEventListener('click', function () {
        const editor = document.getElementById('chat-message-input'); 
        const fileInput = document.getElementById('file-input');
        const csrftoken = document.querySelector('[name="csrfmiddlewaretoken"]').value;
        const file = fileInput.files[0];
        const formData = new FormData();
        formData.append('csrfmiddlewaretoken', csrftoken);
        formData.append('image', file);
    
        fetch('/chatjson/upload_image/', {
            method: 'POST',
            headers: {'X-CSRFToken': csrftoken},
            body: formData
        })
        .then(response => response.json())
        .then(data => {
            if (data.image_url) {
                    //console.log('data image path::',data.image_url);
                    const img = document.createElement('img');
                    img.src = data.image_url;
                    editor.appendChild(img);
                    img.style.width = '300px';
                    img.style.height = 'auto';
                    img.style.objectFit = 'contain';
                    img.style.float = 'none';
        } else {
                    console.error('Error uploading image:', data.error);
        }})
        .catch((error) => {
            console.error('Error:', error);
            alert('Error uploading the image.');
        });
        });

    </script>
{% endblock %}
4. 存在的问题

无法正确处理word内容,拷贝粘贴文字和图片混合内容粘贴显示不太正常,单独复制粘贴图片没问题,文本内容粘贴时需要粘贴为纯文本。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/925056.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Git】Git 命令参考手册

目录 Git 命令参考手册1. 创建仓库1.1 创建一个新的本地仓库1.2 克隆一个仓库1.3 克隆仓库到指定目录 2. 提交更改2.1 显示工作目录中已修改的文件&#xff0c;准备提交2.2 将文件添加到暂存区&#xff0c;准备提交2.3 将所有已修改的文件添加到暂存区&#xff0c;准备提交2.4 …

TDengine在debian安装

参考官网文档&#xff1a; 官网安装文档链接 从列表中下载获得 Deb 安装包&#xff1b; TDengine-server-3.3.4.3-Linux-x64.deb (61 M) 进入到安装包所在目录&#xff0c;执行如下的安装命令&#xff1a; sudo dpkg -i TDengine-server-<version>-Linux-x64.debNOTE 当…

linux安装mysql8.0.40

一、下载MySQL安装包 1.查看glibc版本 rpm -qa | grep glibc 2.到mysql官网下载安装包 ​ 二、解压安装 1.上传压缩包纸/usr/local 目录下&#xff0c;解压&#xff1a; tar -xvf mysql-8.0.40-linux-glibc2.17-x86_64.tar.xz 2.重命名&#xff1a; mv mysql-8.0.40-linux-…

用Pycharm安装manim

由于版本和工具的差异&#xff0c;manim的安装方式不尽相同。本文用Pycharm来安装manim. 一、准备工作&#xff1a;安装相应版本的python、pycharm和ffmpeg. 此处提供一种安装ffmpeg的方式 下载地址&#xff1a;FFmpeg 下载后&#xff0c;解压到指定目录。 配置环境变量&am…

运维面试整理总结

面试题可以参考:面试题总结 查看系统相关信息 查看系统登陆成功与失败记录 成功&#xff1a;last失败&#xff1a;lastb 查看二进制文件 hexdump查看进程端口或连接 netstat -nltp ss -nltp补充&#xff1a;pidof与lsof命令 pidof [进程名] #根据 进程名 查询进程id ls…

Kubernetes 之 Ingress 和 Service 的异同点

1. 概念与作用 1.1 Ingress Ingress 是什么&#xff1f; Ingress主要负责七层负载&#xff0c;将外部 HTTP/HTTPS 请求路由到集群内部的服务。它可以基于域名和路径定义规则&#xff0c;从而将外部请求分配到不同的服务。 ingress作用 提供 基于 HTTP/HTTPS 的路由。 支持 …

初识 Django

声明 适用于想要快速入门的开发者&#xff0c;有前后端开发以及语言基础&#xff0c;想要学习语法或者特性。 想要学会快速开发&#xff0c;快速入门&#xff0c;请看博客【实用向】Django 框架入门并结合本篇文章。 命令 命令描述startproject创建一个 Django 项目startapp…

python简单算法

冒泡 def boll(lis):i 0while i<len(lis)-1:j 0while j<len(lis)-1-i:if lis[j] > lis[j1]:lis[j],lis[j 1] lis[j1],lis[j]j1i1选择排序 def selct1(lit):i 0while i<len(lit)-1:j i1min1 iwhile j < len(lit):if lit[j] < lit[min1]:min1 jj 1li…

【大模型】基于LLaMA-Factory的模型高效微调

LLaMA-Factory项目介绍 LLaMA Factory 是一个简单易用且高效的大型语言模型&#xff08;Large Language Model&#xff09;训练与微调平台。通过 LLaMA Factory&#xff0c;可以在无需编写任何代码的前提下&#xff0c;在本地完成上百种预训练模型的微调&#xff0c;框架特性包…

电脑中的vcruntime140_1.dll文件有问题要怎么解决?一键修复vcruntime140_1.dll

遇到“vcruntime140_1.dll无法继续执行代码”的错误通常表明电脑中的vcruntime140_1.dll文件有问题。这个文件属于Visual C Redistributable&#xff0c;对很多程序的运行至关重要。本文将提供几个步骤&#xff0c;帮助你迅速修复这一错误&#xff0c;使电脑恢复正常工作状态。…

鼠标前进后退键改双击,键盘映射(AutoHotkey)

初衷&#xff1a; 1.大部分鼠标为不可自定义按键&#xff0c;可以自定义的又很贵。 鼠标左键是双击是很频类很高的操作&#xff0c;鼠标前进/后退按键个人感觉使用频率很低&#xff0c;因此把鼠标前进/后退改为双击还是很合适的。 2.有些短款的键盘没有Home或End键&#xff0c;…

VSCode Terminal无法运行node以及node-gyp等指令

无法使用node指令&#xff0c;使用管理员权限启动VSCode即可&#xff0c;或者右键VSCode属性&#xff0c;修改兼容性中使用管理员权限打开。 运行node-gyp等指令出现因为在此系统上禁止运行脚本。有关详细信息&#xff0c;请参阅 https:/go.microsoft.com/fwlink/?LinkID1351…

npm install -g@vue/cli报错解决:npm error code ENOENT npm error syscall open

这里写目录标题 报错信息1解决方案 报错信息2解决方案 报错信息1 使用npm install -gvue/cli时&#xff0c;发生报错&#xff0c;报错图片如下&#xff1a; 根据报错信息可以知道&#xff0c;缺少package.json文件。 解决方案 缺什么补什么&#xff0c;这里我们使用命令npm…

Elasticsearch:Retrievers 介绍

检索器&#xff08;retrievers&#xff09;是 Elasticsearch 中搜索 API 中添加的新抽象层。它们提供了在单个 _search API 调用中配置多阶段检索管道的便利。此架构通过消除对复杂搜索查询的多个 Elasticsearch API 调用的需求&#xff0c;简化了应用程序中的搜索逻辑。它还减…

nvidia-container-toolkit安装问题(OpenPGP)

1.正常情况下 apt-get install -y nvidia-container-toolkit2.使用nvidia源 nvidia-container-toolkit官网有安装教程 2.1 配置生产存储库 curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-containe…

open-instruct - 训练开放式指令跟随语言模型

文章目录 关于 open-instruct设置训练微调偏好调整RLVR 污染检查开发中仓库结构 致谢 关于 open-instruct github : https://github.com/allenai/open-instruct 这个仓库是我们对在公共数据集上对流行的预训练语言模型进行指令微调的开放努力。我们发布这个仓库&#xff0c;并…

38 基于单片机的宠物喂食(ESP8266、红外、电机)

目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 基于STC89C52单片机&#xff0c;采用L298N驱动连接P2.3和P2.4口进行电机驱动&#xff0c; 然后串口连接P3.0和P3.1模拟ESP8266&#xff0c; 红外传感器连接ADC0832数模转换器连接单片机的P1.0~P1.…

Mac安装及合规无限使用Beyond Compare

文章目录 Beyond CompareBeyond Compare简介Beyond Compare安装Beyond Compare到期后继续免费使用 Beyond Compare Beyond Compare简介 Beyond Compare 是一款由 Scooter Software 开发的文件和文件夹比较工具。它主要用于对比两个文件或文件夹之间的差异&#xff0c;并支持文…

详细分析 npm run build 基本知识 | 不同环境不同命令

目录 前言1. 基本知识2. 构建逻辑 前言 关于部署服务器的知识推荐阅读&#xff1a;npm run build部署到云服务器中的Nginx&#xff08;图文配置&#xff09; 1. 基本知识 npm run 是 npm 的一个命令&#xff0c;用于运行 package.json 中定义的脚本&#xff0c;可以通过 “s…

水体分割检测 包含YOLOV,COCO,VOC三种标记的数据集包含 857张图片

说明 水体分割检测指的是利用深度学习模型进行水体区域的分割和检测。YOLO&#xff08;You Only Look Once&#xff09;是一种流行的实时目标检测算法&#xff0c;其主要特点是速度快&#xff0c;适合于实时场景下的目标检测。 在水体分割检测中&#xff0c;可以使用YOLO算法来…