This commit is contained in:
JiXinHui 2025-12-16 14:15:19 +08:00
commit 217d0bd20f
9 changed files with 297 additions and 162 deletions

40
App.vue
View File

@ -10,7 +10,7 @@ export default {
// WebSocket // WebSocket
ws: null, // WebSocket ws: null, // WebSocket
lockReconnect: false, // lockReconnect: false, //
timeout: 30000, // // timeout: 30000, //
timeoutObj: null, // timeoutObj: null, //
serverTimeoutObj: null, // serverTimeoutObj: null, //
timeoutnum: null, // timeoutnum: null, //
@ -84,8 +84,6 @@ export default {
}, },
// WebSocket // WebSocket
initWebSocket() { initWebSocket() {
console.log(this.buildWsUrl());
try { try {
this.ws = new WebSocket(this.buildWsUrl()); this.ws = new WebSocket(this.buildWsUrl());
this.ws.onopen = () => this.handleWsOpen(); this.ws.onopen = () => this.handleWsOpen();
@ -114,32 +112,29 @@ export default {
// //
start() { start() {
this.timeoutObj = setTimeout(() => { this.timeoutObj = setTimeout(() => {
try { //
if ( if (
this.ws && this.ws &&
this.ws.readyState === 1 && this.ws.readyState === 1 &&
this.vuex_user && this.vuex_user &&
(this.vuex_user.id || this.vuex_user.Id) (this.vuex_user.Id || this.vuex_user.id)
) { ) {
//
this.ws.send("heartCheck"); this.ws.send("heartCheck");
} else { } else {
this.lockReconnect = false; //
this.reconnect();
}
} catch (err) {
this.lockReconnect = false; this.lockReconnect = false;
this.reconnect(); this.reconnect();
} }
// //
this.serverTimeoutObj = setTimeout(() => { this.serverTimeoutObj = setTimeout(() => {
try { // console.log("[WebSocket] ");
this.ws && this.ws.close(); this.ws && this.ws.close();
} catch (e) {}
this.lockReconnect = false; this.lockReconnect = false;
this.reconnect(); this.reconnect();
}, this.timeout); }, 30000);
}, this.timeout); }, 30000);
}, },
// //
handleWsOpen() { handleWsOpen() {
@ -153,10 +148,12 @@ export default {
// //
this.reset(); this.reset();
console.log("[WebSocket] 收到消息:", e);
// //
if (typeof e.data === "string" && e.data.indexOf("heartCheck") >= 0) { // if (typeof e.data === "string" && e.data.indexOf("heartCheck") >= 0) {
return; // return;
} // }
// JSON oa-web-phone { Type, Data } // JSON oa-web-phone { Type, Data }
let msgData = null; let msgData = null;
@ -169,6 +166,10 @@ export default {
const type = msgData.Type; const type = msgData.Type;
console.log("收到消息类型:", type);
//
if (type === "pong") return;
// 退 // 退
if (type === "Exit") { if (type === "Exit") {
try { try {
@ -209,14 +210,13 @@ export default {
}, },
// //
handleWsClose(e) { handleWsClose(e) {
console.log(`[WebSocket] 连接关闭: code=${e.code}, reason=${e.reason}`); // console.log(`[WebSocket] : code=${e.code}, reason=${e.reason}`);
this.lockReconnect = false; this.lockReconnect = false;
this.reconnect(); this.reconnect();
}, },
// //
handleWsError(e) { handleWsError(e) {
console.log("[WebSocket] 连接错误:", e); // console.log("[WebSocket] :", e);
this.lockReconnect = false; this.lockReconnect = false;
this.reconnect(); this.reconnect();
}, },
@ -237,7 +237,7 @@ export default {
}, },
mounted() { mounted() {
// 使 oa-web-phone WebSocket // 使 oa-web-phone WebSocket
// this.startLink(); // 线 this.startLink(); // 线
}, },
}; };
</script> </script>

View File

@ -218,9 +218,12 @@ const install = (Vue, vm) => {
// 获取教师列表(学生端-招办在线) // 获取教师列表(学生端-招办在线)
let GetTeacherListApi = (params = {}) => let GetTeacherListApi = (params = {}) =>
vm.$u.get("api/Dialogue/GetTeacherList", params); vm.$u.get("api/Dialogue/GetTeacherList", params);
// 获取会话列表 // 获取会话列表-教师
let GetDialogueListApi = (params = {}) => let GetDialogueListApi = (params = {}) =>
vm.$u.get("api/Dialogue/GetDialogueList", params); vm.$u.get("api/Dialogue/GetDialogueList", params);
// 获取会话列表-用户
let GetDialogueList_UserApi = (params = {}) =>
vm.$u.get("api/Dialogue/GetDialogueList_User", params);
// 创建会话 // 创建会话
let AddDialogueApi = (params = {}) => let AddDialogueApi = (params = {}) =>
vm.$u.post("api/Dialogue/AddDialogue", params); vm.$u.post("api/Dialogue/AddDialogue", params);
@ -236,6 +239,9 @@ const install = (Vue, vm) => {
// 置顶一个会话 // 置顶一个会话
let OverheadOneDialogueApi = (params = {}) => let OverheadOneDialogueApi = (params = {}) =>
vm.$u.post("api/Dialogue/OverheadOneDialogue", params); vm.$u.post("api/Dialogue/OverheadOneDialogue", params);
// 删除会话
let DeleteDialogueApi = (params = {}) =>
vm.$u.post("api/Dialogue/DeleteDialogue", params);
// 将各个定义的接口名称统一放进对象挂载到vm.$u.api(因为vm就是this也即this.$u.api)下 // 将各个定义的接口名称统一放进对象挂载到vm.$u.api(因为vm就是this也即this.$u.api)下
vm.$u.api = { vm.$u.api = {
@ -297,6 +303,7 @@ const install = (Vue, vm) => {
UpdateUserApi, UpdateUserApi,
GetTeacherListApi, GetTeacherListApi,
GetDialogueListApi, GetDialogueListApi,
GetDialogueList_UserApi,
AddDialogueApi, AddDialogueApi,
SendMessage_PrivateApi, SendMessage_PrivateApi,
GetChatHistoryDataApi, GetChatHistoryDataApi,

View File

@ -44,7 +44,7 @@
> >
<!-- 使用新的数据结构渲染聊天历史 --> <!-- 使用新的数据结构渲染聊天历史 -->
<view <view
v-for="(group, groupIndex) in chatHistoryList3" v-for="(group, groupIndex) in currentChatHistory"
:key="'group-' + group.id + groupIndex" :key="'group-' + group.id + groupIndex"
@click="closePopover" @click="closePopover"
> >
@ -63,7 +63,7 @@
'chat-item-active': item.isActiveChat, 'chat-item-active': item.isActiveChat,
}" }"
@click.stop=" @click.stop="
selectChatItem(groupIndex, index, item.id, item.conversationId) selectChatItem(item.id, item.conversationId, item.receiverId)
" "
> >
<view class="chat-item-content"> <view class="chat-item-content">
@ -90,12 +90,7 @@
<view <view
class="popover-item" class="popover-item"
@click.stop=" @click.stop="
selectChatItem( selectChatItem(item.id, item.conversationId, item.receiverId)
groupIndex,
index,
item.id,
item.conversationId
)
" "
> >
<u-icon <u-icon
@ -155,7 +150,11 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
chatHistoryList3: { chatHistoryAI: {
type: Array,
default: () => [],
},
chatHistoryTeacher: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
@ -172,8 +171,6 @@ export default {
return { return {
baseUrl: "", baseUrl: "",
showPopup: false, showPopup: false,
currentActiveGroup: -1,
currentActiveIndex: -1,
activeItemId: "", // ID activeItemId: "", // ID
scrollToView: "", // scroll-into-view scrollToView: "", // scroll-into-view
tabList: [ tabList: [
@ -193,6 +190,12 @@ export default {
}, },
computed: { computed: {
currentChatHistory() {
return this.currentTab === 0
? this.chatHistoryAI
: this.chatHistoryTeacher;
},
headSculptureUrl() { headSculptureUrl() {
if (this.vuex_user.HeadSculptureUrl) { if (this.vuex_user.HeadSculptureUrl) {
return this.baseUrl + "/" + this.vuex_user.HeadSculptureUrl; return this.baseUrl + "/" + this.vuex_user.HeadSculptureUrl;
@ -224,7 +227,7 @@ export default {
} }
}, },
// //
chatHistoryList3: { chatHistoryAI: {
handler() { handler() {
this.$nextTick(() => { this.$nextTick(() => {
this.scrollToActiveItem(); this.scrollToActiveItem();
@ -308,10 +311,10 @@ export default {
for ( for (
let groupIndex = 0; let groupIndex = 0;
groupIndex < this.chatHistoryList3.length; groupIndex < this.chatHistoryAI.length;
groupIndex++ groupIndex++
) { ) {
const group = this.chatHistoryList3[groupIndex]; const group = this.chatHistoryAI[groupIndex];
for (let index = 0; index < group.conversation.length; index++) { for (let index = 0; index < group.conversation.length; index++) {
const item = group.conversation[index]; const item = group.conversation[index];
if (item.isActiveChat) { if (item.isActiveChat) {
@ -326,16 +329,21 @@ export default {
} }
}, },
selectChatItem(groupIndex, index, id, conversationId) { selectChatItem(id, conversationId = null, receiverId = null) {
// this.currentActiveGroup = groupIndex; if (this.currentTab === 0) {
// this.currentActiveIndex = index; // AI
console.log("selectChatItem", groupIndex, index, id, conversationId);
// //
this.$emit("select-conversation", { this.$emit("select-conversation", {
id, id,
conversationId, conversationId,
}); });
} else {
//
this.$store.dispatch("selectTeacherChatItem", {
id,
receiverId,
});
}
}, },
handleCreateConversation() { handleCreateConversation() {

View File

@ -2,7 +2,7 @@
<view class="chat-page"> <view class="chat-page">
<!-- 顶部导航 --> <!-- 顶部导航 -->
<header-bar <header-bar
:title="vuex_msgUser.receiverName" :title="vuex_msgUser.name"
leftIcon="arrow-left" leftIcon="arrow-left"
@leftClick="handleLeftClick" @leftClick="handleLeftClick"
></header-bar> ></header-bar>
@ -15,6 +15,30 @@
:scroll-into-view="scrollToView" :scroll-into-view="scrollToView"
scroll-with-animation scroll-with-animation
> >
<!-- 教师信息 -->
<div class="teacher-info-card">
<image class="teacher-avatar" :src="receiverHeadSculptureUrl"></image>
<div class="teacher-info">
<div class="teacher-name">{{ vuex_msgUser.name }}</div>
<div class="teacher-school">
<image
class="school-icon"
src="/static/common/images/icon_college.png"
></image>
<text class="school-text">{{ vuex_msgUser.collegeName }}</text>
</div>
<div class="teacher-college">
<image
class="college-icon"
src="/static/common/images/icon_major.png"
></image>
<text class="college-text">{{ vuex_msgUser.collegeName }}</text>
</div>
</div>
</div>
<view <view
v-for="(message, index) in vuex_msgList" v-for="(message, index) in vuex_msgList"
:key="message.id" :key="message.id"
@ -28,7 +52,7 @@
<!-- 0 发送消息 --> <!-- 0 发送消息 -->
<view <view
class="message-right" class="message-right"
v-if="message.interactMode === 0" v-if="message.senderId === vuex_user.Id"
:id="'msg-' + message.id" :id="'msg-' + message.id"
> >
<view class="message-content"> <view class="message-content">
@ -44,12 +68,12 @@
<!-- 1 收到消息 --> <!-- 1 收到消息 -->
<view <view
class="message-left" class="message-left"
v-if="message.interactMode === 1" v-if="message.senderId !== vuex_user.Id"
:id="'msg-' + message.id" :id="'msg-' + message.id"
> >
<image <image
class="ai-avatar" class="ai-avatar"
src="/static/common/images/avatar_ai.png" :src="receiverHeadSculptureUrl"
mode="scaleToFill" mode="scaleToFill"
/> />
<view class="message-content"> <view class="message-content">
@ -155,6 +179,7 @@ export default {
onLoad(options) { onLoad(options) {
console.log(this.vuex_msgList); console.log(this.vuex_msgList);
console.log(this.vuex_msgUser, "this.vuex_msgUser");
this.baseUrl = this.$u.http.config.baseUrl; this.baseUrl = this.$u.http.config.baseUrl;
@ -168,6 +193,14 @@ export default {
}, },
computed: { computed: {
receiverHeadSculptureUrl() {
if (this.vuex_msgUser.headSculptureUrl) {
return this.baseUrl + "/" + this.vuex_msgUser.headSculptureUrl;
}
return "/static/common/images/avatar_default2.png";
},
headSculptureUrl() { headSculptureUrl() {
if (this.vuex_user.HeadSculptureUrl) { if (this.vuex_user.HeadSculptureUrl) {
return this.baseUrl + "/" + this.vuex_user.HeadSculptureUrl; return this.baseUrl + "/" + this.vuex_user.HeadSculptureUrl;
@ -231,7 +264,7 @@ export default {
message: this.messageValue, message: this.messageValue,
messageType: 0, messageType: 0,
filePath: "", filePath: "",
interactMode: 0, // interactMode: 0,
}; };
this.$store.commit("push_Msg", msgUserData); this.$store.commit("push_Msg", msgUserData);
@ -274,17 +307,7 @@ export default {
}) })
.then((res) => { .then((res) => {
const msgList = res.data.item1.reverse(); const msgList = res.data.item1.reverse();
// interactMode mock
for (var i = 0; i < msgList.length; i++) {
msgList[i].interactMode = msgList[i].interactMode || 0;
// if (this.PageIndex != 1) {
// this.$store.commit("unshift_Msg", msgList[i]);
// } else {
// this.$store.commit("push_Msg", msgList[i]);
// }
}
this.$store.commit("push_MsgList", msgList); this.$store.commit("push_MsgList", msgList);
}); });
}, },
@ -399,6 +422,62 @@ export default {
// padding: 10rpx 0; // padding: 10rpx 0;
// } // }
.teacher-info-card {
background-color: #ffffff;
padding: 30rpx;
border-radius: 16rpx;
margin: 30rpx 0;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
margin-bottom: 36rpx;
.teacher-avatar {
width: 120rpx;
height: 120rpx;
border-radius: 10rpx;
margin-right: 30rpx;
object-fit: cover;
}
.teacher-info {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 8rpx;
flex: 1;
.teacher-name {
font-family: PingFang SC;
font-weight: bold;
font-size: 32rpx;
color: #333333;
}
.teacher-school,
.teacher-college {
display: flex;
align-items: center;
.school-icon,
.college-icon {
margin-right: 16rpx;
width: 24rpx;
height: 24rpx;
display: inline-block;
text-align: center;
}
.school-text,
.college-text {
font-size: 24rpx;
color: #666666;
}
}
}
}
.message-time { .message-time {
text-align: center; text-align: center;
font-size: 24rpx; font-size: 24rpx;

View File

@ -18,6 +18,7 @@
<view class="teacher-list"> <view class="teacher-list">
<!-- 教师列表 --> <!-- 教师列表 -->
<view v-if="teacherList.length > 0">
<view <view
class="teacher-item" class="teacher-item"
v-for="(teacher, index) in teacherList" v-for="(teacher, index) in teacherList"
@ -36,7 +37,9 @@
<text>在线</text> <text>在线</text>
</view> </view>
</view> </view>
<view class="teacher-department">{{ teacher.collegeName }}</view> <view class="teacher-department">{{
teacher.collegeName
}}</view>
</view> </view>
<view class="ask-button" @click="handleAskQuestion(teacher)"> <view class="ask-button" @click="handleAskQuestion(teacher)">
立即提问 立即提问
@ -44,6 +47,11 @@
</view> </view>
</view> </view>
</view> </view>
<view v-else style="margin-top: 200rpx">
<u-empty text="今日暂无值班客服教师" mode="list"></u-empty>
</view>
</view>
</view> </view>
<!-- 留言弹出层 --> <!-- 留言弹出层 -->
@ -74,20 +82,20 @@ export default {
{ id: 1, name: "迎新在线" }, { id: 1, name: "迎新在线" },
], ],
teacherList: [ teacherList: [
{ // {
id: 1, // id: 1,
name: "孙老师", // name: "",
department: "招就处", // department: "",
avatar: "/static/common/images/avatar.png", // avatar: "/static/common/images/avatar.png",
online: true, // online: true,
}, // },
{ // {
id: 2, // id: 2,
name: "杨老师", // name: "",
department: "电子信息学院", // department: "",
avatar: "/static/common/images/student.png", // avatar: "/static/common/images/student.png",
online: false, // online: false,
}, // },
], ],
}; };
}, },
@ -106,14 +114,8 @@ export default {
handleAskQuestion(teacher) { handleAskQuestion(teacher) {
console.log("点击咨询:", teacher); console.log("点击咨询:", teacher);
var msgUserData = { //
avatar: teacher.headSculptureUrl, this.$store.dispatch("createDialogue", teacher);
friendId: teacher.id,
friendName: teacher.name,
};
//
this.$store.dispatch("openOrCreateDialogue", msgUserData);
return; return;

View File

@ -259,7 +259,8 @@
<!-- 对话弹出层 --> <!-- 对话弹出层 -->
<chat-history <chat-history
:show.sync="popupShow" :show.sync="popupShow"
:chat-history-list3="chatHistoryList3" :chatHistoryAI="chatHistoryAI"
:chatHistoryTeacher="chatHistoryTeacher"
:user-name="vuex_user ? vuex_user.Name : ''" :user-name="vuex_user ? vuex_user.Name : ''"
@select-conversation="handleSelectConversation" @select-conversation="handleSelectConversation"
@create-conversation="handleCreateConversation" @create-conversation="handleCreateConversation"
@ -353,7 +354,7 @@ export default {
}, },
], ],
popupShow: false, popupShow: false,
chatHistoryList3: [ chatHistoryAI: [
// { // {
// "id": "", // "id": "",
// "conversation": [ // "conversation": [
@ -385,6 +386,7 @@ export default {
// ] // ]
// } // }
], ],
chatHistoryTeacher: [],
activeIndex: 0, activeIndex: 0,
commonQuestions: [ commonQuestions: [
"新生报到流程", "新生报到流程",
@ -467,6 +469,7 @@ export default {
methods: { methods: {
async handleLeftClick() { async handleLeftClick() {
await this.getChatHistoryList(); await this.getChatHistoryList();
await this.GetDialogueList_User();
this.handlePopupShow(); this.handlePopupShow();
}, },
@ -493,11 +496,12 @@ export default {
} }
}, },
async getChatHistoryList() { //
this.$u.api.GetConversationPage().then((res) => { async GetDialogueList_User() {
this.chatHistoryList3 = res.data; this.$u.api.GetDialogueList_UserApi().then((res) => {
if (this.chatHistoryList3.length > 0) { this.chatHistoryTeacher = res.data;
this.chatHistoryList3 = res.data.map((group) => { if (this.chatHistoryTeacher.length > 0) {
this.chatHistoryTeacher = res.data.map((group) => {
// conversation // conversation
return { return {
...group, ...group,
@ -511,7 +515,29 @@ export default {
}; };
}); });
} }
console.log("this.chatHistoryList3", this.chatHistoryList3); });
},
// ai
async getChatHistoryList() {
this.$u.api.GetConversationPage().then((res) => {
this.chatHistoryAI = res.data;
if (this.chatHistoryAI.length > 0) {
this.chatHistoryAI = res.data.map((group) => {
// conversation
return {
...group,
conversation: group.conversation.sort((a, b) => {
//
return (
new Date(b.startTime).getTime() -
new Date(a.startTime).getTime()
);
}),
};
});
}
console.log("this.chatHistoryAI", this.chatHistoryAI);
}); });
}, },

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -94,8 +94,6 @@ const store = new Vuex.Store({
// text: "我的" // text: "我的"
// } // }
], ],
vuex_education: [],
vuex_schoolName: "",
}, },
mutations: { mutations: {
$uStore(state, payload) { $uStore(state, payload) {
@ -237,7 +235,7 @@ const store = new Vuex.Store({
// 获取聊天记录(私聊) // 获取聊天记录(私聊)
async fetchChatRecord( async fetchChatRecord(
{ commit, state }, { commit, state },
{ userId, friendId, chatType = 0, PageIndex = 1, PageSize = 20 } { userId, friendId, PageIndex = 1, PageSize = 20 }
) { ) {
const params = { userId, friendId, PageIndex, PageSize }; const params = { userId, friendId, PageIndex, PageSize };
@ -262,58 +260,73 @@ const store = new Vuex.Store({
} }
}, },
// 统一入口:打开或创建会话 // 点击聊天记录,切换到该会话
// 使用位置:所有需要进入聊天的入口调用该方法,避免各页面重复逻辑 selectTeacherChatItem({ commit, dispatch }, { id, receiverId }) {
// 1) 获取最新会话列表 // 清空消息列表,避免旧消息干扰
// 2) 查找是否存在 friendId 对应的会话 commit("push_MsgList", []);
// 3) 有则设置当前会话并(可选)跳转;无则创建,成功后递归重试
async openOrCreateDialogue({ commit, state, dispatch }, user) { Vue.prototype.$u.api
const { friendId, friendName, avatar, chatType = 0 } = user || {}; .GetReceiverUserInfoApi({ Id: receiverId })
const attempt = .then((res) => {
user && typeof user.attempt === "number" ? user.attempt : 1; if (res.succeed && res.data) {
if (!friendId) return Promise.reject(new Error("缺少 friendId")); commit("set_MsgUser", { ...res.data, dialogueManagementId: id });
uni.navigateTo({
url: `/pages/chat/index`,
});
return;
}
});
},
// 点击立即提问进入会话
// 1) 创建新会话
// 2) 获取接收者信息
// 3) 进入会话
async createDialogue({ commit, state, dispatch }, user) {
const { id, dialogueManagementId } = user || {};
if (!id) return Promise.reject(new Error("缺少 id"));
// 清空消息列表,避免旧消息干扰 // 清空消息列表,避免旧消息干扰
commit("push_MsgList", []); commit("push_MsgList", []);
// 第一步:获取列表(复用现有 action保证 state 同步更新) if (
const list = await dispatch("getUserlist"); dialogueManagementId &&
dialogueManagementId !== "00000000-0000-0000-0000-000000000000"
// 第二步:查找是否存在该会话(兼容后端字段 friendId/receiverId ) {
const target = (list || []).find( // 有会话ID直接进入会话
(i) => i && (i.receiverId === friendId || i.friendId === friendId) commit("set_MsgUser", { ...user });
);
if (target) {
commit("set_MsgUser", target);
// 跳转到对话页,参数按需调整
uni.navigateTo({ uni.navigateTo({
url: `/pages/chat/index`, url: `/pages/chat/index`,
}); });
return;
return target;
} }
// 第二次调用时,无论成功与否均在第三步(创建)之前结束 // 没有会话id创建新会话
if (attempt >= 2) { const res1 = await Vue.prototype.$u.api.AddDialogueApi({
return false; receiverId: id,
} onlineConsultationType: 1, // 创建在线咨询固定传1
// 第三步:不存在则创建,成功后重试本流程
const res = await Vue.prototype.$u.api.AddDialogueApi({
receiverId: friendId,
onlineConsultationType: 0, // 0招生在线1迎新在线可按业务传入
}); });
if (res && res.succeed === true) {
// 创建成功后,重新执行流程(再次获取列表并进入会话) const resId = res1.data?.dialogueManagementId || "";
return dispatch("openOrCreateDialogue", {
friendId, if (res1 && res1.succeed) {
friendName, // 获取接收者信息,这里没啥用(先注释)
avatar, // Vue.prototype.$u.api.GetReceiverUserInfoApi({ Id: id }).then((res) => {
chatType, // if (res.succeed && res.data) {
attempt: attempt + 1, // commit("set_MsgUser", { ...res.data, dialogueManagementId });
// uni.navigateTo({
// url: `/pages/chat/index`,
// });
// return;
// }
// });
commit("set_MsgUser", { ...user, dialogueManagementId: resId });
uni.navigateTo({
url: `/pages/chat/index`,
}); });
return;
} }
return Promise.reject(new Error(res?.error || "创建会话失败")); return Promise.reject(new Error(res?.error || "创建会话失败"));
}, },
}, },

2
types/global.d.ts vendored
View File

@ -13,7 +13,7 @@ declare global {
type __VLS_IsAny<T> = 0 extends 1 & T ? true : false; type __VLS_IsAny<T> = 0 extends 1 & T ? true : false;
type __VLS_PickNotAny<A, B> = __VLS_IsAny<A> extends true ? B : A; type __VLS_PickNotAny<A, B> = __VLS_IsAny<A> extends true ? B : A;
type __VLS_SpreadMerge<A, B> = Omit<A, keyof B> & B; type __VLS_SpreadMerge<A, B> = Omit<A, keyof B> & B;
type __VLS_WithComponent<N0 extends string, LocalComponents, Self, N1 extends string, N2 extends string, N3 extends string> = type __VLS_WithComponent<N0 extends string, LocalComponents, Self, N1 extends string, N2 extends string = N1, N3 extends string = N1> =
N1 extends keyof LocalComponents ? { [K in N0]: LocalComponents[N1] } : N1 extends keyof LocalComponents ? { [K in N0]: LocalComponents[N1] } :
N2 extends keyof LocalComponents ? { [K in N0]: LocalComponents[N2] } : N2 extends keyof LocalComponents ? { [K in N0]: LocalComponents[N2] } :
N3 extends keyof LocalComponents ? { [K in N0]: LocalComponents[N3] } : N3 extends keyof LocalComponents ? { [K in N0]: LocalComponents[N3] } :