Skip to content

Commit 9948e6c

Browse files
committed
fix: 优化分组选择与创建房间体验
1 parent fbd0793 commit 9948e6c

7 files changed

Lines changed: 153 additions & 15 deletions

File tree

‎backend/cmd/server/VERSION‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.2.70
1+
1.2.71

‎frontend/src/components/account-share/CreateRoomDialog.vue‎

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,18 +65,27 @@ const emit = defineEmits<Emits>()
6565

6666
<style>
6767
.create-room-dialog-panel {
68-
width: min(60rem, calc(100vw - 1rem));
69-
max-height: calc(100dvh - 1rem);
68+
width: min(84rem, calc(100vw - 2rem));
69+
max-width: min(84rem, calc(100vw - 2rem));
70+
max-height: calc(100dvh - 2rem);
7071
}
7172
7273
.create-room-dialog-body {
74+
display: flex;
75+
min-height: 0;
7376
padding: 0;
77+
overflow: hidden;
7478
overscroll-behavior: contain;
7579
}
7680
7781
.create-room-dialog-shell {
78-
min-height: calc(100dvh - 8rem);
82+
display: flex;
83+
min-height: 0;
84+
max-height: 100%;
85+
flex: 1 1 auto;
86+
flex-direction: column;
7987
background: rgb(248 250 252);
88+
overflow-y: auto;
8089
}
8190
8291
.create-room-dialog-intro {
@@ -97,9 +106,18 @@ const emit = defineEmits<Emits>()
97106
background: rgb(24 24 27);
98107
}
99108
109+
@media (max-width: 1023px) {
110+
.create-room-dialog-shell .create-room-submit-stage {
111+
position: sticky;
112+
bottom: 0;
113+
z-index: 10;
114+
box-shadow: 0 -0.5rem 1.25rem rgb(15 23 42 / 0.08);
115+
}
116+
}
117+
100118
@media (min-width: 640px) {
101119
.create-room-dialog-panel {
102-
max-height: 90dvh;
120+
max-height: 94dvh;
103121
}
104122
105123
.create-room-dialog-shell {
@@ -113,4 +131,56 @@ const emit = defineEmits<Emits>()
113131
padding: 1rem 1.5rem;
114132
}
115133
}
134+
135+
/* Keep the primary action in view on wide screens while the form scrolls. */
136+
@media (min-width: 1024px) {
137+
.create-room-dialog-shell .create-room-workspace {
138+
display: grid;
139+
grid-template-columns: minmax(0, 1fr) minmax(18rem, 23rem);
140+
align-items: start;
141+
gap: 1rem;
142+
padding: 1.25rem 1.5rem;
143+
}
144+
145+
.create-room-dialog-shell .create-room-form-flow,
146+
.create-room-dialog-shell .create-room-submit-stage {
147+
padding: 0;
148+
}
149+
150+
.create-room-dialog-shell .create-room-submit-stage {
151+
position: sticky;
152+
top: 0;
153+
border-top: 0;
154+
border-left: 1px solid rgb(226 232 240);
155+
background: transparent;
156+
padding-left: 1rem;
157+
}
158+
159+
.create-room-dialog-shell .create-room-submit-content {
160+
position: sticky;
161+
top: 0;
162+
grid-template-columns: minmax(0, 1fr);
163+
border: 1px solid rgb(191 219 254);
164+
border-radius: 0.875rem;
165+
background: rgb(239 246 255 / 0.72);
166+
padding: 1rem;
167+
}
168+
169+
.create-room-dialog-shell .create-room-submit-content > :not(.create-room-stage-heading):not(.create-room-submit-button) {
170+
grid-column: auto;
171+
}
172+
173+
.create-room-dialog-shell .create-room-submit-button {
174+
width: 100%;
175+
}
176+
177+
.dark .create-room-dialog-shell .create-room-submit-stage {
178+
border-left-color: rgb(63 63 70);
179+
}
180+
181+
.dark .create-room-dialog-shell .create-room-submit-content {
182+
border-color: rgb(30 64 175);
183+
background: rgb(30 64 175 / 0.14);
184+
}
185+
}
116186
</style>

