集成websocket

This commit is contained in:
wangxulei
2025-01-09 17:33:57 +08:00
parent ed34df44ee
commit 8516efd1c8
21 changed files with 515 additions and 766 deletions

View File

@@ -113,6 +113,13 @@
<version>5.7.2</version> <version>5.7.2</version>
</dependency> </dependency>
<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
<version>2.5.0</version>
</dependency>
<dependency> <dependency>
<groupId>com.alibaba</groupId> <groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId> <artifactId>fastjson</artifactId>

View File

@@ -55,7 +55,7 @@ public class AuthChatApi {
@OperLog(operModule = "获取消息列表",operType = OperType.QUERY,operDesc = "获取消息列表") @OperLog(operModule = "获取消息列表",operType = OperType.QUERY,operDesc = "获取消息列表")
public ResultBean<List<ChatVo>> getMessageList(ChatDto chatDto) { public ResultBean<List<ChatVo>> getMessageList(ChatDto chatDto) {
String followId = String.valueOf(request.getAttribute("authorId")); String followId = String.valueOf(request.getAttribute("authorId"));
List<ChatVo> chatVos = chatService.selectChatList(followId); List<ChatVo> chatVos = chatService.getMessageList(followId);
return ResultBean.success(chatVos); return ResultBean.success(chatVos);
} }

View File

@@ -1,6 +1,8 @@
package com.dd.admin.business.chat.controller; package com.dd.admin.business.chat.controller;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.extra.pinyin.PinyinUtil;
import com.dd.admin.business.chat.domain.AuthorChat;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
@@ -35,6 +37,20 @@ public class ChatController {
@Autowired @Autowired
ChatService chatService; ChatService chatService;
@ApiOperation(value = "作者列表")
@ApiOperationSupport(order = 2)
@GetMapping("/admin/chat/authorList")
public ResultBean<List<AuthorChat>> authorList() {
List<AuthorChat> authorChats = chatService.selectAuthorChatList();
authorChats.stream().forEach(authorChat -> {
authorChat.setIndex(String.valueOf(PinyinUtil.getFirstLetter(authorChat.getIndex().charAt(0))));
});
return ResultBean.success(authorChats);
}
@ApiOperation(value = "-分页列表") @ApiOperation(value = "-分页列表")
@ApiOperationSupport(order = 1) @ApiOperationSupport(order = 1)
@GetMapping("/admin/chat/page") @GetMapping("/admin/chat/page")

View File

@@ -0,0 +1,39 @@
package com.dd.admin.business.chat.domain;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AuthorChat {
// 消息的唯一标识id
@ApiModelProperty(value = "用户id")
private String id;
// 显示名称,例如聊天对象的昵称等
@ApiModelProperty(value = "显示名称")
private String displayName;
// 头像的网络地址,用于展示聊天对象的头像图片
@ApiModelProperty(value = "头像")
private String avatar;
// 索引字段,可能用于排序、分组等功能,具体含义依业务而定
@ApiModelProperty(value = "索引")
private String index;
// 未读消息的数量
@ApiModelProperty(value = "未读消息数量")
private Integer unread;
// 最近一条消息的内容,经过相应的渲染处理(如表情替换等)
@ApiModelProperty(value = "最近一条消息内容")
private String lastContent;
// 最近一条消息的发送时间,通常是时间戳形式(单位可能是毫秒)
@ApiModelProperty(value = "最近一条消息发送时间")
private Long lastSendTime;
}

View File

@@ -0,0 +1,40 @@
package com.dd.admin.business.chat.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MessageBean {
// 发送方用户信息
private FromUser fromUser;
// 消息处理类型这里对应数字6具体含义需根据业务确定
private int handlerType;
// 接收方联系人ID这里是一个字符串形式的ID具体格式由业务定义
private String toContactId;
// 消息的唯一标识IDUUID格式具体使用方式依业务而定
private String id;
// 消息类型这里为text表示文本消息可能还有其他类型如image、audio等
private String type;
// 消息内容此处为文本内容“111”根据不同消息类型会有不同格式
private String content;
// 消息状态这里是going具体状态值及含义需结合业务场景明确
private String status;
// 消息发送时间,这里是一个时间戳形式(可能是毫秒级时间戳,需根据业务确认)
private long sendTime;
// 内部类,用于表示发送方用户信息
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class FromUser {
// 显示名称,例如用户的昵称等
private String displayName;
// 用户的唯一标识ID
private String id;
// 用户头像的URL或者其他相关标识这里为空字符串具体使用方式由业务决定
private String avatar;
}
}

View File

@@ -1,6 +1,7 @@
package com.dd.admin.business.chat.mapper; package com.dd.admin.business.chat.mapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.dd.admin.business.chat.domain.AuthorChat;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -26,5 +27,10 @@ public interface ChatMapper extends BaseMapper<Chat> {
ChatVo selectChat(@Param("chatId") String chatId); ChatVo selectChat(@Param("chatId") String chatId);
List<ChatVo> selectChatDetail(@Param("chatDto") ChatDto chatDto); List<ChatVo> selectChatDetail(@Param("chatDto") ChatDto chatDto);
//查询我的聊天记录列表 当我作为收发方都需要考虑 //查询我的聊天记录列表 当我作为收发方都需要考虑
List<ChatVo> selectChatList(@Param("authorId")String authorId); List<ChatVo> getMessageList(@Param("authorId")String authorId);
//admin下面是后台使用的接口
//查询客服聊天列表 当我作为收发方都需要查询最后一条
List<AuthorChat> selectAuthorChatList(@Param("authorId")String authorId);
} }

View File

@@ -58,7 +58,7 @@
</if> </if>
limit 1 limit 1
</select> </select>
<select id="selectChatList" resultType="com.dd.admin.business.chat.domain.ChatVo" <select id="getMessageList" resultType="com.dd.admin.business.chat.domain.ChatVo"
parameterType="java.lang.String"> parameterType="java.lang.String">
select * from ( select * from (
SELECT SELECT
@@ -83,7 +83,11 @@
business_author b ON a.FROM_ID = b.AUTHOR_ID business_author b ON a.FROM_ID = b.AUTHOR_ID
WHERE WHERE
a.TO_ID = #{authorId} a.TO_ID = #{authorId}
UNION ALL UNION ALL
SELECT SELECT
a.TO_ID AS authorId, a.TO_ID AS authorId,
a.TO_NAME AS authorName, a.TO_NAME AS authorName,
@@ -99,9 +103,66 @@
a.FROM_ID = #{authorId} a.FROM_ID = #{authorId}
ORDER BY ORDER BY
create_time DESC create_time DESC
) a1 ) a1
GROUP BY a1.authorId GROUP BY a1.authorId
ORDER BY ORDER BY
create_time DESC create_time DESC
</select> </select>
<select id="selectAuthorChatList" resultType="com.dd.admin.business.chat.domain.AuthorChat">
SELECT
wa.AUTHOR_ID id,
wa.AUTHOR_NAME displayName,
wa.AVATAR_URL avatar,
wa.AUTHOR_NAME AS 'index',
wb.unReadCount unRead,
UNIX_TIMESTAMP(CONVERT_TZ(wb.CREATE_TIME, '+08:00', '+00:00')) lastSendTime
FROM
business_author wa
LEFT JOIN (
SELECT
a.FROM_ID AS authorId,
a.FROM_NAME AS authorName,
b.AVATAR_URL AS authorAvatar,
a.content,
a.create_time,
(
SELECT
count(1)
FROM
business_chat ca
WHERE
ca.FROM_ID = a.FROM_ID
AND ca.to_id = #{authorId}
AND ca.MESSAGE_STATUS = 0
) AS unReadCount
FROM
business_chat a
LEFT JOIN business_author b ON a.FROM_ID = b.AUTHOR_ID
WHERE
a.TO_ID = #{authorId}
UNION ALL
SELECT
a.TO_ID AS authorId,
a.TO_NAME AS authorName,
b.AVATAR_URL AS authorAvatar,
a.content,
a.create_time,
0 AS unReadCount
FROM
business_chat a
LEFT JOIN business_author b ON a.TO_ID = b.AUTHOR_ID
WHERE
a.FROM_ID = #{authorId}
ORDER BY
create_time DESC
) wb ON wa.author_id = wb.authorId
GROUP BY
wa.author_id
ORDER BY
wb.create_time DESC
</select>
</mapper> </mapper>

View File

@@ -1,6 +1,7 @@
package com.dd.admin.business.chat.service; package com.dd.admin.business.chat.service;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.dd.admin.business.chat.domain.AuthorChat;
import com.dd.admin.business.chat.entity.Chat; import com.dd.admin.business.chat.entity.Chat;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.dd.admin.business.chat.domain.ChatVo; import com.dd.admin.business.chat.domain.ChatVo;
@@ -24,9 +25,13 @@ public interface ChatService extends IService<Chat> {
ChatVo selectChat(String chatId); ChatVo selectChat(String chatId);
//-列表 //-列表
List<ChatVo> selectChatDetail(ChatDto chatDto); List<ChatVo> selectChatDetail(ChatDto chatDto);
List<ChatVo> selectChatList(String authorId); List<ChatVo> getMessageList(String authorId);
void readMessage(String authorId,String loginId); void readMessage(String authorId,String loginId);
//未读聊天消息的数量 //未读聊天消息的数量
Integer selectUnReadCount(String authorId); Integer selectUnReadCount(String authorId);
//admin
List<AuthorChat> selectAuthorChatList(String authorId);
} }

