* feat: Add session page. Signed-off-by: IRONICBo <47499836+IRONICBo@users.noreply.github.com> * feat: Update to OpenIM 3.1. Signed-off-by: IRONICBo <47499836+IRONICBo@users.noreply.github.com> * fix: Update sdk params. Signed-off-by: IRONICBo <47499836+IRONICBo@users.noreply.github.com> * feat: Interact with OpenIM Server 3.1. Signed-off-by: IRONICBo <47499836+IRONICBo@users.noreply.github.com> --------- Signed-off-by: IRONICBo <47499836+IRONICBo@users.noreply.github.com>main
| @@ -47,5 +47,7 @@ module.exports = { | |||
| '@typescript-eslint/ban-types': 'off', | |||
| 'vue/multi-word-component-names': 'off', | |||
| 'vue/comment-directive': 'off', | |||
| 'no-async-promise-executor': 'off', | |||
| '@typescript-eslint/no-non-null-assertion': 'off', | |||
| }, | |||
| }; | |||
| @@ -12,13 +12,14 @@ | |||
| "echarts": "^5.4.2", | |||
| "less-loader": "^11.1.3", | |||
| "lodash": "^4.17.21", | |||
| "open-im-sdk-wasm": "^0.1.1", | |||
| "open-im-sdk-wasm": "^3.1.0", | |||
| "pinia": "^2.1.4", | |||
| "pinia-plugin-persistedstate": "^3.1.0", | |||
| "qs": "^6.11.2", | |||
| "tdesign-icons-vue-next": "^0.1.11", | |||
| "tdesign-vue-next": "^1.3.10", | |||
| "tvision-color": "^1.6.0", | |||
| "uuid": "^9.0.0", | |||
| "vite-svg-loader": "^4.0.0", | |||
| "vue": "^3.3.4", | |||
| "vue-router": "^4.2.2" | |||
| @@ -2864,9 +2865,9 @@ | |||
| } | |||
| }, | |||
| "node_modules/open-im-sdk-wasm": { | |||
| "version": "0.1.1", | |||
| "resolved": "https://registry.npmjs.org/open-im-sdk-wasm/-/open-im-sdk-wasm-0.1.1.tgz", | |||
| "integrity": "sha512-I12XLvP2YwaPPWUCTaOCkqoRuqEx3A1aMvS2f9ouzCMtiFa6FRDLT7koVIri2+XtM8MmEB1XZAxwt3jYBb22gA==", | |||
| "version": "3.1.0", | |||
| "resolved": "https://registry.npmjs.org/open-im-sdk-wasm/-/open-im-sdk-wasm-3.1.0.tgz", | |||
| "integrity": "sha512-Q1VpRCfV6vUrpZKzL/G351zsLeRxByV6FecKWB4blHzJEhXVH47DxbHjXduOs4MU0IcYYRqXiQseiVLsYHUKkQ==", | |||
| "dependencies": { | |||
| "@jlongster/sql.js": "^1.6.7", | |||
| "absurd-sql": "^0.0.53", | |||
| @@ -14,13 +14,14 @@ | |||
| "echarts": "^5.4.2", | |||
| "less-loader": "^11.1.3", | |||
| "lodash": "^4.17.21", | |||
| "open-im-sdk-wasm": "^0.1.1", | |||
| "open-im-sdk-wasm": "^3.1.0", | |||
| "pinia": "^2.1.4", | |||
| "pinia-plugin-persistedstate": "^3.1.0", | |||
| "qs": "^6.11.2", | |||
| "tdesign-icons-vue-next": "^0.1.11", | |||
| "tdesign-vue-next": "^1.3.10", | |||
| "tvision-color": "^1.6.0", | |||
| "uuid": "^9.0.0", | |||
| "vite-svg-loader": "^4.0.0", | |||
| "vue": "^3.3.4", | |||
| "vue-router": "^4.2.2" | |||
| @@ -2,6 +2,7 @@ export interface CommunityInfo { | |||
| avatar: string; | |||
| name: string; | |||
| email: string; | |||
| description: string; | |||
| } | |||
| export type CreateCommunityParam = CommunityInfo; | |||
| @@ -1,3 +1,3 @@ | |||
| import { LoginParam } from '@/utils/open-im-sdk-wasm/types/params'; | |||
| import { InitAndLoginConfig } from '@/utils/open-im-sdk-wasm/types/params'; | |||
| export type IMLoginParam = LoginParam; | |||
| export type IMLoginParam = InitAndLoginConfig; | |||
| @@ -8,5 +8,7 @@ export { store }; | |||
| export * from './menu'; | |||
| export * from './user'; | |||
| export * from './openim_session'; | |||
| export * from './openim_message'; | |||
| export default store; | |||
| @@ -2,6 +2,7 @@ import { defineStore } from 'pinia'; | |||
| import usePermissionStore from '@/store'; | |||
| import type { MenuRoute } from '@/types/interface' | |||
| import type { MenuInfoResponse } from '@/api/response/menuModel'; | |||
| import store from "../index"; | |||
| const MockMenuInfo: MenuInfoResponse[] = [ | |||
| { | |||
| @@ -36,7 +37,7 @@ const MockMenuInfo: MenuInfoResponse[] = [ | |||
| } | |||
| ] | |||
| export const useMenuStore = defineStore('menu', { | |||
| const useStore = defineStore('menu', { | |||
| state: () => ({ | |||
| menu_routes: MockMenuInfo, | |||
| }), | |||
| @@ -46,4 +47,8 @@ export const useMenuStore = defineStore('menu', { | |||
| // TODO: fetch menu info | |||
| } | |||
| } | |||
| }); | |||
| }); | |||
| export default function useMenuStore() { | |||
| return useStore(store); | |||
| } | |||
| @@ -0,0 +1,75 @@ | |||
| import { OpenIM } from '@/api/openim'; | |||
| import { MessageItem } from "open-im-sdk-wasm/lib/types/entity"; | |||
| import { GetAdvancedHistoryMsgParams } from "open-im-sdk-wasm/lib/types/params"; | |||
| import { defineStore } from "pinia"; | |||
| import store from "../index"; | |||
| interface StateType { | |||
| historyMessageList: MessageItem[]; | |||
| hasMore: boolean; | |||
| } | |||
| interface IAdvancedMessageResponse { | |||
| lastMinSeq: number; | |||
| isEnd: boolean; | |||
| messageList: MessageItem[]; | |||
| } | |||
| type GetHistoryMessageListFromReqResp = { | |||
| messageIDList: string[]; | |||
| lastMinSeq: number; | |||
| }; | |||
| const useStore = defineStore("message", { | |||
| state: (): StateType => ({ | |||
| historyMessageList: [], | |||
| hasMore: true, | |||
| }), | |||
| getters: { | |||
| storeHistoryMessageList: (state) => state.historyMessageList, | |||
| storeHistoryMessageHasMore: (state) => state.hasMore, | |||
| }, | |||
| actions: { | |||
| async getHistoryMessageListFromReq( | |||
| params: GetAdvancedHistoryMsgParams | |||
| ): Promise<GetHistoryMessageListFromReqResp> { | |||
| const isFirstPage = | |||
| params.startClientMsgID === "" || params.lastMinSeq === 0; | |||
| try { | |||
| const { data } = await OpenIM.getAdvancedHistoryMessageList<IAdvancedMessageResponse>(params); | |||
| this.historyMessageList = [ | |||
| ...data.messageList, | |||
| ...(isFirstPage ? [] : this.historyMessageList), | |||
| ]; | |||
| this.hasMore = !data.isEnd && data.messageList.length === 20; | |||
| return { | |||
| messageIDList: data.messageList.map( | |||
| (message: MessageItem) => message.clientMsgID | |||
| ), | |||
| lastMinSeq: data.lastMinSeq, | |||
| }; | |||
| } catch (error) { | |||
| console.log("Get history message failed", error); | |||
| this.hasMore = false; | |||
| return { | |||
| messageIDList: [], | |||
| lastMinSeq: 0, | |||
| }; | |||
| } | |||
| }, | |||
| pushNewMessage(message: MessageItem) { | |||
| this.historyMessageList.push(message); | |||
| }, | |||
| clearHistoryMessage() { | |||
| this.historyMessageList = []; | |||
| }, | |||
| resetHistoryMessageList() { | |||
| this.historyMessageList = []; | |||
| this.hasMore = true; | |||
| }, | |||
| }, | |||
| }); | |||
| export default function useMessageStore() { | |||
| return useStore(store); | |||
| } | |||
| @@ -0,0 +1,63 @@ | |||
| import { OpenIM } from '@/api/openim'; | |||
| import { | |||
| ConversationItem, | |||
| } from "@/utils/open-im-sdk-wasm/types/entity"; | |||
| import { defineStore } from "pinia"; | |||
| import store from "../index"; | |||
| interface StateType { | |||
| conversationList: ConversationItem[]; | |||
| currentConversation: ConversationItem; | |||
| unReadCount: number; | |||
| } | |||
| const useStore = defineStore("session", { | |||
| state: (): StateType => ({ | |||
| conversationList: [], | |||
| currentConversation: {} as ConversationItem, | |||
| unReadCount: 0, | |||
| }), | |||
| getters: { | |||
| storeConversationList: (state) => state.conversationList, | |||
| storeCurrentConversation: (state) => state.currentConversation, | |||
| storeUnReadCount: (state) => state.unReadCount, | |||
| }, | |||
| actions: { | |||
| async getConversationListFromReq(): Promise<boolean> { | |||
| try { | |||
| const { data } = await OpenIM.getAllConversationList<ConversationItem[]>(); | |||
| this.conversationList = data; | |||
| console.log('getConversationListFromReq', data); | |||
| return true; | |||
| } catch (error) { | |||
| return false; | |||
| } | |||
| }, | |||
| async getUnReadCountFromReq() { | |||
| const { data } = await OpenIM.getTotalUnreadMsgCount<number>(); | |||
| this.unReadCount = data; | |||
| }, | |||
| updateUnReadCount(data: number) { | |||
| this.unReadCount = data; | |||
| }, | |||
| updateCurrentConversation(item: ConversationItem) { | |||
| this.currentConversation = { ...item }; | |||
| }, | |||
| updateConversationList(list: ConversationItem[]) { | |||
| this.conversationList = [...list]; | |||
| }, | |||
| delConversationByCID(conversationID: string) { | |||
| const idx = this.conversationList.findIndex( | |||
| (cve) => cve.conversationID === conversationID | |||
| ); | |||
| if (idx !== -1) { | |||
| this.conversationList.splice(idx, 1); | |||
| } | |||
| }, | |||
| }, | |||
| }); | |||
| export default function useSessionStore() { | |||
| return useStore(store); | |||
| } | |||
| @@ -7,6 +7,7 @@ import type { GetCommunityInfoResponse } from '@/api/response/communityModel'; | |||
| import { defineStore } from 'pinia'; | |||
| import { getMyCommunityInfo } from '@/api/index/community'; | |||
| import { getMyInfo } from '@/api/index/user'; | |||
| import store from "../index"; | |||
| const InitUserInfo: GetUserInfoResponse = { | |||
| uuid: '', | |||
| @@ -28,14 +29,23 @@ const InitCommunityInfo: GetCommunityInfoResponse = { | |||
| }; | |||
| const InitToken: TokenResponse = { | |||
| token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3V1aWQiOiI5NmUwNGQ5Mi1mZTkwLTRmMGEtYWU4MC1hZTg4YWRmNWY2ZGIiLCJjb21tdW5pdHlfdXVpZCI6ImNmZjkxMjEwLWE5MGEtNGNjYi04MmI3LWI1MzU2M2E3NzkxOSIsImlzcyI6Im9wZW5rZiIsImV4cCI6MTY5MTMxOTM3OCwibmJmIjoxNjkxMjMyOTc4fQ.PaCXrxjP_WyJSY8yishU5jCouRZ1cgpVrh5OSMVBnh0', | |||
| token: '', | |||
| expire_time_seconds: 0, | |||
| }; | |||
| export const useUserStore = defineStore('user', { | |||
| const InitKFToken: TokenResponse = { | |||
| token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3V1aWQiOiI1NTUyNDhhMWQxNDA5YTdhYmI1ODMwZmRhZDVkIiwiY29tbXVuaXR5X3V1aWQiOiI4NjNiOTExMjdmNGYwODhlZDQ2MjZjMzU3MTYyIiwiaXNzIjoib3BlbmtmIiwiZXhwIjoxNjkxNzI5MzgwLCJuYmYiOjE2OTE2NDI5ODB9.JomPDoVlrRYHWr03yIWqHbPaIQfuXdACEkhw4Ccwm0c', | |||
| expire_time_seconds: 0, | |||
| }; | |||
| const InitIMToken: TokenResponse = { | |||
| token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOiI1NTUyNDhhMWQxNDA5YTdhYmI1ODMwZmRhZDVkIiwiUGxhdGZvcm1JRCI6NSwiZXhwIjoxNjk5NDE4OTgwLCJuYmYiOjE2OTE2NDI2ODAsImlhdCI6MTY5MTY0Mjk4MH0.btYIWiNc21o7sowLddv-6k9qEKQdiWlTFnQraX3eAYE', | |||
| expire_time_seconds: 0, | |||
| }; | |||
| const useStore = defineStore('user', { | |||
| state: () => ({ | |||
| kf_token: { ...InitToken }, | |||
| im_token: { ...InitToken }, | |||
| kf_token: { ...InitKFToken }, | |||
| im_token: { ...InitIMToken }, | |||
| userInfo: { ...InitUserInfo }, | |||
| communityInfo: { ...InitCommunityInfo }, | |||
| }), | |||
| @@ -50,7 +60,7 @@ export const useUserStore = defineStore('user', { | |||
| const [userInfo, communityInfo] = await Promise.all([ | |||
| getMyInfo() | |||
| .then(res => { | |||
| console.log('getMyInfo success', res); | |||
| // console.log('getMyInfo success', res); | |||
| return res; | |||
| }) | |||
| .catch(res => { | |||
| @@ -59,7 +69,7 @@ export const useUserStore = defineStore('user', { | |||
| }), // if error, set to default value | |||
| getMyCommunityInfo() | |||
| .then(res => { | |||
| console.log('getMyCommunityInfo success', res); | |||
| // console.log('getMyCommunityInfo success', res); | |||
| return res; | |||
| }) | |||
| .catch(res => { | |||
| @@ -78,3 +88,8 @@ export const useUserStore = defineStore('user', { | |||
| }, | |||
| }, | |||
| }); | |||
| export default function useUserStore() { | |||
| return useStore(store); | |||
| } | |||
| @@ -1,8 +1,22 @@ | |||
| // reset for openkf | |||
| .t-textarea__inner { | |||
| border-radius: var(--td-radius-large); | |||
| height: var(--td-comp-size-xxxxxl); | |||
| padding: var(--td-comp-paddingTB-s) var(--td-comp-paddingLR-l); | |||
| resize: none; | |||
| } | |||
| .t-menu__logo { | |||
| min-height: var(--td-comp-size-xxxxxl); | |||
| } | |||
| .kf-session-input { | |||
| .t-input { | |||
| border-radius: var(--td-radius-large); | |||
| width: 180px; | |||
| } | |||
| } | |||
| .kf-config-input { | |||
| .t-input { | |||
| border-radius: var(--td-radius-large); | |||
| @@ -10,6 +24,31 @@ | |||
| } | |||
| } | |||
| .kf-session-list-card { | |||
| margin-bottom: var(--td-comp-margin-m); | |||
| .t-card { | |||
| border-radius: var(--td-radius-large); | |||
| background-color: var(--td-font-white-1); | |||
| } | |||
| .t-card__header { | |||
| padding: var(--td-comp-paddingTB-xs) var(--td-comp-paddingLR-l); | |||
| padding-top: var(--td-comp-paddingTB-m); | |||
| .t-card__title { | |||
| font-weight: 500; | |||
| } | |||
| } | |||
| .t-card__body { | |||
| padding: var(--td-comp-paddingTB-xs) var(--td-comp-paddingLR-l); | |||
| padding-bottom: var(--td-comp-paddingTB-m); | |||
| font-size: var(--td-font-size-body-small); | |||
| color: var(--td-text-color-secondary); | |||
| } | |||
| } | |||
| .kf-config-list-card { | |||
| margin-bottom: var(--td-comp-margin-m); | |||
| @@ -0,0 +1,37 @@ | |||
| import { | |||
| ConversationItem, | |||
| } from "@/utils/open-im-sdk-wasm/types/entity"; | |||
| export const conversationSort = (conversationList: ConversationItem[]) => { | |||
| const arr: string[] = []; | |||
| const filterArr = conversationList.filter( | |||
| (c) => !arr.includes(c.conversationID) && arr.push(c.conversationID) | |||
| ); | |||
| filterArr.sort((a, b) => { | |||
| if (a.isPinned === b.isPinned) { | |||
| if (!(a.latestMsgSendTime && b.latestMsgSendTime)) { | |||
| return 0; | |||
| } | |||
| const aCompare = | |||
| a.draftTextTime! > a.latestMsgSendTime! | |||
| ? a.draftTextTime! | |||
| : a.latestMsgSendTime!; | |||
| const bCompare = | |||
| b.draftTextTime! > b.latestMsgSendTime! | |||
| ? b.draftTextTime! | |||
| : b.latestMsgSendTime!; | |||
| if (aCompare > bCompare) { | |||
| return -1; | |||
| } else if (aCompare < bCompare) { | |||
| return 1; | |||
| } else { | |||
| return 0; | |||
| } | |||
| } else if (a.isPinned && !b.isPinned) { | |||
| return -1; | |||
| } else { | |||
| return 1; | |||
| } | |||
| }); | |||
| return filterArr; | |||
| }; | |||
| @@ -43,4 +43,18 @@ export const getNowDiffDays = (date: string): number => { | |||
| const diffTime = Math.abs(new Date(date).getTime() - new Date().getTime()); | |||
| const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); | |||
| return diffDays; | |||
| } | |||
| // Return a time from date | |||
| // if date is today, return time | |||
| // if date is yesterday, return yesterday | |||
| // if date is before yesterday, return date | |||
| export const getTimeFromDate = (timestamp: number | string): string => { | |||
| const dat = new Date(timestamp); | |||
| const diffDays = getNowDiffDays(dat.toString()); | |||
| if (diffDays === 0) { | |||
| return dat.getHours() + ':' + dat.getMinutes(); | |||
| } else { | |||
| return dat.getDate() + '/' + (dat.getMonth() + 1) + '/' + dat.getFullYear(); | |||
| } | |||
| } | |||
| @@ -7,7 +7,7 @@ import { ContentTypeEnum } from '@/constants'; | |||
| import { VAxios } from './axios'; | |||
| import type { AxiosTransform, CreateAxiosOptions } from './transform'; | |||
| import { formatRequestDate, joinTimestamp, setObjToUrlParams } from './utils'; | |||
| import { useUserStore } from '@/store'; | |||
| import useUserStore from '@/store/user'; | |||
| // Default API url | |||
| const host = import.meta.env.VITE_API_URL; | |||
| @@ -197,7 +197,7 @@ const COLUMNS: PrimaryTableCol<TableRowData>[] = [ | |||
| > | |||
| <template #avatar="{ row }"> | |||
| <t-avatar v-if="row.avatar === ''">{{ getAvatarString(row.nickname) }}</t-avatar> | |||
| <t-avatar v-else :alt="getAvatarString(row.nickname)" hide-on-load-failed="true" :image="row.avatar" /> | |||
| <t-avatar v-else :alt="getAvatarString(row.nickname)" :hide-on-load-failed="true" :image="row.avatar" /> | |||
| </template> | |||
| <template #op="slotProps"> | |||
| <a class="t-button-link" @click="handleClickEdit(slotProps)">Edit</a> | |||
| @@ -92,14 +92,14 @@ const updateMyAndCommunityInfo = async () => { | |||
| <div :class="[isDark ? 'dark' : 'light', 'config-form-wrapper']"> | |||
| <div class="config-header "> | |||
| <t-row align="middle"> | |||
| <t-col span="11"> | |||
| <t-col :span="11"> | |||
| <h2 class="welcome">Config</h2> | |||
| <h1 class="sub-title"> | |||
| All configs of your custom service. | |||
| </h1> | |||
| </t-col> | |||
| <t-col span="1"> | |||
| <t-col :span="1"> | |||
| <!-- <t-button theme="default" shape="circle" variant="outline" @click="fetchData"><icon name="refresh" /></t-button> --> | |||
| <t-tooltip content="Update config info"><t-button theme="default" variant="outline" @click="updateMyAndCommunityInfo">Update</t-button></t-tooltip> | |||
| </t-col> | |||
| @@ -125,7 +125,7 @@ const updateMyAndCommunityInfo = async () => { | |||
| <t-list-item-meta title="Avatar" description="Your avatar" /> | |||
| <template #action> | |||
| <t-avatar v-if="myInfo.avatar === ''">{{ getAvatarString(myInfo.nickname) }}</t-avatar> | |||
| <t-avatar v-else :alt="getAvatarString(myInfo.nickname)" hide-on-load-failed="true" :image="myInfo.avatar" /> | |||
| <t-avatar v-else :alt="getAvatarString(myInfo.nickname)" :hide-on-load-failed="true" :image="myInfo.avatar" /> | |||
| </template> | |||
| </t-list-item> | |||
| <t-list-item> | |||
| @@ -186,7 +186,7 @@ const updateMyAndCommunityInfo = async () => { | |||
| <t-list-item-meta title="Avatar" description="Community avatar" /> | |||
| <template #action> | |||
| <t-avatar v-if="communityInfo.avatar === ''">{{ getAvatarString(communityInfo.name) }}</t-avatar> | |||
| <t-avatar v-else :alt="getAvatarString(communityInfo.name)" hide-on-load-failed="true" :image="communityInfo.avatar" /> | |||
| <t-avatar v-else :alt="getAvatarString(communityInfo.name)" :hide-on-load-failed="true" :image="communityInfo.avatar" /> | |||
| </template> | |||
| </t-list-item> | |||
| <t-list-item> | |||
| @@ -1,33 +1,54 @@ | |||
| <script setup lang="ts"> | |||
| import { ref } from 'vue'; | |||
| const emit = defineEmits(['change']) | |||
| const isDark = ref(false); | |||
| const cardList = [ | |||
| const selectedConfigOrder = ref<number>(1); | |||
| interface ConfigCard { | |||
| order: number; | |||
| title: string; | |||
| description: string; | |||
| } | |||
| const cardList:ConfigCard[] = [ | |||
| { | |||
| order: 1, | |||
| title: "System", | |||
| description: "Customize your OpenKF system." | |||
| }, | |||
| { | |||
| order: 2, | |||
| title: "Personal", | |||
| description: "Update your personal information." | |||
| }, | |||
| { | |||
| order: 3, | |||
| title: "Community", | |||
| description: "Update your community information." | |||
| }, | |||
| { | |||
| order: 4, | |||
| title: "Group", | |||
| description: "Manage your customer service staff." | |||
| }, | |||
| { | |||
| order: 5, | |||
| title: "Bot", | |||
| description: "Manage LLM Bots." | |||
| }, | |||
| { | |||
| order: 6, | |||
| title: "KnowledgeBase", | |||
| description: "Manage local knowledge base." | |||
| }, | |||
| ] | |||
| const changeConfig = (config: ConfigCard) => { | |||
| // change bind value | |||
| selectedConfigOrder.value = config.order; | |||
| console.log("Change configCard", config.order) | |||
| emit('change', config) | |||
| } | |||
| </script> | |||
| <template> | |||
| @@ -38,8 +59,11 @@ const cardList = [ | |||
| Manage your own custom service config. | |||
| </h1> | |||
| </div> | |||
| <div v-for="item in cardList" :key="item.title" class="kf-config-list-card"> | |||
| <t-card :title="item.title" hover-shadow > | |||
| <div | |||
| v-for="item in cardList" | |||
| :key="item.title" | |||
| :class="[selectedConfigOrder === item.order ? 'kf-selected-card' : '', 'kf-config-list-card']"> | |||
| <t-card :title="item.title" hover-shadow @click="changeConfig(item)"> | |||
| {{ item.description }} | |||
| <template #actions> | |||
| <t-tag | |||
| @@ -215,7 +215,7 @@ const COLUMNS: PrimaryTableCol<TableRowData>[] = [ | |||
| > | |||
| <template #avatar="{ row }"> | |||
| <t-avatar v-if="row.avatar === ''">{{ getAvatarString(row.nickname) }}</t-avatar> | |||
| <t-avatar v-else :alt="getAvatarString(row.nickname)" hide-on-load-failed="true" :image="row.avatar" /> | |||
| <t-avatar v-else :alt="getAvatarString(row.nickname)" :hide-on-load-failed="true" :image="row.avatar" /> | |||
| </template> | |||
| <template #is_enable="{ row }"> | |||
| <t-tag v-if="row.is_enable === true" theme="success" variant="light"> Enabled </t-tag> | |||
| @@ -183,7 +183,7 @@ const COLUMNS: PrimaryTableCol<TableRowData>[] = [ | |||
| > | |||
| <template #avatar="{ row }"> | |||
| <t-avatar v-if="row.avatar === ''">{{ getAvatarString(row.nickname) }}</t-avatar> | |||
| <t-avatar v-else :alt="getAvatarString(row.nickname)" hide-on-load-failed="true" :image="row.avatar" /> | |||
| <t-avatar v-else :alt="getAvatarString(row.nickname)" :hide-on-load-failed="true" :image="row.avatar" /> | |||
| </template> | |||
| <template #op="slotProps"> | |||
| <a class="t-button-link" @click="handleClickEdit(slotProps)">Edit</a> | |||
| @@ -92,4 +92,10 @@ | |||
| border-radius: var(--td-radius-large); | |||
| } | |||
| } | |||
| .kf-selected-card { | |||
| & .t-card--bordered { | |||
| border: 2px solid var(--td-brand-color-5); | |||
| } | |||
| } | |||
| // config form end | |||
| @@ -60,7 +60,7 @@ onMounted(() => { | |||
| > | |||
| <template #image> | |||
| <t-avatar class="kf-list-item-avatar-size" v-if="item.avatar === ''">{{ getAvatarString(item.nickname) }}</t-avatar> | |||
| <t-avatar class="kf-list-item-avatar-size" v-else :alt="getAvatarString(item.nickname)" hide-on-load-failed="true" :image="item.avatar" /> | |||
| <t-avatar class="kf-list-item-avatar-size" v-else :alt="getAvatarString(item.nickname)" :hide-on-load-failed="true" :image="item.avatar" /> | |||
| </template> | |||
| </t-list-item-meta> | |||
| </t-list-item> | |||
| @@ -108,7 +108,7 @@ const fetchCommunityData = async () => { | |||
| </t-button> | |||
| </template> | |||
| <t-row class="content" justify="space-between"> | |||
| <t-col v-for="(item, index) in filteredInfo" :key="index" class="contract" span="3"> | |||
| <t-col v-for="(item, index) in filteredInfo" :key="index" class="contract" :span="3"> | |||
| <div class="contract-title"> | |||
| {{ toUpperCase(index).replaceAll('_', ' ') }} | |||
| </div> | |||
| @@ -125,7 +125,7 @@ const fetchCommunityData = async () => { | |||
| <t-col :flex="1"> | |||
| <t-card class="user-intro" :bordered="false"> | |||
| <t-avatar size="80px" v-if="myInfo.avatar === ''">{{ getAvatarString(myInfo.nickname) }}</t-avatar> | |||
| <t-avatar size="80px" v-else :alt="getAvatarString(myInfo.nickname)" hide-on-load-failed="true" :image="myInfo.avatar" /> | |||
| <t-avatar size="80px" v-else :alt="getAvatarString(myInfo.nickname)" :hide-on-load-failed="true" :image="myInfo.avatar" /> | |||
| <div class="name">{{ myInfo.nickname }}</div> | |||
| <div class="position">{{ myInfo.description }}</div> | |||
| @@ -140,17 +140,17 @@ const fetchCommunityData = async () => { | |||
| </t-button> | |||
| </template> | |||
| <t-row class="content" :gutters="0"> | |||
| <t-col span="3"> | |||
| <t-col :span="3"> | |||
| <t-button size="large" theme="default" shape="circle" variant="outline" @click="navToGitHub"> | |||
| <t-icon name="logo-github" /> | |||
| </t-button> | |||
| </t-col> | |||
| <t-col span="3"> | |||
| <t-col :span="3"> | |||
| <t-button size="large" theme="default" shape="circle" variant="outline" @click="navToDoc"> | |||
| <t-icon name="root-list" /> | |||
| </t-button> | |||
| </t-col> | |||
| <t-col span="3"> | |||
| <t-col :span="3"> | |||
| <t-button size="large" theme="default" shape="circle" variant="outline" @click="navToSlack"> | |||
| <t-icon name="user-talk" /> | |||
| </t-button> | |||
| @@ -2,7 +2,9 @@ | |||
| import { ref } from 'vue'; | |||
| import { computed, onMounted } from 'vue'; | |||
| import { useRouter } from 'vue-router'; | |||
| import { useMenuStore } from '@/store'; | |||
| import useMenuStore from '@/store/menu'; | |||
| import { OpenIM } from '@/api/openim'; | |||
| import { MessagePlugin } from 'tdesign-vue-next'; | |||
| const router = useRouter(); | |||
| const menuStore = useMenuStore(); | |||
| @@ -24,8 +26,16 @@ const changeHandler = (active:string) => { | |||
| }; | |||
| const goHome = () => { | |||
| router.push('/home/dashboard'); | |||
| const goHome = async () => { | |||
| try { | |||
| await OpenIM.logout(); | |||
| } catch (e) { | |||
| MessagePlugin.error('IM logout failed!'); | |||
| console.log(e); | |||
| } finally { | |||
| MessagePlugin.success('Logout successfully!'); | |||
| router.push('/home/dashboard'); | |||
| } | |||
| }; | |||
| </script> | |||
| @@ -0,0 +1,110 @@ | |||
| import { CbEvents } from "@/utils/open-im-sdk-wasm/constant"; | |||
| import { OpenIM } from '@/api/openim'; | |||
| import { onMounted, onUnmounted } from "vue"; | |||
| import { | |||
| ConversationItem, | |||
| WSEvent, | |||
| MessageItem, | |||
| } from "@/utils/open-im-sdk-wasm/types/entity"; | |||
| import useSessionStore from "@/store/openim_session"; | |||
| import useMessageStore from "@/store/openim_message"; | |||
| import { conversationSort } from "@/utils/common/im"; | |||
| import { NotifyPlugin } from "tdesign-vue-next"; | |||
| export function useGlobalEvent() { | |||
| const sessionStore = useSessionStore(); | |||
| const messageStore = useMessageStore(); | |||
| const setIMListener = () => { | |||
| console.log("setIMListener"); | |||
| // message | |||
| OpenIM.on(CbEvents.OnRecvNewMessage, newMessageHandler); | |||
| OpenIM.on(CbEvents.OnRecvNewMessages, newMessageHandler); | |||
| // session | |||
| OpenIM.on(CbEvents.OnConversationChanged, conversationChangeHandler) | |||
| OpenIM.on(CbEvents.OnNewConversation, newConversationHandler); | |||
| OpenIM.on( | |||
| CbEvents.OnTotalUnreadMessageCountChanged, | |||
| totalUnreadChangeHandler | |||
| ); | |||
| }; | |||
| const disposeIMListener = () => { | |||
| console.log("disposeIMListener"); | |||
| // message | |||
| OpenIM.off(CbEvents.OnRecvNewMessage, newMessageHandler); | |||
| OpenIM.off(CbEvents.OnRecvNewMessages, newMessageHandler); | |||
| // conversation | |||
| OpenIM.off(CbEvents.OnConversationChanged, conversationChangeHandler) | |||
| OpenIM.off(CbEvents.OnNewConversation, newConversationHandler); | |||
| OpenIM.off( | |||
| CbEvents.OnTotalUnreadMessageCountChanged, | |||
| totalUnreadChangeHandler | |||
| ); | |||
| }; | |||
| // message | |||
| const newMessageHandler = ({ data }: any) => { | |||
| const parsedData = data; | |||
| // message array | |||
| if (Array.isArray(parsedData)) { | |||
| parsedData.map((message) => handleNewMessage(message)); | |||
| return; | |||
| } | |||
| handleNewMessage(parsedData); | |||
| }; | |||
| const handleNewMessage = (message: MessageItem) => { | |||
| console.log('receve new message', message); | |||
| NotifyPlugin.info({ | |||
| title: 'New Messages: ' + message.senderNickname, | |||
| content: message.textElem?.content, | |||
| duration: 2000, | |||
| placement: 'top-right', | |||
| closeBtn: true, | |||
| }); | |||
| messageStore.pushNewMessage(message); | |||
| }; | |||
| // session | |||
| const conversationChangeHandler = ({ data }: WSEvent<ConversationItem[]>) => { | |||
| let filterArr: ConversationItem[] = []; | |||
| const changes: ConversationItem[] = data; | |||
| const chids = changes.map((ch) => ch.conversationID); | |||
| filterArr = sessionStore.storeConversationList.filter( | |||
| (tc) => !chids.includes(tc.conversationID) | |||
| ); | |||
| const idx = changes.findIndex( | |||
| (c) => | |||
| c.conversationID === | |||
| sessionStore.storeCurrentConversation.conversationID | |||
| ); | |||
| if (idx !== -1) sessionStore.updateCurrentConversation(changes[idx]); | |||
| const result = [...changes, ...filterArr]; | |||
| sessionStore.updateConversationList(conversationSort(result)); | |||
| }; | |||
| const newConversationHandler = ({ data }: WSEvent<ConversationItem[]>) => { | |||
| console.log('receve new conversation', data); | |||
| const news: ConversationItem[] = data; | |||
| const result = [...news, ...sessionStore.storeConversationList]; | |||
| sessionStore.updateConversationList(conversationSort(result)); | |||
| }; | |||
| const totalUnreadChangeHandler = ({ data }: WSEvent<number>) => { | |||
| console.log('receve new unread count', data); | |||
| sessionStore.updateUnReadCount(data); | |||
| }; | |||
| onMounted(() => { | |||
| setIMListener(); | |||
| sessionStore.getConversationListFromReq(); | |||
| sessionStore.getUnReadCountFromReq(); | |||
| }); | |||
| onUnmounted(() => { | |||
| disposeIMListener(); | |||
| }); | |||
| } | |||
| @@ -1,6 +1,56 @@ | |||
| <script setup lang="ts"> | |||
| import LayoutContent from './components/LayoutContent.vue'; | |||
| import LayoutSideNav from './components/LayoutSideNav.vue'; | |||
| import { useGlobalEvent } from './im'; | |||
| import { onMounted, ref } from 'vue'; | |||
| import { OpenIM } from '@/api/openim'; | |||
| import useUserStore from '@/store/user'; | |||
| import { IMLoginParam } from '@/api/request/openimModel'; | |||
| import { OpenIMLoginConfig } from '@/constants'; | |||
| import { useRouter } from 'vue-router'; | |||
| useGlobalEvent(); | |||
| const router = useRouter(); | |||
| const userStore = useUserStore(); | |||
| // login again | |||
| onMounted(() => { | |||
| loginCheck(); | |||
| }); | |||
| const loginCheck = () => { | |||
| OpenIM.getLoginStatus() | |||
| .then(res => { | |||
| if (res.data !== 3) { | |||
| tryLogin(); | |||
| } | |||
| }) | |||
| .catch(tryLogin); | |||
| }; | |||
| const tryLogin = async () => { | |||
| const IMToken = userStore.im_token; | |||
| const KFToken = userStore.kf_token; | |||
| const config: IMLoginParam = { | |||
| userID: userStore.userInfo.uuid, | |||
| token: userStore.im_token.token, | |||
| platformID: OpenIMLoginConfig.PlatformID, | |||
| apiAddr: OpenIMLoginConfig.APIAddress, | |||
| wsAddr: OpenIMLoginConfig.WSAddress, | |||
| }; | |||
| if (IMToken && KFToken) { | |||
| OpenIM.login(config) | |||
| .then(() => { | |||
| console.log("login again success!") | |||
| }) | |||
| .catch(err => { | |||
| console.log("login again error", err); | |||
| router.push('/login'); | |||
| }); | |||
| } | |||
| }; | |||
| </script> | |||
| <template> | |||
| @@ -8,7 +8,8 @@ import { OpenIM } from '@/api/openim'; | |||
| import { OpenIMLoginConfig } from '@/constants'; | |||
| import { IMLoginParam } from '@/api/request/openimModel'; | |||
| import { ref, reactive } from 'vue'; | |||
| import { useUserStore, useMenuStore } from '@/store'; | |||
| import useUserStore from '@/store/user'; | |||
| import useMenuStore from '@/store/menu'; | |||
| import { localCache } from '@/utils/common/cache'; | |||
| const formData = reactive({ email: '', password: '' }); | |||
| @@ -61,8 +62,8 @@ const onSubmit = async (ctx: SubmitContext) => { | |||
| userID: res.uuid, | |||
| token: res.im_token.token, | |||
| platformID: OpenIMLoginConfig.PlatformID, | |||
| apiAddress: OpenIMLoginConfig.APIAddress, | |||
| wsAddress: OpenIMLoginConfig.WSAddress, | |||
| apiAddr: OpenIMLoginConfig.APIAddress, | |||
| wsAddr: OpenIMLoginConfig.WSAddress, | |||
| }; | |||
| let temp_data = res; | |||
| @@ -100,6 +100,7 @@ const onSubmit = (ctx: SubmitContext) => { | |||
| let data: RegisterAdminParam = { | |||
| code: formData.value.admin.verifyCode, | |||
| community_info: { | |||
| description: '', | |||
| avatar: '', | |||
| name: formData.value.community.name, | |||
| email: formData.value.community.email, | |||
| @@ -122,7 +123,7 @@ const onSubmit = (ctx: SubmitContext) => { | |||
| const redirect = route.query.redirect as string; | |||
| const redirectUrl = redirect | |||
| ? decodeURIComponent(redirect) | |||
| : '/dashboard'; | |||
| : '/home/dashboard'; | |||
| router.push(redirectUrl); | |||
| }) | |||
| .catch(err => { | |||
| @@ -31,14 +31,14 @@ const onCancel = () => { | |||
| <div :class="[isDark ? 'dark' : 'light', 'platform-wrapper']"> | |||
| <div class="platform-header "> | |||
| <t-row align="middle"> | |||
| <t-col span="10"> | |||
| <t-col :span="10"> | |||
| <h2 class="welcome">Platform</h2> | |||
| <h1 class="sub-title"> | |||
| Check and use various platform access. | |||
| </h1> | |||
| </t-col> | |||
| <t-col span="2" align="center"> | |||
| <t-col :span="2" align="center"> | |||
| <!-- TODO: check OpenKF version and update. --> | |||
| <t-tooltip content="Check OpenKF Version [TODO...]" > | |||
| <t-button theme="default" variant="outline" @click="checkUpdate"> | |||
| @@ -0,0 +1,91 @@ | |||
| <script setup lang="ts"> | |||
| import { reactive, ref, watch } from 'vue'; | |||
| import { onMounted, computed } from 'vue'; | |||
| import { | |||
| ConversationItem, | |||
| MessageItem, | |||
| } from "@/utils/open-im-sdk-wasm/types/entity"; | |||
| import { getTimeFromDate } from '@/utils/common/time'; | |||
| import useSessionStore from '@/store/openim_session' | |||
| const emit = defineEmits(['change']) | |||
| const isDark = ref(false); | |||
| const selectedSessionId = ref<string>(""); | |||
| const sessionList = ref<ConversationItem[]>([]); | |||
| const sessionStore = useSessionStore(); | |||
| onMounted(() => { | |||
| getAllSession(); | |||
| }); | |||
| watch(() => sessionStore.conversationList, (newVal, oldVal) => { | |||
| console.log("session changed", newVal, oldVal) | |||
| getAllSession(); | |||
| }); | |||
| const getAllSession = async () => { | |||
| sessionList.value = sessionStore.conversationList; | |||
| }; | |||
| // parseJSON like {textELem: {content: 'xxxx'}} | |||
| const parseTextELemInLastMsgJSON = (lastMsg:string) => { | |||
| const lastMsgObj = JSON.parse(lastMsg); | |||
| const textElem = lastMsgObj.textElem; | |||
| return textElem.content; | |||
| } | |||
| const changeSession = (conversation: ConversationItem) => { | |||
| // change bind value | |||
| selectedSessionId.value = conversation.conversationID; | |||
| console.log("Change conversationID", conversation.conversationID) | |||
| emit('change', conversation) | |||
| } | |||
| </script> | |||
| <template> | |||
| <div :class="[isDark ? 'dark' : 'light', 'session-list-wrapper']"> | |||
| <div class="session-title"> | |||
| <h2 class="welcome">OpenKF session</h2> | |||
| <h3 class="sub-title">Start your custom service session.</h3> | |||
| </div> | |||
| <div | |||
| v-for="item in sessionList" | |||
| :key="item.conversationID" | |||
| :class="[selectedSessionId === item.conversationID ? 'kf-select-session' : '', 'kf-session-list-card']" | |||
| > | |||
| <t-card :title="item.showName" hover-shadow @click="changeSession(item)"> | |||
| <t-row justify="space-between"> | |||
| <t-col :span="6" class="msg-ellipsis"> | |||
| {{ parseTextELemInLastMsgJSON(item.latestMsg) }} | |||
| </t-col> | |||
| <t-col :span="3" class="msg-time"> | |||
| {{ getTimeFromDate(item.latestMsgSendTime) }} | |||
| </t-col> | |||
| </t-row> | |||
| <template #actions> | |||
| <t-button | |||
| shape="circle" | |||
| theme="default" | |||
| size="small" | |||
| variant="outline" | |||
| ghost | |||
| ><t-icon name="close" | |||
| /></t-button> | |||
| </template> | |||
| </t-card> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script lang="ts"> | |||
| export default { | |||
| name: 'SessionList', | |||
| }; | |||
| </script> | |||
| <style lang="less" scoped> | |||
| @import url('../index.less'); | |||
| </style> | |||
| @@ -0,0 +1,200 @@ | |||
| <script setup lang="ts"> | |||
| import { ErrorCodes, reactive, ref, watch, nextTick } from 'vue'; | |||
| import { MessagePlugin } from 'tdesign-vue-next'; | |||
| import { Icon } from 'tdesign-icons-vue-next'; | |||
| import { getMyInfo, updateUserInfo } from '@/api/index/user'; | |||
| import { getMyCommunityInfo, updateCommunityInfo } from '@/api/index/community'; | |||
| import { updateStaffEnableStatus, deleteStaff } from '@/api/index/admin'; | |||
| import { UpdateUserInfoParam } from '@/api/request/userModel'; | |||
| import { UpdateCommunityParam } from '@/api/request/communityModel'; | |||
| import { onMounted, computed } from 'vue'; | |||
| import { getAvatarString } from '@/utils/common/string'; | |||
| import { OpenIM } from '@/api/openim'; | |||
| import { ConversationItem, MessageItem } from "@/utils/open-im-sdk-wasm/types/entity"; | |||
| import { SendMsgParams, GetAdvancedHistoryMsgParams } from "@/utils/open-im-sdk-wasm/types/params"; | |||
| import { getUserInfo } from '@/api/index/user'; | |||
| import { GetUserInfoParam } from '@/api/request/userModel'; | |||
| import { GetUserInfoResponse } from '@/api/response/userModel'; | |||
| import { CbEvents } from "@/utils/open-im-sdk-wasm/constant"; | |||
| import { WSEvent } from "@/utils/open-im-sdk-wasm/types/entity"; | |||
| import useUserStore from '@/store/user'; | |||
| import useMessageStore from '@/store/openim_message'; | |||
| const InitUserInfo:GetUserInfoResponse = { | |||
| uuid: '', | |||
| email: '', | |||
| nickname: '', | |||
| avatar: '', | |||
| description: '', | |||
| is_enable: false, | |||
| is_admin: false, | |||
| created_at: '', | |||
| }; | |||
| const isDark = ref(false); | |||
| const messageContent = ref('') | |||
| const messages = ref<MessageItem[]>() | |||
| const senderInfo = ref(useUserStore().userInfo) | |||
| const recvInfo = ref<GetUserInfoResponse>(InitUserInfo) | |||
| const messageStore = useMessageStore() | |||
| const props = defineProps({ | |||
| session: Object as () => ConversationItem, | |||
| }) | |||
| const getRecvIdFromSessionId = (sessionId: string) => { | |||
| const ids = sessionId.split('_'); | |||
| return ids[1] === senderInfo.value.uuid ? ids[2] : ids[1]; | |||
| } | |||
| watch(() => props.session, (newVal, oldVal) => { | |||
| console.log("session changed", newVal, oldVal) | |||
| fetchRecvInfo(); | |||
| getMsgHistory(); | |||
| scrollToBottom(); | |||
| }); | |||
| watch(() => messageStore, (newVal, oldVal) => { | |||
| console.log("messageStore changed", newVal, oldVal) | |||
| fetchRecvInfo(); | |||
| getMsgHistory(); | |||
| scrollToBottom(); | |||
| }); | |||
| const fetchRecvInfo = async () => { | |||
| try { | |||
| const params: GetUserInfoParam = { | |||
| uuid: getRecvIdFromSessionId(props.session?.conversationID || '') | |||
| } | |||
| const info = await getUserInfo(params); | |||
| recvInfo.value = info; | |||
| } catch (e) { | |||
| console.log(e); | |||
| MessagePlugin.error('Fetch recv info error!'); | |||
| } | |||
| } | |||
| const sendMessage = async () => { | |||
| try { | |||
| const msg = await OpenIM.createTextMessage<MessageItem>(messageContent.value) | |||
| const options: SendMsgParams = { | |||
| recvID: recvInfo.value.uuid, | |||
| groupID: "", | |||
| message: msg.data, | |||
| }; | |||
| try { | |||
| const res = await OpenIM.sendMessage(options); | |||
| messageStore.pushNewMessage(msg.data); | |||
| clearMsgContent(); | |||
| console.log(res); | |||
| } catch (e) { | |||
| console.log(e); | |||
| } | |||
| } catch (e) { | |||
| console.log(e); | |||
| MessagePlugin.error('Send message error!'); | |||
| } | |||
| }; | |||
| const clearMsgContent = () => { | |||
| messageContent.value = ''; | |||
| } | |||
| const scrollToBottom = () => { | |||
| const child = document.querySelector(".session-body") // 需要滚动的元素 | |||
| nextTick(() => { | |||
| child?.scrollTo({ | |||
| top: child.scrollHeight , | |||
| }) | |||
| }) | |||
| }; | |||
| // parseJSON like {textELem: {content: 'xxxx'}} | |||
| const parseTextELemInMsgJSON = (Msg:string) => { | |||
| console.log("Msg", Msg) | |||
| const MsgObj = JSON.parse(Msg); | |||
| const textElem = MsgObj.textElem; | |||
| return textElem.content; | |||
| } | |||
| const getMsgHistory = async () => { | |||
| const options:GetAdvancedHistoryMsgParams = { | |||
| conversationID: props.session?.conversationID as string, | |||
| startClientMsgID: "", | |||
| count: 100, // now only find 100 msgs | |||
| lastMinSeq: 0, | |||
| }; | |||
| try { | |||
| const { messageIDList } = await messageStore.getHistoryMessageListFromReq(options); | |||
| console.log("messageIDList", messageIDList); | |||
| messages.value = messageStore.historyMessageList; | |||
| } catch (e) { | |||
| console.log(e); | |||
| } | |||
| }; | |||
| // TODO: Add loading animation | |||
| const dataLoading = ref(false); | |||
| </script> | |||
| <template> | |||
| <div :class="[isDark ? 'dark' : 'light', 'session-window-wrapper']"> | |||
| <div class="session-header "> | |||
| <t-row align="middle"> | |||
| <t-col :span="11"> | |||
| <h2 class="welcome">{{ recvInfo.nickname }}</h2> | |||
| <h1 class="sub-title"> | |||
| UUID: {{ recvInfo.uuid }} IP: {{ 'xxx' }} Device: {{ 'slack' }} | |||
| </h1> | |||
| </t-col> | |||
| <t-col :span="1"> | |||
| <t-button theme="default" shape="circle" variant="outline"><icon name="refresh" /></t-button> | |||
| <t-tooltip content="Update config info"><t-button theme="default" variant="outline" shape="circle" ><icon name="close" /></t-button></t-tooltip> | |||
| </t-col> | |||
| </t-row> | |||
| </div> | |||
| <div class="session-body" ref="sessionBody"> | |||
| <div v-for="(item, idx) in messages" :key="idx" :class="[item.sendID == senderInfo.uuid ? 'send' : 'recv', 'message-card']"> | |||
| <div v-if="item.sendID == senderInfo.uuid" class="session-avatar"> | |||
| <t-avatar v-if="senderInfo.avatar === ''">{{ getAvatarString(senderInfo.nickname!) }}</t-avatar> | |||
| <t-avatar v-else :alt="getAvatarString(senderInfo.nickname!)" :hide-on-load-failed="true" :image="senderInfo.avatar" /> | |||
| </div> | |||
| <div v-else class="session-avatar"> | |||
| <t-avatar v-if="recvInfo.avatar === ''">{{ getAvatarString(recvInfo.nickname!) }}</t-avatar> | |||
| <t-avatar v-else :alt="getAvatarString(recvInfo.nickname!)" :hide-on-load-failed="true" :image="recvInfo.avatar" /> | |||
| </div> | |||
| <div class="session-content"> | |||
| <div class="session-content-text"> | |||
| <span>{{ item.textElem.content }}</span> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div class="session-input"> | |||
| <t-space size="24px" class="tools"> | |||
| <t-button shape="round" variant="outline" size="small"><t-icon name="tools" /></t-button> | |||
| </t-space> | |||
| <div class="inner"> | |||
| <t-textarea v-model="messageContent" placeholder="Enter your text here..." autofocus /> | |||
| <t-button class="send-button" @click="sendMessage">Send</t-button> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script lang="ts"> | |||
| export default { | |||
| name: 'SessionWindow', | |||
| }; | |||
| </script> | |||
| <style lang="less" scoped> | |||
| @import url('../index.less'); | |||
| </style> | |||
| @@ -0,0 +1,160 @@ | |||
| .light { | |||
| &.session-list-wrapper { | |||
| background-color: var(--td-brand-color-1); | |||
| } | |||
| &.session-window-wrapper { | |||
| background-color: var(--td-font-white-1); | |||
| } | |||
| } | |||
| .dark { | |||
| &.session-list-wrapper { | |||
| background-color: var(--td-bg-color-page); | |||
| } | |||
| &.session-window-wrapper { | |||
| background-color: var(--td-bg-color-page); | |||
| } | |||
| } | |||
| .session-window-wrapper { | |||
| height: 100vh; | |||
| display: flex; | |||
| flex-direction: column; | |||
| background-size: cover; | |||
| background-position: 100%; | |||
| position: relative; | |||
| } | |||
| .session-header { | |||
| position: relative; | |||
| padding: var(--td-comp-paddingTB-l) var(--td-comp-paddingLR-xl); | |||
| border-bottom: 1px solid var(--td-component-border); | |||
| .welcome { | |||
| font-size: var(--td-font-size-title-large); | |||
| font-weight: 700; | |||
| } | |||
| .sub-title { | |||
| font-size: var(--td-font-size-body-small); | |||
| font-weight: 400; | |||
| } | |||
| } | |||
| .session-container { | |||
| min-height: 100vh; | |||
| } | |||
| // session list start | |||
| .session-list-wrapper { | |||
| min-height: 100vh; | |||
| padding: var(--td-size-6); | |||
| display: flex; | |||
| flex-direction: column; | |||
| background-size: cover; | |||
| background-position: 100%; | |||
| position: relative; | |||
| } | |||
| .session-title { | |||
| position: relative; | |||
| padding-top: var(--td-size-7); | |||
| padding-bottom: var(--td-size-7); | |||
| .welcome { | |||
| font-size: var(--td-font-size-title-large); | |||
| font-weight: 700; | |||
| } | |||
| .sub-title { | |||
| font-size: var(--td-font-size-body-small); | |||
| font-weight: 400; | |||
| } | |||
| } | |||
| .kf-session-list-card { | |||
| & .msg-ellipsis { | |||
| text-overflow: ellipsis; | |||
| overflow: hidden; | |||
| color: var(--td-gray-color-6) | |||
| } | |||
| & .msg-time { | |||
| color: var(--td-gray-color-6) | |||
| } | |||
| } | |||
| .kf-selected-session { | |||
| & .t-card--bordered { | |||
| border: 2px solid var(--td-brand-color-5); | |||
| } | |||
| } | |||
| // session list end | |||
| // session window start | |||
| .session-body { | |||
| flex: 1 1; | |||
| padding: 20px; | |||
| overflow: auto; | |||
| & .message-card { | |||
| & .session-avatar { | |||
| margin-top: var(--td-comp-margin-s); | |||
| } | |||
| & .session-content { | |||
| margin-top: var(--td-comp-margin-s); | |||
| max-width: 60%; | |||
| width: fit-content; | |||
| padding: var(--td-comp-paddingLR-s) var(--td-comp-paddingTB-l); | |||
| border: 1px solid var(--td-component-border); | |||
| border-radius: var(--td-radius-large); | |||
| & .session-content-text {} | |||
| } | |||
| } | |||
| } | |||
| .send { | |||
| align-items: flex-end; | |||
| display: flex; | |||
| flex-direction: column; | |||
| & .session-content { | |||
| background-color: var(--td-brand-color-1); | |||
| } | |||
| } | |||
| .recv { | |||
| & .session-content { | |||
| background-color: var(--td-gray-color-1); | |||
| } | |||
| } | |||
| .session-input { | |||
| border-top: 1px solid var(--td-component-border); | |||
| width: 100%; | |||
| padding: var(--td-comp-paddingTB-s) var(--td-comp-paddingLR-l); | |||
| position: relative; | |||
| & .tools { | |||
| margin-bottom: var(--td-comp-margin-s); | |||
| } | |||
| & .inner { | |||
| & .t-textarea__inner { | |||
| height: 10vh; | |||
| } | |||
| & .send-button { | |||
| border-radius: var(--td-radius-large); | |||
| position: absolute; | |||
| right: var(--td-comp-size-s); | |||
| bottom: var(--td-comp-size-xxxs); | |||
| } | |||
| } | |||
| } | |||
| // session window end | |||
| @@ -1,7 +1,36 @@ | |||
| <script setup lang="ts"></script> | |||
| <script setup lang="ts"> | |||
| import { reactive, ref } from 'vue'; | |||
| import SessionList from './components/SessionList.vue'; | |||
| import SessionWindow from './components/SessionWindow.vue'; | |||
| import { | |||
| ConversationItem, | |||
| MessageItem, | |||
| } from "@/utils/open-im-sdk-wasm/types/entity"; | |||
| const session = ref<ConversationItem>(); | |||
| const changeSession = (conversation: ConversationItem) => { | |||
| session.value = conversation; | |||
| }; | |||
| </script> | |||
| <template> | |||
| <div>session</div> | |||
| <t-row class="session-container"> | |||
| <t-col :span="3" style="height: 100vh; overflow: auto;"> | |||
| <session-list @change="changeSession" /> | |||
| </t-col> | |||
| <t-col :span="9"> | |||
| <session-window :session="session" style="box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1)" /> | |||
| </t-col> | |||
| </t-row> | |||
| </template> | |||
| <style lang="scss" scoped></style> | |||
| <script lang="ts"> | |||
| export default { | |||
| name: 'SessionIndex', | |||
| }; | |||
| </script> | |||
| <style lang="less" scoped> | |||
| @import url('./index.less'); | |||
| </style> | |||