React 编写网页聊天界面(仿钉钉)

news2024/11/14 6:06:01

效果图

在这里插入图片描述
在这里插入图片描述

文件结构

在这里插入图片描述

对话框

一、 难点

对话框的难点主要在样式上

  1. 双方对话分布在左右
  2. 长对话的长度不能超过整个对话框宽度的一半
  3. 图片的大小最大不能超过整个对话框宽度的一半,并且需要按比例进行收缩
二、与要引入的插件

1、 阿里巴巴的iconfont 可以去这篇博客里面看是怎么使用的:https://blog.csdn.net/qq_45634989/article/details/127851878

三、对话框代码

组件代码 Dialogue.jsx

import React, { Component } from 'react'
import { Row, Col } from 'antd';//蚂蚁金服 Ant Design组件库
import { createFromIconfontCN } from '@ant-design/icons';//创建IconFont 对象需要的方法
import { ALIICONURL } from '../../../../../utils/constant';//个人阿里巴巴图库链接 因为随时会变,所以把他作为一个常量引入了
import { getToken } from '../../../../../utils/request/auth';//自定义的获取token值的方法,如果不需要判断可以不引入
import './Dialogue.css'//引入的css样式,下文中有

export default class Dialogue extends Component {
    IconFont = createFromIconfontCN({
        scriptUrl: ALIICONURL,
    });//创建IconFont 对象
    componentDidMount() {
    //这个方法是为了页面初始化的时候,用户看到的都是最后几条条消息
        if (this.messagesEnd) {
            const scrollHeight = this.messagesEnd.scrollHeight;//里面div的实际高度 
            const height = this.messagesEnd.clientHeight;  //网页可见高度  
            const maxScrollTop = scrollHeight - height;
            this.messagesEnd.scrollTop = maxScrollTop > 0 ? maxScrollTop : 0;
        }
    }
    componentDidUpdate() {
    //这个方法是为了页面更新的时候,用户看到的都是最后几条条消息
        if (this.messagesEnd) {
            const scrollHeight = this.messagesEnd.scrollHeight;//里面div的实际高度 
            const height = this.messagesEnd.clientHeight;  //网页可见高度 
            const maxScrollTop = scrollHeight - height;
            this.messagesEnd.scrollTop = maxScrollTop > 0 ? maxScrollTop : 0;
        }
    }
    //点击聊天中的图片时,会在新窗口看到放大的图片,而不是缩略形式
    showImg = (src) => {
        return () => {
            const img = new window.Image();
            img.src = src;
            const newWin = window.open('');
            newWin.document.write(img.outerHTML);
            newWin.document.title = '图片展示'
            newWin.document.close();
        }
    }
    /**
    采用的思路:
    1、使用Grid栅格来保证双方聊天记录左右分布
    2、使用position(父组件PersonMessage.jsx传过来)这个变量来控制聊天记录显示的位置时左还是右
    **/
    render() {
        let message = this.props
        message = Object.values(message)
        const name = getToken('nickName')
        return (
            <div className='dialogue_all' ref={(el) => { this.messagesEnd = el; }} >
                {
                    message.map((m) => {
                        return <div key={m.id} style={{ width: '100%', 'padding': "20px 0px 20px 0px" }}>
                            <Row>
                                <Col span={12} style={{ visibility: m.position === 'left' ? 'visible' : 'hidden', width: '100%' }}>
                                    <div className='dialogue_div1'>
                                        <span className='dialogue_icon1' style={{ backgroundColor: m.color }}>
                                            <this.IconFont type={m.icon} className='messageItem_icon' />
                                        </span>
                                    </div>
                                    <div className='dialogue_div3'>
                                        <span style={{ margin: '9px 10px 9px 10px', display: 'block' }}>
                                            {
                                                m.info.map((content, i) => {
                                                    return (
                                                        <span key={i}>
                                                            {content.insert.image ? <img onClick={this.showImg(content.insert.image)} src={content.insert.image} style={{ objectFit: 'contain', maxHeight: '100%', maxWidth: '100%' }} alt="" /> : <span>{content.insert}</span>}
                                                        </span>
                                                    )
                                                })
                                            }
                                        </span>
                                    </div>
                                </Col>
                                <Col span={12} style={{ textAlign: 'right', visibility: m.position === 'right' ? 'visible' : 'hidden', width: '100%' }} >
                                    <div className='dialogue_div2'>
                                        <span style={{ margin: '9px 10px 9px 10px', display: 'block', textAlign: 'left' }}>
                                            {
                                                m.info.map((content, i) => {
                                                    return (
                                                        <span key={i}>
                                                            {content.insert.image ? <img onClick={this.showImg(content.insert.image)} src={content.insert.image} style={{ objectFit: 'contain', maxHeight: '100%', maxWidth: '100%' }} alt="" /> : <span>{content.insert}</span>}
                                                        </span>
                                                    )
                                                })
                                            }
                                        </span>
                                    </div>
                                    <div className='dialogue_div1'>
                                        <span className='dialogue_icon2' style={{ backgroundColor: '#007FE1' }}>
                                            <span style={{ fontSize: '16px', color: '#ffffff' }}>{name}</span>
                                        </span>
                                    </div>
                                </Col>
                            </Row>
                        </div>
                    })
                }
            </div>
        )
    }
}

