Merge remote-tracking branch 'yudao/master'
commit
8f9bd94f58
|
|
@ -58,6 +58,7 @@
|
||||||
"diagram-js": "catalog:",
|
"diagram-js": "catalog:",
|
||||||
"fast-xml-parser": "catalog:",
|
"fast-xml-parser": "catalog:",
|
||||||
"highlight.js": "catalog:",
|
"highlight.js": "catalog:",
|
||||||
|
"livekit-client": "catalog:",
|
||||||
"pinia": "catalog:",
|
"pinia": "catalog:",
|
||||||
"steady-xml": "catalog:",
|
"steady-xml": "catalog:",
|
||||||
"tinymce": "catalog:",
|
"tinymce": "catalog:",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export namespace CrmReceivableApi {
|
||||||
customerId?: number;
|
customerId?: number;
|
||||||
customerName?: string;
|
customerName?: string;
|
||||||
contractId?: number;
|
contractId?: number;
|
||||||
|
contractNo?: string;
|
||||||
contract?: Contract;
|
contract?: Contract;
|
||||||
auditStatus: number;
|
auditStatus: number;
|
||||||
processInstanceId: number;
|
processInstanceId: number;
|
||||||
|
|
@ -34,6 +35,11 @@ export namespace CrmReceivableApi {
|
||||||
no: string;
|
no: string;
|
||||||
totalPrice: number;
|
totalPrice: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReceivablePageParam extends PageParam {
|
||||||
|
contractId?: number;
|
||||||
|
customerId?: number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询回款列表 */
|
/** 查询回款列表 */
|
||||||
|
|
@ -45,7 +51,9 @@ export function getReceivablePage(params: PageParam) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询回款列表,基于指定客户 */
|
/** 查询回款列表,基于指定客户 */
|
||||||
export function getReceivablePageByCustomer(params: PageParam) {
|
export function getReceivablePageByCustomer(
|
||||||
|
params: CrmReceivableApi.ReceivablePageParam,
|
||||||
|
) {
|
||||||
return requestClient.get<PageResult<CrmReceivableApi.Receivable>>(
|
return requestClient.get<PageResult<CrmReceivableApi.Receivable>>(
|
||||||
'/crm/receivable/page-by-customer',
|
'/crm/receivable/page-by-customer',
|
||||||
{ params },
|
{ params },
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ export namespace ErpPurchaseInApi {
|
||||||
export interface PurchaseInItem {
|
export interface PurchaseInItem {
|
||||||
count?: number;
|
count?: number;
|
||||||
id?: number;
|
id?: number;
|
||||||
|
seq?: number; // 前端行号
|
||||||
orderItemId?: number;
|
orderItemId?: number;
|
||||||
productBarCode?: string;
|
productBarCode?: string;
|
||||||
productId?: number;
|
productId?: number;
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ export namespace ErpPurchaseOrderApi {
|
||||||
/** 采购订单项信息 */
|
/** 采购订单项信息 */
|
||||||
export interface PurchaseOrderItem {
|
export interface PurchaseOrderItem {
|
||||||
id?: number; // 订单项编号
|
id?: number; // 订单项编号
|
||||||
|
seq?: number; // 前端行号
|
||||||
orderId?: number; // 采购订单编号
|
orderId?: number; // 采购订单编号
|
||||||
productId?: number; // 产品编号
|
productId?: number; // 产品编号
|
||||||
productName?: string; // 产品名称
|
productName?: string; // 产品名称
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ export namespace ErpPurchaseReturnApi {
|
||||||
returnTime?: Date; // 退货时间
|
returnTime?: Date; // 退货时间
|
||||||
totalCount?: number; // 合计数量
|
totalCount?: number; // 合计数量
|
||||||
totalPrice: number; // 合计金额,单位:元
|
totalPrice: number; // 合计金额,单位:元
|
||||||
|
refundPrice?: number; // 已退款金额,单位:元
|
||||||
discountPercent?: number; // 折扣百分比
|
discountPercent?: number; // 折扣百分比
|
||||||
discountPrice?: number; // 折扣金额
|
discountPrice?: number; // 折扣金额
|
||||||
status?: number; // 状态
|
status?: number; // 状态
|
||||||
|
|
@ -24,6 +25,7 @@ export namespace ErpPurchaseReturnApi {
|
||||||
export interface PurchaseReturnItem {
|
export interface PurchaseReturnItem {
|
||||||
count?: number;
|
count?: number;
|
||||||
id?: number;
|
id?: number;
|
||||||
|
seq?: number; // 前端行号
|
||||||
orderItemId?: number;
|
orderItemId?: number;
|
||||||
productBarCode?: string;
|
productBarCode?: string;
|
||||||
productId?: number;
|
productId?: number;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ export namespace ErpSaleOrderApi {
|
||||||
/** 销售订单项 */
|
/** 销售订单项 */
|
||||||
export interface SaleOrderItem {
|
export interface SaleOrderItem {
|
||||||
id?: number; // 订单项编号
|
id?: number; // 订单项编号
|
||||||
|
seq?: number; // 前端行号
|
||||||
orderId?: number; // 采购订单编号
|
orderId?: number; // 采购订单编号
|
||||||
productId?: number; // 产品编号
|
productId?: number; // 产品编号
|
||||||
productName?: string; // 产品名称
|
productName?: string; // 产品名称
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export namespace ErpSaleOutApi {
|
||||||
outTime?: Date; // 出库时间
|
outTime?: Date; // 出库时间
|
||||||
totalCount?: number; // 合计数量
|
totalCount?: number; // 合计数量
|
||||||
totalPrice?: number; // 合计金额,单位:元
|
totalPrice?: number; // 合计金额,单位:元
|
||||||
|
receiptPrice?: number; // 已收款金额,单位:元
|
||||||
status?: number; // 状态
|
status?: number; // 状态
|
||||||
remark?: string; // 备注
|
remark?: string; // 备注
|
||||||
discountPercent?: number; // 折扣百分比
|
discountPercent?: number; // 折扣百分比
|
||||||
|
|
@ -28,6 +29,7 @@ export namespace ErpSaleOutApi {
|
||||||
export interface SaleOutItem {
|
export interface SaleOutItem {
|
||||||
count?: number;
|
count?: number;
|
||||||
id?: number;
|
id?: number;
|
||||||
|
seq?: number; // 前端行号
|
||||||
orderItemId?: number;
|
orderItemId?: number;
|
||||||
productBarCode?: string;
|
productBarCode?: string;
|
||||||
productId?: number;
|
productId?: number;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ export namespace ErpSaleReturnApi {
|
||||||
returnTime?: Date; // 退货时间
|
returnTime?: Date; // 退货时间
|
||||||
totalCount?: number; // 合计数量
|
totalCount?: number; // 合计数量
|
||||||
totalPrice?: number; // 合计金额,单位:元
|
totalPrice?: number; // 合计金额,单位:元
|
||||||
|
refundPrice?: number; // 已退款金额,单位:元
|
||||||
status?: number; // 状态
|
status?: number; // 状态
|
||||||
remark?: string; // 备注
|
remark?: string; // 备注
|
||||||
discountPercent?: number; // 折扣百分比
|
discountPercent?: number; // 折扣百分比
|
||||||
|
|
@ -27,6 +28,7 @@ export namespace ErpSaleReturnApi {
|
||||||
export interface SaleReturnItem {
|
export interface SaleReturnItem {
|
||||||
count?: number;
|
count?: number;
|
||||||
id?: number;
|
id?: number;
|
||||||
|
seq?: number; // 前端行号
|
||||||
orderItemId?: number;
|
orderItemId?: number;
|
||||||
productBarCode?: string;
|
productBarCode?: string;
|
||||||
productId?: number;
|
productId?: number;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export namespace ErpStockCheckApi {
|
||||||
/** 库存盘点项 */
|
/** 库存盘点项 */
|
||||||
export interface StockCheckItem {
|
export interface StockCheckItem {
|
||||||
id?: number; // 编号
|
id?: number; // 编号
|
||||||
|
seq?: number; // 前端行号
|
||||||
warehouseId?: number; // 仓库编号
|
warehouseId?: number; // 仓库编号
|
||||||
productId?: number; // 产品编号
|
productId?: number; // 产品编号
|
||||||
productName?: string; // 产品名称
|
productName?: string; // 产品名称
|
||||||
|
|
|
||||||
|
|
@ -23,15 +23,16 @@ export namespace ErpStockInApi {
|
||||||
/** 其它入库单产品信息 */
|
/** 其它入库单产品信息 */
|
||||||
export interface StockInItem {
|
export interface StockInItem {
|
||||||
id?: number; // 编号
|
id?: number; // 编号
|
||||||
warehouseId: number; // 仓库编号
|
seq?: number; // 前端行号
|
||||||
productId: number; // 产品编号
|
warehouseId?: number; // 仓库编号
|
||||||
|
productId?: number; // 产品编号
|
||||||
productName?: string; // 产品名称
|
productName?: string; // 产品名称
|
||||||
productUnitId?: number; // 产品单位编号
|
productUnitId?: number; // 产品单位编号
|
||||||
productUnitName?: string; // 产品单位名称
|
productUnitName?: string; // 产品单位名称
|
||||||
productBarCode?: string; // 产品条码
|
productBarCode?: string; // 产品条码
|
||||||
count: number; // 数量
|
count?: number; // 数量
|
||||||
productPrice: number; // 产品单价
|
productPrice?: number; // 产品单价
|
||||||
totalPrice: number; // 总价
|
totalPrice?: number; // 总价
|
||||||
stockCount?: number; // 库存数量
|
stockCount?: number; // 库存数量
|
||||||
remark?: string; // 备注
|
remark?: string; // 备注
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export namespace ErpStockMoveApi {
|
||||||
count: number; // 数量
|
count: number; // 数量
|
||||||
fromWarehouseId?: number; // 来源仓库ID
|
fromWarehouseId?: number; // 来源仓库ID
|
||||||
id?: number; // ID
|
id?: number; // ID
|
||||||
|
seq?: number; // 前端行号
|
||||||
productBarCode: string; // 产品条形码
|
productBarCode: string; // 产品条形码
|
||||||
productId?: number; // 产品ID
|
productId?: number; // 产品ID
|
||||||
productName?: string; // 产品名称
|
productName?: string; // 产品名称
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export namespace ErpStockOutApi {
|
||||||
/** 其它出库单产品信息 */
|
/** 其它出库单产品信息 */
|
||||||
export interface StockOutItem {
|
export interface StockOutItem {
|
||||||
id?: number; // 编号
|
id?: number; // 编号
|
||||||
|
seq?: number; // 前端行号
|
||||||
warehouseId?: number; // 仓库编号
|
warehouseId?: number; // 仓库编号
|
||||||
productId?: number; // 产品编号
|
productId?: number; // 产品编号
|
||||||
productName?: string; // 产品名称
|
productName?: string; // 产品名称
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImChannelMaterialApi {
|
||||||
|
/** 用户端能看到的频道素材详情 */
|
||||||
|
export interface Material {
|
||||||
|
id: number;
|
||||||
|
channelId: number;
|
||||||
|
type: number;
|
||||||
|
title: string;
|
||||||
|
coverUrl?: string;
|
||||||
|
summary?: string;
|
||||||
|
content?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 获取频道素材详情;用于客户端点击图文卡片渲染详情页 */
|
||||||
|
export function getChannelMaterial(id: number) {
|
||||||
|
return requestClient.get<ImChannelMaterialApi.Material>(
|
||||||
|
'/im/channel/material/get',
|
||||||
|
{ params: { id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImConversationReadApi {
|
||||||
|
/** IM 会话读位置 Response VO */
|
||||||
|
export interface ConversationReadRespVO {
|
||||||
|
id: number; // 读位置编号(增量拉取游标用)
|
||||||
|
conversationType: number; // 会话类型,参见 ImConversationType
|
||||||
|
targetId: number; // 会话目标编号
|
||||||
|
messageId: number; // 最大已读消息编号
|
||||||
|
updateTime?: number; // 最近更新时间(毫秒时间戳,增量拉取游标用)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 增量拉取当前用户的会话读位置(重连 / 离线补偿) */
|
||||||
|
export function pullMyConversationReadList(params: {
|
||||||
|
lastId?: number;
|
||||||
|
lastUpdateTime?: number;
|
||||||
|
limit: number;
|
||||||
|
}) {
|
||||||
|
return requestClient.get<ImConversationReadApi.ConversationReadRespVO[]>(
|
||||||
|
'/im/conversation-read/pull',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImFacePackApi {
|
||||||
|
/** 用户端表情包项(精简版) */
|
||||||
|
export interface FacePackUserItem {
|
||||||
|
id: number;
|
||||||
|
url: string;
|
||||||
|
name?: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用户端表情包 + 嵌套 items */
|
||||||
|
export interface FacePackUser {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
icon?: string;
|
||||||
|
items: FacePackUserItem[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 拉取所有启用的系统表情包(含表情列表) */
|
||||||
|
export function getFacePackList() {
|
||||||
|
return requestClient.get<ImFacePackApi.FacePackUser[]>('/im/face-pack/list');
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImFaceUserItemApi {
|
||||||
|
/** 个人表情 */
|
||||||
|
export interface FaceUserItem {
|
||||||
|
id: number;
|
||||||
|
url: string;
|
||||||
|
name?: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加个人表情请求 */
|
||||||
|
export interface FaceUserItemSaveReqVO {
|
||||||
|
url: string;
|
||||||
|
name?: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 获取我的个人表情列表 */
|
||||||
|
export function getFaceUserItemList() {
|
||||||
|
return requestClient.get<ImFaceUserItemApi.FaceUserItem[]>('/im/face-user-item/list');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加个人表情 */
|
||||||
|
export function createFaceUserItem(data: ImFaceUserItemApi.FaceUserItemSaveReqVO) {
|
||||||
|
return requestClient.post<number>('/im/face-user-item/create', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除个人表情 */
|
||||||
|
export function deleteFaceUserItem(id: number) {
|
||||||
|
return requestClient.delete<boolean>('/im/face-user-item/delete', {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImFriendApi {
|
||||||
|
/** IM 好友 Response VO */
|
||||||
|
export interface FriendRespVO {
|
||||||
|
id: number; // 关系记录编号
|
||||||
|
friendUserId: number; // 好友的用户编号
|
||||||
|
silent?: boolean; // 是否免打扰
|
||||||
|
displayName?: string; // 好友展示备注(仅自己可见)
|
||||||
|
displayNamePinyin?: string; // 备注的拼音(小写无空格,前端按首字母分桶 / 拼音搜索)
|
||||||
|
addSource?: number; // 添加来源;参见 ImFriendAddSourceEnum
|
||||||
|
pinned?: boolean; // 是否置顶联系人
|
||||||
|
blocked?: boolean; // 是否拉黑
|
||||||
|
status?: number; // 好友状态(0=正常,1=已删除)
|
||||||
|
addTime?: string; // 添加好友时间
|
||||||
|
deleteTime?: string; // 删除好友时间
|
||||||
|
updateTime?: number; // 最近更新时间(毫秒时间戳,增量拉取游标用)
|
||||||
|
nickname?: string; // 好友昵称
|
||||||
|
nicknamePinyin?: string; // 昵称的拼音(小写无空格,前端按首字母分桶 / 拼音搜索)
|
||||||
|
avatar?: string; // 好友头像
|
||||||
|
}
|
||||||
|
|
||||||
|
/** IM 好友更新 Request VO */
|
||||||
|
export interface FriendUpdateReqVO {
|
||||||
|
friendUserId: number; // 好友的用户编号
|
||||||
|
silent?: boolean; // 是否免打扰
|
||||||
|
displayName?: string; // 好友展示备注
|
||||||
|
pinned?: boolean; // 是否置顶联系人
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 获得当前登录用户的好友列表 */
|
||||||
|
export function getMyFriendList() {
|
||||||
|
return requestClient.get<ImFriendApi.FriendRespVO[]>('/im/friend/list');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 增量拉取当前用户的好友关系(重连 / 离线补偿) */
|
||||||
|
export function pullMyFriendList(params: {
|
||||||
|
lastId?: number;
|
||||||
|
lastUpdateTime?: number;
|
||||||
|
limit: number;
|
||||||
|
}) {
|
||||||
|
return requestClient.get<ImFriendApi.FriendRespVO[]>('/im/friend/pull', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得好友详情 */
|
||||||
|
export function getFriend(friendUserId: number | string) {
|
||||||
|
return requestClient.get<ImFriendApi.FriendRespVO>('/im/friend/get', {
|
||||||
|
params: { friendUserId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除好友(单向软删除) */
|
||||||
|
export function deleteFriend(friendUserId: number | string, clear: boolean) {
|
||||||
|
return requestClient.delete<boolean>('/im/friend/delete', {
|
||||||
|
params: { friendUserId, clear },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新好友信息(备注 / 免打扰 / 联系人置顶) */
|
||||||
|
export function updateFriend(data: ImFriendApi.FriendUpdateReqVO) {
|
||||||
|
return requestClient.put<boolean>('/im/friend/update', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拉黑好友(必须先是好友;单边屏蔽对方私聊消息) */
|
||||||
|
export function blockFriend(friendUserId: number | string) {
|
||||||
|
return requestClient.put<boolean>('/im/friend/block', undefined, {
|
||||||
|
params: { friendUserId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 移出黑名单 */
|
||||||
|
export function unblockFriend(friendUserId: number | string) {
|
||||||
|
return requestClient.put<boolean>('/im/friend/unblock', undefined, {
|
||||||
|
params: { friendUserId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImFriendRequestApi {
|
||||||
|
/** IM 好友申请 Response VO */
|
||||||
|
export interface FriendRequestRespVO {
|
||||||
|
id: number; // 申请编号
|
||||||
|
fromUserId: number; // 发起方用户编号
|
||||||
|
toUserId: number; // 接收方用户编号
|
||||||
|
handleResult: number; // 处理结果;0=未处理;1=同意;2=拒绝
|
||||||
|
applyContent?: string; // 申请理由
|
||||||
|
handleContent?: string; // 处理理由(接收方拒绝时可选填)
|
||||||
|
addSource?: number; // 添加来源;参见 ImFriendAddSourceEnum
|
||||||
|
handleTime?: string; // 处理时间
|
||||||
|
createTime: string; // 申请创建时间
|
||||||
|
updateTime?: number; // 最近更新时间(毫秒时间戳,增量拉取游标用)
|
||||||
|
fromNickname?: string; // 发起方昵称
|
||||||
|
fromAvatar?: string; // 发起方头像
|
||||||
|
toNickname?: string; // 接收方昵称
|
||||||
|
toAvatar?: string; // 接收方头像
|
||||||
|
}
|
||||||
|
|
||||||
|
/** IM 好友申请发起 Request VO */
|
||||||
|
export interface FriendRequestApplyReqVO {
|
||||||
|
toUserId: number; // 接收方用户编号
|
||||||
|
applyContent?: string; // 申请理由
|
||||||
|
displayName?: string; // 对接收方的备注(仅自己可见)
|
||||||
|
addSource?: number; // 添加来源
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 发起好友申请 */
|
||||||
|
export function applyFriendRequest(data: ImFriendRequestApi.FriendRequestApplyReqVO) {
|
||||||
|
return requestClient.post<null | number>('/im/friend-request/apply', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同意好友申请 */
|
||||||
|
export function agreeFriendRequest(id: number | string) {
|
||||||
|
return requestClient.put<boolean>('/im/friend-request/agree', undefined, {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拒绝好友申请 */
|
||||||
|
export function refuseFriendRequest(
|
||||||
|
id: number | string,
|
||||||
|
handleContent?: string,
|
||||||
|
) {
|
||||||
|
return requestClient.put<boolean>('/im/friend-request/refuse', undefined, {
|
||||||
|
params: { id, handleContent },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询「我相关」的好友申请列表(游标分页:传 maxId 加载更多) */
|
||||||
|
export function getMyFriendRequestList(limit: number, maxId?: number) {
|
||||||
|
const params: Record<string, number> = { limit };
|
||||||
|
if (maxId != null) {
|
||||||
|
params.maxId = maxId;
|
||||||
|
}
|
||||||
|
return requestClient.get<ImFriendRequestApi.FriendRequestRespVO[]>(
|
||||||
|
'/im/friend-request/list',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 增量拉取「我相关」的好友申请变更(重连 / 离线补偿) */
|
||||||
|
export function pullMyFriendRequestList(params: {
|
||||||
|
lastId?: number;
|
||||||
|
lastUpdateTime?: number;
|
||||||
|
limit: number;
|
||||||
|
}) {
|
||||||
|
return requestClient.get<ImFriendRequestApi.FriendRequestRespVO[]>(
|
||||||
|
'/im/friend-request/pull',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按 id 单查「我相关」的申请记录(带越权过滤;WebSocket 通知到达后用) */
|
||||||
|
export function getMyFriendRequest(id: number) {
|
||||||
|
return requestClient.get<ImFriendRequestApi.FriendRequestRespVO | null>(
|
||||||
|
'/im/friend-request/get',
|
||||||
|
{ params: { id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
import type { ImGroupMessageApi } from '#/api/im/message/group';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImGroupApi {
|
||||||
|
/** 群 Response VO */
|
||||||
|
export interface GroupRespVO {
|
||||||
|
id: number; // 编号
|
||||||
|
name: string; // 群名称
|
||||||
|
ownerUserId: number; // 群主用户编号
|
||||||
|
avatar?: string; // 群头像
|
||||||
|
notice?: string; // 群公告
|
||||||
|
banned?: boolean; // 是否封禁
|
||||||
|
mutedAll?: boolean; // 是否全群禁言
|
||||||
|
joinApproval?: boolean; // 进群是否需群主 / 管理员审批
|
||||||
|
bannedTime?: string; // 封禁时间
|
||||||
|
status: number; // 群状态(0=正常,1=已解散)
|
||||||
|
dissolvedTime?: string; // 解散时间
|
||||||
|
createTime?: string; // 创建时间
|
||||||
|
pinnedMessages?: ImGroupMessageApi.GroupMessageRespVO[]; // 群置顶消息列表(后端关联回填,仅当登录用户是群成员时非空)
|
||||||
|
joinStatus?: number; // 当前登录用户在该群的成员状态(参见 CommonStatusEnum:0 在群 / 1 已退群);历史退群群仍返回,供展示离线消息的群名 / 头像
|
||||||
|
groupRemark?: string; // 当前登录用户对该群的备注
|
||||||
|
silent?: boolean; // 当前登录用户是否免打扰
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 群消息置顶 / 取消置顶 Request VO */
|
||||||
|
export interface GroupMessagePinReqVO {
|
||||||
|
id: number; // 群编号
|
||||||
|
messageId: number; // 消息编号
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 群创建 Request VO */
|
||||||
|
export interface GroupCreateReqVO {
|
||||||
|
name: string; // 群名称
|
||||||
|
memberUserIds?: number[]; // 初始成员用户编号列表(建群同时邀请的好友,不含创建者自己)
|
||||||
|
joinApproval?: boolean; // 进群是否需审批;不传默认 false 自由进群
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 群更新 Request VO */
|
||||||
|
export interface GroupUpdateReqVO {
|
||||||
|
id: number; // 群编号
|
||||||
|
name?: string; // 群名称
|
||||||
|
avatar?: string; // 群头像
|
||||||
|
notice?: string; // 群公告
|
||||||
|
joinApproval?: boolean; // 进群是否需审批
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加 / 撤销群管理员 Request VO */
|
||||||
|
export interface GroupAdminReqVO {
|
||||||
|
id: number; // 群编号
|
||||||
|
userIds: number[]; // 目标用户编号列表
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 群主转让 Request VO */
|
||||||
|
export interface GroupTransferOwnerReqVO {
|
||||||
|
id: number; // 群编号
|
||||||
|
newOwnerUserId: number; // 新群主用户编号
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全群禁言 / 取消 Request VO */
|
||||||
|
export interface GroupMuteAllReqVO {
|
||||||
|
id: number; // 群编号
|
||||||
|
mutedAll: boolean; // 是否全群禁言
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 成员禁言 Request VO */
|
||||||
|
export interface GroupMuteMemberReqVO {
|
||||||
|
id: number; // 群编号
|
||||||
|
userId: number; // 被禁言的用户编号
|
||||||
|
mutedSeconds: number; // 禁言时长(秒),0 表示永久禁言
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消成员禁言 Request VO */
|
||||||
|
export interface GroupCancelMuteMemberReqVO {
|
||||||
|
id: number; // 群编号
|
||||||
|
userId: number; // 被取消禁言的用户编号
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 获得当前登录用户的群列表 */
|
||||||
|
export function getMyGroupList() {
|
||||||
|
return requestClient.get<ImGroupApi.GroupRespVO[]>('/im/group/list');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得群详情 */
|
||||||
|
export function getGroup(id: number | string) {
|
||||||
|
return requestClient.get<ImGroupApi.GroupRespVO>('/im/group/get', { params: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建群 */
|
||||||
|
export function createGroup(data: ImGroupApi.GroupCreateReqVO) {
|
||||||
|
return requestClient.post<ImGroupApi.GroupRespVO>('/im/group/create', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新群 */
|
||||||
|
export function updateGroup(data: ImGroupApi.GroupUpdateReqVO) {
|
||||||
|
return requestClient.put<ImGroupApi.GroupRespVO>('/im/group/update', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解散群 */
|
||||||
|
export function dissolveGroup(id: number | string) {
|
||||||
|
return requestClient.delete<boolean>('/im/group/dissolve', {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加群管理员(仅群主可调) */
|
||||||
|
export function addGroupAdmin(data: ImGroupApi.GroupAdminReqVO) {
|
||||||
|
return requestClient.put<boolean>('/im/group/add-admin', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 撤销群管理员(仅群主可调) */
|
||||||
|
export function removeGroupAdmin(data: ImGroupApi.GroupAdminReqVO) {
|
||||||
|
return requestClient.put<boolean>('/im/group/remove-admin', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 转让群主(仅老群主可调;旧群主转让后降为普通成员) */
|
||||||
|
export function transferGroupOwner(data: ImGroupApi.GroupTransferOwnerReqVO) {
|
||||||
|
return requestClient.put<boolean>('/im/group/transfer-owner', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 置顶群消息(仅群主 / 管理员可调) */
|
||||||
|
export function pinGroupMessage(data: ImGroupApi.GroupMessagePinReqVO) {
|
||||||
|
return requestClient.put<boolean>('/im/group/pin-message', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消置顶群消息(仅群主 / 管理员可调) */
|
||||||
|
export function unpinGroupMessage(data: ImGroupApi.GroupMessagePinReqVO) {
|
||||||
|
return requestClient.put<boolean>('/im/group/unpin-message', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全群禁言 / 取消(仅群主 / 管理员可调) */
|
||||||
|
export function muteAll(data: ImGroupApi.GroupMuteAllReqVO) {
|
||||||
|
return requestClient.put<boolean>('/im/group/mute-all', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 禁言成员 */
|
||||||
|
export function muteMember(data: ImGroupApi.GroupMuteMemberReqVO) {
|
||||||
|
return requestClient.put<boolean>('/im/group/mute-member', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消成员禁言 */
|
||||||
|
export function cancelMuteMember(data: ImGroupApi.GroupCancelMuteMemberReqVO) {
|
||||||
|
return requestClient.put<boolean>('/im/group/cancel-mute-member', data);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImGroupMemberApi {
|
||||||
|
/** 群成员 Response VO */
|
||||||
|
export interface GroupMemberRespVO {
|
||||||
|
id: number; // 编号
|
||||||
|
groupId: number; // 群编号
|
||||||
|
userId: number; // 用户编号
|
||||||
|
displayUserName?: string; // 组内显示名(群主设置的备注)
|
||||||
|
groupRemark?: string; // 群备注(当前用户对群的备注)
|
||||||
|
silent?: boolean; // 是否免打扰
|
||||||
|
status?: number; // 成员状态(0=在群,1=退群)
|
||||||
|
role?: number; // 成员角色,参见 ImGroupMemberRole 枚举
|
||||||
|
joinTime?: string; // 入群时间
|
||||||
|
quitTime?: string; // 退群时间
|
||||||
|
muteEndTime?: string; // 禁言到期时间
|
||||||
|
createTime?: string; // 创建时间
|
||||||
|
nickname?: string; // 用户昵称
|
||||||
|
avatar?: string; // 用户头像
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 群成员邀请 Request VO */
|
||||||
|
export interface GroupMemberInviteReqVO {
|
||||||
|
groupId: number; // 群编号
|
||||||
|
memberUserIds: number[]; // 被邀请的用户编号列表
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 群成员移除 Request VO */
|
||||||
|
export interface GroupMemberRemoveReqVO {
|
||||||
|
groupId: number; // 群编号
|
||||||
|
memberUserIds: number[]; // 被移除的用户编号列表
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 群成员更新 Request VO */
|
||||||
|
export interface GroupMemberUpdateReqVO {
|
||||||
|
groupId: number; // 群编号
|
||||||
|
displayUserName?: string; // 群内昵称
|
||||||
|
groupRemark?: string; // 群备注
|
||||||
|
silent?: boolean; // 是否免打扰
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 邀请用户加入群 */
|
||||||
|
export function inviteGroupMember(data: ImGroupMemberApi.GroupMemberInviteReqVO) {
|
||||||
|
return requestClient.post<boolean>('/im/group/invite', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 退出群 */
|
||||||
|
export function quitGroup(groupId: number | string) {
|
||||||
|
return requestClient.delete<boolean>('/im/group/quit', {
|
||||||
|
params: { groupId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 移除群成员 */
|
||||||
|
export function removeGroupMember(data: ImGroupMemberApi.GroupMemberRemoveReqVO) {
|
||||||
|
return requestClient.delete<boolean>('/im/group/kicking', { data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得群成员详情 */
|
||||||
|
export function getGroupMember(groupId: number, userId: number) {
|
||||||
|
return requestClient.get<ImGroupMemberApi.GroupMemberRespVO>('/im/group-member/get', {
|
||||||
|
params: { groupId, userId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得指定群的成员列表(聚合 AdminUser 昵称 / 头像) */
|
||||||
|
export function getGroupMemberList(groupId: number | string) {
|
||||||
|
return requestClient.get<ImGroupMemberApi.GroupMemberRespVO[]>('/im/group-member/list', {
|
||||||
|
params: { groupId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新群成员 */
|
||||||
|
export function updateGroupMember(data: ImGroupMemberApi.GroupMemberUpdateReqVO) {
|
||||||
|
return requestClient.put<boolean>('/im/group-member/update', data);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImGroupRequestApi {
|
||||||
|
/** IM 加群申请 Response VO */
|
||||||
|
export interface GroupRequestRespVO {
|
||||||
|
id: number; // 申请编号
|
||||||
|
groupId: number; // 群编号
|
||||||
|
userId: number; // 申请人 / 被邀请人用户编号
|
||||||
|
inviterUserId?: number; // 邀请人;NULL 表示用户主动申请
|
||||||
|
handleResult: number; // 处理结果;0=未处理;1=同意;2=拒绝
|
||||||
|
applyContent?: string; // 申请理由
|
||||||
|
handleContent?: string; // 处理理由(拒绝时可选填)
|
||||||
|
handleUserId?: number; // 处理人用户编号
|
||||||
|
addSource?: number; // 加入来源;参见 ImGroupAddSourceEnum
|
||||||
|
handleTime?: string; // 处理时间
|
||||||
|
createTime: string; // 申请创建时间
|
||||||
|
updateTime?: number; // 最近更新时间(毫秒时间戳,增量拉取游标用)
|
||||||
|
userNickname?: string; // 申请人 / 被邀请人昵称
|
||||||
|
userAvatar?: string; // 申请人 / 被邀请人头像
|
||||||
|
inviterNickname?: string; // 邀请人昵称
|
||||||
|
inviterAvatar?: string; // 邀请人头像
|
||||||
|
groupName?: string; // 群名称
|
||||||
|
groupAvatar?: string; // 群头像
|
||||||
|
}
|
||||||
|
|
||||||
|
/** IM 加群申请发起 Request VO */
|
||||||
|
export interface GroupRequestApplyReqVO {
|
||||||
|
groupId: number; // 群编号
|
||||||
|
applyContent?: string; // 申请理由
|
||||||
|
addSource?: number; // 加入来源
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 申请加群 */
|
||||||
|
export function applyJoinGroup(data: ImGroupRequestApi.GroupRequestApplyReqVO) {
|
||||||
|
return requestClient.post<null | number>('/im/group-request/apply', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同意加群申请(群主或管理员) */
|
||||||
|
export function agreeGroupRequest(id: number | string) {
|
||||||
|
return requestClient.put<boolean>('/im/group-request/agree', undefined, {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拒绝加群申请(群主或管理员) */
|
||||||
|
export function refuseGroupRequest(
|
||||||
|
id: number | string,
|
||||||
|
handleContent?: string,
|
||||||
|
) {
|
||||||
|
return requestClient.put<boolean>('/im/group-request/refuse', undefined, {
|
||||||
|
params: { id, handleContent },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询「我管理的所有群」下的未处理加群申请列表(不分页);前端 store 据此派生横幅红点 + Drawer 列表 */
|
||||||
|
export function getUnhandledRequestList() {
|
||||||
|
return requestClient.get<ImGroupRequestApi.GroupRequestRespVO[]>(
|
||||||
|
'/im/group-request/unhandled-list',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询指定群下的全部加群申请(含已处理);仅群主 / 管理员可查 */
|
||||||
|
export function getGroupRequestListByGroupId(groupId: number) {
|
||||||
|
return requestClient.get<ImGroupRequestApi.GroupRequestRespVO[]>(
|
||||||
|
'/im/group-request/list-by-group',
|
||||||
|
{ params: { groupId } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按 id 单查申请记录(带越权过滤;WebSocket 通知到达后用) */
|
||||||
|
export function getMyGroupRequest(id: number) {
|
||||||
|
return requestClient.get<ImGroupRequestApi.GroupRequestRespVO | null>(
|
||||||
|
'/im/group-request/get',
|
||||||
|
{ params: { id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 增量拉取我管理的所有群下加群申请变更(重连 / 离线补偿) */
|
||||||
|
export function pullMyGroupRequestList(params: {
|
||||||
|
lastId?: number;
|
||||||
|
lastUpdateTime?: number;
|
||||||
|
limit: number;
|
||||||
|
}) {
|
||||||
|
return requestClient.get<ImGroupRequestApi.GroupRequestRespVO[]>(
|
||||||
|
'/im/group-request/pull',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImManagerChannelApi {
|
||||||
|
/** 频道 */
|
||||||
|
export interface Channel {
|
||||||
|
id: number;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
avatar?: string;
|
||||||
|
sort: number;
|
||||||
|
status: number;
|
||||||
|
createTime?: Date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 获得频道分页 */
|
||||||
|
export function getManagerChannelPage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<ImManagerChannelApi.Channel>>(
|
||||||
|
'/im/manager/channel/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得频道详情 */
|
||||||
|
export function getManagerChannel(id: number) {
|
||||||
|
return requestClient.get<ImManagerChannelApi.Channel>('/im/manager/channel/get', {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增频道 */
|
||||||
|
export function createManagerChannel(data: ImManagerChannelApi.Channel) {
|
||||||
|
return requestClient.post<number>('/im/manager/channel/create', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改频道 */
|
||||||
|
export function updateManagerChannel(data: ImManagerChannelApi.Channel) {
|
||||||
|
return requestClient.put<boolean>('/im/manager/channel/update', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除频道 */
|
||||||
|
export function deleteManagerChannel(id: number) {
|
||||||
|
return requestClient.delete<boolean>('/im/manager/channel/delete', {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得启用的频道精简列表(表单选择用) */
|
||||||
|
export function getSimpleChannelList() {
|
||||||
|
return requestClient.get<ImManagerChannelApi.Channel[]>(
|
||||||
|
'/im/manager/channel/simple-list',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImManagerChannelMaterialApi {
|
||||||
|
/** 频道素材 */
|
||||||
|
export interface Material {
|
||||||
|
id: number;
|
||||||
|
channelId: number;
|
||||||
|
channelName?: string;
|
||||||
|
type: number;
|
||||||
|
title: string;
|
||||||
|
coverUrl?: string;
|
||||||
|
summary?: string;
|
||||||
|
content?: string;
|
||||||
|
url?: string;
|
||||||
|
createTime?: Date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 获得素材分页 */
|
||||||
|
export function getManagerChannelMaterialPage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<ImManagerChannelMaterialApi.Material>>(
|
||||||
|
'/im/manager/channel-material/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得指定频道下的素材精简列表 */
|
||||||
|
export function getSimpleManagerChannelMaterialList(channelId: number) {
|
||||||
|
return requestClient.get<ImManagerChannelMaterialApi.Material[]>(
|
||||||
|
'/im/manager/channel-material/simple-list',
|
||||||
|
{ params: { channelId } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得素材详情 */
|
||||||
|
export function getManagerChannelMaterial(id: number) {
|
||||||
|
return requestClient.get<ImManagerChannelMaterialApi.Material>(
|
||||||
|
'/im/manager/channel-material/get',
|
||||||
|
{ params: { id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增素材 */
|
||||||
|
export function createManagerChannelMaterial(
|
||||||
|
data: ImManagerChannelMaterialApi.Material,
|
||||||
|
) {
|
||||||
|
return requestClient.post<number>('/im/manager/channel-material/create', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改素材 */
|
||||||
|
export function updateManagerChannelMaterial(
|
||||||
|
data: ImManagerChannelMaterialApi.Material,
|
||||||
|
) {
|
||||||
|
return requestClient.put<boolean>(
|
||||||
|
'/im/manager/channel-material/update',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除素材 */
|
||||||
|
export function deleteManagerChannelMaterial(id: number) {
|
||||||
|
return requestClient.delete<boolean>('/im/manager/channel-material/delete', {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImManagerChannelMessageApi {
|
||||||
|
/** 频道消息 */
|
||||||
|
export interface ChannelMessage {
|
||||||
|
id: number;
|
||||||
|
channelId: number;
|
||||||
|
channelName?: string;
|
||||||
|
materialId: number;
|
||||||
|
materialTitle?: string;
|
||||||
|
materialCoverUrl?: string;
|
||||||
|
type: number;
|
||||||
|
content?: string;
|
||||||
|
receiverUserIds?: number[];
|
||||||
|
sendTime?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 频道消息发送请求 */
|
||||||
|
export interface ChannelMessageSendReqVO {
|
||||||
|
materialId: number;
|
||||||
|
receiverUserIds?: number[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 立即推送频道消息 */
|
||||||
|
export function sendManagerChannelMessage(
|
||||||
|
data: ImManagerChannelMessageApi.ChannelMessageSendReqVO,
|
||||||
|
) {
|
||||||
|
return requestClient.post<number>('/im/manager/channel-message/send', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除频道消息 */
|
||||||
|
export function deleteManagerChannelMessage(id: number) {
|
||||||
|
return requestClient.delete<boolean>('/im/manager/channel-message/delete', {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得频道消息分页 */
|
||||||
|
export function getManagerChannelMessagePage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<ImManagerChannelMessageApi.ChannelMessage>>(
|
||||||
|
'/im/manager/channel-message/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImManagerFacePackItemApi {
|
||||||
|
/** 表情项 */
|
||||||
|
export interface FacePackItem {
|
||||||
|
id: number;
|
||||||
|
packId: number;
|
||||||
|
url: string;
|
||||||
|
name?: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
sort: number;
|
||||||
|
status: number;
|
||||||
|
createTime?: Date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 获得表情分页 */
|
||||||
|
export function getManagerFacePackItemPage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<ImManagerFacePackItemApi.FacePackItem>>(
|
||||||
|
'/im/manager/face-pack-item/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得表情详情 */
|
||||||
|
export function getManagerFacePackItem(id: number) {
|
||||||
|
return requestClient.get<ImManagerFacePackItemApi.FacePackItem>(
|
||||||
|
'/im/manager/face-pack-item/get',
|
||||||
|
{ params: { id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增表情 */
|
||||||
|
export function createManagerFacePackItem(data: ImManagerFacePackItemApi.FacePackItem) {
|
||||||
|
return requestClient.post<number>('/im/manager/face-pack-item/create', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改表情 */
|
||||||
|
export function updateManagerFacePackItem(data: ImManagerFacePackItemApi.FacePackItem) {
|
||||||
|
return requestClient.put<boolean>(
|
||||||
|
'/im/manager/face-pack-item/update',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除表情 */
|
||||||
|
export function deleteManagerFacePackItem(id: number) {
|
||||||
|
return requestClient.delete<boolean>('/im/manager/face-pack-item/delete', {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除表情 */
|
||||||
|
export function deleteManagerFacePackItemList(ids: number[]) {
|
||||||
|
return requestClient.delete<boolean>(
|
||||||
|
'/im/manager/face-pack-item/delete-list',
|
||||||
|
{ params: { ids: ids.join(',') } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImManagerFacePackApi {
|
||||||
|
/** 表情包 */
|
||||||
|
export interface FacePack {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
icon?: string;
|
||||||
|
sort: number;
|
||||||
|
status: number;
|
||||||
|
createTime?: Date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 获得表情包分页 */
|
||||||
|
export function getManagerFacePackPage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<ImManagerFacePackApi.FacePack>>(
|
||||||
|
'/im/manager/face-pack/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得表情包详情 */
|
||||||
|
export function getManagerFacePack(id: number) {
|
||||||
|
return requestClient.get<ImManagerFacePackApi.FacePack>('/im/manager/face-pack/get', {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增表情包 */
|
||||||
|
export function createManagerFacePack(data: ImManagerFacePackApi.FacePack) {
|
||||||
|
return requestClient.post<number>('/im/manager/face-pack/create', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改表情包 */
|
||||||
|
export function updateManagerFacePack(data: ImManagerFacePackApi.FacePack) {
|
||||||
|
return requestClient.put<boolean>('/im/manager/face-pack/update', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除表情包 */
|
||||||
|
export function deleteManagerFacePack(id: number) {
|
||||||
|
return requestClient.delete<boolean>('/im/manager/face-pack/delete', {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除表情包 */
|
||||||
|
export function deleteManagerFacePackList(ids: number[]) {
|
||||||
|
return requestClient.delete<boolean>('/im/manager/face-pack/delete-list', {
|
||||||
|
params: { ids: ids.join(',') },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImManagerFaceUserItemApi {
|
||||||
|
/** 用户表情 */
|
||||||
|
export interface FaceUserItem {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
userNickname?: string;
|
||||||
|
url: string;
|
||||||
|
name?: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
createTime?: Date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 获得用户表情分页 */
|
||||||
|
export function getManagerFaceUserItemPage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<ImManagerFaceUserItemApi.FaceUserItem>>(
|
||||||
|
'/im/manager/face-user-item/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除用户表情 */
|
||||||
|
export function deleteManagerFaceUserItem(id: number) {
|
||||||
|
return requestClient.delete<boolean>('/im/manager/face-user-item/delete', {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImManagerFriendApi {
|
||||||
|
/** 好友关系 */
|
||||||
|
export interface Friend {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
userNickname?: string;
|
||||||
|
friendUserId: number;
|
||||||
|
friendNickname?: string;
|
||||||
|
displayName?: string;
|
||||||
|
addSource?: number;
|
||||||
|
silent: boolean;
|
||||||
|
pinned: boolean;
|
||||||
|
blocked: boolean;
|
||||||
|
status: number;
|
||||||
|
addTime?: Date;
|
||||||
|
deleteTime?: Date;
|
||||||
|
createTime: Date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 获得好友关系分页 */
|
||||||
|
export function getManagerFriendPage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<ImManagerFriendApi.Friend>>(
|
||||||
|
'/im/manager/friend/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImManagerFriendRequestApi {
|
||||||
|
/** 好友申请 */
|
||||||
|
export interface FriendRequest {
|
||||||
|
id: number;
|
||||||
|
fromUserId: number;
|
||||||
|
fromNickname?: string;
|
||||||
|
toUserId: number;
|
||||||
|
toNickname?: string;
|
||||||
|
applyContent?: string;
|
||||||
|
displayName?: string;
|
||||||
|
addSource?: number;
|
||||||
|
handleResult: number;
|
||||||
|
handleContent?: string;
|
||||||
|
handleTime?: Date;
|
||||||
|
createTime: Date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 获得好友申请分页 */
|
||||||
|
export function getManagerFriendRequestPage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<ImManagerFriendRequestApi.FriendRequest>>(
|
||||||
|
'/im/manager/friend-request/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImManagerGroupApi {
|
||||||
|
/** 群 */
|
||||||
|
export interface Group {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
avatar?: string;
|
||||||
|
notice?: string;
|
||||||
|
ownerUserId: number;
|
||||||
|
ownerNickname?: string;
|
||||||
|
memberCount?: number;
|
||||||
|
status: number;
|
||||||
|
banned: boolean;
|
||||||
|
mutedAll?: boolean; // 是否全群禁言
|
||||||
|
bannedReason?: string;
|
||||||
|
bannedTime?: Date;
|
||||||
|
dissolvedTime?: Date;
|
||||||
|
createTime: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 群成员 */
|
||||||
|
export interface GroupMember {
|
||||||
|
userId: number;
|
||||||
|
nickname?: string;
|
||||||
|
avatar?: string;
|
||||||
|
displayUserName?: string;
|
||||||
|
groupRemark?: string;
|
||||||
|
silent?: boolean;
|
||||||
|
status: number;
|
||||||
|
role?: number; // 成员角色,参见 ImGroupMemberRole 枚举
|
||||||
|
joinTime?: Date;
|
||||||
|
quitTime?: Date;
|
||||||
|
muteEndTime?: Date; // 禁言到期时间
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 群封禁请求 */
|
||||||
|
export interface GroupBanReqVO {
|
||||||
|
id: number;
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 获得群分页 */
|
||||||
|
export function getManagerGroupPage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<ImManagerGroupApi.Group>>(
|
||||||
|
'/im/manager/group/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得群详情 */
|
||||||
|
export function getManagerGroup(id: number) {
|
||||||
|
return requestClient.get<ImManagerGroupApi.Group>('/im/manager/group/get', {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 封禁群 */
|
||||||
|
export function banManagerGroup(data: ImManagerGroupApi.GroupBanReqVO) {
|
||||||
|
return requestClient.put<boolean>('/im/manager/group/ban', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解封群 */
|
||||||
|
export function unbanManagerGroup(id: number) {
|
||||||
|
return requestClient.put<boolean>('/im/manager/group/unban', undefined, {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解散群 */
|
||||||
|
export function dissolveManagerGroup(id: number) {
|
||||||
|
return requestClient.delete<boolean>('/im/manager/group/dissolve', {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得群成员列表(含已退群成员,由前端按需过滤) */
|
||||||
|
export function getManagerGroupMemberList(groupId: number) {
|
||||||
|
return requestClient.get<ImManagerGroupApi.GroupMember[]>(
|
||||||
|
'/im/manager/group/member/list',
|
||||||
|
{ params: { groupId } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImManagerGroupRequestApi {
|
||||||
|
/** 加群申请 */
|
||||||
|
export interface GroupRequest {
|
||||||
|
id: number;
|
||||||
|
groupId: number;
|
||||||
|
groupName?: string;
|
||||||
|
userId: number;
|
||||||
|
userNickname?: string;
|
||||||
|
inviterUserId?: number;
|
||||||
|
inviterNickname?: string;
|
||||||
|
applyContent?: string;
|
||||||
|
addSource?: number;
|
||||||
|
handleResult: number;
|
||||||
|
handleUserId?: number;
|
||||||
|
handleNickname?: string;
|
||||||
|
handleContent?: string;
|
||||||
|
handleTime?: Date;
|
||||||
|
createTime: Date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 获得加群申请分页 */
|
||||||
|
export function getManagerGroupRequestPage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<ImManagerGroupRequestApi.GroupRequest>>(
|
||||||
|
'/im/manager/group-request/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImManagerGroupMessageApi {
|
||||||
|
/** 群聊消息 */
|
||||||
|
export interface GroupMessage {
|
||||||
|
id: number;
|
||||||
|
clientMessageId?: string;
|
||||||
|
groupId: number;
|
||||||
|
groupName?: string;
|
||||||
|
senderId: number;
|
||||||
|
senderNickname?: string;
|
||||||
|
type: number;
|
||||||
|
content: string;
|
||||||
|
status: number;
|
||||||
|
atUserIds?: number[];
|
||||||
|
atUserNicknames?: (null | string)[];
|
||||||
|
receiptStatus: number;
|
||||||
|
sendTime: Date;
|
||||||
|
createTime: Date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 获得群聊消息分页 */
|
||||||
|
export function getManagerGroupMessagePage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<ImManagerGroupMessageApi.GroupMessage>>(
|
||||||
|
'/im/manager/message/group/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得群聊消息详情 */
|
||||||
|
export function getManagerGroupMessage(id: number) {
|
||||||
|
return requestClient.get<ImManagerGroupMessageApi.GroupMessage>(
|
||||||
|
'/im/manager/message/group/get',
|
||||||
|
{ params: { id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImManagerPrivateMessageApi {
|
||||||
|
/** 私聊消息 */
|
||||||
|
export interface PrivateMessage {
|
||||||
|
id: number;
|
||||||
|
clientMessageId?: string;
|
||||||
|
senderId: number;
|
||||||
|
senderNickname?: string;
|
||||||
|
receiverId: number;
|
||||||
|
receiverNickname?: string;
|
||||||
|
type: number;
|
||||||
|
content: string;
|
||||||
|
status: number;
|
||||||
|
receiptStatus?: number;
|
||||||
|
sendTime: Date;
|
||||||
|
createTime: Date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 获得私聊消息分页 */
|
||||||
|
export function getManagerPrivateMessagePage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<ImManagerPrivateMessageApi.PrivateMessage>>(
|
||||||
|
'/im/manager/message/private/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得私聊消息详情 */
|
||||||
|
export function getManagerPrivateMessage(id: number) {
|
||||||
|
return requestClient.get<ImManagerPrivateMessageApi.PrivateMessage>(
|
||||||
|
'/im/manager/message/private/get',
|
||||||
|
{ params: { id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImManagerRtcApi {
|
||||||
|
/** RTC 通话 */
|
||||||
|
export interface RtcCall {
|
||||||
|
id: number;
|
||||||
|
room: string;
|
||||||
|
conversationType: number;
|
||||||
|
mediaType: number;
|
||||||
|
inviterUserId: number;
|
||||||
|
inviterNickname?: string;
|
||||||
|
groupId?: number;
|
||||||
|
groupName?: string;
|
||||||
|
status: number;
|
||||||
|
endReason?: number;
|
||||||
|
startTime: Date;
|
||||||
|
acceptTime?: Date;
|
||||||
|
endTime?: Date;
|
||||||
|
createTime: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RTC 通话参与者 */
|
||||||
|
export interface RtcParticipant {
|
||||||
|
id: number;
|
||||||
|
callId: number;
|
||||||
|
userId: number;
|
||||||
|
userNickname?: string;
|
||||||
|
role: number;
|
||||||
|
status: number;
|
||||||
|
inviteTime: Date;
|
||||||
|
acceptTime?: Date;
|
||||||
|
leaveTime?: Date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 获得通话记录分页 */
|
||||||
|
export function getManagerRtcCallPage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<ImManagerRtcApi.RtcCall>>(
|
||||||
|
'/im/manager/rtc/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得通话参与者列表 */
|
||||||
|
export function getManagerRtcCallParticipantList(id: number) {
|
||||||
|
return requestClient.get<ImManagerRtcApi.RtcParticipant[]>(
|
||||||
|
'/im/manager/rtc/participant-list',
|
||||||
|
{ params: { id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImManagerSensitiveWordApi {
|
||||||
|
/** 敏感词 */
|
||||||
|
export interface SensitiveWord {
|
||||||
|
id: number;
|
||||||
|
word: string;
|
||||||
|
status: number;
|
||||||
|
creator?: string;
|
||||||
|
creatorName?: string;
|
||||||
|
createTime?: Date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 获得敏感词分页 */
|
||||||
|
export function getManagerSensitiveWordPage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<ImManagerSensitiveWordApi.SensitiveWord>>(
|
||||||
|
'/im/manager/sensitive-word/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得敏感词详情 */
|
||||||
|
export function getManagerSensitiveWord(id: number) {
|
||||||
|
return requestClient.get<ImManagerSensitiveWordApi.SensitiveWord>(
|
||||||
|
'/im/manager/sensitive-word/get',
|
||||||
|
{ params: { id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增敏感词 */
|
||||||
|
export function createManagerSensitiveWord(data: ImManagerSensitiveWordApi.SensitiveWord) {
|
||||||
|
return requestClient.post<number>('/im/manager/sensitive-word/create', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改敏感词 */
|
||||||
|
export function updateManagerSensitiveWord(data: ImManagerSensitiveWordApi.SensitiveWord) {
|
||||||
|
return requestClient.put<boolean>(
|
||||||
|
'/im/manager/sensitive-word/update',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除敏感词 */
|
||||||
|
export function deleteManagerSensitiveWord(id: number) {
|
||||||
|
return requestClient.delete<boolean>('/im/manager/sensitive-word/delete', {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除敏感词 */
|
||||||
|
export function deleteManagerSensitiveWordList(ids: number[]) {
|
||||||
|
return requestClient.delete<boolean>(
|
||||||
|
'/im/manager/sensitive-word/delete-list',
|
||||||
|
{ params: { ids: ids.join(',') } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImManagerStatisticsApi {
|
||||||
|
/** 统计概览 */
|
||||||
|
export interface Overview {
|
||||||
|
totalUser: number;
|
||||||
|
newUserToday: number;
|
||||||
|
totalGroup: number;
|
||||||
|
newGroupToday: number;
|
||||||
|
activeUserDaily: number;
|
||||||
|
activeUserWeekly: number;
|
||||||
|
activeUserMonthly: number;
|
||||||
|
privateMessageToday: number;
|
||||||
|
groupMessageToday: number;
|
||||||
|
privateMessageYesterday: number;
|
||||||
|
groupMessageYesterday: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 趋势数据 */
|
||||||
|
export interface Trend {
|
||||||
|
dates: string[];
|
||||||
|
series: Record<string, number[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 消息类型分布 */
|
||||||
|
export interface MessageType {
|
||||||
|
type: number; // 参见 ImContentTypeEnum 枚举类,由前端按 DICT_TYPE.IM_CONTENT_TYPE 翻译
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 群规模分布 */
|
||||||
|
export interface GroupSize {
|
||||||
|
range: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 消息发送排行 */
|
||||||
|
export interface TopSender {
|
||||||
|
userId: number;
|
||||||
|
nickname: string;
|
||||||
|
messageCount: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 获得 KPI 概览 */
|
||||||
|
export function getStatisticsOverview() {
|
||||||
|
return requestClient.get<ImManagerStatisticsApi.Overview>(
|
||||||
|
'/im/manager/statistics/overview',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得消息趋势(私聊 + 群聊双线) */
|
||||||
|
export function getMessageTrend(days: number) {
|
||||||
|
return requestClient.get<ImManagerStatisticsApi.Trend>(
|
||||||
|
'/im/manager/statistics/message-trend',
|
||||||
|
{ params: { days } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得用户趋势(新增注册 + 日活双线) */
|
||||||
|
export function getUserTrend(days: number) {
|
||||||
|
return requestClient.get<ImManagerStatisticsApi.Trend>(
|
||||||
|
'/im/manager/statistics/user-trend',
|
||||||
|
{ params: { days } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得内容类型分布(最近 30 天) */
|
||||||
|
export function getMessageTypeDistribution() {
|
||||||
|
return requestClient.get<ImManagerStatisticsApi.MessageType[]>(
|
||||||
|
'/im/manager/statistics/message-type-distribution',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得群规模分布 */
|
||||||
|
export function getGroupSizeDistribution() {
|
||||||
|
return requestClient.get<ImManagerStatisticsApi.GroupSize[]>(
|
||||||
|
'/im/manager/statistics/group-size-distribution',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得消息 TOP 发送者(最近 30 天) */
|
||||||
|
export function getTopSenders() {
|
||||||
|
return requestClient.get<ImManagerStatisticsApi.TopSender[]>(
|
||||||
|
'/im/manager/statistics/top-senders',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImChannelMessageApi {
|
||||||
|
/** 频道消息 */
|
||||||
|
export interface ChannelMessageRespVO {
|
||||||
|
id: number;
|
||||||
|
clientMessageId?: string;
|
||||||
|
channelId: number;
|
||||||
|
materialId: number;
|
||||||
|
type: number;
|
||||||
|
content: string;
|
||||||
|
receiptStatus?: number;
|
||||||
|
sendTime: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 拉取当前用户应收的频道消息(离线增量);按 minId 游标分页 */
|
||||||
|
export function pullChannelMessageList(
|
||||||
|
params: { minId: number; size?: number },
|
||||||
|
signal?: AbortSignal,
|
||||||
|
) {
|
||||||
|
return requestClient.get<ImChannelMessageApi.ChannelMessageRespVO[]>(
|
||||||
|
'/im/channel/message/pull',
|
||||||
|
{ params, signal },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 上报频道消息已读位置;切到频道会话或拉到新消息后调 */
|
||||||
|
export function readChannelMessages(channelId: number, messageId: number) {
|
||||||
|
return requestClient.put<boolean>('/im/channel/message/read', undefined, {
|
||||||
|
params: { channelId, messageId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImGroupMessageApi {
|
||||||
|
/** 群聊消息 Response VO */
|
||||||
|
export interface GroupMessageRespVO {
|
||||||
|
id: number; // 消息编号
|
||||||
|
clientMessageId: string; // 客户端消息编号
|
||||||
|
senderId: number; // 发送人编号
|
||||||
|
groupId: number; // 群编号
|
||||||
|
type: number; // 内容类型
|
||||||
|
content: string; // 消息内容(JSON 格式)
|
||||||
|
status: number; // 消息状态
|
||||||
|
sendTime: string; // 发送时间
|
||||||
|
atUserIds?: number[]; // @ 目标用户编号列表
|
||||||
|
receiverUserIds?: number[]; // 定向接收用户编号列表
|
||||||
|
receiptStatus?: number; // 回执状态
|
||||||
|
readCount?: number; // 已读人数(回执消息、且发送人为当前用户时有值)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 群聊消息发送 Request VO */
|
||||||
|
export interface GroupMessageSendReqVO {
|
||||||
|
clientMessageId: string; // 客户端消息编号
|
||||||
|
groupId: number; // 群编号
|
||||||
|
type: number; // 内容类型
|
||||||
|
content: string; // 消息内容(JSON 格式)
|
||||||
|
atUserIds?: number[]; // @ 目标用户编号列表
|
||||||
|
receipt?: boolean; // 是否需要回执
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 群聊历史消息列表 Request VO */
|
||||||
|
export interface GroupMessageListReqVO {
|
||||||
|
groupId: number | string; // 群编号
|
||||||
|
maxId?: number | string; // 起始消息编号(不含),为空则从最新消息开始
|
||||||
|
limit: number; // 拉取数量(1 ~ 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 发送群聊消息 */
|
||||||
|
export function sendGroupMessage(data: ImGroupMessageApi.GroupMessageSendReqVO) {
|
||||||
|
return requestClient.post<ImGroupMessageApi.GroupMessageRespVO>(
|
||||||
|
'/im/message/group/send',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拉取群聊消息(增量) */
|
||||||
|
export function pullGroupMessageList(
|
||||||
|
params: { minId: number | string; size: number },
|
||||||
|
signal?: AbortSignal,
|
||||||
|
) {
|
||||||
|
return requestClient.get<ImGroupMessageApi.GroupMessageRespVO[]>(
|
||||||
|
'/im/message/group/pull',
|
||||||
|
{ params, signal },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询群聊历史消息 */
|
||||||
|
export function getGroupMessageList(params: ImGroupMessageApi.GroupMessageListReqVO) {
|
||||||
|
return requestClient.get<ImGroupMessageApi.GroupMessageRespVO[]>(
|
||||||
|
'/im/message/group/list',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标记群聊消息已读 */
|
||||||
|
export function readGroupMessages(
|
||||||
|
groupId: number | string,
|
||||||
|
messageId: number | string,
|
||||||
|
) {
|
||||||
|
return requestClient.put<boolean>('/im/message/group/read', undefined, {
|
||||||
|
params: { groupId, messageId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 撤回群聊消息 */
|
||||||
|
export function recallGroupMessage(id: number | string) {
|
||||||
|
return requestClient.delete<ImGroupMessageApi.GroupMessageRespVO>(
|
||||||
|
'/im/message/group/recall',
|
||||||
|
{ params: { id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取群消息已读用户列表 */
|
||||||
|
export function getGroupReadUsers(params: {
|
||||||
|
groupId: number | string;
|
||||||
|
messageId: number | string;
|
||||||
|
}) {
|
||||||
|
return requestClient.get<number[]>('/im/message/group/get-read-user-ids', {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImPrivateMessageApi {
|
||||||
|
/** 私聊消息 Response VO */
|
||||||
|
export interface PrivateMessageRespVO {
|
||||||
|
id: number; // 消息编号
|
||||||
|
clientMessageId: string; // 客户端消息编号
|
||||||
|
senderId: number; // 发送人编号
|
||||||
|
receiverId: number; // 接收人编号
|
||||||
|
type: number; // 内容类型
|
||||||
|
content: string; // 消息内容(JSON 格式)
|
||||||
|
status: number; // 消息状态(正常 / 已撤回)
|
||||||
|
receiptStatus?: number; // 回执状态(不需要 / 待完成 / 已完成),对齐 ImMessageReceiptStatus
|
||||||
|
sendTime: string; // 发送时间
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 私聊消息发送 Request VO */
|
||||||
|
export interface PrivateMessageSendReqVO {
|
||||||
|
clientMessageId: string; // 客户端消息编号
|
||||||
|
receiverId: number; // 接收人编号
|
||||||
|
type: number; // 内容类型
|
||||||
|
content: string; // 消息内容(JSON 格式)
|
||||||
|
receipt?: boolean; // 是否需要回执;不传后端默认 true(普通私聊用户消息)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 私聊历史消息列表 Request VO */
|
||||||
|
export interface PrivateMessageListReqVO {
|
||||||
|
receiverId: number | string; // 接收人编号(对方)
|
||||||
|
maxId?: number | string; // 起始消息编号(不含),为空则从最新消息开始
|
||||||
|
limit: number; // 拉取数量(1 ~ 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 发送私聊消息 */
|
||||||
|
export function sendPrivateMessage(data: ImPrivateMessageApi.PrivateMessageSendReqVO) {
|
||||||
|
return requestClient.post<ImPrivateMessageApi.PrivateMessageRespVO>(
|
||||||
|
'/im/message/private/send',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拉取私聊消息(增量) */
|
||||||
|
export function pullPrivateMessageList(
|
||||||
|
params: { minId: number | string; size: number },
|
||||||
|
signal?: AbortSignal,
|
||||||
|
) {
|
||||||
|
return requestClient.get<ImPrivateMessageApi.PrivateMessageRespVO[]>(
|
||||||
|
'/im/message/private/pull',
|
||||||
|
{ params, signal },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询私聊历史消息 */
|
||||||
|
export function getPrivateMessageList(params: ImPrivateMessageApi.PrivateMessageListReqVO) {
|
||||||
|
return requestClient.get<ImPrivateMessageApi.PrivateMessageRespVO[]>(
|
||||||
|
'/im/message/private/list',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标记私聊消息已读 */
|
||||||
|
export function readPrivateMessages(
|
||||||
|
receiverId: number | string,
|
||||||
|
messageId: number | string,
|
||||||
|
) {
|
||||||
|
return requestClient.put<boolean>('/im/message/private/read', undefined, {
|
||||||
|
params: { receiverId, messageId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询对方已读到我发的最大消息 id(多端 / 离线后用于补齐已读状态) */
|
||||||
|
export function getPrivateMaxReadMessageId(
|
||||||
|
peerId: number | string,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
) {
|
||||||
|
return requestClient.get<null | number>(
|
||||||
|
'/im/message/private/max-read-message-id',
|
||||||
|
{ params: { peerId }, signal },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 撤回私聊消息 */
|
||||||
|
export function recallPrivateMessage(id: number | string) {
|
||||||
|
return requestClient.delete<ImPrivateMessageApi.PrivateMessageRespVO>(
|
||||||
|
'/im/message/private/recall',
|
||||||
|
{ params: { id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace ImRtcApi {
|
||||||
|
/** 创建新通话请求 VO */
|
||||||
|
export interface RtcCallCreateReqVO {
|
||||||
|
conversationType: number;
|
||||||
|
mediaType: number;
|
||||||
|
groupId?: number;
|
||||||
|
inviteeIds: number[]; // 被邀请的用户编号集合;私聊必传 1 个对端,群聊必传至少 1 人
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 通话中追加邀请请求 VO;仅群通话可用 */
|
||||||
|
export interface RtcCallInviteReqVO {
|
||||||
|
room: string;
|
||||||
|
inviteeIds: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 通话会话 VO;create / join / accept / refreshToken 共用 */
|
||||||
|
export interface RtcCallRespVO {
|
||||||
|
room: string; // 业务通话编号(同时作为 LiveKit 房间名)
|
||||||
|
livekitUrl: string;
|
||||||
|
token?: string; // ENDED 状态时为 null(无需 connect LiveKit)
|
||||||
|
conversationType: number;
|
||||||
|
mediaType: number;
|
||||||
|
status: number;
|
||||||
|
endReason?: number; // 结束原因;仅 status=ENDED 时有值
|
||||||
|
inviterId: number;
|
||||||
|
groupId?: number;
|
||||||
|
inviteeIds?: number[];
|
||||||
|
joinedUserIds?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 群活跃通话查询响应;不含 token */
|
||||||
|
export interface RtcGroupCallRespVO {
|
||||||
|
room: string;
|
||||||
|
groupId: number;
|
||||||
|
mediaType: number;
|
||||||
|
inviterId: number;
|
||||||
|
joinedUserIds?: number[];
|
||||||
|
inviteeIds?: number[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 创建新通话;私聊或群聊根据 conversationType 区分 */
|
||||||
|
export function createCall(data: ImRtcApi.RtcCallCreateReqVO) {
|
||||||
|
return requestClient.post<ImRtcApi.RtcCallRespVO>('/im/rtc/create', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 通话中追加邀请;仅群通话可用 */
|
||||||
|
export function inviteCall(data: ImRtcApi.RtcCallInviteReqVO) {
|
||||||
|
return requestClient.post<boolean>('/im/rtc/invite', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加入已有群通话;用于胶囊条「加入」按钮 */
|
||||||
|
export function joinCall(room: string) {
|
||||||
|
return requestClient.post<ImRtcApi.RtcCallRespVO>('/im/rtc/join', undefined, {
|
||||||
|
params: { room },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 接听通话 */
|
||||||
|
export function acceptCall(room: string) {
|
||||||
|
return requestClient.post<ImRtcApi.RtcCallRespVO>('/im/rtc/accept', undefined, {
|
||||||
|
params: { room },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拒绝通话 */
|
||||||
|
export function rejectCall(room: string) {
|
||||||
|
return requestClient.post<boolean>('/im/rtc/reject', undefined, {
|
||||||
|
params: { room },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消邀请;主叫接通前调用 */
|
||||||
|
export function cancelCall(room: string) {
|
||||||
|
return requestClient.post<boolean>('/im/rtc/cancel', undefined, {
|
||||||
|
params: { room },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 离开通话;接通后调用 */
|
||||||
|
export function leaveCall(room: string) {
|
||||||
|
return requestClient.post<boolean>('/im/rtc/leave', undefined, {
|
||||||
|
params: { room },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 振铃超时检查;RUNNING 端 timer 兜底,触发后端立即扫描该 room 的超时 INVITING(接口静默) */
|
||||||
|
export function noAnswerCallCheck(room: string) {
|
||||||
|
return requestClient.post<boolean>(
|
||||||
|
'/im/rtc/no-answer-call-check',
|
||||||
|
undefined,
|
||||||
|
{ params: { room } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询当前进行中的通话;目前仅群聊场景(胶囊条),返回 null 表示无活跃通话 */
|
||||||
|
export function getActiveCall(groupId: number) {
|
||||||
|
return requestClient.get<ImRtcApi.RtcGroupCallRespVO | null>(
|
||||||
|
'/im/rtc/get-active-call',
|
||||||
|
{ params: { groupId } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -75,3 +75,8 @@ export function runJob(id: number) {
|
||||||
export function getJobNextTimes(id: number) {
|
export function getJobNextTimes(id: number) {
|
||||||
return requestClient.get(`/infra/job/get_next_times?id=${id}`);
|
return requestClient.get(`/infra/job/get_next_times?id=${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 同步定时任务到 Quartz */
|
||||||
|
export function syncJob() {
|
||||||
|
return requestClient.post('/infra/job/sync');
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,21 @@ export namespace MesWmSnApi {
|
||||||
createTime?: Date; // 生成时间
|
createTime?: Date; // 生成时间
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** MES SN 码明细 */
|
||||||
|
export interface Sn {
|
||||||
|
id?: number; // 编号
|
||||||
|
uuid?: string; // 批次 UUID
|
||||||
|
code?: string; // SN 码
|
||||||
|
itemId?: number; // 物料编号
|
||||||
|
itemCode?: string; // 物料编码
|
||||||
|
itemName?: string; // 物料名称
|
||||||
|
specification?: string; // 规格型号
|
||||||
|
unitName?: string; // 单位名称
|
||||||
|
batchCode?: string; // 批次号
|
||||||
|
workOrderId?: number; // 生产工单编号
|
||||||
|
createTime?: Date; // 生成时间
|
||||||
|
}
|
||||||
|
|
||||||
/** MES SN 码生成参数 */
|
/** MES SN 码生成参数 */
|
||||||
export interface SnGenerate {
|
export interface SnGenerate {
|
||||||
itemId?: number; // 物料编号
|
itemId?: number; // 物料编号
|
||||||
|
|
@ -48,6 +63,13 @@ export function getSnGroupPage(params: MesWmSnApi.PageParams) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 查询批次 SN 码明细列表 */
|
||||||
|
export function getSnListByUuid(uuid: string) {
|
||||||
|
return requestClient.get<MesWmSnApi.Sn[]>('/mes/wm/sn/list-by-uuid', {
|
||||||
|
params: { uuid },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 批量删除 SN 码(按批次 UUID) */
|
/** 批量删除 SN 码(按批次 UUID) */
|
||||||
export function deleteSnBatch(uuid: string) {
|
export function deleteSnBatch(uuid: string) {
|
||||||
return requestClient.delete('/mes/wm/sn/delete-batch', {
|
return requestClient.delete('/mes/wm/sn/delete-batch', {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,19 @@ import type { PageParam, PageResult } from '@vben/request';
|
||||||
import { requestClient } from '#/api/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
export namespace MpFreePublishApi {
|
export namespace MpFreePublishApi {
|
||||||
|
/** 图文文章内容 */
|
||||||
|
export interface FreePublishArticle {
|
||||||
|
title?: string;
|
||||||
|
thumbUrl?: string;
|
||||||
|
picUrl?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 图文内容 */
|
||||||
|
export interface FreePublishContent {
|
||||||
|
newsItem?: FreePublishArticle[];
|
||||||
|
}
|
||||||
|
|
||||||
/** 自由发布文章信息 */
|
/** 自由发布文章信息 */
|
||||||
export interface FreePublish {
|
export interface FreePublish {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|
@ -12,7 +25,7 @@ export namespace MpFreePublishApi {
|
||||||
title: string;
|
title: string;
|
||||||
author: string;
|
author: string;
|
||||||
digest: string;
|
digest: string;
|
||||||
content: string;
|
content?: FreePublishContent;
|
||||||
thumbUrl: string;
|
thumbUrl: string;
|
||||||
status: number;
|
status: number;
|
||||||
publishTime?: Date;
|
publishTime?: Date;
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,23 @@ export namespace SystemUserApi {
|
||||||
username: string;
|
username: string;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
deptId: number;
|
deptId: number;
|
||||||
|
deptName?: string;
|
||||||
postIds: string[];
|
postIds: string[];
|
||||||
email: string;
|
email: string;
|
||||||
mobile: string;
|
mobile: string;
|
||||||
sex: number;
|
sex: number;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
loginIp: string;
|
loginIp: string;
|
||||||
|
loginDate?: Date;
|
||||||
status: number;
|
status: number;
|
||||||
remark: string;
|
remark: string;
|
||||||
createTime?: Date;
|
createTime?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 用户精简信息 */
|
||||||
|
export interface UserSimple extends User {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询用户管理列表 */
|
/** 查询用户管理列表 */
|
||||||
|
|
@ -34,6 +41,13 @@ export function getUser(id: number) {
|
||||||
return requestClient.get<SystemUserApi.User>(`/system/user/get?id=${id}`);
|
return requestClient.get<SystemUserApi.User>(`/system/user/get?id=${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 查询用户列表 */
|
||||||
|
export function getUserList(ids: number[]) {
|
||||||
|
return requestClient.get<SystemUserApi.User[]>('/system/user/list', {
|
||||||
|
params: { ids: ids.join(',') },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 新增用户 */
|
/** 新增用户 */
|
||||||
export function createUser(data: SystemUserApi.User) {
|
export function createUser(data: SystemUserApi.User) {
|
||||||
return requestClient.post('/system/user/create', data);
|
return requestClient.post('/system/user/create', data);
|
||||||
|
|
@ -86,3 +100,17 @@ export function updateUserStatus(id: number, status: number) {
|
||||||
export function getSimpleUserList() {
|
export function getSimpleUserList() {
|
||||||
return requestClient.get<SystemUserApi.User[]>('/system/user/simple-list');
|
return requestClient.get<SystemUserApi.User[]>('/system/user/simple-list');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 按用户编号查询用户精简信息 */
|
||||||
|
export function getSimpleUser(id: number | string) {
|
||||||
|
return requestClient.get<SystemUserApi.UserSimple>('/system/user/get-simple', {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按昵称模糊搜索用户 */
|
||||||
|
export function getSimpleUserListByNickname(nickname: string) {
|
||||||
|
return requestClient.get<SystemUserApi.UserSimple[]>('/system/user/list-by-nickname', {
|
||||||
|
params: { nickname },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,125 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CascaderProps } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import type { SystemAreaApi } from '#/api/system/area';
|
||||||
|
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Cascader } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getAreaTree } from '#/api/system/area';
|
||||||
|
|
||||||
|
defineOptions({ name: 'AreaCascader' });
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
allowClear: false,
|
||||||
|
changeOnSelect: false,
|
||||||
|
modelValue: undefined,
|
||||||
|
placeholder: '请选择省市区',
|
||||||
|
showSearch: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value?: number];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
allowClear?: boolean;
|
||||||
|
changeOnSelect?: boolean;
|
||||||
|
modelValue?: number;
|
||||||
|
placeholder?: string;
|
||||||
|
showSearch?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AreaTreeNode = SystemAreaApi.Area & {
|
||||||
|
children?: AreaTreeNode[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const areaTree = ref<AreaTreeNode[]>([]); // 地区树
|
||||||
|
const loading = ref(false); // 加载状态
|
||||||
|
const selectedPath = ref<number[]>(); // 选中的地区路径
|
||||||
|
|
||||||
|
const fieldNames = {
|
||||||
|
children: 'children', // 子级字段
|
||||||
|
label: 'name', // 标签字段
|
||||||
|
value: 'id', // 值字段
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找地区编号对应的级联路径
|
||||||
|
*
|
||||||
|
* @param tree 地区树
|
||||||
|
* @param areaId 地区编号
|
||||||
|
* @returns 省市区编号路径
|
||||||
|
*/
|
||||||
|
function findAreaPath(
|
||||||
|
tree: AreaTreeNode[],
|
||||||
|
areaId?: number,
|
||||||
|
): number[] | undefined {
|
||||||
|
if (areaId === undefined || areaId === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const area of tree) {
|
||||||
|
if (area.id === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (area.id === areaId) {
|
||||||
|
return [area.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
const childPath = area.children?.length
|
||||||
|
? findAreaPath(area.children, areaId)
|
||||||
|
: undefined;
|
||||||
|
if (childPath) {
|
||||||
|
return [area.id, ...childPath];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同步级联选择器展示路径 */
|
||||||
|
function syncSelectedPath() {
|
||||||
|
selectedPath.value = findAreaPath(areaTree.value, props.modelValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选择地区后回写最后一级地区编号 */
|
||||||
|
const handleChange: NonNullable<CascaderProps['onChange']> = (value) => {
|
||||||
|
if (!value?.length) {
|
||||||
|
emit('update:modelValue', undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = Array.isArray(value[0]) ? value[0] : value;
|
||||||
|
const leafValue = path.at(-1);
|
||||||
|
const areaId =
|
||||||
|
typeof leafValue === 'number' ? leafValue : Number(leafValue);
|
||||||
|
emit('update:modelValue', Number.isNaN(areaId) ? undefined : areaId);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => props.modelValue, syncSelectedPath);
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
areaTree.value = (await getAreaTree()) as AreaTreeNode[];
|
||||||
|
syncSelectedPath();
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Cascader
|
||||||
|
v-model:value="selectedPath"
|
||||||
|
:allow-clear="allowClear"
|
||||||
|
:change-on-select="changeOnSelect"
|
||||||
|
:field-names="fieldNames"
|
||||||
|
:loading="loading"
|
||||||
|
:options="areaTree"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:show-search="showSearch"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as AreaCascader } from './area-cascader.vue';
|
||||||
|
|
@ -136,9 +136,13 @@ function autoSearch(queryValue: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 处理地址选择 */
|
/** 处理地址选择 */
|
||||||
function handleAddressSelect(value: string) {
|
function handleAddressSelect(value: unknown) {
|
||||||
if (value) {
|
const selectedValue =
|
||||||
regeoCode(value);
|
typeof value === 'object' && value !== null && 'value' in value
|
||||||
|
? (value as { value?: number | string }).value
|
||||||
|
: value;
|
||||||
|
if (selectedValue !== undefined && selectedValue !== null) {
|
||||||
|
regeoCode(String(selectedValue));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
AntdProfileOutlined,
|
AntdProfileOutlined,
|
||||||
BookOpenText,
|
BookOpenText,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
|
IconifyIcon,
|
||||||
SvgGithubIcon,
|
SvgGithubIcon,
|
||||||
} from '@vben/icons';
|
} from '@vben/icons';
|
||||||
import {
|
import {
|
||||||
|
|
@ -27,7 +28,7 @@ import { preferences, usePreferences } from '@vben/preferences';
|
||||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
import { formatDateTime, openWindow } from '@vben/utils';
|
import { formatDateTime, openWindow } from '@vben/utils';
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
import { message, Tooltip } from 'ant-design-vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getUnreadNotifyMessageCount,
|
getUnreadNotifyMessageCount,
|
||||||
|
|
@ -156,6 +157,12 @@ function handleNotificationOpen(open: boolean) {
|
||||||
handleNotificationGetUnreadCount();
|
handleNotificationGetUnreadCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 打开 IM 聊天 */
|
||||||
|
function handleOpenImHome() {
|
||||||
|
const { href } = router.resolve({ name: 'ImHome' });
|
||||||
|
window.open(href, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
// 租户列表
|
// 租户列表
|
||||||
const tenants = ref<SystemTenantApi.Tenant[]>([]);
|
const tenants = ref<SystemTenantApi.Tenant[]>([]);
|
||||||
const tenantEnable = computed(
|
const tenantEnable = computed(
|
||||||
|
|
@ -302,6 +309,17 @@ watch(
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<template #header-right-900>
|
||||||
|
<Tooltip title="IM 聊天">
|
||||||
|
<button
|
||||||
|
class="hover:bg-accent hover:text-accent-foreground mr-1 inline-flex size-8 items-center justify-center rounded-md transition-colors"
|
||||||
|
type="button"
|
||||||
|
@click="handleOpenImHome"
|
||||||
|
>
|
||||||
|
<IconifyIcon class="size-4" icon="lucide:message-circle" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<AuthenticationLoginExpiredModal
|
<AuthenticationLoginExpiredModal
|
||||||
v-model:open="accessStore.loginExpired"
|
v-model:open="accessStore.loginExpired"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: '/im/home',
|
||||||
|
name: 'ImHome',
|
||||||
|
component: () => import('#/views/im/home/index.vue'),
|
||||||
|
redirect: '/im/home/conversation',
|
||||||
|
meta: {
|
||||||
|
title: 'IM 即时通讯',
|
||||||
|
hideInMenu: true,
|
||||||
|
hideInTab: true,
|
||||||
|
keepAlive: true,
|
||||||
|
noBasicLayout: true,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'conversation',
|
||||||
|
name: 'ImHomeConversation',
|
||||||
|
component: () => import('#/views/im/home/pages/conversation/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '消息',
|
||||||
|
activePath: '/im/home/conversation',
|
||||||
|
hideInMenu: true,
|
||||||
|
hideInTab: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'contact',
|
||||||
|
name: 'ImHomeContact',
|
||||||
|
component: () => import('#/views/im/home/pages/contact/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '通讯录',
|
||||||
|
activePath: '/im/home/contact',
|
||||||
|
hideInMenu: true,
|
||||||
|
hideInTab: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
|
|
@ -22,8 +22,6 @@ const document = ref<null | {
|
||||||
}[];
|
}[];
|
||||||
title: string;
|
title: string;
|
||||||
}>(null); // 知识库文档列表
|
}>(null); // 知识库文档列表
|
||||||
const dialogVisible = ref(false); // 知识引用详情弹窗
|
|
||||||
const documentRef = ref<HTMLElement>(); // 知识引用详情弹窗 Ref
|
|
||||||
|
|
||||||
/** 按照 document 聚合 segments */
|
/** 按照 document 聚合 segments */
|
||||||
const documentList = computed(() => {
|
const documentList = computed(() => {
|
||||||
|
|
@ -49,7 +47,6 @@ const documentList = computed(() => {
|
||||||
/** 点击 document 处理 */
|
/** 点击 document 处理 */
|
||||||
function handleClick(doc: any) {
|
function handleClick(doc: any) {
|
||||||
document.value = doc;
|
document.value = doc;
|
||||||
dialogVisible.value = true;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -79,7 +76,7 @@ function handleClick(doc: any) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip placement="topLeft" trigger="click">
|
<Tooltip placement="topLeft" trigger="click">
|
||||||
<div ref="documentRef"></div>
|
<div></div>
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="mb-3 text-base font-bold">{{ document?.title }}</div>
|
<div class="mb-3 text-base font-bold">{{ document?.title }}</div>
|
||||||
<div class="max-h-[60vh] overflow-y-auto">
|
<div class="max-h-[60vh] overflow-y-auto">
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,6 @@ const props = defineProps({
|
||||||
});
|
});
|
||||||
const emits = defineEmits(['onBtnClick', 'onMjBtnClick']);
|
const emits = defineEmits(['onBtnClick', 'onMjBtnClick']);
|
||||||
|
|
||||||
const cardImageRef = ref<any>(); // 卡片 image ref
|
|
||||||
|
|
||||||
/** 处理点击事件 */
|
/** 处理点击事件 */
|
||||||
async function handleButtonClick(type: string, detail: AiImageApi.Image) {
|
async function handleButtonClick(type: string, detail: AiImageApi.Image) {
|
||||||
emits('onBtnClick', type, detail);
|
emits('onBtnClick', type, detail);
|
||||||
|
|
@ -110,7 +108,7 @@ onMounted(async () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 图片展示区域 -->
|
<!-- 图片展示区域 -->
|
||||||
<div class="mt-5 h-72 flex-1 overflow-hidden" ref="cardImageRef">
|
<div class="mt-5 h-72 flex-1 overflow-hidden">
|
||||||
<Image class="w-full rounded-lg" :src="detail?.picUrl" />
|
<Image class="w-full rounded-lg" :src="detail?.picUrl" />
|
||||||
<div v-if="detail?.status === AiImageStatusEnum.FAIL">
|
<div v-if="detail?.status === AiImageStatusEnum.FAIL">
|
||||||
{{ detail?.errorMessage }}
|
{{ detail?.errorMessage }}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ const queryParams = reactive({
|
||||||
}); // 图片分页相关的参数
|
}); // 图片分页相关的参数
|
||||||
const pageTotal = ref<number>(0); // page size
|
const pageTotal = ref<number>(0); // page size
|
||||||
const imageList = ref<AiImageApi.Image[]>([]); // image 列表
|
const imageList = ref<AiImageApi.Image[]>([]); // image 列表
|
||||||
const imageListRef = ref<any>(); // ref
|
|
||||||
|
|
||||||
const inProgressImageMap = ref<{}>({}); // 监听的 image 映射,一般是生成中(需要轮询),key 为 image 编号,value 为 image
|
const inProgressImageMap = ref<{}>({}); // 监听的 image 映射,一般是生成中(需要轮询),key 为 image 编号,value 为 image
|
||||||
const inProgressTimer = ref<any>(); // 生成中的 image 定时器,轮询生成进展
|
const inProgressTimer = ref<any>(); // 生成中的 image 定时器,轮询生成进展
|
||||||
|
|
@ -190,7 +189,6 @@ onUnmounted(async () => {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex flex-1 flex-wrap content-start overflow-y-auto p-3 pb-28 pt-5"
|
class="flex flex-1 flex-wrap content-start overflow-y-auto p-3 pb-28 pt-5"
|
||||||
ref="imageListRef"
|
|
||||||
>
|
>
|
||||||
<ImageCard
|
<ImageCard
|
||||||
v-for="image in imageList"
|
v-for="image in imageList"
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,8 @@ const currentFile = ref<any>(null); // 当前选中的文件
|
||||||
const submitLoading = ref(false); // 提交按钮加载状态
|
const submitLoading = ref(false); // 提交按钮加载状态
|
||||||
|
|
||||||
/** 选择文件 */
|
/** 选择文件 */
|
||||||
async function selectFile(index: number) {
|
async function selectFile(index: number | string) {
|
||||||
currentFile.value = modelData.value.list[index];
|
currentFile.value = modelData.value.list[Number(index)];
|
||||||
await splitContentFile(currentFile.value);
|
await splitContentFile(currentFile.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,7 +256,8 @@ onMounted(async () => {
|
||||||
class="mb-2.5"
|
class="mb-2.5"
|
||||||
>
|
>
|
||||||
<div class="mb-1 text-sm text-gray-500">
|
<div class="mb-1 text-sm text-gray-500">
|
||||||
分片-{{ index + 1 }} · {{ segment.contentLength || 0 }} 字符数 ·
|
分片-{{ Number(index) + 1 }} ·
|
||||||
|
{{ segment.contentLength || 0 }} 字符数 ·
|
||||||
{{ segment.tokens || 0 }} Token
|
{{ segment.tokens || 0 }} Token
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-md bg-card p-2">
|
<div class="rounded-md bg-card p-2">
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,6 @@ const props = defineProps({
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue']);
|
||||||
const formRef = ref(); // 表单引用
|
|
||||||
const uploadRef = ref(); // 上传组件引用
|
|
||||||
const parent = inject('parent', null); // 获取父组件实例
|
const parent = inject('parent', null); // 获取父组件实例
|
||||||
const { uploadUrl, httpRequest } = useUpload(); // 使用上传组件的钩子
|
const { uploadUrl, httpRequest } = useUpload(); // 使用上传组件的钩子
|
||||||
const fileList = ref<UploadProps['fileList']>([]); // 文件列表
|
const fileList = ref<UploadProps['fileList']>([]); // 文件列表
|
||||||
|
|
@ -148,10 +146,10 @@ async function customRequest(info: UploadRequestOption) {
|
||||||
*
|
*
|
||||||
* @param index 要移除的文件索引
|
* @param index 要移除的文件索引
|
||||||
*/
|
*/
|
||||||
function removeFile(index: number) {
|
function removeFile(index: number | string) {
|
||||||
// 从列表中移除文件
|
// 从列表中移除文件
|
||||||
const newList = [...props.modelValue.list];
|
const newList = [...props.modelValue.list];
|
||||||
newList.splice(index, 1);
|
newList.splice(Number(index), 1);
|
||||||
// 更新表单数据
|
// 更新表单数据
|
||||||
emit('update:modelValue', {
|
emit('update:modelValue', {
|
||||||
...props.modelValue,
|
...props.modelValue,
|
||||||
|
|
@ -186,14 +184,13 @@ onMounted(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Form ref="formRef" :model="modelData" label-width="0" class="mt-5">
|
<Form :model="modelData" label-width="0" class="mt-5">
|
||||||
<Form.Item class="mb-5">
|
<Form.Item class="mb-5">
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div
|
<div
|
||||||
class="w-full rounded-md border-2 border-dashed border-gray-200 p-5 text-center hover:border-blue-500"
|
class="w-full rounded-md border-2 border-dashed border-gray-200 p-5 text-center hover:border-blue-500"
|
||||||
>
|
>
|
||||||
<UploadDragger
|
<UploadDragger
|
||||||
ref="uploadRef"
|
|
||||||
class="upload-demo"
|
class="upload-demo"
|
||||||
:action="uploadUrl"
|
:action="uploadUrl"
|
||||||
v-model:file-list="fileList"
|
v-model:file-list="fileList"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
<!-- eslint-disable no-unused-vars -->
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, nextTick, onMounted, ref, toRaw, watch } from 'vue';
|
import { computed, inject, nextTick, onMounted, ref, toRaw, watch } from 'vue';
|
||||||
|
|
||||||
|
|
@ -24,26 +23,7 @@ const prefix = inject('prefix');
|
||||||
|
|
||||||
const formKey = ref<number | string | undefined>(undefined);
|
const formKey = ref<number | string | undefined>(undefined);
|
||||||
const businessKey = ref('');
|
const businessKey = ref('');
|
||||||
const optionModelTitle = ref('');
|
|
||||||
const fieldList = ref<any[]>([]);
|
const fieldList = ref<any[]>([]);
|
||||||
const formFieldForm = ref<any>({});
|
|
||||||
const fieldType = ref({
|
|
||||||
long: '长整型',
|
|
||||||
string: '字符串',
|
|
||||||
boolean: '布尔类',
|
|
||||||
date: '日期类',
|
|
||||||
enum: '枚举类',
|
|
||||||
custom: '自定义类型',
|
|
||||||
});
|
|
||||||
const formFieldIndex = ref(-1); // 编辑中的字段, -1 为新增
|
|
||||||
const formFieldOptionIndex = ref(-1); // 编辑中的字段配置项, -1 为新增
|
|
||||||
const fieldModelVisible = ref(false);
|
|
||||||
const fieldOptionModelVisible = ref(false);
|
|
||||||
const fieldOptionForm = ref<any>({}); // 当前激活的字段配置项数据
|
|
||||||
const fieldOptionType = ref(''); // 当前激活的字段配置项弹窗 类型
|
|
||||||
const fieldEnumList = ref<any[]>([]); // 枚举值列表
|
|
||||||
const fieldConstraintsList = ref<any[]>([]); // 约束条件列表
|
|
||||||
const fieldPropertiesList = ref<any[]>([]); // 绑定属性列表
|
|
||||||
const bpmnELement = ref();
|
const bpmnELement = ref();
|
||||||
const elExtensionElements = ref();
|
const elExtensionElements = ref();
|
||||||
const formData = ref();
|
const formData = ref();
|
||||||
|
|
@ -94,173 +74,6 @@ const _updateElementBusinessKey = () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
// 根据类型调整字段type
|
|
||||||
const _changeFieldTypeType = (type: any) => {
|
|
||||||
formFieldForm.value.type = type === 'custom' ? '' : type;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 打开字段详情侧边栏
|
|
||||||
const _openFieldForm = (field: any, index: any) => {
|
|
||||||
formFieldIndex.value = index;
|
|
||||||
if (index === -1) {
|
|
||||||
formFieldForm.value = {};
|
|
||||||
// 初始化枚举值列表
|
|
||||||
fieldEnumList.value = [];
|
|
||||||
// 初始化约束条件列表
|
|
||||||
fieldConstraintsList.value = [];
|
|
||||||
// 初始化自定义属性列表
|
|
||||||
fieldPropertiesList.value = [];
|
|
||||||
} else {
|
|
||||||
const FieldObject = formData.value.fields[index];
|
|
||||||
formFieldForm.value = cloneDeep(field);
|
|
||||||
// 设置自定义类型
|
|
||||||
// this.$set(this.formFieldForm, "typeType", !this.fieldType[field.type] ? "custom" : field.type);
|
|
||||||
formFieldForm.value.typeType = fieldType.value[
|
|
||||||
field.type as keyof typeof fieldType.value
|
|
||||||
]
|
|
||||||
? field.type
|
|
||||||
: 'custom';
|
|
||||||
// 初始化枚举值列表
|
|
||||||
field.type === 'enum' &&
|
|
||||||
(fieldEnumList.value = cloneDeep(FieldObject?.values || []));
|
|
||||||
// 初始化约束条件列表
|
|
||||||
fieldConstraintsList.value = cloneDeep(
|
|
||||||
FieldObject?.validation?.constraints || [],
|
|
||||||
);
|
|
||||||
// 初始化自定义属性列表
|
|
||||||
fieldPropertiesList.value = cloneDeep(
|
|
||||||
FieldObject?.properties?.values || [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
fieldModelVisible.value = true;
|
|
||||||
};
|
|
||||||
// 打开字段 某个 配置项 弹窗
|
|
||||||
const _openFieldOptionForm = (option: any, index: any, type: any) => {
|
|
||||||
fieldOptionModelVisible.value = true;
|
|
||||||
fieldOptionType.value = type;
|
|
||||||
formFieldOptionIndex.value = index;
|
|
||||||
if (type === 'property') {
|
|
||||||
fieldOptionForm.value = option ? cloneDeep(option) : {};
|
|
||||||
return (optionModelTitle.value = '属性配置');
|
|
||||||
}
|
|
||||||
if (type === 'enum') {
|
|
||||||
fieldOptionForm.value = option ? cloneDeep(option) : {};
|
|
||||||
return (optionModelTitle.value = '枚举值配置');
|
|
||||||
}
|
|
||||||
fieldOptionForm.value = option ? cloneDeep(option) : {};
|
|
||||||
return (optionModelTitle.value = '约束条件配置');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 保存字段 某个 配置项
|
|
||||||
const _saveFieldOption = () => {
|
|
||||||
if (formFieldOptionIndex.value === -1) {
|
|
||||||
if (fieldOptionType.value === 'property') {
|
|
||||||
fieldPropertiesList.value.push(fieldOptionForm.value);
|
|
||||||
}
|
|
||||||
if (fieldOptionType.value === 'constraint') {
|
|
||||||
fieldConstraintsList.value.push(fieldOptionForm.value);
|
|
||||||
}
|
|
||||||
if (fieldOptionType.value === 'enum') {
|
|
||||||
fieldEnumList.value.push(fieldOptionForm.value);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fieldOptionType.value === 'property' &&
|
|
||||||
fieldPropertiesList.value.splice(
|
|
||||||
formFieldOptionIndex.value,
|
|
||||||
1,
|
|
||||||
fieldOptionForm.value,
|
|
||||||
);
|
|
||||||
fieldOptionType.value === 'constraint' &&
|
|
||||||
fieldConstraintsList.value.splice(
|
|
||||||
formFieldOptionIndex.value,
|
|
||||||
1,
|
|
||||||
fieldOptionForm.value,
|
|
||||||
);
|
|
||||||
fieldOptionType.value === 'enum' &&
|
|
||||||
fieldEnumList.value.splice(
|
|
||||||
formFieldOptionIndex.value,
|
|
||||||
1,
|
|
||||||
fieldOptionForm.value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
fieldOptionModelVisible.value = false;
|
|
||||||
fieldOptionForm.value = {};
|
|
||||||
};
|
|
||||||
// 保存字段配置
|
|
||||||
const _saveField = () => {
|
|
||||||
const { id, type, label, defaultValue, datePattern } = formFieldForm.value;
|
|
||||||
const Field = bpmnInstances().moddle.create(`${prefix}:FormField`, {
|
|
||||||
id,
|
|
||||||
type,
|
|
||||||
label,
|
|
||||||
});
|
|
||||||
defaultValue && (Field.defaultValue = defaultValue);
|
|
||||||
datePattern && (Field.datePattern = datePattern);
|
|
||||||
// 构建属性
|
|
||||||
if (fieldPropertiesList.value && fieldPropertiesList.value.length > 0) {
|
|
||||||
const fieldPropertyList = fieldPropertiesList.value.map((fp: any) => {
|
|
||||||
return bpmnInstances().moddle.create(`${prefix}:Property`, {
|
|
||||||
id: fp.id,
|
|
||||||
value: fp.value,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Field.properties = bpmnInstances().moddle.create(`${prefix}:Properties`, {
|
|
||||||
values: fieldPropertyList,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 构建校验规则
|
|
||||||
if (fieldConstraintsList.value && fieldConstraintsList.value.length > 0) {
|
|
||||||
const fieldConstraintList = fieldConstraintsList.value.map((fc: any) => {
|
|
||||||
return bpmnInstances().moddle.create(`${prefix}:Constraint`, {
|
|
||||||
name: fc.name,
|
|
||||||
config: fc.config,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Field.validation = bpmnInstances().moddle.create(`${prefix}:Validation`, {
|
|
||||||
constraints: fieldConstraintList,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 构建枚举值
|
|
||||||
if (fieldEnumList.value && fieldEnumList.value.length > 0) {
|
|
||||||
Field.values = fieldEnumList.value.map((fe: any) => {
|
|
||||||
return bpmnInstances().moddle.create(`${prefix}:Value`, {
|
|
||||||
name: fe.name,
|
|
||||||
id: fe.id,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 更新数组 与 表单配置实例
|
|
||||||
if (formFieldIndex.value === -1) {
|
|
||||||
fieldList.value.push(formFieldForm.value);
|
|
||||||
formData.value.fields.push(Field);
|
|
||||||
} else {
|
|
||||||
fieldList.value.splice(formFieldIndex.value, 1, formFieldForm.value);
|
|
||||||
formData.value.fields.splice(formFieldIndex.value, 1, Field);
|
|
||||||
}
|
|
||||||
updateElementExtensions();
|
|
||||||
fieldModelVisible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 移除某个 字段的 配置项
|
|
||||||
const _removeFieldOptionItem = (_option: any, index: any, type: any) => {
|
|
||||||
// console.log(option, 'option')
|
|
||||||
if (type === 'property') {
|
|
||||||
fieldPropertiesList.value.splice(index, 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (type === 'enum') {
|
|
||||||
fieldEnumList.value.splice(index, 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fieldConstraintsList.value.splice(index, 1);
|
|
||||||
};
|
|
||||||
// 移除 字段
|
|
||||||
const _removeField = (field: any, index: any) => {
|
|
||||||
console.warn(field, 'field');
|
|
||||||
fieldList.value.splice(index, 1);
|
|
||||||
formData.value.fields.splice(index, 1);
|
|
||||||
updateElementExtensions();
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateElementExtensions = () => {
|
const updateElementExtensions = () => {
|
||||||
// 更新回扩展元素
|
// 更新回扩展元素
|
||||||
|
|
@ -329,211 +142,5 @@ watch(
|
||||||
</Select>
|
</Select>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<!--字段列表-->
|
|
||||||
<!-- <div class="element-property list-property">-->
|
|
||||||
<!-- <Divider><Icon icon="ep:coin" /> 表单字段</Divider>-->
|
|
||||||
<!-- <Table :data-source="fieldList" :scroll="{ y: 240 }" bordered>-->
|
|
||||||
<!-- <TableColumn title="序号" type="index" width="50px" />-->
|
|
||||||
<!-- <TableColumn title="字段名称" dataIndex="label" width="80px" :ellipsis="true" />-->
|
|
||||||
<!-- <TableColumn-->
|
|
||||||
<!-- title="字段类型"-->
|
|
||||||
<!-- dataIndex="type"-->
|
|
||||||
<!-- width="80px"-->
|
|
||||||
<!-- :customRender="({ text }) => fieldType[text] || text"-->
|
|
||||||
<!-- :ellipsis="true"-->
|
|
||||||
<!-- />-->
|
|
||||||
<!-- <TableColumn-->
|
|
||||||
<!-- title="默认值"-->
|
|
||||||
<!-- dataIndex="defaultValue"-->
|
|
||||||
<!-- width="80px"-->
|
|
||||||
<!-- :ellipsis="true"-->
|
|
||||||
<!-- />-->
|
|
||||||
<!-- <TableColumn title="操作" width="90px">-->
|
|
||||||
<!-- <template #default="scope">-->
|
|
||||||
<!-- <Button type="link" @click="openFieldForm(scope, scope.$index)">-->
|
|
||||||
<!-- 编辑-->
|
|
||||||
<!-- </Button>-->
|
|
||||||
<!-- <Divider type="vertical" />-->
|
|
||||||
<!-- <Button-->
|
|
||||||
<!-- type="link"-->
|
|
||||||
<!-- danger-->
|
|
||||||
<!-- @click="removeField(scope, scope.$index)"-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- 移除-->
|
|
||||||
<!-- </Button>-->
|
|
||||||
<!-- </template>-->
|
|
||||||
<!-- </TableColumn>-->
|
|
||||||
<!-- </Table>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- <div class="element-drawer__button">-->
|
|
||||||
<!-- <Button type="primary" @click="openFieldForm(null, -1)">添加字段</Button>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
|
|
||||||
<!--字段配置侧边栏-->
|
|
||||||
<!-- <Drawer-->
|
|
||||||
<!-- v-model:open="fieldModelVisible"-->
|
|
||||||
<!-- title="字段配置"-->
|
|
||||||
<!-- :width="`${width}px`"-->
|
|
||||||
<!-- destroyOnClose-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- <Form :model="formFieldForm" :label-col="{ style: { width: '90px' } }">-->
|
|
||||||
<!-- <FormItem label="字段ID">-->
|
|
||||||
<!-- <Input v-model:value="formFieldForm.id" allowClear />-->
|
|
||||||
<!-- </FormItem>-->
|
|
||||||
<!-- <FormItem label="类型">-->
|
|
||||||
<!-- <Select-->
|
|
||||||
<!-- v-model:value="formFieldForm.typeType"-->
|
|
||||||
<!-- placeholder="请选择字段类型"-->
|
|
||||||
<!-- allowClear-->
|
|
||||||
<!-- @change="changeFieldTypeType"-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- <SelectOption v-for="(value, key) of fieldType" :key="key" :value="key">{{ value }}</SelectOption>-->
|
|
||||||
<!-- </Select>-->
|
|
||||||
<!-- </FormItem>-->
|
|
||||||
<!-- <FormItem label="类型名称" v-if="formFieldForm.typeType === 'custom'">-->
|
|
||||||
<!-- <Input v-model:value="formFieldForm.type" allowClear />-->
|
|
||||||
<!-- </FormItem>-->
|
|
||||||
<!-- <FormItem label="名称">-->
|
|
||||||
<!-- <Input v-model:value="formFieldForm.label" allowClear />-->
|
|
||||||
<!-- </FormItem>-->
|
|
||||||
<!-- <FormItem label="时间格式" v-if="formFieldForm.typeType === 'date'">-->
|
|
||||||
<!-- <Input v-model:value="formFieldForm.datePattern" allowClear />-->
|
|
||||||
<!-- </FormItem>-->
|
|
||||||
<!-- <FormItem label="默认值">-->
|
|
||||||
<!-- <Input v-model:value="formFieldForm.defaultValue" allowClear />-->
|
|
||||||
<!-- </FormItem>-->
|
|
||||||
<!-- </Form>-->
|
|
||||||
|
|
||||||
<!-- <!– 枚举值设置 –>-->
|
|
||||||
<!-- <template v-if="formFieldForm.type === 'enum'">-->
|
|
||||||
<!-- <Divider key="enum-divider" />-->
|
|
||||||
<!-- <p class="listener-filed__title" key="enum-title">-->
|
|
||||||
<!-- <span><Icon icon="ep:menu" />枚举值列表:</span>-->
|
|
||||||
<!-- <Button type="primary" @click="openFieldOptionForm(null, -1, 'enum')"-->
|
|
||||||
<!-- >添加枚举值</Button-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- </p>-->
|
|
||||||
<!-- <Table :data-source="fieldEnumList" key="enum-table" :scroll="{ y: 240 }" bordered>-->
|
|
||||||
<!-- <TableColumn title="序号" width="50px" type="index" />-->
|
|
||||||
<!-- <TableColumn title="枚举值编号" dataIndex="id" width="100px" :ellipsis="true" />-->
|
|
||||||
<!-- <TableColumn title="枚举值名称" dataIndex="name" width="100px" :ellipsis="true" />-->
|
|
||||||
<!-- <TableColumn title="操作" width="90px">-->
|
|
||||||
<!-- <template #default="scope">-->
|
|
||||||
<!-- <Button-->
|
|
||||||
<!-- type="link"-->
|
|
||||||
<!-- @click="openFieldOptionForm(scope, scope.$index, 'enum')"-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- 编辑-->
|
|
||||||
<!-- </Button>-->
|
|
||||||
<!-- <Divider type="vertical" />-->
|
|
||||||
<!-- <Button-->
|
|
||||||
<!-- type="link"-->
|
|
||||||
<!-- danger-->
|
|
||||||
<!-- @click="removeFieldOptionItem(scope, scope.$index, 'enum')"-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- 移除-->
|
|
||||||
<!-- </Button>-->
|
|
||||||
<!-- </template>-->
|
|
||||||
<!-- </TableColumn>-->
|
|
||||||
<!-- </Table>-->
|
|
||||||
<!-- </template>-->
|
|
||||||
|
|
||||||
<!-- <!– 校验规则 –>-->
|
|
||||||
<!-- <Divider key="validation-divider" />-->
|
|
||||||
<!-- <p class="listener-filed__title" key="validation-title">-->
|
|
||||||
<!-- <span><Icon icon="ep:menu" />约束条件列表:</span>-->
|
|
||||||
<!-- <Button type="primary" @click="openFieldOptionForm(null, -1, 'constraint')"-->
|
|
||||||
<!-- >添加约束</Button-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- </p>-->
|
|
||||||
<!-- <Table :data-source="fieldConstraintsList" key="validation-table" :scroll="{ y: 240 }" bordered>-->
|
|
||||||
<!-- <TableColumn title="序号" width="50px" type="index" />-->
|
|
||||||
<!-- <TableColumn title="约束名称" dataIndex="name" width="100px" :ellipsis="true" />-->
|
|
||||||
<!-- <TableColumn title="约束配置" dataIndex="config" width="100px" :ellipsis="true" />-->
|
|
||||||
<!-- <TableColumn title="操作" width="90px">-->
|
|
||||||
<!-- <template #default="scope">-->
|
|
||||||
<!-- <Button-->
|
|
||||||
<!-- type="link"-->
|
|
||||||
<!-- @click="openFieldOptionForm(scope, scope.$index, 'constraint')"-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- 编辑-->
|
|
||||||
<!-- </Button>-->
|
|
||||||
<!-- <Divider type="vertical" />-->
|
|
||||||
<!-- <Button-->
|
|
||||||
<!-- type="link"-->
|
|
||||||
<!-- danger-->
|
|
||||||
<!-- @click="removeFieldOptionItem(scope, scope.$index, 'constraint')"-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- 移除-->
|
|
||||||
<!-- </Button>-->
|
|
||||||
<!-- </template>-->
|
|
||||||
<!-- </TableColumn>-->
|
|
||||||
<!-- </Table>-->
|
|
||||||
|
|
||||||
<!-- <!– 表单属性 –>-->
|
|
||||||
<!-- <Divider key="property-divider" />-->
|
|
||||||
<!-- <p class="listener-filed__title" key="property-title">-->
|
|
||||||
<!-- <span><Icon icon="ep:menu" />字段属性列表:</span>-->
|
|
||||||
<!-- <Button type="primary" @click="openFieldOptionForm(null, -1, 'property')"-->
|
|
||||||
<!-- >添加属性</Button-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- </p>-->
|
|
||||||
<!-- <Table :data-source="fieldPropertiesList" key="property-table" :scroll="{ y: 240 }" bordered>-->
|
|
||||||
<!-- <TableColumn title="序号" width="50px" type="index" />-->
|
|
||||||
<!-- <TableColumn title="属性编号" dataIndex="id" width="100px" :ellipsis="true" />-->
|
|
||||||
<!-- <TableColumn title="属性值" dataIndex="value" width="100px" :ellipsis="true" />-->
|
|
||||||
<!-- <TableColumn title="操作" width="90px">-->
|
|
||||||
<!-- <template #default="scope">-->
|
|
||||||
<!-- <Button-->
|
|
||||||
<!-- type="link"-->
|
|
||||||
<!-- @click="openFieldOptionForm(scope, scope.$index, 'property')"-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- 编辑-->
|
|
||||||
<!-- </Button>-->
|
|
||||||
<!-- <Divider type="vertical" />-->
|
|
||||||
<!-- <Button-->
|
|
||||||
<!-- type="link"-->
|
|
||||||
<!-- danger-->
|
|
||||||
<!-- @click="removeFieldOptionItem(scope, scope.$index, 'property')"-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- 移除-->
|
|
||||||
<!-- </Button>-->
|
|
||||||
<!-- </template>-->
|
|
||||||
<!-- </TableColumn>-->
|
|
||||||
<!-- </Table>-->
|
|
||||||
|
|
||||||
<!-- <!– 底部按钮 –>-->
|
|
||||||
<!-- <div class="element-drawer__button">-->
|
|
||||||
<!-- <Button>取 消</Button>-->
|
|
||||||
<!-- <Button type="primary" @click="saveField">保 存</Button>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </Drawer>-->
|
|
||||||
|
|
||||||
<!-- <Modal-->
|
|
||||||
<!-- v-model:open="fieldOptionModelVisible"-->
|
|
||||||
<!-- :title="optionModelTitle"-->
|
|
||||||
<!-- width="600px"-->
|
|
||||||
<!-- destroyOnClose-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- <Form :model="fieldOptionForm" :label-col="{ style: { width: '96px' } }">-->
|
|
||||||
<!-- <FormItem label="编号/ID" v-if="fieldOptionType !== 'constraint'" key="option-id">-->
|
|
||||||
<!-- <Input v-model:value="fieldOptionForm.id" allowClear />-->
|
|
||||||
<!-- </FormItem>-->
|
|
||||||
<!-- <FormItem label="名称" v-if="fieldOptionType !== 'property'" key="option-name">-->
|
|
||||||
<!-- <Input v-model:value="fieldOptionForm.name" allowClear />-->
|
|
||||||
<!-- </FormItem>-->
|
|
||||||
<!-- <FormItem label="配置" v-if="fieldOptionType === 'constraint'" key="option-config">-->
|
|
||||||
<!-- <Input v-model:value="fieldOptionForm.config" allowClear />-->
|
|
||||||
<!-- </FormItem>-->
|
|
||||||
<!-- <FormItem label="值" v-if="fieldOptionType === 'property'" key="option-value">-->
|
|
||||||
<!-- <Input v-model:value="fieldOptionForm.value" allowClear />-->
|
|
||||||
<!-- </FormItem>-->
|
|
||||||
<!-- </Form>-->
|
|
||||||
<!-- <template #footer>-->
|
|
||||||
<!-- <Button @click="fieldOptionModelVisible = false">取 消</Button>-->
|
|
||||||
<!-- <Button type="primary" @click="saveFieldOption">确 定</Button>-->
|
|
||||||
<!-- </template>-->
|
|
||||||
<!-- </Modal>-->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ const props = defineProps({
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const prefix = inject('prefix');
|
const prefix = inject<string>('prefix', 'flowable'); // 增加默认值flowable
|
||||||
const elementListenersList = ref<any[]>([]); // 监听器列表
|
const elementListenersList = ref<any[]>([]); // 监听器列表
|
||||||
const listenerForm = ref<any>({}); // 监听器详情表单
|
const listenerForm = ref<any>({}); // 监听器详情表单
|
||||||
const fieldsListOfListener = ref<any[]>([]);
|
const fieldsListOfListener = ref<any[]>([]);
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ interface Props {
|
||||||
type?: string;
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prefix = inject<string>('prefix');
|
const prefix = inject<string>('prefix', 'flowable');
|
||||||
|
|
||||||
const elementListenersList = ref<any[]>([]);
|
const elementListenersList = ref<any[]>([]);
|
||||||
const listenerEventTypeObject = ref(eventType);
|
const listenerEventTypeObject = ref(eventType);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
import { inject, nextTick, ref, watch } from 'vue';
|
import { inject, nextTick, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { confirm, useVbenModal } from '@vben/common-ui';
|
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
@ -151,7 +153,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
pagerConfig: {
|
pagerConfig: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
},
|
} as VxeTableGridOptions<{ name: string; value: string }>,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [FieldModal, fieldModalApi] = useVbenModal({
|
const [FieldModal, fieldModalApi] = useVbenModal({
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,35 @@
|
||||||
const bpmnInstances = () => (window as any)?.bpmnInstances;
|
const bpmnInstances = () => (window as any)?.bpmnInstances;
|
||||||
|
|
||||||
|
interface ListenerFieldOptions {
|
||||||
|
expression?: string;
|
||||||
|
fieldType: string;
|
||||||
|
name: string;
|
||||||
|
string?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListenerOptions {
|
||||||
|
class?: string;
|
||||||
|
delegateExpression?: string;
|
||||||
|
event?: string;
|
||||||
|
eventDefinitionType?: string;
|
||||||
|
eventTimeDefinitions?: string;
|
||||||
|
expression?: string;
|
||||||
|
fields?: ListenerFieldOptions[];
|
||||||
|
id?: string;
|
||||||
|
listenerType?: string;
|
||||||
|
resource?: string;
|
||||||
|
scriptFormat?: string;
|
||||||
|
scriptType?: string;
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// 创建监听器实例
|
// 创建监听器实例
|
||||||
export function createListenerObject(options, isTask, prefix) {
|
export function createListenerObject(
|
||||||
const listenerObj = Object.create(null);
|
options: ListenerOptions,
|
||||||
|
isTask: boolean,
|
||||||
|
prefix: string,
|
||||||
|
) {
|
||||||
|
const listenerObj: Record<string, any> = Object.create(null);
|
||||||
listenerObj.event = options.event;
|
listenerObj.event = options.event;
|
||||||
isTask && (listenerObj.id = options.id); // 任务监听器特有的 id 字段
|
isTask && (listenerObj.id = options.id); // 任务监听器特有的 id 字段
|
||||||
switch (options.listenerType) {
|
switch (options.listenerType) {
|
||||||
|
|
@ -52,7 +80,10 @@ export function createListenerObject(options, isTask, prefix) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建 监听器的注入字段 实例
|
// 创建 监听器的注入字段 实例
|
||||||
export function createFieldObject(option, prefix) {
|
export function createFieldObject(
|
||||||
|
option: ListenerFieldOptions,
|
||||||
|
prefix: string,
|
||||||
|
) {
|
||||||
const { name, fieldType, string, expression } = option;
|
const { name, fieldType, string, expression } = option;
|
||||||
const fieldConfig =
|
const fieldConfig =
|
||||||
fieldType === 'string' ? { name, string } : { name, expression };
|
fieldType === 'string' ? { name, string } : { name, expression };
|
||||||
|
|
@ -60,7 +91,7 @@ export function createFieldObject(option, prefix) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建脚本实例
|
// 创建脚本实例
|
||||||
export function createScriptObject(options, prefix) {
|
export function createScriptObject(options: ListenerOptions, prefix: string) {
|
||||||
const { scriptType, scriptFormat, value, resource } = options;
|
const { scriptType, scriptFormat, value, resource } = options;
|
||||||
const scriptConfig =
|
const scriptConfig =
|
||||||
scriptType === 'inlineScript'
|
scriptType === 'inlineScript'
|
||||||
|
|
@ -70,7 +101,7 @@ export function createScriptObject(options, prefix) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新元素扩展属性
|
// 更新元素扩展属性
|
||||||
export function updateElementExtensions(element, extensionList) {
|
export function updateElementExtensions(element: any, extensionList: any[]) {
|
||||||
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
|
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
|
||||||
values: extensionList,
|
values: extensionList,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,9 @@ const currentNode = useWatchNode(props);
|
||||||
/** 节点名称配置 */
|
/** 节点名称配置 */
|
||||||
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
|
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
|
||||||
useNodeName(BpmNodeTypeEnum.CHILD_PROCESS_NODE);
|
useNodeName(BpmNodeTypeEnum.CHILD_PROCESS_NODE);
|
||||||
|
function setInputRef(el: unknown) {
|
||||||
|
inputRef.value = el as HTMLInputElement | null;
|
||||||
|
}
|
||||||
// 激活的 Tab 标签页
|
// 激活的 Tab 标签页
|
||||||
const activeTabName = ref('child');
|
const activeTabName = ref('child');
|
||||||
// 子流程表单配置
|
// 子流程表单配置
|
||||||
|
|
@ -391,7 +394,7 @@ onMounted(async () => {
|
||||||
<div class="config-header">
|
<div class="config-header">
|
||||||
<Input
|
<Input
|
||||||
v-if="showInput"
|
v-if="showInput"
|
||||||
ref="inputRef"
|
:ref="setInputRef"
|
||||||
type="text"
|
type="text"
|
||||||
class="focus:border-blue-500 focus:shadow-[0_0_0_2px_rgba(24,144,255,0.2)] focus:outline-none"
|
class="focus:border-blue-500 focus:shadow-[0_0_0_2px_rgba(24,144,255,0.2)] focus:outline-none"
|
||||||
@blur="changeNodeName()"
|
@blur="changeNodeName()"
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,9 @@ const currentNode = useWatchNode(props);
|
||||||
// 节点名称
|
// 节点名称
|
||||||
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
|
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
|
||||||
useNodeName(BpmNodeTypeEnum.COPY_TASK_NODE);
|
useNodeName(BpmNodeTypeEnum.COPY_TASK_NODE);
|
||||||
|
function setInputRef(el: unknown) {
|
||||||
|
inputRef.value = el as HTMLInputElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
// 激活的 Tab 标签页
|
// 激活的 Tab 标签页
|
||||||
const activeTabName = ref('user');
|
const activeTabName = ref('user');
|
||||||
|
|
@ -211,7 +214,7 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
|
||||||
<div class="config-header">
|
<div class="config-header">
|
||||||
<Input
|
<Input
|
||||||
v-if="showInput"
|
v-if="showInput"
|
||||||
ref="inputRef"
|
:ref="setInputRef"
|
||||||
type="text"
|
type="text"
|
||||||
class="focus:border-blue-500 focus:shadow-[0_0_0_2px_rgba(24,144,255,0.2)] focus:outline-none"
|
class="focus:border-blue-500 focus:shadow-[0_0_0_2px_rgba(24,144,255,0.2)] focus:outline-none"
|
||||||
@blur="changeNodeName()"
|
@blur="changeNodeName()"
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,9 @@ const currentNode = useWatchNode(props);
|
||||||
// 节点名称
|
// 节点名称
|
||||||
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
|
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
|
||||||
useNodeName(BpmNodeTypeEnum.DELAY_TIMER_NODE);
|
useNodeName(BpmNodeTypeEnum.DELAY_TIMER_NODE);
|
||||||
|
function setInputRef(el: unknown) {
|
||||||
|
inputRef.value = el as HTMLInputElement | null;
|
||||||
|
}
|
||||||
// 抄送人表单配置
|
// 抄送人表单配置
|
||||||
const formRef = ref(); // 表单 Ref
|
const formRef = ref(); // 表单 Ref
|
||||||
|
|
||||||
|
|
@ -156,7 +159,7 @@ defineExpose({ openDrawer }); // 暴露方法给父组件
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<Input
|
<Input
|
||||||
v-if="showInput"
|
v-if="showInput"
|
||||||
ref="inputRef"
|
:ref="setInputRef"
|
||||||
type="text"
|
type="text"
|
||||||
class="mr-2 w-48"
|
class="mr-2 w-48"
|
||||||
@blur="changeNodeName()"
|
@blur="changeNodeName()"
|
||||||
|
|
|
||||||
|
|
@ -97,21 +97,21 @@ function changeConditionType() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteConditionGroup(conditions: any, index: number) {
|
function deleteConditionGroup(conditions: any, index: number | string) {
|
||||||
conditions.splice(index, 1);
|
conditions.splice(Number(index), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteConditionRule(condition: any, index: number) {
|
function deleteConditionRule(condition: any, index: number | string) {
|
||||||
condition.rules.splice(index, 1);
|
condition.rules.splice(Number(index), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addConditionRule(condition: any, index: number) {
|
function addConditionRule(condition: any, index: number | string) {
|
||||||
const rule = {
|
const rule = {
|
||||||
opCode: '==',
|
opCode: '==',
|
||||||
leftSide: undefined,
|
leftSide: undefined,
|
||||||
rightSide: '',
|
rightSide: '',
|
||||||
};
|
};
|
||||||
condition.rules.splice(index + 1, 0, rule);
|
condition.rules.splice(Number(index) + 1, 0, rule);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addConditionGroup(conditions: any) {
|
function addConditionGroup(conditions: any) {
|
||||||
|
|
|
||||||
|
|
@ -59,9 +59,9 @@ function addHttpResponseSetting(responseSetting: Record<string, string>[]) {
|
||||||
/** 删除 HTTP 请求返回值设置项 */
|
/** 删除 HTTP 请求返回值设置项 */
|
||||||
function deleteHttpResponseSetting(
|
function deleteHttpResponseSetting(
|
||||||
responseSetting: Record<string, string>[],
|
responseSetting: Record<string, string>[],
|
||||||
index: number,
|
index: number | string,
|
||||||
) {
|
) {
|
||||||
responseSetting.splice(index, 1);
|
responseSetting.splice(Number(index), 1);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,9 @@ const currentNode = useWatchNode(props);
|
||||||
/** 节点名称 */
|
/** 节点名称 */
|
||||||
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
|
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
|
||||||
useNodeName(BpmNodeTypeEnum.ROUTER_BRANCH_NODE);
|
useNodeName(BpmNodeTypeEnum.ROUTER_BRANCH_NODE);
|
||||||
|
function setInputRef(el: unknown) {
|
||||||
|
inputRef.value = el as HTMLInputElement | null;
|
||||||
|
}
|
||||||
const routerGroups = ref<RouterSetting[]>([]);
|
const routerGroups = ref<RouterSetting[]>([]);
|
||||||
const nodeOptions = ref<any[]>([]);
|
const nodeOptions = ref<any[]>([]);
|
||||||
const conditionRef = ref<any[]>([]);
|
const conditionRef = ref<any[]>([]);
|
||||||
|
|
@ -203,7 +206,7 @@ defineExpose({ openDrawer }); // 暴露方法给父组件
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<Input
|
<Input
|
||||||
ref="inputRef"
|
:ref="setInputRef"
|
||||||
v-if="showInput"
|
v-if="showInput"
|
||||||
type="text"
|
type="text"
|
||||||
class="mr-2 w-48"
|
class="mr-2 w-48"
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,9 @@ const currentNode = useWatchNode(props);
|
||||||
// 节点名称
|
// 节点名称
|
||||||
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
|
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
|
||||||
useNodeName(BpmNodeTypeEnum.START_USER_NODE);
|
useNodeName(BpmNodeTypeEnum.START_USER_NODE);
|
||||||
|
function setInputRef(el: unknown) {
|
||||||
|
inputRef.value = el as HTMLInputElement | null;
|
||||||
|
}
|
||||||
// 激活的 Tab 标签页
|
// 激活的 Tab 标签页
|
||||||
const activeTabName = ref('user');
|
const activeTabName = ref('user');
|
||||||
|
|
||||||
|
|
@ -144,7 +147,7 @@ defineExpose({ showStartUserNodeConfig });
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="config-header">
|
<div class="config-header">
|
||||||
<Input
|
<Input
|
||||||
ref="inputRef"
|
:ref="setInputRef"
|
||||||
v-if="showInput"
|
v-if="showInput"
|
||||||
type="text"
|
type="text"
|
||||||
class="focus:border-blue-500 focus:shadow-[0_0_0_2px_rgba(24,144,255,0.2)] focus:outline-none"
|
class="focus:border-blue-500 focus:shadow-[0_0_0_2px_rgba(24,144,255,0.2)] focus:outline-none"
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
|
||||||
currentNode,
|
currentNode,
|
||||||
BpmNodeTypeEnum.TRIGGER_NODE,
|
BpmNodeTypeEnum.TRIGGER_NODE,
|
||||||
);
|
);
|
||||||
|
function setInputRef(el: unknown) {
|
||||||
|
inputRef.value = el as HTMLInputElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
const nodeSetting = ref();
|
const nodeSetting = ref();
|
||||||
// 打开节点配置
|
// 打开节点配置
|
||||||
|
|
@ -68,7 +71,7 @@ function deleteNode() {
|
||||||
<span class="iconfont icon-trigger"></span>
|
<span class="iconfont icon-trigger"></span>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
ref="inputRef"
|
:ref="setInputRef"
|
||||||
v-if="!readonly && showInput"
|
v-if="!readonly && showInput"
|
||||||
type="text"
|
type="text"
|
||||||
class="editable-title-input"
|
class="editable-title-input"
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ const [Grid] = useVbenVxeGrid({
|
||||||
toolbarConfig: {
|
toolbarConfig: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
},
|
} as VxeTableGridOptions<BpmProcessExpressionApi.ProcessExpression>,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 配置 Modal
|
// 配置 Modal
|
||||||
|
|
@ -88,7 +88,7 @@ function useGridFormSchema(): VbenFormSchema[] {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
function useGridColumns(): VxeTableGridOptions<BpmProcessExpressionApi.ProcessExpression>['columns'] {
|
||||||
return [
|
return [
|
||||||
{ field: 'name', title: '名字', minWidth: 160 },
|
{ field: 'name', title: '名字', minWidth: 160 },
|
||||||
{ field: 'expression', title: '表达式', minWidth: 260 },
|
{ field: 'expression', title: '表达式', minWidth: 260 },
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,6 @@ const tempStartUserSelectAssignees = ref<Record<string, string[]>>({});
|
||||||
const bpmnXML = ref<string | undefined>(undefined);
|
const bpmnXML = ref<string | undefined>(undefined);
|
||||||
const simpleJson = ref<string | undefined>(undefined);
|
const simpleJson = ref<string | undefined>(undefined);
|
||||||
|
|
||||||
const timelineRef = ref<any>();
|
|
||||||
const activeTab = ref('form');
|
const activeTab = ref('form');
|
||||||
const activityNodes = ref<BpmProcessInstanceApi.ApprovalNodeInfo[]>([]);
|
const activityNodes = ref<BpmProcessInstanceApi.ApprovalNodeInfo[]>([]);
|
||||||
const processInstanceStartLoading = ref(false);
|
const processInstanceStartLoading = ref(false);
|
||||||
|
|
@ -303,7 +302,6 @@ defineExpose({ initProcessInfo });
|
||||||
</Col>
|
</Col>
|
||||||
<Col :xs="24" :sm="24" :md="6" :lg="6" :xl="6">
|
<Col :xs="24" :sm="24" :md="6" :lg="6" :xl="6">
|
||||||
<ProcessInstanceTimeline
|
<ProcessInstanceTimeline
|
||||||
ref="timelineRef"
|
|
||||||
:activity-nodes="activityNodes"
|
:activity-nodes="activityNodes"
|
||||||
:show-status-icon="false"
|
:show-status-icon="false"
|
||||||
@select-user-confirm="selectUserConfirm"
|
@select-user-confirm="selectUserConfirm"
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,24 @@ const popOverVisible: any = ref({
|
||||||
deleteSign: false,
|
deleteSign: false,
|
||||||
}); // 气泡卡是否展示
|
}); // 气泡卡是否展示
|
||||||
const returnList = ref([] as any); // 退回节点
|
const returnList = ref([] as any); // 退回节点
|
||||||
|
const APPROVAL_ATTACHMENT_FILE_TYPES = [
|
||||||
|
'doc',
|
||||||
|
'docx',
|
||||||
|
'xls',
|
||||||
|
'xlsx',
|
||||||
|
'ppt',
|
||||||
|
'pptx',
|
||||||
|
'txt',
|
||||||
|
'pdf',
|
||||||
|
'jpg',
|
||||||
|
'jpeg',
|
||||||
|
'png',
|
||||||
|
'gif',
|
||||||
|
'bmp',
|
||||||
|
'webp',
|
||||||
|
];
|
||||||
|
const APPROVAL_ATTACHMENT_FILE_SIZE = 5;
|
||||||
|
const APPROVAL_ATTACHMENT_DIRECTORY = 'bpm/task-attachment';
|
||||||
|
|
||||||
/** 创建流程表达式 */
|
/** 创建流程表达式 */
|
||||||
function openSignatureModal() {
|
function openSignatureModal() {
|
||||||
|
|
@ -772,7 +790,9 @@ const imagePreviewUrl = ref('');
|
||||||
|
|
||||||
/** 判断文件是否为图片类型 */
|
/** 判断文件是否为图片类型 */
|
||||||
function isImageUrl(url: string) {
|
function isImageUrl(url: string) {
|
||||||
return /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(url);
|
return /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(
|
||||||
|
url.split(/[?#]/)[0] || '',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 处理文件预览 */
|
/** 处理文件预览 */
|
||||||
|
|
@ -893,8 +913,12 @@ defineExpose({ loadTodoTask });
|
||||||
<FormItem label="上传附件/图片" name="attachments">
|
<FormItem label="上传附件/图片" name="attachments">
|
||||||
<FileUpload
|
<FileUpload
|
||||||
v-model:value="approveReasonForm.attachments"
|
v-model:value="approveReasonForm.attachments"
|
||||||
|
:accept="APPROVAL_ATTACHMENT_FILE_TYPES"
|
||||||
|
:directory="APPROVAL_ATTACHMENT_DIRECTORY"
|
||||||
:max-number="10"
|
:max-number="10"
|
||||||
|
:max-size="APPROVAL_ATTACHMENT_FILE_SIZE"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
|
:show-description="true"
|
||||||
:show-download-icon="false"
|
:show-download-icon="false"
|
||||||
help-text="支持多文件/图片上传"
|
help-text="支持多文件/图片上传"
|
||||||
@preview="handleFilePreview"
|
@preview="handleFilePreview"
|
||||||
|
|
@ -960,8 +984,12 @@ defineExpose({ loadTodoTask });
|
||||||
<FormItem label="上传附件/图片" name="attachments">
|
<FormItem label="上传附件/图片" name="attachments">
|
||||||
<FileUpload
|
<FileUpload
|
||||||
v-model:value="rejectReasonForm.attachments"
|
v-model:value="rejectReasonForm.attachments"
|
||||||
|
:accept="APPROVAL_ATTACHMENT_FILE_TYPES"
|
||||||
|
:directory="APPROVAL_ATTACHMENT_DIRECTORY"
|
||||||
:max-number="10"
|
:max-number="10"
|
||||||
|
:max-size="APPROVAL_ATTACHMENT_FILE_SIZE"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
|
:show-description="true"
|
||||||
help-text="支持多文件/图片上传"
|
help-text="支持多文件/图片上传"
|
||||||
@preview="handleFilePreview"
|
@preview="handleFilePreview"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||||
import type { SystemAreaApi } from '#/api/system/area';
|
import type { SystemAreaApi } from '#/api/system/area';
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, shallowRef } from 'vue';
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
|
@ -19,6 +19,7 @@ import { getAreaTree } from '#/api/system/area';
|
||||||
import { getSimpleDeptList } from '#/api/system/dept';
|
import { getSimpleDeptList } from '#/api/system/dept';
|
||||||
import { getSimpleUserList } from '#/api/system/user';
|
import { getSimpleUserList } from '#/api/system/user';
|
||||||
import { decodeFields } from '#/components/form-create';
|
import { decodeFields } from '#/components/form-create';
|
||||||
|
import { registerComponent } from '#/utils';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
|
@ -54,6 +55,7 @@ const userName = computed(() => userStore.userInfo?.nickname ?? '');
|
||||||
const printTime = ref(formatDate(new Date(), 'YYYY-MM-DD HH:mm'));
|
const printTime = ref(formatDate(new Date(), 'YYYY-MM-DD HH:mm'));
|
||||||
const formFields = ref<FormFieldItem[]>([]);
|
const formFields = ref<FormFieldItem[]>([]);
|
||||||
const printDataMap = ref<Record<string, string>>({});
|
const printDataMap = ref<Record<string, string>>({});
|
||||||
|
const BusinessFormComponent = shallowRef<any>();
|
||||||
|
|
||||||
/** 打印配置 */
|
/** 打印配置 */
|
||||||
const printObj = ref({
|
const printObj = ref({
|
||||||
|
|
@ -93,6 +95,17 @@ async function fetchPrintData(id: string) {
|
||||||
printTime.value = formatDate(new Date(), 'YYYY-MM-DD HH:mm');
|
printTime.value = formatDate(new Date(), 'YYYY-MM-DD HH:mm');
|
||||||
initPrintDataMap();
|
initPrintDataMap();
|
||||||
await parseFormFields();
|
await parseFormFields();
|
||||||
|
initBusinessFormComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化业务表单组件 */
|
||||||
|
function initBusinessFormComponent() {
|
||||||
|
const businessFormPath =
|
||||||
|
printData.value?.processInstance.processDefinition?.formCustomViewPath ||
|
||||||
|
'';
|
||||||
|
BusinessFormComponent.value = businessFormPath
|
||||||
|
? registerComponent(businessFormPath)
|
||||||
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 解析表单字段 */
|
/** 解析表单字段 */
|
||||||
|
|
@ -595,6 +608,22 @@ function getPrintTemplateHTML() {
|
||||||
<div v-html="item.html"></div>
|
<div v-html="item.html"></div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<!-- 业务表单:独立成块渲染,不嵌入表格单元格,避免宽度与分页受限 -->
|
||||||
|
<div
|
||||||
|
v-if="BusinessFormComponent && formFields.length === 0"
|
||||||
|
class="mt-3"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="BusinessFormComponent"
|
||||||
|
:id="printData.processInstance.businessKey"
|
||||||
|
:readonly="true"
|
||||||
|
:print-mode="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<table class="mt-3 w-full border-collapse">
|
||||||
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
class="w-full border border-black p-1.5 text-center"
|
class="w-full border border-black p-1.5 text-center"
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,7 @@ function shouldShowCustomUserSelect(
|
||||||
/** 判断是否需要显示审批意见和附件 */
|
/** 判断是否需要显示审批意见和附件 */
|
||||||
function shouldShowReasonAndAttachment(task: any, nodeType: BpmNodeTypeEnum) {
|
function shouldShowReasonAndAttachment(task: any, nodeType: BpmNodeTypeEnum) {
|
||||||
return (
|
return (
|
||||||
(task.reason || task.attachments?.length > 0) &&
|
Boolean(task.reason || task.attachments?.length > 0) &&
|
||||||
[BpmNodeTypeEnum.START_USER_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(
|
[BpmNodeTypeEnum.START_USER_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(
|
||||||
nodeType,
|
nodeType,
|
||||||
)
|
)
|
||||||
|
|
@ -207,11 +207,17 @@ function shouldShowReasonAndAttachment(task: any, nodeType: BpmNodeTypeEnum) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAttachmentName(url: string) {
|
function getAttachmentName(url: string) {
|
||||||
return decodeURIComponent(url.slice(url.lastIndexOf('/') + 1));
|
const cleanUrl = url.split(/[?#]/)[0] || '';
|
||||||
|
const fileName = cleanUrl.slice(cleanUrl.lastIndexOf('/') + 1);
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(fileName);
|
||||||
|
} catch {
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isImageAttachment(url: string) {
|
function isImageAttachment(url: string) {
|
||||||
const ext = url.split('.').pop()?.toLowerCase();
|
const ext = url.split(/[?#]/)[0]?.split('.').pop()?.toLowerCase();
|
||||||
return ['bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp'].includes(ext || '');
|
return ['bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp'].includes(ext || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { VxeGridPropTypes } from '#/adapter/vxe-table';
|
import type {
|
||||||
|
VxeGridPropTypes,
|
||||||
|
VxeTableGridOptions,
|
||||||
|
} from '#/adapter/vxe-table';
|
||||||
import type { BpmProcessListenerApi } from '#/api/bpm/processListener';
|
import type { BpmProcessListenerApi } from '#/api/bpm/processListener';
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
@ -47,7 +50,7 @@ const [Grid] = useVbenVxeGrid({
|
||||||
toolbarConfig: {
|
toolbarConfig: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
},
|
} as VxeTableGridOptions<BpmProcessListenerApi.ProcessListener>,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 配置 Modal
|
// 配置 Modal
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { markRaw } from 'vue';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
import { useUserStore } from '@vben/stores';
|
import { useUserStore } from '@vben/stores';
|
||||||
|
|
||||||
import { getAreaTree } from '#/api/system/area';
|
|
||||||
import { getSimpleUserList } from '#/api/system/user';
|
import { getSimpleUserList } from '#/api/system/user';
|
||||||
|
import { AreaCascader } from '#/components/area';
|
||||||
import { getRangePickerDefaultProps } from '#/utils';
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
/** 新增/修改的表单 */
|
/** 新增/修改的表单 */
|
||||||
|
|
@ -119,11 +121,12 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||||
{
|
{
|
||||||
fieldName: 'areaId',
|
fieldName: 'areaId',
|
||||||
label: '地址',
|
label: '地址',
|
||||||
component: 'ApiTreeSelect',
|
component: markRaw(AreaCascader),
|
||||||
componentProps: {
|
componentProps: {
|
||||||
api: getAreaTree,
|
allowClear: true,
|
||||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
class: '!w-full',
|
||||||
placeholder: '请选择地址',
|
placeholder: '请选择地址',
|
||||||
|
showSearch: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { markRaw } from 'vue';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
import { useUserStore } from '@vben/stores';
|
import { useUserStore } from '@vben/stores';
|
||||||
|
|
||||||
import { getSimpleContactList } from '#/api/crm/contact';
|
import { getSimpleContactList } from '#/api/crm/contact';
|
||||||
import { getCustomerSimpleList } from '#/api/crm/customer';
|
import { getCustomerSimpleList } from '#/api/crm/customer';
|
||||||
import { getAreaTree } from '#/api/system/area';
|
|
||||||
import { getSimpleUserList } from '#/api/system/user';
|
import { getSimpleUserList } from '#/api/system/user';
|
||||||
|
import { AreaCascader } from '#/components/area';
|
||||||
|
|
||||||
/** 新增/修改的表单 */
|
/** 新增/修改的表单 */
|
||||||
export function useFormSchema(): VbenFormSchema[] {
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
|
@ -143,11 +145,12 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||||
{
|
{
|
||||||
fieldName: 'areaId',
|
fieldName: 'areaId',
|
||||||
label: '地址',
|
label: '地址',
|
||||||
component: 'ApiTreeSelect',
|
component: markRaw(AreaCascader),
|
||||||
componentProps: {
|
componentProps: {
|
||||||
api: getAreaTree,
|
allowClear: true,
|
||||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
class: '!w-full',
|
||||||
placeholder: '请选择地址',
|
placeholder: '请选择地址',
|
||||||
|
showSearch: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { markRaw } from 'vue';
|
||||||
|
|
||||||
import { z } from '@vben/common-ui';
|
import { z } from '@vben/common-ui';
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
import { useUserStore } from '@vben/stores';
|
import { useUserStore } from '@vben/stores';
|
||||||
|
|
||||||
import { getAreaTree } from '#/api/system/area';
|
|
||||||
import { getSimpleUserList } from '#/api/system/user';
|
import { getSimpleUserList } from '#/api/system/user';
|
||||||
|
import { AreaCascader } from '#/components/area';
|
||||||
import { getRangePickerDefaultProps } from '#/utils';
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
/** 新增/修改的表单 */
|
/** 新增/修改的表单 */
|
||||||
|
|
@ -128,12 +130,12 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||||
{
|
{
|
||||||
fieldName: 'areaId',
|
fieldName: 'areaId',
|
||||||
label: '地址',
|
label: '地址',
|
||||||
component: 'ApiTreeSelect',
|
component: markRaw(AreaCascader),
|
||||||
componentProps: {
|
componentProps: {
|
||||||
api: getAreaTree,
|
|
||||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
|
||||||
placeholder: '请选择地址',
|
|
||||||
allowClear: true,
|
allowClear: true,
|
||||||
|
class: '!w-full',
|
||||||
|
placeholder: '请选择地址',
|
||||||
|
showSearch: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
'update:items',
|
'update:items',
|
||||||
'update:total-price',
|
'update:totalPrice',
|
||||||
'update:payment-price',
|
'update:paymentPrice',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const tableData = ref<ErpFinancePaymentApi.FinancePaymentItem[]>([]); // 表格数据
|
const tableData = ref<ErpFinancePaymentApi.FinancePaymentItem[]>([]); // 表格数据
|
||||||
|
|
@ -110,8 +110,8 @@ watch(
|
||||||
);
|
);
|
||||||
const finalPaymentPrice = paymentPrice - (props.discountPrice || 0);
|
const finalPaymentPrice = paymentPrice - (props.discountPrice || 0);
|
||||||
// 通知父组件更新
|
// 通知父组件更新
|
||||||
emit('update:total-price', totalPrice);
|
emit('update:totalPrice', totalPrice);
|
||||||
emit('update:payment-price', finalPaymentPrice);
|
emit('update:paymentPrice', finalPaymentPrice);
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
);
|
);
|
||||||
|
|
@ -128,13 +128,15 @@ const handleOpenPurchaseIn = () => {
|
||||||
|
|
||||||
const handleAddPurchaseIn = (rows: ErpPurchaseInApi.PurchaseIn[]) => {
|
const handleAddPurchaseIn = (rows: ErpPurchaseInApi.PurchaseIn[]) => {
|
||||||
rows.forEach((row) => {
|
rows.forEach((row) => {
|
||||||
|
const totalPrice = row.totalPrice ?? 0;
|
||||||
|
const paidPrice = row.paymentPrice ?? 0;
|
||||||
const newItem: ErpFinancePaymentApi.FinancePaymentItem = {
|
const newItem: ErpFinancePaymentApi.FinancePaymentItem = {
|
||||||
bizId: row.id,
|
bizId: row.id ?? 0,
|
||||||
bizType: ErpBizType.PURCHASE_IN,
|
bizType: ErpBizType.PURCHASE_IN,
|
||||||
bizNo: row.no,
|
bizNo: row.no ?? '',
|
||||||
totalPrice: row.totalPrice,
|
totalPrice,
|
||||||
paidPrice: row.paymentPrice,
|
paidPrice,
|
||||||
paymentPrice: row.totalPrice - row.paymentPrice,
|
paymentPrice: totalPrice - paidPrice,
|
||||||
remark: undefined,
|
remark: undefined,
|
||||||
};
|
};
|
||||||
tableData.value.push(newItem);
|
tableData.value.push(newItem);
|
||||||
|
|
@ -154,13 +156,15 @@ const handleOpenSaleReturn = () => {
|
||||||
|
|
||||||
const handleAddSaleReturn = (rows: ErpPurchaseReturnApi.PurchaseReturn[]) => {
|
const handleAddSaleReturn = (rows: ErpPurchaseReturnApi.PurchaseReturn[]) => {
|
||||||
rows.forEach((row) => {
|
rows.forEach((row) => {
|
||||||
|
const totalPrice = row.totalPrice ?? 0;
|
||||||
|
const refundPrice = row.refundPrice ?? 0;
|
||||||
const newItem: ErpFinancePaymentApi.FinancePaymentItem = {
|
const newItem: ErpFinancePaymentApi.FinancePaymentItem = {
|
||||||
bizId: row.id,
|
bizId: row.id ?? 0,
|
||||||
bizType: ErpBizType.PURCHASE_RETURN,
|
bizType: ErpBizType.PURCHASE_RETURN,
|
||||||
bizNo: row.no,
|
bizNo: row.no ?? '',
|
||||||
totalPrice: -row.totalPrice,
|
totalPrice: -totalPrice,
|
||||||
paidPrice: -row.refundPrice,
|
paidPrice: -refundPrice,
|
||||||
paymentPrice: -row.totalPrice + row.refundPrice,
|
paymentPrice: -totalPrice + refundPrice,
|
||||||
remark: undefined,
|
remark: undefined,
|
||||||
};
|
};
|
||||||
tableData.value.push(newItem);
|
tableData.value.push(newItem);
|
||||||
|
|
@ -202,7 +206,7 @@ const validate = () => {
|
||||||
// 检查每行的付款金额
|
// 检查每行的付款金额
|
||||||
for (let i = 0; i < tableData.value.length; i++) {
|
for (let i = 0; i < tableData.value.length; i++) {
|
||||||
const item = tableData.value[i];
|
const item = tableData.value[i];
|
||||||
if (!item.paymentPrice || item.paymentPrice <= 0) {
|
if (!item?.paymentPrice || item.paymentPrice <= 0) {
|
||||||
throw new Error(`第 ${i + 1} 行:本次付款必须大于0`);
|
throw new Error(`第 ${i + 1} 行:本次付款必须大于0`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
'update:items',
|
'update:items',
|
||||||
'update:total-price',
|
'update:totalPrice',
|
||||||
'update:receipt-price',
|
'update:receiptPrice',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const tableData = ref<ErpFinanceReceiptApi.FinanceReceiptItem[]>([]); // 表格数据
|
const tableData = ref<ErpFinanceReceiptApi.FinanceReceiptItem[]>([]); // 表格数据
|
||||||
|
|
@ -110,8 +110,8 @@ watch(
|
||||||
);
|
);
|
||||||
const finalReceiptPrice = receiptPrice - (props.discountPrice || 0);
|
const finalReceiptPrice = receiptPrice - (props.discountPrice || 0);
|
||||||
// 通知父组件更新
|
// 通知父组件更新
|
||||||
emit('update:total-price', totalPrice);
|
emit('update:totalPrice', totalPrice);
|
||||||
emit('update:receipt-price', finalReceiptPrice);
|
emit('update:receiptPrice', finalReceiptPrice);
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
);
|
);
|
||||||
|
|
@ -128,14 +128,15 @@ function handleOpenSaleOut() {
|
||||||
|
|
||||||
function handleAddSaleOut(rows: ErpSaleOutApi.SaleOut[]) {
|
function handleAddSaleOut(rows: ErpSaleOutApi.SaleOut[]) {
|
||||||
rows.forEach((row) => {
|
rows.forEach((row) => {
|
||||||
// TODO 芋艿
|
const totalPrice = row.totalPrice ?? 0;
|
||||||
|
const receiptedPrice = row.receiptPrice ?? 0;
|
||||||
const newItem: ErpFinanceReceiptApi.FinanceReceiptItem = {
|
const newItem: ErpFinanceReceiptApi.FinanceReceiptItem = {
|
||||||
bizId: row.id,
|
bizId: row.id ?? 0,
|
||||||
bizType: ErpBizType.SALE_OUT,
|
bizType: ErpBizType.SALE_OUT,
|
||||||
bizNo: row.no,
|
bizNo: row.no ?? '',
|
||||||
totalPrice: row.totalPrice,
|
totalPrice,
|
||||||
receiptedPrice: row.receiptPrice,
|
receiptedPrice,
|
||||||
receiptPrice: row.totalPrice - row.receiptPrice,
|
receiptPrice: totalPrice - receiptedPrice,
|
||||||
remark: undefined,
|
remark: undefined,
|
||||||
};
|
};
|
||||||
tableData.value.push(newItem);
|
tableData.value.push(newItem);
|
||||||
|
|
@ -154,15 +155,16 @@ function handleOpenSaleReturn() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAddSaleReturn(rows: ErpSaleReturnApi.SaleReturn[]) {
|
function handleAddSaleReturn(rows: ErpSaleReturnApi.SaleReturn[]) {
|
||||||
// TODO 芋艿
|
|
||||||
rows.forEach((row) => {
|
rows.forEach((row) => {
|
||||||
|
const totalPrice = row.totalPrice ?? 0;
|
||||||
|
const refundPrice = row.refundPrice ?? 0;
|
||||||
const newItem: ErpFinanceReceiptApi.FinanceReceiptItem = {
|
const newItem: ErpFinanceReceiptApi.FinanceReceiptItem = {
|
||||||
bizId: row.id,
|
bizId: row.id ?? 0,
|
||||||
bizType: ErpBizType.SALE_RETURN,
|
bizType: ErpBizType.SALE_RETURN,
|
||||||
bizNo: row.no,
|
bizNo: row.no ?? '',
|
||||||
totalPrice: -row.totalPrice,
|
totalPrice: -totalPrice,
|
||||||
receiptedPrice: -row.refundPrice,
|
receiptedPrice: -refundPrice,
|
||||||
receiptPrice: -row.totalPrice + row.refundPrice,
|
receiptPrice: -totalPrice + refundPrice,
|
||||||
remark: undefined,
|
remark: undefined,
|
||||||
};
|
};
|
||||||
tableData.value.push(newItem);
|
tableData.value.push(newItem);
|
||||||
|
|
@ -204,7 +206,7 @@ function validate() {
|
||||||
// 检查每行的收款金额
|
// 检查每行的收款金额
|
||||||
for (let i = 0; i < tableData.value.length; i++) {
|
for (let i = 0; i < tableData.value.length; i++) {
|
||||||
const item = tableData.value[i];
|
const item = tableData.value[i];
|
||||||
if (!item.receiptPrice || item.receiptPrice <= 0) {
|
if (!item?.receiptPrice || item.receiptPrice <= 0) {
|
||||||
throw new Error(`第 ${i + 1} 行:本次收款必须大于0`);
|
throw new Error(`第 ${i + 1} 行:本次收款必须大于0`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,6 @@ import TimeSummaryChart from './modules/time-summary-chart.vue';
|
||||||
defineOptions({ name: 'ErpHome' });
|
defineOptions({ name: 'ErpHome' });
|
||||||
|
|
||||||
const loading = ref(false); // 加载中
|
const loading = ref(false); // 加载中
|
||||||
|
|
||||||
/** 图表组件引用 */
|
|
||||||
const saleChartRef = ref();
|
|
||||||
const purchaseChartRef = ref();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -36,15 +32,11 @@ const purchaseChartRef = ref();
|
||||||
<Row :gutter="16">
|
<Row :gutter="16">
|
||||||
<!-- 销售统计 -->
|
<!-- 销售统计 -->
|
||||||
<Col :md="12" :sm="12" :xs="24">
|
<Col :md="12" :sm="12" :xs="24">
|
||||||
<TimeSummaryChart ref="saleChartRef" title="销售统计" type="sale" />
|
<TimeSummaryChart title="销售统计" type="sale" />
|
||||||
</Col>
|
</Col>
|
||||||
<!-- 采购统计 -->
|
<!-- 采购统计 -->
|
||||||
<Col :md="12" :sm="12" :xs="24">
|
<Col :md="12" :sm="12" :xs="24">
|
||||||
<TimeSummaryChart
|
<TimeSummaryChart title="采购统计" type="purchase" />
|
||||||
ref="purchaseChartRef"
|
|
||||||
title="采购统计"
|
|
||||||
type="purchase"
|
|
||||||
/>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
import type { EChartsOption, EchartsUIType } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
import type { ErpPurchaseStatisticsApi } from '#/api/erp/statistics/purchase';
|
import type { ErpPurchaseStatisticsApi } from '#/api/erp/statistics/purchase';
|
||||||
import type { ErpSaleStatisticsApi } from '#/api/erp/statistics/sale';
|
import type { ErpSaleStatisticsApi } from '#/api/erp/statistics/sale';
|
||||||
|
|
@ -49,7 +49,7 @@ const chartRef = ref<EchartsUIType>();
|
||||||
const { renderEcharts } = useEcharts(chartRef);
|
const { renderEcharts } = useEcharts(chartRef);
|
||||||
|
|
||||||
/** 折线图配置 */
|
/** 折线图配置 */
|
||||||
const lineChartOptions: echarts.EChartsOption = {
|
const lineChartOptions: EChartsOption = {
|
||||||
grid: {
|
grid: {
|
||||||
left: 20,
|
left: 20,
|
||||||
right: 20,
|
right: 20,
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,9 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
'update:items',
|
'update:items',
|
||||||
'update:discount-price',
|
'update:discountPrice',
|
||||||
'update:other-price',
|
'update:otherPrice',
|
||||||
'update:total-price',
|
'update:totalPrice',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const tableData = ref<ErpPurchaseInApi.PurchaseInItem[]>([]); // 表格数据
|
const tableData = ref<ErpPurchaseInApi.PurchaseInItem[]>([]); // 表格数据
|
||||||
|
|
@ -122,9 +122,9 @@ watch(
|
||||||
const finalTotalPrice = discountedPrice + (props.otherPrice || 0);
|
const finalTotalPrice = discountedPrice + (props.otherPrice || 0);
|
||||||
|
|
||||||
// 通知父组件更新
|
// 通知父组件更新
|
||||||
emit('update:discount-price', discountPrice);
|
emit('update:discountPrice', discountPrice);
|
||||||
emit('update:other-price', props.otherPrice || 0);
|
emit('update:otherPrice', props.otherPrice || 0);
|
||||||
emit('update:total-price', finalTotalPrice);
|
emit('update:totalPrice', finalTotalPrice);
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
'update:items',
|
'update:items',
|
||||||
'update:discount-price',
|
'update:discountPrice',
|
||||||
'update:total-price',
|
'update:totalPrice',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const tableData = ref<ErpPurchaseOrderApi.PurchaseOrderItem[]>([]); // 表格数据
|
const tableData = ref<ErpPurchaseOrderApi.PurchaseOrderItem[]>([]); // 表格数据
|
||||||
|
|
@ -113,8 +113,8 @@ watch(
|
||||||
: erpPriceMultiply(totalPrice, props.discountPercent / 100);
|
: erpPriceMultiply(totalPrice, props.discountPercent / 100);
|
||||||
const finalTotalPrice = totalPrice - discountPrice!;
|
const finalTotalPrice = totalPrice - discountPrice!;
|
||||||
// 通知父组件更新
|
// 通知父组件更新
|
||||||
emit('update:discount-price', discountPrice);
|
emit('update:discountPrice', discountPrice);
|
||||||
emit('update:total-price', finalTotalPrice);
|
emit('update:totalPrice', finalTotalPrice);
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,9 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
'update:items',
|
'update:items',
|
||||||
'update:discount-price',
|
'update:discountPrice',
|
||||||
'update:other-price',
|
'update:otherPrice',
|
||||||
'update:total-price',
|
'update:totalPrice',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const tableData = ref<ErpPurchaseReturnApi.PurchaseReturnItem[]>([]); // 表格数据
|
const tableData = ref<ErpPurchaseReturnApi.PurchaseReturnItem[]>([]); // 表格数据
|
||||||
|
|
@ -122,9 +122,9 @@ watch(
|
||||||
const finalTotalPrice = discountedPrice + (props.otherPrice || 0);
|
const finalTotalPrice = discountedPrice + (props.otherPrice || 0);
|
||||||
|
|
||||||
// 通知父组件更新
|
// 通知父组件更新
|
||||||
emit('update:discount-price', discountPrice);
|
emit('update:discountPrice', discountPrice);
|
||||||
emit('update:other-price', props.otherPrice || 0);
|
emit('update:otherPrice', props.otherPrice || 0);
|
||||||
emit('update:total-price', finalTotalPrice);
|
emit('update:totalPrice', finalTotalPrice);
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
'update:items',
|
'update:items',
|
||||||
'update:discount-price',
|
'update:discountPrice',
|
||||||
'update:total-price',
|
'update:totalPrice',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const tableData = ref<ErpSaleOrderApi.SaleOrderItem[]>([]); // 表格数据
|
const tableData = ref<ErpSaleOrderApi.SaleOrderItem[]>([]); // 表格数据
|
||||||
|
|
@ -113,8 +113,8 @@ watch(
|
||||||
: erpPriceMultiply(totalPrice, props.discountPercent / 100);
|
: erpPriceMultiply(totalPrice, props.discountPercent / 100);
|
||||||
const finalTotalPrice = totalPrice - discountPrice!;
|
const finalTotalPrice = totalPrice - discountPrice!;
|
||||||
// 通知父组件更新
|
// 通知父组件更新
|
||||||
emit('update:discount-price', discountPrice);
|
emit('update:discountPrice', discountPrice);
|
||||||
emit('update:total-price', finalTotalPrice);
|
emit('update:totalPrice', finalTotalPrice);
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,9 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
'update:items',
|
'update:items',
|
||||||
'update:discount-price',
|
'update:discountPrice',
|
||||||
'update:other-price',
|
'update:otherPrice',
|
||||||
'update:total-price',
|
'update:totalPrice',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const tableData = ref<ErpSaleOutApi.SaleOutItem[]>([]); // 表格数据
|
const tableData = ref<ErpSaleOutApi.SaleOutItem[]>([]); // 表格数据
|
||||||
|
|
@ -122,9 +122,9 @@ watch(
|
||||||
const finalTotalPrice = discountedPrice + (props.otherPrice || 0);
|
const finalTotalPrice = discountedPrice + (props.otherPrice || 0);
|
||||||
|
|
||||||
// 通知父组件更新
|
// 通知父组件更新
|
||||||
emit('update:discount-price', discountPrice);
|
emit('update:discountPrice', discountPrice);
|
||||||
emit('update:other-price', props.otherPrice || 0);
|
emit('update:otherPrice', props.otherPrice || 0);
|
||||||
emit('update:total-price', finalTotalPrice);
|
emit('update:totalPrice', finalTotalPrice);
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,9 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
'update:items',
|
'update:items',
|
||||||
'update:discount-price',
|
'update:discountPrice',
|
||||||
'update:other-price',
|
'update:otherPrice',
|
||||||
'update:total-price',
|
'update:totalPrice',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const tableData = ref<ErpSaleReturnApi.SaleReturnItem[]>([]); // 表格数据
|
const tableData = ref<ErpSaleReturnApi.SaleReturnItem[]>([]); // 表格数据
|
||||||
|
|
@ -122,9 +122,9 @@ watch(
|
||||||
const finalTotalPrice = discountedPrice + (props.otherPrice || 0);
|
const finalTotalPrice = discountedPrice + (props.otherPrice || 0);
|
||||||
|
|
||||||
// 通知父组件更新
|
// 通知父组件更新
|
||||||
emit('update:discount-price', discountPrice);
|
emit('update:discountPrice', discountPrice);
|
||||||
emit('update:other-price', props.otherPrice || 0);
|
emit('update:otherPrice', props.otherPrice || 0);
|
||||||
emit('update:total-price', finalTotalPrice);
|
emit('update:totalPrice', finalTotalPrice);
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import { isPrivateConversation } from '#/views/im/utils/constants'
|
||||||
|
import {
|
||||||
|
type CardMessage,
|
||||||
|
type CardTarget,
|
||||||
|
getCardLabelInfo
|
||||||
|
} from '#/views/im/utils/message'
|
||||||
|
|
||||||
|
import { UserAvatar } from '../user'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImCardBubble' })
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
/** 名片数据;CardMessage(接收侧消息体)或 CardTarget(发送侧预览)共用结构 */
|
||||||
|
card: CardMessage | CardTarget
|
||||||
|
/** 是否显示 cursor: pointer;调用方负责绑 @click 监听 */
|
||||||
|
clickable?: boolean
|
||||||
|
}>(),
|
||||||
|
{ clickable: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 是否用户名片:决定 UserAvatar 是否带 id 触发 UserInfoCard */
|
||||||
|
const isUser = computed(() => isPrivateConversation(props.card.targetType))
|
||||||
|
/** 名片标签信息 */
|
||||||
|
const labelInfo = computed(() => getCardLabelInfo(props.card))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!--
|
||||||
|
名片消息气泡 / 名片预览卡(240px):用户名片 + 群名片通用
|
||||||
|
- 头像 + 名字 + 群成员数副标题(仅群名片)+ 底部分隔条「群名片 / 个人名片」
|
||||||
|
- 用户名片把 :id 传给 UserAvatar 让点击 avatar 弹 UserInfoCard;群名片不传 id
|
||||||
|
- 整卡 click 由调用方监听(@click),组件不内嵌业务逻辑
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
class="flex flex-col w-[240px] rounded-md overflow-hidden bg-[var(--ant-color-bg-container)] border border-solid border-[var(--im-border-color-lighter)]"
|
||||||
|
:class="{ 'cursor-pointer': clickable }"
|
||||||
|
>
|
||||||
|
<div class="flex gap-2.5 items-center px-3 py-2.5">
|
||||||
|
<UserAvatar
|
||||||
|
:id="isUser ? card.targetId : undefined"
|
||||||
|
:url="card.avatar"
|
||||||
|
:name="card.name"
|
||||||
|
:size="40"
|
||||||
|
:clickable="false"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col flex-1 min-w-0">
|
||||||
|
<div class="text-sm font-medium truncate text-[var(--ant-color-text)]">
|
||||||
|
{{ card.name }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!isUser && card.memberCount"
|
||||||
|
class="text-12px truncate text-[var(--ant-color-text-placeholder)]"
|
||||||
|
>
|
||||||
|
{{ card.memberCount }} 人群聊
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="px-3 py-1 text-12px border-t border-t-solid text-[var(--ant-color-text-placeholder)] border-[var(--im-border-color-lighter)] bg-[var(--ant-color-fill-tertiary)]"
|
||||||
|
>
|
||||||
|
{{ labelInfo.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import { IconifyIcon as Icon } from '@vben/icons'
|
||||||
|
|
||||||
|
import { getCardLabelInfo } from '#/views/im/utils/message'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImCardLineLabel' })
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
/** 名片数据;只读 targetType / name 派生标签 + 显示,结构性类型兼容 CardMessage / 引用预览的 partial */
|
||||||
|
card: null | undefined | { name?: string; targetType?: number; }
|
||||||
|
iconSize?: number
|
||||||
|
}>(),
|
||||||
|
{ iconSize: 14 }
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 标签 + 图标按 targetType 二分;兜底「个人名片」避免 null 时 UI 空白 */
|
||||||
|
const labelInfo = computed(() => getCardLabelInfo(props.card))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- 名片单行 inline label:「[icon] 群名片 / 个人名片:xx」;列表摘要 / 引用预览 / 后台预览复用 -->
|
||||||
|
<span class="inline-flex gap-1.5 items-center">
|
||||||
|
<Icon :icon="labelInfo.icon" :size="iconSize" />
|
||||||
|
<span>{{ labelInfo.label }}:{{ card?.name || '' }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as CardBubble } from './card-bubble.vue';
|
||||||
|
export { default as CardLineLabel } from './card-line-label.vue';
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import { IconifyIcon as Icon } from '@vben/icons'
|
||||||
|
|
||||||
|
import { useImUiStore } from '../store/uiStore'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImContextMenu' })
|
||||||
|
|
||||||
|
const uiStore = useImUiStore()
|
||||||
|
const contextMenu = computed(() => uiStore.contextMenu)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算菜单实际渲染坐标:靠近视口右 / 下边缘时回弹,避免菜单被裁剪
|
||||||
|
*
|
||||||
|
* itemHeight / menuWidth 是和模板里 px-4 py-2 + text-13px / min-w-30 配套的实际尺寸;
|
||||||
|
* dividerHeight = 9px(my-1 上下各 4 + 1px border),仅非首项的 divided 计入;
|
||||||
|
* menuHeight 额外加 8 是外层 py-1 的上下 padding 之和(4px × 2)
|
||||||
|
*/
|
||||||
|
const adjustedPosition = computed(() => {
|
||||||
|
const items = contextMenu.value.items
|
||||||
|
const itemHeight = 34
|
||||||
|
const dividerCount = items.filter((it, i) => it.divided && i > 0).length
|
||||||
|
const menuHeight = items.length * itemHeight + dividerCount * 9 + 8
|
||||||
|
const menuWidth = 120
|
||||||
|
let x = contextMenu.value.position.x
|
||||||
|
let y = contextMenu.value.position.y
|
||||||
|
// SSR 兜底:window 不可用时直接返回原始坐标
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
if (y + menuHeight > window.innerHeight) {
|
||||||
|
y = window.innerHeight - menuHeight
|
||||||
|
}
|
||||||
|
if (x + menuWidth > window.innerWidth) {
|
||||||
|
x = window.innerWidth - menuWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 视口很小 / 菜单项很多时上面减法会算出负值,把菜单顶 / 左边推到 0 兜底
|
||||||
|
return { x: Math.max(0, x), y: Math.max(0, y) }
|
||||||
|
})
|
||||||
|
|
||||||
|
type MenuItem = (typeof contextMenu.value.items)[number]
|
||||||
|
|
||||||
|
/** 选中菜单项:disabled 项忽略;正常项调 onSelect 回调后关闭菜单 */
|
||||||
|
function handleSelect(item: MenuItem) {
|
||||||
|
if (item.disabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uiStore.contextMenu.onSelect?.(item)
|
||||||
|
uiStore.closeContextMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 关闭菜单:点遮罩 / 在遮罩上再次右键都会触发 */
|
||||||
|
function handleClose() {
|
||||||
|
uiStore.closeContextMenu()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!--
|
||||||
|
通用右键菜单
|
||||||
|
由 useImUiStore.openContextMenu(position, items, onSelect) 触发全局单例展示
|
||||||
|
调用方在 @contextmenu.prevent 事件里调 openContextMenu 即可,不需要自己挂组件
|
||||||
|
-->
|
||||||
|
<teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="contextMenu.show"
|
||||||
|
class="fixed inset-0 z-9999"
|
||||||
|
@click.stop="handleClose"
|
||||||
|
@contextmenu.prevent="handleClose"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="fixed min-w-30 py-1 bg-[var(--ant-color-bg-elevated)] rounded-md shadow-lg"
|
||||||
|
:style="{ left: `${adjustedPosition.x }px`, top: `${adjustedPosition.y }px` }"
|
||||||
|
>
|
||||||
|
<template v-for="(item, index) in contextMenu.items" :key="item.key">
|
||||||
|
<!-- divided 项上方插一条分割线(首项跳过,避免空白) -->
|
||||||
|
<div
|
||||||
|
v-if="item.divided && index > 0"
|
||||||
|
class="my-1 mx-2 h-[1px] bg-[var(--ant-color-border-secondary)]"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="flex gap-2 items-center px-4 py-2 text-13px text-left cursor-pointer transition-colors hover:bg-[var(--ant-color-fill)]"
|
||||||
|
:class="[
|
||||||
|
item.disabled
|
||||||
|
? '!text-[var(--ant-color-text-disabled)] cursor-not-allowed hover:!bg-transparent'
|
||||||
|
: item.danger
|
||||||
|
? 'text-[#f56c6c]'
|
||||||
|
: 'text-[var(--ant-color-text)]'
|
||||||
|
]"
|
||||||
|
@click.stop="handleSelect(item)"
|
||||||
|
>
|
||||||
|
<Icon v-if="item.icon" :icon="item.icon" :size="14" />
|
||||||
|
<span>{{ item.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,289 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemUserApi } from '#/api/system/user'
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import { IconifyIcon as Icon } from '@vben/icons'
|
||||||
|
import { useUserStore } from '@vben/stores'
|
||||||
|
|
||||||
|
import { Button, Input, message, Modal, Spin } from 'ant-design-vue'
|
||||||
|
|
||||||
|
import { getSimpleUserListByNickname } from '#/api/system/user'
|
||||||
|
import { getCurrentUserId } from '#/views/im/utils/auth'
|
||||||
|
|
||||||
|
import { ImFriendAddSource } from '../../../utils/constants'
|
||||||
|
import { getGenderColor, getGenderIcon } from '../../../utils/user'
|
||||||
|
import { useFriendStore } from '../../store/friendStore'
|
||||||
|
import { UserAvatar } from '../user'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImFriendAddDialog' })
|
||||||
|
|
||||||
|
const visible = ref(false) // 弹窗是否可见
|
||||||
|
const presetUser = ref<null | SystemUserApi.UserSimple>(null) // 预填目标用户:非 null 时跳过搜索步骤,直接进入申请表单(群成员加好友 / 名片加好友等场景)
|
||||||
|
const addSource = ref<number>(ImFriendAddSource.SEARCH) // 添加来源;参见 ImFriendAddSourceEnum,默认 SEARCH
|
||||||
|
const addSourceExtra = ref<string>('') // 来源附带信息:addSource=ImFriendAddSource.GROUP 时传群名,话术拼为「我是 XX 群的 YY」
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
/** 打开加好友弹窗:reset → 灌参 → visible=true;不传 opts 走搜索模式 */
|
||||||
|
open(opts?: { addSource?: number; addSourceExtra?: string; presetUser?: null | SystemUserApi.UserSimple; }) {
|
||||||
|
presetUser.value = opts?.presetUser ?? null
|
||||||
|
addSource.value = opts?.addSource ?? ImFriendAddSource.SEARCH
|
||||||
|
addSourceExtra.value = opts?.addSourceExtra ?? ''
|
||||||
|
resetAll()
|
||||||
|
visible.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const friendStore = useFriendStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
/** 当前登录用户编号;用 computed 包一层,切账号后随 wsCache 重取,避免顶层求值在 keep-alive 实例里持有旧 id */
|
||||||
|
const currentUserId = computed(() => getCurrentUserId())
|
||||||
|
|
||||||
|
/** 搜索结果过滤掉自己;用 v-if 而非 v-show,避免 DOM 占位 + 头像无效请求 */
|
||||||
|
const visibleUsers = computed(() =>
|
||||||
|
users.value.filter((user) => user.id !== currentUserId.value)
|
||||||
|
)
|
||||||
|
const keyword = ref('') // 搜索关键字
|
||||||
|
const users = ref<SystemUserApi.UserSimple[]>([]) // 搜索结果
|
||||||
|
const searched = ref(false) // 是否已搜索
|
||||||
|
const loading = ref(false) // 搜索加载中
|
||||||
|
const step = ref<'apply' | 'search'>('search') // 当前步骤:search=搜索列表;apply=申请表单
|
||||||
|
const targetUser = ref<null | SystemUserApi.UserSimple>(null) // 申请目标用户
|
||||||
|
const applyContent = ref('') // 申请理由(默认填「我是 ${当前昵称}」,对齐微信交互)
|
||||||
|
const displayName = ref('') // 对接收方的备注(仅自己可见)
|
||||||
|
const submitting = ref(false) // 提交中
|
||||||
|
|
||||||
|
/** 弹窗标题随步骤切换 */
|
||||||
|
const dialogTitle = computed(() => (step.value === 'apply' ? '申请添加朋友' : '添加好友'))
|
||||||
|
|
||||||
|
/** 是否预填模式(presetUser 不为空 → 跳过搜索,关闭即销毁,无「取消返回搜索」按钮) */
|
||||||
|
const presetMode = computed(() => !!presetUser.value)
|
||||||
|
|
||||||
|
function resetAll() {
|
||||||
|
keyword.value = ''
|
||||||
|
users.value = []
|
||||||
|
searched.value = false
|
||||||
|
// 预填模式:直接进申请表单,targetUser 取自 presetUser;申请理由按 addSource 区分话术
|
||||||
|
if (presetUser.value) {
|
||||||
|
targetUser.value = presetUser.value
|
||||||
|
applyContent.value = buildPresetApplyContent()
|
||||||
|
displayName.value = ''
|
||||||
|
step.value = 'apply'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 非预填模式:默认进搜索步骤
|
||||||
|
step.value = 'search'
|
||||||
|
targetUser.value = null
|
||||||
|
applyContent.value = ''
|
||||||
|
displayName.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 预填模式下的申请理由话术:群聊「我是"XX 群"的 YY」;其它「我是 YY」 */
|
||||||
|
function buildPresetApplyContent(): string {
|
||||||
|
const myNickname = userStore.userInfo?.nickname || ''
|
||||||
|
if (!myNickname) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
// 群聊场景拼带群名的话术;其它场景默认「我是 YY」
|
||||||
|
const groupExtra = addSource.value === ImFriendAddSource.GROUP ? addSourceExtra.value : ''
|
||||||
|
return groupExtra ? `我是"${groupExtra}"的${myNickname}` : `我是${myNickname}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按昵称搜索用户:空关键字直接清空结果 */
|
||||||
|
async function handleSearch() {
|
||||||
|
searched.value = true
|
||||||
|
if (!keyword.value.trim()) {
|
||||||
|
users.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
users.value = (await getSimpleUserListByNickname(keyword.value.trim())) || []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 进入申请步骤:预填申请理由「我是 ${当前用户昵称}」(对齐微信交互) */
|
||||||
|
function enterApply(user: SystemUserApi.UserSimple) {
|
||||||
|
targetUser.value = user
|
||||||
|
const myNickname = userStore.userInfo?.nickname || ''
|
||||||
|
applyContent.value = myNickname ? `我是${myNickname}` : ''
|
||||||
|
displayName.value = ''
|
||||||
|
step.value = 'apply'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消申请,回到搜索步骤 */
|
||||||
|
function backToSearch() {
|
||||||
|
step.value = 'search'
|
||||||
|
targetUser.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交好友申请:返回 requestId 走「等待验证」;返回 null 表示后端命中「单向好友静默重启」分支,已直接成为好友 */
|
||||||
|
async function handleSubmitApply() {
|
||||||
|
if (!targetUser.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 预校验:不能加自己(搜索列表已过滤,这里兜底 presetUser / 名片入口等场景)
|
||||||
|
if (targetUser.value.id === currentUserId.value) {
|
||||||
|
message.warning('不能添加自己为好友')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const requestId = await friendStore.applyFriendRequest({
|
||||||
|
toUserId: targetUser.value.id,
|
||||||
|
applyContent: applyContent.value.trim() || undefined,
|
||||||
|
displayName: displayName.value.trim() || undefined,
|
||||||
|
addSource: addSource.value
|
||||||
|
})
|
||||||
|
// silent 分支(已是单向好友被静默重启):主动 fetchFriendInfo 入库,不依赖 WS FRIEND_ADD 推送,避免丢推时列表看不到
|
||||||
|
if (requestId === null) {
|
||||||
|
await friendStore.fetchFriendInfo(targetUser.value.id)
|
||||||
|
}
|
||||||
|
message.success(requestId ? '申请已发送,等待对方验证' : '已添加为好友')
|
||||||
|
visible.value = false
|
||||||
|
} catch {
|
||||||
|
// 业务错误(已是好友 / 被对方拉黑 / 用户被禁用 等):全局拦截器已弹错误提示,本地关弹窗避免脏状态停留
|
||||||
|
visible.value = false
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!--
|
||||||
|
添加好友对话框(双层流程)
|
||||||
|
- 第一层 search:按昵称搜索用户列表
|
||||||
|
- 第二层 apply:选中用户后展开「申请添加朋友」表单(申请理由 + 备注)
|
||||||
|
-->
|
||||||
|
<Modal
|
||||||
|
v-model:open="visible"
|
||||||
|
:title="dialogTitle"
|
||||||
|
width="480px"
|
||||||
|
:mask-closable="false"
|
||||||
|
:footer="step === 'apply' ? undefined : null"
|
||||||
|
>
|
||||||
|
<!-- 第一层:搜索 + 用户列表 -->
|
||||||
|
<template v-if="step === 'search'">
|
||||||
|
<Input
|
||||||
|
v-model:value="keyword"
|
||||||
|
placeholder="输入昵称回车搜索(最多展示 20 条)"
|
||||||
|
allow-clear
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<Icon icon="ant-design:search-outlined" class="cursor-pointer" @click="handleSearch" />
|
||||||
|
</template>
|
||||||
|
</Input>
|
||||||
|
|
||||||
|
<Spin :spinning="loading" wrapper-class-name="w-full">
|
||||||
|
<div class="h-[400px] mt-2.5">
|
||||||
|
<div
|
||||||
|
v-if="visibleUsers.length === 0"
|
||||||
|
class="py-10 text-13px text-center text-[var(--ant-color-text-disabled)]"
|
||||||
|
>
|
||||||
|
{{ searched ? '没有搜到用户' : '输入关键字后回车开始搜索' }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="user in visibleUsers"
|
||||||
|
:key="user.id"
|
||||||
|
class="flex gap-3 items-center px-2 py-2.5 border-b border-b-solid border-[var(--ant-color-border-secondary)]"
|
||||||
|
>
|
||||||
|
<UserAvatar
|
||||||
|
:id="user.id"
|
||||||
|
:url="user.avatar"
|
||||||
|
:name="user.nickname"
|
||||||
|
:size="42"
|
||||||
|
:clickable="false"
|
||||||
|
/>
|
||||||
|
<div class="flex-1 min-w-0 overflow-hidden">
|
||||||
|
<!-- 昵称 + 性别图标 -->
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-1 text-sm font-semibold text-[var(--ant-color-text)]"
|
||||||
|
>
|
||||||
|
<span class="truncate">{{ user.nickname }}</span>
|
||||||
|
<Icon
|
||||||
|
v-if="getGenderIcon(user.sex)"
|
||||||
|
:icon="getGenderIcon(user.sex)"
|
||||||
|
:size="14"
|
||||||
|
:color="getGenderColor(user.sex)"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 部门 -->
|
||||||
|
<div
|
||||||
|
v-if="user.deptName"
|
||||||
|
class="mt-0.5 text-xs truncate text-[var(--ant-color-text-secondary)]"
|
||||||
|
>
|
||||||
|
{{ user.deptName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 已是好友显示「已添加」;否则显示「添加」(点击进入 apply 步骤) -->
|
||||||
|
<Button
|
||||||
|
v-if="!friendStore.isActiveFriend(user.id)"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="enterApply(user)"
|
||||||
|
>
|
||||||
|
添加
|
||||||
|
</Button>
|
||||||
|
<Button v-else size="small" disabled>已添加</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Spin>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 第二层:申请表单(对齐微信「申请添加朋友」) -->
|
||||||
|
<template v-if="step === 'apply' && targetUser">
|
||||||
|
<!-- 选中的用户卡片 -->
|
||||||
|
<div
|
||||||
|
class="flex gap-3 items-center px-2 py-3 mb-4 rounded-md bg-[var(--ant-color-fill-secondary)]"
|
||||||
|
>
|
||||||
|
<UserAvatar
|
||||||
|
:id="targetUser.id"
|
||||||
|
:url="targetUser.avatar"
|
||||||
|
:name="targetUser.nickname"
|
||||||
|
:size="42"
|
||||||
|
:clickable="false"
|
||||||
|
/>
|
||||||
|
<div class="flex-1 min-w-0 overflow-hidden">
|
||||||
|
<div class="text-sm font-semibold text-[var(--ant-color-text)] truncate">
|
||||||
|
{{ targetUser.nickname }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="targetUser.deptName"
|
||||||
|
class="mt-0.5 text-xs truncate text-[var(--ant-color-text-secondary)]"
|
||||||
|
>
|
||||||
|
{{ targetUser.deptName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-13px text-[var(--ant-color-text-secondary)] mb-1.5">发送添加朋友申请</div>
|
||||||
|
<Input.TextArea
|
||||||
|
v-model:value="applyContent"
|
||||||
|
:rows="3"
|
||||||
|
:maxlength="255"
|
||||||
|
show-count
|
||||||
|
placeholder="请填写申请理由"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="text-13px text-[var(--ant-color-text-secondary)] mt-3 mb-1.5">备注</div>
|
||||||
|
<Input
|
||||||
|
v-model:value="displayName"
|
||||||
|
:maxlength="16"
|
||||||
|
placeholder="给对方起个备注(仅自己可见,可不填)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 仅在 apply 步骤显示 footer 操作按钮(slot 必须是 el-dialog 直接子节点) -->
|
||||||
|
<template v-if="step === 'apply'" #footer>
|
||||||
|
<!-- 预填模式无搜索步骤,「取消」直接关闭弹窗 -->
|
||||||
|
<Button @click="presetMode ? (visible = false) : backToSearch()">取消</Button>
|
||||||
|
<Button type="primary" :loading="submitting" @click="handleSubmitApply"> 确定 </Button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { FriendLite } from '../../types'
|
||||||
|
|
||||||
|
import { useImUiStore } from '../../store/uiStore'
|
||||||
|
import { UserAvatar } from '../user'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImFriendItem' })
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
active?: boolean
|
||||||
|
friend: FriendLite
|
||||||
|
menu?: boolean // 是否启用右键菜单;在选择器弹窗里一般关闭
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
menu: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
chat: [friend: FriendLite]
|
||||||
|
click: [friend: FriendLite]
|
||||||
|
delete: [friend: FriendLite]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const uiStore = useImUiStore()
|
||||||
|
|
||||||
|
/** 右键菜单:发送消息 / 删除好友 */
|
||||||
|
function handleContextMenu(event: MouseEvent) {
|
||||||
|
if (!props.menu) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uiStore.openContextMenu(
|
||||||
|
{ x: event.clientX, y: event.clientY },
|
||||||
|
[
|
||||||
|
{ key: 'chat', name: '发送消息' },
|
||||||
|
{ key: 'delete', name: '删除好友' }
|
||||||
|
],
|
||||||
|
(item) => {
|
||||||
|
if (item.key === 'chat') emit('chat', props.friend)
|
||||||
|
else if (item.key === 'delete') emit('delete', props.friend)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!--
|
||||||
|
好友单行项
|
||||||
|
- 头像 + 昵称
|
||||||
|
- 选中态 active
|
||||||
|
- 右键菜单(发消息 / 删除好友)由全局 ContextMenu 承接
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
class="relative flex items-center gap-2.5 px-4 py-3 cursor-pointer transition-colors hover:bg-[var(--ant-color-fill)]"
|
||||||
|
:class="{ '!bg-[#d9ecff] dark:!bg-[var(--ant-color-primary-bg-hover)]': active }"
|
||||||
|
@click="$emit('click', friend)"
|
||||||
|
@contextmenu.prevent="handleContextMenu"
|
||||||
|
>
|
||||||
|
<!-- prefix slot:放在头像前,给选择类弹窗的 checkbox / 圆点用,不传则不渲染 -->
|
||||||
|
<slot name="prefix"></slot>
|
||||||
|
<!-- 头像 -->
|
||||||
|
<UserAvatar
|
||||||
|
:id="friend.id"
|
||||||
|
:url="friend.avatar"
|
||||||
|
:name="friend.nickname"
|
||||||
|
:size="42"
|
||||||
|
:clickable="false"
|
||||||
|
/>
|
||||||
|
<!-- 单行展示 displayName 优先;昵称仅在好友详情面板展示,列表里不重复 -->
|
||||||
|
<div class="flex flex-1 min-w-0">
|
||||||
|
<div class="overflow-hidden text-sm truncate text-[var(--ant-color-text)]">
|
||||||
|
{{ friend.displayName || friend.nickname }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as FriendAddDialog } from './friend-add-dialog.vue';
|
||||||
|
export { default as FriendItem } from './friend-item.vue';
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { GroupMemberLite } from './group-member.vue'
|
||||||
|
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { Button, message, Modal } from 'ant-design-vue'
|
||||||
|
|
||||||
|
import { addGroupAdmin, removeGroupAdmin } from '#/api/im/group'
|
||||||
|
import { GROUP_ADMIN_MAX_COUNT } from '#/views/im/utils/config'
|
||||||
|
|
||||||
|
import { GroupMemberPickerPanel } from '../picker'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImGroupAdminSetDialog' })
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
/** 管理员变更成功;父侧通常用来 reload 群数据 */
|
||||||
|
reload: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const groupId = ref(0)
|
||||||
|
const members = ref<GroupMemberLite[]>([])
|
||||||
|
const currentAdminIds = ref<number[]>([]) // 当前管理员 userId 列表:默认勾选 + 提交时 diff
|
||||||
|
const hideIds = ref<number[]>([])
|
||||||
|
const maxSize = ref(GROUP_ADMIN_MAX_COUNT)
|
||||||
|
const selectedIds = ref<number[]>([])
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
/** 打开设置管理员弹窗:reset → 灌参 → visible=true */
|
||||||
|
open(opts: {
|
||||||
|
/** 当前管理员 userId 列表(默认勾选) */
|
||||||
|
currentAdminIds: number[]
|
||||||
|
groupId: number
|
||||||
|
/** 隐藏 userId(群主) */
|
||||||
|
hideIds?: number[]
|
||||||
|
/** 已选数上限;不传走 GROUP_ADMIN_MAX_COUNT */
|
||||||
|
maxSize?: number
|
||||||
|
members: GroupMemberLite[]
|
||||||
|
}) {
|
||||||
|
groupId.value = opts.groupId
|
||||||
|
members.value = opts.members
|
||||||
|
currentAdminIds.value = [...opts.currentAdminIds]
|
||||||
|
hideIds.value = opts.hideIds ? [...opts.hideIds] : []
|
||||||
|
maxSize.value = opts.maxSize ?? GROUP_ADMIN_MAX_COUNT
|
||||||
|
selectedIds.value = [...opts.currentAdminIds]
|
||||||
|
submitting.value = false
|
||||||
|
visible.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 跟当前管理员列表做差集,分别拿到要新增 / 撤销的 userId */
|
||||||
|
async function handleOk() {
|
||||||
|
if (!groupId.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const previousIds = currentAdminIds.value
|
||||||
|
const previousIdSet = new Set(previousIds)
|
||||||
|
const nextIds = selectedIds.value
|
||||||
|
const nextIdSet = new Set(nextIds)
|
||||||
|
const addedIds = nextIds.filter((id) => !previousIdSet.has(id))
|
||||||
|
const removedIds = previousIds.filter((id) => !nextIdSet.has(id))
|
||||||
|
if (addedIds.length === 0 && removedIds.length === 0) {
|
||||||
|
visible.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
if (addedIds.length > 0) {
|
||||||
|
await addGroupAdmin({ id: groupId.value, userIds: addedIds })
|
||||||
|
}
|
||||||
|
if (removedIds.length > 0) {
|
||||||
|
await removeGroupAdmin({ id: groupId.value, userIds: removedIds })
|
||||||
|
}
|
||||||
|
message.success(`已更新群管理员(新增 ${addedIds.length} 位,撤销 ${removedIds.length} 位)`)
|
||||||
|
emit('reload')
|
||||||
|
visible.value = false
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!--
|
||||||
|
设置群管理员:一个弹窗合并增 / 删,提交时跟当前管理员列表 diff
|
||||||
|
- dialog 壳本组件持有;选择 UI 委托 GroupMemberPickerPanel(grid 形态对齐当前视觉)
|
||||||
|
- 群主从候选里隐藏(不能设为管理员)
|
||||||
|
- 对外接口:ref + open({ groupId, members, currentAdminIds, hideIds, maxSize }) + emit reload()
|
||||||
|
-->
|
||||||
|
<Modal
|
||||||
|
v-model:open="visible"
|
||||||
|
title="设置群管理员"
|
||||||
|
width="700px"
|
||||||
|
:mask-closable="false"
|
||||||
|
class="im-picker-dialog"
|
||||||
|
>
|
||||||
|
<div class="h-[480px]">
|
||||||
|
<GroupMemberPickerPanel
|
||||||
|
v-model:selected-ids="selectedIds"
|
||||||
|
:members="members"
|
||||||
|
:hide-ids="hideIds"
|
||||||
|
:max-size="maxSize"
|
||||||
|
selected-display="grid"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<Button @click="visible = false">取消</Button>
|
||||||
|
<Button type="primary" :loading="submitting" @click="handleOk">确定</Button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use '../picker/picker-dialog' as picker;
|
||||||
|
|
||||||
|
/* :deep 穿透 el-dialog 内部类;复用 picker 公共 mixin */
|
||||||
|
.im-picker-dialog {
|
||||||
|
@include picker.styles;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { GroupMember } from '../../types'
|
||||||
|
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildGroupAvatar,
|
||||||
|
getCachedGroupAvatar,
|
||||||
|
setCachedGroupAvatar
|
||||||
|
} from '../../../utils/group'
|
||||||
|
import { getMemberDisplayName } from '../../../utils/user'
|
||||||
|
import { useFriendStore } from '../../store/friendStore'
|
||||||
|
import { useGroupStore } from '../../store/groupStore'
|
||||||
|
import { UserAvatar } from '../user'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImGroupAvatar' })
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
clickable?: boolean // 是否可点击(默认 false,列表里仅展示)
|
||||||
|
groupId: number // 群编号;用于查 store 拿成员头像
|
||||||
|
name?: string // 群名;色卡兜底文字
|
||||||
|
previewable?: boolean // 是否点头像直接放大预览(群详情大头像位用)
|
||||||
|
previewZIndex?: number // 预览层 z-index
|
||||||
|
radius?: string // 圆角,CSS 长度
|
||||||
|
size?: number // 尺寸(px),正方形
|
||||||
|
url?: string // 服务端已设置的群头像 URL;非空则直接用,不拼图
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
clickable: false,
|
||||||
|
name: '',
|
||||||
|
previewable: false,
|
||||||
|
previewZIndex: 2000,
|
||||||
|
radius: '15%',
|
||||||
|
size: 42,
|
||||||
|
url: ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const friendStore = useFriendStore()
|
||||||
|
const groupStore = useGroupStore()
|
||||||
|
const mergedUrl = ref('')
|
||||||
|
// 竞态保护:丢弃过期 await 结果
|
||||||
|
let mergeToken = 0
|
||||||
|
|
||||||
|
/** 按容器 size × DPR 算 canvas 实际像素,避免 2x / 3x retina 屏拼图糊;DPR 封顶 3 防止超高分辨率画布过大 */
|
||||||
|
function getTargetSize(size: number): number {
|
||||||
|
const dpr = Math.min(window.devicePixelRatio || 1, 3)
|
||||||
|
return Math.max(Math.round(size * dpr), 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** store 里整群成员是否「完整加载」过;只在为 true 时才拼图,避免列表场景批量发接口 */
|
||||||
|
const loadedMembers = computed<GroupMember[] | null>(() => {
|
||||||
|
const g = groupStore.getGroup(props.groupId)
|
||||||
|
if (!g?.membersLoaded || !g.members) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return g.members
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 前 9 个成员的拼图入参;name 走 getMemberDisplayName 口径(好友备注 > 群昵称 > 真实昵称) */
|
||||||
|
const memberItems = computed(() => {
|
||||||
|
const members = loadedMembers.value
|
||||||
|
if (!members) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return members.slice(0, 9).map((m) => ({
|
||||||
|
avatar: m.avatar || '',
|
||||||
|
name: getMemberDisplayName(m, friendStore.getFriend(m.userId))
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 成员快照签名:拼 (avatar, name) 字段,原地修改任一字段都会让 watch 重算 */
|
||||||
|
const memberSignature = computed(() =>
|
||||||
|
memberItems.value.map((it) => `${it.avatar}#${it.name}`).join('|')
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 走 buildGroupAvatar 拼图并写回 mergedUrl;mergeToken 校验避免老 await 覆盖新结果 */
|
||||||
|
async function applyMerge(key: string, targetSize: number): Promise<void> {
|
||||||
|
const myToken = ++mergeToken
|
||||||
|
const cached = getCachedGroupAvatar(key)
|
||||||
|
if (cached) {
|
||||||
|
mergedUrl.value = cached
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const dataUrl = await buildGroupAvatar(memberItems.value, { targetSize })
|
||||||
|
if (myToken !== mergeToken) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (dataUrl) {
|
||||||
|
setCachedGroupAvatar(key, dataUrl)
|
||||||
|
}
|
||||||
|
mergedUrl.value = dataUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.url, props.groupId, props.size, memberSignature.value] as const,
|
||||||
|
([url, groupId, size, signature]) => {
|
||||||
|
if (url) {
|
||||||
|
mergedUrl.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!signature) {
|
||||||
|
mergeToken++
|
||||||
|
mergedUrl.value = ''
|
||||||
|
groupStore.loadGroupMemberList(groupId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const targetSize = getTargetSize(size)
|
||||||
|
const key = `${groupId}:${targetSize}:${signature}`
|
||||||
|
applyMerge(key, targetSize)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 最终展示 url:服务端 url 优先 → 拼图 → 空字符串(让 UserAvatar 走色卡) */
|
||||||
|
const finalUrl = computed(() => props.url || mergedUrl.value)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- url 非空走原图;url 空时取前 9 个成员头像拼九宫格 dataURL,成员未在 store 缓存时走色卡兜底 -->
|
||||||
|
<UserAvatar
|
||||||
|
:url="finalUrl"
|
||||||
|
:name="name"
|
||||||
|
:size="size"
|
||||||
|
:radius="radius"
|
||||||
|
:clickable="clickable"
|
||||||
|
:previewable="previewable"
|
||||||
|
:preview-z-index="previewZIndex"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { FriendLite } from '../../types'
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import { Button, message, Modal } from 'ant-design-vue'
|
||||||
|
|
||||||
|
import { createGroup } from '#/api/im/group'
|
||||||
|
|
||||||
|
import { buildDefaultGroupName } from '../../../utils/group'
|
||||||
|
import { useFriendStore } from '../../store/friendStore'
|
||||||
|
import { useGroupStore } from '../../store/groupStore'
|
||||||
|
import { FriendPickerPanel } from '../picker'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImGroupCreateDialog' })
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
/** 创建成功,携带新群编号;父侧通常用来跳转到新群会话 */
|
||||||
|
created: [groupId: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const friendStore = useFriendStore()
|
||||||
|
const groupStore = useGroupStore()
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const lockedIds = ref<number[]>([])
|
||||||
|
const selectedIds = ref<number[]>([])
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
/** 打开发起群聊弹窗:reset → 灌参 → visible=true */
|
||||||
|
open(opts?: { lockedIds?: number[] }) {
|
||||||
|
lockedIds.value = opts?.lockedIds ? [...opts.lockedIds] : []
|
||||||
|
selectedIds.value = []
|
||||||
|
submitting.value = false
|
||||||
|
visible.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 全量好友:直接复用 friendStore Lite 视图(带拼音字段供分桶用) */
|
||||||
|
const friends = computed<FriendLite[]>(() => friendStore.getActiveFriendLiteList)
|
||||||
|
|
||||||
|
/** 完成按钮可点:至少有 1 个非 locked 勾选(locked 是入口锁定项,不算"用户主动选择") */
|
||||||
|
const canSubmit = computed(() => selectedIds.value.length > 0)
|
||||||
|
|
||||||
|
/** 拿到所有要进群的好友(locked + selected);建群默认群名按这批人生成 */
|
||||||
|
function resolveMembersToInvite(): FriendLite[] {
|
||||||
|
const seen = new Set<number>()
|
||||||
|
const result: FriendLite[] = []
|
||||||
|
const byId = new Map(friends.value.map((f) => [f.id, f]))
|
||||||
|
for (const id of lockedIds.value) {
|
||||||
|
if (seen.has(id)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const friend = byId.get(id)
|
||||||
|
if (friend) {
|
||||||
|
seen.add(id)
|
||||||
|
result.push(friend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const id of selectedIds.value) {
|
||||||
|
if (seen.has(id)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const friend = byId.get(id)
|
||||||
|
if (friend) {
|
||||||
|
seen.add(id)
|
||||||
|
result.push(friend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建群聊:建群(同时邀请初始成员)→ upsert groupStore → emit created 让父页跳转新会话 */
|
||||||
|
async function handleOk() {
|
||||||
|
const members = resolveMembersToInvite()
|
||||||
|
if (members.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const memberUserIds = members.map((m) => m.id)
|
||||||
|
const name = buildDefaultGroupName(members)
|
||||||
|
const group = await createGroup({ name, memberUserIds, joinApproval: false })
|
||||||
|
if (!group?.id) {
|
||||||
|
throw new Error('创建群失败:未返回群编号')
|
||||||
|
}
|
||||||
|
// 直接 upsert 进 groupStore,省一次 fetchGroupList —— 服务端返回 VO 已经够建会话了
|
||||||
|
groupStore.upsertGroup({
|
||||||
|
id: group.id,
|
||||||
|
name: group.name,
|
||||||
|
avatar: group.avatar,
|
||||||
|
notice: group.notice,
|
||||||
|
ownerUserId: group.ownerUserId
|
||||||
|
})
|
||||||
|
message.success('群聊创建成功')
|
||||||
|
emit('created', group.id)
|
||||||
|
visible.value = false
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!--
|
||||||
|
发起群聊:选好友 → 默认按所选成员名生成群名 → createGroup
|
||||||
|
- dialog 壳本组件持有;选择 UI 委托 FriendPickerPanel
|
||||||
|
- lockedIds 由调用方通过 open({ lockedIds }) 传入;私聊侧 +建群锁定对方
|
||||||
|
- 不再要求先输入群名 / 不再展示「进群审批」开关,对齐微信 PC
|
||||||
|
- 对外接口:ref + open({ lockedIds }) + emit created(groupId)
|
||||||
|
-->
|
||||||
|
<Modal
|
||||||
|
v-model:open="visible"
|
||||||
|
title="发起群聊"
|
||||||
|
width="720px"
|
||||||
|
:mask-closable="false"
|
||||||
|
class="im-picker-dialog"
|
||||||
|
>
|
||||||
|
<div class="h-[480px]">
|
||||||
|
<FriendPickerPanel
|
||||||
|
v-model:selected-ids="selectedIds"
|
||||||
|
:friends="friends"
|
||||||
|
:locked-ids="lockedIds"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<Button @click="visible = false">取消</Button>
|
||||||
|
<Button type="primary" :loading="submitting" :disabled="!canSubmit" @click="handleOk">
|
||||||
|
完成
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use '../picker/picker-dialog' as picker;
|
||||||
|
|
||||||
|
/* :deep 穿透 el-dialog 内部类;复用 picker 公共 mixin */
|
||||||
|
.im-picker-dialog {
|
||||||
|
@include picker.styles;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { GroupLite } from '../../types'
|
||||||
|
|
||||||
|
import { computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { prompt } from '@vben/common-ui'
|
||||||
|
|
||||||
|
import { Input, message } from 'ant-design-vue'
|
||||||
|
|
||||||
|
import { applyJoinGroup } from '#/api/im/group/request'
|
||||||
|
|
||||||
|
import { ImConversationType, ImGroupAddSource } from '../../../utils/constants'
|
||||||
|
import { getGroupDisplayName } from '../../../utils/user'
|
||||||
|
import { useConversationStore } from '../../store/conversationStore'
|
||||||
|
import { useGroupStore } from '../../store/groupStore'
|
||||||
|
import { useImUiStore } from '../../store/uiStore'
|
||||||
|
import GroupInfo from './group-info.vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImGroupInfoCard' })
|
||||||
|
|
||||||
|
const uiStore = useImUiStore()
|
||||||
|
const conversationStore = useConversationStore()
|
||||||
|
const groupStore = useGroupStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const card = computed(() => uiStore.groupInfoCard)
|
||||||
|
|
||||||
|
/** 关闭浮层 */
|
||||||
|
function handleClose() {
|
||||||
|
uiStore.closeGroupInfoCard()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Esc 关闭 */
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape' && card.value.show) {
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => window.addEventListener('keydown', handleKeydown))
|
||||||
|
onUnmounted(() => window.removeEventListener('keydown', handleKeydown))
|
||||||
|
|
||||||
|
/** 进入群聊:取本地最新群信息(含 silent / 群备注),新建或激活会话 + 跳路由 */
|
||||||
|
function handleChat(group: GroupLite) {
|
||||||
|
const cached = groupStore.getGroup(group.id)
|
||||||
|
// cached 命中走 getGroupDisplayName 让群备注优先(与 contact / 会话列表的展示名一致);缺 cached 时回落 showGroupName / 原群名
|
||||||
|
const displayName = cached
|
||||||
|
? getGroupDisplayName(cached)
|
||||||
|
: group.showGroupName || group.name || ''
|
||||||
|
// 打开或新建会话
|
||||||
|
conversationStore.openConversation(
|
||||||
|
group.id,
|
||||||
|
ImConversationType.GROUP,
|
||||||
|
displayName,
|
||||||
|
cached?.avatar || group.showImage || '',
|
||||||
|
{ silent: !!cached?.silent }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 如果不在会话页,先跳过去(如果在了,MessagePanel 会自己感知会话变化刷新)
|
||||||
|
if (router.currentRoute.value.name !== 'ImHomeConversation') {
|
||||||
|
router.push({ name: 'ImHomeConversation' })
|
||||||
|
}
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加入群聊:先关浮层(避免与 prompt 的 mask 互相遮挡)→ 弹申请理由(可选)→ applyJoinGroup */
|
||||||
|
async function handleApply(group: GroupLite) {
|
||||||
|
handleClose()
|
||||||
|
let applyContent: string
|
||||||
|
try {
|
||||||
|
const result = await prompt<string>({
|
||||||
|
cancelText: '取消',
|
||||||
|
component: Input,
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
placeholder: '请填写验证消息(可选)'
|
||||||
|
},
|
||||||
|
content: '',
|
||||||
|
defaultValue: '',
|
||||||
|
confirmText: '发送申请',
|
||||||
|
modelPropName: 'value',
|
||||||
|
title: `申请加入「${group.name || ''}」`
|
||||||
|
})
|
||||||
|
applyContent = (result || '').trim()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await applyJoinGroup({
|
||||||
|
groupId: group.id,
|
||||||
|
applyContent: applyContent || undefined,
|
||||||
|
addSource: ImGroupAddSource.SHARE_LINK
|
||||||
|
})
|
||||||
|
message.success('加群申请已发送')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!--
|
||||||
|
群信息浮层(与 UserInfoCard 对位)
|
||||||
|
- 仅承担「浮层定位 + 关闭逻辑(点遮罩 / Esc)」,群信息视觉走 <GroupInfo>,与通讯录详情共用一份组件
|
||||||
|
- 触发:useImUiStore.openGroupInfoCardAtEvent(group, e)
|
||||||
|
- GroupInfo 内部按 groupStore 缓存推导 member / stranger,浮层只负责接 chat / apply 事件做业务
|
||||||
|
-->
|
||||||
|
<teleport to="body">
|
||||||
|
<div v-if="card.show" class="fixed inset-0 z-9998" @click.self="handleClose">
|
||||||
|
<div
|
||||||
|
class="fixed w-80 p-4 bg-[var(--ant-color-bg-elevated)] rounded-md shadow-xl"
|
||||||
|
:style="{ left: `${card.position.x }px`, top: `${card.position.y }px` }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<GroupInfo v-if="card.group" :group="card.group" @chat="handleChat" @apply="handleApply" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Friend, GroupLite, GroupMember } from '../../types'
|
||||||
|
import type { GroupMemberLite } from './group-member.vue'
|
||||||
|
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { CommonStatusEnum } from '@vben/constants'
|
||||||
|
|
||||||
|
import { Button } from 'ant-design-vue'
|
||||||
|
|
||||||
|
import { getCurrentUserId } from '#/views/im/utils/auth'
|
||||||
|
|
||||||
|
import { getMemberDisplayName, isGroupQuit } from '../../../utils/user'
|
||||||
|
import { useFriendStore } from '../../store/friendStore'
|
||||||
|
import { useGroupStore } from '../../store/groupStore'
|
||||||
|
import GroupAvatar from './group-avatar.vue'
|
||||||
|
import GroupMemberGrid from './group-member-grid.vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImGroupInfo' })
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
group: GroupLite
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
/** stranger 点「加入群聊」;父级负责弹申请理由 + 调 applyJoinGroup */
|
||||||
|
apply: [group: GroupLite]
|
||||||
|
/** member 点「进入群聊」;父级负责切会话 + 关浮层 */
|
||||||
|
chat: [group: GroupLite]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const groupStore = useGroupStore()
|
||||||
|
const friendStore = useFriendStore()
|
||||||
|
|
||||||
|
const members = ref<GroupMemberLite[]>([])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已加群:基于"自己确实在成员列表里"判断
|
||||||
|
* - 缓存未命中:直接 false(陌生群)
|
||||||
|
* - 命中且 members 已拉:精准查 self.userId 在不在
|
||||||
|
* - 命中但 members 未拉:fetchGroupList 接口语义即「我加入的群」,命中视为 member(拉成员后会自动收敛)
|
||||||
|
*/
|
||||||
|
const isMember = computed(() => {
|
||||||
|
if (!props.group?.id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const cached = groupStore.getGroup(props.group.id)
|
||||||
|
if (!cached) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 历史退群群:直接判 false,避免成员未加载时误显示「进入群聊」
|
||||||
|
if (isGroupQuit(cached)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (cached.membersLoaded && cached.members) {
|
||||||
|
const myId = getCurrentUserId()
|
||||||
|
return cached.members.some((m) => m.userId === myId && m.status === CommonStatusEnum.ENABLE)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
/** 历史退群群:只读,动作区两个按钮都不渲染(既不「进入群聊」也不「加入群聊」) */
|
||||||
|
const isQuitGroup = computed(() => {
|
||||||
|
const id = props.group?.id
|
||||||
|
return id != null && isGroupQuit(groupStore.getGroup(id))
|
||||||
|
})
|
||||||
|
/** 是否未加群:有 id、非成员、且非历史退群群;只有真·陌生人才给「加入群聊」 */
|
||||||
|
const isStranger = computed(() => !!props.group?.id && !isMember.value && !isQuitGroup.value)
|
||||||
|
|
||||||
|
/** 成员数文案:member 优先用本地拉到的列表长度,stranger 用 props.group.memberCount 卡片快照 */
|
||||||
|
const memberCountText = computed(() => {
|
||||||
|
const count = isMember.value
|
||||||
|
? props.group.memberCount || members.value.length
|
||||||
|
: props.group.memberCount
|
||||||
|
return count ? `${count} 位成员` : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
/** member 切群 / 首挂:拉成员;竞态用 group.id 比对丢弃陈旧响应避免上一条群成员错位 */
|
||||||
|
watch(
|
||||||
|
() => [props.group?.id, isMember.value] as const,
|
||||||
|
async ([id, member]) => {
|
||||||
|
members.value = []
|
||||||
|
if (!id || !member) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const list = await groupStore.fetchGroupMemberList(id, true)
|
||||||
|
if (props.group?.id !== id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
members.value = list.map((m) => convertGroupMemberLite(m, friendStore.getFriend(m.userId)))
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 群成员 → 列表项 */
|
||||||
|
function convertGroupMemberLite(member: GroupMember, friend: Friend | undefined): GroupMemberLite {
|
||||||
|
return {
|
||||||
|
userId: member.userId,
|
||||||
|
showName: getMemberDisplayName(member, friend),
|
||||||
|
nickname: member.nickname,
|
||||||
|
avatar: member.avatar,
|
||||||
|
status: member.status,
|
||||||
|
role: member.role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!--
|
||||||
|
群信息内容组件(与 UserInfo 对位)
|
||||||
|
- 头像 + 群名 + 成员数 + 成员宫格 + 动作区,纯展示 + 抛事件,业务由父级承接
|
||||||
|
- relation 走 groupStore 缓存推导:命中 = member(已加群),否则 = stranger(未加群),无 id = readonly
|
||||||
|
- 成员宫格仅 member 时拉取(陌生群拉不到,所有信息走 props.group 卡片快照)
|
||||||
|
-->
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="w-full max-w-[320px] flex flex-col gap-3 items-center">
|
||||||
|
<GroupAvatar
|
||||||
|
:group-id="group.id"
|
||||||
|
:url="group.showImage || group.showImageThumb"
|
||||||
|
:name="group.showGroupName || group.name"
|
||||||
|
:size="72"
|
||||||
|
:previewable="isMember"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="w-full text-lg font-semibold leading-snug text-[var(--ant-color-text)] truncate text-center"
|
||||||
|
>
|
||||||
|
{{ group.showGroupName || group.name }}
|
||||||
|
</div>
|
||||||
|
<div v-if="memberCountText" class="text-13px text-[var(--ant-color-text-secondary)]">
|
||||||
|
{{ memberCountText }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 成员宫格:仅 member 渲染(陌生群拉不到成员) -->
|
||||||
|
<div
|
||||||
|
v-if="isMember && members.length > 0"
|
||||||
|
class="flex flex-wrap gap-2 justify-center w-full pt-2"
|
||||||
|
>
|
||||||
|
<GroupMemberGrid
|
||||||
|
v-for="member in members"
|
||||||
|
:key="member.userId"
|
||||||
|
:member="member"
|
||||||
|
:group-name="group.name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 动作区:member 进入群聊 / stranger 加入群聊 / readonly 不渲染 -->
|
||||||
|
<div v-if="isMember" class="mt-4">
|
||||||
|
<Button type="primary" @click="emit('chat', group)">进入群聊</Button>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="isStranger" class="mt-4">
|
||||||
|
<Button type="primary" @click="emit('apply', group)">加入群聊</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { GroupLite } from '../../types'
|
||||||
|
|
||||||
|
import GroupAvatar from './group-avatar.vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImGroupItem' })
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
active?: boolean
|
||||||
|
group: GroupLite
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
click: [group: GroupLite]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!--
|
||||||
|
群单行项
|
||||||
|
- 头像 + 群名
|
||||||
|
- 选中态 active
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
class="relative flex items-center gap-2.5 px-4 py-3 cursor-pointer transition-colors hover:bg-[var(--ant-color-fill)]"
|
||||||
|
:class="{ '!bg-[#d9ecff] dark:!bg-[var(--ant-color-primary-bg-hover)]': active }"
|
||||||
|
@click="$emit('click', group)"
|
||||||
|
>
|
||||||
|
<GroupAvatar
|
||||||
|
:group-id="group.id"
|
||||||
|
:url="group.showImage || group.showImageThumb"
|
||||||
|
:name="group.showGroupName || group.name"
|
||||||
|
:size="42"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-1 min-w-0">
|
||||||
|
<!-- 单行展示群名;成员数仅在群详情面板展示,列表里不重复 -->
|
||||||
|
<div class="overflow-hidden text-sm truncate text-[var(--ant-color-text)]">
|
||||||
|
{{ group.showGroupName || group.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue