1. 设备管理界面完成
2. 用户管理界面完成
3. 会话列表完成
This commit is contained in:
Zy
2026-09-30 17:43:01 +08:00
parent a19777a2b8
commit 23703a66c2
47 changed files with 7451 additions and 190 deletions

View File

@@ -8,6 +8,12 @@ VITE_APP_ENV='development'
# 开发环境
VITE_APP_BASE_API='/dev-api'
# 本机环境
VITE_LOCALHOUSET=true
# 本地开发环境
VITE_BASE_API='http://192.168.0.212:8082'
# 应用访问路径 例如使用前缀 /admin/
VITE_APP_CONTEXT_PATH='/'

View File

@@ -0,0 +1,64 @@
import type {AgreementForm, AgreementQuery, AgreementVO} from '@/api/system/agreement/types';
import type {PageResult} from '@/api/types';
import type {AxiosPromise} from '@/utils/api-types';
import request from '@/utils/request';
/**
* 查询协议配置列表
* @param query
* @returns {*}
*/
export const listAgreement = (query?: AgreementQuery): AxiosPromise<PageResult<AgreementVO>> => {
return request({
url: '/system/agreement/list',
method: 'get',
params: query
});
};
/**
* 查询协议配置详细
* @param agreementId
*/
export const getAgreement = (agreementId: string | number): AxiosPromise<AgreementVO> => {
return request({
url: '/system/agreement/' + agreementId,
method: 'get'
});
};
/**
* 新增协议配置
* @param data
*/
export const addAgreement = (data: AgreementForm) => {
return request({
url: '/system/agreement',
method: 'post',
data: data
});
};
/**
* 修改协议配置
* @param data
*/
export const updateAgreement = (data: AgreementForm) => {
return request({
url: '/system/agreement',
method: 'put',
data: data
});
};
/**
* 删除协议配置
* @param agreementId
*/
export const delAgreement = (agreementId: string | number | Array<string | number>) => {
return request({
url: '/system/agreement/' + agreementId,
method: 'delete'
});
};

View File

@@ -0,0 +1,156 @@
export interface AgreementVO {
/**
* 主键(雪花)
*/
agreementId: string | number;
/**
* 协议编号
*/
agreementNo: string;
/**
* 类型(user/privacy/service)
*/
agreementType: string;
/**
* 协议名称
*/
name: string;
/**
* 版本号
*/
versionName: string;
/**
* 协议正文
*/
content: string;
/**
* 状态(0草稿 1已生效 2停用)
*/
status: boolean;
/**
* 生效时间
*/
publishTime: string;
/**
* 已生效类型;同类型仅一条,非生效或已删除为 NULL
*/
activeType: string;
}
export interface AgreementForm extends BaseEntity {
/**
* 主键(雪花)
*/
agreementId?: string | number;
/**
* 协议编号
*/
agreementNo?: string;
/**
* 类型(user/privacy/service)
*/
agreementType?: string;
/**
* 协议名称
*/
name?: string;
/**
* 版本号
*/
versionName?: string;
/**
* 协议正文
*/
content?: string;
/**
* 状态(0草稿 1已生效 2停用)
*/
status?: boolean;
/**
* 生效时间
*/
publishTime?: string;
/**
* 创建部门
*/
createDept?: number;
/**
* 创建者
*/
createBy?: number;
/**
* 创建时间
*/
createTime?: string;
/**
* 更新者
*/
updateBy?: number;
/**
* 更新时间
*/
updateTime?: string;
/**
* 已生效类型;同类型仅一条,非生效或已删除为 NULL
*/
activeType?: string;
}
export interface AgreementQuery extends PageQuery {
/**
* 协议编号
*/
agreementNo?: string;
/**
* 类型(user/privacy/service)
*/
agreementType?: string;
/**
* 协议名称
*/
name?: string;
/**
* 版本号
*/
versionName?: string;
/**
* 协议正文
*/
content?: string;
/**
* 状态(0草稿 1已生效 2停用)
*/
status?: boolean;
/**
* 生效时间
*/
publishTime?: string;
/**
* 创建部门
*/
createDept?: number;
/**
* 创建者
*/
createBy?: number;
/**
* 创建时间
*/
createTime?: string;
/**
* 更新者
*/
updateBy?: number;
/**
* 更新时间
*/
updateTime?: string;
/**
* 已生效类型;同类型仅一条,非生效或已删除为 NULL
*/
activeType?: string;
/**
* 日期范围参数
*/
params?: any;
}

View File

@@ -0,0 +1,64 @@
import type {CsSessionForm, CsSessionQuery, CsSessionVO} from '@/api/system/conversation/types';
import type {PageResult} from '@/api/types';
import type {AxiosPromise} from '@/utils/api-types';
import request from '@/utils/request';
/**
* 查询客服会话列表
* @param query
* @returns {*}
*/
export const listCsSession = (query?: CsSessionQuery): AxiosPromise<PageResult<CsSessionVO>> => {
return request({
url: '/conversation/csSession/list',
method: 'get',
params: query
});
};
/**
* 查询客服会话详细
* @param sessionId
*/
export const getCsSession = (sessionId: string | number): AxiosPromise<CsSessionVO> => {
return request({
url: '/conversation/csSession/' + sessionId,
method: 'get'
});
};
/**
* 新增客服会话
* @param data
*/
export const addCsSession = (data: CsSessionForm) => {
return request({
url: '/conversation/csSession',
method: 'post',
data: data
});
};
/**
* 修改客服会话
* @param data
*/
export const updateCsSession = (data: CsSessionForm) => {
return request({
url: '/conversation/csSession',
method: 'put',
data: data
});
};
/**
* 删除客服会话
* @param sessionId
*/
export const delCsSession = (sessionId: string | number | Array<string | number>) => {
return request({
url: '/conversation/csSession/' + sessionId,
method: 'delete'
});
};

View File

@@ -0,0 +1,176 @@
export interface CsSessionVO {
/**
* 主键(雪花)
*/
sessionId: string | number;
/**
* 会话编号
*/
sessionNo: string;
/**
* APP 用户
*/
userId: string | number;
/**
* 关联设备,可空
*/
deviceId: string | number;
/**
* 问题类型
*/
problemType: string;
/**
* 用户问题摘要
*/
summary: string;
/**
* 状态(0机器人 1排队 2人工 3已结束)
*/
status: boolean;
/**
* 当前接待客服
*/
agentId: string | number;
/**
* 接入方式(bot/bot_to_human/human)
*/
accessType: string;
/**
* 排队序号
*/
queueNo: number;
/**
* 会话开始时间
*/
startTime: string;
/**
* 人工接入时间
*/
acceptTime: string;
/**
* 结束时间
*/
endTime: string;
/**
* 最后消息时间
*/
lastMessageTime: string;
}
export interface CsSessionForm extends BaseEntity {
/**
* 主键(雪花)
*/
sessionId?: string | number;
/**
* 会话编号
*/
sessionNo?: string;
/**
* APP 用户
*/
userId?: string | number;
/**
* 关联设备,可空
*/
deviceId?: string | number;
/**
* 问题类型
*/
problemType?: string;
/**
* 用户问题摘要
*/
summary?: string;
/**
* 状态(0机器人 1排队 2人工 3已结束)
*/
status?: boolean;
/**
* 当前接待客服
*/
agentId?: string | number;
/**
* 接入方式(bot/bot_to_human/human)
*/
accessType?: string;
/**
* 排队序号
*/
queueNo?: number;
/**
* 会话开始时间
*/
startTime?: string;
/**
* 人工接入时间
*/
acceptTime?: string;
/**
* 结束时间
*/
endTime?: string;
/**
* 最后消息时间
*/
lastMessageTime?: string;
}
export interface CsSessionQuery extends PageQuery {
/**
* 会话编号
*/
sessionNo?: string;
/**
* APP 用户
*/
userId?: string | number;
/**
* 关联设备,可空
*/
deviceId?: string | number;
/**
* 问题类型
*/
problemType?: string;
/**
* 用户问题摘要
*/
summary?: string;
/**
* 状态(0机器人 1排队 2人工 3已结束)
*/
status?: boolean;
/**
* 当前接待客服
*/
agentId?: string | number;
/**
* 接入方式(bot/bot_to_human/human)
*/
accessType?: string;
/**
* 排队序号
*/
queueNo?: number;
/**
* 会话开始时间
*/
startTime?: string;
/**
* 人工接入时间
*/
acceptTime?: string;
/**
* 结束时间
*/
endTime?: string;
/**
* 最后消息时间
*/
lastMessageTime?: string;
/**
* 日期范围参数
*/
params?: any;
}

View File

@@ -0,0 +1,64 @@
import type {CsKbForm, CsKbQuery, CsKbVO} from '@/api/system/cskb/types';
import type {PageResult} from '@/api/types';
import type {AxiosPromise} from '@/utils/api-types';
import request from '@/utils/request';
/**
* 查询客服知识库列表
* @param query
* @returns {*}
*/
export const listCsKb = (query?: CsKbQuery): AxiosPromise<PageResult<CsKbVO>> => {
return request({
url: '/system/csKb/list',
method: 'get',
params: query
});
};
/**
* 查询客服知识库详细
* @param kbId
*/
export const getCsKb = (kbId: string | number): AxiosPromise<CsKbVO> => {
return request({
url: '/system/csKb/' + kbId,
method: 'get'
});
};
/**
* 新增客服知识库
* @param data
*/
export const addCsKb = (data: CsKbForm) => {
return request({
url: '/system/csKb',
method: 'post',
data: data
});
};
/**
* 修改客服知识库
* @param data
*/
export const updateCsKb = (data: CsKbForm) => {
return request({
url: '/system/csKb',
method: 'put',
data: data
});
};
/**
* 删除客服知识库
* @param kbId
*/
export const delCsKb = (kbId: string | number | Array<string | number>) => {
return request({
url: '/system/csKb/' + kbId,
method: 'delete'
});
};

View File

@@ -0,0 +1,132 @@
export interface CsKbVO {
/**
* 主键(雪花)
*/
kbId: string | number;
/**
* 知识编号,如 KB-001
*/
kbNo: string;
/**
* 问题标题
*/
title: string;
/**
* 分类(设备配网/耗材滤芯/固件升级/设备报修;固件升级仅文案分类)
*/
category: string;
/**
* 机器人回复内容
*/
content: string;
/**
* 排序权重,数字越小越靠前
*/
sort: number;
/**
* 状态(0停用 1启用)
*/
status: boolean;
}
export interface CsKbForm extends BaseEntity {
/**
* 主键(雪花)
*/
kbId?: string | number;
/**
* 知识编号,如 KB-001
*/
kbNo?: string;
/**
* 问题标题
*/
title?: string;
/**
* 分类(设备配网/耗材滤芯/固件升级/设备报修;固件升级仅文案分类)
*/
category?: string;
/**
* 机器人回复内容
*/
content?: string;
/**
* 排序权重,数字越小越靠前
*/
sort?: number;
/**
* 状态(0停用 1启用)
*/
status?: boolean;
/**
* 创建部门
*/
createDept?: number;
/**
* 创建者
*/
createBy?: number;
/**
* 创建时间
*/
createTime?: string;
/**
* 更新者
*/
updateBy?: number;
/**
* 更新时间
*/
updateTime?: string;
}
export interface CsKbQuery extends PageQuery {
/**
* 知识编号,如 KB-001
*/
kbNo?: string;
/**
* 问题标题
*/
title?: string;
/**
* 分类(设备配网/耗材滤芯/固件升级/设备报修;固件升级仅文案分类)
*/
category?: string;
/**
* 机器人回复内容
*/
content?: string;
/**
* 排序权重,数字越小越靠前
*/
sort?: number;
/**
* 状态(0停用 1启用)
*/
status?: boolean;
/**
* 创建部门
*/
createDept?: number;
/**
* 创建者
*/
createBy?: number;
/**
* 创建时间
*/
createTime?: string;
/**
* 更新者
*/
updateBy?: number;
/**
* 更新时间
*/
updateTime?: string;
/**
* 日期范围参数
*/
params?: any;
}

View File

@@ -0,0 +1,64 @@
import type {CsAgentForm, CsAgentQuery, CsAgentVO} from '@/api/system/cssAgent/types';
import type {PageResult} from '@/api/types';
import type {AxiosPromise} from '@/utils/api-types';
import request from '@/utils/request';
/**
* 查询客服人员列表
* @param query
* @returns {*}
*/
export const listCsAgent = (query?: CsAgentQuery): AxiosPromise<PageResult<CsAgentVO>> => {
return request({
url: '/system/csAgent/list',
method: 'get',
params: query
});
};
/**
* 查询客服人员详细
* @param agentId
*/
export const getCsAgent = (agentId: string | number): AxiosPromise<CsAgentVO> => {
return request({
url: '/system/csAgent/' + agentId,
method: 'get'
});
};
/**
* 新增客服人员
* @param data
*/
export const addCsAgent = (data: CsAgentForm) => {
return request({
url: '/system/csAgent',
method: 'post',
data: data
});
};
/**
* 修改客服人员
* @param data
*/
export const updateCsAgent = (data: CsAgentForm) => {
return request({
url: '/system/csAgent',
method: 'put',
data: data
});
};
/**
* 删除客服人员
* @param agentId
*/
export const delCsAgent = (agentId: string | number | Array<string | number>) => {
return request({
url: '/system/csAgent/' + agentId,
method: 'delete'
});
};

View File

@@ -0,0 +1,168 @@
export interface CsAgentVO {
/**
* 主键(雪花)
*/
agentId: string | number;
/**
* 客服编号,如 KF-001
*/
agentNo: string;
/**
* 工号
*/
jobNo: string;
/**
* 客服姓名
*/
name: string;
/**
* 登录账号
*/
account: string;
/**
* 密码(BCrypt)
*/
password: string;
/**
* 手机号
*/
phone: string;
/**
* 头像 oss_id
*/
avatarOssId: string | number;
/**
* 在线状态(0离线 1在线 2忙碌)
*/
onlineStatus: boolean;
/**
* 账号状态(0启用 1禁用)
*/
status: boolean;
}
export interface CsAgentForm extends BaseEntity {
/**
* 主键(雪花)
*/
agentId?: string | number;
/**
* 客服编号,如 KF-001
*/
agentNo?: string;
/**
* 工号
*/
jobNo?: string;
/**
* 客服姓名
*/
name?: string;
/**
* 登录账号
*/
account?: string;
/**
* 密码(BCrypt)
*/
password?: string;
/**
* 手机号
*/
phone?: string;
/**
* 头像 oss_id
*/
avatarOssId?: string | number;
/**
* 在线状态(0离线 1在线 2忙碌)
*/
onlineStatus?: boolean;
/**
* 账号状态(0启用 1禁用)
*/
status?: boolean;
/**
* 创建部门
*/
createDept?: number;
/**
* 创建者
*/
createBy?: number;
/**
* 创建时间
*/
createTime?: string;
/**
* 更新者
*/
updateBy?: number;
/**
* 更新时间
*/
updateTime?: string;
}
export interface CsAgentQuery extends PageQuery {
/**
* 客服编号,如 KF-001
*/
agentNo?: string;
/**
* 工号
*/
jobNo?: string;
/**
* 客服姓名
*/
name?: string;
/**
* 登录账号
*/
account?: string;
/**
* 密码(BCrypt)
*/
password?: string;
/**
* 手机号
*/
phone?: string;
/**
* 头像 oss_id
*/
avatarOssId?: string | number;
/**
* 在线状态(0离线 1在线 2忙碌)
*/
onlineStatus?: boolean;
/**
* 账号状态(0启用 1禁用)
*/
status?: boolean;
/**
* 创建部门
*/
createDept?: number;
/**
* 创建者
*/
createBy?: number;
/**
* 创建时间
*/
createTime?: string;
/**
* 更新者
*/
updateBy?: number;
/**
* 更新时间
*/
updateTime?: string;
/**
* 日期范围参数
*/
params?: any;
}

View File

@@ -0,0 +1,64 @@
import type {UserForm, UserQuery, UserVO} from '@/api/system/customer/types';
import type {PageResult} from '@/api/types';
import type {AxiosPromise} from '@/utils/api-types';
import request from '@/utils/request';
/**
* 查询App用户列表
* @param query
* @returns {*}
*/
export const listUser = (query?: UserQuery): AxiosPromise<PageResult<UserVO>> => {
return request({
url: '/system/user/list',
method: 'get',
params: query
});
};
/**
* 查询App用户详细
* @param userId
*/
export const getUser = (userId: string | number): AxiosPromise<UserVO> => {
return request({
url: '/system/user/' + userId,
method: 'get'
});
};
/**
* 新增App用户
* @param data
*/
export const addUser = (data: UserForm) => {
return request({
url: '/system/user',
method: 'post',
data: data
});
};
/**
* 修改App用户
* @param data
*/
export const updateUser = (data: UserForm) => {
return request({
url: '/system/user',
method: 'put',
data: data
});
};
/**
* 删除App用户
* @param userId
*/
export const delUser = (userId: string | number | Array<string | number>) => {
return request({
url: '/system/user/' + userId,
method: 'delete'
});
};

View File

@@ -0,0 +1,276 @@
export interface UserVO {
/**
* 用户ID(雪花ID)
*/
userId: string | number;
/**
* 用户账号(默认使用手机号)
*/
userName: string;
/**
* 用户展示编号,如 U20260918001
*/
userNo: string;
/**
* 手机号码
*/
phonenumber: string;
/**
* 用户昵称
*/
nickName: string;
/**
* 头像 oss_id
*/
avatarOssId: string | number;
/**
* 头像 URL
*/
avatarUrl: string;
/**
* 密码(BCrypt 加密)
*/
password: string;
/**
* 最近一次修改密码时间
*/
passwordUpdateTime: string;
/**
* 性别(0未知 1男 2女)
*/
gender: string;
/**
* 生日
*/
birthday: string;
/**
* 省份
*/
province: string;
/**
* 城市
*/
city: string;
/**
* 区县
*/
district: string;
/**
* 账号状态(0正常 1停用)
*/
status: boolean;
/**
* 注销时间;停用仍用 status
*/
cancelTime: string;
/**
* 最后登录IP
*/
loginIp: string;
/**
* 最后登录时间
*/
loginDate: string;
/**
* 登录次数
*/
loginCount: number;
/**
* 备注
*/
remark: string;
}
export interface UserForm extends BaseEntity {
/**
* 用户ID(雪花ID)
*/
userId?: string | number;
/**
* 用户账号(默认使用手机号)
*/
userName?: string;
/**
* 用户展示编号,如 U20260918001
*/
userNo?: string;
/**
* 手机号码
*/
phonenumber?: string;
/**
* 用户昵称
*/
nickName?: string;
/**
* 头像 oss_id
*/
avatarOssId?: string | number;
/**
* 头像 URL
*/
avatarUrl?: string;
/**
* 密码(BCrypt 加密)
*/
password?: string;
/**
* 最近一次修改密码时间
*/
passwordUpdateTime?: string;
/**
* 性别(0未知 1男 2女)
*/
gender?: string;
/**
* 生日
*/
birthday?: string;
/**
* 省份
*/
province?: string;
/**
* 城市
*/
city?: string;
/**
* 区县
*/
district?: string;
/**
* 账号状态(0正常 1停用)
*/
status?: boolean;
/**
* 注销时间;停用仍用 status
*/
cancelTime?: string;
/**
* 最后登录IP
*/
loginIp?: string;
/**
* 最后登录时间
*/
loginDate?: string;
/**
* 登录次数
*/
loginCount?: number;
/**
* 创建者
*/
createBy?: number;
/**
* 创建时间
*/
createTime?: string;
/**
* 更新者
*/
updateBy?: number;
/**
* 更新时间
*/
updateTime?: string;
/**
* 备注
*/
remark?: string;
}
export interface UserQuery extends PageQuery {
/**
* 用户账号(默认使用手机号)
*/
userName?: string;
/**
* 用户展示编号,如 U20260918001
*/
userNo?: string;
/**
* 手机号码
*/
phonenumber?: string;
/**
* 用户昵称
*/
nickName?: string;
/**
* 头像 oss_id
*/
avatarOssId?: string | number;
/**
* 头像 URL
*/
avatarUrl?: string;
/**
* 密码(BCrypt 加密)
*/
password?: string;
/**
* 最近一次修改密码时间
*/
passwordUpdateTime?: string;
/**
* 性别(0未知 1男 2女)
*/
gender?: string;
/**
* 生日
*/
birthday?: string;
/**
* 省份
*/
province?: string;
/**
* 城市
*/
city?: string;
/**
* 区县
*/
district?: string;
/**
* 账号状态(0正常 1停用)
*/
status?: boolean;
/**
* 注销时间;停用仍用 status
*/
cancelTime?: string;
/**
* 最后登录IP
*/
loginIp?: string;
/**
* 最后登录时间
*/
loginDate?: string;
/**
* 登录次数
*/
loginCount?: number;
/**
* 创建者
*/
createBy?: number;
/**
* 创建时间
*/
createTime?: string;
/**
* 更新者
*/
updateBy?: number;
/**
* 更新时间
*/
updateTime?: string;
/**
* 日期范围参数
*/
params?: any;
}