聊天框样式

.dialogue_all{
    max-height: 100%;
    height:100%;
    width: 100%;
    overflow-y:scroll;
    white-space: pre-wrap;
}
.dialogue_icon1 {
    font-size: 20px;
    padding: 7px 10px 7px 10px;
    margin: 0 4px 4px 0;
    border-radius: 10px;
    position: relative;
    top: 0.3rem;
}
.dialogue_icon2 {
    font-size: 20px;
    padding: 0px 10px 7px 10px;
    margin: 0 4px 4px 0;
    border-radius: 10px;
    position: relative;
    top: -0.1rem;
}
.dialogue_div1{
    display: inline-block;
    max-width: 20%;
}
.dialogue_div2{
    display: inline-block;
    background-color: rgb(201, 231, 255);
    max-width: 80%;
    vertical-align: top;
    border-radius: 5px;
    margin-right: 10px;
}
.dialogue_div3{
    display: inline-block;
    background-color: rgb(255, 255, 255);
    max-width: 80%;
    vertical-align: top;
    border-radius: 5px;
    margin-left: 10px;
}

输入框+所有组件

难点

1. 输入框能粘贴图片
对于聊天中的输入框,要求是可以输入一定数量的文字,并且能粘贴图片

  • 第一时间就想到了textArea,但是textArea不能粘贴图片,pass
  • 后面又想到div的可编辑模式,可以模拟输入框,但是后面尝试发现,这种办法的光标太难控制了(需要使用浏览器原生的操作光标的办法,比较难控制),pass
  • 最后一个解决办法就是,引入富文本编辑器,将富文本编辑器的上半部分隐藏掉,就可以模拟成一个聊天系统输入框了
<div contenteditable="true">这里可以编辑</div>

2. 回车键发送,CTRL+回车键换行

  • 使用onKeyUp键盘方法,判断keyCode是否为13+ctrlKey,是的话在文字后面+‘\n’
    3. 在文字中间插入表情,光标在插入表情之后
    使用富文本插件所带的getSelection方法获取光标位置,然后在光标位置插入表情
    4. 回车不会产生\n
    在插件的modules属性中插入这段代码,不能使用event.preventDefault(),无效
keyboard: {
            bindings: {
                enter: {
                    key: 13,
                    handler: (range, context) => {
                        console.log('enter');
                        let ops = this.reactQuillRef.getEditor().getContents().ops
                        console.log(ops)
                        this.setState({ sendMessage: ops }, () => {
                            this.addMessage();//光标放在中间会吞掉后面一个字
                        })
                    }
                },
            }
        }
所使用到的插件

1、emoji-mart 表情插件

yarn add @emoji-mart/data

npm install @emoji-mart/data

2、react-quill 富文本编辑器插件

地址:https://github.com/zenoamaro/react-quill

npm install react-quill --save

3、quill-image-drop-module 可以将图片拖拽复制到输入框