View File

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.dd.admin.business.chat.domain.AuthorChat;
import com.dd.admin.common.model.PageFactory; import com.dd.admin.common.model.PageFactory;
import com.dd.admin.business.chat.entity.Chat; import com.dd.admin.business.chat.entity.Chat;
import com.dd.admin.business.chat.mapper.ChatMapper; import com.dd.admin.business.chat.mapper.ChatMapper;
@@ -43,8 +44,8 @@ public class ChatServiceImpl extends ServiceImpl<ChatMapper, Chat> implements Ch
} }
@Override @Override
public List<ChatVo> selectChatList(String authorId) { public List<ChatVo> getMessageList(String authorId) {
return baseMapper.selectChatList(authorId); return baseMapper.getMessageList(authorId);
} }
@Override @Override
@@ -64,4 +65,9 @@ public class ChatServiceImpl extends ServiceImpl<ChatMapper, Chat> implements Ch
queryWrapper.eq(Chat::getMessageStatus,0); queryWrapper.eq(Chat::getMessageStatus,0);
return baseMapper.selectCount(queryWrapper); return baseMapper.selectCount(queryWrapper);
} }
@Override
public List<AuthorChat> selectAuthorChatList(String authorId) {
return baseMapper.selectAuthorChatList(authorId);
}
} }