View File

@@ -0,0 +1,64 @@
import type {DeviceForm, DeviceQuery, DeviceVO} from '@/api/system/device/types';
import type {PageResult} from '@/api/types';
import type {AxiosPromise} from '@/utils/api-types';
import request from '@/utils/request';
/**
* 查询设备主列表
* @param query
* @returns {*}
*/
export const listDevice = (query?: DeviceQuery): AxiosPromise<PageResult<DeviceVO>> => {
return request({
url: '/device/device/list',
method: 'get',
params: query
});
};
/**
* 查询设备主详细
* @param deviceId
*/
export const getDevice = (deviceId: string | number): AxiosPromise<DeviceVO> => {
return request({
url: '/device/device/' + deviceId,
method: 'get'
});
};
/**
* 新增设备主
* @param data
*/
export const addDevice = (data: DeviceForm) => {
return request({
url: '/device/device',
method: 'post',
data: data
});
};
/**
* 修改设备主
* @param data
*/
export const updateDevice = (data: DeviceForm) => {
return request({
url: '/device/device',
method: 'put',
data: data
});
};
/**
* 删除设备主
* @param deviceId
*/
export const delDevice = (deviceId: string | number | Array<string | number>) => {
return request({
url: '/device/device/' + deviceId,
method: 'delete'
});
};

View File

@@ -0,0 +1,428 @@
export interface DeviceVO {
/**
* 主键(雪花,仅内部关联)
*/
deviceId: string | number;
/**
* 业务编号,未删除记录内唯一;逻辑删除后可再次使用
*/
deviceNo: string;
/**
* 绑定码IMEI,可空;未删除记录内唯一,逻辑删除后可再次使用
*/
imei: string;
/**
* 用户侧名称≤20字符
*/
name: string;
/**
* 产品模板
*/
productId: string | number;
/**
* 型号快照,新增时取自产品模板
*/
model: string;
/**
* 出厂序列号
*/
sn: string;
/**
* 硬件版本
*/
hwVersion: string;
/**
* 软件版本
*/
swVersion: string;
/**
* 固件版本
*/
fwVersion: string;
/**
* 数据包版本
*/
pkgVersion: string;
/**
* 设备IP
*/
ip: string;
/**
* MAC,未删除记录内唯一;逻辑删除后可再次添加
*/
mac: string;
/**
* WiFi SSID
*/
wifiSsid: string | number;
/**
* 蓝牙(0未连接 1已连接)
*/
bluetoothConnected: string;
/**
* 状态(0未激活 1在线 2离线)
*/
status: boolean;
/**
* 当前绑定APP用户
*/
bindUserId: string | number;
/**
* 最近一次绑定时间
*/
bindTime: string;
/**
* 当前家庭
*/
homeId: string | number;
/**
* 当前房间
*/
locationId: string | number;
/**
* 信号等级(0离线 1弱 2一般 3强)
*/
signalLevel: number;
/**
* 最新经度
*/
lng: number;
/**
* 最新纬度
*/
lat: number;
/**
* 地址(设备上报)
*/
address: string;
/**
* 充电(0否 1充电中;无电池设备为空)
*/
charging: string;
/**
* 设备电量0-100
*/
deviceBattery: number;
/**
* 最近一次运行数据上报时间
*/
lastReportTime: string;
/**
* 最后在线时间
*/
lastOnlineTime: string;
/**
* 备注
*/
remark: string;
/**
* 未删除时的业务编号
*/
activeDeviceNo: string;
/**
* 未删除时的IMEI
*/
activeImei: string;
/**
* 未删除时的MAC
*/
activeMac: string;
}
export interface DeviceForm extends BaseEntity {
/**
* 主键(雪花,仅内部关联)
*/
deviceId?: string | number;
/**
* 业务编号,未删除记录内唯一;逻辑删除后可再次使用
*/
deviceNo?: string;
/**
* 绑定码IMEI,可空;未删除记录内唯一,逻辑删除后可再次使用
*/
imei?: string;
/**
* 用户侧名称≤20字符
*/
name?: string;
/**
* 产品模板
*/
productId?: string | number;
/**
* 型号快照,新增时取自产品模板
*/
model?: string;
/**
* 出厂序列号
*/
sn?: string;
/**
* 硬件版本
*/
hwVersion?: string;
/**
* 软件版本
*/
swVersion?: string;
/**
* 固件版本
*/
fwVersion?: string;
/**
* 数据包版本
*/
pkgVersion?: string;
/**
* 设备IP
*/
ip?: string;
/**
* MAC,未删除记录内唯一;逻辑删除后可再次添加
*/
mac?: string;
/**
* WiFi SSID
*/
wifiSsid?: string | number;
/**
* 蓝牙(0未连接 1已连接)
*/
bluetoothConnected?: string;
/**
* 状态(0未激活 1在线 2离线)
*/
status?: boolean;
/**
* 当前绑定APP用户
*/
bindUserId?: string | number;
/**
* 最近一次绑定时间
*/
bindTime?: string;
/**
* 当前家庭
*/
homeId?: string | number;
/**
* 当前房间
*/
locationId?: string | number;
/**
* 信号等级(0离线 1弱 2一般 3强)
*/
signalLevel?: number;
/**
* 最新经度
*/
lng?: number;
/**
* 最新纬度
*/
lat?: number;
/**
* 地址(设备上报)
*/
address?: string;
/**
* 充电(0否 1充电中;无电池设备为空)
*/
charging?: string;
/**
* 设备电量0-100
*/
deviceBattery?: number;
/**
* 最近一次运行数据上报时间
*/
lastReportTime?: string;
/**
* 最后在线时间
*/
lastOnlineTime?: string;
/**
* 备注
*/
remark?: string;
/**
* 创建部门
*/
createDept?: number;
/**
* 创建者
*/
createBy?: number;
/**
* 创建时间
*/
createTime?: string;
/**
* 更新者
*/
updateBy?: number;
/**
* 更新时间
*/
updateTime?: string;
/**
* 未删除时的业务编号
*/
activeDeviceNo?: string;
/**
* 未删除时的IMEI
*/
activeImei?: string;
/**
* 未删除时的MAC
*/
activeMac?: string;
}
export interface DeviceQuery extends PageQuery {
/**
* 业务编号,未删除记录内唯一;逻辑删除后可再次使用
*/
deviceNo?: string;
/**
* 绑定码IMEI,可空;未删除记录内唯一,逻辑删除后可再次使用
*/
imei?: string;
/**
* 用户侧名称≤20字符
*/
name?: string;
/**
* 产品模板
*/
productId?: string | number;
/**
* 型号快照,新增时取自产品模板
*/
model?: string;
/**
* 出厂序列号
*/
sn?: string;
/**
* 硬件版本
*/
hwVersion?: string;
/**
* 软件版本
*/
swVersion?: string;
/**
* 固件版本
*/
fwVersion?: string;
/**
* 数据包版本
*/
pkgVersion?: string;
/**
* 设备IP
*/
ip?: string;
/**
* MAC,未删除记录内唯一;逻辑删除后可再次添加
*/
mac?: string;
/**
* WiFi SSID
*/
wifiSsid?: string | number;
/**
* 蓝牙(0未连接 1已连接)
*/
bluetoothConnected?: string;
/**
* 状态(0未激活 1在线 2离线)
*/
status?: boolean;
/**
* 当前绑定APP用户
*/
bindUserId?: string | number;
/**
* 最近一次绑定时间
*/
bindTime?: string;
/**
* 当前家庭
*/
homeId?: string | number;
/**
* 当前房间
*/
locationId?: string | number;
/**
* 信号等级(0离线 1弱 2一般 3强)
*/
signalLevel?: number;
/**
* 最新经度
*/
lng?: number;
/**
* 最新纬度
*/
lat?: number;
/**
* 地址(设备上报)
*/
address?: string;
/**
* 充电(0否 1充电中;无电池设备为空)
*/
charging?: string;
/**
* 设备电量0-100
*/
deviceBattery?: number;
/**
* 最近一次运行数据上报时间
*/
lastReportTime?: string;
/**
* 最后在线时间
*/
lastOnlineTime?: string;
/**
* 创建部门
*/
createDept?: number;
/**
* 创建者
*/
createBy?: number;
/**
* 创建时间
*/
createTime?: string;
/**
* 更新者
*/
updateBy?: number;
/**
* 更新时间
*/
updateTime?: string;
/**
* 未删除时的业务编号
*/
activeDeviceNo?: string;
/**
* 未删除时的IMEI
*/
activeImei?: string;
/**
* 未删除时的MAC
*/
activeMac?: string;
/**
* 日期范围参数
*/
params?: any;
}

View File

@@ -8,8 +8,7 @@ body {
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
background: var(--app-shell-bg);
font-family:
'MiSans', 'HarmonyOS Sans SC', 'PingFang SC', 'Source Han Sans SC', 'Noto Sans SC', 'Hiragino Sans GB',
font-family: 'MiSans', 'HarmonyOS Sans SC', 'PingFang SC', 'Source Han Sans SC', 'Noto Sans SC', 'Hiragino Sans GB',
'Microsoft YaHei', sans-serif;
}
@@ -88,3 +87,18 @@ h6 {
line-height: 1.1;
color: inherit;
}
ul, li {
list-style: none;
padding: 0;
margin: 0;
}
// 通用卡片
.commonCard {
padding: 24px;
border-radius: 16px 16px 16px 16px;
border: 1px solid rgba(238, 240, 243, 1);
margin-top: 8px;
}

View File

@@ -1,4 +1,4 @@
/* 业务列表/卡片页:分页、树、列表、el-card 等(与 vendors/element-plus 通用覆盖叠加,入口顺序见 index.scss)。
/* 业务列表/卡片页:分页、树、列表、el-card 等(与 vendors/element-plus 通用覆盖叠加,入口顺序见 index.vue.scss)。
* 属「页面模式」而非 EP 公共主题,故放在 components。 */
// --- 历史「表单分区标题」样式 ---
@@ -51,8 +51,7 @@
box-shadow: var(--app-shadow-sm);
border-color: var(--app-surface-border);
overflow: hidden;
transition:
box-shadow 0.2s ease,
transition: box-shadow 0.2s ease,
border-color 0.2s ease;
background: var(--app-surface-bg);
}

View File

@@ -22,6 +22,7 @@
margin: 0;
height: 40px;
color: var(--side-menu-text) !important;
&:hover {
background-color: var(--side-menu-hover-bg) !important;
color: var(--side-menu-hover-text) !important;
@@ -54,11 +55,11 @@
//margin: 3px 8px;
//background: transparent !important;
background: #f7f7f7 !important;
&.is-active {
background: var(--side-menu-active-bg) !important;
color: var(--side-menu-active-text) !important;
box-shadow:
inset 3px 0 0 var(--side-menu-active-line),
box-shadow: inset 3px 0 0 var(--side-menu-active-line),
inset 0 0 0 1px var(--side-menu-active-border);
}
@@ -75,11 +76,11 @@
//margin: 3px 8px;
//background: transparent !important;
background: #f7f7f7 !important;
&.is-active {
background: var(--side-menu-active-bg) !important;
color: var(--side-menu-active-text) !important;
box-shadow:
inset 3px 0 0 var(--side-menu-active-line),
box-shadow: inset 3px 0 0 var(--side-menu-active-line),
inset 0 0 0 1px var(--side-menu-active-border);
}
@@ -111,3 +112,48 @@
}
}
}
#app {
// 侧边栏滚动容器,假设侧边栏滚动区域 class 为 sidebar-scroll
.beautify_scroll {
// Firefox 滚动条
scrollbar-width: thin;
scrollbar-color: var(--side-scrollbar-thumb) var(--side-scrollbar-bg);
// webkit 滚动条
&::-webkit-scrollbar {
width: 6px; // 滚动条宽度
height: 6px;
}
&::-webkit-scrollbar-track {
background: var(--side-scrollbar-bg);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: var(--side-scrollbar-thumb);
border-radius: 3px;
&:hover {
background: var(--side-scrollbar-thumb-hover);
}
}
&::-webkit-scrollbar-corner {
background: transparent;
}
}
// 浅色主题变量
--side-scrollbar-bg: transparent;
--side-scrollbar-thumb: #c0c4cc;
--side-scrollbar-thumb-hover: #909399;
// 暗黑主题覆盖变量
&.theme-dark {
--side-scrollbar-bg: transparent;
--side-scrollbar-thumb: #414243;
--side-scrollbar-thumb-hover: #58595b;
}
}

View File

@@ -64,4 +64,6 @@
--el-box-shadow-light: var(--app-shadow-sm);
--el-box-shadow: var(--app-shadow-md);
--el-bg-color-page: var(--app-shell-bg);
}

View File

@@ -28,12 +28,21 @@
// --- primary 品牌色(亮色;暗色见 tokens/_theme-dark) ---
.el-button--primary {
--el-button-bg-color: var(--app-accent-strong);
border: none;
--el-button-bg-color: #2563eb;
--el-button-border-color: var(--app-accent-strong);
--el-button-hover-bg-color: #4a7cff;
--el-button-hover-border-color: #4a7cff;
--el-button-active-bg-color: #2e5ce0;
--el-button-active-border-color: #2e5ce0;
&.is-plain {
//color: rgba(37, 99, 235, 1);
}
}
.el-button--info {
border: 1px solid #e5e7eb;
}
// --- 表格 CSS 变量接业务 token ---
@@ -82,3 +91,4 @@ html.dark .el-table {
.el-select__wrapper {
box-shadow: 0 0 0 1px var(--el-border-color) inset;
}

View File

@@ -36,7 +36,7 @@ const getBreadcrumb = () => {
}
// 判断是否为首页
if (!isDashboard(matched[0])) {
// matched = [{ path: '/index', meta: { title: '首页' } }].concat(matched);
// matched = [{ path: '/index.vue', meta: { title: '首页' } }].concat(matched);
}
levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false);
};

View File

@@ -0,0 +1,346 @@
<template>
<div class="editor-shell">
<EditorToolbar
:default-config="toolbarConfig"
:editor="editorRef"
class="editor-toolbar"
mode="default"
/>
<div :style="styles" class="editor-body">
<WangEditor
v-model="content"
:default-config="editorConfig"
class="editor-content"
mode="default"
@onChange="handleChange"
@onCreated="handleCreated"
/>
</div>
<!-- 底部提示、示例、字数统计 -->
<div v-if="simpleToolbar" class="editor-tip">
<p>请输入标准回复话术,可使用「有序步骤」插入分步指引,例如:</p>
<div class="demo-box">
<ol>
<li>确认设备已通电并处于配网模式(指示灯橙色快闪)</li>
<li>打开APP「我的设备」→ 添加设备,选择对应型号</li>
<li>输入2.4G Wi-Fi密码并等待约30秒完成连接</li>
</ol>
</div>
<p>以上为示例格式</p>
<p>支持插入图片与链接;回复中可引用设备型号,帮助用户快速对号入座</p>
<div class="word-count">{{ wordCount }}/2000字</div>
</div>
</div>
</template>
<script lang="ts" setup>
import '@wangeditor-next/editor/dist/css/style.css';
import type {IDomEditor, IEditorConfig, IToolbarConfig} from '@wangeditor-next/editor';
import {Editor as WangEditor, Toolbar as EditorToolbar} from '@wangeditor-next/editor-for-vue';
import modal from '@/plugins/modal';
import {propTypes} from '@/utils/propTypes';
import {globalHeaders} from '@/utils/request';
import {shallowRef, ref, computed, watch, onBeforeUnmount} from 'vue';
const props = defineProps({
/* 编辑器的内容 */
modelValue: propTypes.string,
/* 高度 */
height: propTypes.number.def(400),
/* 最小高度 */
minHeight: propTypes.number.def(400),
/* 只读 */
readOnly: propTypes.bool.def(false),
/* 图片上传文件大小限制(MB) */
fileSize: propTypes.number.def(5),
/* 视频上传文件大小限制(MB) */
videoSize: propTypes.number.def(100),
/* 类型(base64格式、url格式) */
type: propTypes.string.def('url'),
/* 精简工具栏(仅加粗/有序步骤/图片/链接/清除格式) */
simpleToolbar: propTypes.bool.def(false)
});
const emit = defineEmits(['update:modelValue']);
const editorRef = shallowRef<IDomEditor>();
const content = ref('');
const wordCount = ref(0);
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadOssUrl = baseUrl + '/resource/oss/upload';
const styles = computed(() => {
const style: Record<string, string> = {};
if (props.minHeight) {
style.minHeight = `${props.minHeight}px`;
}
if (props.height) {
style.height = `${props.height}px`;
}
return style;
});
const toolbarConfig = computed<Partial<IToolbarConfig>>(() => {
if (props.simpleToolbar) {
return {
toolbarKeys: [
'bold',
'numberedList',
'uploadImage',
'insertLink',
'clearStyle',
],
excludeKeys: []
};
} else {
const excludeKeys: string[] = ['fullScreen'];
if (!props.type) {
excludeKeys.push('uploadImage');
excludeKeys.push('uploadVideo');
}
return {excludeKeys};
}
});
/* ==================== OSS 上传 ==================== */
const uploadToOss = async (file: File): Promise<{ url: string; fileName: string }> => {
const formData = new FormData();
formData.append('file', file);
const res = await fetch(uploadOssUrl, {
method: 'POST',
headers: globalHeaders(),
body: formData
});
const data = await res.json();
if (data.code === 200) {
return data.data;
}
throw new Error(data.msg || '上传失败');
};
/* ==================== 文件校验 ==================== */
const validateImageFile = (file: File) => {
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/svg', 'image/svg+xml'];
if (!allowedTypes.includes(file.type)) {
modal.msgError('图片格式错误!');
return false;
}
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
if (!isLt) {
modal.msgError(`上传图片大小不能超过 ${props.fileSize} MB!`);
return false;
}
}
return true;
};
const validateVideoFile = (file: File) => {
const allowedTypes = ['video/mp4', 'video/webm', 'video/ogg'];
if (!allowedTypes.includes(file.type)) {
modal.msgError('视频格式错误, 请上传 mp4/webm/ogg 格式!');
return false;
}
if (props.videoSize) {
const isLt = file.size / 1024 / 1024 < props.videoSize;
if (!isLt) {
modal.msgError(`上传视频大小不能超过 ${props.videoSize} MB!`);
return false;
}
}
return true;
};
/* ==================== wangeditor 菜单配置 ==================== */
const getUploadImageMenuConfig = () => {
if (props.type === 'base64') {
return {
allowedFileTypes: ['image/jpeg', 'image/jpg', 'image/png', 'image/svg+xml'],
customUpload(file: File, insertFn: (url: string, alt?: string, href?: string) => void) {
if (!validateImageFile(file)) return;
modal.loading('正在上传图片,请稍候...');
const reader = new FileReader();
reader.onload = () => {
insertFn(reader.result as string, file.name);
modal.closeLoading();
};
reader.onerror = () => {
modal.msgError('图片插入失败');
modal.closeLoading();
};
reader.readAsDataURL(file);
}
};
}
return {
allowedFileTypes: ['image/jpeg', 'image/jpg', 'image/png', 'image/svg+xml'],
async customUpload(file: File, insertFn: (url: string, alt?: string, href?: string) => void) {
if (!validateImageFile(file)) return;
modal.loading('正在上传图片,请稍候...');
try {
const result = await uploadToOss(file);
insertFn(result.url, file.name, result.url);
} catch {
modal.msgError('图片上传失败');
} finally {
modal.closeLoading();
}
}
};
};
const getUploadVideoMenuConfig = () => ({
allowedFileTypes: ['video/mp4', 'video/webm', 'video/ogg'],
async customUpload(file: File, insertFn: (url: string, poster?: string) => void) {
if (!validateVideoFile(file)) return;
modal.loading('正在上传视频,请稍候...');
try {
const result = await uploadToOss(file);
insertFn(result.url);
} catch {
modal.msgError('视频上传失败');
} finally {
modal.closeLoading();
}
}
});
const editorConfig = computed<Partial<IEditorConfig>>(() => ({
placeholder: props.simpleToolbar ? '请输入标准回复话术...' : '请输入内容',
autoFocus: false,
maxLength: 2000,
MENU_CONF: {
uploadImage: getUploadImageMenuConfig() as any,
uploadVideo: getUploadVideoMenuConfig() as any
},
customMenu: [
{
key: 'numberedList',
title: '有序步骤'
}
]
}));
/* ==================== 事件处理 ==================== */
const handleChange = () => {
if (!editorRef.value) return;
wordCount.value = editorRef.value.getText().length;
};
const syncReadOnly = () => {
const editor = editorRef.value;
if (!editor) return;
if (props.readOnly) {
editor.disable();
} else {
editor.enable();
}
};
const handleCreated = (editor: IDomEditor) => {
editorRef.value = editor;
syncReadOnly();
wordCount.value = editor.getText().length;
};
/* ==================== 双向数据绑定(直接透传HTML) ==================== */
watch(
() => props.modelValue,
value => {
if (value !== content.value) {
content.value = value || '';
}
},
{immediate: true}
);
watch(content, value => {
emit('update:modelValue', value);
});
watch(
() => props.readOnly,
() => {
syncReadOnly();
}
);
onBeforeUnmount(() => {
editorRef.value?.destroy();
});
</script>
<style scoped>
.editor-shell {
border: 1px solid var(--el-border-color);
border-radius: var(--el-border-radius-base);
overflow: hidden;
background: var(--el-bg-color);
}
.editor-toolbar {
border-bottom: 1px solid var(--el-border-color);
}
.editor-body {
display: flex;
flex-direction: column;
}
.editor-content {
height: 100%;
}
.editor-shell :deep(.w-e-toolbar) {
border: 0 !important;
background-color: transparent;
}
.editor-shell :deep(.w-e-text-container) {
border: 0 !important;
background-color: transparent;
}
.editor-shell :deep(.w-e-text-container [data-slate-editor]) {
min-height: inherit;
}
.editor-tip {
padding: 12px 16px;
font-size: 13px;
color: #666;
border-top: 1px solid var(--el-border-color);
}
.demo-box {
background: #f5f7fa;
padding: 8px 12px;
margin: 6px 0;
border-radius: 4px;
}
.word-count {
text-align: right;
margin-top: 4px;
}
</style>