地址:https://github.com/kensnyder/quill-image-drop-module

npm install quill-image-drop-module -S

全部代码

组件代码 PersonMessage.jsx

import React, { Component } from 'react'
import { Divider, Button, message } from 'antd' //antd Degin 组件库
import ReactQuill, { Quill } from 'react-quill'; //富文本编辑器
import { ImageDrop } from 'quill-image-drop-module'; //拖拽图片可复制进编辑器
import 'react-quill/dist/quill.snow.css';//富文本编辑器的主题样式
import PersonMessageHeader from './PersonMessageHeader/PersonMessageHeader'//聊天框的头部组件 后面有粘贴(其他组件部分)
import BottomIcon from './BottomIcon/BottomIcon' // 表情选择等图标组件,后面有粘贴(其他组件部分)
import Dialogue from './Dialogue/Dialogue'//聊天界面组件,上文中有提及
import { getToken } from '../../../../utils/request/auth'//获取token值,不需要判断可以不写
import './PersonMessage.css'//css样式
Quill.register('modules/imageDrop', ImageDrop);//使富文本编辑器有拖拽图片的功能

class PersonMessage extends Component {
    state = {
        myMessage: [],//展示在聊天界面的所有记录
        lastEditIndex: '',//光标停留在输入框中的最后位置
        value: '',//聊天框中的内容
        sendMessage: ''//需要发送的那些消息
    }
    modules = {//富文本编辑器的属性
        toolbar: [
            ['bold', 'italic', 'underline', 'strike', 'blockquote'],
            [{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }],
            ['link', 'image'],
            ['clean'],
        ],
        imageDrop: true,
        keyboard: {
            bindings: {
                enter: {
                    key: 13,
                    handler: (range, context) => {
                        console.log('enter');
                        let ops = this.reactQuillRef.getEditor().getContents().ops
                        console.log(ops)
                        this.setState({ sendMessage: ops }, () => {
                            this.addMessage();//光标放在中间会吞掉后面一个字
                        })
                    }
                },
            }
        }
    }
    formats = ['bold', 'italic', 'underline', 'strike', 'blockquote', 'list', 'bullet', 'indent', 'link', 'image',]//富文本编辑器的属性
    static getDerivedStateFromProps(nextProps, nextState) {//preState
        //TODO 此处有个根据id查询消息的请求
        // console.log('nextProps',nextProps.location.state)//id,messageIcon
        const { messageIcon, color } = nextProps.location.state
        //TODO 此处有个从sessionStorage中查出个人图标的请求 getToken('icon') 暂时定为名字
        let myState = nextState.myMessage
        myState.map((s) => {
            return (s.color = color, s.icon = messageIcon)
        })
        const myIcon = getToken('nickName')
        //假数据,如果是真实数据的话,可以在TODO那里请求
        //id 聊天记录的id
        //color:聊天记录的颜色
        //icon:这个是双方的头像,我用ixon来表示了,可以换成图片
        //position:聊天记录的位置,在左边还是右边
        //info:聊天内容 如果是图片,会在里面再包一层image(这个是根据quill的本身属性来的)
        let m = [
            { id: 1, color: color, icon: messageIcon, position: 'left', info: [{ insert: "你好呀😍" }, { insert: { image: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Flmg.jj20.com%2Fup%2Fallimg%2F1114%2F041621122252%2F210416122252-1-1200.jpg&refer=http%3A%2F%2Flmg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1671784849&t=891f79c11335b0717366ef0105852e68' } }] },
            { id: 2, color: color, icon: myIcon, position: 'right', info: [{ insert: "你好呀🌹" }] },
            { id: 3, color: color, icon: messageIcon, position: 'left', info: [{ insert: "你叫什么名字" }] },
            { id: 4, color: color, icon: myIcon, position: 'right', info: [{ insert: "我叫Mary" }] },
            { id: 5, color: color, icon: messageIcon, position: 'left', info: [{ insert: "我叫jack" }] },
            { id: 6, color: color, icon: myIcon, position: 'right', info: [{ insert: "你是哪里人" }] },
            { id: 7, color: color, icon: messageIcon, position: 'left', info: [{ insert: "我是外国人" }] },
            { id: 8, color: color, icon: myIcon, position: 'right', info: [{ insert: "我也是外国人" }] },
            { id: 9, color: color, icon: messageIcon, position: 'left', info: [{ insert: "好巧" }] },
            { id: 10, color: color, icon: myIcon, position: 'right', info: [{ insert: "好巧" }] },
            { id: 11, color: color, icon: messageIcon, position: 'left', info: [{ insert: "再见" }] },
            { id: 12, color: color, icon: myIcon, position: 'right', info: [{ insert: "好的,我也要走了" }] },
            { id: 13, color: color, icon: messageIcon, position: 'right', info: [{ insert: "下次再聊" }] },
            { id: 14, color: color, icon: myIcon, position: 'right', info: [{ insert: "好的111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" }] },
        ]
        if (myState.length === 0) {
            return {
                myMessage: m
            }
        } else {
            return {
                myMessage: myState
            }
        }
    }
    shouldComponentUpdate(nextProps, nextState) {
        if (this.props.location.state.id === nextProps.location.state.id && nextState.myMessage.length === this.state.myMessage.length) {
            return false
        } else {
            this.setState({ value: '' })
            return true
        }
    }

//选择表情
    pickEmoji = (emoji, event) => {
        const { lastEditIndex } = this.state
        var range = lastEditIndex;
        let position = range ? range.index : 0;
        this.reactQuillRef.getEditor().insertText(position, emoji.native);//插入表情
        this.reactQuillRef.focus()
        this.reactQuillRef.getEditor().setSelection(position + 2);//必须要+2,否则回车键会让表情变乱码
    }
    //将消息加进聊天界面
    addMessage = () => {
        const { myMessage, sendMessage } = this.state
        let value = sendMessage
        let length = value.length
        if (value.length !== 0) {
        //如果是换行就删除
            if (!value[length - 1].insert.image) {
                if (value[length - 1].insert === '\n' || value[length - 1].insert === '\n\n') {
                    console.log(111)
                    value = value.slice(0, -1)
                }
            }
            //将末尾的\n\n或\n删除
            for (let i = 0; i < value.length; i++) {
                if (!value[i].insert.image) {
                    let tt = value[i].insert
                    value[i].insert = tt.slice(0, tt.length - 1)
                }
            }
            if (value.length === 0) {
                message.info('发送内容不能为空,请输入');
            } else {
                let newArr = myMessage
                newArr = newArr.concat({ id: myMessage.length + 1, color: '"#00B853"', icon: 'gk', position: 'right', info: value })
                this.setState({ myMessage: newArr, value: '', sendMessage: '' })
            }

        } else {
            message.info('发送内容不能为空,请输入');
        }
        this.reactQuillRef.focus()
    }
//键盘事件 enter发送  ctrl+enter换行
    onKeyup = (e) => {
        if (e.keyCode === 13) {
            if (window.event.ctrlKey) {
                var range = this.reactQuillRef.getEditor().getSelection();
                let position = range ? range.index : 0;
                this.reactQuillRef.getEditor().insertText(position, "\n");
                this.reactQuillRef.focus()
                this.reactQuillRef.getEditor().setSelection(position + 1);
            }
        }
    }
    //当失去焦点之前,报讯光标在输入框中的最后位置
    blur = () => {
        var range = this.reactQuillRef.getEditor().getSelection();
        this.setState({ lastEditIndex: range })
    };
    render() {
        const { value } = this.state
        return (
            <div style={{ width: '100%', height: '100%' }}>
                <div className='personMessage_padd'>
                    <PersonMessageHeader {...this.props.location.state} />
                </div>
                <div className='person_line_padd'><Divider /></div>
                <div className='personMessageMain'>
                    <Dialogue {...this.state.myMessage} />
                </div>
                <div className='person_line_padd'><Divider /></div>
                <div style={{ marginTop: "10px" }} >
                    <BottomIcon title="表情" icon="icon-biaoqing" pickEmoji={this.pickEmoji} />
                    <BottomIcon title="点赞" icon="icon-dianzan" />
                    <BottomIcon title="发送文件" icon="icon-wenjianshangchuan" />
                    <BottomIcon title="富文本输入" icon="icon-zhankaiquanpingkuozhan" right={true} />
                </div>
                <div style={{ 'marginRight': '5px' }} onBlurCapture={this.blur}>
                    <ReactQuill
                        className='personMessage_textArea'
                        modules={this.modules}
                        formats={this.formats}
                        onChange={this.handleChange}
                        value={value}
                        theme="snow"
                        onKeyUp={this.onKeyup}
                        ref={c => {
                            if (c) {
                                this.reactQuillRef = c;
                                c.focus()
                            }
                        }
                        }

                    // onBlur={this.inputBlur}不可以,当富文本失去焦点的时候,会出现getSelection为null
                    />
                </div>
                <div style={{ padding: "0px 10px 10px 10px", 'float': 'right' }}>
                    <span style={{ marginRight: '15px', fontSize: '13px', 'color': '#BDBDBD', 'fontWeight': '300' }}>Enter键发送,Enter+Ctrl 键换行</span>
                    <Button type="primary" size="middle" style={{ 'borderRadius': '5px' }} onClick={this.addMessage}>
                        发送
                    </Button>
                </div>
            </div>

        )
    }
}
export default PersonMessage

css样式 PersonMessage.css

.personMessage_padd {
    margin: 0px 10px 0px 10px;
    width: 100%;
    height: 40px;
}

.person_line_padd {
    margin: 10px 10px 10px 10px;
}

.personMessageMain {
    /* background-color: aqua; */
    height: 58%;
    margin-top: 5px;
    max-height: 58%;
    min-height: 58%;
    margin-left: 10px;
    width: 99%;
    min-width: 99%;
    max-width: 99%;
    float: left;
}

.personMessage_textArea {
    /* padding: 40px 0px 0px 10px; */
    margin: 40px 10px 10px 10px;
    min-height: 115px;
    max-width: 98%;
    min-width: 98%;
    border: 0;
    background-color: #F1F2F3;
    max-height: 115px;
    resize: none;
    font-size: 15px;
    overflow:auto
}
.personMessage_textArea:focus {
    
    outline: none;
    white-space: pre-wrap;
    font-size: 15px;
}
/* 富文本框的样式 */
.ql-toolbar.ql-snow {
    border: 0;
    display: none;
}
.ql-editor{
    padding: 0;
    min-height: 115px;
    max-height: 115px;
}
.ql-container.ql-snow{
    border: 0;
}
.ql-container{
    min-height: 115px;
    max-height: 115px;
}

/* 输入框滚动条样式 */
/* .personMessage_textArea::-webkit-scrollbar {
    height: 0;
    width: 0;
    color: transparent;
} */
textarea.ant-input{
    min-height: 110px;
}
其他组件的代码和样式

头部组件 PersonMessage_header.jsx

import React, { Component } from 'react'
import { createFromIconfontCN } from '@ant-design/icons';
import { ALIICONURL } from '../../../../../utils/constant';
import './PersonMessageHeader.css'

class PersonMessage_header extends Component {
  state = { myName: '', myTeam: '' }
  IconFont = createFromIconfontCN({
    scriptUrl: ALIICONURL,
  });
  static getDerivedStateFromProps(nextProps, state) {
    const { team, name } = nextProps
    if (state.myName === name && state.myTeam === team) {
      return null
    }
    let t = team
    let n = name
    if (team.length > 40) {
      t = t.strstring(0, 38)+'...'
    }
    if (name.length > 40) {
      n = n.strstring(0, 38)+'...'
    }
    return {
      myName: n, myTeam: t
    }
  }
  render() {
  //messageIcon :聊天对方的头像,我这里用icon代替的可以用图片 
  // color :头像的背景颜色
    const { messageIcon, color } = this.props
    const { myName, myTeam } = this.state
    return (
      <div className={myTeam === '' ?'PersonMessage_header_div0':'PersonMessage_header_div01'}>
      {/* <div className='PersonMessage_header_div0'> */}
        <div className='PersonMessage_header_div1'>
          <span className={myTeam === '' ? 'PersonMessage_header_button2' : 'PersonMessage_header_button1'} style={{ backgroundColor: color }}><this.IconFont type={messageIcon ? messageIcon : " "} className='PersonMessage_header_icon' /></span>
        </div>
        <div className='PersonMessage_header_div2'>
          <span className='PersonMessage_header_name'>{myName}</span>
          <br />
          <span className='PersonMessage_header_team'>{myTeam}</span>
        </div>
      </div>
    )
  }
}
export default PersonMessage_header

头部组件样式
PersonMessage_header.css

.PersonMessage_header_button1 {
    font-size: 16px;
    padding: 6px 8px 6px 8px;
    margin: 0 4px 4px 0;
    border-radius: 5px;
    position: relative;
    top: 0.45rem;
}
.PersonMessage_header_button2 {
    font-size: 16px;
    padding: 6px 8px 6px 8px;
    margin: 0 4px 4px 0;
    border-radius: 5px;
    position: relative;
    top: 0.01rem;
}


.PersonMessage_header_icon {
    font-size: larger;
    color: #fff;
}

.PersonMessage_header_name {
    font-weight: 520;
    font-size: 14px;
}

.PersonMessage_header_team {
    color: #9e9e9e;
    font-size: 12px;
}
.PersonMessage_header_div0{
    padding-top: 12px;
    height: 100%;
}
.PersonMessage_header_div01{
    height: 100%;
    padding: 5px 0px 5px 0px;
}
.PersonMessage_header_div1{
    float: left;
}
.PersonMessage_header_div2{
    font-size: 0;
    float: left;
    margin-left: 8px;
}

图标组件 BottomIcon.jsx

import React, { Component } from 'react'
import { Tooltip, Popover } from 'antd'
import data from '@emoji-mart/data'
import Picker from '@emoji-mart/react'
import { createFromIconfontCN } from '@ant-design/icons';
import { ALIICONURL } from '../../../../../utils/constant';
import './BottomIcon.css'

export default class BottomIcon extends Component {
    state = { move: false, emoji: false }
    IconFont = createFromIconfontCN({
        scriptUrl: ALIICONURL,
    });
    content = (
        <div>
            <Picker data={data} onEmojiSelect={(emoji, event) => {if (this.props.pickEmoji) { this.props.pickEmoji(emoji, event);this.setState({emoji: false}) } }} />
        </div>
    );
    mouseHander = (flag) => {
        return () => {
            this.setState({ move: flag })
        }
    }
    handleVisibleChange = (e) => {
        if (e) {
            if (this.props.title === '表情') {
                this.setState({ emoji: true })
            } else {
                this.setState({ emoji: false })
            }
        } else {
            this.setState({ emoji: false })
        }
    }
    render() {
        const { icon, title } = this.props
        const { move, emoji } = this.state
        return (
            <div >
                <div className={this.props.right ? 'BottomIcon_all1' : 'BottomIcon_all'}  >
                    <Popover content={this.content} open={emoji} trigger="click" onOpenChange={(e) => this.handleVisibleChange(e)}>
                        <Tooltip placement="bottom" title={title} mouseEnterDelay={0} mouseLeaveDelay={0}>
                            <span 
                            onMouseEnter={this.mouseHander(true)} 
                            onMouseLeave={this.mouseHander(false)} 
                            className={move ? 'BottomIcon_icon1' : 'BottomIcon_icon'}
                            >
                                <this.IconFont type={icon} style={{ fontSize: "20px", color: '#5F6061' }} />
                                </span>
                        </Tooltip>
                    </Popover>
                </div>
            </div>
        )
    }
}

图标组件的样式 BottomIcon.css

.BottomIcon_all {
    float: left;
    margin-left: 10px;
}

.BottomIcon_all1 {
    float: right;
    margin-right: 10px;
}

.BottomIcon_icon {
    padding: 8px 8px 5px 8px;
    border-radius: 5px;
}

.BottomIcon_icon1 {
    padding: 8px 8px 5px 8px;
    border-radius: 5px;
    background-color: #D8DBDD;
}

.ant-tooltip-inner {
    font-size: 12px;
}

/* .emoji-mart-search {
    display: none !important;
}

.emoji-mart-preview {
    display: none !important;
} */

结语:如果疑问,欢迎私信或评论区交流~

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Qt QTreeWidget树形控件用法详解

文章目录QTreeWidget控件的创建QTreeWidget\QTreeView的关系和区别QTreeWidgetItem类QTreeWidget的实际应用1) 添加结点2) 给结点添加图标3) 给结点添加复选框4) 多列树形控件5) QTreeWidget中添加其它控件QTreeWidget信号和槽已剪辑自: http://c.biancheng.net/view/vip_9659.…