View File

@@ -1 +1 @@
package com.dd.admin.business.webSocket; package com.dd.admin.business.webSocket;

View File

@@ -0,0 +1 @@
package com.dd.admin.business.webSocket.handler;

View File

@@ -63,7 +63,7 @@ tio:
websocket: websocket:
server: server:
port: 9326 port: 9326
heartbeat-timeout: 10000 heartbeat-timeout: 20000
# 集群配置 默认关闭 # 集群配置 默认关闭
cluster: cluster:
enabled: false enabled: false

108
web/src/api/websocket.js Normal file
View File

@@ -0,0 +1,108 @@
import { Message } from 'element-ui'
let socket = null;//实例对象
let socketLeaveFlag = false
let socketReconnectTimer = null // 计时器对象——重连
let socketReconnectLock = false // WebSocket重连的锁
const heartCheck = {
vueThis: this, // vue实例
timeout: 6000, // 超时时间
timeoutObj: null, // 计时器对象——向后端发送心跳检测
serverTimeoutObj: null, // 计时器对象——等待后端心跳检测的回复
// 心跳检测重置
reset: function () {
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
return this;
},
// 心跳检测启动
start: function () {
this.timeoutObj && clearTimeout(this.timeoutObj);
this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
this.timeoutObj = setTimeout(() => {
// 这里向后端发送一个心跳检测,后端收到后,会返回一个心跳回复
socket.send("ping");
console.log("发送心跳检测");
this.serverTimeoutObj = setTimeout(() => {
// 如果超过一定时间还没重置计时器说明websocket与后端断开了
console.log("未收到心跳检测回复");
// 关闭WebSocket
socket.close();
}, this.timeout);
}, this.timeout);
},
}
const initWebSocket = (wsUrl) => {
return new Promise(function (resolve, reject) {
if ("WebSocket" in window) {
socket = new WebSocket(wsUrl);
socket.onerror = webSocketOnError;
socket.onmessage = webSocketOnMessage;
socket.onclose = webSocketOnClose;
socket.onopen = () => {
console.log('连接成功');
heartCheck.reset().start();
resolve(socket)
};
} else {
Message({
message: "您的浏览器不支持websocket请更换Chrome或者Firefox",
type: 'error',
})
return reject(new Error('浏览器不支持websocket'));
}
});
}
const webSocketOnError = (e) => {
console.log("WebSocket:发生错误");
socketReconnect(e.target.url)
window.dispatchEvent(
new CustomEvent("WSOnError", e)
);
}
//服务器返回的数据
const webSocketOnMessage = (e) => {
if (e.data === 'pong') {
console.log('收到心跳回复');
heartCheck.reset().start();
} else {
console.log('服务器返回的数据')
console.log(e)
}
}
const webSocketOnClose = (e) => {
console.log("WebSocket:已关闭");
// 清除心跳定时器
heartCheck.reset();
if (!socketLeaveFlag) {
// websocket重连
socketReconnect(e.target.url)
}
}
const sendMessage = (data) =>{
if (socket) {
socket.send(data);
console.log('消息已发送:', data);
}
}
const socketReconnect = (url) => {
if (socketReconnectLock) {
return;
}
socketReconnectLock = true;
socketReconnectTimer && clearTimeout(socketReconnectTimer);
socketReconnectTimer = setTimeout(() => {
console.log("WebSocket:重连中...");
socketReconnectLock = false;
// websocket启动
initWebSocket(url);
}, 4000);
}
const closeWebSocket = () => {
socketLeaveFlag = true
if (socket) {
console.log('断开连接');
socket.close();
}
}
//具体问题具体分析,把需要用到的方法暴露出去
export default { closeWebSocket, socket, initWebSocket,sendMessage };