View File

@@ -0,0 +1,371 @@
<template>
<el-form-item
:label="label"
:required="required"
:show-message="false"
class="form-item-tip-wrapper"
v-bind="$attrs"
>
<!-- label插槽,支持父组件自定义label -->
<template #label>
<slot name="label">{{ label }}</slot>
</template>
<!-- 输入框模式 -->
<el-input
v-if="type === 'input'"
:model-value="modelValue"
class="inputItem"
v-bind="inputAttrs"
@input="$emit('update:modelValue', $event)"
>
<template #suffix>
<slot name="suffix"></slot>
</template>
</el-input>
<!-- 输入框模式 -->
<el-input
v-if="type === 'inputNumber'"
:model-value="modelValue"
class="inputItem"
type="number"
v-bind="inputAttrs"
@input="$emit('update:modelValue', $event)"
>
<template #suffix>
<slot name="suffix"></slot>
</template>
</el-input>
<!-- 文本域模式 -->
<el-input
v-else-if="type === 'textarea'"
:model-value="modelValue"
:rows="5"
class="inputItem"
type="textarea"
v-bind="inputAttrs"
@input="$emit('update:modelValue', $event)"
/>
<!-- 单选框模式(账号状态) -->
<el-radio-group
v-else-if="type === 'radio'"
:model-value="modelValue"
class="radioGroup"
@change="$emit('update:modelValue', $event)"
>
<el-radio
v-for="item in radioOptions"
:key="item.value"
:class="{
active: modelValue === item.value
}"
:label="item.value"
class="radio"
>{{ item.label }}
</el-radio>
</el-radio-group>
<!-- 单个日期-->
<el-date-picker
v-else-if="type === 'date'"
:model-value="modelValue"
format="YYYY-MM-DD"
placeholder="选择时间"
size="large"
style="width: 100%"
type="date"
value-format="YYYY-MM-DD"
@update:model-value="$emit('update:modelValue', $event)"
/>
<!-- 头像-->
<div v-else-if="type === 'uploadUserAvatar'" class="uploadUserAvatar">
<div class="img">
<el-image :src="imageUrl" style="width: 100%;height: 100%; object-fit: cover"></el-image>
</div>
<div>
<el-upload
ref="upload"
:auto-upload="false"
:before-upload="beforeAvatarUpload"
:limit="1"
:on-change="handleChange"
:on-exceed="handleExceed"
:show-file-list="false"
accept=".jpg,.png"
>
<template #trigger>
<div class="actionUpload">点击替换头像</div>
</template>
</el-upload>
<div class="tip">支持 jpg / png,建议 200×200,不超过 2MB</div>
</div>
</div>
<!-- 多选-->
<el-checkbox-group
v-else-if="type === 'checkBox'"
:model-value="modelValue"
style="width: 100%"
@update:model-value="$emit('update:modelValue', $event)"
>
<el-checkbox
v-for="item in radioOptions"
:key="item.value"
:label="item.label"
class="checkBox"
>
</el-checkbox>
</el-checkbox-group>
<div v-else-if="type === 'switch'" style="width: 100%">
<el-switch
:model-value="Boolean(modelValue)"
class="custom-switch"
style="margin-right: 12px;"
@change="$emit('update:modelValue', $event)"
/>
<slot name="switchRight"></slot>
</div>
<!-- 底部提示文字 -->
<div v-if="tip" class="form-item-tip-text">
{{ tip }}
</div>
</el-form-item>
</template>
<script lang="ts" setup>
import {ref} from 'vue'
import {genFileId} from 'element-plus'
import type {UploadInstance, UploadProps, UploadRawFile} from 'element-plus'
defineOptions({inheritAttrs: false})
const props = defineProps({
modelValue: {
type: [String, Number, Array] as PropType<string | number | Array<string | number>>,
default: ''
},
label: {
type: String,
default: ''
},
required: {
type: Boolean,
default: false
},
tip: {
type: String,
default: ''
},
type: {
type: String,
default: 'input'
},
inputAttrs: {
type: Object,
default: () => ({})
},
radioOptions: {
type: Array as PropType<Array<{ label: string, value: string | number }>>,
default: () => []
},
})
const emit = defineEmits(['update:modelValue', 'file-change'])
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 imageUrl = ref('')
if (props.type === 'uploadUserAvatar') {
const str = props.modelValue as string;
imageUrl.value = (str.trim() === '' ? defaultImag : props.modelValue as string)
console.log(imageUrl.value)
}
// 保存选中的文件,等表单提交时再传给后端
const avatarFile = ref<File | null>(null)
// 成功获取 临时地址
// ✅ on-change:选中文件立刻预览(重点!)
const handleChange: UploadProps['onChange'] = (uploadFile) => {
avatarFile.value = uploadFile.raw!
imageUrl.value = URL.createObjectURL(uploadFile.raw!)
emit('file-change', uploadFile.raw)
}
// 超出 覆盖
const handleExceed: UploadProps['onExceed'] = (files) => {
upload.value!.clearFiles()
const file = files[0] as UploadRawFile
file.uid = genFileId()
upload.value!.handleStart(file)
}
// 拦截格式
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
console.log(rawFile)
// 修复:jpg mime是 image/jpeg,不是 image/jpg
if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') {
ElMessage.error('只支持jpg,png格式图片')
return false
} else if (rawFile.size / 1024 / 1024 > 2) {
ElMessage.error('图片大小不能大于2MB')
return false
}
return true
}
</script>
<style lang="scss" scoped>
:deep(.el-checkbox) {
.el-checkbox__input {
.el-checkbox__inner {
width: 18px;
height: 18px;
&::after {
width: 10px;
height: 10px;
}
}
}
.el-checkbox__label {
color: rgba(107, 114, 128, 1) !important;
}
&.is-checked {
.el-checkbox__label {
color: #2563EB !important;
}
}
.el-checkbox__input.is-checked {
.el-checkbox__inner {
background-color: #2563EB !important
}
.el-checkbox__inner:after {
border-color: #2563EB !important
}
}
}
:deep(.el-radio) {
.el-radio__input {
.el-radio__inner {
width: 16px;
height: 16px;
border: 1px solid rgba(209, 213, 219, 1);
&::after {
width: 6px;
height: 6px;
}
}
&.is-checked {
.el-radio__inner {
border: 1px solid #2563eb;
background-color: #2563eb;
}
}
}
&.is-checked {
background-color: rgba(239, 246, 255, 1) !important;
.el-radio__label {
color: rgba(37, 99, 235, 1);
}
}
}
.checkBox {
padding: 14px 17px;
min-height: 44px;
height: 44px;
background-color: rgba(248, 250, 252, 1);
border: 1px solid rgba(226, 232, 240, 1);
&.is-checked {
background-color: rgba(239, 246, 255, 1);
border: 1px solid rgba(191, 219, 254, 1);
}
}
.form-item-tip-text {
font-size: 12px;
color: #909399;
margin-top: 4px;
line-height: 1.4;
}
.uploadUserAvatar {
width: 100%;
min-height: 90px;
border-radius: 12px 12px 12px 12px;
background-color: rgba(247, 249, 252, 1);
border: 1px solid rgba(196, 201, 209, 1);
display: flex;
align-items: center;
padding: 17px;
.img {
width: 57px;
height: 56px;
border-radius: 50%;
background-color: rgba(241, 245, 249, 1);
margin-right: 16px;
overflow: hidden;
}
.actionUpload {
line-height: 18px;
font-size: 13px;
text-align: left;
color: rgba(37, 99, 235, 1);
}
.tip {
margin-top: 4px;
line-height: 16px;
color: rgba(156, 163, 175, 1);
font-size: 12px;
text-align: left;
}
}
</style>
<style>
.inputItem {
min-height: 44px;
border-radius: 12px 12px 12px 12px;
background-color: rgba(247, 249, 252, 1);
border: 1px solid rgba(229, 231, 235, 1);
}
.radioGroup {
.radio {
display: flex;
align-items: center;
justify-content: center;
min-width: 80px;
height: 44px;
padding: 14px 21px;
border-radius: 12px 12px 12px 12px;
border: 1px solid rgba(229, 231, 235, 1);
background-color: rgba(247, 249, 252, 1);
&.active {
border-color: rgba(37, 99, 235, 1);
}
}
}
</style>

View File