PS-HDR图像编辑与应用

每天一个PS/PR小技巧&#xff08;原理实践&#xff09;https://blog.csdn.net/tiao_god/article/details/124186746用PS打开一张HDR图像。 一般打开的图像会很黑&#xff0c;只有少部分光源处比较亮&#xff0c;这是因为默认显示时高动态范围的值都除以了一个统一的值来归一化…

TypeError: ‘module‘ object is not callable 报错解决

pycharm 控制台报错内容如下&#xff1a; pgsqlSearch.py 连接pgsql代码如下&#xff1a; import psycopg2 import ReportModelif __name__ __main__:# 创建连接对象conn psycopg2.connect(database"checkdb", user"postgres", password"postgres…

Flutter高仿微信-第21篇-支付-向商家付款(二维码)

Flutter高仿微信系列共59篇&#xff0c;从Flutter客户端、Kotlin客户端、Web服务器、数据库表结构、Xmpp即时通讯服务器、视频通话服务器、腾讯云服务器全面讲解。 详情请查看 效果图&#xff1a; 实现代码&#xff1a; /*** Author : wangning* Email : maoning20080809163.…

IntelliJ IDEA-Debug断点调试 看这篇文章就够了

详解IntelliJ IDEA-Debug断点调试 如今&#xff0c;IntelliJ IDEA 目前深受广大开发者喜爱&#xff0c;我们在实际开发工作中&#xff0c;不管是用来阅读源码还是在开发过程中都需要进行代码调试。 以下为大家准备了一篇关于IntelliJ IDEA-Debug断点调试的文章&#xff0c;如…

