Skip to content

Commit ad69913

Browse files
hahaQWQhahaqwq
and
hahaqwq
authored
feat: new ui layout (#132)
Co-authored-by: hahaqwq <[email protected]>
1 parent 5108f3e commit ad69913

30 files changed

+1039
-312
lines changed

apps/frontend/auto-imports.d.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ declare global {
3131
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
3232
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
3333
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
34-
const createWebsocketV2Context: typeof import('./src/composables/useWebsocket')['createWebsocketV2Context']
34+
const createWebsocketV2Context: typeof import('./src/composables/useWebsocketV2')['createWebsocketV2Context']
3535
const customRef: typeof import('vue')['customRef']
3636
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
3737
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
@@ -307,6 +307,7 @@ declare global {
307307
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
308308
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
309309
const useWebsocket: typeof import('./src/composables/useWebsocket')['useWebsocket']
310+
const useWebsocketV2: typeof import('./src/composables/useWebsocketV2')['useWebsocketV2']
310311
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
311312
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
312313
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
@@ -338,6 +339,9 @@ declare global {
338339
// @ts-ignore
339340
export type { ClientSendEventFn, ClientCreateWsMessageFn } from './src/composables/useWebsocket'
340341
import('./src/composables/useWebsocket')
342+
// @ts-ignore
343+
export type { WsEventHandler, WsRegisterEventHandler } from './src/composables/useWebsocketV2'
344+
import('./src/composables/useWebsocketV2')
341345
}
342346

343347
// for vue template auto import
@@ -365,7 +369,7 @@ declare module 'vue' {
365369
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
366370
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
367371
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
368-
readonly createWebsocketV2Context: UnwrapRef<typeof import('./src/composables/useWebsocket')['createWebsocketV2Context']>
372+
readonly createWebsocketV2Context: UnwrapRef<typeof import('./src/composables/useWebsocketV2')['createWebsocketV2Context']>
369373
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
370374
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
371375
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
@@ -616,6 +620,7 @@ declare module 'vue' {
616620
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
617621
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
618622
readonly useWebsocket: UnwrapRef<typeof import('./src/composables/useWebsocket')['useWebsocket']>
623+
readonly useWebsocketV2: UnwrapRef<typeof import('./src/composables/useWebsocketV2')['useWebsocketV2']>
619624
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
620625
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
621626
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>

apps/frontend/components.d.ts

+9
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,32 @@ export {}
99
declare module 'vue' {
1010
export interface GlobalComponents {
1111
AlertCard: typeof import('./src/components/ui/AlertCard.vue')['default']
12+
ChatGroup: typeof import('./src/components/ui/ChatGroup.vue')['default']
1213
ChatSelector: typeof import('./src/components/ChatSelector.vue')['default']
1314
CheckboxGroup: typeof import('./src/components/ui/CheckboxGroup.vue')['default']
15+
ComposeMessage: typeof import('./src/components/ui/ComposeMessage.vue')['default']
1416
Dialog: typeof import('./src/components/ui/Dialog.vue')['default']
1517
DropdownMenu: typeof import('./src/components/ui/DropdownMenu.vue')['default']
18+
ForwardMessage: typeof import('./src/components/ui/ForwardMessage.vue')['default']
1619
HighlightText: typeof import('./src/components/ui/HighlightText.vue')['default']
1720
IconButton: typeof import('./src/components/ui/IconButton.vue')['default']
21+
ImageMessage: typeof import('./src/components/ui/ImageMessage.vue')['default']
1822
LoadingButton: typeof import('./src/components/ui/LoadingButton.vue')['default']
23+
MessageItem: typeof import('./src/components/ui/MessageItem.vue')['default']
1924
Pagination: typeof import('./src/components/ui/Pagination.vue')['default']
2025
ProgressBar: typeof import('./src/components/ui/ProgressBar.vue')['default']
2126
RadioGroup: typeof import('./src/components/ui/RadioGroup.vue')['default']
27+
RefMessage: typeof import('./src/components/ui/RefMessage.vue')['default']
2228
RouterLink: typeof import('vue-router')['RouterLink']
2329
RouterView: typeof import('vue-router')['RouterView']
2430
SearchSelect: typeof import('./src/components/ui/SearchSelect.vue')['default']
2531
SelectDropdown: typeof import('./src/components/ui/SelectDropdown.vue')['default']
32+
Settings: typeof import('./src/components/ui/Settings.vue')['default']
33+
SlotButton: typeof import('./src/components/ui/SlotButton.vue')['default']
2634
StatusBadge: typeof import('./src/components/ui/StatusBadge.vue')['default']
2735
StepIndicator: typeof import('./src/components/ui/StepIndicator.vue')['default']
2836
Switch: typeof import('./src/components/ui/Switch.vue')['default']
37+
TextMessage: typeof import('./src/components/ui/TextMessage.vue')['default']
2938
ThemeToggle: typeof import('./src/components/ThemeToggle.vue')['default']
3039
}
3140
}

apps/frontend/src/App.vue

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import DefaultLayout from './layouts/default.vue'
88
<div class="min-h-screen bg-white transition-all duration-300 ease-in-out dark:bg-gray-900">
99
<Toaster position="top-right" :expand="true" :rich-colors="true" />
1010
<DefaultLayout>
11-
<RouterView />
11+
<template #default="{ changeTitle, setActions, setHidden, setCollapsed }">
12+
<RouterView :change-title="changeTitle" :set-actions="setActions" :set-hidden="setHidden" :set-collapsed="setCollapsed" />
13+
</template>
1214
</DefaultLayout>
1315
</div>
1416
</template>

apps/frontend/src/components/ChatSelector.vue

+7-7
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ watch([selectedType, searchQuery], () => {
105105
<input
106106
v-model="searchQuery"
107107
type="text"
108-
class="w-full border border-gray-300 rounded-md px-4 py-2 dark:border-gray-600 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100 focus:ring-2 focus:ring-blue-500"
108+
class="w-full border border-secondary rounded-md bg-muted px-4 py-2 focus:border-primary focus:ring-2 focus:ring-primary"
109109
placeholder="Search"
110110
>
111111
</div>
@@ -118,20 +118,20 @@ watch([selectedType, searchQuery], () => {
118118
:key="chat.id"
119119
class="relative w-full flex active:scale-98 cursor-pointer items-center border rounded-lg p-4 text-left transition-all duration-300 space-x-3 hover:shadow-md hover:-translate-y-0.5"
120120
:class="{
121-
'border-blue-500 bg-blue-50 dark:bg-blue-900/20 shadow-md scale-102': isSelected(chat.id),
122-
'border-gray-200 dark:border-gray-700 hover:border-blue-300 dark:hover:border-blue-600': !isSelected(chat.id),
121+
'border-primary bg-primary/10 shadow-md scale-102': isSelected(chat.id),
122+
'border-secondary hover:border-primary': !isSelected(chat.id),
123123
}"
124124
@click="toggleSelection(chat.id)"
125125
>
126126
<div class="min-w-0 flex-1">
127127
<div class="focus:outline-none">
128-
<p class="flex items-center gap-2 text-sm text-gray-900 font-medium dark:text-gray-100">
128+
<p class="flex items-center gap-2 text-sm font-medium">
129129
{{ chat.title }}
130-
<span v-if="isSelected(chat.id)" class="text-blue-500 dark:text-blue-400">
130+
<span v-if="isSelected(chat.id)" class="text-primary">
131131
<div class="i-lucide-circle-check h-4 w-4" />
132132
</span>
133133
</p>
134-
<p class="truncate text-sm text-gray-500 dark:text-gray-400">
134+
<p class="truncate text-sm text-secondary-foreground">
135135
{{ chat.subtitle }}
136136
</p>
137137
</div>
@@ -148,7 +148,7 @@ watch([selectedType, searchQuery], () => {
148148
/>
149149

150150
<!-- No Results Message -->
151-
<div v-if="filteredChats.length === 0" class="py-8 text-center text-gray-500 dark:text-gray-400">
151+
<div v-if="filteredChats.length === 0" class="py-8 text-center text-secondary-foreground">
152152
No chats found
153153
</div>
154154
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<script setup lang="ts">
2+
import type { CoreDialog } from '@tg-search/core'
3+
import type { Chat } from '../../types/chat'
4+
import { ref } from 'vue'
5+
6+
const props = defineProps<{
7+
title: string
8+
avatar: string
9+
icon: string
10+
type: 'user' | 'group' | 'channel'
11+
chats: Chat[]
12+
selectedChatId?: number | null
13+
}>()
14+
15+
const emit = defineEmits<{
16+
(e: 'click', chat: CoreDialog): void
17+
}>()
18+
19+
const active = ref(true)
20+
function toggleActive() {
21+
active.value = !active.value
22+
}
23+
</script>
24+
25+
<template>
26+
<div class="flex cursor-pointer items-center justify-between rounded-md px-4 py-1 text-foreground transition-all duration-300 hover:bg-muted" @click="toggleActive">
27+
<div class="flex cursor-pointer items-center gap-1 text-sm font-medium">
28+
<div class="flex items-center gap-1">
29+
<div :class="props.icon" class="h-4 w-4" />
30+
<span class="select-none">{{ props.title }}</span>
31+
</div>
32+
</div>
33+
<div :class="active ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'" class="h-4 w-4 cursor-pointer" />
34+
</div>
35+
<ul v-show="active" class="scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent max-h-40 overflow-y-auto px-2 space-y-1">
36+
<li v-for="chat in chats" :key="chat.id" :class="{ 'bg-muted': chat.id === props.selectedChatId }" class="rounded-md transition-colors duration-100 hover:bg-muted">
37+
<SlotButton :text="chat.name.slice(0, 22) + (chat.name.length > 22 ? '...' : '')" @click="emit('click', chat)">
38+
<img :alt="`User ${chat.id}`" :src="`https://api.dicebear.com/6.x/bottts/svg?seed=${chat.name}`" class="h-full w-full select-none object-cover">
39+
</SlotButton>
40+
</li>
41+
</ul>
42+
</template>
43+
44+
<style scoped>
45+
/* 自定义滚动条样式 */
46+
.scrollbar-thin::-webkit-scrollbar {
47+
width: 4px;
48+
}
49+
50+
.scrollbar-thin::-webkit-scrollbar-track {
51+
background: transparent;
52+
}
53+
54+
.scrollbar-thin::-webkit-scrollbar-thumb {
55+
background-color: var(--un-color-muted);
56+
border-radius: 4px;
57+
}
58+
59+
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
60+
background-color: var(--un-color-muted-foreground);
61+
}
62+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
direction: 'row' | 'column'
4+
}>()
5+
</script>
6+
7+
<template>
8+
<div class="flex gap-2" :class="{ 'flex-row items-stretch': direction === 'row', 'flex-col items-start': direction === 'column' }">
9+
<div class="flex-1">
10+
<slot name="first" />
11+
</div>
12+
<!-- 分割线 -->
13+
<div
14+
class="transition-all duration-300 ease-in-out"
15+
:class="{
16+
'h-[2px] w-full bg-gray-100 dark:bg-gray-800 rounded-full my-2': direction === 'column',
17+
'min-h-full w-[2px] bg-gray-100 dark:bg-gray-800 rounded-full mx-2': direction === 'row',
18+
}"
19+
/>
20+
<div class="flex-1">
21+
<slot name="second" />
22+
</div>
23+
</div>
24+
</template>

apps/frontend/src/components/ui/Dialog.vue

+43-48
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const emit = defineEmits<{
1313
1414
const dialogRef = ref<HTMLDialogElement | null>(null)
1515
const contentRef = ref<HTMLDivElement | null>(null)
16+
const isVisible = ref(false)
1617
1718
const isOpen = computed({
1819
get: () => props.modelValue,
@@ -39,7 +40,7 @@ function closeWithAnimation() {
3940
contentRef.value.classList.add('dialog-content-leave')
4041
setTimeout(() => {
4142
isOpen.value = false
42-
}, 200)
43+
}, 100)
4344
}
4445
else {
4546
isOpen.value = false
@@ -60,13 +61,17 @@ function enableScroll() {
6061
6162
watch(isOpen, (value) => {
6263
if (value) {
64+
isVisible.value = true
6365
disableScroll()
6466
// 打开时重置动画类
6567
if (contentRef.value) {
6668
contentRef.value.classList.remove('dialog-content-leave')
6769
}
6870
}
6971
else {
72+
setTimeout(() => {
73+
isVisible.value = false
74+
}, 100)
7075
enableScroll()
7176
}
7277
})
@@ -83,74 +88,64 @@ onUnmounted(() => {
8388

8489
<template>
8590
<Teleport to="body">
86-
<Transition name="dialog">
87-
<div
88-
v-if="isOpen"
89-
ref="dialogRef"
90-
class="fixed inset-0 z-50 h-[100dvh] w-[100dvw] overflow-hidden border-black p-4 backdrop-blur-sm"
91-
:class="{ 'cursor-pointer': !persistent }"
92-
@click="handleOutsideClick"
93-
>
94-
<!-- 背景遮罩 -->
95-
<div class="absolute inset-0 h-full w-full bg-black/60 transition-opacity duration-300" />
96-
97-
<!-- 对话框内容 -->
98-
<div class="z-51 h-full w-full flex items-center justify-center">
91+
<div
92+
v-show="isVisible"
93+
ref="dialogRef"
94+
class="fixed inset-0 z-50 h-[100dvh] w-[100dvw] overflow-hidden p-4"
95+
:class="{ 'cursor-pointer': !persistent }"
96+
@click="handleOutsideClick"
97+
>
98+
<!-- 背景遮罩 -->
99+
<Transition name="fade">
100+
<div v-show="isVisible" class="absolute inset-0 h-full w-full backdrop-blur-sm" />
101+
</Transition>
102+
103+
<!-- 对话框内容 -->
104+
<div class="z-51 h-full w-full flex items-center justify-center">
105+
<Transition name="dialog">
99106
<div
107+
v-show="isVisible"
100108
ref="contentRef"
101-
class="dialog-content relative w-full cursor-default rounded-lg bg-white p-6 shadow-2xl ring-1 ring-gray-950/5 dark:bg-gray-800 dark:ring-white/10"
109+
class="dialog-content relative w-full cursor-default rounded-lg bg-popover p-6 shadow-2xl ring-1 ring-secondary/10"
102110
:style="{ maxWidth: maxWidth || '32rem' }"
103111
@click.stop
104112
>
105113
<slot />
106114
</div>
107-
</div>
115+
</Transition>
108116
</div>
109-
</Transition>
117+
</div>
110118
</Teleport>
111119
</template>
112120

113121
<style scoped>
114-
/* 背景动画 */
115-
.dialog-enter-active,
116-
.dialog-leave-active {
117-
transition: opacity 0.3s ease;
122+
/* 背景遮罩动画 */
123+
.fade-enter-active,
124+
.fade-leave-active {
125+
transition: opacity 0.2s ease-in-out;
118126
}
119127
120-
.dialog-enter-from,
121-
.dialog-leave-to {
128+
.fade-enter-from,
129+
.fade-leave-to {
122130
opacity: 0;
123131
}
124132
125-
/* 内容动画 */
126-
.dialog-content {
127-
animation: dialog-content-enter 0.3s cubic-bezier(0.16, 1, 0.3, 1);
128-
}
129-
130-
.dialog-content-leave {
131-
animation: dialog-content-leave 0.2s cubic-bezier(0.16, 1, 0.3, 1);
133+
/* 对话框内容动画 */
134+
.dialog-enter-active,
135+
.dialog-leave-active {
136+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
132137
}
133138
134-
@keyframes dialog-content-enter {
135-
from {
136-
opacity: 0;
137-
transform: scale(0.95) translateY(10px);
138-
}
139-
to {
140-
opacity: 1;
141-
transform: scale(1) translateY(0);
142-
}
139+
.dialog-enter-from,
140+
.dialog-leave-to {
141+
opacity: 0;
142+
transform: scale(0.95);
143143
}
144144
145-
@keyframes dialog-content-leave {
146-
from {
147-
opacity: 1;
148-
transform: scale(1) translateY(0);
149-
}
150-
to {
151-
opacity: 0;
152-
transform: scale(0.95) translateY(10px);
153-
}
145+
.dialog-enter-to,
146+
.dialog-leave-from {
147+
opacity: 1;
148+
transform: scale(1);
154149
}
155150
156151
dialog {

apps/frontend/src/components/ui/DropdownMenu.vue

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,22 @@ const isOpen = ref(false)
1313

1414
<template>
1515
<div
16-
class="relative"
16+
class="relative bg-popover"
1717
@mouseenter="isOpen = true"
1818
@mouseleave="isOpen = false"
1919
>
2020
<!-- Trigger button -->
2121
<IconButton
2222
:icon="icon"
2323
:aria-label="label"
24-
:class="{ 'text-blue-500': isOpen }"
24+
:class="{ 'text-primary': isOpen }"
2525
class="transition-colors duration-200"
2626
/>
2727

2828
<!-- Dropdown content -->
2929
<div
3030
v-show="isOpen"
31-
class="absolute right-0 z-50 w-48 border border-gray-200 rounded-md bg-white py-1 shadow-lg transition-all duration-200 ease-out dark:border-gray-700 dark:bg-gray-800"
31+
class="absolute right-0 z-50 w-48 border border-secondary rounded-md bg-card py-1 shadow-lg transition-all duration-200 ease-out"
3232
:class="[
3333
isOpen ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2 pointer-events-none',
3434
]"

0 commit comments

Comments
 (0)