10/10
1. 设备管理界面完成 2. 用户管理界面完成 3. 会话列表完成
This commit is contained in:
@@ -12,7 +12,8 @@ VITE_APP_BASE_API='/dev-api'
|
|||||||
VITE_LOCALHOUSET=true
|
VITE_LOCALHOUSET=true
|
||||||
# 本地开发环境
|
# 本地开发环境
|
||||||
VITE_BASE_API='http://192.168.0.212:8082'
|
VITE_BASE_API='http://192.168.0.212:8082'
|
||||||
|
# 默认头像
|
||||||
|
VITE_DEFAULT_USER_IMAGE='https://ts3.tc.mm.bing.net/th?id=ORMS.48a27ca8353fa47bf0dc8df38854d0e1&pid=Wdp&w=312&h=172&qlt=90&c=1&rs=1&dpr=1&p=0'
|
||||||
|
|
||||||
# 应用访问路径 例如使用前缀 /admin/
|
# 应用访问路径 例如使用前缀 /admin/
|
||||||
VITE_APP_CONTEXT_PATH='/'
|
VITE_APP_CONTEXT_PATH='/'
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import request from '@/utils/request';
|
|||||||
*/
|
*/
|
||||||
export const listCsSession = (query?: CsSessionQuery): AxiosPromise<PageResult<CsSessionVO>> => {
|
export const listCsSession = (query?: CsSessionQuery): AxiosPromise<PageResult<CsSessionVO>> => {
|
||||||
return request({
|
return request({
|
||||||
url: '/conversation/csSession/list',
|
url: '/cs/session/list',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: query
|
params: query
|
||||||
});
|
});
|
||||||
@@ -22,7 +22,7 @@ export const listCsSession = (query?: CsSessionQuery): AxiosPromise<PageResult<C
|
|||||||
*/
|
*/
|
||||||
export const getCsSession = (sessionId: string | number): AxiosPromise<CsSessionVO> => {
|
export const getCsSession = (sessionId: string | number): AxiosPromise<CsSessionVO> => {
|
||||||
return request({
|
return request({
|
||||||
url: '/conversation/csSession/' + sessionId,
|
url: '/cs/session/' + sessionId,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -33,7 +33,7 @@ export const getCsSession = (sessionId: string | number): AxiosPromise<CsSession
|
|||||||
*/
|
*/
|
||||||
export const addCsSession = (data: CsSessionForm) => {
|
export const addCsSession = (data: CsSessionForm) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/conversation/csSession',
|
url: '/cs/session',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
@@ -45,7 +45,7 @@ export const addCsSession = (data: CsSessionForm) => {
|
|||||||
*/
|
*/
|
||||||
export const updateCsSession = (data: CsSessionForm) => {
|
export const updateCsSession = (data: CsSessionForm) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/conversation/csSession',
|
url: '/cs/session',
|
||||||
method: 'put',
|
method: 'put',
|
||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
@@ -58,7 +58,17 @@ export const updateCsSession = (data: CsSessionForm) => {
|
|||||||
*/
|
*/
|
||||||
export const delCsSession = (sessionId: string | number | Array<string | number>) => {
|
export const delCsSession = (sessionId: string | number | Array<string | number>) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/conversation/csSession/' + sessionId,
|
url: '/cs/session/' + sessionId,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 关闭客服会话
|
||||||
|
* @param sessionId
|
||||||
|
*/
|
||||||
|
export const closeCsSession = (sessionId: string) => {
|
||||||
|
return request({
|
||||||
|
url: '/cs/session/' + sessionId + '/end',
|
||||||
method: 'delete'
|
method: 'delete'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export interface CsSessionVO {
|
|||||||
/**
|
/**
|
||||||
* 主键(雪花)
|
* 主键(雪花)
|
||||||
*/
|
*/
|
||||||
sessionId: string | number;
|
sessionId: string;
|
||||||
/**
|
/**
|
||||||
* 会话编号
|
* 会话编号
|
||||||
*/
|
*/
|
||||||
@@ -10,11 +10,11 @@ export interface CsSessionVO {
|
|||||||
/**
|
/**
|
||||||
* APP 用户
|
* APP 用户
|
||||||
*/
|
*/
|
||||||
userId: string | number;
|
userId: string;
|
||||||
/**
|
/**
|
||||||
* 关联设备,可空
|
* 关联设备,可空
|
||||||
*/
|
*/
|
||||||
deviceId: string | number;
|
deviceId: string;
|
||||||
/**
|
/**
|
||||||
* 问题类型
|
* 问题类型
|
||||||
*/
|
*/
|
||||||
@@ -30,7 +30,7 @@ export interface CsSessionVO {
|
|||||||
/**
|
/**
|
||||||
* 当前接待客服
|
* 当前接待客服
|
||||||
*/
|
*/
|
||||||
agentId: string | number;
|
agentId: string;
|
||||||
/**
|
/**
|
||||||
* 接入方式(bot/bot_to_human/human)
|
* 接入方式(bot/bot_to_human/human)
|
||||||
*/
|
*/
|
||||||
@@ -61,7 +61,7 @@ export interface CsSessionForm extends BaseEntity {
|
|||||||
/**
|
/**
|
||||||
* 主键(雪花)
|
* 主键(雪花)
|
||||||
*/
|
*/
|
||||||
sessionId?: string | number;
|
sessionId?: string;
|
||||||
/**
|
/**
|
||||||
* 会话编号
|
* 会话编号
|
||||||
*/
|
*/
|
||||||
@@ -69,11 +69,11 @@ export interface CsSessionForm extends BaseEntity {
|
|||||||
/**
|
/**
|
||||||
* APP 用户
|
* APP 用户
|
||||||
*/
|
*/
|
||||||
userId?: string | number;
|
userId?: string;
|
||||||
/**
|
/**
|
||||||
* 关联设备,可空
|
* 关联设备,可空
|
||||||
*/
|
*/
|
||||||
deviceId?: string | number;
|
deviceId?: string;
|
||||||
/**
|
/**
|
||||||
* 问题类型
|
* 问题类型
|
||||||
*/
|
*/
|
||||||
@@ -89,7 +89,7 @@ export interface CsSessionForm extends BaseEntity {
|
|||||||
/**
|
/**
|
||||||
* 当前接待客服
|
* 当前接待客服
|
||||||
*/
|
*/
|
||||||
agentId?: string | number;
|
agentId?: string;
|
||||||
/**
|
/**
|
||||||
* 接入方式(bot/bot_to_human/human)
|
* 接入方式(bot/bot_to_human/human)
|
||||||
*/
|
*/
|
||||||
@@ -124,11 +124,11 @@ export interface CsSessionQuery extends PageQuery {
|
|||||||
/**
|
/**
|
||||||
* APP 用户
|
* APP 用户
|
||||||
*/
|
*/
|
||||||
userId?: string | number;
|
userId?: string;
|
||||||
/**
|
/**
|
||||||
* 关联设备,可空
|
* 关联设备,可空
|
||||||
*/
|
*/
|
||||||
deviceId?: string | number;
|
deviceId?: string;
|
||||||
/**
|
/**
|
||||||
* 问题类型
|
* 问题类型
|
||||||
*/
|
*/
|
||||||
@@ -144,7 +144,7 @@ export interface CsSessionQuery extends PageQuery {
|
|||||||
/**
|
/**
|
||||||
* 当前接待客服
|
* 当前接待客服
|
||||||
*/
|
*/
|
||||||
agentId?: string | number;
|
agentId?: string;
|
||||||
/**
|
/**
|
||||||
* 接入方式(bot/bot_to_human/human)
|
* 接入方式(bot/bot_to_human/human)
|
||||||
*/
|
*/
|
||||||
@@ -169,8 +169,4 @@ export interface CsSessionQuery extends PageQuery {
|
|||||||
* 最后消息时间
|
* 最后消息时间
|
||||||
*/
|
*/
|
||||||
lastMessageTime?: string;
|
lastMessageTime?: string;
|
||||||
/**
|
|
||||||
* 日期范围参数
|
|
||||||
*/
|
|
||||||
params?: any;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import request from '@/utils/request';
|
|||||||
*/
|
*/
|
||||||
export const listCsKb = (query?: CsKbQuery): AxiosPromise<PageResult<CsKbVO>> => {
|
export const listCsKb = (query?: CsKbQuery): AxiosPromise<PageResult<CsKbVO>> => {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/csKb/list',
|
url: '/cs/kb/list',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: query
|
params: query
|
||||||
});
|
});
|
||||||
@@ -22,7 +22,7 @@ export const listCsKb = (query?: CsKbQuery): AxiosPromise<PageResult<CsKbVO>> =>
|
|||||||
*/
|
*/
|
||||||
export const getCsKb = (kbId: string | number): AxiosPromise<CsKbVO> => {
|
export const getCsKb = (kbId: string | number): AxiosPromise<CsKbVO> => {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/csKb/' + kbId,
|
url: '/cs/kb/' + kbId,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -33,7 +33,7 @@ export const getCsKb = (kbId: string | number): AxiosPromise<CsKbVO> => {
|
|||||||
*/
|
*/
|
||||||
export const addCsKb = (data: CsKbForm) => {
|
export const addCsKb = (data: CsKbForm) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/csKb',
|
url: '/cs/kb',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
@@ -45,7 +45,7 @@ export const addCsKb = (data: CsKbForm) => {
|
|||||||
*/
|
*/
|
||||||
export const updateCsKb = (data: CsKbForm) => {
|
export const updateCsKb = (data: CsKbForm) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/csKb',
|
url: '/cs/kb',
|
||||||
method: 'put',
|
method: 'put',
|
||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
@@ -58,7 +58,31 @@ export const updateCsKb = (data: CsKbForm) => {
|
|||||||
*/
|
*/
|
||||||
export const delCsKb = (kbId: string | number | Array<string | number>) => {
|
export const delCsKb = (kbId: string | number | Array<string | number>) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/csKb/' + kbId,
|
url: '/cs/kb/' + kbId,
|
||||||
method: 'delete'
|
method: 'delete'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* 移动客服知识库
|
||||||
|
* @param kbId
|
||||||
|
* @param direction
|
||||||
|
*/
|
||||||
|
export const moveSkb = (kbId: string, direction: string = 'up'): AxiosPromise<{ moved: boolean }> => {
|
||||||
|
return request({
|
||||||
|
url: '/cs/kb/move',
|
||||||
|
method: 'put',
|
||||||
|
data: {
|
||||||
|
kbId,
|
||||||
|
direction,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 知识库总览
|
||||||
|
*/
|
||||||
|
export const overViewCsKb = (): AxiosPromise<{ "enabledCount": number, "stoppedCount": number }> => {
|
||||||
|
return request({
|
||||||
|
url: '/cs/kb/overview',
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,32 +1,72 @@
|
|||||||
export interface CsKbVO {
|
export interface CsKbVO {
|
||||||
/**
|
/**
|
||||||
* 主键(雪花)
|
* 快捷按钮
|
||||||
*/
|
*/
|
||||||
kbId: string | number;
|
buttons: CzCsKbButton[];
|
||||||
/**
|
/**
|
||||||
* 知识编号,如 KB-001
|
* 分类
|
||||||
*/
|
|
||||||
kbNo: string;
|
|
||||||
/**
|
|
||||||
* 问题标题
|
|
||||||
*/
|
|
||||||
title: string;
|
|
||||||
/**
|
|
||||||
* 分类(设备配网/耗材滤芯/固件升级/设备报修;固件升级仅文案分类)
|
|
||||||
*/
|
*/
|
||||||
category: string;
|
category: string;
|
||||||
/**
|
/**
|
||||||
* 机器人回复内容
|
* 内容
|
||||||
*/
|
*/
|
||||||
content: string;
|
content: string;
|
||||||
/**
|
/**
|
||||||
* 排序权重,数字越小越靠前
|
* 创建时间
|
||||||
|
*/
|
||||||
|
createTime: string;
|
||||||
|
/**
|
||||||
|
* 知识库主键
|
||||||
|
*/
|
||||||
|
kbId: string;
|
||||||
|
/**
|
||||||
|
* 知识编号
|
||||||
|
*/
|
||||||
|
kbNo: string;
|
||||||
|
/**
|
||||||
|
* 排序
|
||||||
*/
|
*/
|
||||||
sort: number;
|
sort: number;
|
||||||
/**
|
/**
|
||||||
* 状态(0停用 1启用)
|
* 状态(0停用 1启用)
|
||||||
*/
|
*/
|
||||||
status: boolean;
|
status: string;
|
||||||
|
/**
|
||||||
|
* 状态名称
|
||||||
|
*/
|
||||||
|
statusName: string;
|
||||||
|
/**
|
||||||
|
* 标题
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库快捷按钮 cz_cs_kb_button。
|
||||||
|
*
|
||||||
|
* CzCsKbButton
|
||||||
|
*/
|
||||||
|
export type CzCsKbButton = {
|
||||||
|
/**
|
||||||
|
* 按钮编码
|
||||||
|
*/
|
||||||
|
buttonCode: string;
|
||||||
|
/**
|
||||||
|
* 主键
|
||||||
|
*/
|
||||||
|
buttonId: string;
|
||||||
|
/**
|
||||||
|
* 知识库ID
|
||||||
|
*/
|
||||||
|
kbId: string;
|
||||||
|
/**
|
||||||
|
* 按钮文案
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
/**
|
||||||
|
* 排序
|
||||||
|
*/
|
||||||
|
sort: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CsKbForm extends BaseEntity {
|
export interface CsKbForm extends BaseEntity {
|
||||||
@@ -57,7 +97,7 @@ export interface CsKbForm extends BaseEntity {
|
|||||||
/**
|
/**
|
||||||
* 状态(0停用 1启用)
|
* 状态(0停用 1启用)
|
||||||
*/
|
*/
|
||||||
status?: boolean;
|
status?: string;
|
||||||
/**
|
/**
|
||||||
* 创建部门
|
* 创建部门
|
||||||
*/
|
*/
|
||||||
@@ -104,7 +144,7 @@ export interface CsKbQuery extends PageQuery {
|
|||||||
/**
|
/**
|
||||||
* 状态(0停用 1启用)
|
* 状态(0停用 1启用)
|
||||||
*/
|
*/
|
||||||
status?: boolean;
|
status?: string;
|
||||||
/**
|
/**
|
||||||
* 创建部门
|
* 创建部门
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import request from '@/utils/request';
|
|||||||
*/
|
*/
|
||||||
export const listCsAgent = (query?: CsAgentQuery): AxiosPromise<PageResult<CsAgentVO>> => {
|
export const listCsAgent = (query?: CsAgentQuery): AxiosPromise<PageResult<CsAgentVO>> => {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/csAgent/list',
|
url: '/cs/agent/list',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: query
|
params: query
|
||||||
});
|
});
|
||||||
@@ -22,7 +22,7 @@ export const listCsAgent = (query?: CsAgentQuery): AxiosPromise<PageResult<CsAge
|
|||||||
*/
|
*/
|
||||||
export const getCsAgent = (agentId: string | number): AxiosPromise<CsAgentVO> => {
|
export const getCsAgent = (agentId: string | number): AxiosPromise<CsAgentVO> => {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/csAgent/' + agentId,
|
url: '/cs/agent/' + agentId,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -33,7 +33,7 @@ export const getCsAgent = (agentId: string | number): AxiosPromise<CsAgentVO> =>
|
|||||||
*/
|
*/
|
||||||
export const addCsAgent = (data: CsAgentForm) => {
|
export const addCsAgent = (data: CsAgentForm) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/csAgent',
|
url: '/cs/agent',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
@@ -45,7 +45,7 @@ export const addCsAgent = (data: CsAgentForm) => {
|
|||||||
*/
|
*/
|
||||||
export const updateCsAgent = (data: CsAgentForm) => {
|
export const updateCsAgent = (data: CsAgentForm) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/csAgent',
|
url: '/cs/agent',
|
||||||
method: 'put',
|
method: 'put',
|
||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
@@ -58,7 +58,51 @@ export const updateCsAgent = (data: CsAgentForm) => {
|
|||||||
*/
|
*/
|
||||||
export const delCsAgent = (agentId: string | number | Array<string | number>) => {
|
export const delCsAgent = (agentId: string | number | Array<string | number>) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/csAgent/' + agentId,
|
url: '/cs/agent/' + agentId,
|
||||||
method: 'delete'
|
method: 'delete'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客服人员 在线状态汇总
|
||||||
|
*/
|
||||||
|
export const statCsAgent = (): AxiosPromise<{
|
||||||
|
total: number,
|
||||||
|
onlineCount: number,
|
||||||
|
busyCount: number,
|
||||||
|
offlineCount: number
|
||||||
|
}> => {
|
||||||
|
return request({
|
||||||
|
url: '/cs/agent/stat',
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客服人员 重置密码
|
||||||
|
*/
|
||||||
|
export const ResetPwdCsAgent = (agentId: string, password: string = '12345678') => {
|
||||||
|
return request({
|
||||||
|
url: '/cs/agent/resetPwd',
|
||||||
|
method: 'put',
|
||||||
|
data: {
|
||||||
|
agentId: agentId,
|
||||||
|
password: password
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客服人员 批量禁用账号
|
||||||
|
*/
|
||||||
|
export const BatchDisableCsAgent = (agentIds: string[]) => {
|
||||||
|
return request({
|
||||||
|
url: '/cs/agent/batchDisable',
|
||||||
|
method: 'put',
|
||||||
|
data: {
|
||||||
|
agentIds: agentIds,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export interface CsAgentVO {
|
|||||||
/**
|
/**
|
||||||
* 主键(雪花)
|
* 主键(雪花)
|
||||||
*/
|
*/
|
||||||
agentId: string | number;
|
agentId: string;
|
||||||
/**
|
/**
|
||||||
* 客服编号,如 KF-001
|
* 客服编号,如 KF-001
|
||||||
*/
|
*/
|
||||||
@@ -34,18 +34,18 @@ export interface CsAgentVO {
|
|||||||
/**
|
/**
|
||||||
* 在线状态(0离线 1在线 2忙碌)
|
* 在线状态(0离线 1在线 2忙碌)
|
||||||
*/
|
*/
|
||||||
onlineStatus: boolean;
|
onlineStatus: string;
|
||||||
/**
|
/**
|
||||||
* 账号状态(0启用 1禁用)
|
* 账号状态(0启用 1禁用)
|
||||||
*/
|
*/
|
||||||
status: boolean;
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CsAgentForm extends BaseEntity {
|
export interface CsAgentForm extends BaseEntity {
|
||||||
/**
|
/**
|
||||||
* 主键(雪花)
|
* 主键(雪花)
|
||||||
*/
|
*/
|
||||||
agentId?: string | number;
|
agentId?: string;
|
||||||
/**
|
/**
|
||||||
* 客服编号,如 KF-001
|
* 客服编号,如 KF-001
|
||||||
*/
|
*/
|
||||||
@@ -77,11 +77,11 @@ export interface CsAgentForm extends BaseEntity {
|
|||||||
/**
|
/**
|
||||||
* 在线状态(0离线 1在线 2忙碌)
|
* 在线状态(0离线 1在线 2忙碌)
|
||||||
*/
|
*/
|
||||||
onlineStatus?: boolean;
|
onlineStatus?: string;
|
||||||
/**
|
/**
|
||||||
* 账号状态(0启用 1禁用)
|
* 账号状态(0启用 1禁用)
|
||||||
*/
|
*/
|
||||||
status?: boolean;
|
status?: string;
|
||||||
/**
|
/**
|
||||||
* 创建部门
|
* 创建部门
|
||||||
*/
|
*/
|
||||||
@@ -136,11 +136,11 @@ export interface CsAgentQuery extends PageQuery {
|
|||||||
/**
|
/**
|
||||||
* 在线状态(0离线 1在线 2忙碌)
|
* 在线状态(0离线 1在线 2忙碌)
|
||||||
*/
|
*/
|
||||||
onlineStatus?: boolean;
|
onlineStatus?: string;
|
||||||
/**
|
/**
|
||||||
* 账号状态(0启用 1禁用)
|
* 账号状态(0启用 1禁用)
|
||||||
*/
|
*/
|
||||||
status?: boolean;
|
status?: string;
|
||||||
/**
|
/**
|
||||||
* 创建部门
|
* 创建部门
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import type {UserForm, UserQuery, UserVO} from '@/api/system/customer/types';
|
|||||||
import type {PageResult} from '@/api/types';
|
import type {PageResult} from '@/api/types';
|
||||||
import type {AxiosPromise} from '@/utils/api-types';
|
import type {AxiosPromise} from '@/utils/api-types';
|
||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
|
import {blobValidate} from "@/utils/ruoyi";
|
||||||
|
import {saveBlob} from "@/utils/save";
|
||||||
|
import type {LoadingInstance} from "element-plus";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询App用户列表
|
* 查询App用户列表
|
||||||
@@ -10,7 +13,7 @@ import request from '@/utils/request';
|
|||||||
*/
|
*/
|
||||||
export const listUser = (query?: UserQuery): AxiosPromise<PageResult<UserVO>> => {
|
export const listUser = (query?: UserQuery): AxiosPromise<PageResult<UserVO>> => {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/user/list',
|
url: '/customer/user/list',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: query
|
params: query
|
||||||
});
|
});
|
||||||
@@ -20,9 +23,9 @@ export const listUser = (query?: UserQuery): AxiosPromise<PageResult<UserVO>> =>
|
|||||||
* 查询App用户详细
|
* 查询App用户详细
|
||||||
* @param userId
|
* @param userId
|
||||||
*/
|
*/
|
||||||
export const getUser = (userId: string | number): AxiosPromise<UserVO> => {
|
export const getUser = (userId: string): AxiosPromise<UserVO> => {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/user/' + userId,
|
url: '/customer/user/' + userId,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -33,7 +36,7 @@ export const getUser = (userId: string | number): AxiosPromise<UserVO> => {
|
|||||||
*/
|
*/
|
||||||
export const addUser = (data: UserForm) => {
|
export const addUser = (data: UserForm) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/user',
|
url: '/customer/user',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
@@ -45,12 +48,62 @@ export const addUser = (data: UserForm) => {
|
|||||||
*/
|
*/
|
||||||
export const updateUser = (data: UserForm) => {
|
export const updateUser = (data: UserForm) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/user',
|
url: '/customer/user',
|
||||||
method: 'put',
|
method: 'put',
|
||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量禁用App用户
|
||||||
|
* @param userIds
|
||||||
|
*/
|
||||||
|
export const batchDisableUsers = (userIds: string[]) => {
|
||||||
|
return request({
|
||||||
|
url: '/customer/user/batchDisable',
|
||||||
|
method: 'put',
|
||||||
|
data: {
|
||||||
|
userIds: userIds
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 批量导出选择的App用户
|
||||||
|
* @param userIds
|
||||||
|
* @param fileName
|
||||||
|
*/
|
||||||
|
export const exportSelectedUsers = (userIds: string[], fileName: string) => {
|
||||||
|
request({
|
||||||
|
url: '/customer/user/exportSelected',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
userIds: userIds
|
||||||
|
}
|
||||||
|
}).then(async (resp: any) => {
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch((r: any) => {
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改App用户 状态 启用/停用
|
||||||
|
* @param userId
|
||||||
|
* @param status
|
||||||
|
*/
|
||||||
|
export const updateUserStatus = (userId: string, status: '0' | '1') => {
|
||||||
|
return request({
|
||||||
|
url: '/customer/user/changeStatus',
|
||||||
|
method: 'put',
|
||||||
|
data: {
|
||||||
|
status: status,
|
||||||
|
userId: userId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除App用户
|
* 删除App用户
|
||||||
@@ -58,7 +111,25 @@ export const updateUser = (data: UserForm) => {
|
|||||||
*/
|
*/
|
||||||
export const delUser = (userId: string | number | Array<string | number>) => {
|
export const delUser = (userId: string | number | Array<string | number>) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/user/' + userId,
|
url: '/customer/user/' + userId,
|
||||||
method: 'delete'
|
method: 'delete'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* 重置App用户密码
|
||||||
|
* @param userId
|
||||||
|
* @param password
|
||||||
|
*/
|
||||||
|
export const resetPwdUser = (userId: string, password: string) => {
|
||||||
|
return request({
|
||||||
|
url: '/customer/user/resetPwd',
|
||||||
|
method: 'put',
|
||||||
|
data: {
|
||||||
|
userId: userId,
|
||||||
|
password: password
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export interface UserVO {
|
|||||||
/**
|
/**
|
||||||
* 用户ID(雪花ID)
|
* 用户ID(雪花ID)
|
||||||
*/
|
*/
|
||||||
userId: string | number;
|
userId: string;
|
||||||
/**
|
/**
|
||||||
* 用户账号(默认使用手机号)
|
* 用户账号(默认使用手机号)
|
||||||
*/
|
*/
|
||||||
@@ -22,7 +22,7 @@ export interface UserVO {
|
|||||||
/**
|
/**
|
||||||
* 头像 oss_id
|
* 头像 oss_id
|
||||||
*/
|
*/
|
||||||
avatarOssId: string | number;
|
avatarOssId: string;
|
||||||
/**
|
/**
|
||||||
* 头像 URL
|
* 头像 URL
|
||||||
*/
|
*/
|
||||||
@@ -58,7 +58,7 @@ export interface UserVO {
|
|||||||
/**
|
/**
|
||||||
* 账号状态(0正常 1停用)
|
* 账号状态(0正常 1停用)
|
||||||
*/
|
*/
|
||||||
status: boolean;
|
status: string;
|
||||||
/**
|
/**
|
||||||
* 注销时间;停用仍用 status
|
* 注销时间;停用仍用 status
|
||||||
*/
|
*/
|
||||||
@@ -85,7 +85,7 @@ export interface UserForm extends BaseEntity {
|
|||||||
/**
|
/**
|
||||||
* 用户ID(雪花ID)
|
* 用户ID(雪花ID)
|
||||||
*/
|
*/
|
||||||
userId?: string | number;
|
userId?: string;
|
||||||
/**
|
/**
|
||||||
* 用户账号(默认使用手机号)
|
* 用户账号(默认使用手机号)
|
||||||
*/
|
*/
|
||||||
@@ -105,7 +105,7 @@ export interface UserForm extends BaseEntity {
|
|||||||
/**
|
/**
|
||||||
* 头像 oss_id
|
* 头像 oss_id
|
||||||
*/
|
*/
|
||||||
avatarOssId?: string | number;
|
avatarOssId?: string;
|
||||||
/**
|
/**
|
||||||
* 头像 URL
|
* 头像 URL
|
||||||
*/
|
*/
|
||||||
@@ -141,7 +141,7 @@ export interface UserForm extends BaseEntity {
|
|||||||
/**
|
/**
|
||||||
* 账号状态(0正常 1停用)
|
* 账号状态(0正常 1停用)
|
||||||
*/
|
*/
|
||||||
status?: boolean;
|
status?: string;
|
||||||
/**
|
/**
|
||||||
* 注销时间;停用仍用 status
|
* 注销时间;停用仍用 status
|
||||||
*/
|
*/
|
||||||
@@ -158,22 +158,11 @@ export interface UserForm extends BaseEntity {
|
|||||||
* 登录次数
|
* 登录次数
|
||||||
*/
|
*/
|
||||||
loginCount?: number;
|
loginCount?: number;
|
||||||
/**
|
|
||||||
* 创建者
|
|
||||||
*/
|
|
||||||
createBy?: number;
|
|
||||||
/**
|
/**
|
||||||
* 创建时间
|
* 创建时间
|
||||||
*/
|
*/
|
||||||
createTime?: string;
|
createTime?: string;
|
||||||
/**
|
|
||||||
* 更新者
|
|
||||||
*/
|
|
||||||
updateBy?: number;
|
|
||||||
/**
|
|
||||||
* 更新时间
|
|
||||||
*/
|
|
||||||
updateTime?: string;
|
|
||||||
/**
|
/**
|
||||||
* 备注
|
* 备注
|
||||||
*/
|
*/
|
||||||
@@ -181,6 +170,8 @@ export interface UserForm extends BaseEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UserQuery extends PageQuery {
|
export interface UserQuery extends PageQuery {
|
||||||
|
endTime?: string;
|
||||||
|
beginTime?: string;
|
||||||
/**
|
/**
|
||||||
* 用户账号(默认使用手机号)
|
* 用户账号(默认使用手机号)
|
||||||
*/
|
*/
|
||||||
@@ -236,7 +227,7 @@ export interface UserQuery extends PageQuery {
|
|||||||
/**
|
/**
|
||||||
* 账号状态(0正常 1停用)
|
* 账号状态(0正常 1停用)
|
||||||
*/
|
*/
|
||||||
status?: boolean;
|
status?: string;
|
||||||
/**
|
/**
|
||||||
* 注销时间;停用仍用 status
|
* 注销时间;停用仍用 status
|
||||||
*/
|
*/
|
||||||
@@ -253,24 +244,8 @@ export interface UserQuery extends PageQuery {
|
|||||||
* 登录次数
|
* 登录次数
|
||||||
*/
|
*/
|
||||||
loginCount?: number;
|
loginCount?: number;
|
||||||
/**
|
|
||||||
* 创建者
|
|
||||||
*/
|
|
||||||
createBy?: number;
|
|
||||||
/**
|
/**
|
||||||
* 创建时间
|
* 创建时间
|
||||||
*/
|
*/
|
||||||
createTime?: string;
|
createTime?: string;
|
||||||
/**
|
|
||||||
* 更新者
|
|
||||||
*/
|
|
||||||
updateBy?: number;
|
|
||||||
/**
|
|
||||||
* 更新时间
|
|
||||||
*/
|
|
||||||
updateTime?: string;
|
|
||||||
/**
|
|
||||||
* 日期范围参数
|
|
||||||
*/
|
|
||||||
params?: any;
|
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/api/system/dashboard/index.ts
Normal file
16
src/api/system/dashboard/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// 查询部门列表
|
||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
export const dashboardHome = () => {
|
||||||
|
return request({
|
||||||
|
url: '/dashboard/home',
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dashboardTvoc = (range: string = '24h') => {
|
||||||
|
return request({
|
||||||
|
url: '/dashboard/tvoc?range=' + range,
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -2,16 +2,20 @@
|
|||||||
|
|
||||||
// --- 表格工具列链接按钮(全局) ---
|
// --- 表格工具列链接按钮(全局) ---
|
||||||
.data-table .el-button.is-link {
|
.data-table .el-button.is-link {
|
||||||
width: 28px !important;
|
width: unset !important;
|
||||||
height: 28px !important;
|
height: unset !important;
|
||||||
min-width: 28px !important;
|
min-width: 28px !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
border-radius: var(--app-radius-md) !important;
|
border-radius: var(--app-radius-md) !important;
|
||||||
background: var(--app-accent-soft) !important;
|
//background: var(--app-accent-soft) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table .el-button--primary.is-link {
|
||||||
|
color: #4a7cff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table .el-button.is-link + .el-button.is-link {
|
.data-table .el-button.is-link + .el-button.is-link {
|
||||||
margin-left: 4px !important;
|
margin-left: 12px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-wrap {
|
.search-wrap {
|
||||||
@@ -37,11 +41,10 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: 560px;
|
max-height: 560px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition:
|
transition: max-height 0.32s cubic-bezier(0.22, 1, 0.36, 1),
|
||||||
max-height 0.32s cubic-bezier(0.22, 1, 0.36, 1),
|
opacity 0.24s ease,
|
||||||
opacity 0.24s ease,
|
padding-top 0.24s ease,
|
||||||
padding-top 0.24s ease,
|
padding-bottom 0.24s ease;
|
||||||
padding-bottom 0.24s ease;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,9 +76,8 @@
|
|||||||
border-bottom: 2px solid currentColor;
|
border-bottom: 2px solid currentColor;
|
||||||
color: var(--app-text-muted);
|
color: var(--app-text-muted);
|
||||||
transform: rotate(-135deg);
|
transform: rotate(-135deg);
|
||||||
transition:
|
transition: transform 0.24s ease,
|
||||||
transform 0.24s ease,
|
color 0.24s ease;
|
||||||
color 0.24s ease;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const chartRef = ref<HTMLDivElement>();
|
const chartRef = ref<HTMLDivElement>();
|
||||||
// 3. 使用 shallowRef 优化性能,因为 ECharts 实例不需要深层响应式
|
|
||||||
const chartInstance = shallowRef<ECharts | null>(null);
|
const chartInstance = shallowRef<ECharts | null>(null);
|
||||||
let resizeObserver: ResizeObserver | null = null;
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
const initChart = () => {
|
const initChart = () => {
|
||||||
if (!chartRef.value) return;
|
if (!chartRef.value) return;
|
||||||
|
|
||||||
|
// 如果实例已存在,先销毁重建
|
||||||
if (chartInstance.value) {
|
if (chartInstance.value) {
|
||||||
chartInstance.value.dispose();
|
chartInstance.value.dispose();
|
||||||
chartInstance.value = null;
|
chartInstance.value = null;
|
||||||
@@ -50,22 +50,22 @@ const initChart = () => {
|
|||||||
emit('ready', instance);
|
emit('ready', instance);
|
||||||
emit('rendered');
|
emit('rendered');
|
||||||
|
|
||||||
instance.on('click', (params: any) => {
|
instance.on('click', (params: echarts.ECElementEvent) => {
|
||||||
emit('click', params);
|
emit('click', params);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateChart = () => {
|
const updateChart = () => {
|
||||||
if (chartInstance.value) {
|
if (!chartInstance.value) return;
|
||||||
chartInstance.value.setOption(props.options, {
|
// clear: true 清除不存在的系列/坐标轴,切换不同数量数据时防止残留旧图形
|
||||||
notMerge: false,
|
chartInstance.value.setOption(props.options, {
|
||||||
lazyUpdate: false
|
notMerge: false,
|
||||||
});
|
lazyUpdate: false,
|
||||||
emit('rendered');
|
clear: true
|
||||||
}
|
});
|
||||||
|
emit('rendered');
|
||||||
};
|
};
|
||||||
|
|
||||||
// ✅ 核心修复:支持 scale 缩放后重绘
|
|
||||||
const resizeChart = () => {
|
const resizeChart = () => {
|
||||||
if (!chartInstance.value) return;
|
if (!chartInstance.value) return;
|
||||||
chartInstance.value.resize();
|
chartInstance.value.resize();
|
||||||
@@ -74,19 +74,24 @@ const resizeChart = () => {
|
|||||||
const getInstance = () => chartInstance.value;
|
const getInstance = () => chartInstance.value;
|
||||||
|
|
||||||
const exportAsImage = (type: 'png' | 'svg' | 'jpeg' = 'png') => {
|
const exportAsImage = (type: 'png' | 'svg' | 'jpeg' = 'png') => {
|
||||||
return (
|
return chartInstance.value?.getDataURL({
|
||||||
chartInstance.value?.getDataURL({
|
type,
|
||||||
type,
|
pixelRatio: 2,
|
||||||
pixelRatio: 2,
|
backgroundColor: '#fff'
|
||||||
backgroundColor: '#fff'
|
}) || '';
|
||||||
}) || ''
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 深度监听 options 变化自动更新图表
|
||||||
watch(() => props.options, updateChart, {deep: true});
|
watch(() => props.options, updateChart, {deep: true});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(() => initChart());
|
nextTick(() => {
|
||||||
|
if (props.immediate) {
|
||||||
|
initChart();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 开启自适应时监听容器尺寸
|
||||||
if (props.autoResize && chartRef.value) {
|
if (props.autoResize && chartRef.value) {
|
||||||
resizeObserver = new ResizeObserver(resizeChart);
|
resizeObserver = new ResizeObserver(resizeChart);
|
||||||
resizeObserver.observe(chartRef.value);
|
resizeObserver.observe(chartRef.value);
|
||||||
@@ -95,8 +100,11 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
// 【重点修复】顺序:先销毁监听,再销毁图表实例,防止内存泄漏和控制台警告
|
||||||
window.removeEventListener('resize', resizeChart);
|
window.removeEventListener('resize', resizeChart);
|
||||||
resizeObserver?.disconnect();
|
resizeObserver?.disconnect();
|
||||||
|
resizeObserver = null;
|
||||||
|
|
||||||
chartInstance.value?.dispose();
|
chartInstance.value?.dispose();
|
||||||
chartInstance.value = null;
|
chartInstance.value = null;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<p>以上为示例格式</p>
|
<p>以上为示例格式</p>
|
||||||
<p>支持插入图片与链接;回复中可引用设备型号,帮助用户快速对号入座</p>
|
<p>支持插入图片与链接;回复中可引用设备型号,帮助用户快速对号入座</p>
|
||||||
<div class="word-count">{{ wordCount }}/2000字</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form-item
|
<el-form-item
|
||||||
:label="label"
|
:label="label"
|
||||||
:required="required"
|
|
||||||
:show-message="false"
|
:show-message="false"
|
||||||
class="form-item-tip-wrapper"
|
class="form-item-tip-wrapper"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
|
:prop="prop"
|
||||||
>
|
>
|
||||||
<!-- label插槽,支持父组件自定义label -->
|
<!-- label插槽,支持父组件自定义label -->
|
||||||
<template #label>
|
<template #label>
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
v-else-if="type === 'radio'"
|
v-else-if="type === 'radio'"
|
||||||
:model-value="modelValue"
|
:model-value="modelValue"
|
||||||
class="radioGroup"
|
class="radioGroup"
|
||||||
|
style="width: 100%"
|
||||||
@change="$emit('update:modelValue', $event)"
|
@change="$emit('update:modelValue', $event)"
|
||||||
>
|
>
|
||||||
<el-radio
|
<el-radio
|
||||||
@@ -114,20 +115,37 @@
|
|||||||
v-for="item in radioOptions"
|
v-for="item in radioOptions"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:label="item.label"
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
class="checkBox"
|
class="checkBox"
|
||||||
>
|
>
|
||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
|
<!-- ========== 新增密码框 type="password" ========== -->
|
||||||
|
<el-input
|
||||||
|
v-else-if="type === 'password'"
|
||||||
|
:model-value="modelValue"
|
||||||
|
class="inputItem"
|
||||||
|
show-password
|
||||||
|
v-bind="inputAttrs"
|
||||||
|
@input="$emit('update:modelValue', $event)"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<slot name="suffix"></slot>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
|
||||||
<div v-else-if="type === 'switch'" style="width: 100%">
|
<div v-else-if="type === 'switch'" style="width: 100%">
|
||||||
<el-switch
|
<el-switch
|
||||||
:model-value="Boolean(modelValue)"
|
:model-value="modelValue"
|
||||||
|
size="large"
|
||||||
class="custom-switch"
|
class="custom-switch"
|
||||||
|
v-bind="inputAttrs"
|
||||||
style="margin-right: 12px;"
|
style="margin-right: 12px;"
|
||||||
@change="$emit('update:modelValue', $event)"
|
@change="$emit('update:modelValue', $event)"
|
||||||
/>
|
/>
|
||||||
<slot name="switchRight"></slot>
|
<slot name="switchRight"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
<slot v-else-if="type==='default'" name="default"></slot>
|
||||||
<!-- 底部提示文字 -->
|
<!-- 底部提示文字 -->
|
||||||
<div v-if="tip" class="form-item-tip-text">
|
<div v-if="tip" class="form-item-tip-text">
|
||||||
{{ tip }}
|
{{ tip }}
|
||||||
@@ -140,9 +158,14 @@ import {ref} from 'vue'
|
|||||||
import {genFileId} from 'element-plus'
|
import {genFileId} from 'element-plus'
|
||||||
import type {UploadInstance, UploadProps, UploadRawFile} from 'element-plus'
|
import type {UploadInstance, UploadProps, UploadRawFile} from 'element-plus'
|
||||||
|
|
||||||
|
|
||||||
defineOptions({inheritAttrs: false})
|
defineOptions({inheritAttrs: false})
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
prop: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: [String, Number, Array] as PropType<string | number | Array<string | number>>,
|
type: [String, Number, Array] as PropType<string | number | Array<string | number>>,
|
||||||
default: ''
|
default: ''
|
||||||
@@ -178,13 +201,19 @@ const emit = defineEmits(['update:modelValue', 'file-change'])
|
|||||||
|
|
||||||
|
|
||||||
const upload = ref<UploadInstance>()
|
const upload = ref<UploadInstance>()
|
||||||
const defaultImag = "https://ts3.tc.mm.bing.net/th?id=ORMS.48a27ca8353fa47bf0dc8df38854d0e1&pid=Wdp&w=312&h=172&qlt=90&c=1&rs=1&dpr=1&p=0"
|
const defaultImag = import.meta.env.VITE_DEFAULT_USER_IMAGE;
|
||||||
const imageUrl = ref('')
|
const imageUrl = ref('')
|
||||||
if (props.type === 'uploadUserAvatar') {
|
|
||||||
const str = props.modelValue as string;
|
watch(
|
||||||
imageUrl.value = (str.trim() === '' ? defaultImag : props.modelValue as string)
|
() => [props.type, props.modelValue],
|
||||||
console.log(imageUrl.value)
|
([type, val]) => {
|
||||||
}
|
if (type === 'uploadUserAvatar') {
|
||||||
|
const str = val as string
|
||||||
|
imageUrl.value = str.trim() === '' ? defaultImag : str
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
// 保存选中的文件,等表单提交时再传给后端
|
// 保存选中的文件,等表单提交时再传给后端
|
||||||
const avatarFile = ref<File | null>(null)
|
const avatarFile = ref<File | null>(null)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="flex justify-between items-center">
|
||||||
<div class="router_title">{{ route.meta.title }}</div>
|
<div>
|
||||||
<Breadcrumb class="breadcrumb"></Breadcrumb>
|
<div class="router_title">{{ route.meta.title }}</div>
|
||||||
|
<Breadcrumb class="breadcrumb"></Breadcrumb>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<slot name="rightSlot"></slot>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" name="pageCrumb" setup>
|
||||||
import Breadcrumb from '@/components/Breadcrumb/index.vue'
|
import Breadcrumb from '@/components/Breadcrumb/index.vue'
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|||||||
@@ -15,14 +15,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup name="Pagination" lang="ts">
|
<script setup name="Pagination" lang="ts">
|
||||||
import { propTypes } from '@/utils/propTypes';
|
import {propTypes} from '@/utils/propTypes';
|
||||||
import { scrollTo } from '@/utils/scroll-to';
|
import {scrollTo} from '@/utils/scroll-to';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
total: propTypes.number,
|
total: propTypes.number,
|
||||||
page: propTypes.number.def(1),
|
page: propTypes.number.def(1),
|
||||||
limit: propTypes.number.def(20),
|
limit: propTypes.number.def(20),
|
||||||
pageSizes: { type: Array<number>, default: () => [10, 20, 30, 50] },
|
pageSizes: {type: Array<number>, default: () => [10, 20, 30, 50]},
|
||||||
// 移动端页码按钮的数量端默认值5
|
// 移动端页码按钮的数量端默认值5
|
||||||
pagerCount: propTypes.number.def(document.body.clientWidth < 992 ? 5 : 7),
|
pagerCount: propTypes.number.def(document.body.clientWidth < 992 ? 5 : 7),
|
||||||
layout: propTypes.string.def('total, sizes, prev, pager, next, jumper'),
|
layout: propTypes.string.def('total, sizes, prev, pager, next, jumper'),
|
||||||
@@ -49,17 +49,19 @@ const pageSize = computed({
|
|||||||
emit('update:limit', val);
|
emit('update:limit', val);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleSizeChange(val: number) {
|
function handleSizeChange(val: number) {
|
||||||
if (currentPage.value * val > props.total) {
|
if (currentPage.value * val > props.total) {
|
||||||
currentPage.value = 1;
|
currentPage.value = 1;
|
||||||
}
|
}
|
||||||
emit('pagination', { page: currentPage.value, limit: val });
|
emit('pagination', {page: currentPage.value, limit: val});
|
||||||
if (props.autoScroll) {
|
if (props.autoScroll) {
|
||||||
scrollTo(0, 800);
|
scrollTo(0, 800);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCurrentChange(val: number) {
|
function handleCurrentChange(val: number) {
|
||||||
emit('pagination', { page: val, limit: pageSize.value });
|
emit('pagination', {page: val, limit: pageSize.value});
|
||||||
if (props.autoScroll) {
|
if (props.autoScroll) {
|
||||||
scrollTo(0, 800);
|
scrollTo(0, 800);
|
||||||
}
|
}
|
||||||
@@ -73,12 +75,13 @@ function handleCurrentChange(val: number) {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
border-top: 1px solid var(--app-surface-border);
|
//border-top: 1px solid var(--app-surface-border);
|
||||||
|
|
||||||
.el-pagination {
|
.el-pagination {
|
||||||
float: none;
|
float: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-container.hidden {
|
.pagination-container.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: false,
|
||||||
|
default: ''
|
||||||
},
|
},
|
||||||
subTitle: {
|
subTitle: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -13,17 +14,35 @@ const props = defineProps({
|
|||||||
required: false,
|
required: false,
|
||||||
default: '#2563eb'
|
default: '#2563eb'
|
||||||
},
|
},
|
||||||
|
showLine: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="cardNav">
|
<div class="cardNav">
|
||||||
<div :style="{
|
<div class="flex flex-items-center">
|
||||||
backgroundColor: props.lineColor
|
<div
|
||||||
}" class="line"></div>
|
v-if="showLine"
|
||||||
<div class="content">
|
:style="{
|
||||||
<div class="title">{{ props.title }}</div>
|
backgroundColor: props.lineColor
|
||||||
<div v-if="props.subTitle" class="subTitle">{{ props.subTitle }}</div>
|
}"
|
||||||
|
class="line"
|
||||||
|
></div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="title">
|
||||||
|
<!-- 有 title 就显示 span,没有就用 slot -->
|
||||||
|
<span v-if="props.title" style="margin-right: 8px">{{ props.title }}</span>
|
||||||
|
<slot v-else name="title"></slot>
|
||||||
|
<slot name="rightTitle"></slot>
|
||||||
|
</div>
|
||||||
|
<div v-if="props.subTitle" class="subTitle">{{ props.subTitle }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<slot name="rightSlot"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -33,6 +52,7 @@ const props = defineProps({
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
.line {
|
.line {
|
||||||
width: 7px;
|
width: 7px;
|
||||||
@@ -51,9 +71,13 @@ const props = defineProps({
|
|||||||
color: rgba(17, 24, 39, 1);
|
color: rgba(17, 24, 39, 1);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subTitle {
|
.subTitle {
|
||||||
|
margin-top: 8px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
background-color: rgba(0, 0, 0, 0);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
num: String,
|
num: Number,
|
||||||
numColor: {
|
numColor: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '#111827',
|
default: '#111827',
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ function addIframe() {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.app-main {
|
.app-main {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -67,6 +66,7 @@ function addIframe() {
|
|||||||
|
|
||||||
.app-main:not(.with-fixed-header) {
|
.app-main:not(.with-fixed-header) {
|
||||||
min-height: calc(100vh - 64px);
|
min-height: calc(100vh - 64px);
|
||||||
|
max-height: calc(100vh - 64px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-main.with-tags-view:not(.with-fixed-header) {
|
.app-main.with-tags-view:not(.with-fixed-header) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sidebar-shell" :class="{ 'has-logo': showLogo }" :style="menuStyle">
|
<div class="sidebar-shell" :class="{ 'has-logo': showLogo }" :style="menuStyle">
|
||||||
<logo v-if="showLogo" :collapse="isCollapse" />
|
<logo v-if="showLogo" :collapse="isCollapse"/>
|
||||||
<el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper">
|
<el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper">
|
||||||
<transition :enter-active-class="animateConfig.menuSearchAnimate.enter" mode="out-in">
|
<transition :enter-active-class="animateConfig.menuSearchAnimate.enter" mode="out-in">
|
||||||
<el-menu
|
<el-menu
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
:popper-offset="12"
|
:popper-offset="12"
|
||||||
mode="vertical"
|
mode="vertical"
|
||||||
>
|
>
|
||||||
<sidebar-item v-for="(r, index) in sidebarRouters" :key="r.path + index" :item="r" :base-path="r.path" />
|
<sidebar-item v-for="(r, index) in sidebarRouters" :key="r.path + index" :base-path="r.path" :item="r"/>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</transition>
|
</transition>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
@@ -19,26 +19,34 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouteRecordRaw } from 'vue-router';
|
import {RouteRecordRaw} from 'vue-router';
|
||||||
import animateConfig from '@/animate';
|
import animateConfig from '@/animate';
|
||||||
import { useAppStore } from '@/store/modules/app';
|
import {useAppStore} from '@/store/modules/app';
|
||||||
import { usePermissionStore } from '@/store/modules/permission';
|
import {usePermissionStore} from '@/store/modules/permission';
|
||||||
import { useSettingsStore } from '@/store/modules/settings';
|
import {useSettingsStore} from '@/store/modules/settings';
|
||||||
import Logo from './Logo.vue';
|
import Logo from './Logo.vue';
|
||||||
import SidebarItem from './SidebarItem.vue';
|
import SidebarItem from './SidebarItem.vue';
|
||||||
|
import {useUserStore} from "@/store/modules/user";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
const permissionStore = usePermissionStore();
|
const permissionStore = usePermissionStore();
|
||||||
const sidebarRouters = computed<RouteRecordRaw[]>(() => permissionStore.getSidebarRoutes());
|
const sidebarRouters = computed<RouteRecordRaw[]>(() => {
|
||||||
|
let list = permissionStore.getSidebarRoutes();
|
||||||
|
if (userStore.roles.includes('customer_service')) {
|
||||||
|
list = list.filter((item) => item.path !== '')
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
});
|
||||||
const showLogo = computed(() => settingsStore.sidebarLogo);
|
const showLogo = computed(() => settingsStore.sidebarLogo);
|
||||||
const sideTheme = computed(() => settingsStore.sideTheme);
|
const sideTheme = computed(() => settingsStore.sideTheme);
|
||||||
const theme = computed(() => settingsStore.theme);
|
const theme = computed(() => settingsStore.theme);
|
||||||
const isCollapse = computed(() => !appStore.sidebar.opened);
|
const isCollapse = computed(() => !appStore.sidebar.opened);
|
||||||
|
|
||||||
const activeMenu = computed(() => {
|
const activeMenu = computed(() => {
|
||||||
const { meta, path } = route;
|
const {meta, path} = route;
|
||||||
// if set path, the sidebar will highlight the path you set
|
// if set path, the sidebar will highlight the path you set
|
||||||
if (meta.activeMenu) {
|
if (meta.activeMenu) {
|
||||||
return meta.activeMenu;
|
return meta.activeMenu;
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { to as tos } from 'await-to-js';
|
import {to as tos} from 'await-to-js';
|
||||||
import { ElMessage } from 'element-plus/es';
|
import {ElMessage} from 'element-plus/es';
|
||||||
import * as NProgressModule from 'nprogress';
|
import * as NProgressModule from 'nprogress';
|
||||||
import 'nprogress/nprogress.css';
|
import 'nprogress/nprogress.css';
|
||||||
import { usePermissionStore } from '@/store/modules/permission';
|
import {usePermissionStore} from '@/store/modules/permission';
|
||||||
import { useSettingsStore } from '@/store/modules/settings';
|
import {useSettingsStore} from '@/store/modules/settings';
|
||||||
import { useUserStore } from '@/store/modules/user';
|
import {useUserStore} from '@/store/modules/user';
|
||||||
import { getToken } from '@/utils/auth';
|
import {getToken} from '@/utils/auth';
|
||||||
import { isHandledRequestError, isRelogin } from '@/utils/request';
|
import {isHandledRequestError, isRelogin} from '@/utils/request';
|
||||||
import { isHttp, isPathMatch } from '@/utils/validate';
|
import {isHttp, isPathMatch} from '@/utils/validate';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
|
|
||||||
const NProgress = ('default' in NProgressModule ? NProgressModule.default : NProgressModule) as typeof NProgressModule;
|
const NProgress = ('default' in NProgressModule ? NProgressModule.default : NProgressModule) as typeof NProgressModule;
|
||||||
|
|
||||||
NProgress.configure({ showSpinner: false });
|
NProgress.configure({showSpinner: false});
|
||||||
const whiteList = ['/login', '/register', '/social-callback', '/register*', '/register/*'];
|
const whiteList = ['/login', '/register', '/social-callback', '/register*', '/register/*'];
|
||||||
|
|
||||||
const isWhiteList = (path: string) => {
|
const isWhiteList = (path: string) => {
|
||||||
@@ -26,20 +26,22 @@ router.beforeEach(async (to, from) => {
|
|||||||
/* has token*/
|
/* has token*/
|
||||||
if (to.path === '/login') {
|
if (to.path === '/login') {
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
return { path: '/' };
|
return {path: '/'};
|
||||||
} else if (isWhiteList(to.path)) {
|
} else if (isWhiteList(to.path)) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
if (useUserStore().roles.length === 0) {
|
if (useUserStore().roles.length === 0) {
|
||||||
isRelogin.show = true;
|
isRelogin.show = true;
|
||||||
// 判断当前用户是否已拉取完user_info信息
|
// 判断当前用户是否已拉取完user_info信息
|
||||||
const [err] = await tos(useUserStore().getInfo());
|
const [err, res] = await tos(useUserStore().getInfo());
|
||||||
|
console.log(res);
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
await useUserStore().logout();
|
await useUserStore().logout();
|
||||||
if (!isHandledRequestError(err)) {
|
if (!isHandledRequestError(err)) {
|
||||||
ElMessage.error(err instanceof Error ? err.message : String(err));
|
ElMessage.error(err instanceof Error ? err.message : String(err));
|
||||||
}
|
}
|
||||||
return { path: '/' };
|
return {path: '/'};
|
||||||
} else {
|
} else {
|
||||||
isRelogin.show = false;
|
isRelogin.show = false;
|
||||||
const accessRoutes = await usePermissionStore().generateRoutes();
|
const accessRoutes = await usePermissionStore().generateRoutes();
|
||||||
@@ -50,6 +52,21 @@ router.beforeEach(async (to, from) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
// hack方法 确保addRoutes已完成
|
// hack方法 确保addRoutes已完成
|
||||||
|
if (res.data.roles.includes('customer_service')) {
|
||||||
|
if (to.path === '/index') {
|
||||||
|
return {
|
||||||
|
path: '/conversation/conversationList',
|
||||||
|
replace: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 客服登录默认首页:如果原本跳转目标是根路径 /,也跳会话列表
|
||||||
|
if (to.path === '/') {
|
||||||
|
return {
|
||||||
|
path: '/conversation/conversationList',
|
||||||
|
replace: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
path: to.path,
|
path: to.path,
|
||||||
replace: true,
|
replace: true,
|
||||||
|
|||||||
@@ -65,10 +65,10 @@ export const constantRoutes: RouteRecordRaw[] = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
redirect: '/index.vue',
|
redirect: '/index',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '/index.vue',
|
path: '/index',
|
||||||
component: () => import('@/views/index/index.vue'),
|
component: () => import('@/views/index/index.vue'),
|
||||||
name: 'Index',
|
name: 'Index',
|
||||||
meta: {title: '首页', icon: 'dashboard', affix: true}
|
meta: {title: '首页', icon: 'dashboard', affix: true}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { to } from 'await-to-js';
|
import {to} from 'await-to-js';
|
||||||
import { defineStore } from 'pinia';
|
import {defineStore} from 'pinia';
|
||||||
import { ref } from 'vue';
|
import {ref} from 'vue';
|
||||||
import type { UserInfo } from '@/api/system/user/types';
|
import type {UserInfo} from '@/api/system/user/types';
|
||||||
import type { LoginData, LoginResult } from '@/api/types';
|
import type {LoginData, LoginResult} from '@/api/types';
|
||||||
import type { RuoYiAjaxResult } from '@/utils/api-types';
|
import type {RuoYiAjaxResult} from '@/utils/api-types';
|
||||||
import { getInfo as getUserInfo, login as loginApi, logout as logoutApi } from '@/api/login';
|
import {getInfo as getUserInfo, login as loginApi, logout as logoutApi} from '@/api/login';
|
||||||
import defAva from '@/assets/images/profile.jpg';
|
import defAva from '@/assets/images/profile.jpg';
|
||||||
import { getToken, removeToken, setToken } from '@/utils/auth';
|
import {getToken, removeToken, setToken} from '@/utils/auth';
|
||||||
|
|
||||||
export const useUserStore = defineStore('user', () => {
|
export const useUserStore = defineStore('user', () => {
|
||||||
const token = ref(getToken());
|
const token = ref(getToken());
|
||||||
@@ -37,7 +37,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
const getInfo = async (): Promise<void> => {
|
const getInfo = async (): Promise<RuoYiAjaxResult<UserInfo>> => {
|
||||||
const [err, res] = await to(getUserInfo());
|
const [err, res] = await to(getUserInfo());
|
||||||
if (res) {
|
if (res) {
|
||||||
const data = (res as RuoYiAjaxResult<UserInfo>).data;
|
const data = (res as RuoYiAjaxResult<UserInfo>).data;
|
||||||
@@ -58,7 +58,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
nickname.value = user.nickName;
|
nickname.value = user.nickName;
|
||||||
avatar.value = profile;
|
avatar.value = profile;
|
||||||
userId.value = user.userId;
|
userId.value = user.userId;
|
||||||
return Promise.resolve();
|
return Promise.resolve(res);
|
||||||
}
|
}
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
};
|
};
|
||||||
|
|||||||
15
src/types/env.d.ts
vendored
15
src/types/env.d.ts
vendored
@@ -1,11 +1,13 @@
|
|||||||
declare module '*.vue' {
|
declare module '*.vue' {
|
||||||
import { DefineComponent } from 'vue';
|
import {DefineComponent} from 'vue';
|
||||||
const Component: DefineComponent<{}, {}, any>;
|
const Component: DefineComponent<{}, {}, any>;
|
||||||
export default Component;
|
export default Component;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'virtual:svg-icons-register' {}
|
declare module 'virtual:svg-icons-register' {
|
||||||
declare module 'virtual:*' {}
|
}
|
||||||
|
declare module 'virtual:*' {
|
||||||
|
}
|
||||||
|
|
||||||
// 环境变量
|
// 环境变量
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
@@ -24,8 +26,15 @@ interface ImportMetaEnv {
|
|||||||
VITE_APP_CLIENT_ID: string;
|
VITE_APP_CLIENT_ID: string;
|
||||||
VITE_APP_MESSAGE_ENABLED: string;
|
VITE_APP_MESSAGE_ENABLED: string;
|
||||||
VITE_APP_MESSAGE_TRANSPORT: string;
|
VITE_APP_MESSAGE_TRANSPORT: string;
|
||||||
|
/** 默认图片 */
|
||||||
|
VITE_DEFAULT_USER_IMAGE?: string;
|
||||||
|
/** 本机环境 */
|
||||||
|
VITE_LOCALHOUSET?: string;
|
||||||
|
/** 基础地址 */
|
||||||
|
VITE_BASE_API?: string;
|
||||||
VITE_APP_MESSAGE_PATH: string;
|
VITE_APP_MESSAGE_PATH: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv;
|
readonly env: ImportMetaEnv;
|
||||||
// readonly glob: any;
|
// readonly glob: any;
|
||||||
|
|||||||
18
src/utils/phone.ts
Normal file
18
src/utils/phone.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* 隐藏手机号
|
||||||
|
* @params {number} phone
|
||||||
|
* @return {string}
|
||||||
|
* @example 136****9023
|
||||||
|
* */
|
||||||
|
export function hiddenPhone(phone: string): string {
|
||||||
|
if (phone.length < 11) {
|
||||||
|
return phone
|
||||||
|
}
|
||||||
|
let account = phone;
|
||||||
|
const first = account.slice(0, 3);
|
||||||
|
const last = account.slice(phone.length - 4, phone.length);
|
||||||
|
if (first === '' || last === '') {
|
||||||
|
return '---'
|
||||||
|
}
|
||||||
|
return `${first}****${last}`;
|
||||||
|
}
|
||||||
9
src/utils/reg.ts
Normal file
9
src/utils/reg.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* 校验中国大陆手机号
|
||||||
|
* @param phone 手机号字符串
|
||||||
|
*/
|
||||||
|
export function validatePhone(phone: string): boolean {
|
||||||
|
if (!phone) return false
|
||||||
|
const reg = /^1[3-9]\d{9}$/
|
||||||
|
return reg.test(phone.trim())
|
||||||
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import type { LoadingInstance } from 'element-plus';
|
import type {LoadingInstance} from 'element-plus';
|
||||||
import axiosModule from 'axios';
|
import axiosModule from 'axios';
|
||||||
import { HttpStatus } from '@/enums/RespEnum';
|
import {HttpStatus} from '@/enums/RespEnum';
|
||||||
import { getLanguage } from '@/lang';
|
import {getLanguage} from '@/lang';
|
||||||
import cache from '@/plugins/cache';
|
import cache from '@/plugins/cache';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
import { useUserStore } from '@/store/modules/user';
|
import {useUserStore} from '@/store/modules/user';
|
||||||
import { getToken } from '@/utils/auth';
|
import {getToken} from '@/utils/auth';
|
||||||
import { decryptBase64, decryptWithAes, encryptBase64, encryptWithAes, generateAesKey } from '@/utils/crypto';
|
import {decryptBase64, decryptWithAes, encryptBase64, encryptWithAes, generateAesKey} from '@/utils/crypto';
|
||||||
import { errorCode } from '@/utils/errorCode';
|
import {errorCode} from '@/utils/errorCode';
|
||||||
import { decrypt, encrypt } from '@/utils/jsencrypt';
|
import {decrypt, encrypt} from '@/utils/jsencrypt';
|
||||||
import { blobValidate, tansParams } from '@/utils/ruoyi';
|
import {blobValidate, tansParams} from '@/utils/ruoyi';
|
||||||
import { saveBlob } from '@/utils/save';
|
import {saveBlob} from '@/utils/save';
|
||||||
|
|
||||||
/** axios 1.13 + TS6:默认导出在类型上会被解析为不可调用的 export= 形态 */
|
/** axios 1.13 + TS6:默认导出在类型上会被解析为不可调用的 export= 形态 */
|
||||||
const axios = axiosModule as any;
|
const axios = axiosModule as any;
|
||||||
@@ -18,7 +18,7 @@ const axios = axiosModule as any;
|
|||||||
const encryptHeader = 'encrypt-key';
|
const encryptHeader = 'encrypt-key';
|
||||||
let downloadLoadingInstance: LoadingInstance | undefined;
|
let downloadLoadingInstance: LoadingInstance | undefined;
|
||||||
// 是否显示重新登录
|
// 是否显示重新登录
|
||||||
export const isRelogin = { show: false };
|
export const isRelogin = {show: false};
|
||||||
|
|
||||||
function createHandledError(message: string) {
|
function createHandledError(message: string) {
|
||||||
const error = new Error(message) as Error & { isHandled?: boolean };
|
const error = new Error(message) as Error & { isHandled?: boolean };
|
||||||
@@ -205,44 +205,44 @@ service.interceptors.response.use(
|
|||||||
if (code === 401) {
|
if (code === 401) {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
if (!isRelogin.show) {
|
if (!isRelogin.show) {
|
||||||
isRelogin.show = true;
|
isRelogin.show = true;
|
||||||
ElMessageBox.confirm(
|
ElMessageBox.confirm(
|
||||||
"登录状态已过期,您可以继续留在该页面,或者重新登录",
|
"登录状态已过期,您可以继续留在该页面,或者重新登录",
|
||||||
"系统提示",
|
"系统提示",
|
||||||
{
|
{
|
||||||
confirmButtonText: "重新登录",
|
confirmButtonText: "重新登录",
|
||||||
cancelButtonText: "取消",
|
cancelButtonText: "取消",
|
||||||
type: "warning",
|
type: "warning",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
isRelogin.show = false;
|
isRelogin.show = false;
|
||||||
useUserStore()
|
useUserStore()
|
||||||
.logout()
|
.logout()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
router.replace({
|
router.replace({
|
||||||
path: "/login",
|
path: "/login",
|
||||||
query: {
|
query: {
|
||||||
redirect: encodeURIComponent(
|
redirect: encodeURIComponent(
|
||||||
router.currentRoute.value.fullPath || "/",
|
router.currentRoute.value.fullPath || "/",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
isRelogin.show = false;
|
isRelogin.show = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Promise.reject('无效的会话,或者会话已过期,请重新登录。');
|
return Promise.reject('无效的会话,或者会话已过期,请重新登录。');
|
||||||
} else if (code === HttpStatus.SERVER_ERROR) {
|
} else if (code === HttpStatus.SERVER_ERROR) {
|
||||||
ElMessage({ message: msg, type: 'error' });
|
ElMessage({message: msg, type: 'error'});
|
||||||
return Promise.reject(createHandledError(msg));
|
return Promise.reject(createHandledError(msg));
|
||||||
} else if (code === HttpStatus.WARN) {
|
} else if (code === HttpStatus.WARN) {
|
||||||
ElMessage({ message: msg, type: 'warning' });
|
ElMessage({message: msg, type: 'warning'});
|
||||||
return Promise.reject(createHandledError(msg));
|
return Promise.reject(createHandledError(msg));
|
||||||
} else if (code !== HttpStatus.SUCCESS) {
|
} else if (code !== HttpStatus.SUCCESS) {
|
||||||
ElNotification.error({ title: msg });
|
ElNotification.error({title: msg});
|
||||||
return Promise.reject(createHandledError(msg));
|
return Promise.reject(createHandledError(msg));
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve(res.data);
|
return Promise.resolve(res.data);
|
||||||
@@ -250,11 +250,12 @@ service.interceptors.response.use(
|
|||||||
},
|
},
|
||||||
async (error: any) => {
|
async (error: any) => {
|
||||||
const message = (await extractErrorMessage(error)) || errorCode['default'];
|
const message = (await extractErrorMessage(error)) || errorCode['default'];
|
||||||
ElMessage({ message: message, type: 'error', duration: 5 * 1000 });
|
ElMessage({message: message, type: 'error', duration: 5 * 1000});
|
||||||
error.isHandled = true;
|
error.isHandled = true;
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 通用下载方法
|
// 通用下载方法
|
||||||
export function download(url: string, params: any, fileName: string) {
|
export function download(url: string, params: any, fileName: string) {
|
||||||
downloadLoadingInstance = ElLoading.service({
|
downloadLoadingInstance = ElLoading.service({
|
||||||
@@ -263,34 +264,35 @@ export function download(url: string, params: any, fileName: string) {
|
|||||||
});
|
});
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return service
|
return service
|
||||||
.post(url, params, {
|
.post(url, params, {
|
||||||
transformRequest: [
|
transformRequest: [
|
||||||
(params: any) => {
|
(params: any) => {
|
||||||
return tansParams(params);
|
return tansParams(params);
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
headers: {"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
responseType: "blob",
|
responseType: "blob",
|
||||||
})
|
})
|
||||||
.then(async (resp: any) => {
|
.then(async (resp: any) => {
|
||||||
const isLogin = blobValidate(resp);
|
const isLogin = blobValidate(resp);
|
||||||
if (isLogin) {
|
if (isLogin) {
|
||||||
const blob = new Blob([resp]);
|
const blob = new Blob([resp]);
|
||||||
saveBlob(blob, fileName);
|
saveBlob(blob, fileName);
|
||||||
} else {
|
} else {
|
||||||
const blob = new Blob([resp]);
|
const blob = new Blob([resp]);
|
||||||
const resText = await blob.text();
|
const resText = await blob.text();
|
||||||
const rspObj = JSON.parse(resText);
|
const rspObj = JSON.parse(resText);
|
||||||
const errMsg =
|
const errMsg =
|
||||||
errorCode[rspObj.code] || rspObj.msg || errorCode["default"];
|
errorCode[rspObj.code] || rspObj.msg || errorCode["default"];
|
||||||
ElMessage.error(errMsg);
|
ElMessage.error(errMsg);
|
||||||
}
|
}
|
||||||
downloadLoadingInstance?.close();
|
downloadLoadingInstance?.close();
|
||||||
})
|
})
|
||||||
.catch((r: any) => {
|
.catch((r: any) => {
|
||||||
console.error(r);
|
console.error(r);
|
||||||
downloadLoadingInstance?.close();
|
downloadLoadingInstance?.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出 axios 实例
|
// 导出 axios 实例
|
||||||
export default service;
|
export default service;
|
||||||
|
|||||||
23
src/utils/richtext.ts
Normal file
23
src/utils/richtext.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* 富文本转预览摘要:img替换[图片],a替换[链接],去除标签后截断文本
|
||||||
|
* @param html 原始富文本
|
||||||
|
* @param maxLen 最大展示字符数
|
||||||
|
* @returns 处理后的纯文本摘要
|
||||||
|
*/
|
||||||
|
export function getHtmlSummary(html: string, maxLen = 80): string {
|
||||||
|
if (!html) return ''
|
||||||
|
let str = html
|
||||||
|
// 替换图片标签为 [图片]
|
||||||
|
str = str.replace(/<img[^>]*>/gi, ' [图片] ')
|
||||||
|
// 替换a链接标签为 [链接]
|
||||||
|
str = str.replace(/<a\b[^>]*>(.*?)<\/a>/gi, ' [链接] ')
|
||||||
|
// 移除剩余所有html标签
|
||||||
|
str = str.replace(/<[^>]+>/g, '')
|
||||||
|
// 去除多余换行空格
|
||||||
|
str = str.replace(/\s+/g, ' ').trim()
|
||||||
|
|
||||||
|
if (str.length > maxLen) {
|
||||||
|
return str.slice(0, maxLen) + '...'
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
11
src/views/Customer/index.vue
Normal file
11
src/views/Customer/index.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="Customer">Customer</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,21 +1,40 @@
|
|||||||
<script lang="ts" name="activeDevices" setup>
|
<script lang="ts" name="activeDevices" setup>
|
||||||
import Echarts from '@/components/Echarts/index.vue';
|
import Echarts from '@/components/Echarts/index.vue';
|
||||||
|
|
||||||
const options = {
|
const props = defineProps<{
|
||||||
xAxis: {
|
week: number;
|
||||||
type: 'category',
|
daily: number;
|
||||||
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
points: Array<{ count: number, label: string, date: string }>
|
||||||
},
|
}>()
|
||||||
yAxis: {
|
const options = computed(() => {
|
||||||
type: 'value'
|
const data = props.points.map(item => {
|
||||||
},
|
return {
|
||||||
series: [
|
name: item.label,
|
||||||
{
|
value: item.count,
|
||||||
data: [120, 200, 150, 80, 70, 110, 130],
|
|
||||||
type: 'bar'
|
|
||||||
}
|
}
|
||||||
]
|
})
|
||||||
};
|
return {
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
top: '12%',
|
||||||
|
bottom: '18%',
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: data.map(i => i.name)
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: data.map(item => item.value),
|
||||||
|
type: 'bar'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -26,11 +45,11 @@ const options = {
|
|||||||
<div class="actions">
|
<div class="actions">
|
||||||
<div>
|
<div>
|
||||||
<div class="title">本周新增</div>
|
<div class="title">本周新增</div>
|
||||||
<div class="num">3005</div>
|
<div class="num">{{ props.week }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="title">日均激活</div>
|
<div class="title">日均激活</div>
|
||||||
<div class="num day">573</div>
|
<div class="num day">{{ props.daily }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,32 +2,34 @@
|
|||||||
import {ref} from 'vue';
|
import {ref} from 'vue';
|
||||||
import {ElProgress} from 'element-plus';
|
import {ElProgress} from 'element-plus';
|
||||||
|
|
||||||
const list = ref([
|
|
||||||
{
|
const props = defineProps<{
|
||||||
areaName: '华东区',
|
total: number,
|
||||||
num: '3002台',
|
items: Array<{ count: number, name: string, percent: number }>
|
||||||
percent: 23,
|
}>()
|
||||||
},
|
const list = computed(() => {
|
||||||
{
|
return props.items.map(item => {
|
||||||
areaName: '华北区',
|
return {
|
||||||
num: '8002台',
|
areaName: item.name,
|
||||||
percent: 50,
|
num: item.count,
|
||||||
},
|
percent: item.percent,
|
||||||
])
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="Box">
|
<div class="Box">
|
||||||
<div class="NavBox">
|
<div class="NavBox">
|
||||||
<div class="TypeNavBox">区域设备分布</div>
|
<div class="TypeNavBox">区域设备分布</div>
|
||||||
<div class="subTypeNavBox">共 12,860 台在线设备</div>
|
<div class="subTypeNavBox">共 {{ props.total }} 台在线设备</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ChartBox">
|
<div class="ChartBox">
|
||||||
<div v-for="(item,index) in list" :key="index" style="margin-bottom: 18px">
|
<div v-for="(item,index) in list" :key="index" style="margin-bottom: 18px">
|
||||||
<div class="flex justify-between progressInfo">
|
<div class="flex justify-between progressInfo">
|
||||||
<div>{{ item.areaName }}</div>
|
<div>{{ item.areaName }}</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="num">{{ item.num }}</div>
|
<div class="num">{{ item.num }}台</div>
|
||||||
<div class="percent">{{ item.percent }}%</div>
|
<div class="percent">{{ item.percent }}%</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,46 +1,71 @@
|
|||||||
<script lang="ts" name="Cards" setup>
|
<script lang="ts" name="Cards" setup>
|
||||||
const cardList = ref([
|
import {computed} from 'vue'
|
||||||
|
|
||||||
|
// 定义 props:只接收 num、rate 数组,父组件只传这两个字段
|
||||||
|
export interface CardItemProps {
|
||||||
|
num: number
|
||||||
|
rate: string,
|
||||||
|
value: listValue
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
list: CardItemProps[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
type listValue = 'envDevice' | 'online' | 'avgTvoc' | 'todayActivated'
|
||||||
|
const cardStaticConfig = ref([
|
||||||
{
|
{
|
||||||
title: '环境数据',
|
title: '环境数据',
|
||||||
num: 12.3,
|
|
||||||
unit: '台',
|
unit: '台',
|
||||||
rate: '32.2%',
|
|
||||||
upRate: true,
|
upRate: true,
|
||||||
circleColor: "#2563eb",
|
circleColor: "#2563eb",
|
||||||
color: "rgba(22, 163, 74, 1)",
|
color: "rgba(22, 163, 74, 1)",
|
||||||
rateColor: "rgba(22, 163, 74, 0.1)",
|
rateColor: "rgba(22, 163, 74, 0.1)",
|
||||||
|
value: 'envDevice',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '设备在线率',
|
title: '设备在线率',
|
||||||
num: 0.08,
|
|
||||||
unit: '台',
|
unit: '台',
|
||||||
rate: '98.6%',
|
|
||||||
upRate: false,
|
upRate: false,
|
||||||
circleColor: "#6366f1",
|
circleColor: "#6366f1",
|
||||||
color: "#ed3822",
|
color: "#ed3822",
|
||||||
rateColor: "rgb(217 80 64 / 0.27)",
|
rateColor: "rgb(217 80 64 / 0.27)",
|
||||||
|
value: 'online',
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '平均TVOC原始值',
|
title: '平均TVOC原始值',
|
||||||
num: 0.08,
|
|
||||||
unit: '台',
|
unit: '台',
|
||||||
rate: '98.6%',
|
|
||||||
upRate: false,
|
upRate: false,
|
||||||
circleColor: "#6366f1",
|
circleColor: "#6366f1",
|
||||||
color: "#ed3822",
|
color: "#ed3822",
|
||||||
rateColor: "rgb(217 80 64 / 0.27)",
|
rateColor: "rgb(217 80 64 / 0.27)",
|
||||||
|
value: 'avgTvoc',
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '今日设备激活',
|
title: '今日设备激活',
|
||||||
num: 0.08,
|
|
||||||
unit: '台',
|
unit: '台',
|
||||||
rate: '98.6%',
|
|
||||||
upRate: false,
|
upRate: false,
|
||||||
circleColor: "#6366f1",
|
circleColor: "#6366f1",
|
||||||
color: "#ed3822",
|
color: "#ed3822",
|
||||||
rateColor: "rgb(217 80 64 / 0.27)",
|
rateColor: "rgb(217 80 64 / 0.27)",
|
||||||
|
value: 'todayActivated',
|
||||||
|
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
// computed 合并:父传入的 num、rate + 子静态配置
|
||||||
|
const cardList = computed(() => {
|
||||||
|
return props.list.map((item, index) => {
|
||||||
|
const find = cardStaticConfig.value.find(item2 => item2.value === item.value)
|
||||||
|
return {
|
||||||
|
...find,
|
||||||
|
num: item.num,
|
||||||
|
rate: item.rate !== null ? Number(item.rate) : null,
|
||||||
|
upRate: item.rate !== null ? Number(item.rate) > 0 : false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -51,13 +76,19 @@ const cardList = ref([
|
|||||||
<i :style="{backgroundColor:item.circleColor}" class="circle"></i>
|
<i :style="{backgroundColor:item.circleColor}" class="circle"></i>
|
||||||
<span>{{ item.title }}</span>
|
<span>{{ item.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div :style="{backgroundColor:item.rateColor}" class="cardRate">
|
<div v-if="item.rate !== null" :class="{
|
||||||
<span v-if="item.upRate" :style="{color:item.color}" class="up">+{{ item.rate }}</span>
|
upRate: item.upRate,
|
||||||
<span v-else :style="{color:item.color}" class="down">-{{ item.rate }}</span>
|
downRate: !item.upRate,
|
||||||
|
}" class="cardRate">
|
||||||
|
<span v-if="item.upRate" class="up">+{{ item.rate }}%</span>
|
||||||
|
<span v-else class="down">-{{ item.rate }}%</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="cardRate uncardRate">
|
||||||
|
<span>未换算</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cardContent">
|
<div class="cardContent">
|
||||||
<div class="cardNum">{{ item.num }}</div>
|
<div class="cardNum">{{ item.num || 0 }}</div>
|
||||||
<span>{{ item.unit }}</span>
|
<span>{{ item.unit }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,7 +117,7 @@ const cardList = ref([
|
|||||||
.cardTitle {
|
.cardTitle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100px;
|
min-width: 120px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
background-color: rgba(0, 0, 0, 0);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
@@ -107,20 +138,25 @@ const cardList = ref([
|
|||||||
width: 52px;
|
width: 52px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background-color: rgba(232, 247, 238, 1);
|
|
||||||
|
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
color: rgba(22, 163, 74, 1);
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
&.uncardRate {
|
||||||
|
background-color: rgba(241, 245, 249);
|
||||||
|
color: #64748B;
|
||||||
|
}
|
||||||
|
|
||||||
.up {
|
&.upRate {
|
||||||
|
background-color: rgba(232, 247, 238, 1);
|
||||||
color: rgba(22, 163, 74, 1);
|
color: rgba(22, 163, 74, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.down {
|
&.downRate {
|
||||||
color: rgba(22, 163, 74, 1);
|
background-color: rgb(217 80 64 / 0.27);
|
||||||
|
color: #ed3822;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,6 +173,8 @@ const cardList = ref([
|
|||||||
background-color: rgba(0, 0, 0, 0);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
color: rgba(17, 24, 39, 1);
|
color: rgba(17, 24, 39, 1);
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
|
|||||||
@@ -1,44 +1,90 @@
|
|||||||
<script lang="ts" name="Tvoc" setup>
|
<script lang="ts" name="Tvoc" setup>
|
||||||
import Echarts from '@/components/Echarts/index.vue';
|
import Echarts from '@/components/Echarts/index.vue';
|
||||||
|
import {dashboardTvoc} from "@/api/system/dashboard";
|
||||||
const options = {
|
import {ref, computed} from 'vue';
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value'
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
data: [150, 230, 224, 218, 135, 147, 260],
|
|
||||||
type: 'line'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const list = [
|
const list = [
|
||||||
{
|
{label: '24小时', value: 24},
|
||||||
label: '24小时',
|
{label: '7天', value: 7},
|
||||||
value: 24,
|
{label: '30天', value: 30},
|
||||||
},
|
];
|
||||||
{
|
|
||||||
label: '7天',
|
|
||||||
value: 7,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '30天',
|
|
||||||
value: 30,
|
|
||||||
},
|
|
||||||
// +30 300, 99/1000 69, 59/20g/500
|
|
||||||
]
|
|
||||||
const activeDay = ref(24);
|
const activeDay = ref(24);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const dayEnum = {
|
||||||
|
"24": '24h',
|
||||||
|
'7': '7d',
|
||||||
|
'30': '30d'
|
||||||
|
}
|
||||||
|
const points = ref<any[]>([])
|
||||||
|
|
||||||
|
const options = computed(() => {
|
||||||
|
const xData = points.value.map(item => item.time)
|
||||||
|
const yData = points.value.map(item => item.value)
|
||||||
|
return {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
top: '12%',
|
||||||
|
bottom: '18%',
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: xData
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: 'TVOC'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: yData,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function changeActive(val: number) {
|
function changeActive(val: number) {
|
||||||
activeDay.value = val;
|
activeDay.value = val;
|
||||||
|
init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
loading.value = true
|
||||||
|
dashboardTvoc(dayEnum[activeDay.value])
|
||||||
|
.then(res => {
|
||||||
|
points.value = res.data.points.map(item => {
|
||||||
|
if (activeDay.value === 24) {
|
||||||
|
const yearMonthDay = item.time.split(' ')[0]
|
||||||
|
const [_, month, day] = yearMonthDay.split('-')
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
value: item.value || 0,
|
||||||
|
time: `${month}-${day} ${item.label}`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
value: item.value || 0,
|
||||||
|
time: item.label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('TVOC接口请求失败:', err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
init()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -46,14 +92,19 @@ function changeActive(val: number) {
|
|||||||
<div class="TvocNavBox">
|
<div class="TvocNavBox">
|
||||||
<span>平均TVOC原始值</span>
|
<span>平均TVOC原始值</span>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<div v-for="(item,index) in list" :key="index" :class="{
|
<div
|
||||||
active: activeDay === item.value,
|
v-for="(item,index) in list"
|
||||||
}" class="actionItem" @click="changeActive(item.value)">{{ item.label }}
|
:key="index"
|
||||||
|
:class="{ active: activeDay === item.value }"
|
||||||
|
class="actionItem"
|
||||||
|
@click="changeActive(item.value)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="TvocChartBox">
|
<div class="TvocChartBox">
|
||||||
<Echarts :options="options"></Echarts>
|
<Echarts :options="options"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -62,7 +113,7 @@ function changeActive(val: number) {
|
|||||||
.TvocBox {
|
.TvocBox {
|
||||||
padding: 25px;
|
padding: 25px;
|
||||||
height: 372px;
|
height: 372px;
|
||||||
border-radius: 16px 16px 16px 16px;
|
border-radius: 16px;
|
||||||
border: 1px solid rgba(238, 240, 243, 1);
|
border: 1px solid rgba(238, 240, 243, 1);
|
||||||
|
|
||||||
.TvocNavBox {
|
.TvocNavBox {
|
||||||
@@ -74,8 +125,6 @@ function changeActive(val: number) {
|
|||||||
span {
|
span {
|
||||||
height: 22px;
|
height: 22px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
border-radius: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
color: rgba(17, 24, 39, 1);
|
color: rgba(17, 24, 39, 1);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
@@ -83,7 +132,7 @@ function changeActive(val: number) {
|
|||||||
.actions {
|
.actions {
|
||||||
width: 175px;
|
width: 175px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
border-radius: 8px 8px 8px 8px;
|
border-radius: 8px;
|
||||||
background-color: rgba(243, 245, 248, 1);
|
background-color: rgba(243, 245, 248, 1);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -96,10 +145,11 @@ function changeActive(val: number) {
|
|||||||
padding: 7px 12px;
|
padding: 7px 12px;
|
||||||
color: rgba(107, 114, 128, 1);
|
color: rgba(107, 114, 128, 1);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: rgba(37, 99, 235, 1);
|
color: rgba(37, 99, 235, 1);
|
||||||
border-radius: 6px 6px 6px 6px;
|
border-radius: 6px;
|
||||||
background-color: rgba(255, 255, 255, 1);
|
background-color: rgba(255, 255, 255, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +1,49 @@
|
|||||||
<script lang="ts" name="Type" setup>
|
<script lang="ts" name="Type" setup>
|
||||||
import Echarts from '@/components/Echarts/index.vue';
|
import Echarts from '@/components/Echarts/index.vue';
|
||||||
|
|
||||||
const data = genData(7);
|
const props = defineProps<{
|
||||||
const options = ref({
|
total: number;
|
||||||
legend: {
|
items: Array<{ count: number, name: string, percent: number }>
|
||||||
type: 'scroll',
|
}>()
|
||||||
orient: 'vertical',
|
const options = computed(() => {
|
||||||
right: 10,
|
const pieData = props.items.map(item => ({
|
||||||
top: 20,
|
value: item.count,
|
||||||
bottom: 20,
|
name: item.name
|
||||||
data: data.legendData
|
}))
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: '姓名',
|
|
||||||
type: 'pie',
|
|
||||||
itemStyle: {
|
|
||||||
borderRadius: 10,
|
|
||||||
borderColor: '#fff',
|
|
||||||
borderWidth: 2
|
|
||||||
},
|
|
||||||
center: ['38%', '50%'],
|
|
||||||
radius: ['40%', '70%'],
|
|
||||||
data: data.seriesData,
|
|
||||||
label: {
|
|
||||||
show: false,
|
|
||||||
position: 'center'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
function genData(count: number) {
|
|
||||||
// prettier-ignore
|
|
||||||
const nameList = [
|
|
||||||
'赵', '钱', '孙', '李', '周', '吴', '郑', '王', '冯', '陈', '褚', '卫', '蒋', '沈', '韩', '杨', '朱', '秦', '尤', '许', '何', '吕', '施', '张', '孔', '曹', '严', '华', '金', '魏', '陶', '姜', '戚', '谢', '邹', '喻', '柏', '水', '窦', '章', '云', '苏', '潘', '葛', '奚', '范', '彭', '郎', '鲁', '韦', '昌', '马', '苗', '凤', '花', '方', '俞', '任', '袁', '柳', '酆', '鲍', '史', '唐', '费', '廉', '岑', '薛', '雷', '贺', '倪', '汤', '滕', '殷', '罗', '毕', '郝', '邬', '安', '常', '乐', '于', '时', '傅', '皮', '卞', '齐', '康', '伍', '余', '元', '卜', '顾', '孟', '平', '黄', '和', '穆', '萧', '尹', '姚', '邵', '湛', '汪', '祁', '毛', '禹', '狄', '米', '贝', '明', '臧', '计', '伏', '成', '戴', '谈', '宋', '茅', '庞', '熊', '纪', '舒', '屈', '项', '祝', '董', '梁', '杜', '阮', '蓝', '闵', '席', '季', '麻', '强', '贾', '路', '娄', '危'
|
|
||||||
];
|
|
||||||
const legendData = [];
|
|
||||||
const seriesData = [];
|
|
||||||
for (var i = 0; i < count; i++) {
|
|
||||||
var name =
|
|
||||||
Math.random() > 0.65
|
|
||||||
? makeWord(4, 1) + '·' + makeWord(3, 0)
|
|
||||||
: makeWord(2, 1);
|
|
||||||
legendData.push(name);
|
|
||||||
seriesData.push({
|
|
||||||
name: name,
|
|
||||||
value: Math.round(Math.random() * 100000)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
legendData: legendData,
|
legend: {
|
||||||
seriesData: seriesData
|
type: 'scroll',
|
||||||
};
|
orient: 'vertical',
|
||||||
|
right: 10,
|
||||||
function makeWord(max: number, min: number) {
|
top: 20,
|
||||||
const nameLen = Math.ceil(Math.random() * max + min);
|
bottom: 20,
|
||||||
const name = [];
|
data: pieData.map(i => i.name)
|
||||||
for (var i = 0; i < nameLen; i++) {
|
},
|
||||||
name.push(nameList[Math.round(Math.random() * nameList.length - 1)]);
|
series: [
|
||||||
}
|
{
|
||||||
return name.join('');
|
type: 'pie',
|
||||||
|
itemStyle: {
|
||||||
|
borderRadius: 10,
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 2
|
||||||
|
},
|
||||||
|
center: ['38%', '50%'],
|
||||||
|
radius: ['40%', '70%'],
|
||||||
|
data: pieData,
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
position: 'center'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="TypeBox">
|
<div class="TypeBox">
|
||||||
<div class="TypeNavBox">设备类型分布</div>
|
<div class="TypeNavBox">设备类型分布</div>
|
||||||
<div class="subTypeNavBox">共 12,860 台在线设备</div>
|
<div class="subTypeNavBox">共 {{ props.total }} 台在线设备</div>
|
||||||
<div class="TypeChartBox">
|
<div class="TypeChartBox">
|
||||||
<Echarts :options="options"></Echarts>
|
<Echarts :options="options"></Echarts>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,29 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="home">
|
<div class="home">
|
||||||
<div class=" updateTimeBox">
|
<div class="top">
|
||||||
<div class="updateTime">数据更新于 {{ updateTime }}</div>
|
<div class=" updateTimeBox">
|
||||||
<div class="tag">
|
<div class="updateTime">数据更新于 {{ form.updatedAt }}</div>
|
||||||
<div>环境数据为原始读数</div>
|
<div class="tag">
|
||||||
|
<div>{{ form.envNote }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 卡片-->
|
||||||
|
<Cards :list="cardData"></Cards>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :sm="14" :xs="24">
|
||||||
|
<Tvoc/>
|
||||||
|
</el-col>
|
||||||
|
<el-col :sm="10" :xs="24">
|
||||||
|
<Type :items="typeData.items" :total="typeData.total"/>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :sm="14" :xs="24">
|
||||||
|
<ActiveDevices :daily="activeDevices.dailyAvg" :points="activeDevices.points"
|
||||||
|
:week="activeDevices.Week"></ActiveDevices>
|
||||||
|
</el-col>
|
||||||
|
<el-col :sm="10" :xs="24">
|
||||||
|
<AreaDevices :items="areaDevices.items" :total="areaDevices.total"></AreaDevices>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="empty">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 卡片-->
|
|
||||||
<Cards></Cards>
|
|
||||||
<el-row :gutter="20">
|
|
||||||
<el-col :sm="14" :xs="24">
|
|
||||||
<Tvoc/>
|
|
||||||
</el-col>
|
|
||||||
<el-col :sm="10" :xs="24">
|
|
||||||
<Type/>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-row :gutter="20">
|
|
||||||
<el-col :sm="14" :xs="24">
|
|
||||||
<ActiveDevices></ActiveDevices>
|
|
||||||
</el-col>
|
|
||||||
<el-col :sm="10" :xs="24">
|
|
||||||
<AreaDevices></AreaDevices>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -33,17 +41,129 @@ import Tvoc from './compontents/tvoc.vue';
|
|||||||
import Type from './compontents/type.vue';
|
import Type from './compontents/type.vue';
|
||||||
import AreaDevices from './compontents/areaDevices.vue';
|
import AreaDevices from './compontents/areaDevices.vue';
|
||||||
import ActiveDevices from './compontents/activeDevices.vue';
|
import ActiveDevices from './compontents/activeDevices.vue';
|
||||||
|
import {dashboardHome} from '@/api/system/dashboard'
|
||||||
|
import {CardItemProps} from './compontents/card.vue'
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
"activatedCount": 1,
|
||||||
|
"activation": {
|
||||||
|
"dailyAvg": 0,
|
||||||
|
"points": [],
|
||||||
|
"weekNew": 0
|
||||||
|
},
|
||||||
|
"avgTvoc": null,
|
||||||
|
"avgTvocChangeRate": null,
|
||||||
|
"deviceTypes": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "未分类",
|
||||||
|
"percent": 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1
|
||||||
|
},
|
||||||
|
"envDeviceChangeRate": null,
|
||||||
|
"envDeviceCount": 0,
|
||||||
|
"envNote": "环境数据为原始读数",
|
||||||
|
"onlineCount": 1,
|
||||||
|
"onlineRate": "100.00",
|
||||||
|
"regions": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "未知",
|
||||||
|
"percent": 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1
|
||||||
|
},
|
||||||
|
"todayActivated": 0,
|
||||||
|
"todayActivatedChangeRate": null,
|
||||||
|
"tvocTrend": {
|
||||||
|
"points": [],
|
||||||
|
"range": "24h"
|
||||||
|
},
|
||||||
|
"updatedAt": "2026-10-10 08:05:02"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
const updateTime = ref('12:50');
|
// card Data
|
||||||
|
const cardData = ref<CardItemProps[]>([])
|
||||||
|
|
||||||
|
|
||||||
|
// type data
|
||||||
|
const typeData = ref({
|
||||||
|
total: 0,
|
||||||
|
items: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeDevices = ref({
|
||||||
|
Week: 0,
|
||||||
|
dailyAvg: 0,
|
||||||
|
points: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
// area device
|
||||||
|
const areaDevices = ref({
|
||||||
|
total: 0,
|
||||||
|
items: []
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
dashboardHome().then((res) => {
|
||||||
|
form.value = res.data;
|
||||||
|
cardData.value = [
|
||||||
|
{
|
||||||
|
num: res.data.envDeviceCount,
|
||||||
|
rate: res.data.envDeviceChangeRate,
|
||||||
|
value: 'envDevice'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
num: res.data.onlineCount,
|
||||||
|
rate: res.data.onlineRate,
|
||||||
|
value: 'online'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
num: res.data.avgTvoc,
|
||||||
|
rate: res.data.avgTvocChangeRate,
|
||||||
|
value: 'avgTvoc'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
num: res.data.todayActivated,
|
||||||
|
rate: res.data.todayActivatedChangeRate,
|
||||||
|
value: 'todayActivated'
|
||||||
|
},
|
||||||
|
];
|
||||||
|
typeData.value = {
|
||||||
|
total: res.data.deviceTypes.total,
|
||||||
|
items: res.data.deviceTypes.items,
|
||||||
|
};
|
||||||
|
|
||||||
|
activeDevices.value = {
|
||||||
|
dailyAvg: res.data.activation.dailyAvg,
|
||||||
|
Week: res.data.activation.weekNew,
|
||||||
|
points: res.data.activation.points,
|
||||||
|
};
|
||||||
|
areaDevices.value = {
|
||||||
|
total: res.data.regions.total,
|
||||||
|
items: res.data.regions.items,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
init()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.home {
|
.home {
|
||||||
display: flex;
|
.top {
|
||||||
flex-direction: column;
|
display: flex;
|
||||||
gap: 20px;
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.updateTimeBox {
|
.updateTimeBox {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
146
src/views/system/conversation/compontents/ChatInput.vue
Normal file
146
src/views/system/conversation/compontents/ChatInput.vue
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chat-input-wrap">
|
||||||
|
<div class="input-area">
|
||||||
|
<!-- 多行输入框 -->
|
||||||
|
<el-input
|
||||||
|
ref="inputRef"
|
||||||
|
v-model="inputText"
|
||||||
|
:autosize="{ minRows:5, maxRows:8 }"
|
||||||
|
placeholder="请输入回复内容"
|
||||||
|
type="textarea"
|
||||||
|
@keydown="handleKeyDown"
|
||||||
|
/>
|
||||||
|
<!-- 发送按钮 -->
|
||||||
|
<el-button
|
||||||
|
:disabled="!canSend || sending"
|
||||||
|
class="btn-send"
|
||||||
|
size="large"
|
||||||
|
type="primary"
|
||||||
|
@click="handleSend"
|
||||||
|
>
|
||||||
|
{{ sending ? '发送中' : '发送' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<!-- 图片上传按钮 -->
|
||||||
|
<el-button
|
||||||
|
class="btn-img"
|
||||||
|
plain
|
||||||
|
type="info"
|
||||||
|
@click="handleSelectImage"
|
||||||
|
>
|
||||||
|
上传图片
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<!-- 隐藏input用于选图 -->
|
||||||
|
<input
|
||||||
|
ref="fileRef"
|
||||||
|
accept="image/*"
|
||||||
|
style="display:none"
|
||||||
|
type="file"
|
||||||
|
@change="onFileChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {ref, computed} from 'vue'
|
||||||
|
import type {PropType} from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** 是否正在发送中(外部控制,防止重复点击) */
|
||||||
|
sending: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
/** 发送文本消息 */
|
||||||
|
(e: 'send', text: string): void
|
||||||
|
/** 选中图片文件,交给父组件上传oss */
|
||||||
|
(e: 'uploadImage', file: File): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const inputRef = ref<InstanceType<typeof elInput>>()
|
||||||
|
const fileRef = ref<HTMLInputElement | null>(null)
|
||||||
|
const inputText = ref('')
|
||||||
|
|
||||||
|
// 是否可以发送
|
||||||
|
const canSend = computed(() => {
|
||||||
|
return !!inputText.value.trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 点击图片图标,唤起文件选择
|
||||||
|
const handleSelectImage = () => {
|
||||||
|
fileRef.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件选择回调
|
||||||
|
const onFileChange = (e: Event) => {
|
||||||
|
const target = e.target as HTMLInputElement
|
||||||
|
const file = target.files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
emit('uploadImage', file)
|
||||||
|
// 清空file,选同一个图片也能触发change
|
||||||
|
target.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 键盘事件:回车发送,Shift+Enter换行
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
// shift+enter 原生换行,不拦截
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 直接回车发送
|
||||||
|
e.preventDefault()
|
||||||
|
handleSend()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
const handleSend = () => {
|
||||||
|
const rawText = inputText.value.trim()
|
||||||
|
if (!rawText) return
|
||||||
|
// 把换行 \n 替换成 <br/>
|
||||||
|
const htmlText = rawText.replaceAll('\n', '<br/>')
|
||||||
|
emit('send', htmlText)
|
||||||
|
// 发送成功后清空输入框
|
||||||
|
inputText.value = ''
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.chat-input-wrap {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-area {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
:deep(.el-textarea__inner) {
|
||||||
|
background-color: #f7f9fc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-img {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 9px 13px;
|
||||||
|
color: rgba(75, 85, 99, 1);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-send {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-textarea__inner) {
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
91
src/views/system/conversation/compontents/ChatNavBar.vue
Normal file
91
src/views/system/conversation/compontents/ChatNavBar.vue
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="currentClass" class="chat-tip-bar">
|
||||||
|
<!-- 提示条区域,放在MessageList内部最顶部,跟随滚动 -->
|
||||||
|
<span>{{ currentText }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {computed} from 'vue'
|
||||||
|
import {ChatNavBarType} from './types'
|
||||||
|
import type {PropType} from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
type: {
|
||||||
|
type: Number as PropType<ChatNavBarType>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
time: String,
|
||||||
|
awaitNum: Number,
|
||||||
|
agentInfo: {
|
||||||
|
type: Object as PropType<{
|
||||||
|
name: string,
|
||||||
|
no: string,
|
||||||
|
}>,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 根据type自动切换文本
|
||||||
|
const currentText = computed(() => {
|
||||||
|
switch (props.type) {
|
||||||
|
case ChatNavBarType.agent:
|
||||||
|
return `${props.time} 用户点击「仍未解决,转人工」`
|
||||||
|
case ChatNavBarType.await:
|
||||||
|
return `排队提醒:当前排队第 ${props.awaitNum} 位,已等待 ${props.time}`
|
||||||
|
case ChatNavBarType.accept:
|
||||||
|
return `客服${props.agentInfo?.name}(工号 ${props.agentInfo?.no})已接入会话`
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 样式类
|
||||||
|
const currentClass = computed(() => {
|
||||||
|
switch (props.type) {
|
||||||
|
case ChatNavBarType.agent:
|
||||||
|
return 'agent'
|
||||||
|
case ChatNavBarType.await:
|
||||||
|
return 'await'
|
||||||
|
case ChatNavBarType.accept:
|
||||||
|
return 'accept'
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chat-tip-bar {
|
||||||
|
min-width: 250px;
|
||||||
|
max-width: 250px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 7px auto;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-tip-bar span {
|
||||||
|
text-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent {
|
||||||
|
color: rgba(156, 163, 175, 1);
|
||||||
|
background-color: rgba(233, 236, 241, 1);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.await {
|
||||||
|
color: rgba(234, 88, 12, 1);
|
||||||
|
background-color: rgba(255, 247, 237, 1);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accept {
|
||||||
|
background-color: rgba(232, 247, 238, 1);
|
||||||
|
color: rgba(22, 163, 74, 1);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<div class="agent-message">
|
||||||
|
<img :src="avatar" class="avatar"/>
|
||||||
|
<div class="msg-body">
|
||||||
|
<div class="msg-header">
|
||||||
|
<span class="name">客服{{ name }}</span>
|
||||||
|
<span v-if="tag" class="tag agent-tag">{{ tag }}</span>
|
||||||
|
<span class="time">{{ time }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- 文本消息 -->
|
||||||
|
<div v-if="type === 'text'" class="bubble" v-html="content"></div>
|
||||||
|
<!-- 图片消息 -->
|
||||||
|
<div v-else-if="type === 'image'" class="bubble img-bubble">
|
||||||
|
<img :src="content" class="chat-img" @click="$emit('preview', content)"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps({
|
||||||
|
name: {type: String, default: '客服'},
|
||||||
|
avatar: {type: String, default: ''},
|
||||||
|
content: {type: String, required: true},
|
||||||
|
type: {type: String as () => 'text' | 'image', default: 'text'},
|
||||||
|
tag: {type: String, default: '人工客服'},
|
||||||
|
time: {type: String, default: ''}
|
||||||
|
})
|
||||||
|
defineEmits(['preview'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.agent-message {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-body {
|
||||||
|
margin-left: 8px;
|
||||||
|
max-width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-tag {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px 16px 16px 16px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
color: rgba(31, 41, 55, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-bubble {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-img {
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 200px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<template>
|
||||||
|
<div class="robot-message">
|
||||||
|
<div class="avatar">智</div>
|
||||||
|
<div class="msg-body">
|
||||||
|
<!-- 头部:客服名 + 标签 + 时间 -->
|
||||||
|
<div class="msg-header">
|
||||||
|
<span class="name">智能客服 小智</span>
|
||||||
|
<span class="tag robot-tag">机器人</span>
|
||||||
|
<span class="time">{{ time }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- 气泡 -->
|
||||||
|
<div class="bubble" v-html="content"></div>
|
||||||
|
<!-- 快捷按钮 -->
|
||||||
|
<div v-if="quickActions?.length" class="quick-actions">
|
||||||
|
<button
|
||||||
|
v-for="btn in quickActions"
|
||||||
|
:key="btn.action"
|
||||||
|
:class="{ primary: btn.primary }"
|
||||||
|
class="action-btn"
|
||||||
|
@click="$emit('quickAction', btn.action)"
|
||||||
|
>
|
||||||
|
{{ btn.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
interface QuickAction {
|
||||||
|
label: string
|
||||||
|
action: string
|
||||||
|
primary?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
name: {type: String, default: '智能客服'},
|
||||||
|
avatar: {type: String, default: ''},
|
||||||
|
content: {type: String, required: true},
|
||||||
|
time: {type: String, default: ''},
|
||||||
|
quickActions: {type: Array as () => QuickAction[], default: () => []}
|
||||||
|
})
|
||||||
|
defineEmits(['quickAction'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.robot-message {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: rgba(15, 118, 110, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-body {
|
||||||
|
margin-left: 8px;
|
||||||
|
max-width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(156, 163, 175, 1);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.robot-tag {
|
||||||
|
background: #f3f0ff;
|
||||||
|
color: #8b50ef;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px 12px 12px 2px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
padding: 4px 14px;
|
||||||
|
border: 1px solid #2563eb;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #fff;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.primary {
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<div class="system-message">
|
||||||
|
<span class="system-text">{{ text }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps({
|
||||||
|
text: {type: String, required: true}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.system-message {
|
||||||
|
text-align: center;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-text {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
background: transparent;
|
||||||
|
padding: 2px 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-message">
|
||||||
|
<div class="msg-body">
|
||||||
|
<!-- 文本消息 -->
|
||||||
|
<div v-if="type === 'text'" class="bubble" v-html="content"></div>
|
||||||
|
<!-- 图片消息 -->
|
||||||
|
<div v-else-if="type === 'image'" class="bubble img-bubble">
|
||||||
|
<img :src="content" class="chat-img"/>
|
||||||
|
</div>
|
||||||
|
<div class="time">{{ time }}</div>
|
||||||
|
</div>
|
||||||
|
<img :src="avatar" class="avatar"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps({
|
||||||
|
content: {type: String, required: true},
|
||||||
|
type: {type: String as () => 'text' | 'image', default: 'text'},
|
||||||
|
avatar: {type: String, default: ''},
|
||||||
|
time: {type: String, default: ''}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-message {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-body {
|
||||||
|
margin-right: 8px;
|
||||||
|
max-width: 363px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
background-color: rgba(37, 99, 235, 1);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 16px 4px 16px 16px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
|
|
||||||
|
line-height: 22px;
|
||||||
|
color: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-bubble {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-img {
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 200px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
260
src/views/system/conversation/compontents/MessageList/index.vue
Normal file
260
src/views/system/conversation/compontents/MessageList/index.vue
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="listRef"
|
||||||
|
class="message-list "
|
||||||
|
@scroll.passive="handleScroll"
|
||||||
|
>
|
||||||
|
<!-- 加载历史消息提示 -->
|
||||||
|
<div v-if="loadingHistory" class="load-more-tip">
|
||||||
|
<span class="spinner"></span> 加载中...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 消息列表:key 必须用唯一 id,禁止 index -->
|
||||||
|
<template v-for="msg in messages" :key="msg.id">
|
||||||
|
<!-- 2. 用户消息 -->
|
||||||
|
<UserMessage
|
||||||
|
v-if="msg.role === 'user'"
|
||||||
|
:avatar="userAvatar"
|
||||||
|
:content="msg.content"
|
||||||
|
:msg-type="msg.msgType"
|
||||||
|
:status="msg.status"
|
||||||
|
:time="msg.time"
|
||||||
|
@retry="handleRetry(msg)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 3. 机器人消息 -->
|
||||||
|
<RobotMessage
|
||||||
|
v-else-if="msg.agentType === 'robot'"
|
||||||
|
:avatar="msg.avatar"
|
||||||
|
:content="msg.content"
|
||||||
|
:msg-type="msg.msgType"
|
||||||
|
:name="msg.agentName"
|
||||||
|
:quick-actions="msg.quickActions"
|
||||||
|
:time="msg.time"
|
||||||
|
@quick-action="handleQuickAction"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 4. 人工客服消息 -->
|
||||||
|
<AgentMessage
|
||||||
|
v-else
|
||||||
|
:avatar="msg.avatar"
|
||||||
|
:content="msg.content"
|
||||||
|
:msg-type="msg.msgType"
|
||||||
|
:name="msg.agentName"
|
||||||
|
:time="msg.time"
|
||||||
|
@preview="handlePreview"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 底部占位 -->
|
||||||
|
<div class="bottom-space"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {ref, watch, nextTick, onMounted} from 'vue'
|
||||||
|
import SystemMessage from './SystemMessage.vue'
|
||||||
|
import UserMessage from './UserMessage.vue'
|
||||||
|
import RobotMessage from './RobotMessage.vue'
|
||||||
|
import AgentMessage from './AgentMessage.vue'
|
||||||
|
|
||||||
|
interface ChatMessage {
|
||||||
|
id: string | number
|
||||||
|
type: 'system' | 'chat'
|
||||||
|
role?: 'user' | 'agent'
|
||||||
|
agentType?: 'robot' | 'human'
|
||||||
|
content: string
|
||||||
|
msgType?: 'text' | 'image'
|
||||||
|
agentName?: string
|
||||||
|
avatar?: string
|
||||||
|
time: string
|
||||||
|
status?: 'sending' | 'sent' | 'failed'
|
||||||
|
quickActions?: { label: string; action: string; primary?: boolean }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
messages: ChatMessage[]
|
||||||
|
userAvatar: string
|
||||||
|
loadingHistory?: boolean
|
||||||
|
/** 是否还有更多历史可加载(可选,建议父组件传) */
|
||||||
|
hasMoreHistory?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'loadMore'): void
|
||||||
|
(e: 'quickAction', action: string): void
|
||||||
|
(e: 'retry', msg: ChatMessage): void
|
||||||
|
(e: 'preview', url: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const listRef = ref<HTMLElement>()
|
||||||
|
|
||||||
|
/** 用户是否贴近底部(阈值 50px) */
|
||||||
|
const isAtBottom = ref(true)
|
||||||
|
|
||||||
|
/** 首屏标记:首次渲染不走平滑滚动 */
|
||||||
|
let isFirstRender = true
|
||||||
|
|
||||||
|
/** 加载历史前的 scrollHeight 快照 */
|
||||||
|
let prevScrollHeight = 0
|
||||||
|
|
||||||
|
/** 加载历史防抖锁 */
|
||||||
|
let isLoadingMore = false
|
||||||
|
|
||||||
|
/** 阈值常量 */
|
||||||
|
const BOTTOM_THRESHOLD = 50
|
||||||
|
const TOP_THRESHOLD = 30
|
||||||
|
|
||||||
|
/* ========== 判断是否贴近底部 ========== */
|
||||||
|
const checkAtBottom = (): boolean => {
|
||||||
|
const el = listRef.value
|
||||||
|
if (!el) return true
|
||||||
|
return el.scrollHeight - el.scrollTop - el.clientHeight < BOTTOM_THRESHOLD
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 滚动到底部 ========== */
|
||||||
|
const scrollToBottom = (smooth = false) => {
|
||||||
|
const el = listRef.value
|
||||||
|
if (!el) return
|
||||||
|
el.scrollTo({
|
||||||
|
top: el.scrollHeight,
|
||||||
|
behavior: smooth ? 'smooth' : 'auto'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 滚动事件 ========== */
|
||||||
|
const handleScroll = () => {
|
||||||
|
const el = listRef.value
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
// 1. 更新底部状态
|
||||||
|
isAtBottom.value = checkAtBottom()
|
||||||
|
|
||||||
|
// 2. 触顶加载历史
|
||||||
|
if (
|
||||||
|
el.scrollTop <= TOP_THRESHOLD &&
|
||||||
|
!isLoadingMore &&
|
||||||
|
!props.loadingHistory &&
|
||||||
|
props.hasMoreHistory !== false
|
||||||
|
) {
|
||||||
|
isLoadingMore = true
|
||||||
|
// 关键:在 emit 之前记录当前 scrollHeight 快照
|
||||||
|
prevScrollHeight = el.scrollHeight
|
||||||
|
emit('loadMore')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 首屏 + 新消息滚动 ========== */
|
||||||
|
watch(
|
||||||
|
() => props.messages?.length ?? 0,
|
||||||
|
async (newLen, oldLen) => {
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
if (isFirstRender) {
|
||||||
|
// 场景 ①:首次进入页面,无动画直接滚到底
|
||||||
|
scrollToBottom(false)
|
||||||
|
isFirstRender = false
|
||||||
|
isAtBottom.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景 ②:新消息到达 && 用户在底部 → 平滑滚到底
|
||||||
|
// 场景 ③:用户在翻历史 → 不滚动
|
||||||
|
if (newLen > oldLen && isAtBottom.value) {
|
||||||
|
scrollToBottom(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
/* ========== 加载历史后,保持视觉位置 ========== */
|
||||||
|
watch(
|
||||||
|
() => props.loadingHistory,
|
||||||
|
async (loading, wasLoading) => {
|
||||||
|
// 只在 loading 从 true → false 的瞬间处理
|
||||||
|
if (wasLoading && !loading) {
|
||||||
|
await nextTick()
|
||||||
|
const el = listRef.value
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
// 场景 ④:内容变高了,把 scrollTop 平移到等价位置
|
||||||
|
const delta = el.scrollHeight - prevScrollHeight
|
||||||
|
el.scrollTop = el.scrollTop + delta
|
||||||
|
|
||||||
|
// 重算底部状态(此时肯定不在底部)
|
||||||
|
isAtBottom.value = checkAtBottom()
|
||||||
|
|
||||||
|
// 解锁,允许下一次加载
|
||||||
|
isLoadingMore = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/* ========== 挂载后兜底:图片等异步内容渲染后可能改变 scrollHeight ========== */
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
scrollToBottom(false)
|
||||||
|
isAtBottom.value = true
|
||||||
|
isFirstRender = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/* ========== 对外暴露手动滚底(如发送消息成功后) ========== */
|
||||||
|
defineExpose({
|
||||||
|
scrollToBottom: (smooth = true) => scrollToBottom(smooth)
|
||||||
|
})
|
||||||
|
|
||||||
|
/* ========== 事件转发 ========== */
|
||||||
|
const handleQuickAction = (action: string) => emit('quickAction', action)
|
||||||
|
const handleRetry = (msg: ChatMessage) => emit('retry', msg)
|
||||||
|
const handlePreview = (url: string) => emit('preview', url)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.message-list {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0; /* ⚠️ 关键:flex 子项内滚动必须加 */
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 12px;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
|
||||||
|
/* 隐藏滚动条(Chrome / Firefox / IE) */
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-list::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-tip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-top-color: #409eff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-space {
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
287
src/views/system/conversation/compontents/data.ts
Normal file
287
src/views/system/conversation/compontents/data.ts
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
// src/mock/chatData.ts
|
||||||
|
|
||||||
|
export interface QuickAction {
|
||||||
|
label: string
|
||||||
|
action: string
|
||||||
|
primary?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
import {ChatMessage} from './types'
|
||||||
|
|
||||||
|
/** 用户头像(也可以从 store / 用户信息里取) */
|
||||||
|
export const userAvatar = 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix'
|
||||||
|
|
||||||
|
/** 机器人头像 */
|
||||||
|
export const robotAvatar = 'https://api.dicebear.com/7.x/bottts/svg?seed=Robot'
|
||||||
|
|
||||||
|
/** 人工客服头像 */
|
||||||
|
export const agentAvatar = 'https://api.dicebear.com/7.x/avataaars/svg?seed=Aneka'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始消息(首屏展示)
|
||||||
|
* 涵盖:系统消息 / 机器人消息(含快捷按钮) / 用户消息(各种状态) / 人工客服消息(文本+图片)
|
||||||
|
*/
|
||||||
|
export const initMessages: ChatMessage[] = [
|
||||||
|
// ========== 系统消息:时间分割 ==========
|
||||||
|
{
|
||||||
|
id: 'sys_001',
|
||||||
|
type: 'system',
|
||||||
|
content: '2026-01-15 14:00',
|
||||||
|
time: '14:00',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 系统消息:排队提示 ==========
|
||||||
|
{
|
||||||
|
id: 'sys_002',
|
||||||
|
type: 'system',
|
||||||
|
content: '您已进入排队,当前前面有 2 位用户,预计等待 1 分钟',
|
||||||
|
time: '14:00',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 机器人欢迎消息 ==========
|
||||||
|
{
|
||||||
|
id: 'robot_001',
|
||||||
|
type: 'chat',
|
||||||
|
role: 'agent',
|
||||||
|
agentType: 'robot',
|
||||||
|
agentName: '智能助手',
|
||||||
|
avatar: robotAvatar,
|
||||||
|
content: '您好!我是智能客服小助手 🤖\n请问有什么可以帮您的?\n\n您可以选择下面的常见问题,或直接输入您的问题。',
|
||||||
|
msgType: 'text',
|
||||||
|
time: '14:01',
|
||||||
|
quickActions: [
|
||||||
|
{label: '查询订单', action: 'query_order', primary: true},
|
||||||
|
{label: '退换货政策', action: 'refund_policy'},
|
||||||
|
{label: '发票问题', action: 'invoice'},
|
||||||
|
{label: '联系人工', action: 'transfer_agent'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 用户消息:正常文本 ==========
|
||||||
|
{
|
||||||
|
id: 'user_001',
|
||||||
|
type: 'chat',
|
||||||
|
role: 'user',
|
||||||
|
content: '我想查一下我的订单状态',
|
||||||
|
msgType: 'text',
|
||||||
|
time: '14:02',
|
||||||
|
status: 'sent',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 机器人回复 + 快捷按钮 ==========
|
||||||
|
{
|
||||||
|
id: 'robot_002',
|
||||||
|
type: 'chat',
|
||||||
|
role: 'agent',
|
||||||
|
agentType: 'robot',
|
||||||
|
agentName: '智能助手',
|
||||||
|
avatar: robotAvatar,
|
||||||
|
content: '好的,请提供您的订单编号,我帮您查询。\n订单编号格式为:ORD-XXXXXXXX',
|
||||||
|
msgType: 'text',
|
||||||
|
time: '14:02',
|
||||||
|
quickActions: [
|
||||||
|
{label: '不知道订单号', action: 'no_order_id'},
|
||||||
|
{label: '转人工', action: 'transfer_agent', primary: true},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 用户消息:图片 ==========
|
||||||
|
{
|
||||||
|
id: 'user_002',
|
||||||
|
type: 'chat',
|
||||||
|
role: 'user',
|
||||||
|
content: 'https://picsum.photos/400/300?random=1',
|
||||||
|
msgType: 'image',
|
||||||
|
time: '14:03',
|
||||||
|
status: 'sent',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 系统消息:转接人工 ==========
|
||||||
|
{
|
||||||
|
id: 'sys_003',
|
||||||
|
type: 'system',
|
||||||
|
content: '正在为您转接人工客服,请稍候...',
|
||||||
|
time: '14:03',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 系统消息:人工接入 ==========
|
||||||
|
{
|
||||||
|
id: 'sys_004',
|
||||||
|
type: 'system',
|
||||||
|
content: '人工客服已接入,正在为您服务',
|
||||||
|
time: '14:04',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 人工客服:文本 ==========
|
||||||
|
{
|
||||||
|
id: 'agent_001',
|
||||||
|
type: 'chat',
|
||||||
|
role: 'agent',
|
||||||
|
agentType: 'human',
|
||||||
|
agentName: '客服小林',
|
||||||
|
avatar: agentAvatar,
|
||||||
|
content: '您好,我是人工客服小林,很高兴为您服务!\n请把订单号发给我,我马上帮您查。',
|
||||||
|
msgType: 'text',
|
||||||
|
time: '14:04',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 人工客服:图片(订单截图示例) ==========
|
||||||
|
{
|
||||||
|
id: 'agent_002',
|
||||||
|
type: 'chat',
|
||||||
|
role: 'agent',
|
||||||
|
agentType: 'human',
|
||||||
|
agentName: '客服小林',
|
||||||
|
avatar: agentAvatar,
|
||||||
|
content: 'https://picsum.photos/400/300?random=2',
|
||||||
|
msgType: 'image',
|
||||||
|
time: '14:05',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 用户消息:发送中 ==========
|
||||||
|
{
|
||||||
|
id: 'user_003',
|
||||||
|
type: 'chat',
|
||||||
|
role: 'user',
|
||||||
|
content: '好的,我看到了,谢谢',
|
||||||
|
msgType: 'text',
|
||||||
|
time: '14:05',
|
||||||
|
status: 'sending',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 用户消息:发送成功 ==========
|
||||||
|
{
|
||||||
|
id: 'user_004',
|
||||||
|
type: 'chat',
|
||||||
|
role: 'user',
|
||||||
|
content: '请问大概什么时候能到?',
|
||||||
|
msgType: 'text',
|
||||||
|
time: '14:06',
|
||||||
|
status: 'sent',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 用户消息:发送失败(红色感叹号,可重发) ==========
|
||||||
|
{
|
||||||
|
id: 'user_005',
|
||||||
|
type: 'chat',
|
||||||
|
role: 'user',
|
||||||
|
content: '快递单号能再发一下吗?',
|
||||||
|
msgType: 'text',
|
||||||
|
time: '14:06',
|
||||||
|
status: 'failed',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 人工客服:富文本/长文本换行 ==========
|
||||||
|
{
|
||||||
|
id: 'agent_003',
|
||||||
|
type: 'chat',
|
||||||
|
role: 'agent',
|
||||||
|
agentType: 'human',
|
||||||
|
agentName: '客服小林',
|
||||||
|
avatar: agentAvatar,
|
||||||
|
content:
|
||||||
|
'订单已于今天上午发货 📦\n物流单号:SF1234567890\n\n预计 1 月 16 日下午送达,请保持手机畅通。\n如未按时收到,可以随时联系我~',
|
||||||
|
msgType: 'text',
|
||||||
|
time: '14:07',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载历史消息(模拟分页返回,用于验证"保持视觉位置"逻辑)
|
||||||
|
* 每次调用返回 5~8 条更早的消息,id 递增
|
||||||
|
*/
|
||||||
|
let historyBatch = 0
|
||||||
|
|
||||||
|
export function fetchMoreHistory(): ChatMessage[] {
|
||||||
|
historyBatch += 1
|
||||||
|
const base = 100 * historyBatch
|
||||||
|
|
||||||
|
return [
|
||||||
|
// 时间分割
|
||||||
|
{
|
||||||
|
id: `history_${base}_00`,
|
||||||
|
type: 'system',
|
||||||
|
content: `2026-01-15 ${String(13 - historyBatch).padStart(2, '0')}:50`,
|
||||||
|
time: `${String(13 - historyBatch).padStart(2, '0')}:50`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 用户历史消息
|
||||||
|
{
|
||||||
|
id: `history_${base}_01`,
|
||||||
|
type: 'chat',
|
||||||
|
role: 'user',
|
||||||
|
content: `这是第 ${historyBatch} 批历史消息中的用户提问 1`,
|
||||||
|
msgType: 'text',
|
||||||
|
time: `${String(13 - historyBatch).padStart(2, '0')}:50`,
|
||||||
|
status: 'sent',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 机器人回复
|
||||||
|
{
|
||||||
|
id: `history_${base}_02`,
|
||||||
|
type: 'chat',
|
||||||
|
role: 'agent',
|
||||||
|
agentType: 'robot',
|
||||||
|
agentName: '智能助手',
|
||||||
|
avatar: robotAvatar,
|
||||||
|
content: `这是机器人对历史问题 ${historyBatch}-1 的回复。`,
|
||||||
|
msgType: 'text',
|
||||||
|
time: `${String(13 - historyBatch).padStart(2, '0')}:51`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 用户历史图片
|
||||||
|
{
|
||||||
|
id: `history_${base}_03`,
|
||||||
|
type: 'chat',
|
||||||
|
role: 'user',
|
||||||
|
content: `https://picsum.photos/400/300?random=${base + 10}`,
|
||||||
|
msgType: 'image',
|
||||||
|
time: `${String(13 - historyBatch).padStart(2, '0')}:51`,
|
||||||
|
status: 'sent',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 人工客服历史回复
|
||||||
|
{
|
||||||
|
id: `history_${base}_04`,
|
||||||
|
type: 'chat',
|
||||||
|
role: 'agent',
|
||||||
|
agentType: 'human',
|
||||||
|
agentName: '客服小林',
|
||||||
|
avatar: agentAvatar,
|
||||||
|
content: `历史人工回复:已为您记录,请稍候。`,
|
||||||
|
msgType: 'text',
|
||||||
|
time: `${String(13 - historyBatch).padStart(2, '0')}:52`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 系统消息
|
||||||
|
{
|
||||||
|
id: `history_${base}_05`,
|
||||||
|
type: 'system',
|
||||||
|
content: `—— 以上为第 ${historyBatch} 批历史消息 ——`,
|
||||||
|
time: `${String(13 - historyBatch).padStart(2, '0')}:52`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模拟机器人回复(发送消息后延迟触发)
|
||||||
|
*/
|
||||||
|
export function mockRobotReply(userText: string): ChatMessage {
|
||||||
|
return {
|
||||||
|
id: `robot_${Date.now()}`,
|
||||||
|
type: 'chat',
|
||||||
|
role: 'agent',
|
||||||
|
agentType: 'robot',
|
||||||
|
agentName: '智能助手',
|
||||||
|
avatar: robotAvatar,
|
||||||
|
content: `收到您的问题:"${userText}"\n正在为您查询,请稍候...`,
|
||||||
|
msgType: 'text',
|
||||||
|
time: formatNowTime(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 格式化当前时间为 HH:mm */
|
||||||
|
export function formatNowTime(): string {
|
||||||
|
const d = new Date()
|
||||||
|
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||||
|
}
|
||||||
37
src/views/system/conversation/compontents/types.ts
Normal file
37
src/views/system/conversation/compontents/types.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// 消息类型
|
||||||
|
type MessageType = 'text' | 'image' | 'system' | 'robot' | 'action'
|
||||||
|
|
||||||
|
|
||||||
|
// chatNavBar
|
||||||
|
// export type ChatNavBarType = {
|
||||||
|
// /** 需要转人工 */
|
||||||
|
// agent: 'agent',
|
||||||
|
// /** 排队提醒 */
|
||||||
|
// await: 'await',
|
||||||
|
// /** 客服已接入会话*/
|
||||||
|
// accept: 'accept',
|
||||||
|
// }
|
||||||
|
|
||||||
|
export enum ChatNavBarType {
|
||||||
|
/** 需要转人工 */
|
||||||
|
agent,
|
||||||
|
/** 排队提醒 */
|
||||||
|
await,
|
||||||
|
/** 客服已接入会话*/
|
||||||
|
accept,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface ChatMessage {
|
||||||
|
id: string | number
|
||||||
|
type: 'system' | 'chat'
|
||||||
|
role?: 'user' | 'agent'
|
||||||
|
agentType?: 'robot' | 'human'
|
||||||
|
content: string
|
||||||
|
msgType?: 'text' | 'image'
|
||||||
|
agentName?: string
|
||||||
|
avatar?: string
|
||||||
|
time: string
|
||||||
|
status?: 'sending' | 'sent' | 'failed'
|
||||||
|
quickActions?: { label: string; action: string; primary?: boolean }[]
|
||||||
|
}
|
||||||
413
src/views/system/conversation/conversationMain.vue
Normal file
413
src/views/system/conversation/conversationMain.vue
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import pageCrumb from '@/components/Pagecrumb/index.vue'
|
||||||
|
|
||||||
|
|
||||||
|
import ChatNavBar from "./compontents/ChatNavBar.vue";
|
||||||
|
import {ChatNavBarType} from "./compontents/types";
|
||||||
|
|
||||||
|
import MessageList from './compontents/MessageList/index.vue'
|
||||||
|
import {initMessages, userAvatar} from "./compontents/data";
|
||||||
|
import ChatInput from "@/views/system/conversation/compontents/ChatInput.vue";
|
||||||
|
|
||||||
|
const defaultImage = import.meta.env.VITE_DEFAULT_USER_IMAGE;
|
||||||
|
const userDevicesList = [
|
||||||
|
{
|
||||||
|
label: 'Z2 空气净化器 · 客厅',
|
||||||
|
model: 'CZ-Z2023',
|
||||||
|
mac: "AA:BB:CC:DD:A3:B6",
|
||||||
|
status: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Z2 空气净化器 · 主卧',
|
||||||
|
model: 'CZ-Z2023',
|
||||||
|
mac: "AA:BB:CC:DD:A3:B1",
|
||||||
|
status: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Z2 空气净化器 · 儿童房',
|
||||||
|
model: 'CZ-Z2023',
|
||||||
|
mac: "AA:BB:CC:DD:A3:B2",
|
||||||
|
status: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Z2 空气净化器 · 次卧',
|
||||||
|
model: 'CZ-Z2023',
|
||||||
|
mac: "AA:BB:CC:DD:A3:B3",
|
||||||
|
status: 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
status: ChatNavBarType.await as ChatNavBarType,
|
||||||
|
time: new Date().toLocaleTimeString(),
|
||||||
|
awaitNum: 3,
|
||||||
|
agentInfo: {
|
||||||
|
no: '8823',
|
||||||
|
name: '小雨'
|
||||||
|
},
|
||||||
|
message: []
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
form.value.message = [...initMessages]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const sending = ref(false)
|
||||||
|
// 发送文本消息
|
||||||
|
const onSendMsg = async (text: string) => {
|
||||||
|
sending.value = true
|
||||||
|
// 组装消息,推入messages列表
|
||||||
|
const newMsg = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
type: 'chat',
|
||||||
|
role: 'user',
|
||||||
|
msgType: 'text',
|
||||||
|
content: text,
|
||||||
|
time: new Date().toLocaleTimeString(),
|
||||||
|
status: 'sending'
|
||||||
|
}
|
||||||
|
console.log(text)
|
||||||
|
form.value.message.push(newMsg)
|
||||||
|
try {
|
||||||
|
// 请求后端接口
|
||||||
|
// await api.sendChat(...)
|
||||||
|
newMsg.status = 'sent'
|
||||||
|
} catch (err) {
|
||||||
|
newMsg.status = 'failed'
|
||||||
|
} finally {
|
||||||
|
sending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片上传,拿到File,上传oss拿到url,生成图片消息
|
||||||
|
const onUploadImage = async (file: File) => {
|
||||||
|
// 调用oss上传接口,拿到真实图片url
|
||||||
|
// const url = await uploadOss(file)
|
||||||
|
const url = 'https://xxx.oss-cn-beijing.aliyuncs.com/demo.jpg'
|
||||||
|
const imgMsg = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
type: 'chat',
|
||||||
|
role: 'user',
|
||||||
|
msgType: 'image',
|
||||||
|
content: url,
|
||||||
|
time: new Date().toLocaleTimeString(),
|
||||||
|
status: 'sending'
|
||||||
|
}
|
||||||
|
form.value.message.push(imgMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 1. 获取用户列表
|
||||||
|
// 2. 点击用户,开始会话,ws
|
||||||
|
// 3. 查看用户信息,
|
||||||
|
// 4. 获取过往信息
|
||||||
|
// 5. 自动发送开场白
|
||||||
|
// 6. ws 发送消息
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="Main beautify_scroll">
|
||||||
|
<pageCrumb>
|
||||||
|
<template #rightSlot>
|
||||||
|
<el-button plain type="info">返回会话列表</el-button>
|
||||||
|
</template>
|
||||||
|
</pageCrumb>
|
||||||
|
<div class="commonCard">
|
||||||
|
<card-nav
|
||||||
|
:show-line="false"
|
||||||
|
subTitle="开始时间 2026-09-10 12:44:52 · 等待时长 00:38 · 接入方式:机器人接待 → 转人工"
|
||||||
|
title="CS20260918100038"
|
||||||
|
>
|
||||||
|
<template #rightTitle>
|
||||||
|
<el-tag type="primary">人工接待</el-tag>
|
||||||
|
</template>
|
||||||
|
<template #rightSlot>
|
||||||
|
<el-button plain type="default">导出聊天记录</el-button>
|
||||||
|
<el-button plain type="danger">关闭会话</el-button>
|
||||||
|
</template>
|
||||||
|
</card-nav>
|
||||||
|
</div>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :lg="16" :md="15" :span="24" :xl="18">
|
||||||
|
<div class="commonCard userWindowBox">
|
||||||
|
<div class="userWindowNav userWindowNavItem">
|
||||||
|
<el-image :src="defaultImage" class="userAvatar"></el-image>
|
||||||
|
<div class="userWindowNavItem-l">
|
||||||
|
<div class="userWindowNavItem-l-info">
|
||||||
|
<div>
|
||||||
|
<span class="name">林筱曼</span>
|
||||||
|
<span class="phone">138****6688</span>
|
||||||
|
</div>
|
||||||
|
<span class="time">会话时长 06:12</span>
|
||||||
|
</div>
|
||||||
|
<div class="userWindowNavItem-l-status">
|
||||||
|
<i class="circle"></i>
|
||||||
|
<span>用户在线</span>
|
||||||
|
<span style="margin:0 0.5rem;">·</span>
|
||||||
|
<span>正在输入</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content beautify_scroll">
|
||||||
|
<ChatNavBar
|
||||||
|
:agent-info="form.agentInfo"
|
||||||
|
:await-num="form.awaitNum"
|
||||||
|
:time="form.time"
|
||||||
|
:type="form.status"
|
||||||
|
></ChatNavBar>
|
||||||
|
<MessageList :messages="form.message" :user-avatar="userAvatar"></MessageList>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<!-- 输入框组件 -->
|
||||||
|
<ChatInput :sending="sending" @send="onSendMsg" @uploadImage="onUploadImage"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :lg="8" :md="9" :span="24" :xl="6">
|
||||||
|
<div class="commonCard cardUserInfoBox">
|
||||||
|
<card-nav title="用户信息">
|
||||||
|
<template slot="rightTitle">
|
||||||
|
<el-tag type="primary">用户详情</el-tag>
|
||||||
|
</template>
|
||||||
|
</card-nav>
|
||||||
|
<ul class="cardUserInfo">
|
||||||
|
<li class="cardUserInfo_item">
|
||||||
|
<span class="label">用户ID</span>
|
||||||
|
<span class="value">U20260918001</span>
|
||||||
|
</li>
|
||||||
|
<li class="cardUserInfo_item">
|
||||||
|
<span class="label">昵称</span>
|
||||||
|
<span class="value">林小满</span>
|
||||||
|
</li>
|
||||||
|
<li class="cardUserInfo_item">
|
||||||
|
<span class="label">手机号</span>
|
||||||
|
<span class="value">138****6688</span>
|
||||||
|
</li>
|
||||||
|
<li class="cardUserInfo_item">
|
||||||
|
<span class="label">注册时间</span>
|
||||||
|
<span class="value">2026-01-10 14:20</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="userDevicesBox">
|
||||||
|
<card-nav :show-line="false">
|
||||||
|
<template #title>
|
||||||
|
<span class="titleName">名下设备(3台)</span>
|
||||||
|
</template>
|
||||||
|
<template #rightSlot>
|
||||||
|
<span class="tip">点击可跳转设备详情</span>
|
||||||
|
</template>
|
||||||
|
</card-nav>
|
||||||
|
<ul class="userDevicesList beautify_scroll">
|
||||||
|
<li v-for="(item,index) in userDevicesList" :key="index" class="userDevicesItem">
|
||||||
|
<div class="userDevicesItem_item">
|
||||||
|
<div class="userDevicesName">
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
</div>
|
||||||
|
<el-tag v-if="item.status === 0">在线</el-tag>
|
||||||
|
<el-tag v-else>离线</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="userDevices_describe">
|
||||||
|
{{ item.model }} · {{ item.mac }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="commonCard">
|
||||||
|
<card-nav line-color="#14b8a6" title="会话信息"></card-nav>
|
||||||
|
<ul class="cardUserInfo" style="margin-bottom: 0">
|
||||||
|
<li class="cardUserInfo_item">
|
||||||
|
<span class="label">会话 ID</span>
|
||||||
|
<span class="value">U20260918001</span>
|
||||||
|
</li>
|
||||||
|
<li class="cardUserInfo_item">
|
||||||
|
<span class="label">会话开始时间</span>
|
||||||
|
<span class="value">2026-09-10 12:44:52</span>
|
||||||
|
</li>
|
||||||
|
<li class="cardUserInfo_item">
|
||||||
|
<span class="label">接入方式</span>
|
||||||
|
<span class="value">机器人接待 → 转人工</span>
|
||||||
|
</li>
|
||||||
|
<li class="cardUserInfo_item">
|
||||||
|
<span class="label">当前等待时长</span>
|
||||||
|
<span class="value" style="color: #EA580C">00:38</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.Main {
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
padding-top: 20px;
|
||||||
|
|
||||||
|
.cardUserInfoBox {
|
||||||
|
.cardUserInfo {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
min-height: 128px;
|
||||||
|
margin-top: 27px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.cardUserInfo_item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
height: 14px;
|
||||||
|
line-height: 14px;
|
||||||
|
color: rgba(107, 114, 128, 1);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
height: 16px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: rgba(17, 24, 39, 1);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userDevicesBox {
|
||||||
|
.titleName {
|
||||||
|
color: rgba(31, 41, 55, 1);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
line-height: 14px;
|
||||||
|
color: rgba(156, 163, 175, 1);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userDevicesList {
|
||||||
|
padding-right: 10px;
|
||||||
|
min-height: (75+8)*3px;
|
||||||
|
max-height: (75+8)*3px;
|
||||||
|
margin-top: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.userDevicesItem {
|
||||||
|
min-height: 64px;
|
||||||
|
border-radius: 12px 12px 12px 12px;
|
||||||
|
background-color: rgba(247, 249, 252, 1);
|
||||||
|
border: 1px solid rgba(238, 240, 243, 1);
|
||||||
|
padding: 15px 13px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.userDevicesItem_item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
.userDevicesName {
|
||||||
|
width: 230px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 16px;
|
||||||
|
color: rgba(17, 24, 39, 1);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userDevices_describe {
|
||||||
|
line-height: 14px;
|
||||||
|
color: rgba(156, 163, 175, 1);
|
||||||
|
font-size: 11px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userWindowBox {
|
||||||
|
padding: 25px 0;
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
.userWindowNav.userWindowNavItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 21px;
|
||||||
|
padding-bottom: 19px;
|
||||||
|
|
||||||
|
.userAvatar {
|
||||||
|
width: 41px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: rgba(241, 245, 249, 1);
|
||||||
|
object-fit: contain;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userWindowNavItem-l {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.userWindowNavItem-l-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
height: 18px;
|
||||||
|
line-height: 18px;
|
||||||
|
color: rgba(17, 24, 39, 1);
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone, .time {
|
||||||
|
line-height: 14px;
|
||||||
|
color: rgba(156, 163, 175, 1);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userWindowNavItem-l-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: rgba(107, 114, 128, 1);
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
display: block;
|
||||||
|
width: 7px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-right: 6px;
|
||||||
|
background-color: rgba(22, 163, 74, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height: 650px;
|
||||||
|
background-color: rgba(247, 248, 250, 1);
|
||||||
|
padding: 20px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 0 21px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-2 app-container monitor-online-page">
|
<div class="p-2 app-container monitor-online-page">
|
||||||
<Pagecrumb></Pagecrumb>
|
<Pagecrumb style="margin-top:22px"></Pagecrumb>
|
||||||
<div class="search-wrap">
|
<div class="search-wrap">
|
||||||
<el-card class="search-panel" shadow="hover">
|
<el-card class="search-panel" shadow="hover">
|
||||||
<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="query-form" label-position="top">
|
<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="query-form" label-position="top">
|
||||||
@@ -22,10 +22,8 @@
|
|||||||
size="large"
|
size="large"
|
||||||
style="width: 150px"
|
style="width: 150px"
|
||||||
>
|
>
|
||||||
<el-option label="机器人" value="0"></el-option>
|
<el-option v-for="(item,index) in com_session_state " :key="index" :label="item.label"
|
||||||
<el-option label="排队" value="1"></el-option>
|
:value="item.value"></el-option>
|
||||||
<el-option label="人工" value="2"></el-option>
|
|
||||||
<el-option label="已结束" value="3"></el-option>
|
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="问题类型" prop="problemType">
|
<el-form-item label="问题类型" prop="problemType">
|
||||||
@@ -87,10 +85,10 @@
|
|||||||
<span style="margin:0 8px;color: #fff;">全部</span>
|
<span style="margin:0 8px;color: #fff;">全部</span>
|
||||||
<span style="color: #fff;font-weight: bold">1286</span>
|
<span style="color: #fff;font-weight: bold">1286</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
<tableTag circle-color="#7c3aed" num="862" title="机器人接待"></tableTag>
|
<tableTag :num="862" circle-color="#7c3aed" title="机器人接待"></tableTag>
|
||||||
<tableTag circle-color="#ea580c" num="7" title="排队等待"></tableTag>
|
<tableTag :num="7" circle-color="#ea580c" title="排队等待"></tableTag>
|
||||||
<tableTag num="12" title="人工接待"></tableTag>
|
<tableTag :num="12" title="人工接待"></tableTag>
|
||||||
<tableTag circle-color="#94a3b8" num="405" title="已结束"></tableTag>
|
<tableTag :num="405" circle-color="#94a3b8" title="已结束"></tableTag>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<el-button plain type="info">批量导出</el-button>
|
<el-button plain type="info">批量导出</el-button>
|
||||||
@@ -106,41 +104,55 @@
|
|||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-table-column align="center" type="selection"></el-table-column>
|
<el-table-column align="center" type="selection"></el-table-column>
|
||||||
<el-table-column :show-overflow-tooltip="true" align="center" label="会话ID" prop="tokenId"/>
|
<el-table-column :show-overflow-tooltip="true" align="center" label="会话ID" prop="sessionNo"/>
|
||||||
<el-table-column :show-overflow-tooltip="true" align="center" label="用户信息" prop="userName"/>
|
<el-table-column :show-overflow-tooltip="true" align="center" label="用户信息" prop="userNickName"/>
|
||||||
<el-table-column :show-overflow-tooltip="true" align="center" label="关联设备" prop="clientKey"/>
|
<el-table-column :show-overflow-tooltip="true" align="center" label="关联设备" prop="deviceId"/>
|
||||||
<el-table-column align="center" label="会话状态">
|
<el-table-column align="center" label="会话状态" width="100">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag type="primary">在线</el-tag>
|
<dict-tag :options="com_session_state" :value="scope.row.status"></dict-tag>
|
||||||
<el-tag type="danger">离线</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column :show-overflow-tooltip="true" align="center" label="当前接待人" prop="deptName"/>
|
<el-table-column :show-overflow-tooltip="true" align="center" label="当前接待人" prop="agentName" width="160">
|
||||||
<el-table-column :show-overflow-tooltip="true" align="center" label="用户问题摘要" prop="ipaddr"/>
|
<template #default="scope">
|
||||||
<el-table-column :show-overflow-tooltip="true" align="center" label="排队序号" prop="loginLocation"/>
|
<div class="name">{{ scope.row.agentName }}</div>
|
||||||
<el-table-column :show-overflow-tooltip="true" align="center" label="等待时长" prop="os"/>
|
<div class="no"></div>
|
||||||
<el-table-column :show-overflow-tooltip="true" align="center" label="最后消息时间" prop="browser"/>
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column :show-overflow-tooltip="true" align="center" label="用户问题摘要" prop="summary"/>
|
||||||
|
<el-table-column :show-overflow-tooltip="true" align="center" label="排队序号" prop="queueNo" width="100"/>
|
||||||
|
<el-table-column :show-overflow-tooltip="true" align="center" label="等待时长" prop="os" width="100"/>
|
||||||
|
<el-table-column :show-overflow-tooltip="true" align="center" label="最后消息时间" prop="lastMessageTime"
|
||||||
|
width="160"/>
|
||||||
<el-table-column align="center" class-name="small-padding fixed-width" label="操作" min-width="200">
|
<el-table-column align="center" class-name="small-padding fixed-width" label="操作" min-width="200">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<div>
|
<div>
|
||||||
|
<el-button
|
||||||
|
v-if="scope.row.staus !== '3'"
|
||||||
|
v-hasPermi="['cs:session:query']"
|
||||||
|
type="primary"
|
||||||
|
@click="handleOpenConversation(scope.row)"
|
||||||
|
>进入会话
|
||||||
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPermi="['monitor:online:forceLogout']"
|
v-hasPermi="['monitor:online:forceLogout']"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
v-else
|
||||||
@click="handleMain(scope.row)"
|
@click="handleMain(scope.row)"
|
||||||
>进入会话
|
>查看会话
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPermi="['monitor:online:forceLogout']"
|
v-hasPermi="['monitor:online:forceLogout']"
|
||||||
plain
|
plain
|
||||||
type="info"
|
type="info"
|
||||||
@click="handleMain(scope.row)"
|
@click="handleUserMain(scope.row)"
|
||||||
>用户档案
|
>用户档案
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPermi="['monitor:online:forceLogout']"
|
v-hasPermi="['monitor:online:forceLogout']"
|
||||||
plain
|
plain
|
||||||
type="danger"
|
type="danger"
|
||||||
@click="handleMain(scope.row)"
|
v-if="scope.row.status === '2'"
|
||||||
|
@click="handleCloseConversation(scope.row)"
|
||||||
>关闭会话
|
>关闭会话
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,13 +172,13 @@
|
|||||||
|
|
||||||
<script lang="ts" name="Online" setup>
|
<script lang="ts" name="Online" setup>
|
||||||
import {to} from 'await-to-js';
|
import {to} from 'await-to-js';
|
||||||
import {listCsSession as initData} from '@/api/system/conversation/index';
|
import {closeCsSession, listCsSession as initData} from '@/api/system/conversation/index';
|
||||||
import {CsSessionQuery, CsSessionVO} from '@/api/system/conversation/types';
|
import {CsSessionQuery, CsSessionVO} from '@/api/system/conversation/types';
|
||||||
import modal from '@/plugins/modal';
|
import modal from '@/plugins/modal';
|
||||||
import {useDict} from '@/utils/dict';
|
import {useDict} from '@/utils/dict';
|
||||||
import {parseTime} from '@/utils/ruoyi';
|
import {parseTime} from '@/utils/ruoyi';
|
||||||
|
|
||||||
const {sys_device_type} = toRefs<any>(useDict('sys_device_type'));
|
const {com_session_state} = toRefs<any>(useDict('com_session_state'));
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -202,7 +214,35 @@ const resetQuery = () => {
|
|||||||
handleQuery();
|
handleQuery();
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleMain(row) {
|
// 用户 档案
|
||||||
|
function handleUserMain(row: Partial<CsSessionVO>) {
|
||||||
|
router.push({
|
||||||
|
path: '/customer/customerMain',
|
||||||
|
query: {
|
||||||
|
id: row.userId,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进入会话
|
||||||
|
function handleOpenConversation(row: Partial<CsSessionVO>) {
|
||||||
|
router.push({
|
||||||
|
path: '/conversation/conversationMain',
|
||||||
|
query: {
|
||||||
|
id: row.sessionId,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// close conversation
|
||||||
|
function handleCloseConversation(row: Partial<CsSessionVO>) {
|
||||||
|
closeCsSession(row.sessionId).then(() => {
|
||||||
|
getList();
|
||||||
|
modal.msgSuccess('关闭会话成功')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMain(row: Partial<CsSessionVO>) {
|
||||||
console.log(row);
|
console.log(row);
|
||||||
router.push({
|
router.push({
|
||||||
path: '/conversation/conversationMain'
|
path: '/conversation/conversationMain'
|
||||||
|
|||||||
@@ -0,0 +1,408 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import pageCrumb from '@/components/Pagecrumb/index.vue'
|
||||||
|
|
||||||
|
|
||||||
|
import ChatNavBar from "./compontents/ChatNavBar.vue";
|
||||||
|
import {ChatNavBarType} from "./compontents/types";
|
||||||
|
|
||||||
|
import MessageList from './compontents/MessageList/index.vue'
|
||||||
|
import {initMessages, userAvatar} from "./compontents/data";
|
||||||
|
import ChatInput from "@/views/system/conversation/compontents/ChatInput.vue";
|
||||||
|
|
||||||
|
const defaultImage = import.meta.env.VITE_DEFAULT_USER_IMAGE;
|
||||||
|
const userDevicesList = [
|
||||||
|
{
|
||||||
|
label: 'Z2 空气净化器 · 客厅',
|
||||||
|
model: 'CZ-Z2023',
|
||||||
|
mac: "AA:BB:CC:DD:A3:B6",
|
||||||
|
status: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Z2 空气净化器 · 主卧',
|
||||||
|
model: 'CZ-Z2023',
|
||||||
|
mac: "AA:BB:CC:DD:A3:B1",
|
||||||
|
status: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Z2 空气净化器 · 儿童房',
|
||||||
|
model: 'CZ-Z2023',
|
||||||
|
mac: "AA:BB:CC:DD:A3:B2",
|
||||||
|
status: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Z2 空气净化器 · 次卧',
|
||||||
|
model: 'CZ-Z2023',
|
||||||
|
mac: "AA:BB:CC:DD:A3:B3",
|
||||||
|
status: 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
status: ChatNavBarType.await as ChatNavBarType,
|
||||||
|
time: new Date().toLocaleTimeString(),
|
||||||
|
awaitNum: 3,
|
||||||
|
agentInfo: {
|
||||||
|
no: '8823',
|
||||||
|
name: '小雨'
|
||||||
|
},
|
||||||
|
message: []
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
form.value.message = [...initMessages]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const sending = ref(false)
|
||||||
|
// 发送文本消息
|
||||||
|
const onSendMsg = async (text: string) => {
|
||||||
|
sending.value = true
|
||||||
|
// 组装消息,推入messages列表
|
||||||
|
const newMsg = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
type: 'chat',
|
||||||
|
role: 'user',
|
||||||
|
msgType: 'text',
|
||||||
|
content: text,
|
||||||
|
time: new Date().toLocaleTimeString(),
|
||||||
|
status: 'sending'
|
||||||
|
}
|
||||||
|
console.log(text)
|
||||||
|
form.value.message.push(newMsg)
|
||||||
|
try {
|
||||||
|
// 请求后端接口
|
||||||
|
// await api.sendChat(...)
|
||||||
|
newMsg.status = 'sent'
|
||||||
|
} catch (err) {
|
||||||
|
newMsg.status = 'failed'
|
||||||
|
} finally {
|
||||||
|
sending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片上传,拿到File,上传oss拿到url,生成图片消息
|
||||||
|
const onUploadImage = async (file: File) => {
|
||||||
|
// 调用oss上传接口,拿到真实图片url
|
||||||
|
// const url = await uploadOss(file)
|
||||||
|
const url = 'https://xxx.oss-cn-beijing.aliyuncs.com/demo.jpg'
|
||||||
|
const imgMsg = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
type: 'chat',
|
||||||
|
role: 'user',
|
||||||
|
msgType: 'image',
|
||||||
|
content: url,
|
||||||
|
time: new Date().toLocaleTimeString(),
|
||||||
|
status: 'sending'
|
||||||
|
}
|
||||||
|
form.value.message.push(imgMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="Main beautify_scroll">
|
||||||
|
<pageCrumb>
|
||||||
|
<template #rightSlot>
|
||||||
|
<el-button plain type="info">返回会话列表</el-button>
|
||||||
|
</template>
|
||||||
|
</pageCrumb>
|
||||||
|
<div class="commonCard">
|
||||||
|
<card-nav
|
||||||
|
:show-line="false"
|
||||||
|
subTitle="开始时间 2026-09-10 12:44:52 · 等待时长 00:38 · 接入方式:机器人接待 → 转人工"
|
||||||
|
title="CS20260918100038"
|
||||||
|
>
|
||||||
|
<template #rightTitle>
|
||||||
|
<el-tag type="primary">人工接待</el-tag>
|
||||||
|
</template>
|
||||||
|
<template #rightSlot>
|
||||||
|
<el-button plain type="default">导出聊天记录</el-button>
|
||||||
|
<el-button plain type="danger">关闭会话</el-button>
|
||||||
|
</template>
|
||||||
|
</card-nav>
|
||||||
|
</div>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :lg="16" :md="15" :span="24" :xl="18">
|
||||||
|
<div class="commonCard userWindowBox">
|
||||||
|
<div class="userWindowNav userWindowNavItem">
|
||||||
|
<el-image :src="defaultImage" class="userAvatar"></el-image>
|
||||||
|
<div class="userWindowNavItem-l">
|
||||||
|
<div class="userWindowNavItem-l-info">
|
||||||
|
<div>
|
||||||
|
<span class="name">林筱曼</span>
|
||||||
|
<span class="phone">138****6688</span>
|
||||||
|
</div>
|
||||||
|
<span class="time">会话时长 06:12</span>
|
||||||
|
</div>
|
||||||
|
<div class="userWindowNavItem-l-status">
|
||||||
|
<i class="circle"></i>
|
||||||
|
<span>用户在线</span>
|
||||||
|
<span style="margin:0 0.5rem;">·</span>
|
||||||
|
<span>正在输入</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content beautify_scroll">
|
||||||
|
<ChatNavBar
|
||||||
|
:agent-info="form.agentInfo"
|
||||||
|
:await-num="form.awaitNum"
|
||||||
|
:time="form.time"
|
||||||
|
:type="form.status"
|
||||||
|
></ChatNavBar>
|
||||||
|
<MessageList :messages="form.message" :user-avatar="userAvatar"></MessageList>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<!-- 输入框组件 -->
|
||||||
|
<ChatInput :sending="sending" @send="onSendMsg" @uploadImage="onUploadImage"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :lg="8" :md="9" :span="24" :xl="6">
|
||||||
|
<div class="commonCard cardUserInfoBox">
|
||||||
|
<card-nav title="用户信息">
|
||||||
|
<template slot="rightTitle">
|
||||||
|
<el-tag type="primary">用户详情</el-tag>
|
||||||
|
</template>
|
||||||
|
</card-nav>
|
||||||
|
<ul class="cardUserInfo">
|
||||||
|
<li class="cardUserInfo_item">
|
||||||
|
<span class="label">用户ID</span>
|
||||||
|
<span class="value">U20260918001</span>
|
||||||
|
</li>
|
||||||
|
<li class="cardUserInfo_item">
|
||||||
|
<span class="label">昵称</span>
|
||||||
|
<span class="value">林小满</span>
|
||||||
|
</li>
|
||||||
|
<li class="cardUserInfo_item">
|
||||||
|
<span class="label">手机号</span>
|
||||||
|
<span class="value">138****6688</span>
|
||||||
|
</li>
|
||||||
|
<li class="cardUserInfo_item">
|
||||||
|
<span class="label">注册时间</span>
|
||||||
|
<span class="value">2026-01-10 14:20</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="userDevicesBox">
|
||||||
|
<card-nav :show-line="false">
|
||||||
|
<template #title>
|
||||||
|
<span class="titleName">名下设备(3台)</span>
|
||||||
|
</template>
|
||||||
|
<template #rightSlot>
|
||||||
|
<span class="tip">点击可跳转设备详情</span>
|
||||||
|
</template>
|
||||||
|
</card-nav>
|
||||||
|
<ul class="userDevicesList beautify_scroll">
|
||||||
|
<li v-for="(item,index) in userDevicesList" :key="index" class="userDevicesItem">
|
||||||
|
<div class="userDevicesItem_item">
|
||||||
|
<div class="userDevicesName">
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
</div>
|
||||||
|
<el-tag v-if="item.status === 0">在线</el-tag>
|
||||||
|
<el-tag v-else>离线</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="userDevices_describe">
|
||||||
|
{{ item.model }} · {{ item.mac }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="commonCard">
|
||||||
|
<card-nav line-color="#14b8a6" title="会话信息"></card-nav>
|
||||||
|
<ul class="cardUserInfo" style="margin-bottom: 0">
|
||||||
|
<li class="cardUserInfo_item">
|
||||||
|
<span class="label">会话 ID</span>
|
||||||
|
<span class="value">U20260918001</span>
|
||||||
|
</li>
|
||||||
|
<li class="cardUserInfo_item">
|
||||||
|
<span class="label">会话开始时间</span>
|
||||||
|
<span class="value">2026-09-10 12:44:52</span>
|
||||||
|
</li>
|
||||||
|
<li class="cardUserInfo_item">
|
||||||
|
<span class="label">接入方式</span>
|
||||||
|
<span class="value">机器人接待 → 转人工</span>
|
||||||
|
</li>
|
||||||
|
<li class="cardUserInfo_item">
|
||||||
|
<span class="label">当前等待时长</span>
|
||||||
|
<span class="value" style="color: #EA580C">00:38</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.Main {
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
padding-top: 20px;
|
||||||
|
|
||||||
|
.cardUserInfoBox {
|
||||||
|
.cardUserInfo {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
min-height: 128px;
|
||||||
|
margin-top: 27px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.cardUserInfo_item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
height: 14px;
|
||||||
|
line-height: 14px;
|
||||||
|
color: rgba(107, 114, 128, 1);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
height: 16px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: rgba(17, 24, 39, 1);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userDevicesBox {
|
||||||
|
.titleName {
|
||||||
|
color: rgba(31, 41, 55, 1);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
line-height: 14px;
|
||||||
|
color: rgba(156, 163, 175, 1);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userDevicesList {
|
||||||
|
padding-right: 10px;
|
||||||
|
min-height: (75+8)*3px;
|
||||||
|
max-height: (75+8)*3px;
|
||||||
|
margin-top: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.userDevicesItem {
|
||||||
|
min-height: 64px;
|
||||||
|
border-radius: 12px 12px 12px 12px;
|
||||||
|
background-color: rgba(247, 249, 252, 1);
|
||||||
|
border: 1px solid rgba(238, 240, 243, 1);
|
||||||
|
padding: 15px 13px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.userDevicesItem_item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
.userDevicesName {
|
||||||
|
width: 230px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 16px;
|
||||||
|
color: rgba(17, 24, 39, 1);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userDevices_describe {
|
||||||
|
line-height: 14px;
|
||||||
|
color: rgba(156, 163, 175, 1);
|
||||||
|
font-size: 11px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userWindowBox {
|
||||||
|
padding: 25px 0;
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
.userWindowNav.userWindowNavItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 21px;
|
||||||
|
padding-bottom: 19px;
|
||||||
|
|
||||||
|
.userAvatar {
|
||||||
|
width: 41px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: rgba(241, 245, 249, 1);
|
||||||
|
object-fit: contain;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userWindowNavItem-l {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.userWindowNavItem-l-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
height: 18px;
|
||||||
|
line-height: 18px;
|
||||||
|
color: rgba(17, 24, 39, 1);
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone, .time {
|
||||||
|
line-height: 14px;
|
||||||
|
color: rgba(156, 163, 175, 1);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userWindowNavItem-l-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: rgba(107, 114, 128, 1);
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
display: block;
|
||||||
|
width: 7px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-right: 6px;
|
||||||
|
background-color: rgba(22, 163, 74, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height: 650px;
|
||||||
|
background-color: rgba(247, 248, 250, 1);
|
||||||
|
padding: 20px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 0 21px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,17 +1,41 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import FormItemTip from "@/components/FormItemTip/index.vue";
|
import FormItemTip from "@/components/FormItemTip/index.vue";
|
||||||
import cskbEditor from '@/components/Editor/cskb.vue'
|
import cskbEditor from '@/components/Editor/cskb.vue'
|
||||||
|
import {FormInstance, FormRules} from "element-plus";
|
||||||
|
import {addCsKb} from "@/api/system/cskb";
|
||||||
|
import modal from "@/plugins/modal";
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = ref({
|
const form = ref({
|
||||||
phone: "",
|
"title": "",
|
||||||
username: "",
|
"category": "",
|
||||||
sex: 1,
|
"content": "",
|
||||||
creatDay: '',
|
"sort": 1,
|
||||||
status: 1,
|
"status": "1",
|
||||||
quickAction: [],
|
"buttons": []
|
||||||
img: ''
|
|
||||||
})
|
})
|
||||||
|
// ========= 表单校验 rules =========
|
||||||
|
const rules: FormRules = {
|
||||||
|
title: [
|
||||||
|
{required: true, message: '请输入问题标题', trigger: 'blur'},
|
||||||
|
{max: 30, message: '标题最多30个字符', trigger: 'blur'}
|
||||||
|
],
|
||||||
|
category: [
|
||||||
|
{required: true, message: '请选择问题分类', trigger: 'change'}
|
||||||
|
],
|
||||||
|
content: [
|
||||||
|
{required: true, message: '请填写回复内容', trigger: 'blur'}
|
||||||
|
],
|
||||||
|
sort: [
|
||||||
|
{required: true, message: '请输入排序号', trigger: 'blur'},
|
||||||
|
{type: 'number', min: 0, message: '排序号不能小于0', trigger: 'blur'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const actions = {
|
||||||
|
resolved: {"buttonCode": "resolved", "label": "已解决", "sort": 1, value: 'resolved'},
|
||||||
|
transfer_human: {"buttonCode": "transfer_human", "label": "转人工", "sort": 2, value: 'transfer_human'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleBack() {
|
function handleBack() {
|
||||||
@@ -20,16 +44,43 @@ function handleBack() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function submit() {
|
async function submit(isEnable = false) {
|
||||||
|
if (!formRef.value) return
|
||||||
console.log(form.value)
|
console.log(form.value)
|
||||||
|
await formRef.value.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
// isEnable=true 代表保存并启用
|
||||||
|
form.value.buttons = form.value.buttons.map(item => {
|
||||||
|
return actions[item]
|
||||||
|
})
|
||||||
|
addCsKb(form.value).then(() => {
|
||||||
|
modal.msgSuccess('添加成功')
|
||||||
|
if (isEnable) {
|
||||||
|
handleBack()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
form.value = {
|
||||||
|
"title": "",
|
||||||
|
"category": "",
|
||||||
|
"content": "",
|
||||||
|
"sort": 1,
|
||||||
|
"status": "1",
|
||||||
|
"buttons": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="edit">
|
<div class="edit">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between" style="padding-top: 22px">
|
||||||
<pagecrumb></pagecrumb>
|
<pagecrumb></pagecrumb>
|
||||||
<el-button plain type="info" @click="handleBack"> 返回客服列表</el-button>
|
<el-button plain type="info" @click="handleBack"> 返回知识库管理</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="tipBox">
|
<div class="tipBox">
|
||||||
<div class="tip1">新增的知识条目保存后需要「启用」才会在 APP 端展示
|
<div class="tip1">新增的知识条目保存后需要「启用」才会在 APP 端展示
|
||||||
@@ -47,55 +98,41 @@ function submit() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="formbox">
|
<div class="formbox">
|
||||||
<el-form label-position="top">
|
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
基础信息
|
基础信息
|
||||||
</div>
|
</div>
|
||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.phone"
|
v-model.trim="form.title"
|
||||||
:inputAttrs="{ placeholder: '请输入问题标题,如「连不上 Wi-Fi」', showWordLimit:true ,maxlength:30}"
|
:inputAttrs="{ placeholder: '请输入问题标题,如「连不上 Wi-Fi」', showWordLimit:true ,maxlength:30}"
|
||||||
:required="true"
|
|
||||||
label="问题标题"
|
label="问题标题"
|
||||||
prop="phone"
|
prop="title"
|
||||||
tip="建议使用用户口语化问法,便于模糊匹配"
|
tip="建议使用用户口语化问法,便于模糊匹配"
|
||||||
>
|
>
|
||||||
</FormItemTip>
|
</FormItemTip>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.username"
|
v-model.trim="form.category"
|
||||||
:inputAttrs="{ placeholder: '请选择问题分类'}"
|
:inputAttrs="{ placeholder: '请选择问题分类', maxlength:30}"
|
||||||
:required="true"
|
|
||||||
label="问题分类"
|
label="问题分类"
|
||||||
prop="username"
|
prop="category"
|
||||||
>
|
>
|
||||||
</FormItemTip>
|
</FormItemTip>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<div>回复内容 <span>*</span></div>
|
|
||||||
<div>
|
|
||||||
<cskbEditor v-model="form.creatDay" :simple-toolbar="true"></cskbEditor>
|
|
||||||
</div>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-row :gutter="24">
|
|
||||||
<el-col :span="12">
|
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.status"
|
label="回复内容"
|
||||||
:inputAttrs="{ placeholder: '', disabled: false }"
|
prop="content"
|
||||||
:radioOptions="[
|
type="default"
|
||||||
{label:'启用',value:1},
|
|
||||||
{label:'禁用',value:0},
|
|
||||||
]"
|
|
||||||
label="账号状态"
|
|
||||||
prop="status"
|
|
||||||
tip="禁用后该客服无法登录工作台,其接待中会话将转回排队"
|
|
||||||
type="radio"
|
|
||||||
>
|
>
|
||||||
|
<template #default>
|
||||||
|
<cskbEditor v-model="form.content" :simple-toolbar="true" style="width: 100%"></cskbEditor>
|
||||||
|
</template>
|
||||||
</FormItemTip>
|
</FormItemTip>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -105,13 +142,10 @@ function submit() {
|
|||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.quickAction"
|
v-model="form.buttons"
|
||||||
:radio-options="[
|
:radio-options="Object.values(actions)"
|
||||||
{label: '已解决',value:1},
|
|
||||||
{label: '转人工',value:0}
|
|
||||||
]"
|
|
||||||
label="关联快捷按钮"
|
label="关联快捷按钮"
|
||||||
prop="phone"
|
prop="buttons"
|
||||||
tip="回复气泡下方展示的引导按钮,可多选"
|
tip="回复气泡下方展示的引导按钮,可多选"
|
||||||
type="checkBox"
|
type="checkBox"
|
||||||
>
|
>
|
||||||
@@ -119,11 +153,10 @@ function submit() {
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.username"
|
v-model.number="form.sort"
|
||||||
:inputAttrs="{ placeholder: '请输入排序'}"
|
:inputAttrs="{ placeholder: '请输入排序'}"
|
||||||
:required="true"
|
|
||||||
label="排序号"
|
label="排序号"
|
||||||
prop="username"
|
prop="sort"
|
||||||
tip="决定该条目在 APP 常见问题列表中的展示顺序"
|
tip="决定该条目在 APP 常见问题列表中的展示顺序"
|
||||||
type="inputNumber"
|
type="inputNumber"
|
||||||
>
|
>
|
||||||
@@ -134,6 +167,11 @@ function submit() {
|
|||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
|
v-model="form.status"
|
||||||
|
:inputAttrs="{
|
||||||
|
activeValue: '1',
|
||||||
|
inactiveValue: '0',
|
||||||
|
}"
|
||||||
label="启用状态"
|
label="启用状态"
|
||||||
tip="关闭则保存为草稿,仅后台可见"
|
tip="关闭则保存为草稿,仅后台可见"
|
||||||
type="switch"
|
type="switch"
|
||||||
@@ -151,9 +189,9 @@ function submit() {
|
|||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div class="footer-tip">标题、分类与回复内容为必填项;排序号越小展示越靠前</div>
|
<div class="footer-tip">标题、分类与回复内容为必填项;排序号越小展示越靠前</div>
|
||||||
<div>
|
<div>
|
||||||
<el-button>取消</el-button>
|
<el-button @click="reset">取消</el-button>
|
||||||
<el-button plain type="primary" @click="submit">保存并继续新增</el-button>
|
<el-button plain type="primary" @click="submit(false)">保存并继续新增</el-button>
|
||||||
<el-button type="primary" @click="submit">保存并启用</el-button>
|
<el-button type="primary" @click="submit(true)">保存并启用</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,7 +199,9 @@ function submit() {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.edit {
|
.edit {
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 100%;
|
||||||
padding-bottom: 50px;
|
padding-bottom: 50px;
|
||||||
|
|
||||||
.tipBox {
|
.tipBox {
|
||||||
|
|||||||
@@ -1,18 +1,81 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import FormItemTip from "@/components/FormItemTip/index.vue";
|
import FormItemTip from "@/components/FormItemTip/index.vue";
|
||||||
import cskbEditor from '@/components/Editor/cskb.vue'
|
import cskbEditor from '@/components/Editor/cskb.vue'
|
||||||
|
import {getCsKb, updateCsKb} from "@/api/system/cskb";
|
||||||
|
import {FormInstance, FormRules} from "element-plus";
|
||||||
|
import modal from "@/plugins/modal";
|
||||||
|
// 表单实例
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
const form = ref({
|
const form = ref({
|
||||||
phone: "",
|
"buttons": [],
|
||||||
username: "",
|
"category": "",
|
||||||
sex: 1,
|
"content": "",
|
||||||
creatDay: '',
|
"createTime": "",
|
||||||
status: 1,
|
"kbId": "",
|
||||||
quickAction: [],
|
"kbNo": "",
|
||||||
img: ''
|
"sort": 1,
|
||||||
|
"status": "1",
|
||||||
|
"statusName": "",
|
||||||
|
"title": ""
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const showForm = ref({
|
||||||
|
...form.value,
|
||||||
|
})
|
||||||
|
// ========== 校验规则 rules ==========
|
||||||
|
const rules: FormRules = {
|
||||||
|
title: [
|
||||||
|
{required: true, message: '请输入问题标题', trigger: 'blur'},
|
||||||
|
{max: 30, message: '标题最多30个字符', trigger: 'blur'}
|
||||||
|
],
|
||||||
|
category: [
|
||||||
|
{required: true, message: '请选择问题分类', trigger: 'change'}
|
||||||
|
],
|
||||||
|
content: [
|
||||||
|
{required: true, message: '请填写回复内容', trigger: 'blur'}
|
||||||
|
],
|
||||||
|
sort: [
|
||||||
|
{required: true, message: '请输入排序权重', trigger: 'blur'},
|
||||||
|
{type: 'number', min: 0, message: '排序不能小于0', trigger: 'blur'}
|
||||||
|
]
|
||||||
|
// buttons、status非必填,不写规则
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (route.query.id) {
|
||||||
|
getCsKb(route.query.id as string).then(res => {
|
||||||
|
form.value = res.data;
|
||||||
|
form.value.buttons = res.data.buttons.map(item => actions[item.buttonCode].value);
|
||||||
|
|
||||||
|
showForm.value = JSON.parse(JSON.stringify(form.value));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
"resolved": {
|
||||||
|
"buttonCode": "resolved",
|
||||||
|
"buttonId": "2108377417902170114",
|
||||||
|
"kbId": "1761700000000002001",
|
||||||
|
"label": "已解决",
|
||||||
|
'value': 'resolved',
|
||||||
|
"sort": 1
|
||||||
|
},
|
||||||
|
transfer_human: {
|
||||||
|
"buttonCode": "transfer_human",
|
||||||
|
"buttonId": "2108377417910558722",
|
||||||
|
"kbId": "1761700000000002001",
|
||||||
|
"label": "转人工",
|
||||||
|
'value': 'transfer_human',
|
||||||
|
"sort": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
|
||||||
function handleBack() {
|
function handleBack() {
|
||||||
router.replace({
|
router.replace({
|
||||||
@@ -20,111 +83,115 @@ function handleBack() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function submit() {
|
// 表单校验提交
|
||||||
console.log(form.value)
|
async function submit() {
|
||||||
|
if (!formRef.value) return
|
||||||
|
await formRef.value.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
// 这里调用新增/编辑接口
|
||||||
|
form.value.buttons = form.value.buttons.map(item => {
|
||||||
|
return actions[item]
|
||||||
|
})
|
||||||
|
console.log('校验通过,提交数据', form.value)
|
||||||
|
updateCsKb(form.value).then(() => {
|
||||||
|
modal.msgSuccess('编辑成功');
|
||||||
|
handleBack()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.log('表单校验失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消按钮:重置表单,清空校验提示
|
||||||
|
function handleCancel() {
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
handleBack()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="edit">
|
<div class="edit">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between" style="padding-top: 22px">
|
||||||
<pagecrumb></pagecrumb>
|
<pagecrumb></pagecrumb>
|
||||||
<el-button plain type="info" @click="handleBack"> 返回客服列表</el-button>
|
<el-button plain type="info" @click="handleBack"> 返回知识库</el-button>
|
||||||
</div>
|
|
||||||
<div class="tipBox">
|
|
||||||
<div class="tip1">新增的知识条目保存后需要「启用」才会在 APP 端展示
|
|
||||||
</div>
|
|
||||||
<div class="tip2">回复内容支持有序步骤与图片,建议控制在 200 字以内,便于用户快速阅读。
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- form -->
|
<!-- form -->
|
||||||
<section class="commonCard">
|
<section class="commonCard">
|
||||||
<div class="flex justify-between flex-items-center">
|
<div class="flex justify-between flex-items-center">
|
||||||
<card-nav subTitle="配置问题标题、分类与标准回复话术" title="新增知识条目"></card-nav>
|
<card-nav :title="'知识条目 · '+ showForm.kbNo"
|
||||||
<div class="unSave">
|
subTitle="维护机器人自动回复内容,启用后将同步至 APP 智能客服常见问题入口"></card-nav>
|
||||||
|
<div v-if="showForm.status === '1'" class="unSave">
|
||||||
<i class="square"></i>
|
<i class="square"></i>
|
||||||
<span>未保存</span>
|
<span>当前已启用</span>
|
||||||
|
</div>
|
||||||
|
<div v-else :class="{
|
||||||
|
unActive: showForm.status === '0'
|
||||||
|
}" class="unSave">
|
||||||
|
<i class="square"></i>
|
||||||
|
<span>未启用</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="formbox">
|
<div class="formbox">
|
||||||
<el-form label-position="top">
|
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
基础信息
|
基础信息
|
||||||
</div>
|
</div>
|
||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.phone"
|
v-model.trim="form.title"
|
||||||
:inputAttrs="{ placeholder: '请输入问题标题,如「连不上 Wi-Fi」', showWordLimit:true ,maxlength:30}"
|
:inputAttrs="{ placeholder: '请输入问题标题,如「连不上 Wi-Fi」', showWordLimit:true ,maxlength:30}"
|
||||||
:required="true"
|
|
||||||
label="问题标题"
|
label="问题标题"
|
||||||
prop="phone"
|
prop="title"
|
||||||
tip="建议使用用户口语化问法,便于模糊匹配"
|
tip="必填,建议使用用户常见问法,如「Z2 空气净化器连不上 Wi-Fi」"
|
||||||
>
|
>
|
||||||
</FormItemTip>
|
</FormItemTip>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.username"
|
v-model="form.category"
|
||||||
:inputAttrs="{ placeholder: '请选择问题分类'}"
|
:inputAttrs="{ placeholder: '请选择问题分类'}"
|
||||||
:required="true"
|
|
||||||
label="问题分类"
|
label="问题分类"
|
||||||
prop="username"
|
prop="category"
|
||||||
|
tip="必填,可选:设备配网 / 耗材滤芯 / 固件升级 / 设备报修"
|
||||||
>
|
>
|
||||||
</FormItemTip>
|
</FormItemTip>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<div>回复内容 <span>*</span></div>
|
|
||||||
<div>
|
|
||||||
<cskbEditor v-model="form.creatDay" :simple-toolbar="true"></cskbEditor>
|
|
||||||
</div>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-row :gutter="24">
|
|
||||||
<el-col :span="12">
|
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.status"
|
label="回复内容"
|
||||||
:inputAttrs="{ placeholder: '', disabled: false }"
|
prop="content"
|
||||||
:radioOptions="[
|
tip="支持图文混排与有序步骤,建议排查步骤不超过 5 步"
|
||||||
{label:'启用',value:1},
|
type="default"
|
||||||
{label:'禁用',value:0},
|
|
||||||
]"
|
|
||||||
label="账号状态"
|
|
||||||
prop="status"
|
|
||||||
tip="禁用后该客服无法登录工作台,其接待中会话将转回排队"
|
|
||||||
type="radio"
|
|
||||||
>
|
>
|
||||||
|
<template #default>
|
||||||
|
<cskbEditor v-model="form.content" :simple-toolbar="true" style="width: 100%"></cskbEditor>
|
||||||
|
</template>
|
||||||
</FormItemTip>
|
</FormItemTip>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<div class="title">
|
|
||||||
关联与排序
|
|
||||||
</div>
|
|
||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.quickAction"
|
v-model="form.buttons"
|
||||||
:radio-options="[
|
:radio-options="Object.values(actions)"
|
||||||
{label: '已解决',value:1},
|
label="底部快捷按钮"
|
||||||
{label: '转人工',value:0}
|
prop="buttons"
|
||||||
]"
|
tip="将展示在机器人回复气泡下方,用户可直接点击"
|
||||||
label="关联快捷按钮"
|
|
||||||
prop="phone"
|
|
||||||
tip="回复气泡下方展示的引导按钮,可多选"
|
|
||||||
type="checkBox"
|
type="checkBox"
|
||||||
>
|
>
|
||||||
</FormItemTip>
|
</FormItemTip>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.username"
|
v-model.number="form.sort"
|
||||||
:inputAttrs="{ placeholder: '请输入排序'}"
|
:inputAttrs="{ placeholder: '请输入排序'}"
|
||||||
:required="true"
|
label="排序权重"
|
||||||
label="排序号"
|
prop="sort"
|
||||||
prop="username"
|
tip="控制该条目在 APP 常见问题列表中的展示顺序"
|
||||||
tip="决定该条目在 APP 常见问题列表中的展示顺序"
|
|
||||||
type="inputNumber"
|
type="inputNumber"
|
||||||
>
|
>
|
||||||
<template #suffix>越小越靠前</template>
|
<template #suffix>越小越靠前</template>
|
||||||
@@ -135,7 +202,13 @@ function submit() {
|
|||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
label="启用状态"
|
label="启用状态"
|
||||||
tip="关闭则保存为草稿,仅后台可见"
|
v-model="form.status"
|
||||||
|
:inputAttrs="{
|
||||||
|
activeValue: '1',
|
||||||
|
inactiveValue: '0',
|
||||||
|
}"
|
||||||
|
prop="status"
|
||||||
|
tip="关闭后该条目不会出现在 APP 常见问题入口"
|
||||||
type="switch"
|
type="switch"
|
||||||
>
|
>
|
||||||
<template #switchRight>
|
<template #switchRight>
|
||||||
@@ -151,9 +224,8 @@ function submit() {
|
|||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div class="footer-tip">标题、分类与回复内容为必填项;排序号越小展示越靠前</div>
|
<div class="footer-tip">标题、分类与回复内容为必填项;排序号越小展示越靠前</div>
|
||||||
<div>
|
<div>
|
||||||
<el-button>取消</el-button>
|
<el-button @click="handleCancel">取消</el-button>
|
||||||
<el-button plain type="primary" @click="submit">保存并继续新增</el-button>
|
<el-button type="primary" @click="submit">保存</el-button>
|
||||||
<el-button type="primary" @click="submit">保存并启用</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -237,23 +309,34 @@ function submit() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 79px;
|
min-width: 98px;
|
||||||
|
padding: 0 10px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
background-color: rgba(248, 250, 252, 1);
|
background-color: rgba(232, 247, 238, 1);
|
||||||
|
color: rgba(22, 163, 74, 1);
|
||||||
border: 1px solid rgba(241, 245, 249, 1);
|
border: 1px solid rgba(241, 245, 249, 1);
|
||||||
|
border-radius: 14px;
|
||||||
|
|
||||||
|
&.unActive {
|
||||||
|
color: #4B5563;
|
||||||
|
background-color: rgba(245, 247, 251, 1);
|
||||||
|
|
||||||
|
.square {
|
||||||
|
background-color: #4B5563;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.square {
|
.square {
|
||||||
display: block;
|
display: block;
|
||||||
width: 8px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
background-color: rgba(148, 163, 184, 1);
|
background-color: rgba(22, 163, 74, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
height: 14px;
|
height: 14px;
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
color: rgba(107, 114, 128, 1);
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-2 page-shell system-csKb-page">
|
<div class="p-2 page-shell system-csKb-page">
|
||||||
<div class="search-wrap">
|
<div class="search-wrap" style="padding-top:22px">
|
||||||
<pagecrumb></pagecrumb>
|
<pagecrumb></pagecrumb>
|
||||||
<el-card :class="{ 'is-collapsed': !showSearch }" class="search-panel" shadow="hover" style="margin-top:12px;">
|
<el-card :class="{ 'is-collapsed': !showSearch }" class="search-panel" shadow="hover" style="margin-top:12px;">
|
||||||
<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="query-form" label-position="top">
|
<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="query-form" label-position="top">
|
||||||
<el-form-item label="问题标题" prop="title">
|
<el-form-item label="问题标题" prop="title">
|
||||||
<el-input v-model="queryParams.title" clearable placeholder="请输入问题标题" size="large"
|
<el-input v-model.trim="queryParams.title" clearable placeholder="请输入问题标题" size="large"
|
||||||
@keyup.enter="handleQuery"/>
|
@keyup.enter="handleQuery"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="分类" prop="category">
|
<el-form-item label="分类" prop="category">
|
||||||
@@ -14,8 +14,8 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="状态" prop="status">
|
<el-form-item label="状态" prop="status">
|
||||||
<el-select v-model="queryParams.status" clearable placeholder="请选择状态" size="large">
|
<el-select v-model="queryParams.status" clearable placeholder="请选择状态" size="large">
|
||||||
<el-option :value="true" label="是"/>
|
<el-option label="启用" value="1"/>
|
||||||
<el-option :value="false" label="否"/>
|
<el-option label="禁用" value="0"/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
@@ -32,10 +32,10 @@
|
|||||||
<div>
|
<div>
|
||||||
<el-button type="primary">
|
<el-button type="primary">
|
||||||
<span style="margin:0 8px;color: #fff;">全部</span>
|
<span style="margin:0 8px;color: #fff;">全部</span>
|
||||||
<span style="color: #fff;font-weight: bold">86</span>
|
<span style="color: #fff;font-weight: bold">{{ total }}</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
<tableTag circle-color="#16A34A" num="78" title="启用"></tableTag>
|
<tableTag :num="overView.enabledCount " circle-color="#16A34A" title="启用"></tableTag>
|
||||||
<tableTag circle-color="#ef4444" num="8" num-color="#DC2626" title="禁用"></tableTag>
|
<tableTag :num="overView.stoppedCount" circle-color="#ef4444" num-color="#DC2626" title="禁用"></tableTag>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<el-button :disabled="multiple" plain size="large" type="info" @click="handleDelete()">批量删除
|
<el-button :disabled="multiple" plain size="large" type="info" @click="handleDelete()">批量删除
|
||||||
@@ -50,37 +50,61 @@
|
|||||||
<el-table v-loading="loading" :data="csKbList" border class="data-table"
|
<el-table v-loading="loading" :data="csKbList" border class="data-table"
|
||||||
@selection-change="handleSelectionChange">
|
@selection-change="handleSelectionChange">
|
||||||
<el-table-column align="center" type="selection" width="55"/>
|
<el-table-column align="center" type="selection" width="55"/>
|
||||||
<el-table-column align="center" label="ID" prop="kbId"/>
|
<el-table-column align="center" label="ID" prop="kbNo" width="80"/>
|
||||||
<el-table-column align="center" label="问题标题" prop="title"/>
|
<el-table-column align="center" label="问题标题" prop="title">
|
||||||
<el-table-column align="center" label="分类" prop="category"/>
|
|
||||||
<el-table-column align="center" label="回复内容" prop="content"/>
|
|
||||||
<el-table-column align="center" label="关联快捷按钮" prop="sort"/>
|
|
||||||
<el-table-column align="center" label="排序号" prop="sort"/>
|
|
||||||
<el-table-column align="center" label="状态" prop="status" width="120">
|
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-switch
|
<div class="title" style="text-align: left">{{ scope.row.title }}</div>
|
||||||
v-model="scope.row.status"
|
</template>
|
||||||
:active-value="true"
|
</el-table-column>
|
||||||
:inactive-value="false"
|
<el-table-column align="center" label="分类" prop="category" width="100"/>
|
||||||
disabled
|
<el-table-column align="center" label="回复内容" prop="content">
|
||||||
/>
|
<template #default="scope">
|
||||||
|
<div class="content" @click="privew(scope.row)">
|
||||||
|
<div v-html="getHtmlSummary(scope.row.content,60)"></div>
|
||||||
|
<div v-if="scope.row.content.length > 60" class="more" style="text-align: left">...点击查看全部</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="关联快捷按钮" prop="sort" width="160">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag v-for="(item,index) in scope.row.buttons" :key="index"
|
||||||
|
:type="index % 2 === 0 ?'success' : 'primary'"
|
||||||
|
style="margin-right: 4px">{{ item.label }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="排序号" prop="sort" width="80"/>
|
||||||
|
<el-table-column align="center" label="状态" prop="status" width="80">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag v-if="scope.row.status === '1'" type="success">启用</el-tag>
|
||||||
|
<el-tag v-else-if="scope.row.status === '0'" type="danger">禁用</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="创建时间" prop="status" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ scope.row.createTime }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column align="center" class-name="small-padding fixed-width" label="操作">
|
<el-table-column align="center" class-name="small-padding fixed-width" label="操作">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button v-hasPermi="['system:cskb:edit']" link type="primary"
|
<el-button v-hasPermi="['system:cskb:edit']" :disabled="scope.$index === 0" link type="info"
|
||||||
@click="handleUpdate(scope.row)">上移
|
@click="moveSort(scope.row,'up')">上移
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-hasPermi="['system:cskb:edit']" link type="primary"
|
<el-button v-hasPermi="['system:cskb:edit']" :disabled="scope.$index === csKbList.length-1" link
|
||||||
@click="handleUpdate(scope.row)">下移
|
type="info"
|
||||||
|
@click="moveSort(scope.row,'down')">下移
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-hasPermi="['system:cskb:edit']" link type="primary"
|
<el-button v-hasPermi="['system:cskb:edit']" link type="primary"
|
||||||
@click="handleUpdate(scope.row)">编辑
|
@click="handleUpdate(scope.row)">编辑
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-hasPermi="['system:cskb:edit']" link type="primary"
|
|
||||||
@click="handleUpdate(scope.row)">禁用
|
<el-button v-if="scope.row.status === '0'" v-hasPermi="['system:cskb:edit']" link type="success"
|
||||||
|
@click="handleEnableOrUnEnable(scope.row,'1')">启用
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-hasPermi="['system:cskb:remove']" link type="primary"
|
<el-button v-else v-hasPermi="['system:cskb:edit']" link type="info"
|
||||||
|
@click="handleEnableOrUnEnable(scope.row,'0')">禁用
|
||||||
|
</el-button>
|
||||||
|
<el-button v-hasPermi="['system:cskb:remove']" link type="danger"
|
||||||
@click="handleDelete(scope.row)">删除
|
@click="handleDelete(scope.row)">删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -91,62 +115,9 @@
|
|||||||
:total="total" @pagination="getList"/>
|
:total="total" @pagination="getList"/>
|
||||||
</el-card>
|
</el-card>
|
||||||
<!-- 添加或修改客服知识库对话框 -->
|
<!-- 添加或修改客服知识库对话框 -->
|
||||||
<el-dialog v-model="dialog.visible" :title="dialog.title" append-to-body width="500px">
|
<el-dialog v-model="privewForm.visible" :title="privewForm.title" append-to-body width="800px">
|
||||||
<el-form ref="csKbFormRef" :model="form" :rules="rules" label-width="80px">
|
<div style="min-height: 300px;max-height:800px;overflow-y: auto;overflow-x:hidden"
|
||||||
<el-form-item label="知识编号,如 KB-001" prop="kbNo">
|
v-html="privewForm.content"></div>
|
||||||
<el-input v-model="form.kbNo" placeholder="请输入知识编号,如 KB-001"/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="问题标题" prop="title">
|
|
||||||
<el-input v-model="form.title" placeholder="请输入问题标题"/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="分类" prop="category">
|
|
||||||
<el-input v-model="form.category" placeholder="请输入分类"/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="机器人回复内容">
|
|
||||||
<editor v-model="form.content" :min-height="192"/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="排序权重,数字越小越靠前" prop="sort">
|
|
||||||
<el-input-number v-model="form.sort" controls-position="right"/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="状态" prop="status">
|
|
||||||
<el-switch
|
|
||||||
v-model="form.status"
|
|
||||||
:active-value="true"
|
|
||||||
:inactive-value="false"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="创建部门" prop="createDept">
|
|
||||||
<el-input-number v-model="form.createDept" controls-position="right"/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="创建者" prop="createBy">
|
|
||||||
<el-input-number v-model="form.createBy" controls-position="right"/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="创建时间" prop="createTime">
|
|
||||||
<el-date-picker v-model="form.createTime"
|
|
||||||
clearable
|
|
||||||
placeholder="请选择创建时间"
|
|
||||||
type="datetime"
|
|
||||||
value-format="YYYY-MM-DD HH:mm:ss">
|
|
||||||
</el-date-picker>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="更新者" prop="updateBy">
|
|
||||||
<el-input-number v-model="form.updateBy" controls-position="right"/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="更新时间" prop="updateTime">
|
|
||||||
<el-date-picker v-model="form.updateTime"
|
|
||||||
clearable
|
|
||||||
placeholder="请选择更新时间"
|
|
||||||
type="datetime"
|
|
||||||
value-format="YYYY-MM-DD HH:mm:ss">
|
|
||||||
</el-date-picker>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
|
||||||
<el-button @click="cancel">取 消</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -156,12 +127,13 @@ import {
|
|||||||
addCsKb,
|
addCsKb,
|
||||||
delCsKb,
|
delCsKb,
|
||||||
getCsKb,
|
getCsKb,
|
||||||
listCsKb,
|
listCsKb, moveSkb, overViewCsKb,
|
||||||
updateCsKb
|
updateCsKb
|
||||||
} from '@/api/system/csKb';
|
} from '@/api/system/cskb';
|
||||||
import {CsKbForm, CsKbQuery, CsKbVO} from '@/api/system/csKb/types';
|
import {CsKbForm, CsKbQuery, CsKbVO} from '@/api/system/cskb/types';
|
||||||
import {useLoading} from '@/hooks/async/useLoading';
|
import {useLoading} from '@/hooks/async/useLoading';
|
||||||
import {useFormDialog} from '@/hooks/dialog/useFormDialog';
|
import {useFormDialog} from '@/hooks/dialog/useFormDialog';
|
||||||
|
import {getHtmlSummary} from '@/utils/richtext'
|
||||||
import {useDateRangeQuery} from '@/hooks/form/useDateRangeQuery';
|
import {useDateRangeQuery} from '@/hooks/form/useDateRangeQuery';
|
||||||
import {useSearchReset} from '@/hooks/form/useSearchReset';
|
import {useSearchReset} from '@/hooks/form/useSearchReset';
|
||||||
import {useSearchToggle} from '@/hooks/form/useSearchToggle';
|
import {useSearchToggle} from '@/hooks/form/useSearchToggle';
|
||||||
@@ -171,7 +143,6 @@ import {download as requestDownload} from '@/utils/request';
|
|||||||
|
|
||||||
|
|
||||||
const csKbList = ref<CsKbVO[]>([]);
|
const csKbList = ref<CsKbVO[]>([]);
|
||||||
const buttonLoading = ref(false);
|
|
||||||
const {loading, withLoading} = useLoading(true);
|
const {loading, withLoading} = useLoading(true);
|
||||||
const {showSearch} = useSearchToggle();
|
const {showSearch} = useSearchToggle();
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
@@ -243,17 +214,32 @@ const data = reactive<PageData<CsKbForm, CsKbQuery>>({
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const {queryParams, form, rules} = toRefs(data);
|
const {queryParams, form, rules} = toRefs(data);
|
||||||
const {ids, single, multiple, handleSelectionChange} = useTableSelection<CsKbVO>(item => item.kbId);
|
const {ids, single, multiple, handleSelectionChange} = useTableSelection<CsKbVO>(item => item.kbId);
|
||||||
const {dialog, resetForm: reset, openDialog, showDialog, closeDialog} = useFormDialog({
|
|
||||||
form,
|
|
||||||
formRef: csKbFormRef,
|
const privewForm = ref({
|
||||||
initialFormData: initFormData
|
visible: false,
|
||||||
});
|
title: undefined,
|
||||||
|
content: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function privew(row: Partial<CsKbVO>) {
|
||||||
|
privewForm.value = {
|
||||||
|
visible: true,
|
||||||
|
title: row.title,
|
||||||
|
content: row.content
|
||||||
|
}
|
||||||
|
console.log(privewForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/** 查询客服知识库列表 */
|
/** 查询客服知识库列表 */
|
||||||
const getList = async () => {
|
const getList = async () => {
|
||||||
|
getOverView()
|
||||||
await withLoading(async () => {
|
await withLoading(async () => {
|
||||||
let params = queryParams.value;
|
let params = queryParams.value;
|
||||||
params = applyCreateTimeDateRange(params);
|
params = applyCreateTimeDateRange(params);
|
||||||
@@ -264,12 +250,6 @@ const getList = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 取消按钮 */
|
|
||||||
const cancel = () => {
|
|
||||||
reset();
|
|
||||||
closeDialog();
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 搜索按钮操作 */
|
/** 搜索按钮操作 */
|
||||||
const handleQuery = () => {
|
const handleQuery = () => {
|
||||||
queryParams.value.pageNum = 1;
|
queryParams.value.pageNum = 1;
|
||||||
@@ -293,38 +273,54 @@ const {resetQuery} = useSearchReset({
|
|||||||
|
|
||||||
/** 新增按钮操作 */
|
/** 新增按钮操作 */
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
openDialog('添加客服知识库');
|
router.push({
|
||||||
|
path: '/conversation/cskbAdd'
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 修改按钮操作 */
|
/** 修改按钮操作 */
|
||||||
const handleUpdate = async (row?: Partial<CsKbVO>) => {
|
const handleUpdate = async (row?: Partial<CsKbVO>) => {
|
||||||
reset();
|
router.push({
|
||||||
const _kbId = row?.kbId || ids.value[0];
|
path: '/conversation/cskbEdit',
|
||||||
const res = await getCsKb(_kbId);
|
query: {
|
||||||
Object.assign(form.value, res.data);
|
id: row.kbId,
|
||||||
showDialog('修改客服知识库');
|
}
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 提交按钮 */
|
/** 禁用/启用知识库 */
|
||||||
const submitForm = () => {
|
const handleEnableOrUnEnable = (row: Partial<CsKbVO>, status: '0' | '1' = '1') => {
|
||||||
csKbFormRef.value?.validate(async (valid: boolean) => {
|
updateCsKb({
|
||||||
if (valid) {
|
...row,
|
||||||
buttonLoading.value = true;
|
status,
|
||||||
if (form.value.kbId) {
|
}).then(() => {
|
||||||
await updateCsKb(form.value).finally(() => (buttonLoading.value = false));
|
modal.msgSuccess('操作成功');
|
||||||
} else {
|
getList();
|
||||||
await addCsKb(form.value).finally(() => (buttonLoading.value = false));
|
})
|
||||||
}
|
}
|
||||||
modal.msgSuccess('操作成功');
|
|
||||||
closeDialog();
|
/** 上移/下移 */
|
||||||
await getList();
|
const moveSort = (row: Partial<CsKbVO>, arrow: 'down' | 'up' = 'up') => {
|
||||||
|
moveSkb(row.kbId, arrow).then((res) => {
|
||||||
|
if (res.data.moved) {
|
||||||
|
getList();
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
|
const overView = ref({"enabledCount": 0, "stoppedCount": 0})
|
||||||
|
|
||||||
|
// 总览
|
||||||
|
function getOverView() {
|
||||||
|
overViewCsKb().then(res => {
|
||||||
|
console.log(res)
|
||||||
|
overView.value = res.data;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/** 删除按钮操作 */
|
/** 删除按钮操作 */
|
||||||
const handleDelete = async (row?: Partial<CsKbVO>) => {
|
const handleDelete = async (row?: Partial<CsKbVO>) => {
|
||||||
const _kbIds = row?.kbId || ids.value;
|
const _kbIds = row?.kbNo || ids.value;
|
||||||
await modal.confirm('是否确认删除客服知识库编号为"' + _kbIds + '"的数据项?');
|
await modal.confirm('是否确认删除客服知识库编号为"' + _kbIds + '"的数据项?');
|
||||||
await delCsKb(_kbIds);
|
await delCsKb(_kbIds);
|
||||||
modal.msgSuccess('删除成功');
|
modal.msgSuccess('删除成功');
|
||||||
@@ -350,10 +346,24 @@ onMounted(() => {
|
|||||||
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
ul > li {
|
||||||
|
list-style: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.circle {
|
.circle {
|
||||||
width: 7px;
|
width: 7px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background-color: rgba(124, 58, 237, 1)
|
background-color: rgba(124, 58, 237, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,31 +1,109 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import FormItemTip from "@/components/FormItemTip/index.vue";
|
import FormItemTip from "@/components/FormItemTip/index.vue";
|
||||||
|
import modal from "@/plugins/modal";
|
||||||
|
import {addCsAgent} from "@/api/system/cssAgent";
|
||||||
|
import {uploadOss} from "@/api/system/oss";
|
||||||
|
import {validatePhone} from "@/utils/reg";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
import {ref} from "vue";
|
||||||
|
import type {FormInstance, FormRules} from "element-plus";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
phone: "",
|
"agentNo": "",
|
||||||
username: "",
|
"jobNo": "",
|
||||||
sex: 1,
|
"remark": '',
|
||||||
creatDay: '',
|
"name": "",
|
||||||
status: 1,
|
"account": "",
|
||||||
img: ''
|
"password": "12345678",
|
||||||
|
"phone": "",
|
||||||
|
"status": "0",
|
||||||
|
"avatarUrl": "",
|
||||||
|
avatarOssId: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ========== 表单校验规则 ==========
|
||||||
|
const rules: FormRules = {
|
||||||
|
jobNo: [
|
||||||
|
{required: true, message: '客服工号必填', trigger: 'blur'}
|
||||||
|
],
|
||||||
|
name: [
|
||||||
|
{required: true, message: '客服名称必填', trigger: 'blur'}
|
||||||
|
],
|
||||||
|
account: [
|
||||||
|
{required: true, message: '登录账号(手机号)必填', trigger: 'blur'}
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{required: true, message: '请输入登录密码', trigger: 'blur'},
|
||||||
|
{min: 8, max: 20, message: '密码长度8~20位', trigger: 'blur'}
|
||||||
|
],
|
||||||
|
phone: [
|
||||||
|
{required: true, message: '手机号必填', trigger: 'blur'},
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (!validatePhone(value)) {
|
||||||
|
callback(new Error('手机号格式错误'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
status: [
|
||||||
|
{required: true, message: '请选择账号状态', trigger: 'change'}
|
||||||
|
],
|
||||||
|
avatarUrl: []
|
||||||
|
}
|
||||||
|
|
||||||
function handleBack() {
|
function handleBack() {
|
||||||
router.replace({
|
router.replace({
|
||||||
path: "/customer/customerList"
|
path: "/conversation/csAgent"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let files: File | null = null;
|
||||||
|
|
||||||
function handleChangeFile(file: File) {
|
function handleChangeFile(file: File) {
|
||||||
console.log(file)
|
files = file
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveAvatar(file: File) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file, 'avatar.png');
|
||||||
|
return uploadOss(formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 重写提交方法 ==========
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!formRef.value) return
|
||||||
|
await formRef.value.validate(async (valid) => {
|
||||||
|
if (!valid) {
|
||||||
|
modal.msgWarning('请检查表单必填项');
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 校验成功后执行业务逻辑
|
||||||
|
form.value.agentNo = "KF-" + form.value.jobNo;
|
||||||
|
form.value.phone = form.value.account;
|
||||||
|
|
||||||
|
if (files) {
|
||||||
|
const res = await handleSaveAvatar(files);
|
||||||
|
form.value.avatarUrl = res.data.url
|
||||||
|
form.value.avatarOssId = res.data.ossId
|
||||||
|
}
|
||||||
|
addCsAgent(form.value).then(() => {
|
||||||
|
router.replace({
|
||||||
|
path: '/conversation/csAgent'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="edit">
|
<div class="edit">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between" style="margin-top: 22px">
|
||||||
<pagecrumb></pagecrumb>
|
<pagecrumb></pagecrumb>
|
||||||
<el-button plain type="info" @click="handleBack"> 返回客服列表</el-button>
|
<el-button plain type="info" @click="handleBack"> 返回客服列表</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,26 +116,24 @@ function handleChangeFile(file: File) {
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="formbox">
|
<div class="formbox">
|
||||||
<el-form label-position="top">
|
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
|
||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.phone"
|
v-model="form.jobNo"
|
||||||
:inputAttrs="{ placeholder: '请输入工号,如 8823', disabled: true , showWordLimit:false ,maxlength:11}"
|
:inputAttrs="{ placeholder: '请输入工号,如 8823', disabled: false , showWordLimit:false ,maxlength:11}"
|
||||||
:required="true"
|
|
||||||
label="客服工号"
|
label="客服工号"
|
||||||
prop="phone"
|
prop="jobNo"
|
||||||
tip="建议使用 4 位数字,用于内部标识与排班识别"
|
tip="建议使用 4 位数字,用于内部标识与排班识别"
|
||||||
>
|
>
|
||||||
<template #suffix>不可修改</template>
|
|
||||||
</FormItemTip>
|
</FormItemTip>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.username"
|
v-model="form.name"
|
||||||
:inputAttrs="{ placeholder: '请输入客服姓名', disabled: false , showWordLimit:true ,maxlength:20}"
|
:inputAttrs="{ placeholder: '请输入客服姓名', disabled: false , showWordLimit:true ,maxlength:20}"
|
||||||
:required="true"
|
label="客服姓名"
|
||||||
prop="username"
|
prop="name"
|
||||||
tip="必填,将在会话列表与用户端展示"
|
tip="必填,将在会话列表与用户端展示"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
@@ -69,10 +145,10 @@ function handleChangeFile(file: File) {
|
|||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.username"
|
v-model="form.account"
|
||||||
:inputAttrs="{ placeholder: '请输入登录账号', disabled: false, showWordLimit:false ,maxlength:11}"
|
:inputAttrs="{ placeholder: '请输入登录账号', disabled: false, showWordLimit: false ,maxlength: 11}"
|
||||||
:required="true"
|
label="登录账号(手机号)"
|
||||||
prop="username"
|
prop="account"
|
||||||
tip="该手机号同时作为账号找回与首次登录验证凭证"
|
tip="该手机号同时作为账号找回与首次登录验证凭证"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
@@ -80,14 +156,28 @@ function handleChangeFile(file: File) {
|
|||||||
</template>
|
</template>
|
||||||
</FormItemTip>
|
</FormItemTip>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<FormItemTip
|
||||||
|
v-model="form.password"
|
||||||
|
:inputAttrs="{ placeholder: '请输入登录密码', disabled: false, showWordLimit: false ,maxlength: 20, minlength:8}"
|
||||||
|
label="初始密码"
|
||||||
|
prop="password"
|
||||||
|
type="password"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span>初始密码</span>
|
||||||
|
</template>
|
||||||
|
</FormItemTip>
|
||||||
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.img"
|
v-model="form.avatarUrl"
|
||||||
label="账号状态"
|
label="客服头像"
|
||||||
prop="img"
|
prop="avatarUrl"
|
||||||
type="uploadUserAvatar"
|
type="uploadUserAvatar"
|
||||||
|
@file-change="handleChangeFile"
|
||||||
>
|
>
|
||||||
</FormItemTip>
|
</FormItemTip>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -98,8 +188,8 @@ function handleChangeFile(file: File) {
|
|||||||
v-model="form.status"
|
v-model="form.status"
|
||||||
:inputAttrs="{ placeholder: '', disabled: false }"
|
:inputAttrs="{ placeholder: '', disabled: false }"
|
||||||
:radioOptions="[
|
:radioOptions="[
|
||||||
{label:'启用',value:1},
|
{label:'启用',value:'0'},
|
||||||
{label:'禁用',value:0},
|
{label:'禁用',value:'1'},
|
||||||
]"
|
]"
|
||||||
label="账号状态"
|
label="账号状态"
|
||||||
prop="status"
|
prop="status"
|
||||||
@@ -109,12 +199,24 @@ function handleChangeFile(file: File) {
|
|||||||
</FormItemTip>
|
</FormItemTip>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
<el-row :gutter="24">
|
||||||
|
<el-col :span="24">
|
||||||
|
<FormItemTip
|
||||||
|
v-model="form.remark"
|
||||||
|
:inputAttrs="{ placeholder: '可填写该客服的擅长领域、所属团队等信息,最多 200 字', disabled: false, maxlength:200, }"
|
||||||
|
label="备注"
|
||||||
|
prop="remark"
|
||||||
|
type="textarea"
|
||||||
|
>
|
||||||
|
</FormItemTip>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<el-button>取消</el-button>
|
<el-button>取消</el-button>
|
||||||
<el-button type="primary">保存</el-button>
|
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -123,7 +225,6 @@ function handleChangeFile(file: File) {
|
|||||||
.edit {
|
.edit {
|
||||||
.customerInfoBox {
|
.customerInfoBox {
|
||||||
.infoBox {
|
.infoBox {
|
||||||
|
|
||||||
.customer-Img {
|
.customer-Img {
|
||||||
width: 73px;
|
width: 73px;
|
||||||
height: 72px;
|
height: 72px;
|
||||||
@@ -161,7 +262,6 @@ function handleChangeFile(file: File) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.subTitle {
|
.subTitle {
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
color: rgba(156, 163, 175, 1);
|
color: rgba(156, 163, 175, 1);
|
||||||
@@ -178,5 +278,4 @@ function handleChangeFile(file: File) {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,61 +1,117 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import FormItemTip from "@/components/FormItemTip/index.vue";
|
import FormItemTip from "@/components/FormItemTip/index.vue";
|
||||||
|
import {getCsAgent, updateCsAgent} from "@/api/system/cssAgent";
|
||||||
|
import modal from '@/plugins/modal';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
const form = ref({
|
const form = ref({
|
||||||
phone: "",
|
"account": "",
|
||||||
username: "",
|
"agentId": "",
|
||||||
sex: 1,
|
"agentNo": "",
|
||||||
creatDay: '',
|
"avatarUrl": "",
|
||||||
status: 1,
|
"activeSessionCount": 0,
|
||||||
img: ''
|
"createTime": "",
|
||||||
|
"jobNo": "",
|
||||||
|
"name": "",
|
||||||
|
"onlineStatus": "0",
|
||||||
|
"onlineStatusName": "离线",
|
||||||
|
"phone": null,
|
||||||
|
"status": "0",
|
||||||
|
"statusName": "启用",
|
||||||
|
"password": ''
|
||||||
|
})
|
||||||
|
const editForm = ref({...form.value})
|
||||||
|
|
||||||
|
const defaultImag = computed(() => {
|
||||||
|
if (form.value.avatarUrl) {
|
||||||
|
return form.value.avatarUrl
|
||||||
|
}
|
||||||
|
return import.meta.env.VITE_DEFAULT_USER_IMAGE
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (route.query.id) {
|
||||||
|
getCsAgent(route.query.id as string).then(res => {
|
||||||
|
form.value = {
|
||||||
|
...form.value,
|
||||||
|
...res.data,
|
||||||
|
password: "",
|
||||||
|
}
|
||||||
|
editForm.value = JSON.parse(JSON.stringify(form.value))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init()
|
||||||
|
|
||||||
function handleBack() {
|
function handleBack() {
|
||||||
router.replace({
|
router.replace({
|
||||||
path: "/customer/customerList"
|
path: "/conversation/csAgent"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeFile(file: File) {
|
|
||||||
console.log(file)
|
function handleSubmit() {
|
||||||
|
if (!editForm.value.account || editForm.value.account.trim() === '') {
|
||||||
|
modal.msgWarning('登录账号不能为空')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!editForm.value.name || editForm.value.name.trim() === '') {
|
||||||
|
modal.msgWarning('客服姓名不能为空')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
editForm.value.phone = editForm.value.account;
|
||||||
|
updateCsAgent(editForm.value).then(() => {
|
||||||
|
handleBack()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
editForm.value = JSON.parse(JSON.stringify(form.value))
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="edit">
|
<div class="edit">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between" style="margin-top: 22px">
|
||||||
<pagecrumb></pagecrumb>
|
<pagecrumb></pagecrumb>
|
||||||
<el-button plain type="info" @click="handleBack"> 返回客服列表</el-button>
|
<el-button plain type="info" @click="handleBack"> 返回客服列表</el-button>
|
||||||
</div>
|
</div>
|
||||||
<!-- 用户信息-->
|
<!-- 用户信息-->
|
||||||
<section class="customerInfoBox commonCard flex justify-between flex-items-center">
|
<section class="customerInfoBox commonCard flex justify-between flex-items-center">
|
||||||
<div class="flex infoBox">
|
<div class="flex infoBox">
|
||||||
<el-image
|
<el-image :src="defaultImag" class="customer-Img"></el-image>
|
||||||
class="customer-Img"
|
|
||||||
src="https://ts3.tc.mm.bing.net/th?id=ORMS.48a27ca8353fa47bf0dc8df38854d0e1&pid=Wdp&w=312&h=172&qlt=90&c=1&rs=1&dpr=1&p=0"></el-image>
|
|
||||||
<div class="flex flex-col justify-between">
|
<div class="flex flex-col justify-between">
|
||||||
<div>
|
<div>
|
||||||
<span class="userName">林小满</span>
|
<span class="userName">{{ form.name }}</span>
|
||||||
<el-tag effect="light" round style="margin: 0 12px " type="success">
|
<el-tag v-if="form.status === '0' " effect="light" round style="margin: 0 12px " type="success">
|
||||||
<div class="flex flex-items-center">
|
<div class="flex flex-items-center">
|
||||||
<span>启用</span></div>
|
<span>启用</span></div>
|
||||||
</el-tag>
|
</el-tag>
|
||||||
|
<el-tag v-else effect="light" round style="margin: 0 12px " type="danger">
|
||||||
|
<div class="flex flex-items-center">
|
||||||
|
<span>禁用</span></div>
|
||||||
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="subInfoBox">
|
<div class="subInfoBox">
|
||||||
<span> KF-001 </span>
|
<span> {{ form.agentNo }} </span>
|
||||||
<span class="dat">·</span>
|
<span class="dat">·</span>
|
||||||
<span>工号 8823 </span>
|
<span>工号 {{ form.account }} </span>
|
||||||
<span class="dat">·</span>
|
<span class="dat">·</span>
|
||||||
<span>138****6688</span>
|
<span>138****6688</span>
|
||||||
<span class="dat">·</span>
|
<span class="dat">·</span>
|
||||||
<span>创建于 2025-11-08</span>
|
<span>创建于 {{ form.createTime }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div :class="{
|
||||||
当前在线·接待中3/5
|
offline:form.onlineStatus === '0'
|
||||||
|
}" class="onlineStatus">
|
||||||
|
<span>当前{{ form.onlineStatusName }}</span>
|
||||||
|
<span v-if="form.onlineStatus !== '0'">·接待中 {{ form.activeSessionCount }}</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -68,11 +124,11 @@ function handleChangeFile(file: File) {
|
|||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.phone"
|
v-model="editForm.jobNo"
|
||||||
:inputAttrs="{ placeholder: '请输入工号', disabled: true , showWordLimit:false ,maxlength:11}"
|
:inputAttrs="{ placeholder: '请输入工号', disabled: true , showWordLimit:false ,maxlength:11}"
|
||||||
:required="true"
|
:required="true"
|
||||||
label="工号"
|
label="工号"
|
||||||
prop="phone"
|
prop="jobNo"
|
||||||
tip="必填且全平台唯一,示例:8823"
|
tip="必填且全平台唯一,示例:8823"
|
||||||
>
|
>
|
||||||
<template #suffix>不可修改</template>
|
<template #suffix>不可修改</template>
|
||||||
@@ -80,10 +136,10 @@ function handleChangeFile(file: File) {
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.username"
|
v-model="editForm.name"
|
||||||
:inputAttrs="{ placeholder: '请输入客服姓名', disabled: false , showWordLimit:true ,maxlength:20}"
|
:inputAttrs="{ placeholder: '请输入客服姓名', disabled: false , showWordLimit:true ,maxlength:20}"
|
||||||
:required="true"
|
:required="true"
|
||||||
prop="username"
|
prop="name"
|
||||||
tip="必填,将在会话列表与用户端展示"
|
tip="必填,将在会话列表与用户端展示"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
@@ -95,10 +151,10 @@ function handleChangeFile(file: File) {
|
|||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.username"
|
v-model="editForm.account"
|
||||||
:inputAttrs="{ placeholder: '请输入登录账号', disabled: false , showWordLimit:true ,maxlength:20}"
|
:inputAttrs="{ placeholder: '请输入登录账号', disabled: false , showWordLimit:true ,maxlength:20}"
|
||||||
:required="true"
|
:required="true"
|
||||||
prop="username"
|
prop="account"
|
||||||
tip="必填,手机号格式,全平台唯一,同时作为登录账号"
|
tip="必填,手机号格式,全平台唯一,同时作为登录账号"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
@@ -108,10 +164,10 @@ function handleChangeFile(file: File) {
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.username"
|
v-model="editForm.password"
|
||||||
:inputAttrs="{ placeholder: '不修改请留空', disabled: false , showWordLimit:true ,maxlength:20, minlength:8}"
|
:inputAttrs="{ placeholder: '不修改请留空', disabled: false , showWordLimit:true ,maxlength:20, minlength:8}"
|
||||||
:required="true"
|
:required="true"
|
||||||
prop="username"
|
prop="password"
|
||||||
tip="如需修改请重新输入,长度 8–20 位,含字母与数字"
|
tip="如需修改请重新输入,长度 8–20 位,含字母与数字"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
@@ -123,11 +179,11 @@ function handleChangeFile(file: File) {
|
|||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.status"
|
v-model="editForm.status"
|
||||||
:inputAttrs="{ placeholder: '', disabled: false }"
|
:inputAttrs="{ placeholder: '', disabled: false }"
|
||||||
:radioOptions="[
|
:radioOptions="[
|
||||||
{label:'启用',value:1},
|
{label:'启用',value:'0'},
|
||||||
{label:'禁用',value:0},
|
{label:'禁用',value:'1'},
|
||||||
]"
|
]"
|
||||||
label="账号状态"
|
label="账号状态"
|
||||||
prop="status"
|
prop="status"
|
||||||
@@ -141,8 +197,8 @@ function handleChangeFile(file: File) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<el-button>取消</el-button>
|
<el-button @click="resetForm">取消</el-button>
|
||||||
<el-button type="primary">保存</el-button>
|
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -187,6 +243,16 @@ function handleChangeFile(file: File) {
|
|||||||
margin: 0 1px;
|
margin: 0 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.onlineStatus {
|
||||||
|
line-height: 18px;
|
||||||
|
color: rgba(37, 99, 235, 1);
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&.offline {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-2 page-shell system-csAgent-page">
|
<div class="p-2 page-shell system-csAgent-page">
|
||||||
<div class="search-wrap">
|
<div class="search-wrap" style="margin-top:22px;">
|
||||||
<pagecrumb></pagecrumb>
|
<pagecrumb></pagecrumb>
|
||||||
<el-card :class="{ 'is-collapsed': !showSearch }" class="search-panel" shadow="hover">
|
<el-card :class="{ 'is-collapsed': !showSearch }" class="search-panel" shadow="hover">
|
||||||
<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="query-form" label-position="top">
|
<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="query-form" label-position="top">
|
||||||
@@ -18,15 +18,15 @@
|
|||||||
<el-form-item label="账号状态" prop="status">
|
<el-form-item label="账号状态" prop="status">
|
||||||
<el-select v-model="queryParams.status" clearable placeholder="请选择账号状态" size="large"
|
<el-select v-model="queryParams.status" clearable placeholder="请选择账号状态" size="large"
|
||||||
style="width:180px;">
|
style="width:180px;">
|
||||||
<el-option :value="true" label="是"/>
|
<el-option label="启用" value="0"/>
|
||||||
<el-option :value="false" label="否"/>
|
<el-option label="禁用" value="1"/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="在线状态" prop="onlineStatus">
|
<el-form-item label="在线状态" prop="onlineStatus">
|
||||||
<el-select v-model="queryParams.onlineStatus" clearable placeholder="请选择在线状态" size="large"
|
<el-select v-model="queryParams.onlineStatus" clearable placeholder="请选择在线状态" size="large"
|
||||||
style="width:180px;">
|
style="width:180px;">
|
||||||
<el-option :value="true" label="是"/>
|
<el-option label="在线" value="1"/>
|
||||||
<el-option :value="false" label="否"/>
|
<el-option label="离线" value="0"/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
@@ -43,15 +43,16 @@
|
|||||||
<div class="table-heading">
|
<div class="table-heading">
|
||||||
<el-button type="primary">
|
<el-button type="primary">
|
||||||
<span style="margin:0 8px;color: #fff;">全部</span>
|
<span style="margin:0 8px;color: #fff;">全部</span>
|
||||||
<span style="color: #fff;font-weight: bold">1286</span>
|
<span style="color: #fff;font-weight: bold">{{ statForm.total }}</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
<tableTag circle-color="#16a34a" num="6" title="在线"></tableTag>
|
<tableTag :num="statForm.onlineCount" circle-color="#16a34a" title="在线"></tableTag>
|
||||||
<tableTag circle-color="#ea580c" num="2" title="忙碌"></tableTag>
|
<tableTag :num="statForm.busyCount" circle-color="#ea580c" title="忙碌"></tableTag>
|
||||||
<tableTag circle-color="#94a3b8" num="4" title="离线"></tableTag>
|
<tableTag :num="statForm.offlineCount" circle-color="#94a3b8" title="离线"></tableTag>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<el-button v-hasPermi="['system:csAgent:edit']" :disabled="single" plain type="info"
|
<el-button v-hasPermi="['system:csAgent:edit']" :disabled="multiple" plain type="info"
|
||||||
@click="handleUpdate()">批量禁用账号
|
@click="batchDisableSelection"
|
||||||
|
>批量禁用账号
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-hasPermi="['system:csAgent:add']" type="primary" @click="handleAdd">新增客服
|
<el-button v-hasPermi="['system:csAgent:add']" type="primary" @click="handleAdd">新增客服
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -63,44 +64,66 @@
|
|||||||
<el-table v-loading="loading" :data="csAgentList" border class="data-table"
|
<el-table v-loading="loading" :data="csAgentList" border class="data-table"
|
||||||
@selection-change="handleSelectionChange">
|
@selection-change="handleSelectionChange">
|
||||||
<el-table-column align="center" type="selection" width="55"/>
|
<el-table-column align="center" type="selection" width="55"/>
|
||||||
<el-table-column align="center" label="主键" prop="agentId"/>
|
<el-table-column align="center" label="客服ID" prop="agentNo"/>
|
||||||
<el-table-column align="center" label="客服编号,如 KF-001" prop="agentNo"/>
|
|
||||||
<el-table-column align="center" label="工号" prop="jobNo"/>
|
<el-table-column align="center" label="工号" prop="jobNo"/>
|
||||||
<el-table-column align="center" label="客服姓名" prop="name"/>
|
<el-table-column align="center" label="客服姓名" prop="name">
|
||||||
<el-table-column align="center" label="登录账号" prop="account"/>
|
<template #default="scope">
|
||||||
<el-table-column align="center" label="密码" prop="password"/>
|
<div class="avatarBox">
|
||||||
<el-table-column align="center" label="手机号" prop="phone"/>
|
<el-image v-if="scope.row.avatarUrl" :preview-src-list="[scope.row.avatarUrl]" :src="scope.row.avatarUrl"
|
||||||
<el-table-column align="center" label="头像 oss_id" prop="avatarOssId"/>
|
class="avatar"
|
||||||
|
preview-teleported></el-image>
|
||||||
|
<span :class="{
|
||||||
|
disable: scope.row.status === '1'
|
||||||
|
}" class="name">{{ scope.row.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="登录账号" prop="account">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ hiddenPhone(scope.row.account) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column align="center" label="在线状态" prop="onlineStatus" width="120">
|
<el-table-column align="center" label="在线状态" prop="onlineStatus" width="120">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-switch
|
<el-tag v-if="scope.row.onlineStatus === '1'" type="success">在线</el-tag>
|
||||||
v-model="scope.row.onlineStatus"
|
<el-tag v-else-if="scope.row.onlineStatus === '0'" type="info">离线</el-tag>
|
||||||
:active-value="true"
|
<el-tag v-else type="warning">忙碌</el-tag>
|
||||||
:inactive-value="false"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column align="center" label="账号状态" prop="status" width="120">
|
<el-table-column align="center" label="接待中会话" prop="onlineStatus" width="120">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-switch
|
<span :class="{
|
||||||
v-model="scope.row.status"
|
disable: scope.row.status === '1'
|
||||||
:active-value="true"
|
}" class="active">5 通</span>
|
||||||
:inactive-value="false"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column align="center" class-name="small-padding fixed-width" label="操作">
|
<el-table-column align="center" label="手机号" prop="phone" width="120">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tooltip content="修改" placement="top">
|
<span>{{ scope.row.phone || '---' }}</span>
|
||||||
<el-button v-hasPermi="['system:csAgent:edit']" icon="Edit" link type="primary"
|
</template>
|
||||||
@click="handleUpdate(scope.row)"></el-button>
|
</el-table-column>
|
||||||
</el-tooltip>
|
<el-table-column align="center" label="账号状态" prop="status" width="100">
|
||||||
<el-tooltip content="删除" placement="top">
|
<template #default="scope">
|
||||||
<el-button v-hasPermi="['system:csAgent:remove']" icon="Delete" link type="primary"
|
<el-tag v-if="scope.row.status === '0'" type="success">启用</el-tag>
|
||||||
@click="handleDelete(scope.row)"></el-button>
|
<el-tag v-else type="danger">禁用</el-tag>
|
||||||
</el-tooltip>
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="创建时间" prop="createTime" width="140"></el-table-column>
|
||||||
|
<el-table-column align="center" class-name="small-padding fixed-width" label="操作" min-width="120">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button v-hasPermi="['system:csAgent:edit']" link type="primary"
|
||||||
|
@click="handleUpdate(scope.row)">编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button v-hasPermi="['system:csAgent:remove']" link type="info"
|
||||||
|
@click="handleResetPwd(scope.row)">重置密码
|
||||||
|
</el-button>
|
||||||
|
<!-- 0启用 1禁用-->
|
||||||
|
<el-button v-if="scope.row.status === '0'" v-hasPermi="['system:csAgent:remove']" link type="warning"
|
||||||
|
@click="handleEnableOrUnEnable(scope.row,'1')">禁用
|
||||||
|
</el-button>
|
||||||
|
<el-button v-else v-hasPermi="['system:csAgent:remove']" link type="success"
|
||||||
|
@click="handleEnableOrUnEnable(scope.row,'0')">启用
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -185,10 +208,10 @@
|
|||||||
<script lang="ts" name="CsAgent" setup>
|
<script lang="ts" name="CsAgent" setup>
|
||||||
import tableTag from '@/components/tableTag/index.vue'
|
import tableTag from '@/components/tableTag/index.vue'
|
||||||
import {
|
import {
|
||||||
addCsAgent,
|
addCsAgent, BatchDisableCsAgent,
|
||||||
delCsAgent,
|
delCsAgent,
|
||||||
getCsAgent,
|
getCsAgent,
|
||||||
listCsAgent,
|
listCsAgent, ResetPwdCsAgent, statCsAgent,
|
||||||
updateCsAgent
|
updateCsAgent
|
||||||
} from '@/api/system/cssAgent';
|
} from '@/api/system/cssAgent';
|
||||||
import {CsAgentForm, CsAgentQuery, CsAgentVO} from '@/api/system/cssAgent/types';
|
import {CsAgentForm, CsAgentQuery, CsAgentVO} from '@/api/system/cssAgent/types';
|
||||||
@@ -200,6 +223,7 @@ import {useSearchToggle} from '@/hooks/form/useSearchToggle';
|
|||||||
import {useTableSelection} from '@/hooks/table/useTableSelection';
|
import {useTableSelection} from '@/hooks/table/useTableSelection';
|
||||||
import modal from '@/plugins/modal';
|
import modal from '@/plugins/modal';
|
||||||
import {download as requestDownload} from '@/utils/request';
|
import {download as requestDownload} from '@/utils/request';
|
||||||
|
import {hiddenPhone} from "@/utils/phone";
|
||||||
|
|
||||||
|
|
||||||
const csAgentList = ref<CsAgentVO[]>([]);
|
const csAgentList = ref<CsAgentVO[]>([]);
|
||||||
@@ -298,6 +322,7 @@ const {dialog, resetForm: reset, openDialog, showDialog, closeDialog} = useFormD
|
|||||||
|
|
||||||
/** 查询客服人员列表 */
|
/** 查询客服人员列表 */
|
||||||
const getList = async () => {
|
const getList = async () => {
|
||||||
|
getStat()
|
||||||
await withLoading(async () => {
|
await withLoading(async () => {
|
||||||
let params = queryParams.value;
|
let params = queryParams.value;
|
||||||
params = applyCreateTimeDateRange(params);
|
params = applyCreateTimeDateRange(params);
|
||||||
@@ -334,21 +359,80 @@ const {resetQuery} = useSearchReset({
|
|||||||
handleQuery();
|
handleQuery();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const router = useRouter();
|
||||||
/** 新增按钮操作 */
|
/** 新增按钮操作 */
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
openDialog('添加客服人员');
|
// openDialog('添加客服人员');
|
||||||
|
router.push({
|
||||||
|
path: "/conversation/csAgentAdd",
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/** 修改按钮操作 */
|
/** 修改按钮操作 */
|
||||||
const handleUpdate = async (row?: Partial<CsAgentVO>) => {
|
const handleUpdate = async (row?: Partial<CsAgentVO>) => {
|
||||||
reset();
|
router.push({
|
||||||
const _agentId = row?.agentId || ids.value[0];
|
path: '/conversation/csAgentEdit',
|
||||||
const res = await getCsAgent(_agentId);
|
query: {
|
||||||
Object.assign(form.value, res.data);
|
id: row.agentId
|
||||||
showDialog('修改客服人员');
|
}
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 重置密码 */
|
||||||
|
const handleResetPwd = (row: Partial<CsAgentVO>) => {
|
||||||
|
ElMessageBox.prompt("重置密码后,该客服人员登录失效,需重新登录。<br/> <span style='font-size:small;color:gray;'>不填默认12345678</span>", '重置密码', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
}).then(({value}) => {
|
||||||
|
if (value.length > 20) {
|
||||||
|
modal.msgWarning('重置密码过长!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pwd = value || '12345678';
|
||||||
|
ResetPwdCsAgent(row.agentId, pwd).then(() => {
|
||||||
|
modal.msgSuccess('重置成功');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 启用/禁用 */
|
||||||
|
const handleEnableOrUnEnable = (row: Partial<CsAgentVO>, status: '1' | '0' = '1') => {
|
||||||
|
if (status === '1') {
|
||||||
|
modal.confirm('禁用后,该客服将退出登录,未结束的会话重新排队,等待其他客服接入').then(() => {
|
||||||
|
updateCsAgent({
|
||||||
|
...row,
|
||||||
|
status,
|
||||||
|
}).then(() => {
|
||||||
|
getList();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
updateCsAgent({
|
||||||
|
...row,
|
||||||
|
status,
|
||||||
|
}).then(() => {
|
||||||
|
getList();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 批量禁用 */
|
||||||
|
const batchDisableSelection = () => {
|
||||||
|
if (ids.value.length <= 0) {
|
||||||
|
modal.msgWarning('请选择禁用客服')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BatchDisableCsAgent(ids.value as string[]).then(() => {
|
||||||
|
modal.msgSuccess('禁用成功');
|
||||||
|
getList();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/** 提交按钮 */
|
/** 提交按钮 */
|
||||||
const submitForm = () => {
|
const submitForm = () => {
|
||||||
csAgentFormRef.value?.validate(async (valid: boolean) => {
|
csAgentFormRef.value?.validate(async (valid: boolean) => {
|
||||||
@@ -386,9 +470,58 @@ const handleExport = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const statForm = ref({
|
||||||
|
"busyCount": 0,
|
||||||
|
"offlineCount": 0,
|
||||||
|
"onlineCount": 0,
|
||||||
|
"total": 0
|
||||||
|
})
|
||||||
|
|
||||||
|
function getStat() {
|
||||||
|
statCsAgent().then(res => {
|
||||||
|
statForm.value = res.data;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getList();
|
getList();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.avatarBox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 29px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 14px;
|
||||||
|
object-fit: cover;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
line-height: 16px;
|
||||||
|
color: rgba(17, 24, 39, 1);
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
&.disable {
|
||||||
|
color: rgba(156, 163, 175, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.active {
|
||||||
|
color: rgba(37, 99, 235, 1);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&.disable {
|
||||||
|
color: rgba(156, 163, 175, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,16 +1,57 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import FormItemTip from "@/components/FormItemTip/index.vue";
|
import FormItemTip from "@/components/FormItemTip/index.vue";
|
||||||
|
import pageCrumb from "@/components/Pagecrumb/index.vue";
|
||||||
|
import {getUser, updateUser} from "@/api/system/customer";
|
||||||
|
import {hiddenPhone} from "@/utils/phone";
|
||||||
|
import modal from "@/plugins/modal";
|
||||||
|
import {uploadOss} from "@/api/system/oss";
|
||||||
|
import {FormInstance, FormRules} from "element-plus";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
const form = ref({
|
const form = ref({
|
||||||
phone: "",
|
"birthday": null,
|
||||||
username: "",
|
"city": null,
|
||||||
sex: 1,
|
"createTime": "",
|
||||||
creatDay: '',
|
"district": null,
|
||||||
status: 1,
|
"gender": "0",
|
||||||
img: ''
|
"genderName": "未知",
|
||||||
|
"loginCount": 0,
|
||||||
|
"loginDate": "",
|
||||||
|
"loginIp": "",
|
||||||
|
"nickName": "",
|
||||||
|
"phonenumber": "",
|
||||||
|
"province": null,
|
||||||
|
"remark": "",
|
||||||
|
"status": "0",
|
||||||
|
"statusName": "正常",
|
||||||
|
"updateTime": "",
|
||||||
|
"userId": "",
|
||||||
|
"userName": "",
|
||||||
|
"userNo": "",
|
||||||
|
"avatarOssId": "",
|
||||||
|
"avatarUrl": "",
|
||||||
})
|
})
|
||||||
|
const editForm = ref({
|
||||||
|
...form.value,
|
||||||
|
})
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
|
||||||
|
const rules: FormRules = {
|
||||||
|
nickName: [
|
||||||
|
{required: true, message: '用户昵称不能为空', trigger: 'blur'},
|
||||||
|
{max: 20, message: '最多20个字符', trigger: 'blur'}
|
||||||
|
],
|
||||||
|
// gender、birthday、remark 选填,不用写required
|
||||||
|
gender: [],
|
||||||
|
birthday: [],
|
||||||
|
status: [
|
||||||
|
{required: true, message: '请选择账号状态', trigger: 'change'}
|
||||||
|
],
|
||||||
|
remark: [
|
||||||
|
{max: 20, message: '最多20个字符', trigger: 'blur'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
function handleBack() {
|
function handleBack() {
|
||||||
router.replace({
|
router.replace({
|
||||||
@@ -18,15 +59,66 @@ function handleBack() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeFile(file: File) {
|
let files = []
|
||||||
console.log(file)
|
|
||||||
|
async function handleChangeFile(file: File) {
|
||||||
|
files = []
|
||||||
|
files.push(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveAvatar(file: File) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file, 'avatar.png');
|
||||||
|
return uploadOss(formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (route.query.id) {
|
||||||
|
getUser(route.query.id as string).then(res => {
|
||||||
|
form.value = {
|
||||||
|
...form.value,
|
||||||
|
...res.data
|
||||||
|
}
|
||||||
|
editForm.value = JSON.parse(JSON.stringify(form.value))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init()
|
||||||
|
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
if (!formRef.value) return
|
||||||
|
formRef.value.validate(async (valid) => {
|
||||||
|
console.log('校验结果:', valid)
|
||||||
|
if (!valid) {
|
||||||
|
modal.msgWarning('请检查表单必填项');
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 校验通过再执行上传和保存
|
||||||
|
if (files.length != 0) {
|
||||||
|
const res = await handleSaveAvatar(files.at(0));
|
||||||
|
editForm.value.avatarUrl = res.data.url
|
||||||
|
editForm.value.avatarOssId = res.data.ossId
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUser(editForm.value).then(() => {
|
||||||
|
modal.msgSuccess('修改成功');
|
||||||
|
handleBack()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
editForm.value = JSON.parse(JSON.stringify(form.value))
|
||||||
|
formRef.value?.clearValidate()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="edit">
|
<div class="edit">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between" style="padding-top:22px">
|
||||||
<pagecrumb></pagecrumb>
|
<pageCrumb></pageCrumb>
|
||||||
<el-button plain type="info" @click="handleBack"> 返回用户列表</el-button>
|
<el-button plain type="info" @click="handleBack"> 返回用户列表</el-button>
|
||||||
</div>
|
</div>
|
||||||
<!-- 用户信息-->
|
<!-- 用户信息-->
|
||||||
@@ -34,22 +126,22 @@ function handleChangeFile(file: File) {
|
|||||||
<div class="flex infoBox">
|
<div class="flex infoBox">
|
||||||
<el-image
|
<el-image
|
||||||
class="customer-Img"
|
class="customer-Img"
|
||||||
src="https://ts3.tc.mm.bing.net/th?id=ORMS.48a27ca8353fa47bf0dc8df38854d0e1&pid=Wdp&w=312&h=172&qlt=90&c=1&rs=1&dpr=1&p=0"></el-image>
|
:src="form.avatarUrl"></el-image>
|
||||||
<div class="flex flex-col justify-between">
|
<div class="flex flex-col justify-between">
|
||||||
<div>
|
<div>
|
||||||
<span class="userName">林小满</span>
|
<span class="userName">{{ form.nickName }}</span>
|
||||||
<el-tag effect="light" round style="margin: 0 12px " type="success">
|
<el-tag effect="light" round style="margin: 0 12px " type="success">
|
||||||
<div class="flex flex-items-center"><i class="circle"></i>
|
<div class="flex flex-items-center"><i class="circle"></i>
|
||||||
<span>正常</span></div>
|
<span>正常</span></div>
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<el-tag effect="light" round type="info">150****0831</el-tag>
|
<el-tag effect="light" round type="info">{{ hiddenPhone(form.phonenumber) }}</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="subInfoBox">
|
<div class="subInfoBox">
|
||||||
<span>用户ID: U20260918001 </span>
|
<span>用户ID: {{ form.userNo }} </span>
|
||||||
<span class="dat">·</span>
|
<span class="dat">·</span>
|
||||||
<span>注册时间: 2026-01-10 14:20</span>
|
<span>注册时间: {{ form.createTime }}</span>
|
||||||
<span class="dat">·</span>
|
<span class="dat">·</span>
|
||||||
<span>最后登录: 2026-09-17 09:35</span>
|
<span>最后登录: {{ form.loginDate }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,14 +152,14 @@ function handleChangeFile(file: File) {
|
|||||||
<section class="commonCard">
|
<section class="commonCard">
|
||||||
<card-nav subTitle="手机号为账号唯一标识不可修改;个人资料与账号状态可维护" title="用户资料"></card-nav>
|
<card-nav subTitle="手机号为账号唯一标识不可修改;个人资料与账号状态可维护" title="用户资料"></card-nav>
|
||||||
<div class="formbox">
|
<div class="formbox">
|
||||||
<el-form label-position="top">
|
<el-form :model="editForm" :rules="rules" label-position="top">
|
||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.phone"
|
v-model="editForm.phonenumber"
|
||||||
:inputAttrs="{ placeholder: '请输入手机号', disabled: true , showWordLimit:false ,maxlength:11}"
|
:inputAttrs="{ placeholder: '请输入手机号', disabled: true , showWordLimit:false ,maxlength:11}"
|
||||||
label="手机号"
|
label="手机号"
|
||||||
prop="phone"
|
prop="phonenumber"
|
||||||
tip="账号唯一标识,如需更换手机号请联系用户重新注册"
|
tip="账号唯一标识,如需更换手机号请联系用户重新注册"
|
||||||
>
|
>
|
||||||
<template #suffix>不可修改</template>
|
<template #suffix>不可修改</template>
|
||||||
@@ -75,10 +167,11 @@ function handleChangeFile(file: File) {
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.username"
|
v-model="editForm.nickName"
|
||||||
|
prop="nickName"
|
||||||
:inputAttrs="{ placeholder: '请输入用户昵称', disabled: false , showWordLimit:true ,maxlength:20}"
|
:inputAttrs="{ placeholder: '请输入用户昵称', disabled: false , showWordLimit:true ,maxlength:20}"
|
||||||
:required="true"
|
:required="true"
|
||||||
prop="username"
|
type="input"
|
||||||
tip="必填,最多 20 个字符,将同步展示在 APP 端"
|
tip="必填,最多 20 个字符,将同步展示在 APP 端"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
@@ -90,13 +183,13 @@ function handleChangeFile(file: File) {
|
|||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.sex"
|
v-model="editForm.gender"
|
||||||
:radioOptions="[
|
:radioOptions="[
|
||||||
{label:'男',value:1},
|
{label:'男',value:'1'},
|
||||||
{label:'女',value:0},
|
{label:'女',value:'2'},
|
||||||
{label:'未填写',value:2},
|
{label:'未填写',value:'0'},
|
||||||
]"
|
]"
|
||||||
prop="sex"
|
prop="gender"
|
||||||
tip="选填,选择「未填写」时 APP 端不展示性别"
|
tip="选填,选择「未填写」时 APP 端不展示性别"
|
||||||
type="radio"
|
type="radio"
|
||||||
>
|
>
|
||||||
@@ -108,10 +201,10 @@ function handleChangeFile(file: File) {
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.creatDay"
|
v-model="editForm.birthday"
|
||||||
:inputAttrs="{ placeholder: '请选择生日'}"
|
:inputAttrs="{ placeholder: '请选择生日'}"
|
||||||
label="生日"
|
label="生日"
|
||||||
prop="creatDay"
|
prop="birthday"
|
||||||
tip="选填,修改后将同步至用户个人资料"
|
tip="选填,修改后将同步至用户个人资料"
|
||||||
type="date"
|
type="date"
|
||||||
>
|
>
|
||||||
@@ -125,11 +218,11 @@ function handleChangeFile(file: File) {
|
|||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.status"
|
v-model="editForm.status"
|
||||||
:inputAttrs="{ placeholder: '', disabled: false }"
|
:inputAttrs="{ placeholder: '', disabled: false }"
|
||||||
:radioOptions="[
|
:radioOptions="[
|
||||||
{label:'正常',value:1},
|
{label:'正常',value:'0'},
|
||||||
{label:'禁用',value:0},
|
{label:'禁用',value:'1'},
|
||||||
]"
|
]"
|
||||||
label="账号状态"
|
label="账号状态"
|
||||||
prop="status"
|
prop="status"
|
||||||
@@ -140,11 +233,11 @@ function handleChangeFile(file: File) {
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.username"
|
|
||||||
:inputAttrs="{ placeholder: '请输入用户昵称', disabled: true }"
|
:inputAttrs="{ placeholder: '请输入用户昵称', disabled: true }"
|
||||||
:required="true"
|
:required="true"
|
||||||
|
v-model="editForm.avatarUrl"
|
||||||
label="头像"
|
label="头像"
|
||||||
prop="username"
|
prop="avatarUrl"
|
||||||
tip="必填,最多 20 个字符,将同步展示在 APP 端"
|
tip="必填,最多 20 个字符,将同步展示在 APP 端"
|
||||||
type="uploadUserAvatar"
|
type="uploadUserAvatar"
|
||||||
@file-change="handleChangeFile"
|
@file-change="handleChangeFile"
|
||||||
@@ -155,11 +248,11 @@ function handleChangeFile(file: File) {
|
|||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.phone"
|
v-model="editForm.remark"
|
||||||
:inputAttrs="{ placeholder: '请输入后台内部备注,如工单号、对接人、异常说明等信息', disabled: false , showWordLimit:true ,maxlength:20}"
|
:inputAttrs="{ placeholder: '请输入后台内部备注,如工单号、对接人、异常说明等信息', disabled: false , showWordLimit:true ,maxlength:20}"
|
||||||
:required="false"
|
:required="false"
|
||||||
label="备注"
|
label="备注"
|
||||||
prop="phone"
|
prop="remark"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
@@ -173,8 +266,8 @@ function handleChangeFile(file: File) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<el-button>取消</el-button>
|
<el-button @click="resetForm">取消</el-button>
|
||||||
<el-button type="primary">保存</el-button>
|
<el-button type="primary" @click="handleSave">保存</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-2 app-container monitor-online-page">
|
<div class="p-2 app-container monitor-online-page">
|
||||||
<Pagecrumb></Pagecrumb>
|
<pageCrumb style="margin-top: 22px"></pageCrumb>
|
||||||
<div class="search-wrap">
|
<div class="search-wrap">
|
||||||
<el-card class="search-panel" shadow="hover">
|
<el-card class="search-panel" shadow="hover">
|
||||||
<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="query-form" label-position="top">
|
<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="query-form" label-position="top">
|
||||||
<el-form-item label="手机号" prop="phoneNumber">
|
<el-form-item label="手机号" prop="phonenumber">
|
||||||
<el-input v-model="queryParams.phoneNumber" clearable placeholder="请输入手机号" size="large"
|
<el-input v-model="queryParams.phonenumber" clearable placeholder="请输入手机号" size="large"
|
||||||
style="width: 190px;" @keyup.enter="handleQuery"/>
|
style="width: 190px;" @keyup.enter="handleQuery"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="用户昵称" prop="nickName">
|
<el-form-item label="用户昵称" prop="nickName">
|
||||||
@@ -39,12 +39,17 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="注册时间" prop="createTime">
|
<el-form-item label="注册时间段" prop="createTime">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="queryParams.createTime"
|
v-model="createTime"
|
||||||
|
end-placeholder="结束日期"
|
||||||
size="large"
|
size="large"
|
||||||
style="width:150px;"
|
single-panel
|
||||||
type="date"
|
start-placeholder="开始日期"
|
||||||
|
style="width:230px;"
|
||||||
|
type="daterange"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
@update:model-value="handleChangeTime"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="账号状态" prop="status">
|
<el-form-item label="账号状态" prop="status">
|
||||||
@@ -55,8 +60,8 @@
|
|||||||
size="large"
|
size="large"
|
||||||
style="width:100px;"
|
style="width:100px;"
|
||||||
>
|
>
|
||||||
<el-option label="正常" value="1"></el-option>
|
<el-option label="正常" value="0"></el-option>
|
||||||
<el-option label="停用" value="0"></el-option>
|
<el-option label="停用" value="1"></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
@@ -69,35 +74,40 @@
|
|||||||
<el-card class="table-panel" shadow="hover">
|
<el-card class="table-panel" shadow="hover">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<el-button plain type="info">批量导出用户信息</el-button>
|
<el-button plain type="info" @click="exportSelectUser">批量导出用户信息</el-button>
|
||||||
<el-button plain type="danger">批量禁用账号</el-button>
|
<el-button plain type="danger" @click="selectDisableUser">批量禁用账号</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="onlineList"
|
:data="onlineList"
|
||||||
class="data-table"
|
class="data-table"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-table-column align="center" type="selection"></el-table-column>
|
<el-table-column align="center" type="selection"></el-table-column>
|
||||||
<el-table-column :show-overflow-tooltip="true" align="center" label="用户昵称" prop="nickName"/>
|
<el-table-column :show-overflow-tooltip="true" align="center" label="用户昵称" prop="nickName"/>
|
||||||
<el-table-column :show-overflow-tooltip="true" align="center" label="手机号" prop="phoneNumber"/>
|
<el-table-column :show-overflow-tooltip="true" align="center" label="手机号" prop="phonenumber">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ hiddenPhone(scope.row.phonenumber) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column :show-overflow-tooltip="true" align="center" label="性别" prop="gender">
|
<el-table-column :show-overflow-tooltip="true" align="center" label="性别" prop="gender">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span v-if="scope.row.gender === '1'">男</span>
|
<span v-if="scope.row.gender === '1'">男</span>
|
||||||
<span v-else-if="scope.row.gender==='2'">女</span>
|
<span v-else-if="scope.row.gender==='2'">女</span>
|
||||||
<span v-else>未填写</span>
|
<span v-else style="color: #9CA3AF">未填写</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column align="center" label="生日" prop="birthday"></el-table-column>
|
<el-table-column align="center" label="生日" prop="birthday"></el-table-column>
|
||||||
<el-table-column :show-overflow-tooltip="true" align="center" label="所在地区" prop="deptName"/>
|
<el-table-column :show-overflow-tooltip="true" align="center" label="所在地区" prop="deptName"/>
|
||||||
<el-table-column :show-overflow-tooltip="true" align="center" label="绑定设备量" prop="ipaddr"/>
|
<el-table-column :show-overflow-tooltip="true" align="center" label="绑定设备量" prop="deviceCount"/>
|
||||||
<el-table-column :show-overflow-tooltip="true" align="center" label="注册时间" prop="creatTime"/>
|
<el-table-column :show-overflow-tooltip="true" align="center" label="注册时间" prop="createTime"/>
|
||||||
<el-table-column :show-overflow-tooltip="true" align="center" label="最后登录时间" prop="loginDate"/>
|
<el-table-column :show-overflow-tooltip="true" align="center" label="最后登录时间" prop="loginDate"/>
|
||||||
<el-table-column :show-overflow-tooltip="true" align="center" label="账号状态" prop="status">
|
<el-table-column :show-overflow-tooltip="true" align="center" label="账号状态" prop="status">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag v-if="scope.row.status === '0'" type="success">正常</el-tag>
|
<el-tag v-if="scope.row.status === '0'" type="success">正常</el-tag>
|
||||||
<el-tag v-else type="success">异常</el-tag>
|
<el-tag v-else type="danger">异常</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column align="center" class-name="small-padding fixed-width" label="操作" min-width="250">
|
<el-table-column align="center" class-name="small-padding fixed-width" label="操作" min-width="250">
|
||||||
@@ -113,12 +123,14 @@
|
|||||||
v-hasPermi="['monitor:online:forceLogout']"
|
v-hasPermi="['monitor:online:forceLogout']"
|
||||||
plain
|
plain
|
||||||
type="info"
|
type="info"
|
||||||
|
@click="handleEdit(scope.row)"
|
||||||
>编辑
|
>编辑
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPermi="['monitor:online:forceLogout']"
|
v-hasPermi="['monitor:online:forceLogout']"
|
||||||
plain
|
plain
|
||||||
type="info"
|
type="info"
|
||||||
|
@click="handleReboot(scope.row)"
|
||||||
>重置密码
|
>重置密码
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
@@ -126,6 +138,7 @@
|
|||||||
v-hasPermi="['monitor:online:forceLogout']"
|
v-hasPermi="['monitor:online:forceLogout']"
|
||||||
plain
|
plain
|
||||||
type="warning"
|
type="warning"
|
||||||
|
@click="changeStatus(scope.row, '1')"
|
||||||
>禁用
|
>禁用
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
@@ -133,12 +146,12 @@
|
|||||||
v-hasPermi="['monitor:online:forceLogout']"
|
v-hasPermi="['monitor:online:forceLogout']"
|
||||||
plain
|
plain
|
||||||
type="success"
|
type="success"
|
||||||
|
@click="changeStatus(scope.row, '0')"
|
||||||
>启用
|
>启用
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<pagination
|
<pagination
|
||||||
v-show="total > 0"
|
v-show="total > 0"
|
||||||
v-model:limit="queryParams.pageSize"
|
v-model:limit="queryParams.pageSize"
|
||||||
@@ -150,20 +163,37 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {listUser as initData} from '@/api/system/customer/index';
|
import {batchDisableUsers, listUser as initData, resetPwdUser, updateUserStatus} from '@/api/system/customer/index';
|
||||||
import {useDict} from '@/utils/dict';
|
|
||||||
import {UserQuery, UserVO} from "@/api/system/customer/types";
|
import {UserQuery, UserVO} from "@/api/system/customer/types";
|
||||||
|
import {hiddenPhone} from "@/utils/phone";
|
||||||
const {sys_device_type} = toRefs<any>(useDict('sys_device_type'));
|
import pageCrumb from "@/components/Pagecrumb/index.vue";
|
||||||
|
import modal from "@/plugins/modal";
|
||||||
|
import {download as requestDownload} from '@/utils/request';
|
||||||
|
import {useTableSelection} from "@/hooks/table/useTableSelection";
|
||||||
|
import {PostVO} from "@/api/system/post/types";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const onlineList = ref<UserVO[]>([]);
|
const onlineList = ref<UserVO[]>([]);
|
||||||
|
const {ids, single, multiple, handleSelectionChange} = useTableSelection<UserVO>(item => item.userId as string);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
|
|
||||||
const queryFormRef = ref<ElFormInstance>();
|
const queryFormRef = ref<ElFormInstance>();
|
||||||
|
|
||||||
|
const createTime = ref([])
|
||||||
|
|
||||||
|
function handleChangeTime(data) {
|
||||||
|
if (data) {
|
||||||
|
queryParams.value.beginTime = data[0] + ' 00:00:00';
|
||||||
|
queryParams.value.endTime = data[1] + ' 23:59:59';
|
||||||
|
} else {
|
||||||
|
createTime.value = []
|
||||||
|
queryParams.value.beginTime = undefined
|
||||||
|
queryParams.value.endTime = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const queryParams = ref<UserQuery>({
|
const queryParams = ref<UserQuery>({
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@@ -187,13 +217,87 @@ const handleQuery = () => {
|
|||||||
/** 重置按钮操作 */
|
/** 重置按钮操作 */
|
||||||
const resetQuery = () => {
|
const resetQuery = () => {
|
||||||
queryFormRef.value?.resetFields();
|
queryFormRef.value?.resetFields();
|
||||||
|
createTime.value = []
|
||||||
|
queryParams.value.beginTime = undefined
|
||||||
|
queryParams.value.endTime = undefined
|
||||||
handleQuery();
|
handleQuery();
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleMain(row) {
|
/// 详情
|
||||||
console.log(row);
|
function handleMain(row: Partial<UserVO>) {
|
||||||
router.push({
|
router.push({
|
||||||
path: '/customer/customerMain'
|
path: '/customer/customerMain',
|
||||||
|
query: {
|
||||||
|
id: row.userId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置密码
|
||||||
|
function handleReboot(row: Partial<UserVO>) {
|
||||||
|
console.log(row);
|
||||||
|
modal.confirm('确认重置该用户的登录密码吗?系统将恢复默认密码,并踢掉该用户登陆状态').then(() => {
|
||||||
|
resetPwdUser(row.userId, '123456').then(() => {
|
||||||
|
modal.msgSuccess('重置成功');
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 改变用户状态
|
||||||
|
async function changeStatus(row: Partial<UserVO>, status: '0' | '1') {
|
||||||
|
if (status === '1') {
|
||||||
|
await modal.confirm('禁用账号会立即退出该用户的 APP 登录状态,确定要禁用该用户吗?')
|
||||||
|
}
|
||||||
|
updateUserStatus(row.userId, status).then(() => {
|
||||||
|
modal.msgSuccess('操作成功');
|
||||||
|
getList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 导出按钮操作 */
|
||||||
|
const handleExport = () => {
|
||||||
|
requestDownload(
|
||||||
|
'customer/user/export',
|
||||||
|
{
|
||||||
|
...queryParams.value
|
||||||
|
},
|
||||||
|
`APP用户_${new Date().getTime()}.xlsx`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
async function selectDisableUser() {
|
||||||
|
await modal.confirm('禁用账号会立即退出该用户的 APP 登录状态,确定要禁用这些选择的用户吗?')
|
||||||
|
const userIds = ids.value || [];
|
||||||
|
batchDisableUsers(userIds as string[]).then(() => {
|
||||||
|
modal.msgSuccess('禁用成功');
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportSelectUser() {
|
||||||
|
const userIds = ids.value || [];
|
||||||
|
if (userIds.length > 0) {
|
||||||
|
|
||||||
|
requestDownload('customer/user/exportSelected',
|
||||||
|
{
|
||||||
|
userIds: userIds.join(',')
|
||||||
|
},
|
||||||
|
`APP用户_${Date.now()}.xlsx`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
modal.msgWarning('请选择导出的用户')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑用户
|
||||||
|
function handleEdit(row: Partial<UserVO>) {
|
||||||
|
router.push({
|
||||||
|
path: '/customer/customerEdit',
|
||||||
|
query: {
|
||||||
|
id: row.userId
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,55 +1,125 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import Pagecrumb from '@/components/Pagecrumb/index.vue';
|
import pageCrumb from '@/components/Pagecrumb/index.vue';
|
||||||
|
import {getUser, resetPwdUser, updateUserStatus} from '@/api/system/customer';
|
||||||
|
import {hiddenPhone} from "@/utils/phone";
|
||||||
|
import modal from "@/plugins/modal";
|
||||||
|
|
||||||
|
const defaultImag = import.meta.env.VITE_DEFAULT_USER_IMAGE;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
function handleEdit() {
|
function handleEdit() {
|
||||||
router.push({
|
router.push({
|
||||||
path: '/customer/customerEdit'
|
path: '/customer/customerEdit',
|
||||||
|
query: {
|
||||||
|
id: form.value.userId
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重置密码
|
||||||
|
function handleReboot() {
|
||||||
|
modal.confirm('确认重置该用户的登录密码吗?系统将恢复默认密码,并踢掉该用户登陆状态').then(() => {
|
||||||
|
resetPwdUser(form.value.userId, '123456').then(() => {
|
||||||
|
modal.msgSuccess('重置成功');
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 改变用户状态
|
||||||
|
function changeStatus(status: '0' | '1') {
|
||||||
|
updateUserStatus(form.value.userId, status).then(() => {
|
||||||
|
modal.msgSuccess('操作成功');
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleBack() {
|
function handleBack() {
|
||||||
router.push({
|
router.push({
|
||||||
path: '/customer/customerList'
|
path: '/customer/customerList'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
"birthday": null,
|
||||||
|
"city": null,
|
||||||
|
"createTime": "",
|
||||||
|
"district": null,
|
||||||
|
"gender": "0",
|
||||||
|
"genderName": "未知",
|
||||||
|
"loginCount": 19,
|
||||||
|
"loginDate": "",
|
||||||
|
"loginIp": "",
|
||||||
|
"nickName": "",
|
||||||
|
"phonenumber": "",
|
||||||
|
"province": null,
|
||||||
|
"deviceCount": null,
|
||||||
|
"remark": "",
|
||||||
|
"status": "0",
|
||||||
|
"statusName": "正常",
|
||||||
|
"updateTime": "",
|
||||||
|
"userId": "",
|
||||||
|
"userName": "",
|
||||||
|
"userNo": "",
|
||||||
|
"avatarOssId": "",
|
||||||
|
"avatarUrl": "",
|
||||||
|
})
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (route.query.id) {
|
||||||
|
getUser(route.query.id as string).then(res => {
|
||||||
|
form.value = {
|
||||||
|
...form.value,
|
||||||
|
...res.data
|
||||||
|
}
|
||||||
|
console.log(form.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="Main">
|
<div class="Main">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between" style="padding-top:22px">
|
||||||
<Pagecrumb></Pagecrumb>
|
<pageCrumb></pageCrumb>
|
||||||
<el-button plain type="info" @click="handleBack">返回用户列表</el-button>
|
<el-button plain type="info" @click="handleBack">返回用户列表</el-button>
|
||||||
</div>
|
</div>
|
||||||
<!-- 用户信息-->
|
<!-- 用户信息-->
|
||||||
<section class="customerInfoBox commonCard flex justify-between flex-items-center">
|
<section class="customerInfoBox commonCard flex justify-between flex-items-center">
|
||||||
<div class="flex infoBox">
|
<div class="flex infoBox">
|
||||||
<el-image
|
<el-image :src="form.avatarUrl || defaultImag" class="customer-Img"></el-image>
|
||||||
class="customer-Img"
|
|
||||||
src="https://ts3.tc.mm.bing.net/th?id=ORMS.48a27ca8353fa47bf0dc8df38854d0e1&pid=Wdp&w=312&h=172&qlt=90&c=1&rs=1&dpr=1&p=0"></el-image>
|
|
||||||
<div class="flex flex-col justify-between">
|
<div class="flex flex-col justify-between">
|
||||||
<div>
|
<div>
|
||||||
<span class="userName">林小满</span>
|
<span class="userName">{{ form.nickName }}</span>
|
||||||
<el-tag effect="light" round style="margin: 0 12px " type="success">
|
<el-tag v-if="form.status === '0'" effect="light" round style="margin: 0 12px " type="success">
|
||||||
<div class="flex flex-items-center"><i class="circle"></i>
|
<div class="flex flex-items-center"><i class="circle"></i>
|
||||||
<span>正常</span></div>
|
<span>正常</span></div>
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<el-tag effect="light" round type="info">150****0831</el-tag>
|
<el-tag v-else effect="light" round style="margin: 0 12px " type="danger">
|
||||||
|
<div class="flex flex-items-center"><i class="circle" style="background-color: red"></i>
|
||||||
|
<span>禁用</span></div>
|
||||||
|
</el-tag>
|
||||||
|
<el-tag effect="light" round type="info"> {{ hiddenPhone(form.phonenumber) }}</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="subInfoBox">
|
<div class="subInfoBox">
|
||||||
<span>用户ID: U20260918001 </span>
|
<span>用户ID: {{ form.userNo }} </span>
|
||||||
<span class="dat">·</span>
|
<span class="dat">·</span>
|
||||||
<span>注册时间: 2026-01-10 14:20</span>
|
<span>注册时间: {{ form.createTime }}</span>
|
||||||
<span class="dat">·</span>
|
<span class="dat">·</span>
|
||||||
<span>最后登录: 2026-09-17 09:35</span>
|
<span>最后登录: {{ form.loginDate }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<el-button size="large" type="primary" @click="handleEdit">编辑用户</el-button>
|
<el-button size="large" type="primary" @click="handleEdit">编辑用户</el-button>
|
||||||
<el-button plain size="large" type="primary">重置密码</el-button>
|
<el-button plain size="large" type="primary" @click="handleReboot">重置密码</el-button>
|
||||||
<el-button plain size="large" type="danger">禁用账号</el-button>
|
<el-button v-if="form.status === '0'" plain size="large" type="danger" @click="changeStatus('1')">禁用账号
|
||||||
|
</el-button>
|
||||||
|
<el-button v-else plain size="large" type="success" @click="changeStatus('0')">启用账号</el-button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -61,27 +131,28 @@ function handleBack() {
|
|||||||
<ul class=" userNameInfoBox">
|
<ul class=" userNameInfoBox">
|
||||||
<li>
|
<li>
|
||||||
<span class="label">用户ID</span>
|
<span class="label">用户ID</span>
|
||||||
<span class="value">U20260918001</span>
|
<span class="value">{{ form.userNo }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="label">手机号</span>
|
<span class="label">手机号</span>
|
||||||
<span class="value">138****6688</span>
|
<span class="value">{{ hiddenPhone(form.phonenumber) }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="label">账号状态</span>
|
<span class="label">账号状态</span>
|
||||||
<el-tag type="success">正常</el-tag>
|
<el-tag v-if="form.status === '0'" type="success">正常</el-tag>
|
||||||
|
<el-tag v-else type="danger">禁用</el-tag>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="label">注册时间</span>
|
<span class="label">注册时间</span>
|
||||||
<span class="value">2026-01-10 14:20</span>
|
<span class="value">{{ form.createTime }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="label">最后登录时间</span>
|
<span class="label">最后登录时间</span>
|
||||||
<span class="value">2026-09-17 09:35</span>
|
<span class="value">{{ form.loginDate }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="label">登录次数</span>
|
<span class="label">登录次数</span>
|
||||||
<span class="value">126次</span>
|
<span class="value">{{ form.loginCount }}次</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,23 +163,23 @@ function handleBack() {
|
|||||||
<cardNav line-color="#14b8a6" title="个人资料"></cardNav>
|
<cardNav line-color="#14b8a6" title="个人资料"></cardNav>
|
||||||
<div class="userInfoInfoBox">
|
<div class="userInfoInfoBox">
|
||||||
<div class="userInfoImageBox">
|
<div class="userInfoImageBox">
|
||||||
<el-image
|
<el-image :src="form.avatarUrl || defaultImag" class="userImage"></el-image>
|
||||||
class="userImage"
|
|
||||||
src="https://ts1.tc.mm.bing.net/th?id=OADD2.1174278572182418_11B688OHMGU9KL6&pid=21.2&c=3&roil=0.0242&roit=0.0014&roir=0.9742&roib=1&w=334&h=184&dynsize=1&qlt=90&dpr=1.50"></el-image>
|
|
||||||
<div class="description">APP端头像</div>
|
<div class="description">APP端头像</div>
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<span class="label">昵称</span>
|
<span class="label">昵称</span>
|
||||||
<span class="value">黎晓曼</span>
|
<span class="value">{{ form.nickName }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="label">性别</span>
|
<span class="label">性别</span>
|
||||||
<span class="value">男</span>
|
<span v-if="form.gender === '0'" class="value">男</span>
|
||||||
|
<span v-else-if="form.gender === '1'" class="value">女</span>
|
||||||
|
<span v-else class="value">未知</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="label">生日</span>
|
<span class="label">生日</span>
|
||||||
<span class="value">1926-09-18</span>
|
<span class="value">{{ form.birthday }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="label"> 所在地区</span>
|
<span class="label"> 所在地区</span>
|
||||||
@@ -129,7 +200,7 @@ function handleBack() {
|
|||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<card-nav line-color="#f59e0b" subTitle="该用户名下全部绑定设备,点击操作可跳转设备详情"
|
<card-nav line-color="#f59e0b" subTitle="该用户名下全部绑定设备,点击操作可跳转设备详情"
|
||||||
title="绑定设备清单"></card-nav>
|
title="绑定设备清单"></card-nav>
|
||||||
<el-tag type="primary">共3台</el-tag>
|
<el-tag type="primary">共{{ form.deviceCount }}台</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<el-table
|
<el-table
|
||||||
class="data-table"
|
class="data-table"
|
||||||
|
|||||||
514
src/views/system/device/devicelist/add.vue
Normal file
514
src/views/system/device/devicelist/add.vue
Normal file
@@ -0,0 +1,514 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-2 page-shell system-device-page">
|
||||||
|
<el-card class="table-panel" shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="toolbar-shell">
|
||||||
|
<div class="table-heading">
|
||||||
|
<h3>设备主列表</h3>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-actions">
|
||||||
|
<el-button v-hasPermi="['system:device:add']" icon="Plus" plain type="primary" @click="handleAdd">新增
|
||||||
|
</el-button>
|
||||||
|
<el-button v-hasPermi="['system:device:edit']" :disabled="single" icon="Edit" plain type="success"
|
||||||
|
@click="handleUpdate()">修改
|
||||||
|
</el-button>
|
||||||
|
<el-button v-hasPermi="['system:device:remove']" :disabled="multiple" icon="Delete" plain type="danger"
|
||||||
|
@click="handleDelete()">删除
|
||||||
|
</el-button>
|
||||||
|
<el-button v-hasPermi="['system:device:export']" icon="Download" plain type="warning" @click="handleExport">
|
||||||
|
导出
|
||||||
|
</el-button>
|
||||||
|
<right-toolbar v-model:show-search="showSearch" :search="false" @query-table="getList"></right-toolbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="deviceList" border class="data-table"
|
||||||
|
@selection-change="handleSelectionChange">
|
||||||
|
<el-table-column align="center" type="selection" width="55"/>
|
||||||
|
<el-table-column align="center" label="主键" prop="deviceId"/>
|
||||||
|
<el-table-column align="center" label="业务编号,未删除记录内唯一;逻辑删除后可再次使用" prop="deviceNo"/>
|
||||||
|
<el-table-column align="center" label="绑定码IMEI,可空;未删除记录内唯一,逻辑删除后可再次使用" prop="imei"/>
|
||||||
|
<el-table-column align="center" label="用户侧名称≤20字符" prop="name"/>
|
||||||
|
<el-table-column align="center" label="产品模板" prop="productId"/>
|
||||||
|
<el-table-column align="center" label="型号快照,新增时取自产品模板" prop="model"/>
|
||||||
|
<el-table-column align="center" label="出厂序列号" prop="sn"/>
|
||||||
|
<el-table-column align="center" label="硬件版本" prop="hwVersion"/>
|
||||||
|
<el-table-column align="center" label="软件版本" prop="swVersion"/>
|
||||||
|
<el-table-column align="center" label="固件版本" prop="fwVersion"/>
|
||||||
|
<el-table-column align="center" label="数据包版本" prop="pkgVersion"/>
|
||||||
|
<el-table-column align="center" label="设备IP" prop="ip"/>
|
||||||
|
<el-table-column align="center" label="MAC,未删除记录内唯一;逻辑删除后可再次添加" prop="mac"/>
|
||||||
|
<el-table-column align="center" label="WiFi SSID" prop="wifiSsid"/>
|
||||||
|
<el-table-column align="center" label="蓝牙" prop="bluetoothConnected"/>
|
||||||
|
<el-table-column align="center" label="状态" prop="status" width="120">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-switch
|
||||||
|
v-model="scope.row.status"
|
||||||
|
:active-value="true"
|
||||||
|
:inactive-value="false"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="当前绑定APP用户" prop="bindUserId"/>
|
||||||
|
<el-table-column align="center" label="最近一次绑定时间" prop="bindTime" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ parseTime(scope.row.bindTime, '{y}-{m}-{d}') }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="当前家庭" prop="homeId"/>
|
||||||
|
<el-table-column align="center" label="当前房间" prop="locationId"/>
|
||||||
|
<el-table-column align="center" label="信号等级" prop="signalLevel"/>
|
||||||
|
<el-table-column align="center" label="最新经度" prop="lng"/>
|
||||||
|
<el-table-column align="center" label="最新纬度" prop="lat"/>
|
||||||
|
<el-table-column align="center" label="地址" prop="address"/>
|
||||||
|
<el-table-column align="center" label="充电" prop="charging"/>
|
||||||
|
<el-table-column align="center" label="设备电量0-100" prop="deviceBattery"/>
|
||||||
|
<el-table-column align="center" label="最近一次运行数据上报时间" prop="lastReportTime" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ parseTime(scope.row.lastReportTime, '{y}-{m}-{d}') }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="最后在线时间" prop="lastOnlineTime" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ parseTime(scope.row.lastOnlineTime, '{y}-{m}-{d}') }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="备注" prop="remark"/>
|
||||||
|
<el-table-column align="center" label="未删除时的业务编号" prop="activeDeviceNo"/>
|
||||||
|
<el-table-column align="center" label="未删除时的IMEI" prop="activeImei"/>
|
||||||
|
<el-table-column align="center" label="未删除时的MAC" prop="activeMac"/>
|
||||||
|
<el-table-column align="center" class-name="small-padding fixed-width" label="操作">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tooltip content="修改" placement="top">
|
||||||
|
<el-button v-hasPermi="['system:device:edit']" icon="Edit" link type="primary"
|
||||||
|
@click="handleUpdate(scope.row)"></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="删除" placement="top">
|
||||||
|
<el-button v-hasPermi="['system:device:remove']" icon="Delete" link type="primary"
|
||||||
|
@click="handleDelete(scope.row)"></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination v-show="total > 0" v-model:limit="queryParams.pageSize" v-model:page="queryParams.pageNum"
|
||||||
|
:total="total" @pagination="getList"/>
|
||||||
|
</el-card>
|
||||||
|
<!-- 添加或修改设备主对话框 -->
|
||||||
|
<el-dialog v-model="dialog.visible" :title="dialog.title" append-to-body width="800px">
|
||||||
|
<el-form ref="deviceFormRef" :model="form" :rules="rules" label-width="250px">
|
||||||
|
<el-form-item label="业务编号,未删除记录内唯一;逻辑删除后可再次使用" prop="deviceNo">
|
||||||
|
<el-input v-model="form.deviceNo" placeholder="请输入业务编号,未删除记录内唯一;逻辑删除后可再次使用"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="绑定码IMEI,可空;未删除记录内唯一,逻辑删除后可再次使用" prop="imei">
|
||||||
|
<el-input v-model="form.imei" placeholder="请输入绑定码IMEI,可空;未删除记录内唯一,逻辑删除后可再次使用"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="用户侧名称≤20字符" prop="name">
|
||||||
|
<el-input v-model="form.name" placeholder="请输入用户侧名称≤20字符"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="产品模板" prop="productId">
|
||||||
|
<el-input v-model="form.productId"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="型号快照,新增时取自产品模板" prop="model">
|
||||||
|
<el-input v-model="form.model" placeholder="请输入型号快照,新增时取自产品模板"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="出厂序列号" prop="sn">
|
||||||
|
<el-input v-model="form.sn" placeholder="请输入出厂序列号"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="硬件版本" prop="hwVersion">
|
||||||
|
<el-input v-model="form.hwVersion" placeholder="请输入硬件版本"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="软件版本" prop="swVersion">
|
||||||
|
<el-input v-model="form.swVersion" placeholder="请输入软件版本"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="固件版本" prop="fwVersion">
|
||||||
|
<el-input v-model="form.fwVersion" placeholder="请输入固件版本"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="数据包版本" prop="pkgVersion">
|
||||||
|
<el-input v-model="form.pkgVersion" placeholder="请输入数据包版本"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备IP" prop="ip">
|
||||||
|
<el-input v-model="form.ip" placeholder="请输入设备IP"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="MAC,未删除记录内唯一;逻辑删除后可再次添加" prop="mac">
|
||||||
|
<el-input v-model="form.mac" placeholder="请输入MAC,未删除记录内唯一;逻辑删除后可再次添加"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="WiFi SSID" prop="wifiSsid">
|
||||||
|
<el-input v-model="form.wifiSsid" placeholder="请输入WiFi SSID"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="蓝牙" prop="bluetoothConnected">
|
||||||
|
<el-input v-model="form.bluetoothConnected" placeholder="请输入蓝牙"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-switch
|
||||||
|
v-model="form.status"
|
||||||
|
:active-value="true"
|
||||||
|
:inactive-value="false"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="当前绑定APP用户" prop="bindUserId">
|
||||||
|
<el-input-number v-model="form.bindUserId" controls-position="right"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="最近一次绑定时间" prop="bindTime">
|
||||||
|
<el-date-picker v-model="form.bindTime"
|
||||||
|
clearable
|
||||||
|
placeholder="请选择最近一次绑定时间"
|
||||||
|
type="datetime"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss">
|
||||||
|
</el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="当前家庭" prop="homeId">
|
||||||
|
<el-input v-model="form.homeId" controls-position="right"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="当前房间" prop="locationId">
|
||||||
|
<el-input v-model="form.locationId" controls-position="right"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="信号等级" prop="signalLevel">
|
||||||
|
<el-input v-model="form.signalLevel" controls-position="right"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="最新经度" prop="lng">
|
||||||
|
<el-input v-model="form.lng" controls-position="right"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="最新纬度" prop="lat">
|
||||||
|
<el-input v-model="form.lat" controls-position="right"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="地址" prop="address">
|
||||||
|
<el-input v-model="form.address" placeholder="请输入地址"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="充电" prop="charging">
|
||||||
|
<el-input v-model="form.charging" placeholder="请输入充电"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备电量0-100" prop="deviceBattery">
|
||||||
|
<el-input-number v-model="form.deviceBattery" controls-position="right"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="最近一次运行数据上报时间" prop="lastReportTime">
|
||||||
|
<el-date-picker v-model="form.lastReportTime"
|
||||||
|
clearable
|
||||||
|
placeholder="请选择最近一次运行数据上报时间"
|
||||||
|
type="datetime"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss">
|
||||||
|
</el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="最后在线时间" prop="lastOnlineTime">
|
||||||
|
<el-date-picker v-model="form.lastOnlineTime"
|
||||||
|
clearable
|
||||||
|
placeholder="请选择最后在线时间"
|
||||||
|
type="datetime"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss">
|
||||||
|
</el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注" prop="remark">
|
||||||
|
<el-input v-model="form.remark" placeholder="请输入内容" type="textarea"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建部门" prop="createDept">
|
||||||
|
<el-input-number v-model="form.createDept" controls-position="right"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建者" prop="createBy">
|
||||||
|
<el-input-number v-model="form.createBy" controls-position="right"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建时间" prop="createTime">
|
||||||
|
<el-date-picker v-model="form.createTime"
|
||||||
|
clearable
|
||||||
|
placeholder="请选择创建时间"
|
||||||
|
type="datetime"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss">
|
||||||
|
</el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="更新者" prop="updateBy">
|
||||||
|
<el-input-number v-model="form.updateBy" controls-position="right"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="更新时间" prop="updateTime">
|
||||||
|
<el-date-picker v-model="form.updateTime"
|
||||||
|
clearable
|
||||||
|
placeholder="请选择更新时间"
|
||||||
|
type="datetime"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss">
|
||||||
|
</el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="未删除时的业务编号" prop="activeDeviceNo">
|
||||||
|
<el-input v-model="form.activeDeviceNo" placeholder="请输入未删除时的业务编号"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="未删除时的IMEI" prop="activeImei">
|
||||||
|
<el-input v-model="form.activeImei" placeholder="请输入未删除时的IMEI"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="未删除时的MAC" prop="activeMac">
|
||||||
|
<el-input v-model="form.activeMac" placeholder="请输入未删除时的MAC"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||||
|
<el-button @click="cancel">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" name="Device" setup>
|
||||||
|
import {
|
||||||
|
addDevice,
|
||||||
|
delDevice,
|
||||||
|
getDevice,
|
||||||
|
listDevice,
|
||||||
|
updateDevice
|
||||||
|
} from '@/api/system/device';
|
||||||
|
import {DeviceForm, DeviceQuery, DeviceVO} from '@/api/system/device/types';
|
||||||
|
import {useLoading} from '@/hooks/async/useLoading';
|
||||||
|
import {useFormDialog} from '@/hooks/dialog/useFormDialog';
|
||||||
|
import {useDateRangeQuery} from '@/hooks/form/useDateRangeQuery';
|
||||||
|
import {useSearchReset} from '@/hooks/form/useSearchReset';
|
||||||
|
import {useSearchToggle} from '@/hooks/form/useSearchToggle';
|
||||||
|
import {useTableSelection} from '@/hooks/table/useTableSelection';
|
||||||
|
import {parseTime} from '@/utils/ruoyi';
|
||||||
|
import modal from '@/plugins/modal';
|
||||||
|
import {download as requestDownload} from '@/utils/request';
|
||||||
|
|
||||||
|
|
||||||
|
const deviceList = ref<DeviceVO[]>([]);
|
||||||
|
const buttonLoading = ref(false);
|
||||||
|
const {loading, withLoading} = useLoading(true);
|
||||||
|
const {showSearch} = useSearchToggle();
|
||||||
|
const total = ref(0);
|
||||||
|
const {
|
||||||
|
dateRange: dateRangeBindTime,
|
||||||
|
applyDateRange: applyBindTimeDateRange,
|
||||||
|
resetDateRange: resetBindTimeDateRange
|
||||||
|
} = useDateRangeQuery('BindTime');
|
||||||
|
const {
|
||||||
|
dateRange: dateRangeLastReportTime,
|
||||||
|
applyDateRange: applyLastReportTimeDateRange,
|
||||||
|
resetDateRange: resetLastReportTimeDateRange
|
||||||
|
} = useDateRangeQuery('LastReportTime');
|
||||||
|
const {
|
||||||
|
dateRange: dateRangeLastOnlineTime,
|
||||||
|
applyDateRange: applyLastOnlineTimeDateRange,
|
||||||
|
resetDateRange: resetLastOnlineTimeDateRange
|
||||||
|
} = useDateRangeQuery('LastOnlineTime');
|
||||||
|
const {
|
||||||
|
dateRange: dateRangeCreateTime,
|
||||||
|
applyDateRange: applyCreateTimeDateRange,
|
||||||
|
resetDateRange: resetCreateTimeDateRange
|
||||||
|
} = useDateRangeQuery('CreateTime');
|
||||||
|
const {
|
||||||
|
dateRange: dateRangeUpdateTime,
|
||||||
|
applyDateRange: applyUpdateTimeDateRange,
|
||||||
|
resetDateRange: resetUpdateTimeDateRange
|
||||||
|
} = useDateRangeQuery('UpdateTime');
|
||||||
|
|
||||||
|
const queryFormRef = ref<ElFormInstance>();
|
||||||
|
const deviceFormRef = ref<ElFormInstance>();
|
||||||
|
|
||||||
|
const initFormData: DeviceForm = {
|
||||||
|
deviceId: undefined,
|
||||||
|
deviceNo: undefined,
|
||||||
|
imei: undefined,
|
||||||
|
name: undefined,
|
||||||
|
productId: undefined,
|
||||||
|
model: undefined,
|
||||||
|
sn: undefined,
|
||||||
|
hwVersion: undefined,
|
||||||
|
swVersion: undefined,
|
||||||
|
fwVersion: undefined,
|
||||||
|
pkgVersion: undefined,
|
||||||
|
ip: undefined,
|
||||||
|
mac: undefined,
|
||||||
|
wifiSsid: undefined,
|
||||||
|
bluetoothConnected: undefined,
|
||||||
|
status: undefined,
|
||||||
|
bindUserId: undefined,
|
||||||
|
bindTime: undefined,
|
||||||
|
homeId: undefined,
|
||||||
|
locationId: undefined,
|
||||||
|
signalLevel: undefined,
|
||||||
|
lng: undefined,
|
||||||
|
lat: undefined,
|
||||||
|
address: undefined,
|
||||||
|
charging: undefined,
|
||||||
|
deviceBattery: undefined,
|
||||||
|
lastReportTime: undefined,
|
||||||
|
lastOnlineTime: undefined,
|
||||||
|
remark: undefined,
|
||||||
|
createDept: undefined,
|
||||||
|
createBy: undefined,
|
||||||
|
createTime: undefined,
|
||||||
|
updateBy: undefined,
|
||||||
|
updateTime: undefined,
|
||||||
|
activeDeviceNo: undefined,
|
||||||
|
activeImei: undefined,
|
||||||
|
activeMac: undefined,
|
||||||
|
}
|
||||||
|
const data = reactive<PageData<DeviceForm, DeviceQuery>>({
|
||||||
|
form: {...initFormData},
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
deviceNo: undefined,
|
||||||
|
imei: undefined,
|
||||||
|
name: undefined,
|
||||||
|
productId: undefined,
|
||||||
|
model: undefined,
|
||||||
|
sn: undefined,
|
||||||
|
hwVersion: undefined,
|
||||||
|
swVersion: undefined,
|
||||||
|
fwVersion: undefined,
|
||||||
|
pkgVersion: undefined,
|
||||||
|
ip: undefined,
|
||||||
|
mac: undefined,
|
||||||
|
wifiSsid: undefined,
|
||||||
|
bluetoothConnected: undefined,
|
||||||
|
status: undefined,
|
||||||
|
bindUserId: undefined,
|
||||||
|
homeId: undefined,
|
||||||
|
locationId: undefined,
|
||||||
|
signalLevel: undefined,
|
||||||
|
lng: undefined,
|
||||||
|
lat: undefined,
|
||||||
|
address: undefined,
|
||||||
|
charging: undefined,
|
||||||
|
deviceBattery: undefined,
|
||||||
|
createDept: undefined,
|
||||||
|
createBy: undefined,
|
||||||
|
updateBy: undefined,
|
||||||
|
activeDeviceNo: undefined,
|
||||||
|
activeImei: undefined,
|
||||||
|
activeMac: undefined,
|
||||||
|
params: {
|
||||||
|
bindTime: undefined,
|
||||||
|
lastReportTime: undefined,
|
||||||
|
lastOnlineTime: undefined,
|
||||||
|
createTime: undefined,
|
||||||
|
updateTime: undefined,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
deviceId: [
|
||||||
|
{required: true, message: "主键不能为空", trigger: "change"}
|
||||||
|
],
|
||||||
|
deviceNo: [
|
||||||
|
{required: true, message: "业务编号,未删除记录内唯一;逻辑删除后可再次使用不能为空", trigger: "blur"}
|
||||||
|
],
|
||||||
|
bluetoothConnected: [
|
||||||
|
{required: true, message: "蓝牙不能为空", trigger: "blur"}
|
||||||
|
],
|
||||||
|
status: [
|
||||||
|
{required: true, message: "状态不能为空", trigger: "change"}
|
||||||
|
],
|
||||||
|
signalLevel: [
|
||||||
|
{required: true, message: "信号等级不能为空", trigger: "change"}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const {queryParams, form, rules} = toRefs(data);
|
||||||
|
const {ids, single, multiple, handleSelectionChange} = useTableSelection<DeviceVO>(item => item.deviceId);
|
||||||
|
const {dialog, resetForm: reset, openDialog, showDialog, closeDialog} = useFormDialog({
|
||||||
|
form,
|
||||||
|
formRef: deviceFormRef,
|
||||||
|
initialFormData: initFormData
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 查询设备主列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
await withLoading(async () => {
|
||||||
|
let params = queryParams.value;
|
||||||
|
params = applyBindTimeDateRange(params);
|
||||||
|
params = applyLastReportTimeDateRange(params);
|
||||||
|
params = applyLastOnlineTimeDateRange(params);
|
||||||
|
params = applyCreateTimeDateRange(params);
|
||||||
|
params = applyUpdateTimeDateRange(params);
|
||||||
|
const res = await listDevice(params);
|
||||||
|
deviceList.value = res.data?.rows;
|
||||||
|
total.value = res.data?.total;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 取消按钮 */
|
||||||
|
const cancel = () => {
|
||||||
|
reset();
|
||||||
|
closeDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.value.pageNum = 1;
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
const {resetQuery} = useSearchReset({
|
||||||
|
queryFormRef,
|
||||||
|
queryParams,
|
||||||
|
pageNumKey: 'pageNum',
|
||||||
|
pageSizeKey: 'pageSize',
|
||||||
|
initialPageSize: 10,
|
||||||
|
resetExtras: () => {
|
||||||
|
resetBindTimeDateRange();
|
||||||
|
resetLastReportTimeDateRange();
|
||||||
|
resetLastOnlineTimeDateRange();
|
||||||
|
resetCreateTimeDateRange();
|
||||||
|
resetUpdateTimeDateRange();
|
||||||
|
},
|
||||||
|
afterReset: () => {
|
||||||
|
handleQuery();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 新增按钮操作 */
|
||||||
|
const handleAdd = () => {
|
||||||
|
openDialog('添加设备主');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 修改按钮操作 */
|
||||||
|
const handleUpdate = async (row?: Partial<DeviceVO>) => {
|
||||||
|
reset();
|
||||||
|
const _deviceId = row?.deviceId || ids.value[0];
|
||||||
|
const res = await getDevice(_deviceId);
|
||||||
|
Object.assign(form.value, res.data);
|
||||||
|
showDialog('修改设备主');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 提交按钮 */
|
||||||
|
const submitForm = () => {
|
||||||
|
deviceFormRef.value?.validate(async (valid: boolean) => {
|
||||||
|
if (valid) {
|
||||||
|
buttonLoading.value = true;
|
||||||
|
if (form.value.deviceId) {
|
||||||
|
await updateDevice(form.value).finally(() => (buttonLoading.value = false));
|
||||||
|
} else {
|
||||||
|
await addDevice(form.value).finally(() => (buttonLoading.value = false));
|
||||||
|
}
|
||||||
|
modal.msgSuccess('操作成功');
|
||||||
|
closeDialog();
|
||||||
|
await getList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 删除按钮操作 */
|
||||||
|
const handleDelete = async (row?: Partial<DeviceVO>) => {
|
||||||
|
const _deviceIds = row?.deviceId || ids.value;
|
||||||
|
await modal.confirm('是否确认删除设备主编号为"' + _deviceIds + '"的数据项?');
|
||||||
|
await delDevice(_deviceIds);
|
||||||
|
modal.msgSuccess('删除成功');
|
||||||
|
await getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 导出按钮操作 */
|
||||||
|
const handleExport = () => {
|
||||||
|
requestDownload(
|
||||||
|
'device/device/export',
|
||||||
|
{
|
||||||
|
...queryParams.value
|
||||||
|
},
|
||||||
|
`device_${new Date().getTime()}.xlsx`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
@@ -2,15 +2,20 @@
|
|||||||
import cardNav from '@/components/cardNav/index.vue';
|
import cardNav from '@/components/cardNav/index.vue';
|
||||||
import FormItemTip from '@/components/FormItemTip/index.vue';
|
import FormItemTip from '@/components/FormItemTip/index.vue';
|
||||||
import {DeviceForm} from "@/api/system/device/types";
|
import {DeviceForm} from "@/api/system/device/types";
|
||||||
import {getDevice} from "@/api/system/device";
|
import {getDevice, updateDevice} from "@/api/system/device";
|
||||||
|
import modal from "@/plugins/modal";
|
||||||
|
|
||||||
function submitForm() {
|
function submitForm() {
|
||||||
if (!form.value.name || form.value.name.trim() === "") {
|
if (!form.value.name || form.value.name.trim() === "") {
|
||||||
return ElMessage.warning('设备名称不能为空')
|
return ElMessage.warning('设备名称不能为空')
|
||||||
}
|
}
|
||||||
if (!form.value.houseName || form.value.houseName.trim() === "") {
|
if (!form.value.locationName || form.value.locationName.trim() === "") {
|
||||||
return ElMessage.warning('所在房间不能为空')
|
return ElMessage.warning('所在房间不能为空')
|
||||||
}
|
}
|
||||||
|
updateDevice(form.value).then(() => {
|
||||||
|
modal.msgSuccess('修改成功');
|
||||||
|
backMain()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -19,11 +24,20 @@ const router = useRouter()
|
|||||||
function backMain() {
|
function backMain() {
|
||||||
router.replace(
|
router.replace(
|
||||||
{
|
{
|
||||||
path: '/device/deviceMain'
|
path: '/device/deviceMain',
|
||||||
|
query: {
|
||||||
|
id: form.value.deviceId,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function backList() {
|
||||||
|
router.replace({
|
||||||
|
path: '/device/deviceList',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const initFormData: DeviceForm = {
|
const initFormData: DeviceForm = {
|
||||||
deviceId: undefined,
|
deviceId: undefined,
|
||||||
@@ -82,9 +96,10 @@ init()
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="edit">
|
<div class="edit">
|
||||||
<nav class="flex justify-between">
|
<nav class="flex justify-between" style="padding-top:22px">
|
||||||
<Pagecrumb></Pagecrumb>
|
<Pagecrumb></Pagecrumb>
|
||||||
<div>
|
<div>
|
||||||
|
<el-button plain type="info" @click="backList">返回设备列表</el-button>
|
||||||
<el-button plain type="info" @click="backMain">返回设备详情</el-button>
|
<el-button plain type="info" @click="backMain">返回设备详情</el-button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -153,11 +168,11 @@ init()
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTip
|
<FormItemTip
|
||||||
v-model="form.houseName"
|
v-model="form.locationName"
|
||||||
:inputAttrs="{ placeholder: '请输入所在房间', }"
|
:inputAttrs="{ placeholder: '请输入所在房间', }"
|
||||||
:required="true"
|
:required="true"
|
||||||
label="所在房间"
|
label="所在房间"
|
||||||
prop="houseName"
|
prop="locationName"
|
||||||
tip="必填,可选:客厅 / 洗衣间 / 阳台 / 厨房"
|
tip="必填,可选:客厅 / 洗衣间 / 阳台 / 厨房"
|
||||||
>
|
>
|
||||||
</FormItemTip>
|
</FormItemTip>
|
||||||
@@ -179,13 +194,13 @@ init()
|
|||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<div class="readTimeInfo">
|
<div v-if="form.purifier" class="readTimeInfo">
|
||||||
<div class="nav flex justify-between">
|
<div class="nav flex justify-between">
|
||||||
<div>
|
<div>
|
||||||
<span class="title">实时运行数据</span>
|
<span class="title">实时运行数据</span>
|
||||||
<el-tag style="margin-left:4px" type="info">只读</el-tag>
|
<el-tag style="margin-left:4px" type="info">只读</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<span class="tip">由设备蓝牙自动上报,后台不可编辑 · 更新于 2026-09-10 12:50:30</span>
|
<span class="tip">由设备蓝牙自动上报,后台不可编辑 · 更新于 {{ form.updateTime }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -193,34 +208,39 @@ init()
|
|||||||
<li class="li-item">
|
<li class="li-item">
|
||||||
<div class="title">TVOC数值</div>
|
<div class="title">TVOC数值</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<span class="Num">286</span>
|
<span class="Num">{{ form.tvoc }}</span>
|
||||||
<el-tag type="success">优</el-tag>
|
<el-tag type="success">优</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="li-item">
|
<li class="li-item">
|
||||||
<div class="title">运行模式</div>
|
<div class="title">运行模式</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<span class="Num">桌面模式</span>
|
<span v-if="form.runMode == '2'" class="Num">桌面模式</span>
|
||||||
|
<span v-else-if="form.runMode == '1'" class="Num">车载模式</span>
|
||||||
|
<span v-else class="Num">自动</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="li-item">
|
<li class="li-item">
|
||||||
<div class="title">风速挡位</div>
|
<div class="title">风速挡位</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<span class="Num">标准</span>
|
<span v-if="form.fanLevel == '1'" class="Num">强力</span>
|
||||||
|
<span v-else-if="form.fanLevel == '2'" class="Num">低档</span>
|
||||||
|
<span v-else class="Num">标准</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="li-item">
|
<li class="li-item">
|
||||||
<div class="title">供电模式</div>
|
<div class="title">供电模式</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<span class="Num">USB供电</span>
|
<span v-if="form.powerMode == '0'" class="Num">USB供电</span>
|
||||||
|
<span v-else class="Num">电池供电</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="li-item">
|
<li class="li-item">
|
||||||
<div class="title">滤芯寿命</div>
|
<div class="title">滤芯寿命</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<span class="Num">76%</span>
|
<span class="Num">{{ form.filterLife }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tip">剩余128天</div>
|
<div class="tip">剩余{{ form.filterRemainDays }}天</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="li-item">
|
<li class="li-item">
|
||||||
<div class="title">电池电量</div>
|
<div class="title">电池电量</div>
|
||||||
|
|||||||
@@ -2,11 +2,6 @@
|
|||||||
<div class="p-2 page-shell system-device-page">
|
<div class="p-2 page-shell system-device-page">
|
||||||
<div class="search-wrap">
|
<div class="search-wrap">
|
||||||
<el-card :class="{ 'is-collapsed': !showSearch }" class="search-panel" shadow="hover">
|
<el-card :class="{ 'is-collapsed': !showSearch }" class="search-panel" shadow="hover">
|
||||||
<template #header>
|
|
||||||
<div class="panel-heading search-panel-toggle" @click.stop="showSearch = !showSearch">
|
|
||||||
<div><h3>筛选条件</h3></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="query-form" label-position="top">
|
<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="query-form" label-position="top">
|
||||||
<el-form-item label="设备名称" prop="name">
|
<el-form-item label="设备名称" prop="name">
|
||||||
<el-input v-model="queryParams.name" clearable placeholder="请输入设备名称" size="large"
|
<el-input v-model="queryParams.name" clearable placeholder="请输入设备名称" size="large"
|
||||||
@@ -37,11 +32,11 @@
|
|||||||
<div class="table-heading">
|
<div class="table-heading">
|
||||||
<el-button type="primary">
|
<el-button type="primary">
|
||||||
<span style="margin:0 8px;color: #fff;">全部</span>
|
<span style="margin:0 8px;color: #fff;">全部</span>
|
||||||
<span style="color: #fff;font-weight: bold">1286</span>
|
<span style="color: #fff;font-weight: bold">{{ total }}</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<el-button v-hasPermi="['system:device:export']" plain size="large" type="info" @click="handleExport">
|
<el-button v-hasPermi="['device:device:export']" plain size="large" type="info" @click="handleExport">
|
||||||
批量导出
|
批量导出
|
||||||
</el-button>
|
</el-button>
|
||||||
<right-toolbar v-model:show-search="showSearch" :search="false" @query-table="getList"></right-toolbar>
|
<right-toolbar v-model:show-search="showSearch" :search="false" @query-table="getList"></right-toolbar>
|
||||||
@@ -54,63 +49,62 @@
|
|||||||
<el-table-column align="center" type="selection" width="55"/>
|
<el-table-column align="center" type="selection" width="55"/>
|
||||||
<el-table-column align="center" label="设备名称" prop="name"/>
|
<el-table-column align="center" label="设备名称" prop="name"/>
|
||||||
<el-table-column align="center" label="产品型号" prop="model"/>
|
<el-table-column align="center" label="产品型号" prop="model"/>
|
||||||
<el-table-column align="center" label="所在房间" prop="homeId"/>
|
<el-table-column align="center" label="所在房间" prop="homeName"/>
|
||||||
<el-table-column align="center" label="在线状态" prop="status">
|
<el-table-column align="center" label="在线状态" prop="status" width="100">
|
||||||
<template #default>
|
<template #default="scope">
|
||||||
<el-tag type="success">在线</el-tag>
|
<el-tag v-if="scope.row.status === '0'" type="success">在线</el-tag>
|
||||||
<el-tag type="danger">离线</el-tag>
|
<el-tag v-else type="danger">离线</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column align="center" label="当前TVOC" prop="tvoc">
|
<el-table-column align="center" label="当前TVOC" prop="tvoc">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<div>
|
<div class="flex flex-items-center justify-center">
|
||||||
<div>{{ scope.row.tvoc ?? '--' }}</div>
|
<div>{{ scope.row.tvoc ?? '--' }}</div>
|
||||||
<div>
|
<div>
|
||||||
<el-tag v-if="scope.row.airLevel == '0'" type="primary">优</el-tag>
|
<el-tag v-if="scope.row.airLevel == '0'" type="primary">优</el-tag>
|
||||||
<el-tag v-else-if="scope.row.airLevel == '1'" type="success">良</el-tag>
|
<el-tag v-else-if="scope.row.airLevel == '1'" type="success">良</el-tag>
|
||||||
<el-tag v-else-if="scope.row.airLevel == '2'" type="warning">差</el-tag>
|
<el-tag v-else-if="scope.row.airLevel == '2'" type="warning">差</el-tag>
|
||||||
<el-tag v-else-if="scope.row.airLevel == '3'" type="danger">污染</el-tag>
|
<el-tag v-else-if="scope.row.airLevel == '3'" type="danger">污染</el-tag>
|
||||||
<el-tag v-else type="info">--</el-tag>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column align="center" label="滤芯寿命" prop="fwVersion">
|
<el-table-column align="center" label="滤芯寿命" prop="filterLife" width="100">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span :class="{
|
<span :class="{
|
||||||
lowLife: scope.row.filterLife < 50
|
lowLife: scope.row.filterLife < 50
|
||||||
}" class="defaultLife">{{ scope.row.filterLife }}</span>
|
}" class="defaultLife">{{ scope.row.filterLife }}%</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column align="center" label="电池电量" prop="deviceBattery">
|
<el-table-column align="center" label="电池电量" prop="deviceBattery" width="100">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span :class="{
|
<span :class="{
|
||||||
lowBattery: scope.row.filterLife < 50
|
lowBattery: scope.row.filterLife < 50
|
||||||
}" class="defaultLife">{{ scope.row.filterLife }}</span>
|
}" class="defaultLife">{{ scope.row.filterLife }}%</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column align="center" label="运行模式" prop="runMode">
|
<el-table-column align="center" label="运行模式" prop="runMode" width="100">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span v-if="scope.row.runMode == '2'" style="color: rgba(17, 24, 39, 1);">桌面模式</span>
|
<span v-if="scope.row.runMode == '2'" style="color: rgba(17, 24, 39, 1);">桌面模式</span>
|
||||||
<span v-else-if="scope.row.runMode == '1'" style="color: rgba(17, 24, 39, 1);">车载模式</span>
|
<span v-else-if="scope.row.runMode == '1'" style="color: rgba(17, 24, 39, 1);">车载模式</span>
|
||||||
<span v-else style="color: rgba(17, 24, 39, 1);">自动</span>
|
<span v-else style="color: rgba(17, 24, 39, 1);">自动</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column align="center" label="风速挡位" prop="mac">
|
<el-table-column align="center" label="风速挡位" prop="mac" width="100">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span v-if="scope.row.fanLevel == '1'" style="color: rgba(17, 24, 39, 1);">强力</span>
|
<span v-if="scope.row.fanLevel == '1'" style="color: rgba(17, 24, 39, 1);">强力</span>
|
||||||
<span v-else-if="scope.row.fanLevel == '2'" style="color: rgba(17, 24, 39, 1);">低档</span>
|
<span v-else-if="scope.row.fanLevel == '2'" style="color: rgba(17, 24, 39, 1);">低档</span>
|
||||||
<span v-else style="color: rgba(17, 24, 39, 1);">标准</span>
|
<span v-else style="color: rgba(17, 24, 39, 1);">标准</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column align="center" label="创建时间" prop="wifiSsid"/>
|
<el-table-column align="center" label="创建时间" prop="createTime"/>
|
||||||
<el-table-column align="center" class-name="small-padding fixed-width" label="操作">
|
<el-table-column align="center" class-name="small-padding fixed-width" label="操作" min-width="100">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button v-hasPermi="['system:device:edit']" plain type="primary"
|
<el-button v-hasPermi="['device:device:query']" plain type="primary"
|
||||||
@click="handleUpdate(scope.row)">查看详情
|
@click="handleUpdate(scope.row)">查看详情
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-hasPermi="['system:device:remove']" plain type="info"
|
<el-button v-hasPermi="['device:device:edit']" plain type="info"
|
||||||
@click="handleDelete(scope.row)">编辑
|
@click="handleEdit(scope.row)">编辑
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -129,7 +123,6 @@ import {
|
|||||||
} from '@/api/system/device';
|
} from '@/api/system/device';
|
||||||
import {DeviceForm, DeviceQuery, DeviceVO} from '@/api/system/device/types';
|
import {DeviceForm, DeviceQuery, DeviceVO} from '@/api/system/device/types';
|
||||||
import {useLoading} from '@/hooks/async/useLoading';
|
import {useLoading} from '@/hooks/async/useLoading';
|
||||||
import {useFormDialog} from '@/hooks/dialog/useFormDialog';
|
|
||||||
import {useDateRangeQuery} from '@/hooks/form/useDateRangeQuery';
|
import {useDateRangeQuery} from '@/hooks/form/useDateRangeQuery';
|
||||||
import {useSearchReset} from '@/hooks/form/useSearchReset';
|
import {useSearchReset} from '@/hooks/form/useSearchReset';
|
||||||
import {useSearchToggle} from '@/hooks/form/useSearchToggle';
|
import {useSearchToggle} from '@/hooks/form/useSearchToggle';
|
||||||
@@ -165,7 +158,6 @@ const {
|
|||||||
} = useDateRangeQuery('UpdateTime');
|
} = useDateRangeQuery('UpdateTime');
|
||||||
|
|
||||||
const queryFormRef = ref<ElFormInstance>();
|
const queryFormRef = ref<ElFormInstance>();
|
||||||
const deviceFormRef = ref<ElFormInstance>();
|
|
||||||
|
|
||||||
const initFormData: DeviceForm = {
|
const initFormData: DeviceForm = {
|
||||||
deviceId: undefined,
|
deviceId: undefined,
|
||||||
@@ -268,13 +260,9 @@ const data = reactive<PageData<DeviceForm, DeviceQuery>>({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const {queryParams, form} = toRefs(data);
|
const {queryParams,} = toRefs(data);
|
||||||
const {ids, handleSelectionChange} = useTableSelection<DeviceVO>(item => item.deviceId);
|
const {ids, handleSelectionChange} = useTableSelection<DeviceVO>(item => item.deviceId);
|
||||||
useFormDialog({
|
|
||||||
form,
|
|
||||||
formRef: deviceFormRef,
|
|
||||||
initialFormData: initFormData
|
|
||||||
});
|
|
||||||
|
|
||||||
/** 查询设备主列表 */
|
/** 查询设备主列表 */
|
||||||
const getList = async () => {
|
const getList = async () => {
|
||||||
@@ -326,6 +314,15 @@ function handleUpdate(row: Partial<DeviceVO>) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleEdit(row: Partial<DeviceVO>) {
|
||||||
|
router.push({
|
||||||
|
path: '/device/deviceEdit',
|
||||||
|
query: {
|
||||||
|
id: row.deviceId,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/** 删除按钮操作 */
|
/** 删除按钮操作 */
|
||||||
const handleDelete = async (row?: Partial<DeviceVO>) => {
|
const handleDelete = async (row?: Partial<DeviceVO>) => {
|
||||||
const _deviceIds = row?.deviceId || ids.value;
|
const _deviceIds = row?.deviceId || ids.value;
|
||||||
@@ -338,11 +335,11 @@ const handleDelete = async (row?: Partial<DeviceVO>) => {
|
|||||||
/** 导出按钮操作 */
|
/** 导出按钮操作 */
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
requestDownload(
|
requestDownload(
|
||||||
'system/device/export',
|
'/device/device/export',
|
||||||
{
|
{
|
||||||
...queryParams.value
|
...queryParams.value
|
||||||
},
|
},
|
||||||
`device_${new Date().getTime()}.xlsx`
|
`设备管理_${new Date().getTime()}.xlsx`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -352,6 +349,11 @@ onMounted(() => {
|
|||||||
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.search-wrap {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.defaultLife {
|
.defaultLife {
|
||||||
color: gray;
|
color: gray;
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ function backList() {
|
|||||||
|
|
||||||
function editDevice() {
|
function editDevice() {
|
||||||
router.push({
|
router.push({
|
||||||
path: '/device/deviceEdit'
|
path: '/device/deviceEdit',
|
||||||
|
query: {
|
||||||
|
id: form.value.deviceId,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +79,7 @@ init()
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="Main">
|
<div class="Main">
|
||||||
<nav class="flex justify-between">
|
<nav class="flex justify-between" style="padding-top: 22px;">
|
||||||
<Pagecrumb></Pagecrumb>
|
<Pagecrumb></Pagecrumb>
|
||||||
<div>
|
<div>
|
||||||
<el-button plain type="info" @click="backList">返回设备列表</el-button>
|
<el-button plain type="info" @click="backList">返回设备列表</el-button>
|
||||||
@@ -98,7 +101,7 @@ init()
|
|||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-list-item">
|
<div class="info-list-item">
|
||||||
<div class="title">设备ID</div>
|
<div class="title">设备ID</div>
|
||||||
<div class="value">{{ form.deviceid }}</div>
|
<div class="value">{{ form.deviceId }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-list-item">
|
<div class="info-list-item">
|
||||||
<div class="title">MAC地址</div>
|
<div class="title">MAC地址</div>
|
||||||
@@ -117,7 +120,7 @@ init()
|
|||||||
</div>
|
</div>
|
||||||
<div class="timeBox">
|
<div class="timeBox">
|
||||||
<div class="title">最后上报时间</div>
|
<div class="title">最后上报时间</div>
|
||||||
<div class="time">{{ form.lastReportTime }}</div>
|
<div class="time">{{ form.updateTime }}</div>
|
||||||
<div class="tip">
|
<div class="tip">
|
||||||
<i class="circle"></i>
|
<i class="circle"></i>
|
||||||
<span>每30s自动刷新</span>
|
<span>每30s自动刷新</span>
|
||||||
@@ -161,7 +164,7 @@ init()
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="readTimeBox">
|
<div v-if="form.purifier" class="readTimeBox">
|
||||||
<cardNav line-color="#14b8a6" subTitle="设备蓝牙上报 · 只读 · 每 30 秒自动刷新"
|
<cardNav line-color="#14b8a6" subTitle="设备蓝牙上报 · 只读 · 每 30 秒自动刷新"
|
||||||
title="设备实时运行状态"></cardNav>
|
title="设备实时运行状态"></cardNav>
|
||||||
<div class="flexBox">
|
<div class="flexBox">
|
||||||
@@ -228,17 +231,17 @@ init()
|
|||||||
<div class="ageBox">
|
<div class="ageBox">
|
||||||
<div class="title">滤芯寿命</div>
|
<div class="title">滤芯寿命</div>
|
||||||
<div class="processInfo">
|
<div class="processInfo">
|
||||||
<span class="num">76</span>
|
<span class="num">{{ form.filterLife }}</span>
|
||||||
<span class="unit">%</span>
|
<span class="unit">%</span>
|
||||||
</div>
|
</div>
|
||||||
<el-progress :percentage="30" :show-text="false"></el-progress>
|
<el-progress :percentage="form.filterLife" :show-text="false"></el-progress>
|
||||||
<div class="tip">
|
<div class="tip">
|
||||||
<span>预计剩余可用128天</span>
|
<span>预计剩余可用{{ form.filterRemainDays }}天</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="line"></div>
|
<div class="line"></div>
|
||||||
<div class="ageBox BatteryBox">
|
<div class="ageBox BatteryBox">
|
||||||
<div class="title">滤芯寿命</div>
|
<div class="title">电池电量</div>
|
||||||
<div class="processInfo">
|
<div class="processInfo">
|
||||||
<span class="num">76</span>
|
<span class="num">76</span>
|
||||||
<span class="unit">%</span>
|
<span class="unit">%</span>
|
||||||
@@ -274,6 +277,8 @@ init()
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
|
||||||
.infoBox {
|
.infoBox {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
@@ -461,7 +466,7 @@ init()
|
|||||||
margin-top: 44px;
|
margin-top: 44px;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(381px, 1fr));
|
grid-template-columns: repeat(2, minmax(360px, 1fr));
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
grid-template-columns: repeat(1, minmax(381px, 1fr));
|
grid-template-columns: repeat(1, minmax(381px, 1fr));
|
||||||
}
|
}
|
||||||
|
|||||||
77
src/views/system/device/devicelist/type.ts
Normal file
77
src/views/system/device/devicelist/type.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/**设备状态*/
|
||||||
|
enum Status {
|
||||||
|
/**未激活*/
|
||||||
|
unActive = '0',
|
||||||
|
/**在线*/
|
||||||
|
online = '1',
|
||||||
|
/**离线*/
|
||||||
|
offline = '2',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**运行模式*/
|
||||||
|
enum runMode {
|
||||||
|
/**自动*/
|
||||||
|
auto = 0,
|
||||||
|
/**车载模式*/
|
||||||
|
car = 0,
|
||||||
|
/**桌面模式*/
|
||||||
|
dask = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 风速
|
||||||
|
* */
|
||||||
|
enum fanLevel {
|
||||||
|
/**
|
||||||
|
* 标准
|
||||||
|
*/
|
||||||
|
normal = 0,
|
||||||
|
/**
|
||||||
|
* 强力
|
||||||
|
* */
|
||||||
|
power = 1,
|
||||||
|
/**
|
||||||
|
* 低档
|
||||||
|
* */
|
||||||
|
lower = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 空气质量
|
||||||
|
* */
|
||||||
|
enum airLevel {
|
||||||
|
/**优*/
|
||||||
|
Excellent,
|
||||||
|
/**良*/
|
||||||
|
good,
|
||||||
|
/**差*/
|
||||||
|
poor,
|
||||||
|
/**污染*/
|
||||||
|
polluted
|
||||||
|
}
|
||||||
|
|
||||||
|
/**供电方式*/
|
||||||
|
enum powerMode {
|
||||||
|
/**USB*/
|
||||||
|
USB,
|
||||||
|
/**电池*/
|
||||||
|
Battery
|
||||||
|
}
|
||||||
|
|
||||||
|
/**蓝牙连接情况*/
|
||||||
|
enum bluetoothConnected {
|
||||||
|
/**未连接*/
|
||||||
|
unConnected = 0,
|
||||||
|
/**已连接*/
|
||||||
|
Connected = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {number}
|
||||||
|
* */
|
||||||
|
enum parkingShutDown {
|
||||||
|
/**开*/
|
||||||
|
open,
|
||||||
|
/**关*/
|
||||||
|
close
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-2 app-container dict-page">
|
<div class="dict-page">
|
||||||
<el-row :gutter="16" class="dict-grid">
|
<el-row :gutter="16" class="dict-grid">
|
||||||
<!-- 字典类型 -->
|
<!-- 字典类型 -->
|
||||||
<el-col :xs="24" :lg="12">
|
<el-col :xs="24" :lg="12">
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<right-toolbar v-model:show-search="showTypeSearch" :search="false" @query-table="getTypeList" />
|
<right-toolbar v-model:show-search="showTypeSearch" :search="false" @query-table="getTypeList"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -97,15 +97,13 @@
|
|||||||
ref="typeTableRef"
|
ref="typeTableRef"
|
||||||
v-loading="typeLoading"
|
v-loading="typeLoading"
|
||||||
border
|
border
|
||||||
class="data-table"
|
|
||||||
:data="typeList"
|
:data="typeList"
|
||||||
highlight-current-row
|
|
||||||
@row-click="handleTypeRowClick"
|
@row-click="handleTypeRowClick"
|
||||||
@selection-change="handleTypeSelectionChange"
|
@selection-change="handleTypeSelectionChange"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" align="center" />
|
<el-table-column align="center" type="selection" width="55"/>
|
||||||
<el-table-column v-if="false" label="字典编号" align="center" prop="dictId" />
|
<el-table-column v-if="false" align="center" label="字典编号" prop="dictId"/>
|
||||||
<el-table-column label="字典名称" align="center" prop="dictName" width="120" />
|
<el-table-column align="center" label="字典名称" prop="dictName" width="120"/>
|
||||||
<el-table-column label="字典类型" align="center" prop="dictType" width="160">
|
<el-table-column label="字典类型" align="center" prop="dictType" width="160">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span class="link-type" @click.stop="handleTypeRowClick(scope.row)">
|
<span class="link-type" @click.stop="handleTypeRowClick(scope.row)">
|
||||||
@@ -113,7 +111,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="备注" align="center" prop="remark" width="160" />
|
<el-table-column align="center" label="备注" prop="remark" width="160"/>
|
||||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||||
@@ -178,7 +176,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<right-toolbar v-model:show-search="showDataSearch" :search="false" @query-table="getDataList" />
|
<right-toolbar v-model:show-search="showDataSearch" :search="false" @query-table="getDataList"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -250,12 +248,12 @@
|
|||||||
<el-table
|
<el-table
|
||||||
v-loading="dataLoading"
|
v-loading="dataLoading"
|
||||||
border
|
border
|
||||||
class="data-table"
|
style="max-height:500px;"
|
||||||
:data="dataList"
|
:data="dataList"
|
||||||
@selection-change="handleDataSelectionChange"
|
@selection-change="handleDataSelectionChange"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" align="center" />
|
<el-table-column align="center" type="selection" width="55"/>
|
||||||
<el-table-column v-if="false" label="字典编码" align="center" prop="dictCode" />
|
<el-table-column v-if="false" align="center" label="字典编码" prop="dictCode"/>
|
||||||
<el-table-column label="字典标签" align="center" prop="dictLabel" width="100">
|
<el-table-column label="字典标签" align="center" prop="dictLabel" width="100">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span
|
<span
|
||||||
@@ -279,9 +277,9 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="字典键值" align="center" prop="dictValue" width="100" />
|
<el-table-column align="center" label="字典键值" prop="dictValue" width="100"/>
|
||||||
<el-table-column label="字典排序" align="center" prop="dictSort" width="80" />
|
<el-table-column align="center" label="字典排序" prop="dictSort" width="80"/>
|
||||||
<el-table-column label="备注" align="center" prop="remark" width="100" />
|
<el-table-column align="center" label="备注" prop="remark" width="100"/>
|
||||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||||
@@ -333,7 +331,7 @@
|
|||||||
<el-dialog v-model="typeDialog.visible" :title="typeDialog.title" width="500px" append-to-body>
|
<el-dialog v-model="typeDialog.visible" :title="typeDialog.title" width="500px" append-to-body>
|
||||||
<el-form ref="typeFormRef" :model="typeForm" :rules="typeRules" label-width="100px">
|
<el-form ref="typeFormRef" :model="typeForm" :rules="typeRules" label-width="100px">
|
||||||
<el-form-item label="字典名称" prop="dictName">
|
<el-form-item label="字典名称" prop="dictName">
|
||||||
<el-input v-model="typeForm.dictName" placeholder="请输入字典名称" />
|
<el-input v-model="typeForm.dictName" placeholder="请输入字典名称"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="dictType">
|
<el-form-item prop="dictType">
|
||||||
<template #label>
|
<template #label>
|
||||||
@@ -344,7 +342,7 @@
|
|||||||
字典类型
|
字典类型
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<el-input v-model="typeForm.dictType" placeholder="请输入字典类型" maxlength="100" />
|
<el-input v-model="typeForm.dictType" maxlength="100" placeholder="请输入字典类型"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="备注" prop="remark">
|
<el-form-item label="备注" prop="remark">
|
||||||
<el-input v-model="typeForm.remark" type="textarea" placeholder="请输入内容"></el-input>
|
<el-input v-model="typeForm.remark" type="textarea" placeholder="请输入内容"></el-input>
|
||||||
@@ -362,19 +360,19 @@
|
|||||||
<el-dialog v-model="dataDialog.visible" :title="dataDialog.title" width="500px" append-to-body>
|
<el-dialog v-model="dataDialog.visible" :title="dataDialog.title" width="500px" append-to-body>
|
||||||
<el-form ref="dataFormRef" :model="dataForm" :rules="dataRules" label-width="80px">
|
<el-form ref="dataFormRef" :model="dataForm" :rules="dataRules" label-width="80px">
|
||||||
<el-form-item label="字典类型">
|
<el-form-item label="字典类型">
|
||||||
<el-input v-model="dataForm.dictType" :disabled="true" />
|
<el-input v-model="dataForm.dictType" :disabled="true"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="数据标签" prop="dictLabel">
|
<el-form-item label="数据标签" prop="dictLabel">
|
||||||
<el-input v-model="dataForm.dictLabel" placeholder="请输入数据标签" />
|
<el-input v-model="dataForm.dictLabel" placeholder="请输入数据标签"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="数据键值" prop="dictValue">
|
<el-form-item label="数据键值" prop="dictValue">
|
||||||
<el-input v-model="dataForm.dictValue" placeholder="请输入数据键值" />
|
<el-input v-model="dataForm.dictValue" placeholder="请输入数据键值"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="样式属性" prop="cssClass">
|
<el-form-item label="样式属性" prop="cssClass">
|
||||||
<el-input v-model="dataForm.cssClass" placeholder="请输入样式属性" />
|
<el-input v-model="dataForm.cssClass" placeholder="请输入样式属性"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="显示排序" prop="dictSort">
|
<el-form-item label="显示排序" prop="dictSort">
|
||||||
<el-input-number v-model="dataForm.dictSort" controls-position="right" :min="0" />
|
<el-input-number v-model="dataForm.dictSort" :min="0" controls-position="right"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="回显样式" prop="listClass">
|
<el-form-item label="回显样式" prop="listClass">
|
||||||
<el-select v-model="dataForm.listClass">
|
<el-select v-model="dataForm.listClass">
|
||||||
@@ -401,14 +399,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup name="Dict" lang="ts">
|
<script setup name="Dict" lang="ts">
|
||||||
import { listData, getData, delData, addData, updateData } from '@/api/system/dict/data';
|
import {listData, getData, delData, addData, updateData} from '@/api/system/dict/data';
|
||||||
import { DictDataForm, DictDataQuery, DictDataVO } from '@/api/system/dict/data/types';
|
import {DictDataForm, DictDataQuery, DictDataVO} from '@/api/system/dict/data/types';
|
||||||
import { listType, getType, delType, addType, updateType, refreshCache } from '@/api/system/dict/type';
|
import {listType, getType, delType, addType, updateType, refreshCache} from '@/api/system/dict/type';
|
||||||
import { DictTypeForm, DictTypeQuery, DictTypeVO } from '@/api/system/dict/type/types';
|
import {DictTypeForm, DictTypeQuery, DictTypeVO} from '@/api/system/dict/type/types';
|
||||||
import modal from '@/plugins/modal';
|
import modal from '@/plugins/modal';
|
||||||
import { useDictStore } from '@/store/modules/dict';
|
import {useDictStore} from '@/store/modules/dict';
|
||||||
import { download as requestDownload } from '@/utils/request';
|
import {download as requestDownload} from '@/utils/request';
|
||||||
import { parseTime } from '@/utils/ruoyi';
|
import {parseTime} from '@/utils/ruoyi';
|
||||||
|
|
||||||
const typeList = ref<DictTypeVO[]>([]);
|
const typeList = ref<DictTypeVO[]>([]);
|
||||||
const typeLoading = ref(true);
|
const typeLoading = ref(true);
|
||||||
@@ -435,7 +433,7 @@ const typeInitFormData: DictTypeForm = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const typeState = reactive<PageData<DictTypeForm, DictTypeQuery>>({
|
const typeState = reactive<PageData<DictTypeForm, DictTypeQuery>>({
|
||||||
form: { ...typeInitFormData },
|
form: {...typeInitFormData},
|
||||||
queryParams: {
|
queryParams: {
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@@ -443,12 +441,12 @@ const typeState = reactive<PageData<DictTypeForm, DictTypeQuery>>({
|
|||||||
dictType: ''
|
dictType: ''
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
dictName: [{ required: true, message: '字典名称不能为空', trigger: 'blur' }],
|
dictName: [{required: true, message: '字典名称不能为空', trigger: 'blur'}],
|
||||||
dictType: [{ required: true, message: '字典类型不能为空', trigger: 'blur' }]
|
dictType: [{required: true, message: '字典类型不能为空', trigger: 'blur'}]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { queryParams: typeQueryParams, form: typeForm, rules: typeRules } = toRefs(typeState);
|
const {queryParams: typeQueryParams, form: typeForm, rules: typeRules} = toRefs(typeState);
|
||||||
|
|
||||||
const currentDict = ref<DictTypeVO | null>(null);
|
const currentDict = ref<DictTypeVO | null>(null);
|
||||||
const hasCurrentDict = computed(() => !!currentDict.value);
|
const hasCurrentDict = computed(() => !!currentDict.value);
|
||||||
@@ -474,12 +472,12 @@ const dataDialog = reactive<DialogOption>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const listClassOptions = ref<Array<{ value: string; label: string }>>([
|
const listClassOptions = ref<Array<{ value: string; label: string }>>([
|
||||||
{ value: 'default', label: '默认' },
|
{value: 'default', label: '默认'},
|
||||||
{ value: 'primary', label: '主要' },
|
{value: 'primary', label: '主要'},
|
||||||
{ value: 'success', label: '成功' },
|
{value: 'success', label: '成功'},
|
||||||
{ value: 'info', label: '信息' },
|
{value: 'info', label: '信息'},
|
||||||
{ value: 'warning', label: '警告' },
|
{value: 'warning', label: '警告'},
|
||||||
{ value: 'danger', label: '危险' }
|
{value: 'danger', label: '危险'}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const dataInitFormData: DictDataForm = {
|
const dataInitFormData: DictDataForm = {
|
||||||
@@ -493,7 +491,7 @@ const dataInitFormData: DictDataForm = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const dataState = reactive<PageData<DictDataForm, DictDataQuery>>({
|
const dataState = reactive<PageData<DictDataForm, DictDataQuery>>({
|
||||||
form: { ...dataInitFormData },
|
form: {...dataInitFormData},
|
||||||
queryParams: {
|
queryParams: {
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@@ -502,13 +500,13 @@ const dataState = reactive<PageData<DictDataForm, DictDataQuery>>({
|
|||||||
dictLabel: ''
|
dictLabel: ''
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
dictLabel: [{ required: true, message: '数据标签不能为空', trigger: 'blur' }],
|
dictLabel: [{required: true, message: '数据标签不能为空', trigger: 'blur'}],
|
||||||
dictValue: [{ required: true, message: '数据键值不能为空', trigger: 'blur' }],
|
dictValue: [{required: true, message: '数据键值不能为空', trigger: 'blur'}],
|
||||||
dictSort: [{ required: true, message: '数据顺序不能为空', trigger: 'blur' }]
|
dictSort: [{required: true, message: '数据顺序不能为空', trigger: 'blur'}]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { queryParams: dataQueryParams, form: dataForm, rules: dataRules } = toRefs(dataState);
|
const {queryParams: dataQueryParams, form: dataForm, rules: dataRules} = toRefs(dataState);
|
||||||
|
|
||||||
const getTypeList = () => {
|
const getTypeList = () => {
|
||||||
typeLoading.value = true;
|
typeLoading.value = true;
|
||||||
@@ -553,7 +551,7 @@ const cancelType = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resetTypeForm = () => {
|
const resetTypeForm = () => {
|
||||||
typeForm.value = { ...typeInitFormData };
|
typeForm.value = {...typeInitFormData};
|
||||||
typeFormRef.value?.resetFields();
|
typeFormRef.value?.resetFields();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -643,7 +641,7 @@ const cancelData = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resetDataForm = () => {
|
const resetDataForm = () => {
|
||||||
dataForm.value = { ...dataInitFormData };
|
dataForm.value = {...dataInitFormData};
|
||||||
dataFormRef.value?.resetFields();
|
dataFormRef.value?.resetFields();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -734,30 +732,30 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.dict-page {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.dict-grid {
|
.dict-grid {
|
||||||
row-gap: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dict-card {
|
.dict-card {
|
||||||
height: 100%;
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column; // ✅ 关键:让card内部上下分区自动分配高度
|
||||||
|
}
|
||||||
|
|
||||||
|
// el-card 默认内部是流式,加上这个,card内部分区自动挤压表格区域
|
||||||
|
.dict-card :deep(.el-card__body) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dict-card__header {
|
.dict-card__header {
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
flex-shrink: 0; // 头部搜索栏固定高度,不压缩
|
||||||
|
|
||||||
.dict-title-toggle {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dict-title-toggle.is-collapsed::after {
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dict-card__subtitle {
|
|
||||||
margin: 4px 0 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dict-search {
|
.dict-search {
|
||||||
@@ -765,10 +763,10 @@ onMounted(() => {
|
|||||||
max-height: 240px;
|
max-height: 240px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
transition:
|
transition: max-height 0.32s cubic-bezier(0.22, 1, 0.36, 1),
|
||||||
max-height 0.32s cubic-bezier(0.22, 1, 0.36, 1),
|
opacity 0.24s ease,
|
||||||
opacity 0.24s ease,
|
margin-bottom 0.24s ease;
|
||||||
margin-bottom 0.24s ease;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dict-search.is-collapsed {
|
.dict-search.is-collapsed {
|
||||||
@@ -785,6 +783,7 @@ onMounted(() => {
|
|||||||
.dict-actions {
|
.dict-actions {
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
flex-shrink: 0; // 按钮栏固定高度,不压缩
|
||||||
}
|
}
|
||||||
|
|
||||||
.dict-actions :deep(.el-button) {
|
.dict-actions :deep(.el-button) {
|
||||||
@@ -799,5 +798,11 @@ onMounted(() => {
|
|||||||
|
|
||||||
.dict-table-wrap {
|
.dict-table-wrap {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
max-height: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格设置最大高度:自动占满剩余空间,不要写死500px
|
||||||
|
.dict-table-wrap :deep(.el-table) {
|
||||||
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
258
src/views/system/user/add.vue
Normal file
258
src/views/system/user/add.vue
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import {ArrowLeft, InfoFilled} from "@element-plus/icons-vue";
|
||||||
|
import FormItemTip from '@/components/FormItemTip/index.vue'
|
||||||
|
|
||||||
|
const roleslist = [
|
||||||
|
{
|
||||||
|
label: '超级管理员',
|
||||||
|
sutTitle: "拥有系统所有功能权限"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '设备管理员',
|
||||||
|
sutTitle: "管理设备列表、OTA升级等"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '普通用户',
|
||||||
|
sutTitle: "仅可查看设备信息"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '审计员',
|
||||||
|
sutTitle: "查看操作日志与审计"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="Add">
|
||||||
|
<div class="AddNav">
|
||||||
|
<div class="addItem">
|
||||||
|
<div class="arrowBox">
|
||||||
|
<el-icon>
|
||||||
|
<arrow-left></arrow-left>
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="title">新增账号</div>
|
||||||
|
</div>
|
||||||
|
<div class="describe">创建新的管理后台用户账号并配置角色权限</div>
|
||||||
|
</div>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="17">
|
||||||
|
<el-form label-position="top">
|
||||||
|
<div class="commonCard">
|
||||||
|
<card-nav :show-line="false" title="基本信息"></card-nav>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<FormItemTip
|
||||||
|
:inputAttrs="{placeholder:'请输入登录用户名'}"
|
||||||
|
:required="true"
|
||||||
|
label="登录用户名"
|
||||||
|
tip="4-20位字母、数字或下划线,创建后不可修改"
|
||||||
|
type="input"
|
||||||
|
></FormItemTip>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<FormItemTip
|
||||||
|
:inputAttrs="{placeholder:'请输入真实姓名'}"
|
||||||
|
:required="true"
|
||||||
|
label="用户姓名"
|
||||||
|
type="input"
|
||||||
|
></FormItemTip>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<FormItemTip
|
||||||
|
:inputAttrs="{placeholder:'请输入手机号码'}"
|
||||||
|
:required="true"
|
||||||
|
label="手机号码"
|
||||||
|
type="input"
|
||||||
|
></FormItemTip>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<FormItemTip
|
||||||
|
:inputAttrs="{placeholder:'请输入邮箱'}"
|
||||||
|
:required="false"
|
||||||
|
label="邮箱"
|
||||||
|
type="input"
|
||||||
|
></FormItemTip>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<FormItemTip
|
||||||
|
:radio-options="[
|
||||||
|
{label:'男',value:1},
|
||||||
|
{label:'女',value:0},
|
||||||
|
]"
|
||||||
|
:required="false"
|
||||||
|
label="性别"
|
||||||
|
type="radio"
|
||||||
|
></FormItemTip>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
<div class="commonCard">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<FormItemTip
|
||||||
|
:required="true"
|
||||||
|
label="角色"
|
||||||
|
type="select"
|
||||||
|
></FormItemTip>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<FormItemTip
|
||||||
|
:input-attrs="{maxlength:20,minlength:8}"
|
||||||
|
:required="true"
|
||||||
|
label="初始密码"
|
||||||
|
tip="8-20位,包含字母和数字"
|
||||||
|
type="input"
|
||||||
|
></FormItemTip>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<FormItemTip
|
||||||
|
:input-attrs="{placeholder:'请再次输入密码',maxlength:20,minlength:8}"
|
||||||
|
:required="true"
|
||||||
|
label="重复初始密码"
|
||||||
|
type="input"
|
||||||
|
></FormItemTip>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<FormItemTip
|
||||||
|
:radio-options="[
|
||||||
|
{label:'启用',value:1},
|
||||||
|
{label:'禁用',value:0},
|
||||||
|
]"
|
||||||
|
:required="false"
|
||||||
|
label="性别"
|
||||||
|
type="radio"
|
||||||
|
></FormItemTip>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="7">
|
||||||
|
<div class="commonCard roleInfoBox">
|
||||||
|
<card-nav :show-line="false" title="角色说明"></card-nav>
|
||||||
|
<ul class="roleInfoListBox">
|
||||||
|
<li v-for="(item,index) in roleslist" :key="index" class="roleInfoListItem">
|
||||||
|
<div class="item">
|
||||||
|
<i class="circle"></i>
|
||||||
|
<span class="title">{{ item.label }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="subTitle">{{ item.sutTitle }}</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="commonCard tipBox">
|
||||||
|
<div>
|
||||||
|
<el-icon>
|
||||||
|
<InfoFilled/>
|
||||||
|
</el-icon>
|
||||||
|
<span>操作提示</span>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<li>1. 带 * 号为必填项</li>
|
||||||
|
<li>2. 用户名创建后不可修改</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<el-button>确认创建</el-button>
|
||||||
|
<el-button>取消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.Add {
|
||||||
|
padding: 24px;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
.AddNav {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.addItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.arrowBox {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 4px 4px 4px 4px;
|
||||||
|
border: 1px solid rgba(209, 213, 219, 1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
height: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: rgba(30, 41, 59, 1);
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.describe {
|
||||||
|
margin-top: 8px;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: rgba(100, 116, 139, 1);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.roleInfoBox {
|
||||||
|
.roleInfoListBox {
|
||||||
|
min-height: 219px;
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.roleInfoListItem {
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
display: block;
|
||||||
|
width: 9px;
|
||||||
|
height: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: rgba(37, 99, 235, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
line-height: 16px;
|
||||||
|
color: rgba(30, 41, 59, 1);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subTitle {
|
||||||
|
line-height: 13px;
|
||||||
|
color: rgba(100, 116, 139, 1);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tipBox {
|
||||||
|
margin-top: 20px;
|
||||||
|
border-radius: 8px 8px 8px 8px;
|
||||||
|
background-color: rgba(239, 246, 255, 1);
|
||||||
|
border: 1px solid rgba(191, 219, 254, 1);
|
||||||
|
padding: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,60 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-2 system-user-page">
|
<div class="p-2 system-user-page">
|
||||||
|
<div style="margin-top:24px">
|
||||||
|
<pagecrumb>
|
||||||
|
<template #rightSlot>
|
||||||
|
<el-button size="large" type="primary">新增账号</el-button>
|
||||||
|
</template>
|
||||||
|
</pagecrumb>
|
||||||
|
</div>
|
||||||
<el-row :gutter="20" class="content-grid">
|
<el-row :gutter="20" class="content-grid">
|
||||||
<!-- 部门树 -->
|
<!-- 部门树 -->
|
||||||
<tree-panel
|
<!-- <tree-panel-->
|
||||||
ref="treePanelRef"
|
<!-- ref="treePanelRef"-->
|
||||||
v-model:collapsed="treeCollapsed"
|
<!-- v-model:collapsed="treeCollapsed"-->
|
||||||
title="部门结构"
|
<!-- title="部门结构"-->
|
||||||
placeholder="请输入部门名称"
|
<!-- placeholder="请输入部门名称"-->
|
||||||
:data="deptOptions"
|
<!-- :data="deptOptions"-->
|
||||||
:expanded-span="5"
|
<!-- :expanded-span="5"-->
|
||||||
@node-click="handleNodeClick"
|
<!-- @node-click="handleNodeClick"-->
|
||||||
/>
|
<!-- />-->
|
||||||
<el-col
|
<el-col
|
||||||
:lg="treeCollapsed ? 23 : 19"
|
:lg="treeCollapsed ? 24 : 24"
|
||||||
:xs="24"
|
:xs="24"
|
||||||
class="tree-content-col content-main"
|
class="tree-content-col content-main"
|
||||||
:class="{ 'is-tree-collapsed': treeCollapsed }"
|
:class="{ 'is-tree-collapsed': treeCollapsed }"
|
||||||
>
|
>
|
||||||
<div class="search-wrap">
|
<div class="search-wrap">
|
||||||
<el-card shadow="hover" class="search-panel" :class="{ 'is-collapsed': !showSearch }">
|
<el-card shadow="hover" class="search-panel" :class="{ 'is-collapsed': !showSearch }">
|
||||||
<template #header>
|
<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="query-form" label-position="top">
|
||||||
<div class="panel-heading search-panel-toggle" @click.stop="showSearch = !showSearch">
|
|
||||||
<div>
|
|
||||||
<span class="panel-kicker">Search Filters</span>
|
|
||||||
<h3>筛选条件</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true" class="query-form">
|
|
||||||
<el-form-item label="用户名称" prop="userName">
|
<el-form-item label="用户名称" prop="userName">
|
||||||
<el-input
|
<el-input
|
||||||
|
size="large"
|
||||||
v-model="queryParams.userName"
|
v-model="queryParams.userName"
|
||||||
placeholder="请输入用户名称"
|
placeholder="请输入用户名称"
|
||||||
clearable
|
clearable
|
||||||
@keyup.enter="handleQuery"
|
@keyup.enter="handleQuery"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="用户昵称" prop="nickName">
|
<el-form-item label="角色" prop="status">
|
||||||
<el-input
|
<el-select v-model="queryParams.status" clearable placeholder="用户角色" size="large">
|
||||||
v-model="queryParams.nickName"
|
|
||||||
placeholder="请输入用户昵称"
|
|
||||||
clearable
|
|
||||||
@keyup.enter="handleQuery"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="手机号码" prop="phoneNumber">
|
|
||||||
<el-input
|
|
||||||
v-model="queryParams.phoneNumber"
|
|
||||||
placeholder="请输入手机号码"
|
|
||||||
clearable
|
|
||||||
@keyup.enter="handleQuery"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="状态" prop="status">
|
|
||||||
<el-select v-model="queryParams.status" placeholder="用户状态" clearable>
|
|
||||||
<el-option
|
<el-option
|
||||||
v-for="dict in sys_normal_disable"
|
v-for="dict in sys_normal_disable"
|
||||||
:key="dict.value"
|
:key="dict.value"
|
||||||
@@ -63,20 +46,19 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="创建时间" style="width: 308px">
|
<el-form-item label="状态" prop="status">
|
||||||
<el-date-picker
|
<el-select v-model="queryParams.status" clearable placeholder="用户状态" size="large">
|
||||||
v-model="dateRange"
|
<el-option
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
v-for="dict in sys_normal_disable"
|
||||||
type="daterange"
|
:key="dict.value"
|
||||||
range-separator="-"
|
:label="dict.label"
|
||||||
start-placeholder="开始日期"
|
:value="dict.value"
|
||||||
end-placeholder="结束日期"
|
/>
|
||||||
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
</el-select>
|
||||||
></el-date-picker>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
<el-button size="large" type="primary" @click="handleQuery">搜索</el-button>
|
||||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
<el-button size="large" @click="resetQuery">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -88,60 +70,11 @@
|
|||||||
<div class="table-heading">
|
<div class="table-heading">
|
||||||
<span class="panel-kicker">User Dataset</span>
|
<span class="panel-kicker">User Dataset</span>
|
||||||
<h3>用户列表</h3>
|
<h3>用户列表</h3>
|
||||||
<p>共 {{ total }} 条记录,支持部门筛选、状态切换、导入导出和角色分配。</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<el-button v-has-permi="['system:user:add']" type="primary" plain icon="Plus" @click="handleAdd()">
|
<el-button v-has-permi="['system:user:add']" type="primary" plain icon="Plus" @click="handleAdd()">
|
||||||
新增
|
新增
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
|
||||||
v-has-permi="['system:user:edit']"
|
|
||||||
type="success"
|
|
||||||
plain
|
|
||||||
:disabled="single"
|
|
||||||
icon="Edit"
|
|
||||||
@click="handleUpdate()"
|
|
||||||
>
|
|
||||||
修改
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
v-has-permi="['system:user:remove']"
|
|
||||||
type="danger"
|
|
||||||
plain
|
|
||||||
:disabled="multiple"
|
|
||||||
icon="Delete"
|
|
||||||
@click="handleDelete()"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
v-hasPermi="['system:user:edit']"
|
|
||||||
type="warning"
|
|
||||||
plain
|
|
||||||
icon="Unlock"
|
|
||||||
:disabled="single"
|
|
||||||
@click="handleUnlock()"
|
|
||||||
>
|
|
||||||
解锁
|
|
||||||
</el-button>
|
|
||||||
<el-dropdown class="mt-[1px]">
|
|
||||||
<el-button plain type="info">
|
|
||||||
更多
|
|
||||||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
<template #dropdown>
|
|
||||||
<el-dropdown-menu>
|
|
||||||
<el-dropdown-item icon="Download" @click="importTemplate">下载模板</el-dropdown-item>
|
|
||||||
<!-- 注意 由于el-dropdown-item标签是延迟加载的 所以v-has-permi自定义标签不生效 需要使用v-if调用方法执行 -->
|
|
||||||
<el-dropdown-item v-if="checkPermi(['system:user:import'])" icon="Top" @click="handleImport">
|
|
||||||
导入数据
|
|
||||||
</el-dropdown-item>
|
|
||||||
<el-dropdown-item v-if="checkPermi(['system:user:export'])" icon="Download" @click="handleExport">
|
|
||||||
导出数据
|
|
||||||
</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</template>
|
|
||||||
</el-dropdown>
|
|
||||||
<right-toolbar
|
<right-toolbar
|
||||||
v-model:show-search="showSearch"
|
v-model:show-search="showSearch"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
@@ -160,12 +93,11 @@
|
|||||||
:data="userList"
|
:data="userList"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="50" align="center" />
|
<el-table-column align="center" type="selection" width="50"/>
|
||||||
<el-table-column v-if="columns[0].visible" key="userId" label="用户编号" align="center" prop="userId" />
|
<el-table-column align="center" label="用户名" prop="userId"/>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
v-if="columns[1].visible"
|
|
||||||
key="userName"
|
key="userName"
|
||||||
label="用户名称"
|
label="姓名"
|
||||||
align="center"
|
align="center"
|
||||||
:show-overflow-tooltip="true"
|
:show-overflow-tooltip="true"
|
||||||
>
|
>
|
||||||
@@ -176,86 +108,80 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
v-if="columns[2].visible"
|
|
||||||
key="nickName"
|
key="nickName"
|
||||||
label="用户昵称"
|
label="角色"
|
||||||
align="center"
|
align="center"
|
||||||
prop="nickName"
|
prop="nickName"
|
||||||
:show-overflow-tooltip="true"
|
:show-overflow-tooltip="true"
|
||||||
/>
|
></el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
v-if="columns[3].visible"
|
|
||||||
key="deptName"
|
|
||||||
label="部门"
|
|
||||||
align="center"
|
|
||||||
prop="deptName"
|
|
||||||
:show-overflow-tooltip="true"
|
|
||||||
/>
|
|
||||||
<el-table-column
|
|
||||||
v-if="columns[4].visible"
|
|
||||||
key="phoneNumber"
|
key="phoneNumber"
|
||||||
label="手机号码"
|
label="手机号码"
|
||||||
align="center"
|
align="center"
|
||||||
prop="phoneNumber"
|
prop="phoneNumber"
|
||||||
width="120"
|
width="120"
|
||||||
/>
|
>
|
||||||
<el-table-column v-if="columns[5].visible" key="status" label="状态" align="center">
|
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-switch
|
{{ hiddenPhone(scope.row.phoneNumber) }}
|
||||||
v-model="scope.row.status"
|
</template>
|
||||||
active-value="0"
|
</el-table-column>
|
||||||
inactive-value="1"
|
<el-table-column align="center" label="账号状态" width="100">
|
||||||
@change="handleStatusChange(scope.row)"
|
<template #default="scope">
|
||||||
></el-switch>
|
<div class="flex justify-center">
|
||||||
|
<div class="open tag"><i class="circle"></i><span>启用</span></div>
|
||||||
|
<!-- <div class="close tag"><i class="circle"></i><span>已禁用</span></div>-->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="最后登录" prop="createTime" width="160">
|
||||||
|
<template #default="scope">
|
||||||
|
<span style="color:#64748B;">{{ scope.row.createTime }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="创建时间" prop="createTime" width="160">
|
||||||
|
<template #default="scope">
|
||||||
|
<span style="color:#64748B;">{{ scope.row.createTime }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column v-if="columns[6].visible" label="创建时间" align="center" prop="createTime" width="160">
|
<el-table-column align="center" class-name="small-padding fixed-width" fixed="right" label="操作"
|
||||||
|
width="300">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span>{{ scope.row.createTime }}</span>
|
<el-button
|
||||||
</template>
|
v-hasPermi="['system:user:edit']"
|
||||||
</el-table-column>
|
link
|
||||||
|
type="primary"
|
||||||
<el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
|
@click="handleUpdate(scope.row)"
|
||||||
<template #default="scope">
|
>编辑
|
||||||
<el-tooltip v-if="scope.row.userId !== '1761100000000000001'" content="修改" placement="top">
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPermi="['system:user:edit']"
|
v-hasPermi="['system:user:resetPwd']"
|
||||||
link
|
link
|
||||||
type="primary"
|
type="primary"
|
||||||
icon="Edit"
|
@click="handleResetPwd(scope.row)"
|
||||||
@click="handleUpdate(scope.row)"
|
>重置密码
|
||||||
></el-button>
|
</el-button>
|
||||||
</el-tooltip>
|
<el-button
|
||||||
<el-tooltip v-if="scope.row.userId !== '1761100000000000001'" content="删除" placement="top">
|
v-hasPermi="['system:user:edit']"
|
||||||
<el-button
|
link
|
||||||
v-hasPermi="['system:user:remove']"
|
type="danger"
|
||||||
link
|
@click="handleAuthRole(scope.row)"
|
||||||
type="primary"
|
>禁用
|
||||||
icon="Delete"
|
</el-button>
|
||||||
@click="handleDelete(scope.row)"
|
<el-button
|
||||||
></el-button>
|
v-hasPermi="['system:user:edit']"
|
||||||
</el-tooltip>
|
link
|
||||||
|
type="success"
|
||||||
<el-tooltip v-if="scope.row.userId !== '1761100000000000001'" content="重置密码" placement="top">
|
@click="handleAuthRole(scope.row)"
|
||||||
<el-button
|
>启用
|
||||||
v-hasPermi="['system:user:resetPwd']"
|
</el-button>
|
||||||
link
|
<el-button
|
||||||
type="primary"
|
v-hasPermi="['system:user:remove']"
|
||||||
icon="Key"
|
link
|
||||||
@click="handleResetPwd(scope.row)"
|
type="danger"
|
||||||
></el-button>
|
@click="handleDelete(scope.row)"
|
||||||
</el-tooltip>
|
>删除
|
||||||
|
</el-button>
|
||||||
<el-tooltip v-if="scope.row.userId !== '1761100000000000001'" content="分配角色" placement="top">
|
|
||||||
<el-button
|
|
||||||
v-hasPermi="['system:user:edit']"
|
|
||||||
link
|
|
||||||
type="primary"
|
|
||||||
icon="CircleCheck"
|
|
||||||
@click="handleAuthRole(scope.row)"
|
|
||||||
></el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -284,7 +210,7 @@
|
|||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="用户昵称" prop="nickName">
|
<el-form-item label="用户昵称" prop="nickName">
|
||||||
<el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="30" />
|
<el-input v-model="form.nickName" maxlength="30" placeholder="请输入用户昵称"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId">
|
<el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId">
|
||||||
@@ -304,19 +230,19 @@
|
|||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="手机号码" prop="phoneNumber">
|
<el-form-item label="手机号码" prop="phoneNumber">
|
||||||
<el-input v-model="form.phoneNumber" placeholder="请输入手机号码" maxlength="11" />
|
<el-input v-model="form.phoneNumber" maxlength="11" placeholder="请输入手机号码"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="邮箱" prop="email">
|
<el-form-item label="邮箱" prop="email">
|
||||||
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
|
<el-input v-model="form.email" maxlength="50" placeholder="请输入邮箱"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item v-if="form.userId == undefined" label="用户名称" prop="userName">
|
<el-form-item v-if="form.userId == undefined" label="用户名称" prop="userName">
|
||||||
<el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" />
|
<el-input v-model="form.userName" maxlength="30" placeholder="请输入用户名称"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
@@ -413,7 +339,7 @@
|
|||||||
drag
|
drag
|
||||||
>
|
>
|
||||||
<el-icon class="el-icon--upload">
|
<el-icon class="el-icon--upload">
|
||||||
<UploadFilled />
|
<UploadFilled/>
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<div class="el-upload__text">
|
<div class="el-upload__text">
|
||||||
将文件拖到此处,或
|
将文件拖到此处,或
|
||||||
@@ -422,7 +348,7 @@
|
|||||||
<template #tip>
|
<template #tip>
|
||||||
<div class="text-center el-upload__tip">
|
<div class="text-center el-upload__tip">
|
||||||
<div class="el-upload__tip">
|
<div class="el-upload__tip">
|
||||||
<el-checkbox v-model="upload.updateSupport" />
|
<el-checkbox v-model="upload.updateSupport"/>
|
||||||
是否更新已经存在的用户数据
|
是否更新已经存在的用户数据
|
||||||
</div>
|
</div>
|
||||||
<span>仅允许导入xls、xlsx格式文件。</span>
|
<span>仅允许导入xls、xlsx格式文件。</span>
|
||||||
@@ -446,44 +372,45 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 用户详情抽屉 -->
|
<!-- 用户详情抽屉 -->
|
||||||
<user-view-drawer ref="userViewRef" />
|
<user-view-drawer ref="userViewRef"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup name="User" lang="ts">
|
<script setup name="User" lang="ts">
|
||||||
import { to } from 'await-to-js';
|
import {to} from 'await-to-js';
|
||||||
import { useRouter } from 'vue-router';
|
import {useRouter} from 'vue-router';
|
||||||
import { getConfigKey } from '@/api/system/config';
|
import {getConfigKey} from '@/api/system/config';
|
||||||
import { DeptTreeVO, DeptVO } from '@/api/system/dept/types';
|
import {DeptTreeVO, DeptVO} from '@/api/system/dept/types';
|
||||||
import { optionselect } from '@/api/system/post';
|
import {optionselect} from '@/api/system/post';
|
||||||
import { PostVO } from '@/api/system/post/types';
|
import {PostVO} from '@/api/system/post/types';
|
||||||
import { RoleVO } from '@/api/system/role/types';
|
import {RoleVO} from '@/api/system/role/types';
|
||||||
import api from '@/api/system/user';
|
import api from '@/api/system/user';
|
||||||
import { UserForm, UserQuery, UserVO } from '@/api/system/user/types';
|
import {UserForm, UserQuery, UserVO} from '@/api/system/user/types';
|
||||||
import TreePanel from '@/components/TreePanel/index.vue';
|
import TreePanel from '@/components/TreePanel/index.vue';
|
||||||
import { useLoading } from '@/hooks/async/useLoading';
|
import {useLoading} from '@/hooks/async/useLoading';
|
||||||
import { useDialogState } from '@/hooks/dialog/useDialogState';
|
import {useDialogState} from '@/hooks/dialog/useDialogState';
|
||||||
import { useDateRangeQuery } from '@/hooks/form/useDateRangeQuery';
|
import {useDateRangeQuery} from '@/hooks/form/useDateRangeQuery';
|
||||||
import { useSearchReset } from '@/hooks/form/useSearchReset';
|
import {useSearchReset} from '@/hooks/form/useSearchReset';
|
||||||
import { useSearchToggle } from '@/hooks/form/useSearchToggle';
|
import {useSearchToggle} from '@/hooks/form/useSearchToggle';
|
||||||
import { useTableSelection } from '@/hooks/table/useTableSelection';
|
import {useTableSelection} from '@/hooks/table/useTableSelection';
|
||||||
import { useTreeCollapsed } from '@/hooks/tree/useTreeCollapsed';
|
import {useTreeCollapsed} from '@/hooks/tree/useTreeCollapsed';
|
||||||
import modal from '@/plugins/modal';
|
import modal from '@/plugins/modal';
|
||||||
import { useUserStore } from '@/store/modules/user';
|
import {useUserStore} from '@/store/modules/user';
|
||||||
import { useDict } from '@/utils/dict';
|
import {useDict} from '@/utils/dict';
|
||||||
import { checkPermi } from '@/utils/permission';
|
import {checkPermi} from '@/utils/permission';
|
||||||
import { globalHeaders } from '@/utils/request';
|
import {globalHeaders} from '@/utils/request';
|
||||||
import { download as requestDownload } from '@/utils/request';
|
import {download as requestDownload} from '@/utils/request';
|
||||||
import UserViewDrawer from './view.vue';
|
import UserViewDrawer from './view.vue';
|
||||||
|
import {hiddenPhone} from "@/utils/phone";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { sys_normal_disable, sys_user_gender } = toRefs<any>(useDict('sys_normal_disable', 'sys_user_gender'));
|
const {sys_normal_disable, sys_user_gender} = toRefs<any>(useDict('sys_normal_disable', 'sys_user_gender'));
|
||||||
const userList = ref<UserVO[]>();
|
const userList = ref<UserVO[]>();
|
||||||
const { loading, withLoading } = useLoading(true);
|
const {loading, withLoading} = useLoading(true);
|
||||||
const { showSearch } = useSearchToggle();
|
const {showSearch} = useSearchToggle();
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const { dateRange, applyDateRange, resetDateRange } = useDateRangeQuery();
|
const {dateRange, applyDateRange, resetDateRange} = useDateRangeQuery();
|
||||||
const { treeCollapsed } = useTreeCollapsed();
|
const {treeCollapsed} = useTreeCollapsed();
|
||||||
const deptOptions = ref<DeptTreeVO[]>([]);
|
const deptOptions = ref<DeptTreeVO[]>([]);
|
||||||
const enabledDeptOptions = ref<DeptTreeVO[]>([]);
|
const enabledDeptOptions = ref<DeptTreeVO[]>([]);
|
||||||
const initPassword = ref<string>('');
|
const initPassword = ref<string>('');
|
||||||
@@ -506,13 +433,13 @@ const upload = reactive<ImportOption>({
|
|||||||
});
|
});
|
||||||
// 列显隐信息
|
// 列显隐信息
|
||||||
const columns = ref<FieldOption[]>([
|
const columns = ref<FieldOption[]>([
|
||||||
{ key: 0, label: `用户编号`, visible: false, children: [] },
|
{key: 0, label: `用户编号`, visible: false, children: []},
|
||||||
{ key: 1, label: `用户名称`, visible: true, children: [] },
|
{key: 1, label: `用户名称`, visible: true, children: []},
|
||||||
{ key: 2, label: `用户昵称`, visible: true, children: [] },
|
{key: 2, label: `用户昵称`, visible: true, children: []},
|
||||||
{ key: 3, label: `部门`, visible: true, children: [] },
|
{key: 3, label: `部门`, visible: true, children: []},
|
||||||
{ key: 4, label: `手机号码`, visible: true, children: [] },
|
{key: 4, label: `手机号码`, visible: true, children: []},
|
||||||
{ key: 5, label: `状态`, visible: true, children: [] },
|
{key: 5, label: `状态`, visible: true, children: []},
|
||||||
{ key: 6, label: `创建时间`, visible: true, children: [] }
|
{key: 6, label: `创建时间`, visible: true, children: []}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const treePanelRef = ref<InstanceType<typeof TreePanel>>();
|
const treePanelRef = ref<InstanceType<typeof TreePanel>>();
|
||||||
@@ -538,7 +465,7 @@ const initFormData: UserForm = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initData: PageData<UserForm, UserQuery> = {
|
const initData: PageData<UserForm, UserQuery> = {
|
||||||
form: { ...initFormData },
|
form: {...initFormData},
|
||||||
queryParams: {
|
queryParams: {
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@@ -550,7 +477,7 @@ const initData: PageData<UserForm, UserQuery> = {
|
|||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
userName: [
|
userName: [
|
||||||
{ required: true, message: '用户名称不能为空', trigger: 'blur' },
|
{required: true, message: '用户名称不能为空', trigger: 'blur'},
|
||||||
{
|
{
|
||||||
min: 2,
|
min: 2,
|
||||||
max: 20,
|
max: 20,
|
||||||
@@ -558,9 +485,9 @@ const initData: PageData<UserForm, UserQuery> = {
|
|||||||
trigger: 'blur'
|
trigger: 'blur'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
nickName: [{ required: true, message: '用户昵称不能为空', trigger: 'blur' }],
|
nickName: [{required: true, message: '用户昵称不能为空', trigger: 'blur'}],
|
||||||
password: [
|
password: [
|
||||||
{ required: true, message: '用户密码不能为空', trigger: 'blur' },
|
{required: true, message: '用户密码不能为空', trigger: 'blur'},
|
||||||
{
|
{
|
||||||
min: 5,
|
min: 5,
|
||||||
max: 20,
|
max: 20,
|
||||||
@@ -587,14 +514,14 @@ const initData: PageData<UserForm, UserQuery> = {
|
|||||||
trigger: 'blur'
|
trigger: 'blur'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
roleIds: [{ required: true, message: '用户角色不能为空', trigger: 'blur' }]
|
roleIds: [{required: true, message: '用户角色不能为空', trigger: 'blur'}]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const data = reactive<PageData<UserForm, UserQuery>>(initData);
|
const data = reactive<PageData<UserForm, UserQuery>>(initData);
|
||||||
|
|
||||||
const { queryParams, form, rules } = toRefs<PageData<UserForm, UserQuery>>(data);
|
const {queryParams, form, rules} = toRefs<PageData<UserForm, UserQuery>>(data);
|
||||||
const { ids, single, multiple, handleSelectionChange } = useTableSelection<UserVO>(item => item.userId);
|
const {ids, single, multiple, handleSelectionChange} = useTableSelection<UserVO>(item => item.userId);
|
||||||
const { dialog, openDialog: openUserDialog, closeDialog: closeUserDialog, setTitle: setDialogTitle } = useDialogState();
|
const {dialog, openDialog: openUserDialog, closeDialog: closeUserDialog, setTitle: setDialogTitle} = useDialogState();
|
||||||
|
|
||||||
/** 查询用户列表 */
|
/** 查询用户列表 */
|
||||||
const getList = async () => {
|
const getList = async () => {
|
||||||
@@ -637,7 +564,7 @@ const handleQuery = () => {
|
|||||||
queryParams.value.pageNum = 1;
|
queryParams.value.pageNum = 1;
|
||||||
getList();
|
getList();
|
||||||
};
|
};
|
||||||
const { resetQuery } = useSearchReset({
|
const {resetQuery} = useSearchReset({
|
||||||
queryFormRef,
|
queryFormRef,
|
||||||
queryParams,
|
queryParams,
|
||||||
pageNumKey: 'pageNum',
|
pageNumKey: 'pageNum',
|
||||||
@@ -654,7 +581,7 @@ const { resetQuery } = useSearchReset({
|
|||||||
/** 删除按钮操作 */
|
/** 删除按钮操作 */
|
||||||
const handleDelete = async (row?: Partial<UserVO>) => {
|
const handleDelete = async (row?: Partial<UserVO>) => {
|
||||||
const userIds = row?.userId || ids.value;
|
const userIds = row?.userId || ids.value;
|
||||||
const [err] = await to(modal.confirm('是否确认删除用户编号为"' + userIds + '"的数据项?') as any);
|
const [err] = await to(modal.confirm('是否确认删除该用户?') as any);
|
||||||
if (!err) {
|
if (!err) {
|
||||||
await api.delUser(userIds);
|
await api.delUser(userIds);
|
||||||
await getList();
|
await getList();
|
||||||
@@ -766,7 +693,7 @@ function submitFileForm() {
|
|||||||
|
|
||||||
/** 重置操作表单 */
|
/** 重置操作表单 */
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
form.value = { ...initFormData };
|
form.value = {...initFormData};
|
||||||
userFormRef.value?.resetFields();
|
userFormRef.value?.resetFields();
|
||||||
};
|
};
|
||||||
/** 取消按钮 */
|
/** 取消按钮 */
|
||||||
@@ -778,7 +705,7 @@ const cancel = () => {
|
|||||||
/** 新增按钮操作 */
|
/** 新增按钮操作 */
|
||||||
const handleAdd = async () => {
|
const handleAdd = async () => {
|
||||||
reset();
|
reset();
|
||||||
const { data } = await api.getUser();
|
const {data} = await api.getUser();
|
||||||
setDialogTitle('新增用户');
|
setDialogTitle('新增用户');
|
||||||
openUserDialog();
|
openUserDialog();
|
||||||
postOptions.value = data.posts;
|
postOptions.value = data.posts;
|
||||||
@@ -790,7 +717,7 @@ const handleAdd = async () => {
|
|||||||
const handleUpdate = async (row?: Partial<UserForm>) => {
|
const handleUpdate = async (row?: Partial<UserForm>) => {
|
||||||
reset();
|
reset();
|
||||||
const userId = row?.userId || ids.value[0];
|
const userId = row?.userId || ids.value[0];
|
||||||
const { data } = await api.getUser(userId);
|
const {data} = await api.getUser(userId);
|
||||||
setDialogTitle('修改用户');
|
setDialogTitle('修改用户');
|
||||||
openUserDialog();
|
openUserDialog();
|
||||||
Object.assign(form.value, data.user);
|
Object.assign(form.value, data.user);
|
||||||
@@ -871,4 +798,30 @@ async function handleDeptChange(value: number | string) {
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
width: 9px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: rgba(34, 197, 94, 1);
|
||||||
|
margin-right: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
color: #16A34A;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.close {
|
||||||
|
color: #9ca3af;
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
background-color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user