BIN
web/src/assets/sdz.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

View File

@@ -4,10 +4,13 @@
<breadcrumb class="breadcrumb-container" /> <breadcrumb class="breadcrumb-container" />
<div class="right-menu"> <div class="right-menu">
<el-dropdown class="avatar-container" trigger="click"> <el-dropdown class="avatar-container" trigger="click">
<div class="avatar-wrapper"> <div class="avatar-wrapper">
<img :src="require( '@/assets/xiaoxin.jpeg' )" class="user-avatar"> <img :src="require( '@/assets/sdz.jpg' )" class="user-avatar">
<span style="cursor: pointer;">{{user.username}}({{user.deptName}})</span> <span style="cursor: pointer;">{{user.username}}({{user.deptName}})</span>
<i class="el-icon-caret-bottom" /> <i class="el-icon-caret-bottom" />
</div> </div>
@@ -32,7 +35,18 @@
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
</div> </div>
<div class="right-menu" style="margin-right: 20px;">
<div class="top-icon" @click="openIm">
<el-badge :value="200" :max="99" >
<i class="el-icon-bell"></i>
</el-badge>
</div>
</div>
<UpdatePassword ref="updatePass"/> <UpdatePassword ref="updatePass"/>
<Im ref="im"/>
</div> </div>
</template> </template>
@@ -41,12 +55,14 @@ import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb' import Breadcrumb from '@/components/Breadcrumb'
import Hamburger from '@/components/Hamburger' import Hamburger from '@/components/Hamburger'
import UpdatePassword from '@/views/common/system/UpdatePassword' import UpdatePassword from '@/views/common/system/UpdatePassword'
import Im from "@/views/common/Im";
export default { export default {
components: { components: {
UpdatePassword, UpdatePassword,
Breadcrumb, Breadcrumb,
Hamburger Hamburger,
Im
}, },
computed: { computed: {
...mapGetters([ ...mapGetters([
@@ -59,6 +75,9 @@ export default {
updatePassword(){ updatePassword(){
this.$refs.updatePass.open() this.$refs.updatePass.open()
}, },
openIm(){
this.$refs.im.open()
},
toggleSideBar() { toggleSideBar() {
this.$store.dispatch('app/toggleSideBar') this.$store.dispatch('app/toggleSideBar')
}, },
@@ -95,10 +114,36 @@ export default {
} }
} }
.top-icon{
text-align: center;
height: 50px;
width: 50px;
line-height: 50px;
cursor: pointer;
color: #606266;
font-size: 18px;
&:hover {
background: rgba(0, 0, 0, .025)
}
}
.breadcrumb-container { .breadcrumb-container {
float: left; float: left;
} }
.top-menu{
margin-right: 25px;
font-size: 14px;
}
//修改徽章位置
.el-badge {
::v-deep .el-badge__content
{
margin-top:12px;
}
}
.right-menu { .right-menu {
float: right; float: right;
height: 100%; height: 100%;

128
web/src/views/common/Im.vue Normal file
View File

@@ -0,0 +1,128 @@
<template>
<div>
<el-dialog
:title="user.displayName"
append-to-body
top="15vh"
width="60%"
:visible.sync="dialogVisible"
center
@close="handleCancel"
>
<div class="el-dialog-div" style="height:60vh;">
<lemon-imui
width="100%"
height="60vh"
position="center"
:user='this.user' ref="IMUI"
@pull-messages='handlePullMessages'
@send="handleSend"
/>
</div>
</el-dialog>
</div>
</template>
<script>
import {getAuthorChatList} from "@/api/business/chat/chat";
import {isNotEmpty} from "@/utils";
import Websocket from "@/api/websocket.js";
export default {
name: "Im",
props: {
sellList: Array,
},
data() {
return {
dialogVisible: false,
temp:{},
user:{id:8,displayName:'官方客服-薯队长',avatar:'http://8.146.211.120:8080/upload/avatar/kefu.jpg'},
}
},
mounted(){
Websocket.initWebSocket('ws://192.168.1.136:9326/?authorId=8').then(response=>{
console.log(response.onmessage = this.handleMessage)
})
},
methods: {
handleMessage(e) {
console.log('在Vue组件中接收到消息并处理消息内容:');
// 这里可以根据组件的业务逻辑,比如将消息展示在页面上,更新组件状态等操作
},
open() {
this.dialogVisible = true
this.getAuthorList()
},
getAuthorList(){
getAuthorChatList().then(response=> {
this.$nextTick(() => {
const {IMUI} = this.$refs;
const authorList = response.data;
const contacts = [];
for (let item of authorList) {
let data = {
id: item.id,
displayName: item.displayName,
avatar: item.avatar,
index: item.index,
unread: item.unread,
//最近一条消息的内容,如果值为空,不会出现在“聊天”列表里面。
//lastContentRender 函数会将 file 消息转换为 '[文件]', image 消息转换为 '[图片]',对 text 会将文字里的表情标识替换为img标签,
//最近一条消息的发送时间
lastSendTime: item.lastSendTime,
}
if(isNotEmpty(item.lastContent)){
data.lastContent = IMUI.lastContentRender({type: 'text', content: item.lastContent})
}
contacts.push(data)
}
IMUI.initContacts(contacts);
})
})
},
handleCancel(){
this.dialogVisible = false
},
submit(){
},
handleSend(message, next, file) {
console.log('发送了信息')
message.handlerType = '6'
Websocket.sendMessage(JSON.stringify(message))
//执行到next消息会停止转圈如果接口调用失败可以修改消息的状态 next({status:'failed'});
next();
},
handlePullMessages(contact, next) {
console.log('拉取信息')
//从后端请求消息数据,包装成下面的样子
const messages = [{
id: '唯一消息ID',
status: 'succeed',
type: 'text',
sendTime: 1566047865417,
content: '你什么才能对接完?',
toContactId: contact.id,
fromUser:this.user
}]
//将第二个参数设为true表示已到末尾聊天窗口顶部会显示“暂无更多消息”不然会一直转圈。
next(messages,true);
},
},
}
</script>
<style scoped>
/deep/ .el-dialog__headerbtn{
top:10px;
}
/deep/ .el-dialog__header{
padding: 5px 20px 5px;background: #F7F7F7;
}
/deep/ .el-dialog--center .el-dialog__body{
padding:0;
}
</style>

View File

@@ -1,102 +0,0 @@
<template>
<div :class="className" :style="{height:height,width:width}" />
</template>
<script>
import echarts from 'echarts'
require('echarts/theme/macarons') // echarts theme
import resize from './mixins/resize'
const animationDuration = 6000
export default {
mixins: [resize],
props: {
className: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '300px'
}
},
data() {
return {
chart: null
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
})
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(this.$el, 'macarons')
this.chart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { // 坐标轴指示器,坐标轴触发有效
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
}
},
grid: {
top: 10,
left: '2%',
right: '2%',
bottom: '3%',
containLabel: true
},
xAxis: [{
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
axisTick: {
alignWithLabel: true
}
}],
yAxis: [{
type: 'value',
axisTick: {
show: false
}
}],
series: [{
name: 'pageA',
type: 'bar',
stack: 'vistors',
barWidth: '60%',
data: [79, 52, 200, 334, 390, 330, 220],
animationDuration
}, {
name: 'pageB',
type: 'bar',
stack: 'vistors',
barWidth: '60%',
data: [80, 52, 200, 334, 390, 330, 220],
animationDuration
}, {
name: 'pageC',
type: 'bar',
stack: 'vistors',
barWidth: '60%',
data: [30, 52, 200, 334, 390, 330, 220],
animationDuration
}]
})
}
}
}
</script>

View File

@@ -1,79 +0,0 @@
<template>
<div :class="className" :style="{height:height,width:width}" />
</template>
<script>
import echarts from 'echarts'
require('echarts/theme/macarons') // echarts theme
import resize from './mixins/resize'
export default {
mixins: [resize],
props: {
className: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '300px'
}
},
data() {
return {
chart: null
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
})
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(this.$el, 'macarons')
this.chart.setOption({
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
legend: {
left: 'center',
bottom: '0',
data: ['Industries', 'Technology', 'Forex', 'Gold', 'Forecasts']
},
series: [
{
name: 'WEEKLY WRITE ARTICLES',
type: 'pie',
roseType: 'radius',
radius: [15, 95],
center: ['50%', '38%'],
data: [
{ value: 320, name: 'Industries' },
{ value: 240, name: 'Technology' },
{ value: 149, name: 'Forex' },
{ value: 100, name: 'Gold' },
{ value: 59, name: 'Forecasts' }
],
animationEasing: 'cubicInOut',
animationDuration: 2600
}
]
})
}
}
}
</script>

