当前位置:   article > 正文

django项目中使用百度UEditor富文本编辑器_djangoueditor

djangoueditor
获取UEditor

从 官网下载UEditor源码,下载的源码中并没有ueditor.all.js文件。需要使用grunt来把源码包打包成部署版本(含有ueditor.all.js 文件)

  1. 安装 node.js
  2. 全局安装grunt (npm install -g grunt-cli)
  3. 命令行切换目录到ueditor目录下(含有Gruntfile.js文件)
  4. npm install 安装依赖
  5. 执行命令grunt default,命令会将源码文件打包,同时会生成 ueditor.all.js 文件,执行完后会在 ueditor 目录下生成一个 dist 目录
  6. dist里面有个utf8-php目录,里面就是部署版本,修改重命名为ueditor
服务器需要处理的ueditor相关请求

把UEditor部署版本放到项目的static静态目录下,然后按照官网的说明在相关要使用的页面引入ueditor.config.js和ueditor.all.js文件,就可以使用UEditor的绝大部分功能。只有上传文件、图片、视频、在线图片、在线文件这一部分和上传有关系的部分不好用,因为这些功能需要后端服务器的支持才可以,所以需要在我们的django项目中实现响应的视图函数来处理这部分请求:

  1. 获取后端配置项信息请求(请求地址携带的参数为action=config)
  2. 上传图片请求(请求地址携带参数看后端配置项设置的具体值,默认为:action=uploadimage)
  3. 上传视频请求(请求地址携带参数看后端配置项设置的具体值,默认为:action=uploadvideo)
  4. 上传附件请求(请求地址携带参数看后端配置项设置的具体值,默认为:action=uploadfile)
  5. 列出所有图片请求(请求地址携带参数看后端配置项设置的具体值,默认为:action=listimage)
  6. 列出所有附件请求(请求地址携带参数看后端配置项设置的具体值,默认为:action=listfile)
  7. 上传涂鸦图片请求(请求地址携带参数看后端配置项设置的具体值,默认为:action=uploadscrawl)
  8. 抓取远程图片(请求地址携带参数看后端配置项设置的具体值,默认为:action=catchimage)

上传图片、上传截图、上传视频、上传附件和上传涂鸦图片,后台服务器应返回的JSON格式数据如下:

{
    "state": "SUCCESS",                         // 状态信息,成功时返回值固定为SUCCESS
    "url": "/ueditor/2_%E5%B9%BF%E5%91%8A%E6%8E%92%E6%9C%9F%E5%B9%B3%E5%8F%B0/%E6%9C%80%E6%96%B0%E7%89%A9%E6%96%99%E5%8D%95ID%E6%98%BE%E7%A4%BA%E9%94%99%E8%AF%AF001.png",
    "title": "最新物料单ID显示错误001.png",      // 文件名称
    "original": "最新物料单ID显示错误001.png"    // 内部文件名,一般和titile相同
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

抓取远程图片,后台服务器应返回的JSON格式数据如下:

{
    "state": "SUCCESS",
    "list": [
        {
            "state": "SUCCESS",
            "url": "/ueditor/2_%E5%B9%BF%E5%91%8A%E6%8E%92%E6%9C%9F%E5%B9%B3%E5%8F%B0/sww.png",
            "size": 7200,
            "title": "sww.png",
            "original": "sww.png",
            "source": "http://seventest.cn/static/images/sww.png",  // 下载地址
        }
    ]
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

列出指定目录下的图片、列出指定目录下的文件,后台服务器应返回的JSON格式数据如下:

{
    "state": "SUCCESS",
    "list": [
        {
            "url": "/ueditor/2_%E5%B9%BF%E5%91%8A%E6%8E%92%E6%9C%9F%E5%B9%B3%E5%8F%B0/del_image_01.png"
        },
        {
            "url": "/ueditor/2_%E5%B9%BF%E5%91%8A%E6%8E%92%E6%9C%9F%E5%B9%B3%E5%8F%B0/image.png"
        },
        {
            "url": "/ueditor/2_%E5%B9%BF%E5%91%8A%E6%8E%92%E6%9C%9F%E5%B9%B3%E5%8F%B0/test_picture_002.gif"
        },
        {
            "url": "/ueditor/2_%E5%B9%BF%E5%91%8A%E6%8E%92%E6%9C%9F%E5%B9%B3%E5%8F%B0/%E6%9C%80%E6%96%B0%E7%89%A9%E6%96%99%E5%8D%95ID%E6%98%BE%E7%A4%BA%E9%94%99%E8%AF%AF001.png"
        },
        {
            "url": "/ueditor/2_%E5%B9%BF%E5%91%8A%E6%8E%92%E6%9C%9F%E5%B9%B3%E5%8F%B0/%E6%B6%82%E9%B8%A6.png"
        }
    ],
    "start": 0,
    "total": 5
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
django项目处理ueditor请求代码

以下是我开发的转测试流程管理系统中实现处理以上ueditor请求的相关代码

  • settings.py配置
    在这里插入图片描述

  • 项目路由配置(urls.py)

# -*- coding:utf-8 -*- ''' Created on 2019年12月15日

@author: siwenwei '''

from django.urls import re_path from . import views

urlpatterns = [
    re_path(r'^$', views.Index.as_view()),
    re_path(r'^login$', views.Login.as_view()),
    re_path(r'^logout$', views.Logout.as_view()),
    re_path(r'^userguide$', views.UserGuide.as_view()),
    re_path(r'^author$', views.Author.as_view()),
    re_path(r'^register$', views.Register.as_view()),
    re_path(r'^{}$'.format(views.Settings.suburl), views.Settings.as_view()),
    re_path(r'^{}$'.format(views.BaseData.suburl), views.BaseData.as_view()),
    re_path(r'^testproject/{}$'.format(views.Project.suburl), views.Project.as_view()),
    re_path(r'^user/list$', views.UserList.as_view()),
    re_path(r'^member/{}$'.format(views.Member.suburl), views.Member.as_view()),
    re_path(r'^flow/list$', views.FlowList.as_view()),
    re_path(r'^flow/{}$'.format(views.Flow.suburl), views.Flow.as_view()),
    re_path(r'^flow/node/list$', views.FlowNodes.as_view()),
    re_path(r'^flow/node/settings$', views.NodeSettings.as_view()),
    re_path(r'^flow/node/run$', views.NodeRunner.as_view()),
    re_path(r'^flow/node/file/{}$'.format(views.NodeFiles.suburl), views.NodeFiles.as_view()),
    re_path(r'^{}$'.format(views.UEditor.SERVER_URL['value']), views.UEditor.as_view()),  # 处理 ueditor 获取服务端配置 上传图片 上传附件 列出所有图片 列出所有附件 删除图片 删除附件等请求
    re_path(r'^{}/{}$'.format(views.UEditor.SERVER_URL['value'], views.Ueditor_FileServer.suburl), views.Ueditor_FileServer.as_view()),
# 处理ueditor 获取各种文件的请求,其实就是提供获取静态文件的服务 ]

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 处理ueditor各种上传请求、图片列表、附件列表请求视图(ueditor.py)
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@Author: 思文伟
@Date: 2021/03/07 14:45:44
'''

import os
import json
import base64
from urllib.parse import quote
from urllib.parse import unquote
from urllib.request import urlopen

from django.views import View
from django.conf import settings
from django.http import HttpResponse
from django.http import JsonResponse
from django.http import HttpResponseBadRequest
from django.core.exceptions import ObjectDoesNotExist
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.http import QueryDict

from .. import models
from ..utils.filesize import FileSize
from ..utils.ueditor_path_formatter import UeditorPathFormatter


@method_decorator(csrf_exempt, name='dispatch')
class UEditor(View):

    ROOT_DIR = settings.UEDITOR_FILE_DIR
    SERVER_URL = {'key': 'serverUrl', 'value': 'ueditor', 'root_dir': ROOT_DIR}

    # 上传图片配置项
    IMAGE = {
        "imageActionName": "uploadimage",  # 执行上传图片的action名称
        "imageMaxSize": 20485760,  # 上传大小限制,单位B,10M
        "imageFieldName": "upfile",  # * 提交的图片表单名称
        "imageUrlPrefix": "",
        "imagePathFormat": "",
        "imageAllowFiles": [".png", ".jpg", ".jpeg", ".gif", ".bmp"],  # 上传图片格式显示
    }

    # 涂鸦图片上传配置项
    SCRAWL = {
        "scrawlActionName": "uploadscrawl",  # 执行上传涂鸦的action名称
        "scrawlFieldName": "upfile",  # 提交的图片表单名称
        "scrawlMaxSize": 10485760,  # 上传大小限制,单位B  10M
        "scrawlUrlPrefix": "",
        "scrawlPathFormat": "",
    }

    # 截图工具上传
    SNAPSCREEN = {
        "snapscreenActionName": "uploadimage",  # 执行上传截图的action名称
        "snapscreenPathFormat": "",
        "snapscreenUrlPrefix": "",
    }

    # 抓取远程图片配置
    CATCHER = {
        "catcherLocalDomain": ["127.0.0.1", "localhost", "img.baidu.com"],
        "catcherPathFormat": "",
        "catcherActionName": "catchimage",  # 执行抓取远程图片的action名称
        "catcherFieldName": "source",  # 提交的图片列表表单名称
        "catcherMaxSize": 10485760,  # 上传大小限制,单位B
        "catcherAllowFiles": [".png", ".jpg", ".jpeg", ".gif", ".bmp"],  # 抓取图片格式显示
        "catcherUrlPrefix": "",
    }

    # 上传视频配置
    VIDEO = {
        "videoActionName": "uploadvideo",  # 执行上传视频的action名称
        "videoPathFormat": "",
        "videoFieldName": "upfile",  # 提交的视频表单名称
        "videoMaxSize": 102400000,  # 上传大小限制,单位B,默认100MB
        "videoUrlPrefix": "",
        "videoAllowFiles": [".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg", ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid"],  # 上传视频格式显示
    }

    # 上传文件配置
    __FILE_ALLOW_FILES = [
        ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg", ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid", ".rar", ".zip", ".tar",
        ".gz", ".7z", ".bz2", ".cab", ".iso", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt", ".md", ".xml"
    ]
    FILE = {
        "fileActionName": "uploadfile",  # controller里,执行上传视频的action名称
        "filePathFormat": "",
        "fileFieldName": "upfile",  # 提交的文件表单名称
        "fileMaxSize": 204800000,  # 上传大小限制,单位B,200MB
        "fileUrlPrefix": "",  # 文件访问路径前缀
        "fileAllowFiles": __FILE_ALLOW_FILES,  # 上传文件格式显示
    }

    # 列出指定目录下的图片
    IMAGE_MANAGER = {
        "imageManagerActionName": "listimages",  # 执行图片管理的action名称
        "imageManagerListPath": "",
        "imageManagerListSize": 30,  # 每次列出文件数量
        # 列出的文件类型
        "imageManagerAllowFiles": [".png", ".jpg", ".jpeg", ".gif", ".bmp"],
        "imageManagerUrlPrefix": "",  # 图片访问路径前缀
    }

    # 列出指定目录下的文件
    __FILE_MANAGER_ALLOW_FILES = [
        ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tif", ".psd"
        ".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg", ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid", ".rar", ".zip", ".tar", ".gz", ".7z", ".bz2", ".cab", ".iso",
        ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt", ".md", ".xml", ".exe", ".com", ".dll", ".msi"
    ]
    FILE_MANAGER = {
        "fileManagerActionName": "listfiles",  # 执行文件管理的action名称
        "fileManagerListPath": "",
        "fileManagerUrlPrefix": "",
        "fileManagerListSize": 30,  # 每次列出文件数量
        "fileManagerAllowFiles": __FILE_MANAGER_ALLOW_FILES  # 列出的文件类型
    }

    DEFAULT_MAX_SIZE = 20485760

    IMAGE_REMOVE = {"imageRemoveActionName": "removeimage"}
    FILE_REMOVE = {"fileRemoveActionName": "removefile"}
    SAVE_TO_SERVER = {"saveToServerActionName": "savecontent"}

    @property
    def upload_config(self):
        """返回给前端的上传配置项"""

        self.IMAGE.setdefault('imageActionName ', 'uploadimage')
        self.SCRAWL.setdefault("scrawlActionName", "uploadscrawl")
        self.SNAPSCREEN.setdefault("snapscreenActionName", "uploadimage")
        self.CATCHER.setdefault("catcherActionName", "catchimage")
        self.VIDEO.setdefault("videoActionName", "uploadvideo")
        self.FILE.setdefault("fileActionName", "uploadfile")
        self.IMAGE_MANAGER.setdefault("imageManagerActionName", "listimages")
        self.FILE_MANAGER.setdefault("fileManagerActionName", "listfiles")
        self.IMAGE_REMOVE.setdefault("imageRemoveActionName", "removeimage")
        self.FILE_REMOVE.setdefault("fileRemoveActionName", "removefile")
        self.SAVE_TO_SERVER.setdefault("saveToServerActionName", "savecontent")

        to_be_add_items = [self.IMAGE, self.SCRAWL, self.SNAPSCREEN, self.CATCHER]
        to_be_add_items.append(self.VIDEO)
        to_be_add_items.append(self.FILE)
        to_be_add_items.append(self.IMAGE_MANAGER)
        to_be_add_items.append(self.FILE_MANAGER)
        to_be_add_items.append(self.IMAGE_REMOVE)
        to_be_add_items.append(self.FILE_REMOVE)
        to_be_add_items.append(self.SAVE_TO_SERVER)
        items = {}
        for item in to_be_add_items:
            items.update(item)
        return items

    @property
    def action_views(self):
        """配置对应action的视图函数"""

        views = {
            'config': self.get_ueditor_config,
            self.upload_config.get('imageActionName '): self.general_upload_file,
            self.upload_config.get("scrawlActionName"): self.upload_scrawl,
            self.upload_config.get("snapscreenActionName"): self.general_upload_file,
            self.upload_config.get("catcherActionName"): self.catch_remote_image,
            self.upload_config.get("videoActionName"): self.general_upload_file,
            self.upload_config.get("fileActionName"): self.general_upload_file,
            self.upload_config.get("imageManagerActionName"): self.list_images,
            self.upload_config.get("fileManagerActionName"): self.list_files,
            self.upload_config.get("imageRemoveActionName"): self.remove_file,
            self.upload_config.get("fileRemoveActionName"): self.remove_file,
            self.upload_config.get("saveToServerActionName"): self.save_ueditor_content,
        }
        return views

    @staticmethod
    def to_json(obj):
        return json.dumps(obj, ensure_ascii=False)

    @staticmethod
    def get_or_post(request):

        if request.method.upper() == 'get'.upper():
            return request.GET
        elif request.method.upper() == 'post'.upper():
            parts = request.get_full_path().split('?')
            query_params = parts[1] if len(parts) > 1 else ''
            qd = QueryDict(query_params)
            final_post = request.POST.copy()
            final_post.update(qd.copy())
            return final_post
        else:
            return None

    def call_action_views(self, request, *args, **kwargs):

        params = self.get_or_post(request)
        action = params.get('action')
        action_view = self.action_views.get(action, None)
        if action_view:
            return action_view(request, *args, **kwargs)
        else:
            message = '没有为{}配置对应的视图函数'.format(action)
            return HttpResponseBadRequest(message)

    def create_full_file_path(self, subpath):
        """构建ueditor上传存放目录

        Args:
            subpath: 子路径

        """
        return os.path.join(self.ROOT_DIR, subpath)

    def get_ueditor_config(self, request, *args, **kwargs):
        """返回配置"""

        return HttpResponse(self.to_json(self.upload_config), content_type="application/javascript")

    def get(self, request, *args, **kwargs):
        """获取ueditor的后端URL地址"""

        return self.call_action_views(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        """获取ueditor的后端URL地址"""

        return self.call_action_views(request, *args, **kwargs)

    def list_images(self, request, *args, **kwargs):

        params = self.get_or_post(request)
        allow_types_key = 'imageManagerAllowFiles'
        list_size_key = 'imageManagerListSize'
        list_path_key = 'imageManagerListPath'
        list_size = int(params.get('size', self.upload_config.get(list_size_key, 7)))
        list_start = int(params.get("start", 0))
        listpath = self.upload_config.get(list_path_key, '')
        allow_types = self.upload_config.get(allow_types_key, '')

        subdirs = self._project_flow_node_dirname(params.get('project_id', ''), params.get('flow_pk', ''), params.get('node_id', ''))
        files = self.get_url_files(listpath, allow_types, listpath_dirs=subdirs)
        if (len(files) == 0):
            return_info = {"state": "未找到匹配文件!", "list": [], "start": list_start, "total": 0}
        else:
            return_info = {"state": "SUCCESS", "list": files[list_start:list_start + list_size], "start": list_start, "total": len(files)}
        print(json.dumps(return_info))
        return HttpResponse(json.dumps(return_info), content_type="application/javascript")

    def remove_file(self, request, *args, **kwargs):

        state = True
        msg = ''
        params = self.get_or_post(request)
        subdirs = self._project_flow_node_dirname(params.get('project_id', ''), params.get('flow_pk', ''), params.get('node_id', ''))
        suburlpath = unquote(params.get('path', ''))
        parts = suburlpath.split('/')
        root_dir = self.SERVER_URL['root_dir']

        if parts:
            fpath = subdirs
            fpath.append(parts[-1])
            filepath = os.path.join(root_dir, *fpath)
            try:
                if os.path.isfile(filepath):
                    os.remove(filepath)
            except Exception as e:
                msg = str(e)
                state = False
        res = dict(state=state, msg=msg)
        dumps_params = dict(ensure_ascii=False)
        return JsonResponse(res, json_dumps_params=dumps_params)

    def list_files(self, request, *args, **kwargs):
        """列出文件"""

        params = self.get_or_post(request)
        allow_types_key = 'fileManagerAllowFiles'
        list_size_key = 'fileManagerListSize'
        list_path_key = 'fileManagerListPath'
        list_size = int(params.get('size', self.upload_config.get(list_size_key, 7)))
        list_start = int(params.get("start", 0))
        listpath = self.upload_config.get(list_path_key, '')
        allow_types = self.upload_config.get(allow_types_key, '')
        subdirs = self._project_flow_node_dirname(params.get('project_id', ''), params.get('flow_pk', ''), params.get('node_id', ''))
        files = self.get_url_files(listpath, allow_types, listpath_dirs=subdirs)
        if (len(files) == 0):
            return_info = {"state": "未找到匹配文件!", "list": [], "start": list_start, "total": 0}
        else:
            return_info = {"state": "SUCCESS", "list": files[list_start:list_start + list_size], "start": list_start, "total": len(files)}
        return HttpResponse(json.dumps(return_info), content_type="application/javascript")

    def get_url_files(self, listpath, allow_types=[], listpath_dirs=[]):

        urlsep = '/'
        listpath_dirs.extend(listpath.split(urlsep))
        dirlist = listpath_dirs
        url_files = []
        root_dir = self.SERVER_URL['root_dir']
        rd_parts = root_dir.split(os.sep)
        plength = len(rd_parts)
        root_url = self.SERVER_URL['value']
        if dirlist:
            dirpath = os.path.join(root_dir, os.sep.join(dirlist))
            items = os.listdir(dirpath)
            for item in items:
                if os.path.isfile(os.path.join(dirpath, item)):
                    name, ext = os.path.splitext(item)
                    urlparts = dirpath.split(os.sep)
                    urlparts.append(item)
                    urlparts = urlparts[plength:]
                    urlparts.insert(0, root_url)
                    url = quote(urlsep.join(urlparts))
                    url = url if url.startswith(urlsep) else (urlsep + url)
                    if allow_types:
                        if ext in allow_types:
                            url_files.append({"url": url})
                    else:
                        url_files.append({"url": url})
        return url_files

    def catch_remote_image(self, request, *args, **kwargs):
        """远程抓图,当catchRemoteImageEnable:true时,如果前端插入图片地址与当前web不在同一个域,则由本函数从远程下载图片到本地
        """

        state = "SUCCESS"
        path_format_key = 'catcherPathFormat'
        params = self.get_or_post(request)
        allow_type = list(params.get("catcherAllowFiles", self.upload_config.get("catcherAllowFiles", "")))
        # max_size = int(params.get("catcherMaxSize", self.upload_config.get("catcherMaxSize", 0)))
        remote_urls = params.getlist(self.upload_config['catcherFieldName'], [])
        catcher_infos = []

        for remote_url in remote_urls:

            # 取得上传的文件的原始名称
            remote_file_name = os.path.basename(remote_url)
            remote_original_name, remote_original_ext = os.path.splitext(remote_file_name)

            # 文件类型检验
            if remote_original_ext in allow_type:
                path_format = params.get(path_format_key, self.upload_config.get(path_format_key, ''))
                formatter = UeditorPathFormatter()
                formatter.format(remote_original_name, path_format)
                subdirectories = self._project_flow_node_dirname(params.get('project_id', ''), params.get('flow_pk', ''), params.get('node_id', ''))
                formatter.set_url_path_prefix(*subdirectories)  # 项目 流程 节点 构成的路径
                full_file_path = self.create_full_file_path(formatter.save_subpath)
                self._create_directory(full_file_path)

                # 读取远程图片文件
                try:
                    remote_image = urlopen(remote_url)
                    # 将抓取到的文件写入文件
                    try:
                        f = open(full_file_path, 'wb')
                        f.write(remote_image.read())
                        f.close()
                        state = "SUCCESS"
                    except Exception as E:
                        state = u"写入抓取图片文件错误:%s" % E.message
                except Exception as E:
                    state = u"抓取图片错误:%s" % E.message
                formatter.set_url_path_prefix(self.SERVER_URL['value'])  # 添加根路径
                catcher_infos.append({
                    "state": state,
                    "url": quote(formatter.url_path),
                    "size": os.path.getsize(full_file_path),
                    "title": os.path.basename(full_file_path),
                    "original": remote_file_name,
                    "source": remote_url
                })

        return_info = {"state": "SUCCESS" if len(catcher_infos) > 0 else "ERROR", "list": catcher_infos}
        return HttpResponse(json.dumps(return_info, ensure_ascii=False), content_type="application/javascript")

    @classmethod
    def get_db_object(cls, model_klass, **db_fields):

        obj = None
        try:
            obj = model_klass.objects.get(**db_fields)
        except ObjectDoesNotExist:
            obj = None
        return obj

    def _project_flow_node_dirname(self, project_id=None, flow_id=None, node_id=None, sep='_'):

        dirname_list = []
        if node_id:
            node = self.get_db_object(models.Node, id=node_id)
            dirname_list.append(sep.join([str(node.flow.project.id), node.flow.project.name]))
            dirname_list.append(sep.join([node.flow.number]))
            dirname_list.append(sep.join([node.name]))
        elif flow_id:
            flow = self.get_db_object(models.Flow, id=flow_id)
            dirname_list.append(sep.join([str(flow.project.id), flow.project.name]))
            dirname_list.append(sep.join([flow.number]))
        elif project_id:
            project = self.get_db_object(models.Project, id=project_id)
            dirname_list.append(sep.join([project_id, project.name]))
        else:
            pass
        return dirname_list

    def general_upload_file(self, request, *args, **kwargs):
        """上传文件"""

        state = "SUCCESS"
        params = self.get_or_post(request)
        action = params.get('action')
        if action == self.upload_config.get('imageActionName'):
            submit_field_key = 'imageFieldName'
            max_size_field = 'imageMaxSize'
            ext_field = 'imageAllowFiles'
            path_format_key = 'imagePathFormat'
        elif action == self.upload_config.get('fileActionName'):
            submit_field_key = 'fileFieldName'
            max_size_field = 'fileMaxSize'
            ext_field = 'fileAllowFiles'
            path_format_key = 'filePathFormat'
        elif action == self.upload_config.get('videoActionName'):
            submit_field_key = 'videoFieldName'
            max_size_field = 'videoMaxSize'
            ext_field = 'videoAllowFiles'
            path_format_key = 'videoPathFormat'
        else:
            return JsonResponse({'state': 'action传值错误'})

        submit_field_name = params.get(submit_field_key, self.upload_config.get(submit_field_key, 'upfile'))
        submit_file = request.FILES.get(submit_field_name, None)
        if submit_file is None:
            return JsonResponse({'state': '未上传任何文件'})
        submit_file_name = submit_file.name
        submit_file_size = submit_file.size
        submit_file_ext = os.path.splitext(submit_file_name)[1]
        support_file_exts = list(params.get(ext_field, self.upload_config.get(ext_field, "")))
        if submit_file_ext not in support_file_exts:
            state = "服务器只支持以下类型的文件:{},实际上传的是:{}".format(' | '.join(support_file_exts), submit_file_name)
            return JsonResponse({'state': state})
        limit_size = int(params.get(max_size_field, self.upload_config.get(max_size_field, self.DEFAULT_MAX_SIZE)))
        asize = FileSize(submit_file_size)
        lsize = FileSize(limit_size)
        if asize > lsize:
            state = "上传文件大小({})已超过最大限制({})%s。".format(asize.human_readable, lsize.human_readable)
            return JsonResponse({'state': state})
        path_format = params.get(path_format_key, self.upload_config.get(path_format_key, ''))
        formatter = UeditorPathFormatter()
        formatter.format(submit_file_name, path_format)
        subdirectories = self._project_flow_node_dirname(params.get('project_id', ''), params.get('flow_pk', ''), params.get('node_id', ''))
        formatter.set_url_path_prefix(*subdirectories)  # 项目 流程 节点 构成的路径
        full_file_path = self.create_full_file_path(formatter.save_subpath)
        try:
            self._create_directory(full_file_path)
            with open(full_file_path, 'wb') as f:
                for chunk in submit_file.chunks():
                    f.write(chunk)
        except Exception as e:
            state = "保存文件{}错误: {}".format(submit_file_name, str(e))
        formatter.set_url_path_prefix(self.SERVER_URL['value'])  # 添加根路径
        res = dict(state=state, url=quote(formatter.url_path), title=submit_file_name, original=submit_file_name)
        dumps_params = dict(ensure_ascii=False)
        return JsonResponse(res, json_dumps_params=dumps_params)

    def _create_directory(self, path):

        target_dir = os.path.dirname(path)
        if not os.path.exists(target_dir):
            os.makedirs(target_dir)

    def upload_scrawl(self, request, *args, **kwargs):
        """上传涂鸦"""

        state = "SUCCESS"
        sfn = 'scrawlFieldName'
        max_size_field = 'scrawlMaxSize'
        scrawl_file_name = kwargs.get('scrawl_file_name', '涂鸦.png')
        params = self.get_or_post(request)
        field_name = params.get(sfn, self.upload_config.get(sfn, 'upfile'))
        base64_content = params.get(field_name)
        content = base64.b64decode(base64_content)
        actual_size = len(content)
        limit_size = int(params.get(max_size_field, self.upload_config.get(max_size_field, 0)))
        asize = FileSize(actual_size)
        lsize = FileSize(limit_size)
        if asize > lsize:
            state = "上传文件大小({})已超过最大限制({})%s。".format(asize.human_readable, lsize.human_readable)
            return JsonResponse({'state': state})
        path_format = params.get('scrawlPathFormat', self.upload_config.get('scrawlPathFormat', ''))
        formatter = UeditorPathFormatter()
        formatter.format(scrawl_file_name, path_format)
        subdirectories = self._project_flow_node_dirname(params.get('project_id', ''), params.get('flow_pk', ''), params.get('node_id', ''))
        formatter.set_url_path_prefix(*subdirectories)  # 项目 流程 节点 构成的路径
        full_file_path = self.create_full_file_path(formatter.save_subpath)
        try:
            self._create_directory(full_file_path)
            with open(full_file_path, 'wb') as f:
                f.write(content)
        except Exception as e:
            state = "写入图片文件错误: {}".format(str(e))
        formatter.set_url_path_prefix(self.SERVER_URL['value'])  # 添加根路径
        res = dict(state=state, url=quote(formatter.url_path), title=scrawl_file_name, original=scrawl_file_name)
        dumps_params = dict(ensure_ascii=False)
        return JsonResponse(res, json_dumps_params=dumps_params)

    def save_ueditor_content(self, request, *args, **kwargs):

        state = True
        msg = ""
        submit_datas = self.get_or_post(request)
        project_id = submit_datas.get('project_id', '')
        flow_id = submit_datas.get('flow_pk', '')
        node_id = submit_datas.get('node_id', '')
        ueditor_content = submit_datas.get("ueditor_content")
        node = self.get_db_object(models.Node, id=node_id)
        if node:
            if node.is_review_kind or node.is_env_kind or node.is_test_kind:
                r = self.get_db_object(models.Result, node=node, batch=node.batch)
                if r:
                    r.doc = ueditor_content
                    r.save()
                else:
                    code, name = (-1, "还没有结果")
                    nr = models.Result(code=code, name=name, node=node, batch=node.batch, creator=request.user, doc=ueditor_content)
                    nr.save()
            else:
                state = False
                msg = "节点({})不支持".format(node.name)
        else:
            flow = self.get_db_object(models.Flow, id=flow_id)
            if flow:
                flow.doc = ueditor_content
                flow.save()
            else:
                project = self.get_db_object(models.Project, id=project_id)
                if project:
                    project.doc = ueditor_content
                    project.save()
                else:
                    state = False
                    # 项目 流程 节点 任意一个都找不到,不知道保存到哪
                    msg = "error code: p{}f{}n{}".format(project_id, flow_id, node_id)
        res = dict(state=state, msg=msg)
        dumps_params = dict(ensure_ascii=False)
        return JsonResponse(res, json_dumps_params=dumps_params)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410
  • 411
  • 412
  • 413
  • 414
  • 415
  • 416
  • 417
  • 418
  • 419
  • 420
  • 421
  • 422
  • 423
  • 424
  • 425
  • 426
  • 427
  • 428
  • 429
  • 430
  • 431
  • 432
  • 433
  • 434
  • 435
  • 436
  • 437
  • 438
  • 439
  • 440
  • 441
  • 442
  • 443
  • 444
  • 445
  • 446
  • 447
  • 448
  • 449
  • 450
  • 451
  • 452
  • 453
  • 454
  • 455
  • 456
  • 457
  • 458
  • 459
  • 460
  • 461
  • 462
  • 463
  • 464
  • 465
  • 466
  • 467
  • 468
  • 469
  • 470
  • 471
  • 472
  • 473
  • 474
  • 475
  • 476
  • 477
  • 478
  • 479
  • 480
  • 481
  • 482
  • 483
  • 484
  • 485
  • 486
  • 487
  • 488
  • 489
  • 490
  • 491
  • 492
  • 493
  • 494
  • 495
  • 496
  • 497
  • 498
  • 499
  • 500
  • 501
  • 502
  • 503
  • 504
  • 505
  • 506
  • 507
  • 508
  • 509
  • 510
  • 511
  • 512
  • 513
  • 514
  • 515
  • 516
  • 517
  • 518
  • 519
  • 520
  • 521
  • 522
  • 523
  • 524
  • 525
  • 526
  • 527
  • 528
  • 529
  • 530
  • 531
  • 532
  • 533
  • 534
  • 535
  • 536
  • 537
  • 538
  • 539
  • 540
  • 541
  • 542
  • 543
  • 544
  • 545
  • 提供ueditor获取静态文件的服务视图(ueditor_fileserver.py)
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@Author: 思文伟
@Date: 2021/03/07 14:47:32
'''

from django.views import View
from django.conf import settings
from django.views import static
from django.http import HttpResponseRedirect
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt


@method_decorator(csrf_exempt, name='dispatch')
class Ueditor_FileServer(View):

    suburl_group_name = 'suburl'
    suburl = '(?P<{}>.+)'.format(suburl_group_name)
    redirect_url = "/login"

    @property
    def ueditor_file_dir(self):

        return settings.UEDITOR_FILE_DIR

    def download(self, request, *args, **kwargs):

        if not request.user.is_authenticated:
            return HttpResponseRedirect(self.redirect_url)
        sub_url = kwargs.get(self.suburl_group_name)
        document_root = self.ueditor_file_dir
        path = sub_url
        res = static.serve(request, path, document_root=document_root)
        return res

    def get(self, request, *args, **kwargs):

        return self.download(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):

        return self.download(request, *args, **kwargs)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 处理文件大小(filesize.py)
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@Author: 思文伟
@Date: 2021/03/07 14:48:18
'''


class FileSize():
    SIZE_UNIT = {"Byte": 1, "KB": 1024, "MB": 1048576, "GB": 1073741824, "TB": 1099511627776}

    def __init__(self, size):
        self.size = int(FileSize.Format(size))

    @staticmethod
    def Format(size):
        import re
        if isinstance(size, int):
            return size
        else:
            if not isinstance(size, str):
                return 0
            else:
                oSize = size.lstrip().upper().replace(" ", "")
                pattern = re.compile(r"(\d*\.?(?=\d)\d*)(byte|kb|mb|gb|tb)", re.I)
                match = pattern.match(oSize)
                if match:
                    m_size, m_unit = match.groups()
                    if m_size.find(".") == -1:
                        m_size = int(m_size)
                    else:
                        m_size = float(m_size)
                    if m_unit != "BYTE":
                        return m_size * FileSize.SIZE_UNIT[m_unit]
                    else:
                        return m_size
                else:
                    return 0

    # 返回字节为单位的值
    @property
    def size(self):
        return self._size

    @size.setter
    def size(self, newsize):
        try:
            self._size = int(newsize)
        except Exception:
            self._size = 0

    # 返回带单位的自动值
    @property
    def human_readable(self):
        if self.size < FileSize.SIZE_UNIT["KB"]:
            unit = "Byte"
        elif self.size < FileSize.SIZE_UNIT["MB"]:
            unit = "KB"
        elif self.size < FileSize.SIZE_UNIT["GB"]:
            unit = "MB"
        elif self.size < FileSize.SIZE_UNIT["TB"]:
            unit = "GB"
        else:
            unit = "TB"

        if (self.size % FileSize.SIZE_UNIT[unit]) == 0:
            return "%s%s" % ((self.size / FileSize.SIZE_UNIT[unit]), unit)
        else:
            return "%0.2f%s" % (round(float(self.size) / float(FileSize.SIZE_UNIT[unit]), 2), unit)

    def __str__(self):
        return self.human_readable

    # 相加
    def __add__(self, other):
        if isinstance(other, FileSize):
            return FileSize(other.size + self.size)
        else:
            return FileSize(FileSize(other).size + self.size)

    def __sub__(self, other):
        if isinstance(other, FileSize):
            return FileSize(self.size - other.size)
        else:
            return FileSize(self.size - FileSize(other).size)

    def __gt__(self, other):
        if isinstance(other, FileSize):
            if self.size > other.size:
                return True
            else:
                return False
        else:
            if self.size > FileSize(other).size:
                return True
            else:
                return False

    def __lt__(self, other):
        if isinstance(other, FileSize):
            if other.size > self.size:
                return True
            else:
                return False
        else:
            if FileSize(other).size > self.size:
                return True
            else:
                return False

    def __ge__(self, other):
        if isinstance(other, FileSize):
            if self.size >= other.size:
                return True
            else:
                return False
        else:
            if self.size >= FileSize(other).size:
                return True
            else:
                return False

    def __le__(self, other):
        if isinstance(other, FileSize):
            if other.size >= self.size:
                return True
            else:
                return False
        else:
            if FileSize(other).size >= self.size:
                return True
            else:
                return False

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 处理ueditor中的PathFormat相关项(ueditor_path_formatter.py)
    在这里插入图片描述
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@Author: 思文伟
@Date: 2021/03/07 14:39:03
'''

import re
import os
import random
import datetime


class UeditorPathFormatter(object):
    def __init__(self, url_path_sep='/'):

        self.url_path_sep = url_path_sep
        self.url_path_prefix = None
        self.url_path = None

    def _replace(self, file_name, rand_prefix='rand:', dtime=datetime.datetime.now()):
        def wrapper(match_obj):

            ms = match_obj.group(0)
            if ms == '{filename}':
                repl = file_name
            elif ms == '{time}':
                repl = str(int(dtime.timestamp()))
            elif ms == '{yyyy}':
                repl = dtime.strftime('%Y')
            elif ms == '{yy}':
                repl = dtime.strftime('%y')
            elif ms == '{mm}':
                repl = dtime.strftime('%m')
            elif ms == '{dd}':
                repl = dtime.strftime('%d')
            elif ms == '{hh}':
                repl = dtime.strftime('%H')
            elif ms == '{ii}':
                repl = dtime.strftime('%M')
            elif ms == '{ss}':  # datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f') # 含微秒的日期时间
                repl = dtime.strftime('%S')
            else:
                ms_content = ms.lstrip("{").rstrip("}")
                if ms_content.startswith(rand_prefix):
                    length = int(ms_content[len(rand_prefix):])
                    number = random.randint(1, int('9' * length))
                    fmt = '{:>0%sd}' % length
                    repl = fmt.format(number)
                else:
                    repl = ms
            return repl

        return wrapper

    def _check_path_format(self, path_format):

        pass

    def _check_file_name(self, file_name):

        pass

    def format(self, file_name, path_format, endswith_filename=True):
        """

        # {filename}  # 会替换成文件名 [要注意中文文件乱码问题]
        # {rand:6}    # 会替换成随机数,后面的数字是随机数的位数
        # {time}      # 会替换成时间戳
        # {yyyy}      # 会替换成四位年份
        # {yy}        # 会替换成两位年份
        # {mm}        # 会替换成两位月份
        # {dd}        # 会替换成两位日期
        # {hh}        # 会替换成两位小时
        # {ii}        # 会替换成两位分钟
        # {ss}        # 会替换成两位秒

        Args:
            file_name: 文件名(含拓展名) eg: index.html
            path_format: ueditor 相关项的PathFormat 参见 http://fex.baidu.com/ueditor/#server-path
            endswith_filename: 控制path_format 不是以文件名结尾的话是否自动附加文件名结尾, True - 附加
        """

        self.reset()
        parts = [
            "\\{filename\\}",
            "\\{rand:\\d+\\}",
            "\\{time\\}",
            "\\{yyyy\\}",
            "\\{yy\\}",
            "\\{mm\\}",
            "\\{dd\\}",
            "\\{hh\\}",
            "\\{ii\\}",
            "\\{ss\\}",
        ]
        file_name_fmt = '{filename}'
        regex = '(' + '|'.join(parts) + ')'
        matcher = re.compile(regex)
        if endswith_filename and not path_format.endswith(file_name_fmt):
            path_format = '/'.join([path_format, file_name_fmt])
        self.url_path = matcher.sub(self._replace(file_name), path_format)
        return self.url_path

    @property
    def save_subpath(self):

        try:
            p = self.url_path
        except AttributeError:
            raise AttributeError('Please call format method before calling save_subpath.')
        parts = p.split(self.url_path_sep)
        return os.path.join('', *parts)

    def set_url_path_prefix(self, *directories):

        self.url_path_prefix = self.url_path_sep.join(directories)
        if not self.url_path_prefix.startswith(self.url_path_sep):
            self.url_path_prefix = self.url_path_sep + self.url_path_prefix
        if self.url_path.startswith(self.url_path_sep):
            self.url_path = self.url_path_prefix + self.url_path
        else:
            self.url_path = self.url_path_prefix + self.url_path.lstrip(self.url_path_sep)

    def reset(self):

        self.url_path_prefix = None
        self.url_path = None


if __name__ == '__main__':
    formatter = UeditorPathFormatter()
    formatter.format('index.html', "/ueditor/php/upload/image/{yyyy}{mm}{dd}/{time}/{rand:11}/{filename}")
    print(formatter.url_path)
    print(formatter.save_subpath)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
体验地址

http://116.63.153.101:8005/
在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/498405
推荐阅读
相关标签
  

闽ICP备14008679号