‎frontend/src/components/common/GroupBadge.vue‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<!-- Platform logo -->
99
<PlatformIcon v-if="platform" :platform="platform" size="sm" class="flex-shrink-0" />
1010
<!-- Group name -->
11-
<span class="min-w-0 truncate">{{ name }}</span>
11+
<span class="min-w-0 truncate">{{ displayName }}</span>
1212
<span v-if="isPrivateScope" :class="['flex-shrink-0', scopeLabelClass]">
1313
{{ t('groups.private') }}
1414
</span>
@@ -31,6 +31,7 @@
3131
import { computed } from 'vue'
3232
import { useI18n } from 'vue-i18n'
3333
import type { SubscriptionType, GroupPlatform, GroupScope } from '@/types'
34+
import { displayText } from '@/utils/displayText'
3435
import PlatformIcon from './PlatformIcon.vue'
3536
3637
interface Props {
@@ -62,6 +63,8 @@ const props = withDefaults(defineProps<Props>(), {
6263
alwaysShowRate: false
6364
})
6465
66+
const displayName = computed(() => displayText(props.name))
67+
6568
const { t } = useI18n()
6669
6770
const isSubscription = computed(() => props.subscriptionType === 'subscription')

‎frontend/src/components/common/GroupOptionItem.vue‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
v-if="description"
2727
class="mt-1.5 w-full text-left text-xs leading-relaxed text-gray-500 dark:text-gray-400 line-clamp-2"
2828
>
29-
{{ description }}
29+
{{ displayDescription }}
3030
</span>
3131
</div>
3232

@@ -69,6 +69,7 @@ import type {
6969
GroupPlatform,
7070
GroupScope
7171
} from '@/types'
72+
import { displayText } from '@/utils/displayText'
7273
7374
interface Props {
7475
name: string
@@ -98,6 +99,8 @@ const props = withDefaults(defineProps<Props>(), {
9899
})
99100
const { t } = useI18n()
100101
102+
const displayDescription = computed(() => displayText(props.description))
103+
101104
const effectiveDisplayRate = computed(() => props.effectiveRateMultiplier ?? props.userRateMultiplier ?? null)
102105
103106
// Whether user has an effective rate different from default

‎frontend/src/utils/displayText.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/** Repair UTF-8 text that was accidentally decoded as Latin-1. */
2+
const mojibakeMarkers = /[ÃÂÆÇÐÑØÙÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ锟�]/g
3+
4+
const markerScore = (value: string): number =>
5+
(value.match(mojibakeMarkers)?.length ?? 0) + (value.match(/�/g)?.length ?? 0) * 3
6+
7+
export function displayText(value: string | null | undefined): string {
8+
if (!value || markerScore(value) === 0) return value ?? ''
9+
try {
10+
const bytes = Uint8Array.from(value, (character) => {
11+
const code = character.codePointAt(0) ?? 0
12+
return code <= 0xff ? code : 0x3f
13+
})
14+
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
15+
return markerScore(decoded) < markerScore(value) ? decoded : value
16+
} catch {
17+
return value
18+
}
19+
}

‎frontend/src/views/admin/GroupsView.vue‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@
130130
<template #cell-name="{ value, row }">
131131
<div class="flex min-w-0 items-center gap-2">
132132
<span class="truncate font-medium text-gray-900 dark:text-white">{{
133-
value
133+
displayText(String(value ?? ""))
134134
}}</span>
135135
<span
136136
v-if="row.scope === 'user_private'"
@@ -3217,7 +3217,7 @@
32173217
</div>
32183218
<div class="flex-1">
32193219
<div class="font-medium text-gray-900 dark:text-white">
3220-
{{ group.name }}
3220+
{{ displayText(group.name) }}
32213221
</div>
32223222
<div class="text-xs text-gray-500 dark:text-gray-400">
32233223
<span
@@ -3499,6 +3499,7 @@ import GroupCapacityBadge from "@/components/common/GroupCapacityBadge.vue";
34993499
import { VueDraggable } from "vue-draggable-plus";
35003500
import { createStableObjectKeyResolver } from "@/utils/stableObjectKey";
35013501
import { extractApiErrorMessage } from "@/utils/apiError";
3502+
import { displayText } from "@/utils/displayText";
35023503
import { useKeyedDebouncedSearch } from "@/composables/useKeyedDebouncedSearch";
35033504
import { getPersistedPageSize } from "@/composables/usePersistedPageSize";
35043505
import {

‎frontend/src/views/user/KeysView.vue‎

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,7 +1422,7 @@
14221422
:id="getGroupSelectorPopoverId(groupSelectorKeyId)"
14231423
ref="dropdownRef"
14241424
:data-ui-skin="uiSkin"
1425-
class="data-teleport-panel animate-in fade-in slide-in-from-top-2 fixed z-[100000020] flex max-h-[calc(100dvh-2rem)] min-w-0 w-[calc(100vw-2rem)] max-w-[380px] flex-col duration-200"
1425+
class="data-teleport-panel animate-in fade-in slide-in-from-top-2 fixed z-[100000020] flex max-h-[calc(100dvh-2rem)] min-w-0 w-[min(34rem,calc(100vw-2rem))] flex-col duration-200"
14261426
style="pointer-events: auto !important;"
14271427
role="dialog"
14281428
:aria-label="t('keys.groupSelectorLabel')"
@@ -1452,6 +1452,26 @@
14521452
@click.stop
14531453
/>
14541454
</div>
1455+
<div class="mt-2 flex items-center gap-1 overflow-x-auto pb-0.5" role="tablist" :aria-label="t('keys.groupSelectorLabel')">
1456+
<button
1457+
v-for="platform in groupPlatforms"
1458+
:key="platform.value || 'all'"
1459+
type="button"
1460+
role="tab"
1461+
:aria-selected="activeGroupPlatform === platform.value"
1462+
:class="[
1463+
'inline-flex min-h-8 shrink-0 items-center gap-1.5 rounded-lg px-2.5 text-xs font-medium transition-colors',
1464+
activeGroupPlatform === platform.value
1465+
? 'bg-brand-soft text-brand'
1466+
: 'text-content-muted hover:bg-surface-muted hover:text-content'
1467+
]"
1468+
@click="activeGroupPlatform = platform.value"
1469+
>
1470+
<PlatformIcon v-if="platform.value" :platform="platform.value" size="xs" />
1471+
<span>{{ platform.label }}</span>
1472+
<span class="rounded-full bg-black/5 px-1.5 py-0.5 text-[10px] dark:bg-white/10">{{ platform.count }}</span>
1473+
</button>
1474+
</div>
14551475
</div>
14561476
<!-- Group list -->
14571477
<div
@@ -1536,6 +1556,7 @@ import TablePageLayout from '@/components/layout/TablePageLayout.vue'
15361556
import GroupBadge from '@/components/common/GroupBadge.vue'
15371557
import GroupApiKeyBadge from '@/components/common/GroupApiKeyBadge.vue'
15381558
import GroupOptionItem from '@/components/common/GroupOptionItem.vue'
1559+
import PlatformIcon from '@/components/common/PlatformIcon.vue'
15391560
import type {
15401561
ApiKey,
15411562
ApiKeyGroupBadgeType,
@@ -1551,6 +1572,7 @@ import type { BatchApiKeyUsageStats } from '@/api/usage'
15511572
import type { AccountShareAPIKeyBindingStatus } from '@/api/accountShare'
15521573
import { formatDateTime } from '@/utils/format'
15531574
import { maskApiKey } from '@/utils/maskApiKey'
1575+
import { displayText } from '@/utils/displayText'
15541576
import { buildCcSwitchImportDeeplink } from '@/utils/ccswitchImport'
15551577
import { buildImagePlaygroundImportUrl } from '@/utils/imagePlaygroundImport'
15561578
import { extractApiErrorCode, extractApiErrorMessage, isAbortError } from '@/utils/apiError'
@@ -2012,8 +2034,8 @@ const groupOptions = computed<GroupOption[]>(() =>
20122034
const fallbackUserRate = userGroupRates.value[group.id] ?? null
20132035
return {
20142036
value: group.id,
2015-
label: group.name,
2016-
description: group.description,
2037+
label: displayText(group.name),
2038+
description: displayText(group.description),
20172039
rate: group.rate_multiplier,
20182040
userRate: fallbackUserRate,
20192041
effectiveRate: group.effective_rate_multiplier ?? fallbackUserRate,
@@ -2176,11 +2198,29 @@ const groupRoutingChanged = (
21762198
21772199
// Group dropdown search
21782200
const groupSearchQuery = ref('')
2201+
const activeGroupPlatform = ref<GroupPlatform | null>(null)
2202+
const groupPlatforms = computed(() => {
2203+
const platforms = Array.from(new Set(groupOptions.value.map((option) => option.platform)))
2204+
return [
2205+
{
2206+
value: null as GroupPlatform | null,
2207+
label: t('admin.groups.platforms.all'),
2208+
count: groupOptions.value.length
2209+
},
2210+
...platforms.map((platform) => ({
2211+
value: platform,
2212+
label: t(`admin.groups.platforms.${platform}`),
2213+
count: groupOptions.value.filter((option) => option.platform === platform).length
2214+
}))
2215+
]
2216+
})
21792217
const filteredGroupOptions = computed(() => {
21802218
const query = groupSearchQuery.value.trim().toLowerCase()
2181-
if (!query) return groupOptions.value
2219+
const platform = activeGroupPlatform.value
2220+
if (!query && !platform) return groupOptions.value
21822221
return groupOptions.value.filter((opt) => {
2183-
return opt.label.toLowerCase().includes(query) ||
2222+
if (platform && opt.platform !== platform) return false
2223+
return !query || opt.label.toLowerCase().includes(query) ||
21842224
(opt.description && opt.description.toLowerCase().includes(query))
21852225
})
21862226
})
@@ -2350,6 +2390,7 @@ const closeGroupSelectorPopover = (restoreFocus: boolean) => {
23502390
groupSelectorKeyId.value = null
23512391
dropdownPosition.value = null
23522392
groupSearchQuery.value = ''
2393+
activeGroupPlatform.value = null
23532394
if (restoreFocus) {
23542395
void nextTick(() => groupButtonRefs.value.get(keyId)?.focus())
23552396
}
@@ -2367,7 +2408,7 @@ const updateGroupSelectorPosition = (): boolean => {
23672408
const dropdownHeight = Math.min(dropdownRef.value?.offsetHeight || 400, maxDropdownHeight)
23682409
const spaceBelow = window.innerHeight - rect.bottom - viewportPadding
23692410
const spaceAbove = rect.top - viewportPadding
2370-
const dropdownWidth = Math.min(380, Math.max(0, window.innerWidth - viewportPadding * 2))
2411+
const dropdownWidth = Math.min(544, Math.max(0, window.innerWidth - viewportPadding * 2))
23712412
const maxLeft = Math.max(viewportPadding, window.innerWidth - dropdownWidth - viewportPadding)
23722413
const clampedLeft = Math.min(Math.max(rect.left, viewportPadding), maxLeft)
23732414
const preferredTop = spaceBelow >= dropdownHeight || spaceBelow >= spaceAbove
@@ -2395,6 +2436,7 @@ const openGroupSelector = async (key: ApiKey) => {
23952436
23962437
groupSelectorKeyId.value = key.id
23972438
groupSearchQuery.value = ''
2439+
activeGroupPlatform.value = null
23982440
if (!updateGroupSelectorPosition()) {
23992441
closeGroupSelectorPopover(false)
24002442
return

0 commit comments

Comments
 (0)