View File

@@ -1,116 +0,0 @@
<template>
<div :class="className" :style="{height:height,width:width}" />
</template>
<script>
import echarts from 'echarts'
require('echarts/theme/macarons') // echarts theme
import resize from './mixins/resize'
const animationDuration = 3000
export default {
mixins: [resize],
props: {
className: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '300px'
}
},
data() {
return {
chart: null
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
})
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(this.$el, 'macarons')
this.chart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { // 坐标轴指示器,坐标轴触发有效
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
}
},
radar: {
radius: '66%',
center: ['50%', '42%'],
splitNumber: 8,
splitArea: {
areaStyle: {
color: 'rgba(127,95,132,.3)',
opacity: 1,
shadowBlur: 45,
shadowColor: 'rgba(0,0,0,.5)',
shadowOffsetX: 0,
shadowOffsetY: 15
}
},
indicator: [
{ name: 'Sales', max: 10000 },
{ name: 'Administration', max: 20000 },
{ name: 'Information Technology', max: 20000 },
{ name: 'Customer Support', max: 20000 },
{ name: 'Development', max: 20000 },
{ name: 'Marketing', max: 20000 }
]
},
legend: {
left: 'center',
bottom: '0',
data: ['Allocated Budget', 'Expected Spending', 'Actual Spending']
},
series: [{
type: 'radar',
symbolSize: 0,
areaStyle: {
normal: {
shadowBlur: 13,
shadowColor: 'rgba(0,0,0,.2)',
shadowOffsetX: 0,
shadowOffsetY: 10,
opacity: 1
}
},
data: [
{
value: [5000, 7000, 12000, 11000, 15000, 14000],
name: 'Allocated Budget'
},
{
value: [4000, 9000, 15000, 15000, 13000, 11000],
name: 'Expected Spending'
},
{
value: [5500, 11000, 12000, 15000, 12000, 12000],
name: 'Actual Spending'
}
],
animationDuration: animationDuration
}]
})
}
}
}
</script>