嗨 Jina,帮我画一幅高山流水图

本项目将 Whisper 与 Stable Diffusion 模型结合&#xff0c;可以直接完成语音生成图像的任务。用户可以语音输入一个短句&#xff0c;Whisper 会自动将语音转化为文本&#xff0c;接着&#xff0c;Stable Diffusion 会根据文本生成图像。 本项目基于 Jina AI MLOps 平台搭建&a…

一个Python爬虫案例,带你掌握xpath数据解析方法!

文章目录 xpath基本概念xpath解析原理环境安装如何实例化一个etree对象&#xff1a;xpath(‘xpath表达式’)xpath爬取58二手房实例爬取网址完整代码效果图xpath图片解析下载实例爬取网址完整代码效果图xpath爬取全国城市名称实例爬取网址完整代码效果图xpath爬取简历模板实例爬…

【pygame学习+实战】第一篇:游戏最小系统

14天学习训练营导师课程&#xff1a; 李宁《Python Pygame游戏开发入门与实战》 李宁《计算机视觉OpenCV Python项目实战》1 李宁《计算机视觉OpenCV Python项目实战》2 李宁《计算机视觉OpenCV Python项目实战》3 文章目录前言一、什么是pygame&#xff1f;1.1 学习pygame的用…

当我们谈论DDD时我们在谈论什么

