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_APP_BASE_API='/dev-api'
# 本机环境
VITE_LOCALHOUSET=true
# 本地开发环境
VITE_BASE_API='http://192.168.0.212:8082'
# 应用访问路径 例如使用前缀 /admin/ # 应用访问路径 例如使用前缀 /admin/
VITE_APP_CONTEXT_PATH='/' 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,9 +8,8 @@ body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
background: var(--app-shell-bg); background: var(--app-shell-bg);
font-family: font-family: 'MiSans', 'HarmonyOS Sans SC', 'PingFang SC', 'Source Han Sans SC', 'Noto Sans SC', 'Hiragino Sans GB',
'MiSans', 'HarmonyOS Sans SC', 'PingFang SC', 'Source Han Sans SC', 'Noto Sans SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
'Microsoft YaHei', sans-serif;
} }
label { label {
@@ -88,3 +87,18 @@ h6 {
line-height: 1.1; line-height: 1.1;
color: inherit; 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。 */ * 属「页面模式」而非 EP 公共主题,故放在 components。 */
// --- 历史「表单分区标题」样式 --- // --- 历史「表单分区标题」样式 ---
@@ -51,9 +51,8 @@
box-shadow: var(--app-shadow-sm); box-shadow: var(--app-shadow-sm);
border-color: var(--app-surface-border); border-color: var(--app-surface-border);
overflow: hidden; overflow: hidden;
transition: transition: box-shadow 0.2s ease,
box-shadow 0.2s ease, border-color 0.2s ease;
border-color 0.2s ease;
background: var(--app-surface-bg); background: var(--app-surface-bg);
} }

View File

@@ -22,6 +22,7 @@
margin: 0; margin: 0;
height: 40px; height: 40px;
color: var(--side-menu-text) !important; color: var(--side-menu-text) !important;
&:hover { &:hover {
background-color: var(--side-menu-hover-bg) !important; background-color: var(--side-menu-hover-bg) !important;
color: var(--side-menu-hover-text) !important; color: var(--side-menu-hover-text) !important;
@@ -54,12 +55,12 @@
//margin: 3px 8px; //margin: 3px 8px;
//background: transparent !important; //background: transparent !important;
background: #f7f7f7 !important; background: #f7f7f7 !important;
&.is-active { &.is-active {
background: var(--side-menu-active-bg) !important; background: var(--side-menu-active-bg) !important;
color: var(--side-menu-active-text) !important; color: var(--side-menu-active-text) !important;
box-shadow: box-shadow: inset 3px 0 0 var(--side-menu-active-line),
inset 3px 0 0 var(--side-menu-active-line), inset 0 0 0 1px var(--side-menu-active-border);
inset 0 0 0 1px var(--side-menu-active-border);
} }
&:not(.is-active):hover { &:not(.is-active):hover {
@@ -75,12 +76,12 @@
//margin: 3px 8px; //margin: 3px 8px;
//background: transparent !important; //background: transparent !important;
background: #f7f7f7 !important; background: #f7f7f7 !important;
&.is-active { &.is-active {
background: var(--side-menu-active-bg) !important; background: var(--side-menu-active-bg) !important;
color: var(--side-menu-active-text) !important; color: var(--side-menu-active-text) !important;
box-shadow: box-shadow: inset 3px 0 0 var(--side-menu-active-line),
inset 3px 0 0 var(--side-menu-active-line), inset 0 0 0 1px var(--side-menu-active-border);
inset 0 0 0 1px var(--side-menu-active-border);
} }
&:not(.is-active):hover { &:not(.is-active):hover {
@@ -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-light: var(--app-shadow-sm);
--el-box-shadow: var(--app-shadow-md); --el-box-shadow: var(--app-shadow-md);
--el-bg-color-page: var(--app-shell-bg); --el-bg-color-page: var(--app-shell-bg);
} }

View File

@@ -28,12 +28,21 @@
// --- primary 品牌色(亮色;暗色见 tokens/_theme-dark) --- // --- primary 品牌色(亮色;暗色见 tokens/_theme-dark) ---
.el-button--primary { .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-border-color: var(--app-accent-strong);
--el-button-hover-bg-color: #4a7cff; --el-button-hover-bg-color: #4a7cff;
--el-button-hover-border-color: #4a7cff; --el-button-hover-border-color: #4a7cff;
--el-button-active-bg-color: #2e5ce0; --el-button-active-bg-color: #2e5ce0;
--el-button-active-border-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 --- // --- 表格 CSS 变量接业务 token ---
@@ -82,3 +91,4 @@ html.dark .el-table {
.el-select__wrapper { .el-select__wrapper {
box-shadow: 0 0 0 1px var(--el-border-color) inset; box-shadow: 0 0 0 1px var(--el-border-color) inset;
} }

View File

@@ -11,8 +11,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { RouteLocationMatched } from 'vue-router'; import {RouteLocationMatched} from 'vue-router';
import { usePermissionStore } from '@/store/modules/permission'; import {usePermissionStore} from '@/store/modules/permission';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@@ -36,7 +36,7 @@ const getBreadcrumb = () => {
} }
// 判断是否为首页 // 判断是否为首页
if (!isDashboard(matched[0])) { 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); levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false);
}; };
@@ -62,7 +62,7 @@ const isDashboard = (route: RouteLocationMatched) => {
return name.trim() === 'Index'; return name.trim() === 'Index';
}; };
const handleLink = item => { const handleLink = item => {
const { redirect, path } = item; const {redirect, path} = item;
redirect ? router.push(redirect) : router.push(path); redirect ? router.push(redirect) : router.push(path);
}; };

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

@@ -26,7 +26,7 @@
<template #title>更多菜单</template> <template #title>更多菜单</template>
<template v-for="(item, index) in topMenus"> <template v-for="(item, index) in topMenus">
<el-menu-item v-if="index >= visibleNumber" :key="index" :index="item.path"> <el-menu-item v-if="index >= visibleNumber" :key="index" :index="item.path">
<svg-icon :icon-class="item.meta ? item.meta.icon : ''" /> <svg-icon :icon-class="item.meta ? item.meta.icon : ''"/>
{{ item.meta?.title }} {{ item.meta?.title }}
</el-menu-item> </el-menu-item>
</template> </template>
@@ -35,17 +35,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { RouteRecordRaw } from 'vue-router'; import {RouteRecordRaw} from 'vue-router';
import { constantRoutes } from '@/router'; import {constantRoutes} from '@/router';
import { useAppStore } from '@/store/modules/app'; import {useAppStore} from '@/store/modules/app';
import { usePermissionStore } from '@/store/modules/permission'; import {usePermissionStore} from '@/store/modules/permission';
import { useSettingsStore } from '@/store/modules/settings'; import {useSettingsStore} from '@/store/modules/settings';
import { isHttp } from '@/utils/validate'; import {isHttp} from '@/utils/validate';
// 顶部栏初始数 // 顶部栏初始数
const visibleNumber = ref<number>(-1); const visibleNumber = ref<number>(-1);
// 隐藏侧边栏路由 // 隐藏侧边栏路由
const hideList = ['/index', '/user/profile']; const hideList = ['/index.vue', '/user/profile'];
const appStore = useAppStore(); const appStore = useAppStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
@@ -97,9 +97,9 @@ const childrenMenus = computed(() => {
// 默认激活的菜单 // 默认激活的菜单
const activeMenu = computed(() => { const activeMenu = computed(() => {
const { meta } = route; const {meta} = route;
let path = meta.activeMenu || route.path; let path = meta.activeMenu || route.path;
if (path === '/index' || route.path === '/index') { if (path === '/index.vue' || route.path === '/index.vue') {
path = '/system/user'; path = '/system/user';
} }
let activePath = path; let activePath = path;
@@ -135,9 +135,9 @@ const handleSelect = (key: string) => {
const routeMenu = childrenMenus.value.find(item => item.path === key); const routeMenu = childrenMenus.value.find(item => item.path === key);
if (routeMenu && routeMenu.query) { if (routeMenu && routeMenu.query) {
const query = JSON.parse(routeMenu.query); const query = JSON.parse(routeMenu.query);
router.push({ path: key, query: query }); router.push({path: key, query: query});
} else { } else {
router.push({ path: key }); router.push({path: key});
} }
appStore.toggleSideBarHide(true); appStore.toggleSideBarHide(true);
} else { } else {
@@ -214,12 +214,11 @@ onMounted(() => {
border-radius: 13px; border-radius: 13px;
color: var(--topbar-pill-text) !important; color: var(--topbar-pill-text) !important;
background: transparent !important; background: transparent !important;
transition: transition: background-color 0.2s ease,
background-color 0.2s ease, color 0.2s ease,
color 0.2s ease, box-shadow 0.2s ease,
box-shadow 0.2s ease, border-color 0.2s ease,
border-color 0.2s ease, transform 0.2s ease;
transform 0.2s ease;
} }
#app .mix-topnav-menu.el-menu--horizontal > .el-menu-item:hover, #app .mix-topnav-menu.el-menu--horizontal > .el-menu-item:hover,
@@ -305,11 +304,10 @@ onMounted(() => {
border-radius: 12px; border-radius: 12px;
color: var(--app-text-title) !important; color: var(--app-text-title) !important;
background: transparent !important; background: transparent !important;
transition: transition: background-color 0.2s ease,
background-color 0.2s ease, color 0.2s ease,
color 0.2s ease, box-shadow 0.2s ease,
box-shadow 0.2s ease, transform 0.2s ease;
transform 0.2s ease;
} }
.mix-topnav-popper .el-menu--popup .el-menu-item:last-child, .mix-topnav-popper .el-menu--popup .el-menu-item:last-child,
@@ -365,9 +363,8 @@ html.dark .mix-topnav-menu.el-menu--horizontal {
html.dark .mix-topnav-popper .el-menu--popup { html.dark .mix-topnav-popper .el-menu--popup {
border-color: rgba(71, 85, 105, 0.34); 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; background: linear-gradient(180deg, rgba(15, 23, 42, 0.98), rgba(30, 41, 59, 0.94)) !important;
box-shadow: box-shadow: 0 20px 42px rgba(0, 0, 0, 0.34),
0 20px 42px rgba(0, 0, 0, 0.34), inset 0 1px 0 rgba(255, 255, 255, 0.06);
inset 0 1px 0 rgba(255, 255, 255, 0.06);
} }
html.dark .mix-topnav-popper .el-menu--popup .el-menu-item, html.dark .mix-topnav-popper .el-menu--popup .el-menu-item,

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,21 +1,21 @@
<template> <template>
<section class="app-main"> <section class="app-main beautify_scroll">
<router-view v-slot="{ Component, route }"> <router-view v-slot="{ Component, route }">
<transition :enter-active-class="animate" mode="out-in"> <transition :enter-active-class="animate" mode="out-in">
<keep-alive :include="tagsViewStore.cachedViews"> <keep-alive :include="tagsViewStore.cachedViews">
<component :is="Component" v-if="!route.meta.link" :key="route.path" /> <component :is="Component" v-if="!route.meta.link" :key="route.path"/>
</keep-alive> </keep-alive>
</transition> </transition>
</router-view> </router-view>
<iframe-toggle /> <iframe-toggle/>
</section> </section>
</template> </template>
<script setup name="AppMain" lang="ts"> <script setup name="AppMain" lang="ts">
import animateConfig from '@/animate'; import animateConfig from '@/animate';
import { useFullHeightTable } from '@/hooks/table/useFullHeightTable'; import {useFullHeightTable} from '@/hooks/table/useFullHeightTable';
import { useSettingsStore } from '@/store/modules/settings'; import {useSettingsStore} from '@/store/modules/settings';
import { useTagsViewStore } from '@/store/modules/tagsView'; import {useTagsViewStore} from '@/store/modules/tagsView';
import IframeToggle from './IframeToggle/index.vue'; import IframeToggle from './IframeToggle/index.vue';
const route = useRoute(); const route = useRoute();
@@ -33,7 +33,7 @@ watch(
animate.value = animateConfig.defaultAnimate as string; animate.value = animateConfig.defaultAnimate as string;
} }
}, },
{ immediate: true } {immediate: true}
); );
watchEffect(() => { watchEffect(() => {
@@ -50,10 +50,11 @@ function addIframe() {
<style lang="scss" scoped> <style lang="scss" scoped>
.app-main { .app-main {
min-height: 100vh; min-height: 100vh;
height: 100%;
width: 100%; width: 100%;
position: relative; position: relative;
overflow: hidden; overflow: auto;
//padding: 12px; padding: 0 12px;
&:fullscreen, &:fullscreen,
&:-webkit-full-screen, &:-webkit-full-screen,

View File

@@ -9,7 +9,7 @@
> >
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)"> <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }"> <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> <template #title>
<span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span> <span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span>
</template> </template>
@@ -26,7 +26,7 @@
teleported teleported
> >
<template v-if="item.meta" #title> <template v-if="item.meta" #title>
<svg-icon :icon-class="item.meta ? item.meta.icon : ''" /> <svg-icon :icon-class="item.meta ? item.meta.icon : ''"/>
<span class="menu-title" :title="hasTitle(item.meta?.title)">{{ item.meta?.title }}</span> <span class="menu-title" :title="hasTitle(item.meta?.title)">{{ item.meta?.title }}</span>
</template> </template>
@@ -44,9 +44,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { RouteRecordRaw } from 'vue-router'; import {RouteRecordRaw} from 'vue-router';
import { getNormalPath } from '@/utils/ruoyi'; import {getNormalPath} from '@/utils/ruoyi';
import { isExternal } from '@/utils/validate'; import {isExternal} from '@/utils/validate';
import AppLink from './Link.vue'; import AppLink from './Link.vue';
const props = defineProps({ const props = defineProps({
@@ -89,7 +89,7 @@ const hasOneShowingChild = (parent: RouteRecordRaw, children?: RouteRecordRaw[])
// Show parent if there are no child router to display // Show parent if there are no child router to display
if (showingChildren.length === 0) { if (showingChildren.length === 0) {
onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }; onlyOneChild.value = {...parent, path: '', noShowingChildren: true};
return true; return true;
} }

View File

@@ -1,7 +1,7 @@
<template> <template>
<div id="tags-view-container" class="tags-view-container"> <div id="tags-view-container" class="tags-view-container">
<span v-if="canScrollLeft" class="tags-nav-btn tags-nav-btn--left" @click="scrollLeft"> <span v-if="canScrollLeft" class="tags-nav-btn tags-nav-btn--left" @click="scrollLeft">
<el-icon><ArrowLeft /></el-icon> <el-icon><ArrowLeft/></el-icon>
</span> </span>
<scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll" @update-arrows="updateArrowState"> <scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll" @update-arrows="updateArrowState">
@@ -16,21 +16,21 @@
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : undefined" @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : undefined"
@contextmenu.prevent="openMenu(tag, $event)" @contextmenu.prevent="openMenu(tag, $event)"
> >
<svg-icon v-if="tagsIcon && tag.meta && tag.meta.icon && tag.meta.icon !== '#'" :icon-class="tag.meta.icon" /> <svg-icon v-if="tagsIcon && tag.meta && tag.meta.icon && tag.meta.icon !== '#'" :icon-class="tag.meta.icon"/>
<span class="tags-view-item-title">{{ tag.title || tag.meta?.title }}</span> <span class="tags-view-item-title">{{ tag.title || tag.meta?.title }}</span>
<span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)"> <span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
<Close class="el-icon-close" style="width: 1em; height: 1em; vertical-align: middle" /> <Close class="el-icon-close" style="width: 1em; height: 1em; vertical-align: middle"/>
</span> </span>
</router-link> </router-link>
</scroll-pane> </scroll-pane>
<span v-if="canScrollRight" class="tags-nav-btn tags-nav-btn--right" @click="scrollRight"> <span v-if="canScrollRight" class="tags-nav-btn tags-nav-btn--right" @click="scrollRight">
<el-icon><ArrowRight /></el-icon> <el-icon><ArrowRight/></el-icon>
</span> </span>
<el-dropdown class="tags-action-dropdown" trigger="click" placement="bottom-end" @command="handleDropdownCommand"> <el-dropdown class="tags-action-dropdown" trigger="click" placement="bottom-end" @command="handleDropdownCommand">
<span class="tags-action-btn"> <span class="tags-action-btn">
<el-icon><ArrowDown /></el-icon> <el-icon><ArrowDown/></el-icon>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu class="tags-dropdown-menu"> <el-dropdown-menu class="tags-dropdown-menu">
@@ -41,11 +41,11 @@
<el-dropdown-item command="closeAll">全部关闭</el-dropdown-item> <el-dropdown-item command="closeAll">全部关闭</el-dropdown-item>
<el-dropdown-item command="fullscreen" divided> <el-dropdown-item command="fullscreen" divided>
<template v-if="!isFullscreen"> <template v-if="!isFullscreen">
<FullScreen /> <FullScreen/>
<span>全屏显示</span> <span>全屏显示</span>
</template> </template>
<template v-else> <template v-else>
<CloseBold /> <CloseBold/>
<span>退出全屏</span> <span>退出全屏</span>
</template> </template>
</el-dropdown-item> </el-dropdown-item>
@@ -54,33 +54,33 @@
</el-dropdown> </el-dropdown>
<span class="tags-action-btn tags-refresh-btn" title="刷新页面" @click="refreshSelectedTag(selectedDropdownTag)"> <span class="tags-action-btn tags-refresh-btn" title="刷新页面" @click="refreshSelectedTag(selectedDropdownTag)">
<el-icon><RefreshRight /></el-icon> <el-icon><RefreshRight/></el-icon>
<span>刷新</span> <span>刷新</span>
</span> </span>
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu"> <ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
<li @click="refreshSelectedTag(selectedTag)"> <li @click="refreshSelectedTag(selectedTag)">
<RefreshRight style="width: 1em; height: 1em" /> <RefreshRight style="width: 1em; height: 1em"/>
刷新页面 刷新页面
</li> </li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"> <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
<Close style="width: 1em; height: 1em" /> <Close style="width: 1em; height: 1em"/>
关闭当前 关闭当前
</li> </li>
<li @click="closeOthersTags"> <li @click="closeOthersTags">
<CircleClose style="width: 1em; height: 1em" /> <CircleClose style="width: 1em; height: 1em"/>
关闭其他 关闭其他
</li> </li>
<li v-if="!isFirstView()" @click="closeLeftTags"> <li v-if="!isFirstView()" @click="closeLeftTags">
<Back style="width: 1em; height: 1em" /> <Back style="width: 1em; height: 1em"/>
关闭左侧 关闭左侧
</li> </li>
<li v-if="!isLastView()" @click="closeRightTags"> <li v-if="!isLastView()" @click="closeRightTags">
<Right style="width: 1em; height: 1em" /> <Right style="width: 1em; height: 1em"/>
关闭右侧 关闭右侧
</li> </li>
<li @click="closeAllTags(selectedTag)"> <li @click="closeAllTags(selectedTag)">
<CircleClose style="width: 1em; height: 1em" /> <CircleClose style="width: 1em; height: 1em"/>
全部关闭 全部关闭
</li> </li>
</ul> </ul>
@@ -88,7 +88,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'; import type {RouteLocationNormalized, RouteRecordRaw} from 'vue-router';
import { import {
ArrowDown, ArrowDown,
ArrowLeft, ArrowLeft,
@@ -102,10 +102,10 @@ import {
Right Right
} from '@element-plus/icons-vue'; } from '@element-plus/icons-vue';
import tab from '@/plugins/tab'; import tab from '@/plugins/tab';
import { usePermissionStore } from '@/store/modules/permission'; import {usePermissionStore} from '@/store/modules/permission';
import { useSettingsStore } from '@/store/modules/settings'; import {useSettingsStore} from '@/store/modules/settings';
import { useTagsViewStore } from '@/store/modules/tagsView'; import {useTagsViewStore} from '@/store/modules/tagsView';
import { getNormalPath } from '@/utils/ruoyi'; import {getNormalPath} from '@/utils/ruoyi';
import ScrollPane from './ScrollPane.vue'; import ScrollPane from './ScrollPane.vue';
const visible = ref(false); const visible = ref(false);
@@ -152,7 +152,7 @@ watch(
updateArrowState(); updateArrowState();
}); });
}, },
{ deep: true } {deep: true}
); );
const isActive = (currentRoute: RouteLocationNormalized): boolean => { const isActive = (currentRoute: RouteLocationNormalized): boolean => {
@@ -181,7 +181,7 @@ const isFirstView = () => {
return false; return false;
} }
try { try {
return tag.fullPath === '/index' || tag.fullPath === visitedViews.value[1]?.fullPath; return tag.fullPath === '/index.vue' || tag.fullPath === visitedViews.value[1]?.fullPath;
} catch { } catch {
return false; return false;
} }
@@ -213,7 +213,7 @@ const filterAffixTags = (routeList: RouteRecordRaw[], basePath = '') => {
fullPath: tagPath, fullPath: tagPath,
path: tagPath, path: tagPath,
name: item.name as string, name: item.name as string,
meta: { ...item.meta }, meta: {...item.meta},
title: item.meta?.title || 'no-name' title: item.meta?.title || 'no-name'
} as RouteLocationNormalized); } as RouteLocationNormalized);
} }
@@ -277,7 +277,7 @@ const closeSelectedTag = (view?: RouteLocationNormalized) => {
if (!view) { if (!view) {
return; return;
} }
tab.closePage(view).then(({ visitedViews }: { visitedViews: RouteLocationNormalized[] }) => { tab.closePage(view).then(({visitedViews}: { visitedViews: RouteLocationNormalized[] }) => {
if (isActive(view)) { if (isActive(view)) {
toLastView(visitedViews, view); toLastView(visitedViews, view);
} }
@@ -313,14 +313,15 @@ const closeOthersTags = () => {
if (!tag) { if (!tag) {
return; return;
} }
router.push(tag.fullPath || tag.path || '/').catch(() => {}); router.push(tag.fullPath || tag.path || '/').catch(() => {
});
tab.closeOtherPage(tag).then(() => { tab.closeOtherPage(tag).then(() => {
moveToCurrentTag(); moveToCurrentTag();
}); });
}; };
const closeAllTags = (view?: RouteLocationNormalized) => { const closeAllTags = (view?: RouteLocationNormalized) => {
tab.closeAllPage().then(({ visitedViews: views }: { visitedViews: RouteLocationNormalized[] }) => { tab.closeAllPage().then(({visitedViews: views}: { visitedViews: RouteLocationNormalized[] }) => {
if (affixTags.value.some(tag => tag.path === route.path)) { if (affixTags.value.some(tag => tag.path === route.path)) {
return; return;
} }
@@ -333,7 +334,7 @@ const toLastView = (views: RouteLocationNormalized[], view?: RouteLocationNormal
if (latestView?.fullPath) { if (latestView?.fullPath) {
router.push(latestView.fullPath); router.push(latestView.fullPath);
} else if (view?.name === 'Dashboard' && view.fullPath) { } else if (view?.name === 'Dashboard' && view.fullPath) {
router.replace({ path: '/redirect' + view.fullPath }); router.replace({path: '/redirect' + view.fullPath});
} else { } else {
router.push('/'); router.push('/');
} }
@@ -505,11 +506,10 @@ onBeforeUnmount(() => {
background-color: var(--app-surface-bg); background-color: var(--app-surface-bg);
border: 1px solid var(--app-surface-border); border: 1px solid var(--app-surface-border);
border-radius: var(--app-radius-md); border-radius: var(--app-radius-md);
transition: transition: box-shadow 0.2s ease,
box-shadow 0.2s ease, transform 0.2s ease,
transform 0.2s ease, border-color 0.2s ease,
border-color 0.2s ease, color 0.2s ease;
color 0.2s ease;
&:hover:not(.disabled) { &:hover:not(.disabled) {
background: $btn-hover-bg; background: $btn-hover-bg;
@@ -549,11 +549,10 @@ onBeforeUnmount(() => {
margin-left: 5px; margin-left: 5px;
margin-top: 4px; margin-top: 4px;
border-radius: var(--app-radius-md); border-radius: var(--app-radius-md);
transition: transition: box-shadow 0.2s ease,
box-shadow 0.2s ease, transform 0.2s ease,
transform 0.2s ease, border-color 0.2s ease,
border-color 0.2s ease, color 0.2s ease;
color 0.2s ease;
&:hover { &:hover {
color: var(--el-color-primary); color: var(--el-color-primary);
@@ -620,11 +619,10 @@ onBeforeUnmount(() => {
background-color: var(--app-surface-bg); background-color: var(--app-surface-bg);
border: 1px solid var(--app-surface-border); border: 1px solid var(--app-surface-border);
border-radius: var(--app-radius-md); border-radius: var(--app-radius-md);
transition: transition: box-shadow 0.2s ease,
box-shadow 0.2s ease, transform 0.2s ease,
transform 0.2s ease, border-color 0.2s ease,
border-color 0.2s ease, color 0.2s ease;
color 0.2s ease;
&:hover { &:hover {
background: $btn-hover-bg; background: $btn-hover-bg;

View File

@@ -1,24 +1,24 @@
<template> <template>
<div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }"> <div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" /> <div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
<side-bar v-if="showSidebar" class="sidebar-container" /> <side-bar v-if="showSidebar" class="sidebar-container"/>
<div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container"> <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
<div :class="{ 'fixed-header': fixedHeader }" class="layout-header"> <div :class="{ 'fixed-header': fixedHeader }" class="layout-header">
<navbar @set-layout="setLayout" /> <navbar @set-layout="setLayout"/>
<tags-view v-if="needTagsView" /> <tags-view v-if="needTagsView"/>
</div> </div>
<app-main :class="{ 'with-fixed-header': fixedHeader, 'with-tags-view': needTagsView }" /> <app-main :class="{ 'with-fixed-header': fixedHeader, 'with-tags-view': needTagsView }"/>
<settings ref="settingRef" /> <settings ref="settingRef"/>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { NavTypeEnum } from '@/enums/NavTypeEnum'; import {NavTypeEnum} from '@/enums/NavTypeEnum';
import { useAppStore } from '@/store/modules/app'; import {useAppStore} from '@/store/modules/app';
import { useSettingsStore } from '@/store/modules/settings'; import {useSettingsStore} from '@/store/modules/settings';
import { initMessageBox, initPush } from '@/utils/push'; import {initMessageBox, initPush} from '@/utils/push';
import { AppMain, Navbar, Settings, TagsView } from './components'; import {AppMain, Navbar, Settings, TagsView} from './components';
import SideBar from './components/Sidebar/index.vue'; import SideBar from './components/Sidebar/index.vue';
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
@@ -42,7 +42,7 @@ const classObj = computed(() => ({
mobile: device.value === 'mobile' mobile: device.value === 'mobile'
})); }));
const { width } = useWindowSize(); const {width} = useWindowSize();
const WIDTH = 992; // refer to Bootstrap's responsive design const WIDTH = 992; // refer to Bootstrap's responsive design
watch( watch(
@@ -50,12 +50,12 @@ watch(
w => { w => {
if (w - 1 < WIDTH) { if (w - 1 < WIDTH) {
useAppStore().toggleDevice('mobile'); useAppStore().toggleDevice('mobile');
useAppStore().closeSideBar({ withoutAnimation: true }); useAppStore().closeSideBar({withoutAnimation: true});
} else { } else {
useAppStore().toggleDevice('desktop'); useAppStore().toggleDevice('desktop');
} }
}, },
{ immediate: true } {immediate: true}
); );
const settingRef = ref<InstanceType<typeof Settings>>(); const settingRef = ref<InstanceType<typeof Settings>>();
@@ -69,7 +69,7 @@ onMounted(async () => {
}); });
const handleClickOutside = () => { const handleClickOutside = () => {
useAppStore().closeSideBar({ withoutAnimation: false }); useAppStore().closeSideBar({withoutAnimation: false});
}; };
const setLayout = () => { const setLayout = () => {
@@ -111,7 +111,7 @@ const setLayout = () => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 3px; gap: 3px;
//padding: 12px 12px 0; padding: 0 12px;
background: transparent; background: transparent;
} }

View File

@@ -1,4 +1,4 @@
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'; import {createRouter, createWebHistory, type RouteRecordRaw} from 'vue-router';
/* Layout */ /* Layout */
import Layout from '@/layout/index.vue'; import Layout from '@/layout/index.vue';
@@ -16,12 +16,12 @@ import Layout from '@/layout/index.vue';
* roles: ['admin', 'common'] // 访问路由的角色权限 * roles: ['admin', 'common'] // 访问路由的角色权限
* permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限 * permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限
* meta : { * meta : {
noCache: true // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false) noCache: true // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字 title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
icon: 'svg-name' // 设置该路由的图标,对应路径src/assets/icons/svg icon: 'svg-name' // 设置该路由的图标,对应路径src/assets/icons/svg
breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示 breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示
activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。 activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。
} }
*/ */
// 公共路由 // 公共路由
@@ -65,13 +65,13 @@ export const constantRoutes: RouteRecordRaw[] = [
{ {
path: '', path: '',
component: Layout, component: Layout,
redirect: '/index', redirect: '/index.vue',
children: [ children: [
{ {
path: '/index', path: '/index.vue',
component: () => import('@/views/index/index.vue'), component: () => import('@/views/index/index.vue'),
name: 'Index', name: 'Index',
meta: { title: '首页', icon: 'dashboard', affix: true } meta: {title: '首页', icon: 'dashboard', affix: true}
} }
] ]
}, },
@@ -85,7 +85,7 @@ export const constantRoutes: RouteRecordRaw[] = [
path: 'profile', path: 'profile',
component: () => import('@/views/system/user/profile/index.vue'), component: () => import('@/views/system/user/profile/index.vue'),
name: 'Profile', name: 'Profile',
meta: { title: '个人中心', icon: 'user' } meta: {title: '个人中心', icon: 'user'}
} }
] ]
} }
@@ -105,7 +105,7 @@ const router = createRouter({
if (savedPosition) { if (savedPosition) {
return savedPosition; return savedPosition;
} }
return { top: 0 }; return {top: 0};
} }
}); });

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> </template>
<el-form ref="queryFormRef" :model="queryParams" :inline="true" class="query-form"> <el-form ref="queryFormRef" :model="queryParams" :inline="true" class="query-form">
<el-form-item label="文件名" prop="fileName"> <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>
<el-form-item label="原名" prop="originalName"> <el-form-item label="原名" prop="originalName">
<el-input <el-input
@@ -42,7 +42,7 @@
></el-date-picker> ></el-date-picker>
</el-form-item> </el-form-item>
<el-form-item label="服务商" prop="service"> <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-form-item> <el-form-item>
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button> <el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
@@ -109,11 +109,11 @@
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
@header-click="handleHeaderCLick" @header-click="handleHeaderCLick"
> >
<el-table-column type="selection" width="55" align="center" /> <el-table-column align="center" type="selection" width="55"/>
<el-table-column v-if="false" label="对象存储主键" align="center" prop="ossId" /> <el-table-column v-if="false" align="center" label="对象存储主键" prop="ossId"/>
<el-table-column label="文件名" align="center" prop="fileName" /> <el-table-column align="center" label="文件名" prop="fileName"/>
<el-table-column label="原名" align="center" prop="originalName" /> <el-table-column align="center" label="原名" prop="originalName"/>
<el-table-column label="文件后缀" align="center" prop="fileSuffix" /> <el-table-column align="center" label="文件后缀" prop="fileSuffix"/>
<el-table-column label="文件展示" align="center" prop="url"> <el-table-column label="文件展示" align="center" prop="url">
<template #default="scope"> <template #default="scope">
<ImagePreview <ImagePreview
@@ -123,7 +123,7 @@
:src="scope.row.url" :src="scope.row.url"
:preview-src-list="[scope.row.url]" :preview-src-list="[scope.row.url]"
/> />
<span v-if="!checkFileSuffix(scope.row.fileSuffix) || !previewListResource" v-text="scope.row.url" /> <span v-if="!checkFileSuffix(scope.row.fileSuffix) || !previewListResource" v-text="scope.row.url"/>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180" sortable="custom"> <el-table-column label="创建时间" align="center" prop="createTime" width="180" sortable="custom">
@@ -131,8 +131,8 @@
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span> <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="上传人" align="center" prop="createByName" /> <el-table-column align="center" label="上传人" prop="createByName"/>
<el-table-column label="服务商" align="center" prop="service" sortable="custom" /> <el-table-column align="center" label="服务商" prop="service" sortable="custom"/>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope"> <template #default="scope">
<el-tooltip content="下载" placement="top"> <el-tooltip content="下载" placement="top">
@@ -169,8 +169,8 @@
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body> <el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
<el-form ref="ossFormRef" :model="form" :rules="rules" label-width="80px"> <el-form ref="ossFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="文件名"> <el-form-item label="文件名">
<fileUpload v-if="type === 0" v-model="form.file" /> <fileUpload v-if="type === 0" v-model="form.file"/>
<imageUpload v-if="type === 1" v-model="form.file" /> <imageUpload v-if="type === 1" v-model="form.file"/>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@@ -184,28 +184,28 @@
</template> </template>
<script setup name="Oss" lang="ts"> <script setup name="Oss" lang="ts">
import { useRouter } from 'vue-router'; import {useRouter} from 'vue-router';
import { getConfigKey, updateConfigByKey } from '@/api/system/config'; import {getConfigKey, updateConfigByKey} from '@/api/system/config';
import { listOss, delOss } from '@/api/system/oss'; import {listOss, delOss} from '@/api/system/oss';
import { OssForm, OssQuery, OssVO } from '@/api/system/oss/types'; import {OssForm, OssQuery, OssVO} from '@/api/system/oss/types';
import ImagePreview from '@/components/ImagePreview/index.vue'; import ImagePreview from '@/components/ImagePreview/index.vue';
import { useLoading } from '@/hooks/async/useLoading'; import {useLoading} from '@/hooks/async/useLoading';
import { useFormDialog } from '@/hooks/dialog/useFormDialog'; import {useFormDialog} from '@/hooks/dialog/useFormDialog';
import { useDateRangeQuery } from '@/hooks/form/useDateRangeQuery'; import {useDateRangeQuery} from '@/hooks/form/useDateRangeQuery';
import { useSearchReset } from '@/hooks/form/useSearchReset'; import {useSearchReset} from '@/hooks/form/useSearchReset';
import { useSearchToggle } from '@/hooks/form/useSearchToggle'; import {useSearchToggle} from '@/hooks/form/useSearchToggle';
import { useTableSelection } from '@/hooks/table/useTableSelection'; import {useTableSelection} from '@/hooks/table/useTableSelection';
import download from '@/plugins/download'; import download from '@/plugins/download';
import modal from '@/plugins/modal'; import modal from '@/plugins/modal';
import { parseTime } from '@/utils/ruoyi'; import {parseTime} from '@/utils/ruoyi';
const router = useRouter(); const router = useRouter();
const ossList = ref<OssVO[]>([]); const ossList = ref<OssVO[]>([]);
const showTable = ref(true); const showTable = ref(true);
const buttonLoading = ref(false); const buttonLoading = ref(false);
const { loading, setLoading, withLoading } = useLoading(true); const {loading, setLoading, withLoading} = useLoading(true);
const { showSearch } = useSearchToggle(); const {showSearch} = useSearchToggle();
const total = ref(0); const total = ref(0);
const type = ref(0); const type = ref(0);
const previewListResource = ref(true); const previewListResource = ref(true);
@@ -216,7 +216,7 @@ const {
} = useDateRangeQuery('CreateTime'); } = useDateRangeQuery('CreateTime');
// 默认排序 // 默认排序
const defaultSort = ref({ prop: 'createTime', order: 'ascending' }); const defaultSort = ref({prop: 'createTime', order: 'ascending'});
const ossFormRef = ref<ElFormInstance>(); const ossFormRef = ref<ElFormInstance>();
const queryFormRef = ref<ElFormInstance>(); const queryFormRef = ref<ElFormInstance>();
@@ -225,7 +225,7 @@ const initFormData = {
file: undefined file: undefined
}; };
const data = reactive<PageData<OssForm, OssQuery>>({ const data = reactive<PageData<OssForm, OssQuery>>({
form: { ...initFormData }, form: {...initFormData},
// 查询参数 // 查询参数
queryParams: { queryParams: {
pageNum: 1, pageNum: 1,
@@ -239,12 +239,12 @@ const data = reactive<PageData<OssForm, OssQuery>>({
isAsc: defaultSort.value.order isAsc: defaultSort.value.order
}, },
rules: { rules: {
file: [{ required: true, message: '文件不能为空', trigger: 'blur' }] file: [{required: true, message: '文件不能为空', trigger: 'blur'}]
} }
}); });
const { queryParams, form, rules } = toRefs(data); const {queryParams, form, rules} = toRefs(data);
const { ids, single, multiple, handleSelectionChange } = useTableSelection<OssVO>(item => item.ossId); const {ids, single, multiple, handleSelectionChange} = useTableSelection<OssVO>(item => item.ossId);
const { const {
dialog, dialog,
resetForm: reset, resetForm: reset,
@@ -267,22 +267,26 @@ const getList = async () => {
showTable.value = true; showTable.value = true;
}); });
}; };
function checkFileSuffix(fileSuffix: string | string[]) { function checkFileSuffix(fileSuffix: string | string[]) {
const arr = ['.png', '.jpg', '.jpeg']; const arr = ['.png', '.jpg', '.jpeg'];
const suffixArray = Array.isArray(fileSuffix) ? fileSuffix : [fileSuffix]; const suffixArray = Array.isArray(fileSuffix) ? fileSuffix : [fileSuffix];
return suffixArray.some(suffix => arr.includes(suffix.toLowerCase())); return suffixArray.some(suffix => arr.includes(suffix.toLowerCase()));
} }
/** 取消按钮 */ /** 取消按钮 */
function cancel() { function cancel() {
reset(); reset();
closeDialog(); closeDialog();
} }
/** 搜索按钮操作 */ /** 搜索按钮操作 */
function handleQuery() { function handleQuery() {
queryParams.value.pageNum = 1; queryParams.value.pageNum = 1;
getList(); getList();
} }
const { resetQuery } = useSearchReset({
const {resetQuery} = useSearchReset({
queryFormRef, queryFormRef,
queryParams, queryParams,
pageNumKey: 'pageNum', pageNumKey: 'pageNum',
@@ -297,7 +301,7 @@ const { resetQuery } = useSearchReset({
} }
}); });
/** 设置列的排序为我们自定义的排序 */ /** 设置列的排序为我们自定义的排序 */
const handleHeaderClass = ({ column }: any): any => { const handleHeaderClass = ({column}: any): any => {
column.order = column.multiOrder; column.order = column.multiOrder;
}; };
/** 点击表头进行排序 */ /** 点击表头进行排序 */
@@ -343,7 +347,7 @@ const handleOrderChange = (prop: string, order: string) => {
}; };
/** 任务日志列表查询 */ /** 任务日志列表查询 */
const handleOssConfig = () => { const handleOssConfig = () => {
router.push('/system/oss-config/index'); router.push('/system/oss-config/index.vue');
}; };
/** 文件按钮操作 */ /** 文件按钮操作 */
const handleFile = () => { const handleFile = () => {

View File

@@ -13,12 +13,12 @@
<el-form ref="queryFormRef" :model="queryParams" :inline="true" class="query-form"> <el-form ref="queryFormRef" :model="queryParams" :inline="true" class="query-form">
<el-form-item label="数据源" prop="dataName"> <el-form-item label="数据源" prop="dataName">
<el-select v-model="queryParams.dataName" filterable clearable placeholder="请选择/输入数据源名称"> <el-select v-model="queryParams.dataName" filterable clearable placeholder="请选择/输入数据源名称">
<el-option key="" label="全部" value="" /> <el-option key="" label="全部" value=""/>
<el-option v-for="item in dataNameList" :key="item" :label="item" :value="item"></el-option> <el-option v-for="item in dataNameList" :key="item" :label="item" :value="item"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="表名称" prop="tableName"> <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>
<el-form-item label="表描述" prop="tableComment"> <el-form-item label="表描述" prop="tableComment">
<el-input <el-input
@@ -99,12 +99,12 @@
<span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span> <span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="数据源" align="center" prop="dataName" :show-overflow-tooltip="true" /> <el-table-column :show-overflow-tooltip="true" align="center" label="数据源" prop="dataName"/>
<el-table-column label="表名称" align="center" prop="tableName" :show-overflow-tooltip="true" /> <el-table-column :show-overflow-tooltip="true" align="center" label="表名称" prop="tableName"/>
<el-table-column label="表描述" align="center" prop="tableComment" :show-overflow-tooltip="true" /> <el-table-column :show-overflow-tooltip="true" align="center" label="表描述" prop="tableComment"/>
<el-table-column label="实体" align="center" prop="className" :show-overflow-tooltip="true" /> <el-table-column :show-overflow-tooltip="true" align="center" label="实体" prop="className"/>
<el-table-column label="创建时间" align="center" prop="createTime" width="160" /> <el-table-column align="center" label="创建时间" prop="createTime" width="160"/>
<el-table-column label="更新时间" align="center" prop="updateTime" 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"> <el-table-column label="操作" align="center" width="330" class-name="small-padding fixed-width">
<template #default="scope"> <template #default="scope">
<el-tooltip content="预览" placement="top"> <el-tooltip content="预览" placement="top">
@@ -182,24 +182,24 @@
> >
&nbsp;复制 &nbsp;复制
</el-link> </el-link>
<highlightjs :code="value" /> <highlightjs :code="value"/>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</el-dialog> </el-dialog>
<import-table ref="importRef" @ok="handleQuery" /> <import-table ref="importRef" @ok="handleQuery"/>
</div> </div>
</template> </template>
<script setup name="Gen" lang="ts"> <script setup name="Gen" lang="ts">
import { useRoute } from 'vue-router'; import {useRoute} from 'vue-router';
import { delTable, getDataNames, listTable, previewTable, synchDb } from '@/api/tool/gen'; import {delTable, getDataNames, listTable, previewTable, synchDb} from '@/api/tool/gen';
import { TableQuery, TableVO } from '@/api/tool/gen/types'; import {TableQuery, TableVO} from '@/api/tool/gen/types';
import { useLoading } from '@/hooks/async/useLoading'; import {useLoading} from '@/hooks/async/useLoading';
import { useDialogState } from '@/hooks/dialog/useDialogState'; import {useDialogState} from '@/hooks/dialog/useDialogState';
import { useDateRangeQuery } from '@/hooks/form/useDateRangeQuery'; import {useDateRangeQuery} from '@/hooks/form/useDateRangeQuery';
import { useSearchReset } from '@/hooks/form/useSearchReset'; import {useSearchReset} from '@/hooks/form/useSearchReset';
import { useSearchToggle } from '@/hooks/form/useSearchToggle'; import {useSearchToggle} from '@/hooks/form/useSearchToggle';
import { useTableSelection } from '@/hooks/table/useTableSelection'; import {useTableSelection} from '@/hooks/table/useTableSelection';
import download from '@/plugins/download'; import download from '@/plugins/download';
import modal from '@/plugins/modal'; import modal from '@/plugins/modal';
import router from '@/router'; import router from '@/router';
@@ -208,10 +208,10 @@ import ImportTable from './importTable.vue';
const route = useRoute(); const route = useRoute();
const tableList = ref<TableVO[]>([]); const tableList = ref<TableVO[]>([]);
const { loading, withLoading } = useLoading(true); const {loading, withLoading} = useLoading(true);
const { showSearch } = useSearchToggle(); const {showSearch} = useSearchToggle();
const total = ref(0); const total = ref(0);
const { dateRange, applyDateRange, resetDateRange } = useDateRangeQuery(); const {dateRange, applyDateRange, resetDateRange} = useDateRangeQuery();
const uniqueId = ref(''); const uniqueId = ref('');
const dataNameList = ref<Array<string>>([]); const dataNameList = ref<Array<string>>([]);
@@ -240,7 +240,7 @@ const {
multiple, multiple,
handleSelectionChange: updateSelection handleSelectionChange: updateSelection
} = useTableSelection<TableVO>(item => item.tableId); } = useTableSelection<TableVO>(item => item.tableId);
const { dialog, openDialog: openPreviewDialog } = useDialogState('代码预览'); const {dialog, openDialog: openPreviewDialog} = useDialogState('代码预览');
const handleTableSelectionChange = (selection: TableVO[]) => { const handleTableSelectionChange = (selection: TableVO[]) => {
selectedRows.value = selection; selectedRows.value = selection;
@@ -288,7 +288,7 @@ const handleSynchDb = async (row: Partial<TableVO>) => {
const openImportTable = () => { const openImportTable = () => {
importRef.value?.show(queryParams.value.dataName); importRef.value?.show(queryParams.value.dataName);
}; };
const { resetQuery } = useSearchReset({ const {resetQuery} = useSearchReset({
queryFormRef, queryFormRef,
queryParams, queryParams,
pageNumKey: 'pageNum', pageNumKey: 'pageNum',
@@ -314,8 +314,8 @@ const copyTextSuccess = () => {
const handleEditTable = (row?: Partial<TableVO>) => { const handleEditTable = (row?: Partial<TableVO>) => {
const tableId = row?.tableId || ids.value[0]; const tableId = row?.tableId || ids.value[0];
router.push({ router.push({
path: '/tool/gen-edit/index/' + tableId, path: '/tool/gen-edit/index.vue/' + tableId,
query: { pageNum: queryParams.value.pageNum } query: {pageNum: queryParams.value.pageNum}
}); });
}; };
/** 删除按钮操作 */ /** 删除按钮操作 */
@@ -347,6 +347,7 @@ onMounted(() => {
.el-tab-pane { .el-tab-pane {
background-color: #282c34; background-color: #282c34;
.el-link { .el-link {
color: #fff; color: #fff;
} }

View File

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