View File

@@ -1,55 +0,0 @@
import { debounce } from '@/utils'
export default {
data() {
return {
$_sidebarElm: null,
$_resizeHandler: null
}
},
mounted() {
this.$_resizeHandler = debounce(() => {
if (this.chart) {
this.chart.resize()
}
}, 100)
this.$_initResizeEvent()
this.$_initSidebarResizeEvent()
},
beforeDestroy() {
this.$_destroyResizeEvent()
this.$_destroySidebarResizeEvent()
},
// to fixed bug when cached by keep-alive
// https://github.com/PanJiaChen/vue-element-admin/issues/2116
activated() {
this.$_initResizeEvent()
this.$_initSidebarResizeEvent()
},
deactivated() {
this.$_destroyResizeEvent()
this.$_destroySidebarResizeEvent()
},
methods: {
// use $_ for mixins properties
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
$_initResizeEvent() {
window.addEventListener('resize', this.$_resizeHandler)
},
$_destroyResizeEvent() {
window.removeEventListener('resize', this.$_resizeHandler)
},
$_sidebarResizeHandler(e) {
if (e.propertyName === 'width') {
this.$_resizeHandler()
}
},
$_initSidebarResizeEvent() {
this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
},
$_destroySidebarResizeEvent() {
this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
}
}
}