谈论到 DDD&#xff0c;我们会聊事件风暴&#xff0c;会聊限界上下文&#xff0c;会聊六边形架构&#xff0c;会聊实体值对象。这些概念各不相同&#xff0c;相关的概念也很不一样&#xff0c;但都属于DDD的范畴。见过了很多DDD的讨论和工作坊&#xff0c;我发现大家唇枪舌剑无…

【同时完成超分和MEF】

Deep Coupled Feedback Network for Joint Exposure Fusion and Image Super-Resolution &#xff08;用于联合曝光融合和图像超分辨的深度耦合反馈网络&#xff09; 如今&#xff0c;人们已经习惯了拍照来记录自己的日常生活&#xff0c;然而&#xff0c;照片实际上与真实的…

SB30100LCT-ASEMI插件肖特基二极管SB30100LCT

编辑-Z SB30100LCT在TO-220AB封装里采用的2个芯片&#xff0c;其尺寸都是94MIL&#xff0c;是一款插件肖特基二极管。SB30100LCT的浪涌电流Ifsm为200A&#xff0c;漏电流(Ir)为12uA&#xff0c;其工作时耐温度范围为-55~150摄氏度。SB30100LCT采用金属硅芯片材质&#xff0c;里…

解决OpenCV在Cmake时,因网络问题无法下载部分所需文件