@@ -45,7 +45,7 @@ import { isHttp } from '@/utils/validate';
// 顶部栏初始数
const visibleNumber = ref<number>(-1);
// 隐藏侧边栏路由
const hideList = ['/index', '/user/profile'];
const hideList = ['/index.vue', '/user/profile'];
const appStore = useAppStore();
const settingsStore = useSettingsStore();
@@ -99,7 +99,7 @@ const childrenMenus = computed(() => {
const activeMenu = computed(() => {
const {meta} = route;
let path = meta.activeMenu || route.path;
if (path === '/index' || route.path === '/index') {
if (path === '/index.vue' || route.path === '/index.vue') {
path = '/system/user';
}
let activePath = path;
@@ -214,8 +214,7 @@ onMounted(() => {
border-radius: 13px;
color: var(--topbar-pill-text) !important;
background: transparent !important;
transition:
background-color 0.2s ease,
transition: background-color 0.2s ease,
color 0.2s ease,
box-shadow 0.2s ease,
border-color 0.2s ease,
@@ -305,8 +304,7 @@ onMounted(() => {
border-radius: 12px;
color: var(--app-text-title) !important;
background: transparent !important;
transition:
background-color 0.2s ease,
transition: background-color 0.2s ease,
color 0.2s ease,
box-shadow 0.2s ease,
transform 0.2s ease;
@@ -365,8 +363,7 @@ html.dark .mix-topnav-menu.el-menu--horizontal {
html.dark .mix-topnav-popper .el-menu--popup {
border-color: rgba(71, 85, 105, 0.34);
background: linear-gradient(180deg, rgba(15, 23, 42, 0.98), rgba(30, 41, 59, 0.94)) !important;
box-shadow:
0 20px 42px rgba(0, 0, 0, 0.34),
box-shadow: 0 20px 42px rgba(0, 0, 0, 0.34),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
}

View File

@@ -0,0 +1,65 @@
<script lang="ts" name="cardNav" setup>
const props = defineProps({
title: {
type: String,
required: true
},
subTitle: {
type: String,
required: false
},
lineColor: {
type: String,
required: false,
default: '#2563eb'
},
})
</script>
<template>
<div class="cardNav">
<div :style="{
backgroundColor: props.lineColor
}" class="line"></div>
<div class="content">
<div class="title">{{ props.title }}</div>
<div v-if="props.subTitle" class="subTitle">{{ props.subTitle }}</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.cardNav {
width: 100%;
display: flex;
align-items: center;
.line {
width: 7px;
height: 20px;
border-radius: 10px;
background-color: rgba(37, 99, 235, 1);
margin-right: 10px;
}
.content {
.title {
height: 22px;
line-height: 22px;
background-color: rgba(0, 0, 0, 0);
color: rgba(17, 24, 39, 1);
font-size: 16px;
text-align: left;
}
.subTitle {
height: 16px;
line-height: 16px;
background-color: rgba(0, 0, 0, 0);
color: rgba(156, 163, 175, 1);
font-size: 12px;
}
}
}
</style>

View File

@@ -0,0 +1,43 @@
<script lang="ts" name="tableTag" setup>
const props = defineProps({
type: {
type: String as PropType<"" | "default" | "text" | "primary" | "success" | "warning" | "info" | "danger">,
default: 'info'
},
circleColor: {
type: String,
default: '#2563eb',
},
plain: {
type: Boolean,
default: true,
},
num: String,
numColor: {
type: String,
default: '#111827',
},
title: String,
})
</script>
<template>
<el-button :plain="props.plain" :type="props.type">
<i :style="{
backgroundColor: props.circleColor,
}" class="circle"></i>
<span style="margin:0 8px;color: rgba(75, 85, 99, 1);">{{ props.title }}</span>
<span :style="{
color: props.numColor,
}" style="font-weight: bold">{{ props.num }}</span>
</el-button>
</template>
<style lang="scss" scoped>
.circle {
width: 7px;
height: 6px;
border-radius: 3px;
background-color: rgba(124, 58, 237, 1)
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<section class="app-main">
<section class="app-main beautify_scroll">
<router-view v-slot="{ Component, route }">
<transition :enter-active-class="animate" mode="out-in">
<keep-alive :include="tagsViewStore.cachedViews">
@@ -50,10 +50,11 @@ function addIframe() {
<style lang="scss" scoped>
.app-main {
min-height: 100vh;
height: 100%;
width: 100%;
position: relative;
overflow: hidden;
//padding: 12px;
overflow: auto;
padding: 0 12px;
&:fullscreen,
&:-webkit-full-screen,

View File

@@ -9,7 +9,7 @@
>
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
<!-- <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" />-->
<svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon) || '' "/>
<template #title>
<span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span>
</template>

View File

@@ -181,7 +181,7 @@ const isFirstView = () => {
return false;
}
try {
return tag.fullPath === '/index' || tag.fullPath === visitedViews.value[1]?.fullPath;
return tag.fullPath === '/index.vue' || tag.fullPath === visitedViews.value[1]?.fullPath;
} catch {
return false;
}
@@ -313,7 +313,8 @@ const closeOthersTags = () => {
if (!tag) {
return;
}
router.push(tag.fullPath || tag.path || '/').catch(() => {});
router.push(tag.fullPath || tag.path || '/').catch(() => {
});
tab.closeOtherPage(tag).then(() => {
moveToCurrentTag();
});
@@ -505,8 +506,7 @@ onBeforeUnmount(() => {
background-color: var(--app-surface-bg);
border: 1px solid var(--app-surface-border);
border-radius: var(--app-radius-md);
transition:
box-shadow 0.2s ease,
transition: box-shadow 0.2s ease,
transform 0.2s ease,
border-color 0.2s ease,
color 0.2s ease;
@@ -549,8 +549,7 @@ onBeforeUnmount(() => {
margin-left: 5px;
margin-top: 4px;
border-radius: var(--app-radius-md);
transition:
box-shadow 0.2s ease,
transition: box-shadow 0.2s ease,
transform 0.2s ease,
border-color 0.2s ease,
color 0.2s ease;
@@ -620,8 +619,7 @@ onBeforeUnmount(() => {
background-color: var(--app-surface-bg);
border: 1px solid var(--app-surface-border);
border-radius: var(--app-radius-md);
transition:
box-shadow 0.2s ease,
transition: box-shadow 0.2s ease,
transform 0.2s ease,
border-color 0.2s ease,
color 0.2s ease;

View File

@@ -111,7 +111,7 @@ const setLayout = () => {
display: flex;
flex-direction: column;
gap: 3px;
//padding: 12px 12px 0;
padding: 0 12px;
background: transparent;
}

View File

@@ -65,10 +65,10 @@ export const constantRoutes: RouteRecordRaw[] = [
{
path: '',
component: Layout,
redirect: '/index',
redirect: '/index.vue',
children: [
{
path: '/index',
path: '/index.vue',
component: () => import('@/views/index/index.vue'),
name: 'Index',
meta: {title: '首页', icon: 'dashboard', affix: true}

View File

@@ -0,0 +1,438 @@
<template>
<div class="p-2 page-shell system-agreement-page">
<div class="search-wrap">
<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">
<el-form-item label="协议编号" prop="agreementNo">
<el-input v-model="queryParams.agreementNo" clearable placeholder="请输入协议编号"
@keyup.enter="handleQuery"/>
</el-form-item>
<el-form-item label="类型" prop="agreementType">
<el-select v-model="queryParams.agreementType" clearable placeholder="请选择类型">
<el-option label="请选择字典生成" value=""/>
</el-select>
</el-form-item>
<el-form-item label="协议名称" prop="name">
<el-input v-model="queryParams.name" clearable placeholder="请输入协议名称" @keyup.enter="handleQuery"/>
</el-form-item>
<el-form-item label="版本号" prop="versionName">
<el-input v-model="queryParams.versionName" clearable placeholder="请输入版本号"
@keyup.enter="handleQuery"/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" clearable placeholder="请选择状态">
<el-option :value="true" label="是"/>
<el-option :value="false" label="否"/>
</el-select>
</el-form-item>
<el-form-item label="生效时间" style="width: 308px">
<el-date-picker
v-model="dateRangePublishTime"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
end-placeholder="结束日期"
range-separator="-"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item label="创建部门" prop="createDept">
<el-input-number v-model="queryParams.createDept" controls-position="right"/>
</el-form-item>
<el-form-item label="创建者" prop="createBy">
<el-input-number v-model="queryParams.createBy" controls-position="right"/>
</el-form-item>
<el-form-item label="创建时间" style="width: 308px">
<el-date-picker
v-model="dateRangeCreateTime"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
end-placeholder="结束日期"
range-separator="-"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item label="更新者" prop="updateBy">
<el-input-number v-model="queryParams.updateBy" controls-position="right"/>
</el-form-item>
<el-form-item label="更新时间" style="width: 308px">
<el-date-picker
v-model="dateRangeUpdateTime"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
end-placeholder="结束日期"
range-separator="-"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item label="已生效类型;同类型仅一条,非生效或已删除为 NULL" prop="activeType">
<el-select v-model="queryParams.activeType"
clearable placeholder="请选择已生效类型;同类型仅一条,非生效或已删除为 NULL">
<el-option label="请选择字典生成" value=""/>
</el-select>
</el-form-item>
<el-form-item>
<el-button icon="Search" type="primary" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<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:agreement:add']" icon="Plus" plain type="primary" @click="handleAdd">新增
</el-button>
<el-button v-hasPermi="['system:agreement:edit']" :disabled="single" icon="Edit" plain type="success"
@click="handleUpdate()">修改
</el-button>
<el-button v-hasPermi="['system:agreement:remove']" :disabled="multiple" icon="Delete" plain type="danger"
@click="handleDelete()">删除
</el-button>
<el-button v-hasPermi="['system:agreement: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="agreementList" border class="data-table"
@selection-change="handleSelectionChange">
<el-table-column align="center" type="selection" width="55"/>
<el-table-column align="center" label="主键" prop="agreementId"/>
<el-table-column align="center" label="协议编号" prop="agreementNo"/>
<el-table-column align="center" label="类型" prop="agreementType"/>
<el-table-column align="center" label="协议名称" prop="name"/>
<el-table-column align="center" label="版本号" prop="versionName"/>
<el-table-column align="center" label="协议正文" prop="content"/>
<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="生效时间" prop="publishTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.publishTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column align="center" label="已生效类型;同类型仅一条,非生效或已删除为 NULL" prop="activeType"/>
<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:agreement:edit']" icon="Edit" link type="primary"
@click="handleUpdate(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button v-hasPermi="['system:agreement: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="500px">
<el-form ref="agreementFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="协议编号" prop="agreementNo">
<el-input v-model="form.agreementNo" placeholder="请输入协议编号"/>
</el-form-item>
<el-form-item label="类型" prop="agreementType">
<el-select v-model="form.agreementType" placeholder="请选择类型">
<el-option label="请选择字典生成" value=""/>
</el-select>
</el-form-item>
<el-form-item label="协议名称" prop="name">
<el-input v-model="form.name" placeholder="请输入协议名称"/>
</el-form-item>
<el-form-item label="版本号" prop="versionName">
<el-input v-model="form.versionName" placeholder="请输入版本号"/>
</el-form-item>
<el-form-item label="协议正文">
<editor v-model="form.content" :min-height="192"/>
</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="publishTime">
<el-date-picker v-model="form.publishTime"
clearable
placeholder="请选择生效时间"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss">
</el-date-picker>
</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="已生效类型;同类型仅一条,非生效或已删除为 NULL" prop="activeType">
<el-select v-model="form.activeType" placeholder="请选择已生效类型;同类型仅一条,非生效或已删除为 NULL">
<el-option label="请选择字典生成" value=""/>
</el-select>
</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="Agreement" setup>
import {
addAgreement,
delAgreement,
getAgreement,
listAgreement,
updateAgreement
} from '@/api/system/agreement';
import {AgreementForm, AgreementQuery, AgreementVO} from '@/api/system/agreement/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 agreementList = ref<AgreementVO[]>([]);
const buttonLoading = ref(false);
const {loading, withLoading} = useLoading(true);
const {showSearch} = useSearchToggle();
const total = ref(0);
const {
dateRange: dateRangePublishTime,
applyDateRange: applyPublishTimeDateRange,
resetDateRange: resetPublishTimeDateRange
} = useDateRangeQuery('PublishTime');
const {
dateRange: dateRangeCreateTime,
applyDateRange: applyCreateTimeDateRange,
resetDateRange: resetCreateTimeDateRange
} = useDateRangeQuery('CreateTime');
const {
dateRange: dateRangeUpdateTime,
applyDateRange: applyUpdateTimeDateRange,
resetDateRange: resetUpdateTimeDateRange
} = useDateRangeQuery('UpdateTime');
const queryFormRef = ref<ElFormInstance>();
const agreementFormRef = ref<ElFormInstance>();
const initFormData: AgreementForm = {
agreementId: undefined,
agreementNo: undefined,
agreementType: undefined,
name: undefined,
versionName: undefined,
content: undefined,
status: undefined,
publishTime: undefined,
createDept: undefined,
createBy: undefined,
createTime: undefined,
updateBy: undefined,
updateTime: undefined,
activeType: undefined,
}
const data = reactive<PageData<AgreementForm, AgreementQuery>>({
form: {...initFormData},
queryParams: {
pageNum: 1,
pageSize: 10,
agreementNo: undefined,
agreementType: undefined,
name: undefined,
versionName: undefined,
content: undefined,
status: undefined,
createDept: undefined,
createBy: undefined,
updateBy: undefined,
activeType: undefined,
params: {
publishTime: undefined,
createTime: undefined,
updateTime: undefined,
}
},
rules: {
agreementId: [
{required: true, message: "主键不能为空", trigger: "change"}
],
agreementNo: [
{required: true, message: "协议编号不能为空", trigger: "blur"}
],
agreementType: [
{required: true, message: "类型不能为空", trigger: "change"}
],
name: [
{required: true, message: "协议名称不能为空", trigger: "blur"}
],
versionName: [
{required: true, message: "版本号不能为空", trigger: "blur"}
],
status: [
{required: true, message: "状态不能为空", trigger: "change"}
],
}
});
const {queryParams, form, rules} = toRefs(data);
const {ids, single, multiple, handleSelectionChange} = useTableSelection<AgreementVO>(item => item.agreementId);
const {dialog, resetForm: reset, openDialog, showDialog, closeDialog} = useFormDialog({
form,
formRef: agreementFormRef,
initialFormData: initFormData
});
/** 查询协议配置列表 */
const getList = async () => {
await withLoading(async () => {
let params = queryParams.value;
params = applyPublishTimeDateRange(params);
params = applyCreateTimeDateRange(params);
params = applyUpdateTimeDateRange(params);
const res = await listAgreement(params);
agreementList.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: () => {
resetPublishTimeDateRange();
resetCreateTimeDateRange();
resetUpdateTimeDateRange();
},
afterReset: () => {
handleQuery();
}
});
/** 新增按钮操作 */
const handleAdd = () => {
openDialog('添加协议配置');
};
/** 修改按钮操作 */
const handleUpdate = async (row?: Partial<AgreementVO>) => {
reset();
const _agreementId = row?.agreementId || ids.value[0];
const res = await getAgreement(_agreementId);
Object.assign(form.value, res.data);
showDialog('修改协议配置');
};
/** 提交按钮 */
const submitForm = () => {
agreementFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.agreementId) {
await updateAgreement(form.value).finally(() => (buttonLoading.value = false));
} else {
await addAgreement(form.value).finally(() => (buttonLoading.value = false));
}
modal.msgSuccess('操作成功');
closeDialog();
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: Partial<AgreementVO>) => {
const _agreementIds = row?.agreementId || ids.value;
await modal.confirm('是否确认删除协议配置编号为"' + _agreementIds + '"的数据项?');
await delAgreement(_agreementIds);
modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
requestDownload(
'system/agreement/export',
{
...queryParams.value
},
`agreement_${new Date().getTime()}.xlsx`
);
};
onMounted(() => {
getList();
});
</script>

View File

@@ -0,0 +1,229 @@
<template>
<div class="p-2 app-container monitor-online-page">
<Pagecrumb></Pagecrumb>
<div class="search-wrap">
<el-card class="search-panel" shadow="hover">
<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="query-form" label-position="top">
<el-form-item label="会话ID" prop="sessionNo">
<el-input v-model="queryParams.sessionNo" clearable placeholder="请输入会话编号" size="large"
style="width: 150px" @keyup.enter="handleQuery"/>
</el-form-item>
<el-form-item label="用户手机号" prop="ipaddr">
<el-input v-model="queryParams.ipaddr" clearable placeholder="请输入用户手机号" size="large"
style="width: 150px" @keyup.enter="handleQuery"/>
</el-form-item>
<el-form-item label="会话状态" prop="status">
<el-select
v-model="queryParams.status"
:empty-values="[null, undefined]"
:value-on-clear="null"
clearable
placeholder="请选择会话状态"
size="large"
style="width: 150px"
>
<el-option label="机器人" value="0"></el-option>
<el-option label="排队" value="1"></el-option>
<el-option label="人工" value="2"></el-option>
<el-option label="已结束" value="3"></el-option>
</el-select>
</el-form-item>
<el-form-item label="问题类型" prop="problemType">
<el-select
v-model="queryParams.problemType"
:empty-values="[null, undefined]"
:value-on-clear="null"
clearable
placeholder="请选择问题类型"
size="large"
style="width: 150px"
>
<el-option label="在线" value="0"></el-option>
<el-option label="离线" value="1"></el-option>
</el-select>
</el-form-item>
<el-form-item label="关联设备" prop="deviceId">
<el-input v-model="queryParams.deviceId" clearable placeholder="请输入MAC/设备名称" size="large"
style="width: 150px" @keyup.enter="handleQuery"/>
</el-form-item>
<el-form-item label="会话创建时间" prop="startTime">
<el-date-picker
v-model="queryParams.startTime"
end-placeholder="结束日期"
range-separator="-"
single-panel
size="large"
start-placeholder="开始日期"
style="width: 220px"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item label="接待客服" prop="agentId">
<el-select
v-model="queryParams.agentId"
:empty-values="[null, undefined]"
:value-on-clear="null"
clearable
size="large"
style="width: 150px"
>
<el-option label="在线" value="0"></el-option>
<el-option label="离线" value="1"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button size="large" type="primary" @click="handleQuery">搜索</el-button>
<el-button size="large" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<el-card class="table-panel" shadow="hover">
<template #header>
<div class="flex justify-between">
<div>
<el-button type="primary">
<span style="margin:0 8px;color: #fff;">全部</span>
<span style="color: #fff;font-weight: bold">1286</span>
</el-button>
<tableTag circle-color="#7c3aed" num="862" title="机器人接待"></tableTag>
<tableTag circle-color="#ea580c" num="7" title="排队等待"></tableTag>
<tableTag num="12" title="人工接待"></tableTag>
<tableTag circle-color="#94a3b8" num="405" title="已结束"></tableTag>
</div>
<div>
<el-button plain type="info">批量导出</el-button>
<el-button plain type="info">刷新</el-button>
</div>
</div>
</template>
<el-table
v-loading="loading"
:data="onlineList"
border
class="data-table"
style="width: 100%"
>
<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="用户信息" prop="userName"/>
<el-table-column :show-overflow-tooltip="true" align="center" label="关联设备" prop="clientKey"/>
<el-table-column align="center" label="会话状态">
<template #default="scope">
<el-tag type="primary">在线</el-tag>
<el-tag type="danger">离线</el-tag>
</template>
</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="ipaddr"/>
<el-table-column :show-overflow-tooltip="true" align="center" label="排队序号" prop="loginLocation"/>
<el-table-column :show-overflow-tooltip="true" align="center" label="等待时长" prop="os"/>
<el-table-column :show-overflow-tooltip="true" align="center" label="最后消息时间" prop="browser"/>
<el-table-column align="center" class-name="small-padding fixed-width" label="操作" min-width="200">
<template #default="scope">
<div>
<el-button
v-hasPermi="['monitor:online:forceLogout']"
type="primary"
@click="handleMain(scope.row)"
>进入会话
</el-button>
<el-button
v-hasPermi="['monitor:online:forceLogout']"
plain
type="info"
@click="handleMain(scope.row)"
>用户档案
</el-button>
<el-button
v-hasPermi="['monitor:online:forceLogout']"
plain
type="danger"
@click="handleMain(scope.row)"
>关闭会话
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNum"
:total="total"
/>
</el-card>
</div>
</template>
<script lang="ts" name="Online" setup>
import {to} from 'await-to-js';
import {listCsSession as initData} from '@/api/system/conversation/index';
import {CsSessionQuery, CsSessionVO} from '@/api/system/conversation/types';
import modal from '@/plugins/modal';
import {useDict} from '@/utils/dict';
import {parseTime} from '@/utils/ruoyi';
const {sys_device_type} = toRefs<any>(useDict('sys_device_type'));
const router = useRouter();
const onlineList = ref<CsSessionVO[]>([]);
const loading = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const queryParams = ref<CsSessionQuery>({
pageNum: 1,
pageSize: 10,
ipaddr: '',
userName: ''
});
/** 查询登录日志列表 */
const getList = async () => {
loading.value = true;
const res = await initData(queryParams.value);
onlineList.value = res.data?.rows;
total.value = res.data?.total;
loading.value = false;
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
function handleMain(row) {
console.log(row);
router.push({
path: '/conversation/conversationMain'
})
}
onMounted(() => {
getList();
});
</script>
<style lang="scss" scoped>
@use '@/assets/styles/components/page-shell' as pageShell;
@include pageShell.table-crud-page;
.circle {
width: 7px;
height: 6px;
border-radius: 3px;
background-color: rgba(124, 58, 237, 1)
}
</style>

View File

View File

@@ -0,0 +1,306 @@
<script lang="ts" setup>
import FormItemTip from "@/components/FormItemTip/index.vue";
import cskbEditor from '@/components/Editor/cskb.vue'
const router = useRouter();
const form = ref({
phone: "",
username: "",
sex: 1,
creatDay: '',
status: 1,
quickAction: [],
img: ''
})
function handleBack() {
router.replace({
path: "/conversation/cskbList"
})
}
function submit() {
console.log(form.value)
}
</script>
<template>
<div class="edit">
<div class="flex justify-between">
<pagecrumb></pagecrumb>
<el-button plain type="info" @click="handleBack"> 返回客服列表</el-button>
</div>
<div class="tipBox">
<div class="tip1">新增的知识条目保存后需要「启用」才会在 APP 端展示
</div>
<div class="tip2">回复内容支持有序步骤与图片,建议控制在 200 字以内,便于用户快速阅读。
</div>
</div>
<!-- form -->
<section class="commonCard">
<div class="flex justify-between flex-items-center">
<card-nav subTitle="配置问题标题、分类与标准回复话术" title="新增知识条目"></card-nav>
<div class="unSave">
<i class="square"></i>
<span>未保存</span>
</div>
</div>
<div class="formbox">
<el-form label-position="top">
<div class="title">
基础信息
</div>
<el-row :gutter="24">
<el-col :span="12">
<FormItemTip
v-model="form.phone"
:inputAttrs="{ placeholder: '请输入问题标题,如「连不上 Wi-Fi」', showWordLimit:true ,maxlength:30}"
:required="true"
label="问题标题"
prop="phone"
tip="建议使用用户口语化问法,便于模糊匹配"
>
</FormItemTip>
</el-col>
<el-col :span="12">
<FormItemTip
v-model="form.username"
:inputAttrs="{ placeholder: '请选择问题分类'}"
:required="true"
label="问题分类"
prop="username"
>
</FormItemTip>
</el-col>
</el-row>
<el-row :gutter="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
v-model="form.status"
:inputAttrs="{ placeholder: '', disabled: false }"
:radioOptions="[
{label:'启用',value:1},
{label:'禁用',value:0},
]"
label="账号状态"
prop="status"
tip="禁用后该客服无法登录工作台,其接待中会话将转回排队"
type="radio"
>
</FormItemTip>
</el-col>
</el-row>
<div class="title">
关联与排序
</div>
<el-row :gutter="24">
<el-col :span="12">
<FormItemTip
v-model="form.quickAction"
:radio-options="[
{label: '已解决',value:1},
{label: '转人工',value:0}
]"
label="关联快捷按钮"
prop="phone"
tip="回复气泡下方展示的引导按钮,可多选"
type="checkBox"
>
</FormItemTip>
</el-col>
<el-col :span="12">
<FormItemTip
v-model="form.username"
:inputAttrs="{ placeholder: '请输入排序'}"
:required="true"
label="排序号"
prop="username"
tip="决定该条目在 APP 常见问题列表中的展示顺序"
type="inputNumber"
>
<template #suffix>越小越靠前</template>
</FormItemTip>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<FormItemTip
label="启用状态"
tip="关闭则保存为草稿,仅后台可见"
type="switch"
>
<template #switchRight>
<span
style="color: rgba(17, 24, 39, 1);font-size: 13px;font-weight: bolder">启用后立即在APP端展示</span>
</template>
</FormItemTip>
</el-col>
</el-row>
</el-form>
</div>
</section>
<div class="footer">
<div class="footer-tip">标题、分类与回复内容为必填项;排序号越小展示越靠前</div>
<div>
<el-button>取消</el-button>
<el-button plain type="primary" @click="submit">保存并继续新增</el-button>
<el-button type="primary" @click="submit">保存并启用</el-button>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.edit {
overflow: auto;
padding-bottom: 50px;
.tipBox {
margin-top: 16px;
background-color: rgba(239, 246, 255, 1);
border: 1px solid rgba(219, 234, 254, 1);
padding: 13px 53px;
.tip1 {
line-height: 18px;
color: rgba(30, 64, 175, 1);
font-size: 13px;
}
.tip2 {
line-height: 16px;
color: rgba(59, 130, 246, 1);
font-size: 12px;
}
}
.customerInfoBox {
margin-top: 16px;
.infoBox {
.customer-Img {
width: 73px;
height: 72px;
border-radius: 50%;
margin-right: 24px;
}
.userName {
height: 26px;
line-height: 26px;
font-weight: bold;
color: rgba(17, 24, 39, 1);
font-size: 22px;
}
.circle {
display: block;
width: 7px;
height: 6px;
border-radius: 3px;
margin-right: 6px;
background-color: rgba(22, 163, 74, 1);
}
}
.subInfoBox {
line-height: 20px;
color: rgba(107, 114, 128, 1);
font-size: 13px;
text-align: left;
.dat {
margin: 0 1px;
}
}
}
.subTitle {
line-height: 14px;
color: rgba(156, 163, 175, 1);
font-size: 12px;
text-align: left;
}
.unSave {
display: flex;
align-items: center;
justify-content: center;
width: 79px;
height: 28px;
background-color: rgba(248, 250, 252, 1);
border: 1px solid rgba(241, 245, 249, 1);
.square {
display: block;
width: 8px;
height: 6px;
margin-right: 8px;
background-color: rgba(148, 163, 184, 1);
}
span {
height: 14px;
line-height: 14px;
color: rgba(107, 114, 128, 1);
font-size: 12px;
}
}
.formbox {
margin-top: 46px;
.title {
line-height: 16px;
color: rgba(55, 65, 81, 1);
font-size: 13px;
text-align: left;
margin: 24px 0;
font-weight: bold;
}
}
.footer {
margin-top: 27px;
text-align: right;
display: flex;
align-items: center;
justify-content: space-between;
.footer-tip {
line-height: 14px;
color: rgba(107, 114, 128, 1);
font-size: 12px;
text-align: left;
}
&:deep(.el-button) {
padding: 14px 25px;
min-height: 44px;
border-radius: 0;
border: 1px solid rgba(226, 232, 240, 1);
&.el-button--primary.is-plain {
color: #2563EB;
}
}
.el-button + .el-button {
margin-left: 21px;
}
}
}
</style>

View File

@@ -0,0 +1,306 @@
<script lang="ts" setup>
import FormItemTip from "@/components/FormItemTip/index.vue";
import cskbEditor from '@/components/Editor/cskb.vue'
const router = useRouter();
const form = ref({
phone: "",
username: "",
sex: 1,
creatDay: '',
status: 1,
quickAction: [],
img: ''
})
function handleBack() {
router.replace({
path: "/conversation/cskbList"
})
}
function submit() {
console.log(form.value)
}
</script>
<template>
<div class="edit">
<div class="flex justify-between">
<pagecrumb></pagecrumb>
<el-button plain type="info" @click="handleBack"> 返回客服列表</el-button>
</div>
<div class="tipBox">
<div class="tip1">新增的知识条目保存后需要「启用」才会在 APP 端展示
</div>
<div class="tip2">回复内容支持有序步骤与图片,建议控制在 200 字以内,便于用户快速阅读。
</div>
</div>
<!-- form -->
<section class="commonCard">
<div class="flex justify-between flex-items-center">
<card-nav subTitle="配置问题标题、分类与标准回复话术" title="新增知识条目"></card-nav>
<div class="unSave">
<i class="square"></i>
<span>未保存</span>
</div>
</div>
<div class="formbox">
<el-form label-position="top">
<div class="title">
基础信息
</div>
<el-row :gutter="24">
<el-col :span="12">
<FormItemTip
v-model="form.phone"
:inputAttrs="{ placeholder: '请输入问题标题,如「连不上 Wi-Fi」', showWordLimit:true ,maxlength:30}"
:required="true"
label="问题标题"
prop="phone"
tip="建议使用用户口语化问法,便于模糊匹配"
>
</FormItemTip>
</el-col>
<el-col :span="12">
<FormItemTip
v-model="form.username"
:inputAttrs="{ placeholder: '请选择问题分类'}"
:required="true"
label="问题分类"
prop="username"
>
</FormItemTip>
</el-col>
</el-row>
<el-row :gutter="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
v-model="form.status"
:inputAttrs="{ placeholder: '', disabled: false }"
:radioOptions="[
{label:'启用',value:1},
{label:'禁用',value:0},
]"
label="账号状态"
prop="status"
tip="禁用后该客服无法登录工作台,其接待中会话将转回排队"
type="radio"
>
</FormItemTip>
</el-col>
</el-row>
<div class="title">
关联与排序
</div>
<el-row :gutter="24">
<el-col :span="12">
<FormItemTip
v-model="form.quickAction"
:radio-options="[
{label: '已解决',value:1},
{label: '转人工',value:0}
]"
label="关联快捷按钮"
prop="phone"
tip="回复气泡下方展示的引导按钮,可多选"
type="checkBox"
>
</FormItemTip>
</el-col>
<el-col :span="12">
<FormItemTip
v-model="form.username"
:inputAttrs="{ placeholder: '请输入排序'}"
:required="true"
label="排序号"
prop="username"
tip="决定该条目在 APP 常见问题列表中的展示顺序"
type="inputNumber"
>
<template #suffix>越小越靠前</template>
</FormItemTip>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<FormItemTip
label="启用状态"
tip="关闭则保存为草稿,仅后台可见"
type="switch"
>
<template #switchRight>
<span
style="color: rgba(17, 24, 39, 1);font-size: 13px;font-weight: bolder">启用后立即在APP端展示</span>
</template>
</FormItemTip>
</el-col>
</el-row>
</el-form>
</div>
</section>
<div class="footer">
<div class="footer-tip">标题、分类与回复内容为必填项;排序号越小展示越靠前</div>
<div>
<el-button>取消</el-button>
<el-button plain type="primary" @click="submit">保存并继续新增</el-button>
<el-button type="primary" @click="submit">保存并启用</el-button>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.edit {
overflow: auto;
padding-bottom: 50px;
.tipBox {
margin-top: 16px;
background-color: rgba(239, 246, 255, 1);
border: 1px solid rgba(219, 234, 254, 1);
padding: 13px 53px;
.tip1 {
line-height: 18px;
color: rgba(30, 64, 175, 1);
font-size: 13px;
}
.tip2 {
line-height: 16px;
color: rgba(59, 130, 246, 1);
font-size: 12px;
}
}
.customerInfoBox {
margin-top: 16px;
.infoBox {
.customer-Img {
width: 73px;
height: 72px;
border-radius: 50%;
margin-right: 24px;
}
.userName {
height: 26px;
line-height: 26px;
font-weight: bold;
color: rgba(17, 24, 39, 1);
font-size: 22px;
}
.circle {
display: block;
width: 7px;
height: 6px;
border-radius: 3px;
margin-right: 6px;
background-color: rgba(22, 163, 74, 1);
}
}
.subInfoBox {
line-height: 20px;
color: rgba(107, 114, 128, 1);
font-size: 13px;
text-align: left;
.dat {
margin: 0 1px;
}
}
}
.subTitle {
line-height: 14px;
color: rgba(156, 163, 175, 1);
font-size: 12px;
text-align: left;
}
.unSave {
display: flex;
align-items: center;
justify-content: center;
width: 79px;
height: 28px;
background-color: rgba(248, 250, 252, 1);
border: 1px solid rgba(241, 245, 249, 1);
.square {
display: block;
width: 8px;
height: 6px;
margin-right: 8px;
background-color: rgba(148, 163, 184, 1);
}
span {
height: 14px;
line-height: 14px;
color: rgba(107, 114, 128, 1);
font-size: 12px;
}
}
.formbox {
margin-top: 46px;
.title {
line-height: 16px;
color: rgba(55, 65, 81, 1);
font-size: 13px;
text-align: left;
margin: 24px 0;
font-weight: bold;
}
}
.footer {
margin-top: 27px;
text-align: right;
display: flex;
align-items: center;
justify-content: space-between;
.footer-tip {
line-height: 14px;
color: rgba(107, 114, 128, 1);
font-size: 12px;
text-align: left;
}
&:deep(.el-button) {
padding: 14px 25px;
min-height: 44px;
border-radius: 0;
border: 1px solid rgba(226, 232, 240, 1);
&.el-button--primary.is-plain {
color: #2563EB;
}
}
.el-button + .el-button {
margin-left: 21px;
}
}
}
</style>

View File

@@ -0,0 +1,359 @@
<template>
<div class="p-2 page-shell system-csKb-page">
<div class="search-wrap">
<pagecrumb></pagecrumb>
<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-item label="问题标题" prop="title">
<el-input v-model="queryParams.title" clearable placeholder="请输入问题标题" size="large"
@keyup.enter="handleQuery"/>
</el-form-item>
<el-form-item label="分类" prop="category">
<el-input v-model="queryParams.category" clearable placeholder="请输入分类" size="large"
@keyup.enter="handleQuery"/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" clearable placeholder="请选择状态" size="large">
<el-option :value="true" label="是"/>
<el-option :value="false" label="否"/>
</el-select>
</el-form-item>
<el-form-item>
<el-button size="large" type="primary" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" size="large" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<el-card class="table-panel" shadow="hover">
<template #header>
<div class="toolbar-shell">
<div>
<el-button type="primary">
<span style="margin:0 8px;color: #fff;">全部</span>
<span style="color: #fff;font-weight: bold">86</span>
</el-button>
<tableTag circle-color="#16A34A" num="78" title="启用"></tableTag>
<tableTag circle-color="#ef4444" num="8" num-color="#DC2626" title="禁用"></tableTag>
</div>
<div class="toolbar-actions">
<el-button :disabled="multiple" plain size="large" type="info" @click="handleDelete()">批量删除
</el-button>
<el-button size="large" type="primary" @click="handleAdd">新增问题
</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="csKbList" border class="data-table"
@selection-change="handleSelectionChange">
<el-table-column align="center" type="selection" width="55"/>
<el-table-column align="center" label="ID" prop="kbId"/>
<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">
<el-switch
v-model="scope.row.status"
:active-value="true"
:inactive-value="false"
disabled
/>
</template>
</el-table-column>
<el-table-column align="center" class-name="small-padding fixed-width" label="操作">
<template #default="scope">
<el-button v-hasPermi="['system:cskb:edit']" link type="primary"
@click="handleUpdate(scope.row)">上移
</el-button>
<el-button v-hasPermi="['system:cskb:edit']" link type="primary"
@click="handleUpdate(scope.row)">下移
</el-button>
<el-button v-hasPermi="['system:cskb:edit']" link type="primary"
@click="handleUpdate(scope.row)">编辑
</el-button>
<el-button v-hasPermi="['system:cskb:edit']" link type="primary"
@click="handleUpdate(scope.row)">禁用
</el-button>
<el-button v-hasPermi="['system:cskb:remove']" link type="primary"
@click="handleDelete(scope.row)">删除
</el-button>
</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="500px">
<el-form ref="csKbFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="知识编号,如 KB-001" prop="kbNo">
<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>
</div>
</template>
<script lang="ts" name="CsKb" setup>
import {
addCsKb,
delCsKb,
getCsKb,
listCsKb,
updateCsKb
} from '@/api/system/csKb';
import {CsKbForm, CsKbQuery, CsKbVO} from '@/api/system/csKb/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 modal from '@/plugins/modal';
import {download as requestDownload} from '@/utils/request';
const csKbList = ref<CsKbVO[]>([]);
const buttonLoading = ref(false);
const {loading, withLoading} = useLoading(true);
const {showSearch} = useSearchToggle();
const total = ref(0);
const {
dateRange: dateRangeCreateTime,
applyDateRange: applyCreateTimeDateRange,
resetDateRange: resetCreateTimeDateRange
} = useDateRangeQuery('CreateTime');
const {
dateRange: dateRangeUpdateTime,
applyDateRange: applyUpdateTimeDateRange,
resetDateRange: resetUpdateTimeDateRange
} = useDateRangeQuery('UpdateTime');
const queryFormRef = ref<ElFormInstance>();
const csKbFormRef = ref<ElFormInstance>();
const initFormData: CsKbForm = {
kbId: undefined,
kbNo: undefined,
title: undefined,
category: undefined,
content: undefined,
sort: undefined,
status: undefined,
createDept: undefined,
createBy: undefined,
createTime: undefined,
updateBy: undefined,
updateTime: undefined,
}
const data = reactive<PageData<CsKbForm, CsKbQuery>>({
form: {...initFormData},
queryParams: {
pageNum: 1,
pageSize: 10,
kbNo: undefined,
title: undefined,
category: undefined,
content: undefined,
sort: undefined,
status: undefined,
createDept: undefined,
createBy: undefined,
updateBy: undefined,
params: {
createTime: undefined,
updateTime: undefined,
}
},
rules: {
kbId: [
{required: true, message: "主键不能为空", trigger: "change"}
],
kbNo: [
{required: true, message: "知识编号,如 KB-001不能为空", trigger: "blur"}
],
title: [
{required: true, message: "问题标题不能为空", trigger: "blur"}
],
category: [
{required: true, message: "分类不能为空", trigger: "blur"}
],
sort: [
{required: true, message: "排序权重,数字越小越靠前不能为空", trigger: "change"}
],
status: [
{required: true, message: "状态不能为空", trigger: "change"}
],
}
});
const {queryParams, form, rules} = toRefs(data);
const {ids, single, multiple, handleSelectionChange} = useTableSelection<CsKbVO>(item => item.kbId);
const {dialog, resetForm: reset, openDialog, showDialog, closeDialog} = useFormDialog({
form,
formRef: csKbFormRef,
initialFormData: initFormData
});
/** 查询客服知识库列表 */
const getList = async () => {
await withLoading(async () => {
let params = queryParams.value;
params = applyCreateTimeDateRange(params);
params = applyUpdateTimeDateRange(params);
const res = await listCsKb(params);
csKbList.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: () => {
resetCreateTimeDateRange();
resetUpdateTimeDateRange();
},
afterReset: () => {
handleQuery();
}
});
/** 新增按钮操作 */
const handleAdd = () => {
openDialog('添加客服知识库');
};
/** 修改按钮操作 */
const handleUpdate = async (row?: Partial<CsKbVO>) => {
reset();
const _kbId = row?.kbId || ids.value[0];
const res = await getCsKb(_kbId);
Object.assign(form.value, res.data);
showDialog('修改客服知识库');
};
/** 提交按钮 */
const submitForm = () => {
csKbFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.kbId) {
await updateCsKb(form.value).finally(() => (buttonLoading.value = false));
} else {
await addCsKb(form.value).finally(() => (buttonLoading.value = false));
}
modal.msgSuccess('操作成功');
closeDialog();
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: Partial<CsKbVO>) => {
const _kbIds = row?.kbId || ids.value;
await modal.confirm('是否确认删除客服知识库编号为"' + _kbIds + '"的数据项?');
await delCsKb(_kbIds);
modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
requestDownload(
'system/cskb/export',
{
...queryParams.value
},
`csKb_${new Date().getTime()}.xlsx`
);
};
onMounted(() => {
getList();
});
</script>
<style lang="scss" scoped>
.circle {
width: 7px;
height: 6px;
border-radius: 3px;
background-color: rgba(124, 58, 237, 1)
}
</style>

View File

@@ -0,0 +1,182 @@
<script lang="ts" setup>
import FormItemTip from "@/components/FormItemTip/index.vue";
const router = useRouter();
const form = ref({
phone: "",
username: "",
sex: 1,
creatDay: '',
status: 1,
img: ''
})
function handleBack() {
router.replace({
path: "/customer/customerList"
})
}
function handleChangeFile(file: File) {
console.log(file)
}
</script>
<template>
<div class="edit">
<div class="flex justify-between">
<pagecrumb></pagecrumb>
<el-button plain type="info" @click="handleBack"> 返回客服列表</el-button>
</div>
<!-- form -->
<section class="commonCard">
<div class="flex justify-between flex-items-center">
<card-nav subTitle="填写客服基础信息、登录凭证与接待权限" title="新增客服账号"></card-nav>
<el-tag style="color:rgba(107, 114, 128, 1)" type="info">
<span> 带<span style="color: red;font-weight: bold;">* </span>为必填项</span>
</el-tag>
</div>
<div class="formbox">
<el-form label-position="top">
<el-row :gutter="24">
<el-col :span="12">
<FormItemTip
v-model="form.phone"
:inputAttrs="{ placeholder: '请输入工号,如 8823', disabled: true , showWordLimit:false ,maxlength:11}"
:required="true"
label="客服工号"
prop="phone"
tip="建议使用 4 位数字,用于内部标识与排班识别"
>
<template #suffix>不可修改</template>
</FormItemTip>
</el-col>
<el-col :span="12">
<FormItemTip
v-model="form.username"
:inputAttrs="{ placeholder: '请输入客服姓名', disabled: false , showWordLimit:true ,maxlength:20}"
:required="true"
prop="username"
tip="必填,将在会话列表与用户端展示"
>
<template #label>
<span>客服姓名</span>
</template>
</FormItemTip>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<FormItemTip
v-model="form.username"
:inputAttrs="{ placeholder: '请输入登录账号', disabled: false, showWordLimit:false ,maxlength:11}"
:required="true"
prop="username"
tip="该手机号同时作为账号找回与首次登录验证凭证"
>
<template #label>
<span>登录账号(手机号)</span>
</template>
</FormItemTip>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<FormItemTip
v-model="form.img"
label="账号状态"
prop="img"
type="uploadUserAvatar"
>
</FormItemTip>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<FormItemTip
v-model="form.status"
:inputAttrs="{ placeholder: '', disabled: false }"
:radioOptions="[
{label:'启用',value:1},
{label:'禁用',value:0},
]"
label="账号状态"
prop="status"
tip="禁用后该客服无法登录工作台,其接待中会话将转回排队"
type="radio"
>
</FormItemTip>
</el-col>
</el-row>
</el-form>
</div>
</section>
<div class="footer">
<el-button>取消</el-button>
<el-button type="primary">保存</el-button>
</div>
</div>
</template>
<style lang="scss" scoped>
.edit {
.customerInfoBox {
.infoBox {
.customer-Img {
width: 73px;
height: 72px;
border-radius: 50%;
margin-right: 24px;
}
.userName {
height: 26px;
line-height: 26px;
font-weight: bold;
color: rgba(17, 24, 39, 1);
font-size: 22px;
}
.circle {
display: block;
width: 7px;
height: 6px;
border-radius: 3px;
margin-right: 6px;
background-color: rgba(22, 163, 74, 1);
}
}
.subInfoBox {
line-height: 20px;
color: rgba(107, 114, 128, 1);
font-size: 13px;
text-align: left;
.dat {
margin: 0 1px;
}
}
}
.subTitle {
line-height: 14px;
color: rgba(156, 163, 175, 1);
font-size: 12px;
text-align: left;
}
.formbox {
margin-top: 46px;
}
.footer {
margin-top: 27px;
text-align: right;
}
}
</style>

View File

@@ -0,0 +1,210 @@
<script lang="ts" setup>
import FormItemTip from "@/components/FormItemTip/index.vue";
const router = useRouter();
const form = ref({
phone: "",
username: "",
sex: 1,
creatDay: '',
status: 1,
img: ''
})
function handleBack() {
router.replace({
path: "/customer/customerList"
})
}
function handleChangeFile(file: File) {
console.log(file)
}
</script>
<template>
<div class="edit">
<div class="flex justify-between">
<pagecrumb></pagecrumb>
<el-button plain type="info" @click="handleBack"> 返回客服列表</el-button>
</div>
<!-- 用户信息-->
<section class="customerInfoBox commonCard flex justify-between flex-items-center">
<div class="flex infoBox">
<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>
<span class="userName">林小满</span>
<el-tag effect="light" round style="margin: 0 12px " type="success">
<div class="flex flex-items-center">
<span>启用</span></div>
</el-tag>
</div>
<div class="subInfoBox">
<span> KF-001 </span>
<span class="dat">·</span>
<span>工号 8823 </span>
<span class="dat">·</span>
<span>138****6688</span>
<span class="dat">·</span>
<span>创建于 2025-11-08</span>
</div>
</div>
</div>
<div>
当前在线·接待中3/5
</div>
</section>
<!-- form -->
<section class="commonCard">
<card-nav subTitle="工号唯一且不可重复;账号状态切换后该客服将无法登录工作台" title="客服账号信息"></card-nav>
<div class="formbox">
<el-form label-position="top">
<el-row :gutter="24">
<el-col :span="12">
<FormItemTip
v-model="form.phone"
:inputAttrs="{ placeholder: '请输入工号', disabled: true , showWordLimit:false ,maxlength:11}"
:required="true"
label="工号"
prop="phone"
tip="必填且全平台唯一,示例:8823"
>
<template #suffix>不可修改</template>
</FormItemTip>
</el-col>
<el-col :span="12">
<FormItemTip
v-model="form.username"
:inputAttrs="{ placeholder: '请输入客服姓名', disabled: false , showWordLimit:true ,maxlength:20}"
:required="true"
prop="username"
tip="必填,将在会话列表与用户端展示"
>
<template #label>
<span>客服姓名</span>
</template>
</FormItemTip>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<FormItemTip
v-model="form.username"
:inputAttrs="{ placeholder: '请输入登录账号', disabled: false , showWordLimit:true ,maxlength:20}"
:required="true"
prop="username"
tip="必填,手机号格式,全平台唯一,同时作为登录账号"
>
<template #label>
<span>登录账号</span>
</template>
</FormItemTip>
</el-col>
<el-col :span="12">
<FormItemTip
v-model="form.username"
:inputAttrs="{ placeholder: '不修改请留空', disabled: false , showWordLimit:true ,maxlength:20, minlength:8}"
:required="true"
prop="username"
tip="如需修改请重新输入,长度 8–20 位,含字母与数字"
>
<template #label>
<span>登录密码</span>
</template>
</FormItemTip>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<FormItemTip
v-model="form.status"
:inputAttrs="{ placeholder: '', disabled: false }"
:radioOptions="[
{label:'启用',value:1},
{label:'禁用',value:0},
]"
label="账号状态"
prop="status"
tip="禁用后该客服无法登录工作台,其接待中会话将转回排队"
type="radio"
>
</FormItemTip>
</el-col>
</el-row>
</el-form>
</div>
</section>
<div class="footer">
<el-button>取消</el-button>
<el-button type="primary">保存</el-button>
</div>
</div>
</template>
<style lang="scss" scoped>
.edit {
.customerInfoBox {
.infoBox {
.customer-Img {
width: 73px;
height: 72px;
border-radius: 50%;
margin-right: 24px;
}
.userName {
height: 26px;
line-height: 26px;
font-weight: bold;
color: rgba(17, 24, 39, 1);
font-size: 22px;
}
.circle {
display: block;
width: 7px;
height: 6px;
border-radius: 3px;
margin-right: 6px;
background-color: rgba(22, 163, 74, 1);
}
}
.subInfoBox {
line-height: 20px;
color: rgba(107, 114, 128, 1);
font-size: 13px;
text-align: left;
.dat {
margin: 0 1px;
}
}
}
.subTitle {
line-height: 14px;
color: rgba(156, 163, 175, 1);
font-size: 12px;
text-align: left;
}
.formbox {
margin-top: 46px;
}
.footer {
margin-top: 27px;
text-align: right;
}
}
</style>

View File

@@ -0,0 +1,394 @@
<template>
<div class="p-2 page-shell system-csAgent-page">
<div class="search-wrap">
<pagecrumb></pagecrumb>
<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-item label="客服姓名" prop="name">
<el-input v-model="queryParams.name" clearable placeholder="请输入客服姓名" size="large"
style="width:240px;"
@keyup.enter="handleQuery"/>
</el-form-item>
<el-form-item label="客服工号" prop="jobNo">
<el-input v-model="queryParams.jobNo" clearable placeholder="请输入客服工号" size="large"
style="width:240px;"
@keyup.enter="handleQuery"/>
</el-form-item>
<el-form-item label="账号状态" prop="status">
<el-select v-model="queryParams.status" clearable placeholder="请选择账号状态" size="large"
style="width:180px;">
<el-option :value="true" label="是"/>
<el-option :value="false" label="否"/>
</el-select>
</el-form-item>
<el-form-item label="在线状态" prop="onlineStatus">
<el-select v-model="queryParams.onlineStatus" clearable placeholder="请选择在线状态" size="large"
style="width:180px;">
<el-option :value="true" label="是"/>
<el-option :value="false" label="否"/>
</el-select>
</el-form-item>
<el-form-item>
<el-button size="large" type="primary" @click="handleQuery">查询</el-button>
<el-button size="large" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<el-card class="table-panel" shadow="hover">
<template #header>
<div class="toolbar-shell">
<div class="table-heading">
<el-button type="primary">
<span style="margin:0 8px;color: #fff;">全部</span>
<span style="color: #fff;font-weight: bold">1286</span>
</el-button>
<tableTag circle-color="#16a34a" num="6" title="在线"></tableTag>
<tableTag circle-color="#ea580c" num="2" title="忙碌"></tableTag>
<tableTag circle-color="#94a3b8" num="4" title="离线"></tableTag>
</div>
<div class="toolbar-actions">
<el-button v-hasPermi="['system:csAgent:edit']" :disabled="single" plain type="info"
@click="handleUpdate()">批量禁用账号
</el-button>
<el-button v-hasPermi="['system:csAgent:add']" type="primary" @click="handleAdd">新增客服
</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="csAgentList" border class="data-table"
@selection-change="handleSelectionChange">
<el-table-column align="center" type="selection" width="55"/>
<el-table-column align="center" label="主键" prop="agentId"/>
<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="name"/>
<el-table-column align="center" label="登录账号" prop="account"/>
<el-table-column align="center" label="密码" prop="password"/>
<el-table-column align="center" label="手机号" prop="phone"/>
<el-table-column align="center" label="头像 oss_id" prop="avatarOssId"/>
<el-table-column align="center" label="在线状态" prop="onlineStatus" width="120">
<template #default="scope">
<el-switch
v-model="scope.row.onlineStatus"
:active-value="true"
:inactive-value="false"
disabled
/>
</template>
</el-table-column>
<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" class-name="small-padding fixed-width" label="操作">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button v-hasPermi="['system:csAgent:edit']" icon="Edit" link type="primary"
@click="handleUpdate(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button v-hasPermi="['system:csAgent: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="500px">
<el-form ref="csAgentFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="客服编号,如 KF-001" prop="agentNo">
<el-input v-model="form.agentNo" placeholder="请输入客服编号,如 KF-001"/>
</el-form-item>
<el-form-item label="工号" prop="jobNo">
<el-input v-model="form.jobNo" placeholder="请输入工号"/>
</el-form-item>
<el-form-item label="客服姓名" prop="name">
<el-input v-model="form.name" placeholder="请输入客服姓名"/>
</el-form-item>
<el-form-item label="登录账号" prop="account">
<el-input v-model="form.account" placeholder="请输入登录账号"/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="form.password" placeholder="请输入密码"/>
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号"/>
</el-form-item>
<el-form-item label="头像 oss_id" prop="avatarOssId">
<el-input-number v-model="form.avatarOssId" controls-position="right"/>
</el-form-item>
<el-form-item label="在线状态" prop="onlineStatus">
<el-switch
v-model="form.onlineStatus"
:active-value="true"
:inactive-value="false"
/>
</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>
</div>
</template>
<script lang="ts" name="CsAgent" setup>
import tableTag from '@/components/tableTag/index.vue'
import {
addCsAgent,
delCsAgent,
getCsAgent,
listCsAgent,
updateCsAgent
} from '@/api/system/cssAgent';
import {CsAgentForm, CsAgentQuery, CsAgentVO} from '@/api/system/cssAgent/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 modal from '@/plugins/modal';
import {download as requestDownload} from '@/utils/request';
const csAgentList = ref<CsAgentVO[]>([]);
const buttonLoading = ref(false);
const {loading, withLoading} = useLoading(true);
const {showSearch} = useSearchToggle();
const total = ref(0);
const {
dateRange: dateRangeCreateTime,
applyDateRange: applyCreateTimeDateRange,
resetDateRange: resetCreateTimeDateRange
} = useDateRangeQuery('CreateTime');
const {
dateRange: dateRangeUpdateTime,
applyDateRange: applyUpdateTimeDateRange,
resetDateRange: resetUpdateTimeDateRange
} = useDateRangeQuery('UpdateTime');
const queryFormRef = ref<ElFormInstance>();
const csAgentFormRef = ref<ElFormInstance>();
const initFormData: CsAgentForm = {
agentId: undefined,
agentNo: undefined,
jobNo: undefined,
name: undefined,
account: undefined,
password: undefined,
phone: undefined,
avatarOssId: undefined,
onlineStatus: undefined,
status: undefined,
createDept: undefined,
createBy: undefined,
createTime: undefined,
updateBy: undefined,
updateTime: undefined,
}
const data = reactive<PageData<CsAgentForm, CsAgentQuery>>({
form: {...initFormData},
queryParams: {
pageNum: 1,
pageSize: 10,
agentNo: undefined,
jobNo: undefined,
name: undefined,
account: undefined,
password: undefined,
phone: undefined,
avatarOssId: undefined,
onlineStatus: undefined,
status: undefined,
createDept: undefined,
createBy: undefined,
updateBy: undefined,
params: {
createTime: undefined,
updateTime: undefined,
}
},
rules: {
agentId: [
{required: true, message: "主键不能为空", trigger: "change"}
],
agentNo: [
{required: true, message: "客服编号,如 KF-001不能为空", trigger: "blur"}
],
jobNo: [
{required: true, message: "工号不能为空", trigger: "blur"}
],
name: [
{required: true, message: "客服姓名不能为空", trigger: "blur"}
],
account: [
{required: true, message: "登录账号不能为空", trigger: "blur"}
],
password: [
{required: true, message: "密码不能为空", trigger: "blur"}
],
onlineStatus: [
{required: true, message: "在线状态不能为空", trigger: "change"}
],
status: [
{required: true, message: "账号状态不能为空", trigger: "change"}
],
}
});
const {queryParams, form, rules} = toRefs(data);
const {ids, single, multiple, handleSelectionChange} = useTableSelection<CsAgentVO>(item => item.agentId);
const {dialog, resetForm: reset, openDialog, showDialog, closeDialog} = useFormDialog({
form,
formRef: csAgentFormRef,
initialFormData: initFormData
});
/** 查询客服人员列表 */
const getList = async () => {
await withLoading(async () => {
let params = queryParams.value;
params = applyCreateTimeDateRange(params);
params = applyUpdateTimeDateRange(params);
const res = await listCsAgent(params);
csAgentList.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: () => {
resetCreateTimeDateRange();
resetUpdateTimeDateRange();
},
afterReset: () => {
handleQuery();
}
});
/** 新增按钮操作 */
const handleAdd = () => {
openDialog('添加客服人员');
};
/** 修改按钮操作 */
const handleUpdate = async (row?: Partial<CsAgentVO>) => {
reset();
const _agentId = row?.agentId || ids.value[0];
const res = await getCsAgent(_agentId);
Object.assign(form.value, res.data);
showDialog('修改客服人员');
};
/** 提交按钮 */
const submitForm = () => {
csAgentFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.agentId) {
await updateCsAgent(form.value).finally(() => (buttonLoading.value = false));
} else {
await addCsAgent(form.value).finally(() => (buttonLoading.value = false));
}
modal.msgSuccess('操作成功');
closeDialog();
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: Partial<CsAgentVO>) => {
const _agentIds = row?.agentId || ids.value;
await modal.confirm('是否确认删除客服人员编号为"' + _agentIds + '"的数据项?');
await delCsAgent(_agentIds);
modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
requestDownload(
'system/csAgent/export',
{
...queryParams.value
},
`csAgent_${new Date().getTime()}.xlsx`
);
};
onMounted(() => {
getList();
});
</script>

View File

@@ -0,0 +1,242 @@
<script lang="ts" setup>
import FormItemTip from "@/components/FormItemTip/index.vue";
const router = useRouter();
const form = ref({
phone: "",
username: "",
sex: 1,
creatDay: '',
status: 1,
img: ''
})
function handleBack() {
router.replace({
path: "/customer/customerList"
})
}
function handleChangeFile(file: File) {
console.log(file)
}
</script>
<template>
<div class="edit">
<div class="flex justify-between">
<pagecrumb></pagecrumb>
<el-button plain type="info" @click="handleBack"> 返回用户列表</el-button>
</div>
<!-- 用户信息-->
<section class="customerInfoBox commonCard flex justify-between flex-items-center">
<div class="flex infoBox">
<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>
<span class="userName">林小满</span>
<el-tag effect="light" round style="margin: 0 12px " type="success">
<div class="flex flex-items-center"><i class="circle"></i>
<span>正常</span></div>
</el-tag>
<el-tag effect="light" round type="info">150****0831</el-tag>
</div>
<div class="subInfoBox">
<span>用户ID: U20260918001 </span>
<span class="dat">·</span>
<span>注册时间: 2026-01-10 14:20</span>
<span class="dat">·</span>
<span>最后登录: 2026-09-17 09:35</span>
</div>
</div>
</div>
</section>
<!-- form -->
<section class="commonCard">
<card-nav subTitle="手机号为账号唯一标识不可修改;个人资料与账号状态可维护" title="用户资料"></card-nav>
<div class="formbox">
<el-form label-position="top">
<el-row :gutter="24">
<el-col :span="12">
<FormItemTip
v-model="form.phone"
:inputAttrs="{ placeholder: '请输入手机号', disabled: true , showWordLimit:false ,maxlength:11}"
label="手机号"
prop="phone"
tip="账号唯一标识,如需更换手机号请联系用户重新注册"
>
<template #suffix>不可修改</template>
</FormItemTip>
</el-col>
<el-col :span="12">
<FormItemTip
v-model="form.username"
:inputAttrs="{ placeholder: '请输入用户昵称', disabled: false , showWordLimit:true ,maxlength:20}"
:required="true"
prop="username"
tip="必填,最多 20 个字符,将同步展示在 APP 端"
>
<template #label>
<span>用户昵称</span>
</template>
</FormItemTip>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<FormItemTip
v-model="form.sex"
:radioOptions="[
{label:'男',value:1},
{label:'女',value:0},
{label:'未填写',value:2},
]"
prop="sex"
tip="选填,选择「未填写」时 APP 端不展示性别"
type="radio"
>
<template #label>
<span>性别</span>
<span class="subTitle">(选填)</span>
</template>
</FormItemTip>
</el-col>
<el-col :span="12">
<FormItemTip
v-model="form.creatDay"
:inputAttrs="{ placeholder: '请选择生日'}"
label="生日"
prop="creatDay"
tip="选填,修改后将同步至用户个人资料"
type="date"
>
<template #label>
<span>生日</span>
<span class="subTitle">(选填)</span>
</template>
</FormItemTip>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<FormItemTip
v-model="form.status"
:inputAttrs="{ placeholder: '', disabled: false }"
:radioOptions="[
{label:'正常',value:1},
{label:'禁用',value:0},
]"
label="账号状态"
prop="status"
tip="切换为「禁用」后该账号将无法登录 APP,且已登录设备将下线"
type="radio"
>
</FormItemTip>
</el-col>
<el-col :span="12">
<FormItemTip
v-model="form.username"
:inputAttrs="{ placeholder: '请输入用户昵称', disabled: true }"
:required="true"
label="头像"
prop="username"
tip="必填,最多 20 个字符,将同步展示在 APP 端"
type="uploadUserAvatar"
@file-change="handleChangeFile"
>
</FormItemTip>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="24">
<FormItemTip
v-model="form.phone"
:inputAttrs="{ placeholder: '请输入后台内部备注,如工单号、对接人、异常说明等信息', disabled: false , showWordLimit:true ,maxlength:20}"
:required="false"
label="备注"
prop="phone"
type="textarea"
>
<template #label>
<span>备注 </span>
<span>(选填 · 仅后台管理员可见)</span>
</template>
</FormItemTip>
</el-col>
</el-row>
</el-form>
</div>
</section>
<div class="footer">
<el-button>取消</el-button>
<el-button type="primary">保存</el-button>
</div>
</div>
</template>
<style lang="scss" scoped>
.edit {
.customerInfoBox {
.infoBox {
.customer-Img {
width: 73px;
height: 72px;
border-radius: 50%;
margin-right: 24px;
}
.userName {
height: 26px;
line-height: 26px;
font-weight: bold;
color: rgba(17, 24, 39, 1);
font-size: 22px;
}
.circle {
display: block;
width: 7px;
height: 6px;
border-radius: 3px;
margin-right: 6px;
background-color: rgba(22, 163, 74, 1);
}
}
.subInfoBox {
line-height: 20px;
color: rgba(107, 114, 128, 1);
font-size: 13px;
text-align: left;
.dat {
margin: 0 1px;
}
}
}
.subTitle {
line-height: 14px;
color: rgba(156, 163, 175, 1);
font-size: 12px;
text-align: left;
}
.formbox {
margin-top: 46px;
}
.footer {
margin-top: 27px;
text-align: right;
}
}
</style>

View File

@@ -0,0 +1,209 @@
<template>
<div class="p-2 app-container monitor-online-page">
<Pagecrumb></Pagecrumb>
<div class="search-wrap">
<el-card class="search-panel" shadow="hover">
<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="query-form" label-position="top">
<el-form-item label="手机号" prop="phoneNumber">
<el-input v-model="queryParams.phoneNumber" clearable placeholder="请输入手机号" size="large"
style="width: 190px;" @keyup.enter="handleQuery"/>
</el-form-item>
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="queryParams.nickName" clearable placeholder="请输入用户昵称" size="large"
style="width: 190px;" @keyup.enter="handleQuery"/>
</el-form-item>
<el-form-item label="性别" prop="gender">
<el-select
v-model="queryParams.gender"
:value-on-clear="null"
clearable
size="large"
style="width:100px;"
>
<el-option label="女" value="2"></el-option>
<el-option label="男" value="1"></el-option>
<el-option label="未知" value="0"></el-option>
</el-select>
</el-form-item>
<el-form-item label="所在地区" prop="userName">
<el-select
v-model="queryParams.ipaddr"
:empty-values="[null, undefined]"
:value-on-clear="null"
clearable
size="large"
style="width: 190px;"
>
<el-option label="在线" value="0"></el-option>
<el-option label="离线" value="1"></el-option>
</el-select>
</el-form-item>
<el-form-item label="注册时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
size="large"
style="width:150px;"
type="date"
/>
</el-form-item>
<el-form-item label="账号状态" prop="status">
<el-select
v-model="queryParams.status"
:value-on-clear="null"
clearable
size="large"
style="width:100px;"
>
<el-option label="正常" value="1"></el-option>
<el-option label="停用" value="0"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button size="large" type="primary" @click="handleQuery">查询</el-button>
<el-button size="large" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<el-card class="table-panel" shadow="hover">
<template #header>
<div class="flex justify-end">
<el-button plain type="info">批量导出用户信息</el-button>
<el-button plain type="danger">批量禁用账号</el-button>
</div>
</template>
<el-table
v-loading="loading"
:data="onlineList"
class="data-table"
style="width: 100%"
>
<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="phoneNumber"/>
<el-table-column :show-overflow-tooltip="true" align="center" label="性别" prop="gender">
<template #default="scope">
<span v-if="scope.row.gender === '1'">男</span>
<span v-else-if="scope.row.gender==='2'">女</span>
<span v-else>未填写</span>
</template>
</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="ipaddr"/>
<el-table-column :show-overflow-tooltip="true" align="center" label="注册时间" prop="creatTime"/>
<el-table-column :show-overflow-tooltip="true" align="center" label="最后登录时间" prop="loginDate"/>
<el-table-column :show-overflow-tooltip="true" align="center" label="账号状态" prop="status">
<template #default="scope">
<el-tag v-if="scope.row.status === '0'" type="success">正常</el-tag>
<el-tag v-else type="success">异常</el-tag>
</template>
</el-table-column>
<el-table-column align="center" class-name="small-padding fixed-width" label="操作" min-width="250">
<template #default="scope">
<el-button
v-hasPermi="['monitor:online:forceLogout']"
plain
type="primary"
@click="handleMain(scope.row)"
>查看详情
</el-button>
<el-button
v-hasPermi="['monitor:online:forceLogout']"
plain
type="info"
>编辑
</el-button>
<el-button
v-hasPermi="['monitor:online:forceLogout']"
plain
type="info"
>重置密码
</el-button>
<el-button
v-if="scope.row.status === '0'"
v-hasPermi="['monitor:online:forceLogout']"
plain
type="warning"
>禁用
</el-button>
<el-button
v-if="scope.row.status === '1'"
v-hasPermi="['monitor:online:forceLogout']"
plain
type="success"
>启用
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNum"
:total="total"
/>
</el-card>
</div>
</template>
<script lang="ts" setup>
import {listUser as initData} from '@/api/system/customer/index';
import {useDict} from '@/utils/dict';
import {UserQuery, UserVO} from "@/api/system/customer/types";
const {sys_device_type} = toRefs<any>(useDict('sys_device_type'));
const router = useRouter();
const onlineList = ref<UserVO[]>([]);
const loading = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const queryParams = ref<UserQuery>({
pageNum: 1,
pageSize: 10,
ipaddr: '',
userName: ''
});
/** 查询登录日志列表 */
const getList = async () => {
loading.value = true;
const res = await initData(queryParams.value);
onlineList.value = res.data?.rows;
total.value = res.data?.total;
loading.value = false;
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
function handleMain(row) {
console.log(row);
router.push({
path: '/customer/customerMain'
})
}
onMounted(() => {
getList();
});
</script>
<style lang="scss" scoped>
//@use '@/assets/styles/components/page-shell' as pageShell;
//
//@include pageShell.table-crud-page;
</style>

View File

@@ -0,0 +1,311 @@
<script lang="ts" setup>
import Pagecrumb from '@/components/Pagecrumb/index.vue';
const router = useRouter();
function handleEdit() {
router.push({
path: '/customer/customerEdit'
})
}
function handleBack() {
router.push({
path: '/customer/customerList'
})
}
</script>
<template>
<div class="Main">
<div class="flex justify-between">
<Pagecrumb></Pagecrumb>
<el-button plain type="info" @click="handleBack">返回用户列表</el-button>
</div>
<!-- 用户信息-->
<section class="customerInfoBox commonCard flex justify-between flex-items-center">
<div class="flex infoBox">
<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>
<span class="userName">林小满</span>
<el-tag effect="light" round style="margin: 0 12px " type="success">
<div class="flex flex-items-center"><i class="circle"></i>
<span>正常</span></div>
</el-tag>
<el-tag effect="light" round type="info">150****0831</el-tag>
</div>
<div class="subInfoBox">
<span>用户ID: U20260918001 </span>
<span class="dat">·</span>
<span>注册时间: 2026-01-10 14:20</span>
<span class="dat">·</span>
<span>最后登录: 2026-09-17 09:35</span>
</div>
</div>
</div>
<div>
<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="danger">禁用账号</el-button>
</div>
</section>
<el-row :gutter="20">
<el-col :span="12">
<!-- 账号信息-->
<div class="userNameBox commonCard">
<cardNav line-color="#2563eb" title="账号基础信息"></cardNav>
<ul class=" userNameInfoBox">
<li>
<span class="label">用户ID</span>
<span class="value">U20260918001</span>
</li>
<li>
<span class="label">手机号</span>
<span class="value">138****6688</span>
</li>
<li>
<span class="label">账号状态</span>
<el-tag type="success">正常</el-tag>
</li>
<li>
<span class="label">注册时间</span>
<span class="value">2026-01-10 14:20</span>
</li>
<li>
<span class="label">最后登录时间</span>
<span class="value">2026-09-17 09:35</span>
</li>
<li>
<span class="label">登录次数</span>
<span class="value">126次</span>
</li>
</ul>
</div>
</el-col>
<el-col :span="12">
<!-- 个人信息-->
<div class="userInfoBox commonCard">
<cardNav line-color="#14b8a6" title="个人资料"></cardNav>
<div class="userInfoInfoBox">
<div class="userInfoImageBox">
<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>
<ul>
<li>
<span class="label">昵称</span>
<span class="value">黎晓曼</span>
</li>
<li>
<span class="label">性别</span>
<span class="value">男</span>
</li>
<li>
<span class="label">生日</span>
<span class="value">1926-09-18</span>
</li>
<li>
<span class="label"> 所在地区</span>
<span class="value">上海市・浦东新区</span>
</li>
</ul>
</div>
<div class="footer">
个人资料由用户在 APP 端自行维护,后台仅可查看;如需调整请在「编辑用户」中操作。
</div>
</div>
</el-col>
</el-row>
<!-- 绑定设备清单-->
<div class="bindDevicesListBox commonCard">
<div class="flex justify-between">
<card-nav line-color="#f59e0b" subTitle="该用户名下全部绑定设备,点击操作可跳转设备详情"
title="绑定设备清单"></card-nav>
<el-tag type="primary">共3台</el-tag>
</div>
<el-table
class="data-table"
style="width: 100%"
>
<el-table-column :show-overflow-tooltip="true" align="center" label="设备名称" prop="tokenId"/>
<el-table-column :show-overflow-tooltip="true" align="center" label="MAC地址" prop="userName"/>
<el-table-column :show-overflow-tooltip="true" align="center" label="型号" prop="clientKey"/>
<el-table-column align="center" label="在线状态">
<template #default="scope">
<el-tag type="primary">在线</el-tag>
<el-tag type="danger">离线</el-tag>
</template>
</el-table-column>
<el-table-column :show-overflow-tooltip="true" align="center" label="绑定时间" prop="userName"/>
<el-table-column align="center" label="操作">
<template #default="scope">
<el-button plain type="primary">查看设备详情</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<style lang="scss" scoped>
.Main {
.customerInfoBox {
.infoBox {
.customer-Img {
width: 73px;
height: 72px;
border-radius: 50%;
margin-right: 24px;
}
.userName {
height: 26px;
line-height: 26px;
font-weight: bold;
color: rgba(17, 24, 39, 1);
font-size: 22px;
}
.circle {
display: block;
width: 7px;
height: 6px;
border-radius: 3px;
margin-right: 6px;
background-color: rgba(22, 163, 74, 1);
}
}
.subInfoBox {
line-height: 20px;
color: rgba(107, 114, 128, 1);
font-size: 13px;
text-align: left;
.dat {
margin: 0 1px;
}
}
}
.userNameBox {
min-height: 389px;
display: flex;
flex-direction: column;
.userNameInfoBox {
flex: 1;
margin-top: 25px;
display: flex;
align-items: center;
flex-direction: column;
justify-content: space-around;
li {
width: 100%;
display: flex;
justify-content: space-between;
.label {
line-height: 16px;
color: rgba(107, 114, 128, 1);
font-size: 13px;
}
.value {
line-height: 17px;
color: rgba(17, 24, 39, 1);
font-size: 14px;
}
}
}
}
.userInfoBox {
.userInfoInfoBox {
margin-top: 24px;
margin-bottom: 32px;
display: flex;
.userInfoImageBox {
width: 100px;
margin-right: 24px;
.userImage {
width: 97px;
height: 96px;
border-radius: 50%;
}
.description {
line-height: 16px;
color: rgba(156, 163, 175, 1);
font-size: 12px;
text-align: center;
}
}
ul {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-around;
li {
display: flex;
align-items: center;
justify-content: space-between;
.label {
line-height: 16px;
color: rgba(107, 114, 128, 1);
font-size: 13px;
text-align: left;
}
.value {
height: 17px;
line-height: 17px;
color: rgba(17, 24, 39, 1);
font-size: 14px;
text-align: left;
}
}
}
}
.footer {
height: 44px;
border-radius: 12px 12px 12px 12px;
background-color: rgba(247, 249, 252, 1);
border: 1px solid rgba(238, 240, 243, 1);
line-height: 18px;
padding: 13px;
color: rgba(156, 163, 175, 1);
font-size: 12px;
text-align: left;
}
}
.bindDevicesListBox {
margin-top: 16px;
.data-table {
margin-top: 16px;
}
}
}
</style>

View File

@@ -0,0 +1,474 @@
<script lang="ts" setup>
import cardNav from '@/components/cardNav/index.vue';
import FormItemTip from '@/components/FormItemTip/index.vue';
import {DeviceForm} from "@/api/system/device/types";
import {getDevice} from "@/api/system/device";
function submitForm() {
if (!form.value.name || form.value.name.trim() === "") {
return ElMessage.warning('设备名称不能为空')
}
if (!form.value.houseName || form.value.houseName.trim() === "") {
return ElMessage.warning('所在房间不能为空')
}
}
const router = useRouter()
function backMain() {
router.replace(
{
path: '/device/deviceMain'
}
)
}
const route = useRoute();
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 form = ref();
function init() {
form.value = {...initFormData};
console.log(form.value);
if (route.query.id) {
getDevice(route.query.id as string).then(res => {
Object.assign(form.value, res.data);
});
}
}
init()
</script>
<template>
<div class="edit">
<nav class="flex justify-between">
<Pagecrumb></Pagecrumb>
<div>
<el-button plain type="info" @click="backMain">返回设备详情</el-button>
</div>
</nav>
<div class="infoBox">
<div class="info-item">
<el-image class="info_img"
src="https://ts2.tc.mm.bing.net/th/id/OIP-C.2KXRoOZ1EPBDMf6jrp6xeAHaE7?r=0&rs=1&pid=ImgDetedit&o=7&rm=3"></el-image>
<div class="info">
<div class="nav">
<div class="title">Z2空气净化器</div>
<div class="tag online">
<i class="circle"></i>
<span>在线</span>
</div>
</div>
<div class="describe">DEV-2026-000138·AA:BB:CC:DD:EE:B·客厅·CZ-Z2023</div>
</div>
</div>
</div>
<div class="editCard commonCard">
<card-nav line-color="#2563eb" subTitle="仅可修改设备名称、所在房间与备注,实时运行数据不可编辑"
title="静态档案信息"></card-nav>
<el-form :model="form" label-position="top" style="margin-top:44px;">
<el-row :gutter="24">
<el-col :span="12">
<FormItemTip
v-model="form.name"
:inputAttrs="{ placeholder: '请输入设备名称', disabled: false , showWordLimit:true ,maxlength:20}"
:required="true"
label="设备名称"
prop="name"
tip="必填,最多20个字符"
>
</FormItemTip>
</el-col>
<el-col :span="12">
<FormItemTip
v-model="form.model"
:inputAttrs="{ placeholder: '请输入产品型号', disabled: true }"
:required="false"
prop="model"
tip="设备型号出厂固定,不支持修改"
>
<template #label>
<span>产品型号</span>
<el-tag style="margin-left: 4px" type="info">锁定</el-tag>
</template>
</FormItemTip>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<FormItemTip
v-model="form.mac"
:inputAttrs="{ placeholder: '请输入MAC地址', disabled: true }"
:required="false"
prop="mac"
tip="MAC 地址为设备唯一标识,不支持修改"
>
<template #label>
<span>MAC地址</span>
<el-tag style="margin-left: 4px" type="info">锁定</el-tag>
</template>
</FormItemTip>
</el-col>
<el-col :span="12">
<FormItemTip
v-model="form.houseName"
:inputAttrs="{ placeholder: '请输入所在房间', }"
:required="true"
label="所在房间"
prop="houseName"
tip="必填,可选:客厅 / 洗衣间 / 阳台 / 厨房"
>
</FormItemTip>
</el-col>
</el-row>
<div>
<FormItemTip
v-model="form.remark"
:inputAttrs="{ placeholder: '请输入备注信息,如采购批次、安装位置说明等' }"
:required="false"
prop="remark"
type="textarea"
>
<template #label>
<span>备注</span>
<span style="color:rgba(156, 163, 175, 1);font-size: 12px">(选填)</span>
</template>
</FormItemTip>
</div>
</el-form>
<div class="readTimeInfo">
<div class="nav flex justify-between">
<div>
<span class="title">实时运行数据</span>
<el-tag style="margin-left:4px" type="info">只读</el-tag>
</div>
<span class="tip">由设备蓝牙自动上报,后台不可编辑 · 更新于 2026-09-10 12:50:30</span>
</div>
<ul>
<li class="li-item">
<div class="title">TVOC数值</div>
<div class="value">
<span class="Num">286</span>
<el-tag type="success">优</el-tag>
</div>
</li>
<li class="li-item">
<div class="title">运行模式</div>
<div class="value">
<span class="Num">桌面模式</span>
</div>
</li>
<li class="li-item">
<div class="title">风速挡位</div>
<div class="value">
<span class="Num">标准</span>
</div>
</li>
<li class="li-item">
<div class="title">供电模式</div>
<div class="value">
<span class="Num">USB供电</span>
</div>
</li>
<li class="li-item">
<div class="title">滤芯寿命</div>
<div class="value">
<span class="Num">76%</span>
</div>
<div class="tip">剩余128天</div>
</li>
<li class="li-item">
<div class="title">电池电量</div>
<div class="value">
<span class="Num">85%</span>
</div>
<div class="tip">剩余128天</div>
</li>
</ul>
<div class="tip">
运行数据来源于设备上报,无法在后台修改;如需调整运行模式与风速,请在 APP 设备详情页操作。
</div>
</div>
</div>
<div class="footer">
<el-button size="large" type="default">取消</el-button>
<el-button size="large" type="primary" @click="submitForm">保存</el-button>
</div>
</div>
</template>
<style lang="scss" scoped>
.edit {
height: 100%;
width: 100%;
overflow: auto;
.infoBox {
padding: 24px;
display: flex;
justify-content: space-between;
align-items: center;
max-height: 177px;
//min-width: 750px;
border-radius: 16px 16px 16px 16px;
border: 1px solid rgba(238, 240, 243, 1);
margin-top: 8px;
.info-item {
display: flex;
align-items: center;
.info_img {
width: 57px;
height: 56px;
border-radius: 12px 12px 12px 12px;
}
.info {
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: 24px;
.nav {
display: flex;
align-items: end;
margin-bottom: 10px;
.title {
height: 28px;
line-height: 28px;
color: rgba(17, 24, 39, 1);
font-size: 22px;
text-align: left;
}
.tag {
margin-left: 5px;
width: 62px;
height: 28px;
border-radius: 14px;
&.online {
background-color: rgba(232, 247, 238, 1);
color: rgba(22, 163, 74, 1);
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
.circle {
margin-right: 6px;
display: block;
width: 7px;
height: 6px;
border-radius: 3px;
background-color: rgba(22, 163, 74, 1);
}
}
}
}
.describe {
line-height: 16px;
color: rgba(156, 163, 175, 1);
font-size: 12px;
text-align: left;
}
.info-list {
display: flex;
align-items: center;
gap: 20px;
margin-top: 28px;
.info-list-item {
.title {
height: 16px;
line-height: 16px;
color: rgba(156, 163, 175, 1);
font-size: 12px;
}
.value {
height: 40px;
line-height: 20px;
color: rgba(17, 24, 39, 1);
font-size: 14px;
}
}
}
}
}
.timeBox {
.title {
text-align: right;
line-height: 16px;
color: rgba(156, 163, 175, 1);
font-size: 12px;
}
.time {
line-height: 22px;
background-color: rgba(0, 0, 0, 0);
color: rgba(17, 24, 39, 1);
font-size: 16px;
margin-top: 6px;
margin-bottom: 12px;
}
.tip {
display: flex;
align-items: center;
justify-content: end;
span {
background-color: rgba(0, 0, 0, 0);
color: rgba(37, 99, 235, 1);
font-size: 12px;
text-align: left;
}
.circle {
margin-right: 6px;
display: block;
width: 7px;
height: 6px;
border-radius: 3px;
background-color: #2563eb;
}
}
}
}
.editCard {
min-height: 763px;
.readTimeInfo {
.nav {
margin-bottom: 16px;
.title {
line-height: 17px;
color: rgba(31, 41, 55, 1);
font-size: 14px;
font-weight: bold;
}
.tip {
line-height: 16px;
color: rgba(156, 163, 175, 1);
font-size: 12px;
}
}
ul {
gap: 16px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
.li-item {
height: 102px;
border-radius: 12px 12px 12px 12px;
background-color: rgba(247, 249, 252, 1);
border: 1px solid rgba(238, 240, 243, 1);
padding: 17px;
.title {
margin-bottom: 10px;
line-height: 16px;
color: rgba(156, 163, 175, 1);
font-size: 12px;
}
.value {
margin: 4px 0 8px 0;
.Num {
line-height: 24px;
color: rgba(17, 24, 39, 1);
font-size: 20px;
font-weight: bold;
margin-right: 8px;
}
}
.tip {
line-height: 14px;
color: rgba(156, 163, 175, 1);
font-size: 11px;
text-align: left;
}
}
}
& > .tip {
margin-top: 16px;
height: 42px;
border-radius: 12px 12px 12px 12px;
background-color: rgba(241, 245, 249, 1);
padding: 12px;
line-height: 18px;
color: rgba(100, 116, 139, 1);
font-size: 12px;
text-align: left;
}
}
}
.footer {
margin-top: 20px;
width: 100%;
text-align: right;
}
}
</style>

View File

@@ -0,0 +1,366 @@
<template>
<div class="p-2 page-shell system-device-page">
<div class="search-wrap">
<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-item label="设备名称" prop="name">
<el-input v-model="queryParams.name" clearable placeholder="请输入设备名称" size="large"
@keyup.enter="handleQuery"/>
</el-form-item>
<el-form-item label="产品型号" prop="model">
<el-input v-model="queryParams.model" clearable placeholder="请输入产品型号" size="large"
@keyup.enter="handleQuery"/>
</el-form-item>
<el-form-item label="在线状态" prop="status">
<el-select v-model="queryParams.status" clearable placeholder="请选择状态" size="large">
<el-option :value="true" label="是"/>
<el-option :value="false" label="否"/>
</el-select>
</el-form-item>
<el-form-item>
<el-button size="large" type="primary" @click="handleQuery">搜索</el-button>
<el-button size="large" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<el-card class="table-panel" shadow="hover">
<template #header>
<div class="toolbar-shell">
<div class="table-heading">
<el-button type="primary">
<span style="margin:0 8px;color: #fff;">全部</span>
<span style="color: #fff;font-weight: bold">1286</span>
</el-button>
</div>
<div class="toolbar-actions">
<el-button v-hasPermi="['system:device:export']" plain size="large" type="info" @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="name"/>
<el-table-column align="center" label="产品型号" prop="model"/>
<el-table-column align="center" label="所在房间" prop="homeId"/>
<el-table-column align="center" label="在线状态" prop="status">
<template #default>
<el-tag type="success">在线</el-tag>
<el-tag type="danger">离线</el-tag>
</template>
</el-table-column>
<el-table-column align="center" label="当前TVOC" prop="tvoc">
<template #default="scope">
<div>
<div>{{ scope.row.tvoc ?? '--' }}</div>
<div>
<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 == '2'" type="warning">差</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>
</template>
</el-table-column>
<el-table-column align="center" label="滤芯寿命" prop="fwVersion">
<template #default="scope">
<span :class="{
lowLife: scope.row.filterLife < 50
}" class="defaultLife">{{ scope.row.filterLife }}</span>
</template>
</el-table-column>
<el-table-column align="center" label="电池电量" prop="deviceBattery">
<template #default="scope">
<span :class="{
lowBattery: scope.row.filterLife < 50
}" class="defaultLife">{{ scope.row.filterLife }}</span>
</template>
</el-table-column>
<el-table-column align="center" label="运行模式" prop="runMode">
<template #default="scope">
<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 style="color: rgba(17, 24, 39, 1);">自动</span>
</template>
</el-table-column>
<el-table-column align="center" label="风速挡位" prop="mac">
<template #default="scope">
<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 style="color: rgba(17, 24, 39, 1);">标准</span>
</template>
</el-table-column>
<el-table-column align="center" label="创建时间" prop="wifiSsid"/>
<el-table-column align="center" class-name="small-padding fixed-width" label="操作">
<template #default="scope">
<el-button v-hasPermi="['system:device:edit']" plain type="primary"
@click="handleUpdate(scope.row)">查看详情
</el-button>
<el-button v-hasPermi="['system:device:remove']" plain type="info"
@click="handleDelete(scope.row)">编辑
</el-button>
</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>
</div>
</template>
<script lang="ts" name="Device" setup>
import {
delDevice,
listDevice,
} 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 modal from '@/plugins/modal';
import {download as requestDownload} from '@/utils/request';
const router = useRouter();
const deviceList = ref<DeviceVO[]>([]);
const {loading, withLoading} = useLoading(true);
const {showSearch} = useSearchToggle();
const total = ref(0);
const {
applyDateRange: applyBindTimeDateRange,
resetDateRange: resetBindTimeDateRange
} = useDateRangeQuery('BindTime');
const {
applyDateRange: applyLastReportTimeDateRange,
resetDateRange: resetLastReportTimeDateRange
} = useDateRangeQuery('LastReportTime');
const {
applyDateRange: applyLastOnlineTimeDateRange,
resetDateRange: resetLastOnlineTimeDateRange
} = useDateRangeQuery('LastOnlineTime');
const {
applyDateRange: applyCreateTimeDateRange,
resetDateRange: resetCreateTimeDateRange
} = useDateRangeQuery('CreateTime');
const {
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} = toRefs(data);
const {ids, handleSelectionChange} = useTableSelection<DeviceVO>(item => item.deviceId);
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 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();
}
});
function handleUpdate(row: Partial<DeviceVO>) {
router.push({
path: '/device/deviceMain',
query: {
id: row.deviceId,
}
})
}
/** 删除按钮操作 */
const handleDelete = async (row?: Partial<DeviceVO>) => {
const _deviceIds = row?.deviceId || ids.value;
await modal.confirm('是否确认删除该设备?');
await delDevice(_deviceIds);
modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
requestDownload(
'system/device/export',
{
...queryParams.value
},
`device_${new Date().getTime()}.xlsx`
);
};
onMounted(() => {
getList();
});
</script>
<style lang="scss" scoped>
.defaultLife {
color: gray;
&.lowLife {
color: red;
}
&.lowBattery {
color: #4B5563;
}
}
</style>

View File

@@ -0,0 +1,611 @@
<script lang="ts" setup>
import cardNav from '@/components/cardNav/index.vue';
import {getDevice} from "@/api/system/device";
import {DeviceForm} from "@/api/system/device/types";
const router = useRouter();
function backList() {
router.replace({
path: '/device/deviceList'
})
}
function editDevice() {
router.push({
path: '/device/deviceEdit'
})
}
const route = useRoute();
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 form = ref();
function init() {
form.value = {...initFormData};
console.log(form.value);
if (route.query.id) {
getDevice(route.query.id as string).then(res => {
Object.assign(form.value, res.data);
});
}
}
init()
</script>
<template>
<div class="Main">
<nav class="flex justify-between">
<Pagecrumb></Pagecrumb>
<div>
<el-button plain type="info" @click="backList">返回设备列表</el-button>
<el-button type="primary" @click="editDevice">编辑设备</el-button>
</div>
</nav>
<div class="infoBox">
<div class="info-item">
<el-image class="info_img"
src="https://ts2.tc.mm.bing.net/th/id/OIP-C.2KXRoOZ1EPBDMf6jrp6xeAHaE7?r=0&rs=1&pid=ImgDetMain&o=7&rm=3"></el-image>
<div class="info">
<div class="nav">
<div class="title">{{ form.name }}</div>
<div class="tag online">
<i class="circle"></i>
<span>在线</span>
</div>
</div>
<div class="info-list">
<div class="info-list-item">
<div class="title">设备ID</div>
<div class="value">{{ form.deviceid }}</div>
</div>
<div class="info-list-item">
<div class="title">MAC地址</div>
<div class="value">{{ form.mac }}</div>
</div>
<div class="info-list-item">
<div class="title">所在房间</div>
<div class="value">{{ form.locationName }}</div>
</div>
<div class="info-list-item">
<div class="title">产品型号</div>
<div class="value">{{ form.model }}</div>
</div>
</div>
</div>
</div>
<div class="timeBox">
<div class="title">最后上报时间</div>
<div class="time">{{ form.lastReportTime }}</div>
<div class="tip">
<i class="circle"></i>
<span>每30s自动刷新</span>
</div>
</div>
</div>
<div class="baseInfo">
<cardNav subTitle="静态档案信息,如需修改请前往「编辑设备」页" title="设备基础档案"></cardNav>
<div class=" gridBox">
<div class="grid-item">
<div class="title">设备ID</div>
<div class="value">{{ form.deviceId }}</div>
</div>
<div class="grid-item">
<div class="title">设备名称</div>
<div class="value">{{ form.name }}</div>
</div>
<div class="grid-item">
<div class="title">MAC地址</div>
<div class="value">{{ form.mac }}</div>
</div>
<div class="grid-item">
<div class="title">产品型号</div>
<div class="value">{{ form.model }}</div>
</div>
<div class="grid-item">
<div class="title">所在房间</div>
<div class="value">{{ form.homeName }}</div>
</div>
<div class="grid-item">
</div>
<div class="grid-item">
<div class="title">档案创建时间</div>
<div class="value">{{ form.createTime }}</div>
</div>
<div class="grid-item">
<div class="title">档案更新时间</div>
<div class="value">{{ form.updateTime }}</div>
</div>
</div>
</div>
<div class="readTimeBox">
<cardNav line-color="#14b8a6" subTitle="设备蓝牙上报 · 只读 · 每 30 秒自动刷新"
title="设备实时运行状态"></cardNav>
<div class="flexBox">
<div class="flex-item">
<div class="flex-item-nav">
<i class="circle"></i>
<span>环境数据</span>
</div>
<div class="numBox">
<div class="num">{{ form.tvoc }}</div>
<div class="tag">
<i class="circle"></i>
<span>在线</span>
</div>
</div>
<div class="subTitle">TVOC 传感器原始读数(未换算为浓度值)</div>
<div class="footer">
<span>数据更新时间</span>
<span style="color: rgba(17, 24, 39, 1);">2026-09-30 12:50:30</span>
</div>
</div>
<div class="flex-item flex flex-col">
<div class="flex-item-nav">
<i class="circle" style="background: #2563eb"></i>
<span>运行数据</span>
</div>
<div class="flex-1 flex flex-col justify-evenly">
<div class="footer" style="margin-top: 0">
<span>运行模式</span>
<div>
<span v-if="form.runMode == '2'" style="color: rgba(17, 24, 39, 1);">桌面模式</span>
<span v-else-if="form.runMode == '1'" style="color: rgba(17, 24, 39, 1);">车载模式</span>
<span v-else style="color: rgba(17, 24, 39, 1);">自动</span>
</div>
</div>
<div class="footer" style="margin-top: 0">
<span>风速挡位</span>
<div>
<span v-if="form.fanLevel == '1'" style="color: rgba(17, 24, 39, 1);">强力</span>
<span v-else-if="form.fanLevel == '2'" style="color: rgba(17, 24, 39, 1);">低档</span>
<span v-else style="color: rgba(17, 24, 39, 1);">标准</span>
</div>
</div>
<div class="footer" style="margin-top: 0">
<span>供电模式</span>
<div>
<span v-if="form.powerMode == '0'" style="color: rgba(17, 24, 39, 1);">USB供电</span>
<span v-else style="color: rgba(17, 24, 39, 1);">电池供电</span>
</div>
</div>
<div class="footer" style="margin-top: 0">
<span>停车关机模式</span>
<el-tag v-if="form.parkingShutdown == '1'" type="success">开启</el-tag>
<el-tag v-else type="danger">关闭</el-tag>
</div>
</div>
</div>
<div class="flex-item flex flex-col ">
<div class="flex-item-nav">
<i class="circle" style="background: #f59e0b"></i>
<span>耗材与电量</span>
</div>
<div class="flex-1 flex justify-evenly">
<div class="ageBox">
<div class="title">滤芯寿命</div>
<div class="processInfo">
<span class="num">76</span>
<span class="unit">%</span>
</div>
<el-progress :percentage="30" :show-text="false"></el-progress>
<div class="tip">
<span>预计剩余可用128天</span>
</div>
</div>
<div class="line"></div>
<div class="ageBox BatteryBox">
<div class="title">滤芯寿命</div>
<div class="processInfo">
<span class="num">76</span>
<span class="unit">%</span>
</div>
<el-progress :percentage="30" :show-text="false" color="#22C55E"></el-progress>
<el-tag type="success">充电中</el-tag>
</div>
</div>
</div>
<div class="flex-item flex flex-col ">
<div class="flex-item-nav">
<i class="circle" style="background: #6366f1"></i>
<span>策略说明 · 停车关机模式</span>
</div>
<ul style="margin-top: 16px;">
<li class="modelLi">
<el-tag type="success">开启</el-tag>
<span class="describe">USB 外部供电断开后,电池供电继续运行 5 分钟消杀再自动关机;低电量 SOC<10% 时关闭等离子消杀;SOC<5% 整机锁机(防止过放)。</span>
</li>
<li class="modelLi">
<el-tag type="info">关闭</el-tag>
<span class="describe">不执行该套低电量策略,仅保留基础净化逻辑。</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.Main {
height: 100%;
width: 100%;
overflow: auto;
.infoBox {
padding: 24px;
display: flex;
justify-content: space-between;
align-items: center;
max-height: 177px;
min-width: 750px;
border-radius: 16px 16px 16px 16px;
border: 1px solid rgba(238, 240, 243, 1);
margin-top: 8px;
.info-item {
display: flex;
align-items: center;
.info_img {
width: 97px;
height: 97px;
border-radius: 16px 16px 16px 16px;
}
.info {
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: 24px;
.nav {
display: flex;
align-items: end;
.title {
height: 28px;
line-height: 28px;
color: rgba(17, 24, 39, 1);
font-size: 22px;
text-align: left;
}
.tag {
margin-left: 5px;
width: 62px;
height: 28px;
border-radius: 14px;
&.online {
background-color: rgba(232, 247, 238, 1);
color: rgba(22, 163, 74, 1);
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
.circle {
margin-right: 6px;
display: block;
width: 7px;
height: 6px;
border-radius: 3px;
background-color: rgba(22, 163, 74, 1);
}
}
}
}
.info-list {
display: flex;
align-items: center;
gap: 20px;
margin-top: 28px;
.info-list-item {
.title {
height: 16px;
line-height: 16px;
color: rgba(156, 163, 175, 1);
font-size: 12px;
}
.value {
height: 40px;
line-height: 20px;
color: rgba(17, 24, 39, 1);
font-size: 14px;
}
}
}
}
}
.timeBox {
.title {
text-align: right;
line-height: 16px;
color: rgba(156, 163, 175, 1);
font-size: 12px;
}
.time {
line-height: 22px;
background-color: rgba(0, 0, 0, 0);
color: rgba(17, 24, 39, 1);
font-size: 16px;
margin-top: 6px;
margin-bottom: 12px;
}
.tip {
display: flex;
align-items: center;
justify-content: end;
span {
background-color: rgba(0, 0, 0, 0);
color: rgba(37, 99, 235, 1);
font-size: 12px;
text-align: left;
}
.circle {
margin-right: 6px;
display: block;
width: 7px;
height: 6px;
border-radius: 3px;
background-color: #2563eb;
}
}
}
}
.baseInfo {
padding: 25px;
min-height: 248px;
border-radius: 16px 16px 16px 16px;
border: 1px solid rgba(238, 240, 243, 1);
margin: 16px 0;
.gridBox {
margin-top: 44px;
display: grid;
grid-template-columns: repeat(4, minmax(381px, 1fr));
gap: 20px;
@media (max-width: 1659px) {
grid-template-columns: repeat(3, minmax(381px, 1fr));
}
@media (max-width: 1250px) {
grid-template-columns: repeat(2, minmax(381px, 1fr));
}
@media (max-width: 600px) {
grid-template-columns: repeat(1, minmax(381px, 1fr));
}
.grid-item {
max-width: 381px;
.title {
height: 16px;
line-height: 16px;
color: rgba(156, 163, 175, 1);
font-size: 12px;
margin-bottom: 6px;
}
.value {
height: 20px;
line-height: 20px;
color: rgba(17, 24, 39, 1);
font-size: 14px;
text-align: left;
}
}
}
}
.readTimeBox {
padding: 25px;
border-radius: 16px 16px 16px 16px;
border: 1px solid rgba(238, 240, 243, 1);
.flexBox {
margin-top: 44px;
gap: 20px;
display: grid;
grid-template-columns: repeat(2, minmax(381px, 1fr));
@media (max-width: 900px) {
grid-template-columns: repeat(1, minmax(381px, 1fr));
}
.flex-item {
min-height: 201px;
border-radius: 12px 12px 12px 12px;
background-color: rgba(247, 249, 252, 1);
border: 1px solid rgba(238, 240, 243, 1);
padding: 21px;
.flex-item-nav {
display: flex;
align-items: center;
.circle {
margin-right: 6px;
display: block;
width: 7px;
height: 6px;
border-radius: 3px;
background-color: rgba(22, 163, 74, 1);
}
span {
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
}
}
.numBox {
display: flex;
align-items: end;
margin-top: 17px;
.num {
line-height: 43px;
color: rgba(17, 24, 39, 1);
font-size: 36px;
text-align: left;
}
.tag {
margin-left: 5px;
width: 62px;
height: 28px;
border-radius: 14px;
$backColor: rgba(232, 247, 238, 1);
$textColor: rgba(22, 163, 74, 1);
background-color: $backColor;
color: $textColor;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
.circle {
margin-right: 6px;
display: block;
width: 7px;
height: 6px;
border-radius: 3px;
background-color: $textColor;
}
}
}
.subTitle {
margin-top: 12px;
line-height: 16px;
color: rgba(156, 163, 175, 1);
font-size: 12px;
}
.footer {
margin-top: 44px;
display: flex;
align-items: center;
justify-content: space-between;
line-height: 14px;
color: rgba(107, 114, 128, 1);
font-size: 12px;
}
// 滤芯寿命
.ageBox {
flex: 1;
margin-top: 16px;
.title {
line-height: 18px;
color: rgba(107, 114, 128, 1);
font-size: 13px;
}
.processInfo {
margin: 12px 0;
.num {
line-height: 31px;
color: rgba(17, 24, 39, 1);
font-size: 26px;
}
.unit {
line-height: 17px;
color: rgba(107, 114, 128, 1);
font-size: 14px;
margin-left: 4px;
}
}
.tip {
line-height: 16px;
color: rgba(156, 163, 175, 1);
font-size: 12px;
}
}
.line {
width: 2px;
height: 96px;
background-color: rgba(238, 240, 243, 1);
margin: 15px 24px;
}
.modelLi {
margin-bottom: 12px;
span.describe {
margin-left: 12px;
line-height: 16px;
color: rgba(75, 85, 99, 1);
font-size: 13px;
text-align: left;
}
}
}
}
}
}
</style>

View File

@@ -12,7 +12,7 @@
</template>
<el-form ref="queryFormRef" :model="queryParams" :inline="true" class="query-form">
<el-form-item label="文件名" prop="fileName">
<el-input v-model="queryParams.fileName" placeholder="请输入文件名" clearable @keyup.enter="handleQuery" />
<el-input v-model="queryParams.fileName" clearable placeholder="请输入文件名" @keyup.enter="handleQuery"/>
</el-form-item>
<el-form-item label="原名" prop="originalName">
<el-input
@@ -42,7 +42,7 @@
></el-date-picker>
</el-form-item>
<el-form-item label="服务商" prop="service">
<el-input v-model="queryParams.service" placeholder="请输入服务商" clearable @keyup.enter="handleQuery" />
<el-input v-model="queryParams.service" clearable placeholder="请输入服务商" @keyup.enter="handleQuery"/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
@@ -109,11 +109,11 @@
@selection-change="handleSelectionChange"
@header-click="handleHeaderCLick"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column v-if="false" label="对象存储主键" align="center" prop="ossId" />
<el-table-column label="文件名" align="center" prop="fileName" />
<el-table-column label="原名" align="center" prop="originalName" />
<el-table-column label="文件后缀" align="center" prop="fileSuffix" />
<el-table-column align="center" type="selection" width="55"/>
<el-table-column v-if="false" align="center" label="对象存储主键" prop="ossId"/>
<el-table-column align="center" label="文件名" prop="fileName"/>
<el-table-column align="center" label="原名" prop="originalName"/>
<el-table-column align="center" label="文件后缀" prop="fileSuffix"/>
<el-table-column label="文件展示" align="center" prop="url">
<template #default="scope">
<ImagePreview
@@ -131,8 +131,8 @@
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="上传人" align="center" prop="createByName" />
<el-table-column label="服务商" align="center" prop="service" sortable="custom" />
<el-table-column align="center" label="上传人" prop="createByName"/>
<el-table-column align="center" label="服务商" prop="service" sortable="custom"/>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="下载" placement="top">
@@ -267,21 +267,25 @@ const getList = async () => {
showTable.value = true;
});
};
function checkFileSuffix(fileSuffix: string | string[]) {
const arr = ['.png', '.jpg', '.jpeg'];
const suffixArray = Array.isArray(fileSuffix) ? fileSuffix : [fileSuffix];
return suffixArray.some(suffix => arr.includes(suffix.toLowerCase()));
}
/** 取消按钮 */
function cancel() {
reset();
closeDialog();
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
const {resetQuery} = useSearchReset({
queryFormRef,
queryParams,
@@ -343,7 +347,7 @@ const handleOrderChange = (prop: string, order: string) => {
};
/** 任务日志列表查询 */
const handleOssConfig = () => {
router.push('/system/oss-config/index');
router.push('/system/oss-config/index.vue');
};
/** 文件按钮操作 */
const handleFile = () => {

View File

@@ -18,7 +18,7 @@
</el-select>
</el-form-item>
<el-form-item label="表名称" prop="tableName">
<el-input v-model="queryParams.tableName" placeholder="请输入表名称" clearable @keyup.enter="handleQuery" />
<el-input v-model="queryParams.tableName" clearable placeholder="请输入表名称" @keyup.enter="handleQuery"/>
</el-form-item>
<el-form-item label="表描述" prop="tableComment">
<el-input
@@ -99,12 +99,12 @@
<span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
</template>
</el-table-column>
<el-table-column label="数据源" align="center" prop="dataName" :show-overflow-tooltip="true" />
<el-table-column label="表名称" align="center" prop="tableName" :show-overflow-tooltip="true" />
<el-table-column label="表描述" align="center" prop="tableComment" :show-overflow-tooltip="true" />
<el-table-column label="实体" align="center" prop="className" :show-overflow-tooltip="true" />
<el-table-column label="创建时间" align="center" prop="createTime" width="160" />
<el-table-column label="更新时间" align="center" prop="updateTime" width="160" />
<el-table-column :show-overflow-tooltip="true" align="center" label="数据源" prop="dataName"/>
<el-table-column :show-overflow-tooltip="true" align="center" label="表名称" prop="tableName"/>
<el-table-column :show-overflow-tooltip="true" align="center" label="表描述" prop="tableComment"/>
<el-table-column :show-overflow-tooltip="true" align="center" label="实体" prop="className"/>
<el-table-column align="center" label="创建时间" prop="createTime" width="160"/>
<el-table-column align="center" label="更新时间" prop="updateTime" width="160"/>
<el-table-column label="操作" align="center" width="330" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="预览" placement="top">
@@ -314,7 +314,7 @@ const copyTextSuccess = () => {
const handleEditTable = (row?: Partial<TableVO>) => {
const tableId = row?.tableId || ids.value[0];
router.push({
path: '/tool/gen-edit/index/' + tableId,
path: '/tool/gen-edit/index.vue/' + tableId,
query: {pageNum: queryParams.value.pageNum}
});
};
@@ -347,6 +347,7 @@ onMounted(() => {
.el-tab-pane {
background-color: #282c34;
.el-link {
color: #fff;
}

View File

@@ -4,6 +4,7 @@ import autoprefixer from 'autoprefixer'; // css自动添加兼容性前缀
export default defineConfig(({mode, command}) => {
const env = loadEnv(mode, process.cwd());
console.log(env);
return {
// 部署生产环境和开发环境下的URL。
// 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
@@ -30,7 +31,7 @@ export default defineConfig(({ mode, command }) => {
open: true,
proxy: {
[env.VITE_APP_BASE_API]: {
target: 'http://localhost:5660',
target: env.VITE_LOCALHOUSET === 'false' ? 'http://localhost:5660' : env.VITE_BASE_API,
changeOrigin: true,
ws: true,
rewrite: path => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')