Spaces:
Paused
Paused
| <template> | |
| <div> | |
| <div class="flex items-center justify-between mb-6"> | |
| <h2 class="text-xl font-bold text-ink-950">Team 成员</h2> | |
| <button @click="fetchMembers" :disabled="loading" | |
| class="px-3 py-1.5 bg-surface hover:bg-ink-100 text-sm rounded-lg border border-hairline transition disabled:opacity-50 focus-ring text-ink-700"> | |
| {{ loading ? '加载中...' : '刷新' }} | |
| </button> | |
| </div> | |
| <div v-if="error" class="mb-4 px-4 py-3 rounded-lg text-sm bg-rose-50 text-rose-700 border border-rose-200"> | |
| {{ error }} | |
| </div> | |
| <div v-if="data" class="space-y-4"> | |
| <!-- 统计 --> | |
| <div class="flex gap-4 text-sm"> | |
| <span class="px-3 py-1.5 bg-ink-50 rounded-lg text-ink-600 border border-hairline">成员: <span class="text-ink-950 font-medium">{{ data.total }}</span></span> | |
| <span v-if="data.invites > 0" class="px-3 py-1.5 bg-amber-50 rounded-lg text-amber-800 border border-amber-200">待接受邀请: <span class="font-medium">{{ data.invites }}</span></span> | |
| </div> | |
| <!-- 成员表格 --> | |
| <div class="glass rounded-lg overflow-hidden"> | |
| <div class="overflow-x-auto"> | |
| <table class="w-full text-sm"> | |
| <thead> | |
| <tr class="text-ink-500 text-left border-b border-hairline"> | |
| <th class="px-4 py-3 font-medium">#</th> | |
| <th class="px-4 py-3 font-medium">邮箱</th> | |
| <th class="px-4 py-3 font-medium">角色</th> | |
| <th class="px-4 py-3 font-medium">类型</th> | |
| <th class="px-4 py-3 font-medium">来源</th> | |
| <th class="px-4 py-3 font-medium text-right">操作</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr v-for="(m, i) in data.members" :key="m.email + m.type" | |
| class="border-b border-hairline hover:bg-ink-50 transition"> | |
| <td class="px-4 py-3 text-ink-500">{{ i + 1 }}</td> | |
| <td class="px-4 py-3 font-mono text-xs">{{ m.email }}</td> | |
| <td class="px-4 py-3"> | |
| <span class="px-2 py-0.5 rounded text-xs font-medium" | |
| :class="{ | |
| 'bg-indigo-50 text-indigo-700': m.role === 'account-owner', | |
| 'bg-sky-50 text-sky-700': m.role === 'account-admin', | |
| 'bg-ink-50 text-ink-600': m.role !== 'account-owner' && m.role !== 'account-admin', | |
| }"> | |
| {{ m.role || 'member' }} | |
| </span> | |
| </td> | |
| <td class="px-4 py-3"> | |
| <span class="px-2 py-0.5 rounded text-xs font-medium" | |
| :class="m.type === 'invite' ? 'bg-amber-50 text-amber-700' : 'bg-emerald-50 text-emerald-700'"> | |
| {{ m.type === 'invite' ? '待接受' : '已加入' }} | |
| </span> | |
| </td> | |
| <td class="px-4 py-3"> | |
| <span class="text-xs" :class="m.is_local ? 'text-sky-700' : 'text-ink-500'"> | |
| {{ m.is_local ? '本地管理' : '外部' }} | |
| </span> | |
| </td> | |
| <td class="px-4 py-3 text-right"> | |
| <button | |
| v-if="m.role !== 'account-owner'" | |
| @click="removeMember(m)" | |
| :disabled="removingId === memberKey(m)" | |
| class="px-3 py-1.5 rounded-lg text-xs font-medium border transition" | |
| :class="removingId === memberKey(m) | |
| ? 'bg-ink-100 text-ink-400 border-hairline cursor-not-allowed' | |
| : 'bg-rose-50 text-rose-700 border-rose-200 hover:bg-rose-100'" | |
| > | |
| {{ removingId === memberKey(m) ? '处理中...' : '移出' }} | |
| </button> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Loading --> | |
| <div v-else-if="loading" class="glass-soft rounded-lg h-64 shimmer-bg"></div> | |
| <!-- Empty --> | |
| <div v-else class="text-center text-ink-500 py-12"> | |
| 点击「刷新」加载 Team 成员列表 | |
| </div> | |
| </div> | |
| </template> | |
| <script setup> | |
| import { ref, onMounted } from 'vue' | |
| import { api } from '../api.js' | |
| const data = ref(null) | |
| const loading = ref(false) | |
| const error = ref('') | |
| const removingId = ref('') | |
| const CACHE_KEY = 'autoteam_team_members' | |
| function loadCache() { | |
| try { | |
| const raw = localStorage.getItem(CACHE_KEY) | |
| if (raw) { | |
| const cached = JSON.parse(raw) | |
| // 缓存 10 分钟有效 | |
| if (cached.time && Date.now() - cached.time < 600000) { | |
| return cached.data | |
| } | |
| } | |
| } catch {} | |
| return null | |
| } | |
| function saveCache(d) { | |
| try { | |
| localStorage.setItem(CACHE_KEY, JSON.stringify({ data: d, time: Date.now() })) | |
| } catch {} | |
| } | |
| function memberKey(member) { | |
| return `${member.type}:${member.user_id}:${member.email}` | |
| } | |
| async function fetchMembers() { | |
| loading.value = true | |
| error.value = '' | |
| try { | |
| data.value = await api.getTeamMembers() | |
| saveCache(data.value) | |
| } catch (e) { | |
| error.value = e.message | |
| } finally { | |
| loading.value = false | |
| } | |
| } | |
| async function removeMember(member) { | |
| const actionText = member.type === 'invite' ? '取消邀请' : '移出 Team' | |
| const ok = window.confirm(`确认${actionText} ${member.email}?`) | |
| if (!ok) return | |
| removingId.value = memberKey(member) | |
| error.value = '' | |
| try { | |
| await api.removeTeamMember({ | |
| email: member.email, | |
| user_id: member.user_id, | |
| type: member.type, | |
| }) | |
| try { | |
| localStorage.removeItem(CACHE_KEY) | |
| } catch {} | |
| await fetchMembers() | |
| } catch (e) { | |
| error.value = e.message | |
| } finally { | |
| removingId.value = '' | |
| } | |
| } | |
| onMounted(() => { | |
| const cached = loadCache() | |
| if (cached) { | |
| data.value = cached | |
| } else { | |
| fetchMembers() | |
| } | |
| }) | |
| </script> | |