解决OpenCV在Cmake时&#xff0c;因网络问题无法下载部分所需文件 在安装CUDA-Opecv进行Cmake的过程中&#xff0c;因为网络问题很多文件都无法下载。可以在你的opencv/.cache下可以看到&#xff0c;很多文件都是0kb的。这样肯定是不行的&#xff0c;我们要保证每个文件都要下…

深入 category 数据类型

目录 前言 1 作用 2 用法 2.1通过 pd.Categorical 创建 category 类型数据&#xff0c;同时指定可选项 2.2 通过 dtype 参数创建 category 类型数据 2.3 此时对数据进行排序 2.4 通过 CategoricalDtype 指定 category 数据的类型顺序 2.5 想要临时修改排序规则&…

低代码开发是未来软件开发的主流模式

低代码平台起始于20世纪80年代4GL“第四代编程语言”。2014年&#xff0c;Forrester research 提出低代码平台的概念&#xff0c;中国低代码市场进入发展期&#xff1b;2018 年&#xff0c;西门子收购低代码企业 Mendix 、美国低代码独角兽企业 Outsystem 获得 1.5 亿美元的融资…

Flutter高仿微信-第22篇-支付-二维码收款(二维码)

Flutter高仿微信系列共59篇&#xff0c;从Flutter客户端、Kotlin客户端、Web服务器、数据库表结构、Xmpp即时通讯服务器、视频通话服务器、腾讯云服务器全面讲解。 详情请查看 效果图&#xff1a; 实现代码&#xff1a; /*** Author : wangning* Email : maoning20080809163.…

