759 lines
18 KiB
Vue
759 lines
18 KiB
Vue
<template>
|
||
<view class="page">
|
||
<view class="header-fixed">
|
||
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||
<view class="nav-bar">
|
||
<view class="nav-back" @tap="goBack">
|
||
<Icon name="arrowLeft" :size="40" color="#2D1B1B" />
|
||
</view>
|
||
<text class="nav-title">一起记</text>
|
||
<view :style="{ width: capsuleRight + 72 + 'px' }"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 创建/加入 Tab 卡片 -->
|
||
<view class="action-card">
|
||
<view class="tab-bar">
|
||
<view class="tab-item" :class="{ active: actionTab === 'create' }" @tap="actionTab = 'create'">
|
||
<text class="tab-text">创建群组</text>
|
||
</view>
|
||
<view class="tab-item" :class="{ active: actionTab === 'join' }" @tap="actionTab = 'join'">
|
||
<text class="tab-text">加入群组</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 创建 -->
|
||
<view v-if="actionTab === 'create'" class="action-body">
|
||
<input class="action-input" v-model="newGroupName" placeholder="输入群组名称" maxlength="100" />
|
||
<view class="action-btn" :class="{ disabled: saving }" @tap="handleCreate">
|
||
<Icon name="plus" :size="28" color="#FFFFFF" />
|
||
<text class="action-btn-text">创建</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 加入 -->
|
||
<view v-else class="action-body">
|
||
<input class="action-input" v-model="inviteCode" placeholder="输入 6 位邀请码" maxlength="6" />
|
||
<view class="action-btn" :class="{ disabled: saving }" @tap="handleJoin">
|
||
<text class="action-btn-text">加入</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 我的群组列表 -->
|
||
<view class="section-header">
|
||
<text class="section-title">我的群组</text>
|
||
</view>
|
||
|
||
<view v-if="groupStore.loading" class="loading-box">
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
|
||
<view v-else-if="groupStore.groups.length === 0" class="empty-box">
|
||
<Icon name="user" :size="48" color="#BFB3B3" />
|
||
<text class="empty-text">还没有加入任何群组</text>
|
||
</view>
|
||
|
||
<view v-else class="group-list">
|
||
<view v-for="group in groupStore.groups" :key="group.id" class="group-item">
|
||
<view class="group-icon">
|
||
<Icon name="user" :size="32" color="#FF8C69" />
|
||
</view>
|
||
<view class="group-info">
|
||
<view class="group-name-row">
|
||
<text class="group-name">{{ group.name }}</text>
|
||
<view class="role-badge" :style="{ background: group.role === 'owner' ? '#FFE8E0' : '#F0E0D6' }">
|
||
<text class="role-badge-text" :style="{ color: group.role === 'owner' ? '#FF8C69' : '#8B7E7E' }">{{ group.role === 'owner' ? '群主' : '成员' }}</text>
|
||
</view>
|
||
</view>
|
||
<text class="group-meta">{{ group.member_count }} 人</text>
|
||
|
||
<!-- 邀请码行 -->
|
||
<view class="invite-row">
|
||
<text class="invite-label">邀请码:</text>
|
||
<text class="invite-code" @tap="copyInviteCode(group.invite_code)">{{ group.invite_code }}</text>
|
||
<view class="invite-action" @tap="copyInviteCode(group.invite_code)">
|
||
<text class="invite-action-text">复制</text>
|
||
</view>
|
||
<!-- #ifdef MP-WEIXIN -->
|
||
<button class="invite-action share-btn" open-type="share" @tap="shareInviteCode = group.invite_code">
|
||
<text class="invite-action-text">分享</text>
|
||
</button>
|
||
<!-- #endif -->
|
||
<view v-if="group.role === 'owner'" class="invite-action" @tap="handleRefreshCode(group)">
|
||
<Icon name="refresh" :size="24" color="#8B7E7E" />
|
||
<text class="invite-action-text">刷新</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="group-actions">
|
||
<view v-if="group.role === 'owner'" class="group-btn manage" @tap="openMemberModal(group)">
|
||
<text class="group-btn-text manage-text">成员</text>
|
||
</view>
|
||
<view v-if="group.role === 'owner'" class="group-btn dissolve" @tap="handleDissolve(group)">
|
||
<text class="group-btn-text dissolve-text">解散</text>
|
||
</view>
|
||
<view v-else class="group-btn leave" @tap="handleLeave(group)">
|
||
<text class="group-btn-text leave-text">退出</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 成员管理弹窗 -->
|
||
<view class="modal-mask" v-if="showMemberModal" @tap="showMemberModal = false">
|
||
<view class="member-modal" @tap.stop>
|
||
<view class="modal-header">
|
||
<text class="modal-title">成员管理</text>
|
||
<view class="modal-close" @tap="showMemberModal = false">
|
||
<text class="modal-close-text">×</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="membersLoading" class="modal-loading">
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
|
||
<view v-else-if="members.length === 0" class="modal-empty">
|
||
<text class="loading-text">暂无成员</text>
|
||
</view>
|
||
|
||
<scroll-view v-else class="member-list" scroll-y>
|
||
<view v-for="m in members" :key="m.user_id" class="member-item">
|
||
<image class="member-avatar" :src="getAvatarUrl(m.avatar_url)" mode="aspectFill" />
|
||
<view class="member-info">
|
||
<text class="member-name">{{ m.nickname || '用户' + m.user_id }}</text>
|
||
<view class="member-role-wrap">
|
||
<text class="member-role-text" :style="{ color: m.role === 'owner' ? '#FF8C69' : '#BFB3B3' }">{{ m.role === 'owner' ? '群主' : '成员' }}</text>
|
||
</view>
|
||
</view>
|
||
<view v-if="m.role !== 'owner' && currentGroup?.role === 'owner'" class="member-remove" @tap="handleRemoveMember(m)">
|
||
<text class="member-remove-text">移除</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted } from 'vue'
|
||
import { onShow, onPullDownRefresh, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||
import { useGroupStore } from '@/stores/group'
|
||
import { useConfigStore } from '@/stores/config'
|
||
import { waitForReady } from '@/utils/app-ready'
|
||
import { statusBarHeight, capsuleRight } from '@/utils/system'
|
||
import { API_BASE } from '@/config'
|
||
import Icon from '@/components/Icon/Icon.vue'
|
||
import type { GroupMember, Group } from '@/api/group'
|
||
|
||
const groupStore = useGroupStore()
|
||
const configStore = useConfigStore()
|
||
|
||
const actionTab = ref<'create' | 'join'>('create')
|
||
const newGroupName = ref('')
|
||
const inviteCode = ref('')
|
||
const saving = ref(false)
|
||
const initialLoaded = ref(false)
|
||
const shareInviteCode = ref('') // 当前要分享的邀请码
|
||
|
||
// 成员管理弹窗
|
||
const showMemberModal = ref(false)
|
||
const membersLoading = ref(false)
|
||
const members = ref<GroupMember[]>([])
|
||
const currentGroup = ref<Group | null>(null)
|
||
|
||
onMounted(async () => {
|
||
await waitForReady()
|
||
await groupStore.fetchGroups()
|
||
initialLoaded.value = true
|
||
})
|
||
|
||
// 返回页面时静默刷新
|
||
onShow(() => {
|
||
if (initialLoaded.value) groupStore.fetchGroups()
|
||
})
|
||
|
||
onPullDownRefresh(async () => {
|
||
await groupStore.fetchGroups()
|
||
uni.stopPullDownRefresh()
|
||
})
|
||
|
||
// #ifdef MP-WEIXIN
|
||
onShareAppMessage(() => {
|
||
// 空值保护:通过右上角菜单触发时 invite_code 可能为空
|
||
const code = shareInviteCode.value
|
||
const path = code
|
||
? `/pages/group-manage/index?invite_code=${code}`
|
||
: '/pages/group-manage/index'
|
||
return {
|
||
title: '一起来记账吧!',
|
||
path
|
||
}
|
||
})
|
||
|
||
onShareTimeline(() => ({
|
||
title: `${configStore.appName} — 一起来记账吧!`
|
||
}))
|
||
// #endif
|
||
|
||
function goBack() {
|
||
uni.navigateBack()
|
||
}
|
||
|
||
function copyInviteCode(code: string) {
|
||
uni.setClipboardData({
|
||
data: code,
|
||
success: () => uni.showToast({ title: '已复制', icon: 'success' })
|
||
})
|
||
}
|
||
|
||
/** 获取头像 URL */
|
||
function getAvatarUrl(avatarUrl: string): string {
|
||
if (!avatarUrl) return ''
|
||
if (avatarUrl.startsWith('http') || avatarUrl.startsWith('blob:')) return avatarUrl
|
||
return `${API_BASE}/user/avatar/${avatarUrl}`
|
||
}
|
||
|
||
async function handleCreate() {
|
||
if (saving.value) return
|
||
const name = newGroupName.value.trim()
|
||
if (!name) {
|
||
uni.showToast({ title: '请输入群组名称', icon: 'none' })
|
||
return
|
||
}
|
||
saving.value = true
|
||
try {
|
||
const result = await groupStore.createGroup(name)
|
||
newGroupName.value = ''
|
||
uni.setClipboardData({
|
||
data: result.invite_code,
|
||
success: () => {
|
||
uni.showModal({
|
||
title: '创建成功',
|
||
content: `邀请码 ${result.invite_code} 已复制,分享给好友即可加入`,
|
||
showCancel: false
|
||
})
|
||
}
|
||
})
|
||
} catch (e: any) {
|
||
uni.showToast({ title: e.message || '创建失败', icon: 'none' })
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
async function handleJoin() {
|
||
if (saving.value) return
|
||
const code = inviteCode.value.trim().toUpperCase()
|
||
if (!code) {
|
||
uni.showToast({ title: '请输入邀请码', icon: 'none' })
|
||
return
|
||
}
|
||
saving.value = true
|
||
try {
|
||
const result = await groupStore.joinGroup(code)
|
||
inviteCode.value = ''
|
||
uni.showToast({ title: `已加入「${result.name}」`, icon: 'success' })
|
||
} catch (e: any) {
|
||
uni.showToast({ title: e.message || '加入失败', icon: 'none' })
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
/** 刷新邀请码 */
|
||
function handleRefreshCode(group: Group) {
|
||
uni.showModal({
|
||
title: '刷新邀请码',
|
||
content: `刷新后旧邀请码将失效,确定继续?`,
|
||
success: async (res) => {
|
||
if (res.confirm) {
|
||
try {
|
||
const result = await groupStore.refreshInviteCode(group.id)
|
||
uni.setClipboardData({
|
||
data: result.invite_code,
|
||
success: () => uni.showToast({ title: '新邀请码已复制', icon: 'success' })
|
||
})
|
||
} catch (e: any) {
|
||
uni.showToast({ title: e.message || '刷新失败', icon: 'none' })
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
/** 打开成员管理弹窗 */
|
||
async function openMemberModal(group: Group) {
|
||
currentGroup.value = group
|
||
showMemberModal.value = true
|
||
membersLoading.value = true
|
||
try {
|
||
members.value = await groupStore.fetchGroupMembers(group.id)
|
||
} catch (e: any) {
|
||
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
|
||
members.value = []
|
||
} finally {
|
||
membersLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 移除成员 */
|
||
function handleRemoveMember(member: GroupMember) {
|
||
if (!currentGroup.value) return
|
||
uni.showModal({
|
||
title: '移除成员',
|
||
content: `确定移除「${member.nickname || '该用户'}」?该成员的记录将回到个人账本。`,
|
||
success: async (res) => {
|
||
if (res.confirm) {
|
||
try {
|
||
await groupStore.removeMember(currentGroup.value!.id, member.user_id)
|
||
members.value = members.value.filter(m => m.user_id !== member.user_id)
|
||
uni.showToast({ title: '已移除', icon: 'success' })
|
||
} catch (e: any) {
|
||
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
function handleLeave(group: Group) {
|
||
uni.showModal({
|
||
title: '退出群组',
|
||
content: `确定退出「${group.name}」?你在该群组的记录将回到个人账本。`,
|
||
success: async (res) => {
|
||
if (res.confirm) {
|
||
try {
|
||
await groupStore.leaveGroup(group.id)
|
||
uni.showToast({ title: '已退出', icon: 'success' })
|
||
} catch (e: any) {
|
||
uni.showToast({ title: e.message || '退出失败', icon: 'none' })
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
function handleDissolve(group: Group) {
|
||
uni.showModal({
|
||
title: '解散群组',
|
||
content: `确定解散「${group.name}」?群组记录将回到各成员的个人账本。`,
|
||
success: async (res) => {
|
||
if (res.confirm) {
|
||
try {
|
||
await groupStore.deleteGroup(group.id)
|
||
uni.showToast({ title: '已解散', icon: 'success' })
|
||
} catch (e: any) {
|
||
uni.showToast({ title: e.message || '解散失败', icon: 'none' })
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import '@/styles/mixins.scss';
|
||
|
||
.page {
|
||
@include page-base;
|
||
}
|
||
|
||
.header-fixed {
|
||
@include sticky-header;
|
||
}
|
||
|
||
.status-bar { @include status-bar; }
|
||
|
||
.nav-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: $space-sm $space-xl $space-md;
|
||
}
|
||
|
||
.nav-back {
|
||
@include nav-back;
|
||
}
|
||
|
||
.nav-title {
|
||
flex: 1;
|
||
text-align: center;
|
||
font-size: $font-2xl;
|
||
font-weight: 600;
|
||
color: $text;
|
||
}
|
||
|
||
/* Tab 卡片 */
|
||
.action-card {
|
||
margin: $space-md $space-xl 0;
|
||
background: $surface;
|
||
border-radius: $radius-2xl;
|
||
border: 2rpx solid $border;
|
||
box-shadow: $shadow-md;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.tab-bar {
|
||
display: flex;
|
||
padding: $space-sm $space-sm 0;
|
||
gap: $space-xs;
|
||
}
|
||
|
||
.tab-item {
|
||
flex: 1;
|
||
height: 72rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: $radius-lg $radius-lg 0 0;
|
||
transition: all $transition-normal;
|
||
|
||
&.active {
|
||
background: $surface-warm;
|
||
|
||
.tab-text { color: $primary; }
|
||
}
|
||
}
|
||
|
||
.tab-text {
|
||
font-size: $font-lg;
|
||
font-weight: 500;
|
||
color: $text-sec;
|
||
}
|
||
|
||
.action-body {
|
||
display: flex;
|
||
gap: $space-sm;
|
||
padding: $space-md $space-lg $space-lg;
|
||
}
|
||
|
||
.action-input {
|
||
flex: 1;
|
||
height: 80rpx;
|
||
padding: 0 $space-md;
|
||
background: $bg;
|
||
border-radius: $radius-lg;
|
||
border: 2rpx solid $border;
|
||
font-size: $font-lg;
|
||
color: $text;
|
||
}
|
||
|
||
.action-btn {
|
||
height: 80rpx;
|
||
padding: 0 $space-lg;
|
||
background: linear-gradient(135deg, $primary, #E67355);
|
||
border-radius: $radius-lg;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: $space-xs;
|
||
|
||
&.disabled { opacity: 0.5; }
|
||
&:active { opacity: 0.8; }
|
||
}
|
||
|
||
.action-btn-text {
|
||
font-size: $font-lg;
|
||
font-weight: 600;
|
||
color: $surface;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 群组列表 */
|
||
.section-header {
|
||
padding: $space-xl $space-xl $space-sm;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: $font-lg;
|
||
font-weight: 500;
|
||
color: $text-sec;
|
||
}
|
||
|
||
.loading-box, .empty-box {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: $space-sm;
|
||
padding: 80rpx 0;
|
||
}
|
||
|
||
.loading-text, .empty-text {
|
||
font-size: $font-lg;
|
||
color: $text-muted;
|
||
}
|
||
|
||
.group-list {
|
||
padding: 0 $space-xl;
|
||
}
|
||
|
||
.group-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
padding: $font-lg $space-lg;
|
||
margin-bottom: $space-sm;
|
||
background: $surface;
|
||
border-radius: $radius-xl;
|
||
border: 2rpx solid $border;
|
||
box-shadow: $shadow-md;
|
||
gap: $space-md;
|
||
}
|
||
|
||
.group-icon {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border-radius: 50%;
|
||
background: $surface-warm;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
margin-top: 4rpx;
|
||
}
|
||
|
||
.group-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.group-name-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.group-name {
|
||
font-size: $font-lg;
|
||
font-weight: 600;
|
||
color: $text;
|
||
}
|
||
|
||
.role-badge {
|
||
padding: 2rpx 12rpx;
|
||
border-radius: $radius-xs;
|
||
}
|
||
|
||
.role-badge-text {
|
||
font-size: $font-xs;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.group-meta {
|
||
font-size: $font-md;
|
||
color: $text-muted;
|
||
display: block;
|
||
margin-top: 4rpx;
|
||
}
|
||
|
||
/* 邀请码行 */
|
||
.invite-row {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-top: 12rpx;
|
||
gap: $space-xs;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.invite-label {
|
||
font-size: $font-md;
|
||
color: $text-sec;
|
||
}
|
||
|
||
.invite-code {
|
||
font-size: $font-md;
|
||
font-weight: 600;
|
||
color: $primary;
|
||
font-family: 'Fredoka', monospace;
|
||
}
|
||
|
||
.invite-action {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4rpx;
|
||
padding: 4rpx 12rpx;
|
||
background: $bg;
|
||
border-radius: $radius-sm;
|
||
|
||
&:active { background: $border; }
|
||
}
|
||
|
||
.invite-action-text {
|
||
font-size: $font-sm;
|
||
color: $text-sec;
|
||
}
|
||
|
||
/* 分享按钮重置默认样式 */
|
||
.share-btn {
|
||
margin: 0;
|
||
padding: 4rpx 12rpx;
|
||
line-height: normal;
|
||
background: $bg;
|
||
border: none;
|
||
border-radius: $radius-sm;
|
||
font-size: inherit;
|
||
color: inherit;
|
||
|
||
&::after { border: none; }
|
||
}
|
||
|
||
/* 操作按钮 */
|
||
.group-actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: $space-xs;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.group-btn {
|
||
padding: 12rpx $space-md;
|
||
border-radius: $radius-md;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
&.manage { background: $surface-warm; }
|
||
&.leave { background: $surface-warm; }
|
||
&.dissolve { background: $danger-light; }
|
||
|
||
&:active { opacity: 0.7; }
|
||
}
|
||
|
||
.group-btn-text {
|
||
font-size: $font-md;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
|
||
&.manage-text { color: $primary; }
|
||
&.leave-text { color: $primary; }
|
||
&.dissolve-text { color: $danger; }
|
||
}
|
||
|
||
/* 成员管理弹窗 */
|
||
.modal-mask {
|
||
@include modal-mask;
|
||
}
|
||
|
||
.member-modal {
|
||
width: 100%;
|
||
max-height: 75vh;
|
||
background: $surface;
|
||
border-radius: $radius-2xl $radius-2xl 0 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
padding-bottom: env(safe-area-inset-bottom);
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: $space-lg $space-xl $space-md;
|
||
border-bottom: 2rpx solid $border;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: $font-xl;
|
||
font-weight: 600;
|
||
color: $text;
|
||
}
|
||
|
||
.modal-close {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 50%;
|
||
background: $border;
|
||
|
||
&:active { background: darken($border, 5%); }
|
||
}
|
||
|
||
.modal-close-text {
|
||
font-size: $font-2xl;
|
||
color: $text-sec;
|
||
line-height: 1;
|
||
}
|
||
|
||
.modal-loading, .modal-empty {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 80rpx 0;
|
||
}
|
||
|
||
.member-list {
|
||
max-height: 60vh;
|
||
padding: $space-sm $space-xl $space-xl;
|
||
width: 100%;
|
||
}
|
||
|
||
.member-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: $space-lg 0;
|
||
gap: $space-lg;
|
||
border-bottom: 2rpx solid #F8F0EB;
|
||
width: 100%;
|
||
|
||
&:last-child { border-bottom: none; }
|
||
}
|
||
|
||
.member-avatar {
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
border-radius: 50%;
|
||
background: $border;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.member-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.member-name {
|
||
font-size: $font-lg;
|
||
font-weight: 500;
|
||
color: $text;
|
||
display: block;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.member-role-wrap {
|
||
margin-top: 4rpx;
|
||
}
|
||
|
||
.member-role-text {
|
||
font-size: $font-sm;
|
||
}
|
||
|
||
.member-remove {
|
||
padding: 12rpx $space-md;
|
||
background: $danger-light;
|
||
border-radius: $radius-md;
|
||
flex-shrink: 0;
|
||
|
||
&:active { opacity: 0.7; }
|
||
}
|
||
|
||
.member-remove-text {
|
||
font-size: $font-md;
|
||
font-weight: 500;
|
||
color: $danger;
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.tab-item, .action-btn, .invite-action, .group-btn, .member-remove {
|
||
transition: none;
|
||
}
|
||
}
|
||
</style>
|