View File

@@ -1,419 +1,58 @@
<template> <template>
<div class="dashboard-container"> <div class="dashboard-container">
<lemon-imui ref="IMUI" />
<div class="dashboard-editor-container">
<div>
<a href="https://github.com/iimeepo/vue-admin-template" target="_blank" class="github-corner" aria-label="View source on Github">
<svg
width="80"
height="80"
viewBox="0 0 250 250"
style="fill:#40c9c6; color:#fff;"
aria-hidden="true"
>
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z" />
<path
d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
fill="currentColor"
style="transform-origin: 130px 106px;"
class="octo-arm"
/>
<path
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
fill="currentColor"
class="octo-body"
/>
</svg>
</a>
</div>
<el-row :gutter="40" class="panel-group">
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel">
<div class="card-panel-icon-wrapper icon-people">
<i class="el-icon-user-solid" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
用户总数
</div>
<span class="card-panel-num">102400</span>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel">
<div class="card-panel-icon-wrapper icon-message">
<i class="el-icon-document" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
文章总数
</div>
<span class="card-panel-num">81212</span>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel">
<div class="card-panel-icon-wrapper icon-money">
<i class="el-icon-chat-dot-square" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
评论总数
</div>
<span class="card-panel-num">9280</span>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel">
<div class="card-panel-icon-wrapper icon-shopping">
<i class="el-icon-view" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
阅读总数
</div>
<span class="card-panel-num">13600</span>
</div>
</div>
</el-col>
</el-row>
<el-row :gutter="32">
<el-col :xs="24" :sm="24" :lg="8">
<div class="chart-wrapper">
<raddar-chart />
</div>
</el-col>
<el-col :xs="24" :sm="24" :lg="8">
<div class="chart-wrapper">
<pie-chart />
</div>
</el-col>
<el-col :xs="24" :sm="24" :lg="8">
<div class="chart-wrapper">
<bar-chart />
</div>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :xs="24" :sm="24" :lg="8">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>阅读量排行</span>
</div>
<el-table
:data="tableData"
style="width: 100%"
>
<el-table-column
prop="sort"
label="排名"
/>
<el-table-column
prop="address"
label="地区"
/>
<el-table-column
prop="number"
label="人数"
/>
</el-table>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :lg="8">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>收益排行</span>
</div>
<div class="block">
<div style="padding-top:35px;" class="progress-item">
<span>Vue</span>
<el-progress :percentage="70" />
</div>
<div class="progress-item">
<span>JavaScript</span>
<el-progress :percentage="18" />
</div>
<div class="progress-item">
<span>CSS</span>
<el-progress :percentage="12" />
</div>
<div class="progress-item">
<span>PHP</span>
<el-progress :percentage="22" />
</div>
<div class="progress-item">
<span>Hyperf</span>
<el-progress :percentage="38" />
</div>
<div class="progress-item">
<span>ESLint</span>
<el-progress :percentage="100" status="success" />
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :lg="8">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>待办事项</span>
</div>
<div class="block">
<el-timeline>
<el-timeline-item
v-for="(activity, index) in activities"
:key="index"
:icon="activity.icon"
:type="activity.type"
:color="activity.color"
:size="activity.size"
:timestamp="activity.timestamp"
>
{{ activity.content }}
</el-timeline-item>
</el-timeline>
</div>
</el-card>
</el-col>
</el-row>
</div>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'
import BarChart from './components/BarChart'
import PieChart from './components/PieChart'
import RaddarChart from './components/RaddarChart'
export default { export default {
name: 'Dashboard', name: 'Dashboard',
components: {
BarChart,
PieChart,
RaddarChart
},
data() { data() {
return { return {
tableData: [{ user:{id:1,displayName:'June',avatar:''}
sort: '1',
address: '北京',
number: 23423
}, {
sort: '2',
address: '上海',
number: 2312
}, {
sort: '3',
address: '广州',
number: 2231
}, {
sort: '4',
address: '深圳',
number: 1234
}, {
sort: '5',
address: '杭州',
number: 123
}],
activities: [{
content: '支持使用图标',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: 'el-icon-more'
}, {
content: '支持自定义颜色',
timestamp: '2018-04-03 20:46',
color: '#0bbd87'
}, {
content: '支持自定义尺寸',
timestamp: '2018-04-03 20:46',
size: 'large'
}, {
content: '默认样式的节点',
timestamp: '2018-04-03 20:46'
}]
}
},
computed: {
...mapGetters([
'name'
])
} }
},
mounted(){
const { IMUI } = this.$refs;
//初始化表情包。
//从后端请求联系人数据,包装成下面的样子
const contacts = [{
id: 2,
displayName: '丽安娜',
avatar:'http://8.146.211.120:8080/upload/notes/note (6).jpg',
index: 'L',
unread: 0,
//最近一条消息的内容,如果值为空,不会出现在“聊天”列表里面。
//lastContentRender 函数会将 file 消息转换为 '[文件]', image 消息转换为 '[图片]',对 text 会将文字里的表情标识替换为img标签,
lastContent: IMUI.lastContentRender({type:'text',content:'你在干嘛呢?'}),
//最近一条消息的发送时间
lastSendTime: 1566047865417
}];
IMUI.initContacts(contacts);
},
methods: {
handleSend(message, next, file) {
console.log('发送了信息')
//执行到next消息会停止转圈如果接口调用失败可以修改消息的状态 next({status:'failed'});
next();
},
handlePullMessages(contact, next) {
console.log('拉取信息')
//从后端请求消息数据,包装成下面的样子
const messages = [{
id: '唯一消息ID',
status: 'succeed',
type: 'text',
sendTime: 1566047865417,
content: '你什么才能对接完?',
toContactId: contact.id,
fromUser:this.user
}]
//将第二个参数设为true表示已到末尾聊天窗口顶部会显示“暂无更多消息”不然会一直转圈。
next(messages,true);
},
}
} }
</script> </script>
<style lang="scss" scoped>
.dashboard-editor-container {
padding: 32px;
background-color: rgb(240, 242, 245);
position: relative;
.github-corner {
position: absolute;
top: 0px;
border: 0;
right: 0;
}
.chart-wrapper {
background: #fff;
padding: 16px 16px 0;
margin-bottom: 32px;
}
}
.panel-group {
margin-top: 18px;
.card-panel-col {
margin-bottom: 32px;
}
.card-panel {
height: 108px;
cursor: pointer;
font-size: 12px;
position: relative;
overflow: hidden;
color: #666;
background: #fff;
box-shadow: 4px 4px 40px rgba(0, 0, 0, .05);
border-color: rgba(0, 0, 0, .05);
&:hover {
.card-panel-icon-wrapper {
color: #fff;
}
.icon-people {
background: #40c9c6;
}
.icon-message {
background: #36a3f7;
}
.icon-money {
background: #f4516c;
}
.icon-shopping {
background: #34bfa3
}
}
.icon-people {
font-size: 48px;
color: #40c9c6;
}
.icon-message {
font-size: 48px;
color: #36a3f7;
}
.icon-money {
font-size: 48px;
color: #f4516c;
}
.icon-shopping {
font-size: 48px;
color: #34bfa3
}
.card-panel-icon-wrapper {
float: left;
margin: 14px 0 0 14px;
padding: 16px;
transition: all 0.38s ease-out;
border-radius: 6px;
}
.card-panel-icon {
float: left;
font-size: 48px;
}
.card-panel-description {
float: right;
font-weight: bold;
margin: 26px;
margin-left: 0px;
.card-panel-text {
line-height: 18px;
color: rgba(0, 0, 0, 0.45);
font-size: 16px;
margin-bottom: 12px;
}
.card-panel-num {
font-size: 20px;
}
}
}
}
.github-corner:hover .octo-arm {
animation: octocat-wave 560ms ease-in-out
}
@keyframes octocat-wave {
0%,
100% {
transform: rotate(0)
}
20%,
60% {
transform: rotate(-25deg)
}
40%,
80% {
transform: rotate(10deg)
}
}
@media (max-width:500px) {
.github-corner:hover .octo-arm {
animation: none
}
.github-corner .octo-arm {
animation: octocat-wave 560ms ease-in-out
}
}
@media (max-width:550px) {
.card-panel-description {
display: none;
}
.card-panel-icon-wrapper {
float: none !important;
width: 100%;
height: 100%;
margin: 0 !important;
.svg-icon {
display: block;
margin: 14px auto !important;
float: none !important;
}
}
}
@media (max-width:1024px) {
.chart-wrapper {
padding: 8px;
}
}
</style>