安装Jenkins并在ruby中访问

1. 安装Jenkins 最近不知道为啥&#xff0c;根据官网Linux安装Jenkins的时候下不来安装包&#xff0c;提示连接超时。尝试多次无果后决定在window上安装 curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io.key | sudo tee /usr/share/keyrings/jenkins-keyring.as…

105-120-Hadoop-MapReduce-outputformat:

105-Hadoop-MapReduce-outputformat&#xff1a; OutputFormat 数据输出&#xff0c;OutputFormat接口实现类 OutputFormat是MapReduce输出的基类&#xff0c;所有实现MapReduce输出都实现了 OutputFormat 接口。下面我们介绍几种常见的OutputFormat实现类。 1&#xff0e;O…

Fiddler利用Edxposed框架+TrustMeAlready来突破SSL pinning抓取手机APP数据

一、背景 在使用fiddler做代理抓取应用数据包时&#xff0c;如果要抓取到HTTPS数据&#xff0c;需要将fiddler证书导入到浏览器或手机。浏览器或手机设置好fiddler的代理地址&#xff0c;即可抓取到https数据包。 如果APP应用采用证书锁定后&#xff0c;将无法抓取到https数据…

[附源码]java毕业设计医院档案管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

RabbitMQ 简介

RabbitMQ 简介 首先我们先看一下常见的MQ产品 在上图我们可以知道RabbitMQ和ActiveMQ都支持AMQP协议&#xff0c;那么什么时AMQP呢&#xff1f; AMQP&#xff0c;即 Advanced Message Queuing Protocol&#xff08;高级消息队列协议&#xff09;&#xff0c;是一个网络协议&a…