feat: UI redesign v0.0.2 — color unification, navigation improvements, mobile support
- Unify color palette from mixed green/blue/purple to consistent green theme - Sidebar: add text labels to create/join group buttons for discoverability - Header: add quick action buttons (create group, join group, notifications) - Mobile: add hamburger menu with slide-out sidebar and overlay - Home: add prominent CTA buttons, onboarding card for empty state - Join group dialog: add search-by-name mode alongside existing ID lookup - Games library: inline group selector dropdown instead of external selection Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getGroup, joinGroup, createJoinRequest } from '@/api/groups'
|
||||
import { getGroup, joinGroup, createJoinRequest, searchGroups } from '@/api/groups'
|
||||
import { useGroupStore } from '@/stores/group'
|
||||
import type { Group } from '@/types'
|
||||
import { Search, Link } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
@@ -12,46 +14,76 @@ const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
}>()
|
||||
|
||||
const groupStore = useGroupStore()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const searchKeyword = ref('')
|
||||
const searchResults = ref<Group[]>([])
|
||||
const groupId = ref('')
|
||||
const groupInfo = ref<Group | null>(null)
|
||||
const selectedGroup = ref<Group | null>(null)
|
||||
const loading = ref(false)
|
||||
const joining = ref(false)
|
||||
const mode = ref<'search' | 'id'>('search')
|
||||
|
||||
async function searchGroup() {
|
||||
// 已加入的群组 ID 集合
|
||||
const joinedGroupIds = computed(() => new Set(groupStore.groups.map(g => g.id)))
|
||||
|
||||
async function handleSearch() {
|
||||
const kw = searchKeyword.value.trim()
|
||||
if (!kw) {
|
||||
ElMessage.warning('请输入群组名称')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
searchResults.value = await searchGroups(kw)
|
||||
if (searchResults.value.length === 0) {
|
||||
ElMessage.info('未找到匹配的群组')
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('搜索失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function searchById() {
|
||||
if (!groupId.value.trim()) {
|
||||
ElMessage.warning('请输入群组 ID')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
groupInfo.value = await getGroup(groupId.value.trim())
|
||||
selectedGroup.value = await getGroup(groupId.value.trim())
|
||||
} catch {
|
||||
groupInfo.value = null
|
||||
selectedGroup.value = null
|
||||
ElMessage.error('未找到该群组')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectFromResults(group: Group) {
|
||||
selectedGroup.value = group
|
||||
}
|
||||
|
||||
async function handleJoin() {
|
||||
if (!groupInfo.value) return
|
||||
if (!selectedGroup.value) return
|
||||
joining.value = true
|
||||
try {
|
||||
if (groupInfo.value.requireApproval) {
|
||||
await createJoinRequest(groupInfo.value.id)
|
||||
if (selectedGroup.value.requireApproval) {
|
||||
await createJoinRequest(selectedGroup.value.id)
|
||||
ElMessage.success('已提交加入申请,等待群主审核')
|
||||
} else {
|
||||
await joinGroup(groupInfo.value.id)
|
||||
await joinGroup(selectedGroup.value.id)
|
||||
ElMessage.success('已成功加入群组')
|
||||
}
|
||||
visible.value = false
|
||||
groupId.value = ''
|
||||
groupInfo.value = null
|
||||
reset()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
} finally {
|
||||
@@ -60,36 +92,124 @@ async function handleJoin() {
|
||||
}
|
||||
|
||||
function reset() {
|
||||
searchKeyword.value = ''
|
||||
searchResults.value = []
|
||||
groupId.value = ''
|
||||
groupInfo.value = null
|
||||
selectedGroup.value = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="加入群组" width="440px" @close="reset">
|
||||
<el-dialog v-model="visible" title="加入群组" width="480px" @close="reset">
|
||||
<div class="join-form">
|
||||
<div class="form-field">
|
||||
<label>群组 ID</label>
|
||||
<div class="search-row">
|
||||
<el-input v-model="groupId" placeholder="输入群主分享的群组 ID" />
|
||||
<el-button type="primary" :loading="loading" @click="searchGroup">查找</el-button>
|
||||
</div>
|
||||
<!-- 切换模式 -->
|
||||
<div class="mode-tabs">
|
||||
<button
|
||||
class="mode-tab"
|
||||
:class="{ 'mode-tab--active': mode === 'search' }"
|
||||
@click="mode = 'search'; selectedGroup = null"
|
||||
>
|
||||
<el-icon><Search /></el-icon> 按名称搜索
|
||||
</button>
|
||||
<button
|
||||
class="mode-tab"
|
||||
:class="{ 'mode-tab--active': mode === 'id' }"
|
||||
@click="mode = 'id'; selectedGroup = null"
|
||||
>
|
||||
<el-icon><Link /></el-icon> 按 ID 查找
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="groupInfo" class="group-preview">
|
||||
<div class="preview-header">
|
||||
<h3 class="preview-name">{{ groupInfo.name }}</h3>
|
||||
<span class="preview-members">{{ groupInfo.members?.length || 0 }} / {{ groupInfo.maxMembers }} 人</span>
|
||||
<!-- 搜索模式 -->
|
||||
<template v-if="mode === 'search'">
|
||||
<div class="form-field">
|
||||
<div class="search-row">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="输入群组名称搜索..."
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<el-button type="primary" :loading="loading" @click="handleSearch">搜索</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="groupInfo.description" class="preview-desc">{{ groupInfo.description }}</p>
|
||||
<div class="approval-tag">
|
||||
<span v-if="groupInfo.requireApproval" class="tag-approval">需要审核</span>
|
||||
<span v-else class="tag-direct">直接加入</span>
|
||||
|
||||
<!-- 搜索结果列表 -->
|
||||
<div v-if="searchResults.length > 0 && !selectedGroup" class="results-list">
|
||||
<div
|
||||
v-for="group in searchResults"
|
||||
:key="group.id"
|
||||
class="result-item"
|
||||
@click="selectFromResults(group)"
|
||||
>
|
||||
<div class="result-info">
|
||||
<span class="result-name">{{ group.name }}</span>
|
||||
<span class="result-meta">{{ group.members?.length || 0 }} / {{ group.maxMembers }} 人</span>
|
||||
</div>
|
||||
<div class="result-tags">
|
||||
<span v-if="joinedGroupIds.has(group.id)" class="tag-joined">已加入</span>
|
||||
<span v-else-if="group.requireApproval" class="tag-approval">需审核</span>
|
||||
<span v-else class="tag-direct">可直接加入</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-button type="primary" :loading="joining" @click="handleJoin" style="width:100%; margin-top: 12px;">
|
||||
{{ groupInfo.requireApproval ? '申请加入' : '加入群组' }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 已选中群组预览 -->
|
||||
<div v-if="selectedGroup && mode === 'search'" class="group-preview">
|
||||
<div class="preview-header">
|
||||
<h3 class="preview-name">{{ selectedGroup.name }}</h3>
|
||||
<span class="preview-members">{{ selectedGroup.members?.length || 0 }} / {{ selectedGroup.maxMembers }} 人</span>
|
||||
</div>
|
||||
<p v-if="selectedGroup.description" class="preview-desc">{{ selectedGroup.description }}</p>
|
||||
<div class="approval-tag">
|
||||
<span v-if="joinedGroupIds.has(selectedGroup.id)" class="tag-joined">已加入</span>
|
||||
<span v-else-if="selectedGroup.requireApproval" class="tag-approval">需要审核</span>
|
||||
<span v-else class="tag-direct">直接加入</span>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="!joinedGroupIds.has(selectedGroup.id)"
|
||||
type="primary"
|
||||
:loading="joining"
|
||||
@click="handleJoin"
|
||||
style="width: 100%; margin-top: 12px;"
|
||||
>
|
||||
{{ selectedGroup.requireApproval ? '申请加入' : '加入群组' }}
|
||||
</el-button>
|
||||
<button class="back-btn" @click="selectedGroup = null">返回搜索结果</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ID 查找模式 -->
|
||||
<template v-if="mode === 'id'">
|
||||
<div class="form-field">
|
||||
<label>群组 ID</label>
|
||||
<div class="search-row">
|
||||
<el-input v-model="groupId" placeholder="输入群主分享的群组 ID" />
|
||||
<el-button type="primary" :loading="loading" @click="searchById">查找</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedGroup && mode === 'id'" class="group-preview">
|
||||
<div class="preview-header">
|
||||
<h3 class="preview-name">{{ selectedGroup.name }}</h3>
|
||||
<span class="preview-members">{{ selectedGroup.members?.length || 0 }} / {{ selectedGroup.maxMembers }} 人</span>
|
||||
</div>
|
||||
<p v-if="selectedGroup.description" class="preview-desc">{{ selectedGroup.description }}</p>
|
||||
<div class="approval-tag">
|
||||
<span v-if="joinedGroupIds.has(selectedGroup.id)" class="tag-joined">已加入</span>
|
||||
<span v-else-if="selectedGroup.requireApproval" class="tag-approval">需要审核</span>
|
||||
<span v-else class="tag-direct">直接加入</span>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="!joinedGroupIds.has(selectedGroup.id)"
|
||||
type="primary"
|
||||
:loading="joining"
|
||||
@click="handleJoin"
|
||||
style="width: 100%; margin-top: 12px;"
|
||||
>
|
||||
{{ selectedGroup.requireApproval ? '申请加入' : '加入群组' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -98,7 +218,39 @@ function reset() {
|
||||
.join-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* ── 模式切换 ── */
|
||||
.mode-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
background: var(--gg-bg);
|
||||
border-radius: var(--gg-radius-sm);
|
||||
}
|
||||
|
||||
.mode-tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 9px 12px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--gg-text-secondary);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.mode-tab--active {
|
||||
background: var(--gg-bg-card);
|
||||
color: var(--gg-primary);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.form-field {
|
||||
@@ -121,6 +273,54 @@ function reset() {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ── 搜索结果列表 ── */
|
||||
.results-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 14px;
|
||||
background: var(--gg-bg);
|
||||
border: 1px solid var(--gg-border);
|
||||
border-radius: var(--gg-radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.result-item:hover {
|
||||
border-color: var(--gg-primary);
|
||||
background: rgba(5, 150, 105, 0.04);
|
||||
}
|
||||
|
||||
.result-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.result-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--gg-text);
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
font-size: 12px;
|
||||
color: var(--gg-text-muted);
|
||||
}
|
||||
|
||||
.result-tags {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── 群组预览 ── */
|
||||
.group-preview {
|
||||
padding: 16px;
|
||||
background: var(--gg-bg-card);
|
||||
@@ -174,4 +374,32 @@ function reset() {
|
||||
background: rgba(5, 150, 105, 0.12);
|
||||
color: var(--gg-primary);
|
||||
}
|
||||
|
||||
.tag-joined {
|
||||
display: inline-block;
|
||||
padding: 3px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: var(--gg-bg-elevated);
|
||||
color: var(--gg-text-muted);
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--gg-text-muted);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
border-radius: var(--gg-radius-sm);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
color: var(--gg-text);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user