feat: refine embedded avatar authorization controls
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app" :class="{ 'embedded-shell': isInUniAppWebView }">
|
||||||
<router-view />
|
<router-view />
|
||||||
<!-- 底部导航栏 -->
|
<!-- 底部导航栏 -->
|
||||||
<nav class="bottom-nav" v-if="showNav">
|
<nav class="bottom-nav" v-if="showNav">
|
||||||
@@ -20,12 +20,13 @@
|
|||||||
<span class="nav-label">授权管理</span>
|
<span class="nav-label">授权管理</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="isInUniAppWebView"
|
||||||
class="nav-item"
|
class="nav-item"
|
||||||
:class="{ active: currentRoute === '/token/charge' }"
|
:class="{ active: currentRoute === '/token/charge' }"
|
||||||
@click="navigateTo('/token/charge')"
|
@click="navigateTo('/token/charge')"
|
||||||
>
|
>
|
||||||
<span class="nav-icon">💰</span>
|
<span class="nav-icon">💰</span>
|
||||||
<span class="nav-label">Token</span>
|
<span class="nav-label">充值购买</span>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,9 +35,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, watch } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { isInUniWebView } from '@/utils/uniapp-bridge'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const isInUniAppWebView = isInUniWebView()
|
||||||
|
|
||||||
const currentRoute = ref<string>(route.path)
|
const currentRoute = ref<string>(route.path)
|
||||||
const showNav = ref<boolean>(shouldShowNav(route.path))
|
const showNav = ref<boolean>(shouldShowNav(route.path))
|
||||||
@@ -81,6 +84,12 @@ onMounted(() => {
|
|||||||
padding-bottom: env(safe-area-inset-bottom);
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 原生 App / 微信小程序容器已经提供了自己的导航栏,H5 不再重复显示页头。 */
|
||||||
|
.embedded-shell .page-header,
|
||||||
|
.embedded-shell .chat-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* 底部导航栏 */
|
/* 底部导航栏 */
|
||||||
.bottom-nav {
|
.bottom-nav {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -1,324 +1,219 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="auth-manage-page">
|
<div class="auth-manage-page">
|
||||||
<!-- 顶部导航 -->
|
|
||||||
<header class="page-header">
|
<header class="page-header">
|
||||||
<button class="back-btn" @click="goBack">‹</button>
|
<button class="back-btn" @click="goBack">‹</button>
|
||||||
<h1 class="page-title">授权管理</h1>
|
<h1 class="page-title">授权管理</h1>
|
||||||
<button class="add-btn" @click="addAuthorization">+</button>
|
<span class="header-spacer"></span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- 授权列表 -->
|
<section class="intro-card">
|
||||||
<section class="auth-list" v-if="authList.length > 0">
|
<div class="intro-icon">🛡️</div>
|
||||||
<div class="auth-card" v-for="auth in authList" :key="auth.id">
|
<div>
|
||||||
<div class="auth-icon" :class="auth.targetType">
|
<h2>由你决定分身能做什么</h2>
|
||||||
{{ getAuthIcon(auth.targetType) }}
|
<p>授权后,数字分身会以你的会会身份参与广场互动;撤销后不再创建新的互动。</p>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="auth-list">
|
||||||
|
<div class="auth-card square-card">
|
||||||
|
<div class="auth-icon application">📰</div>
|
||||||
<div class="auth-info">
|
<div class="auth-info">
|
||||||
<h3 class="auth-name">{{ auth.targetName }}</h3>
|
<h3 class="auth-name">会会广场互动</h3>
|
||||||
|
<p class="auth-type">按照广场调度器设置自动执行</p>
|
||||||
|
<div class="auth-permissions">
|
||||||
|
<span class="permission-tag" v-for="permission in squareAuthorization.permissions" :key="permission">
|
||||||
|
{{ getPermissionText(permission) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="scheduler-note">功能已开发,当前仍在验证中;执行时段、互动间隔及各操作触发概率由运营后台统一控制。</p>
|
||||||
|
</div>
|
||||||
|
<div class="auth-actions">
|
||||||
|
<span class="auth-status" :class="squareAuthorization.status">
|
||||||
|
{{ squareAuthorization.status === 'active' ? '已授权' : '未授权' }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
class="auth-toggle-btn"
|
||||||
|
:class="{ revoke: squareAuthorization.status === 'active' }"
|
||||||
|
:disabled="saving"
|
||||||
|
@click="toggleSquareAuthorization"
|
||||||
|
>
|
||||||
|
{{ saving ? '处理中' : squareAuthorization.status === 'active' ? '撤销' : '授权' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="feature in unavailableFeatures" :key="feature.id" class="auth-card unavailable-card">
|
||||||
|
<div class="auth-icon application">{{ feature.icon }}</div>
|
||||||
|
<div class="auth-info">
|
||||||
|
<h3 class="auth-name">{{ feature.name }}</h3>
|
||||||
|
<p class="auth-type">{{ feature.description }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="auth-actions">
|
||||||
|
<span class="auth-status inactive">未开启</span>
|
||||||
|
<button class="auth-toggle-btn unavailable-toggle" @click="showUnavailableNotice">开启</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="auth-card" v-for="auth in otherAuthorizations" :key="auth.id">
|
||||||
|
<div class="auth-icon" :class="auth.targetType">{{ getAuthIcon(auth.targetType) }}</div>
|
||||||
|
<div class="auth-info">
|
||||||
|
<h3 class="auth-name">{{ getAuthTargetName(auth.targetName) }}</h3>
|
||||||
<p class="auth-type">{{ getAuthTypeText(auth.targetType) }}</p>
|
<p class="auth-type">{{ getAuthTypeText(auth.targetType) }}</p>
|
||||||
<div class="auth-permissions">
|
<div class="auth-permissions">
|
||||||
<span class="permission-tag" v-for="perm in auth.permissions" :key="perm">
|
<span class="permission-tag" v-for="permission in auth.permissions" :key="permission">
|
||||||
{{ getPermissionText(perm) }}
|
{{ getPermissionText(permission) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="auth-actions">
|
<div class="auth-actions">
|
||||||
<span class="auth-status" :class="auth.status">
|
<span class="auth-status" :class="auth.status">{{ auth.status === 'active' ? '已授权' : '已撤销' }}</span>
|
||||||
{{ auth.status === 'active' ? '已授权' : '已撤销' }}
|
<button class="auth-toggle-btn" :disabled="saving" @click="toggleExistingAuthorization(auth)">
|
||||||
</span>
|
|
||||||
<button class="auth-toggle-btn" @click="toggleAuth(auth)">
|
|
||||||
{{ auth.status === 'active' ? '撤销' : '授权' }}
|
{{ auth.status === 'active' ? '撤销' : '授权' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
|
||||||
<section class="empty-state" v-else>
|
|
||||||
<div class="empty-icon">🔐</div>
|
|
||||||
<h3 class="empty-title">暂无授权</h3>
|
|
||||||
<p class="empty-desc">授权其他用户或应用访问你的数字分身</p>
|
|
||||||
<button class="empty-btn" @click="addAuthorization">添加授权</button>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { computed, ref, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useAvatarStore } from '@/store/avatar'
|
import { useAvatarStore } from '@/store/avatar'
|
||||||
import { getAuthorizationList, updateAuthorization } from '@/api'
|
import {
|
||||||
|
getAuthorizationList,
|
||||||
|
updateAuthorization,
|
||||||
|
SQUARE_INTERACTION_TARGET_ID,
|
||||||
|
SQUARE_INTERACTION_PERMISSIONS,
|
||||||
|
type Authorization
|
||||||
|
} from '@/api'
|
||||||
import { pickAvatarId, unwrapListData } from '@/utils/avatar-page-data.js'
|
import { pickAvatarId, unwrapListData } from '@/utils/avatar-page-data.js'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const avatarStore = useAvatarStore()
|
const avatarStore = useAvatarStore()
|
||||||
|
const authList = ref<Authorization[]>([])
|
||||||
|
const saving = ref(false)
|
||||||
|
const UNAVAILABLE_MESSAGE = '该功能尚未向公众用户开放,如需使用请联系会会运营团队'
|
||||||
|
|
||||||
// 授权列表
|
const unavailableFeatures = [
|
||||||
const authList = ref<Array<{
|
{ id: 'make-friends', name: '交友与添加好友', description: '暂未开放', icon: '👥' },
|
||||||
id: string
|
{ id: 'start-chat', name: '主动发起聊天', description: '暂未开放', icon: '💬' },
|
||||||
targetType: 'user' | 'organization' | 'application'
|
{ id: 'publish-microblog', name: '发布微播内容', description: '暂未开放', icon: '📝' },
|
||||||
targetName: string
|
{ id: 'browse-square', name: '浏览会会广场', description: '暂未开放', icon: '🔎' }
|
||||||
permissions: string[]
|
]
|
||||||
status: 'active' | 'inactive'
|
|
||||||
}>>([])
|
const squareAuthorization = computed<Authorization>(() => {
|
||||||
|
return authList.value.find((item) => item.targetId === SQUARE_INTERACTION_TARGET_ID) || {
|
||||||
|
id: '',
|
||||||
|
avatarId: avatarStore.currentAvatarId || '',
|
||||||
|
targetType: 'application',
|
||||||
|
targetId: SQUARE_INTERACTION_TARGET_ID,
|
||||||
|
targetName: '会会广场互动',
|
||||||
|
permissions: [...SQUARE_INTERACTION_PERMISSIONS],
|
||||||
|
status: 'inactive',
|
||||||
|
createdAt: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const otherAuthorizations = computed(() =>
|
||||||
|
authList.value.filter((item) => item.targetId !== SQUARE_INTERACTION_TARGET_ID && !isUnavailableFeature(item.targetName))
|
||||||
|
)
|
||||||
|
|
||||||
|
const isUnavailableFeature = (name: string) => unavailableFeatures.some((feature) =>
|
||||||
|
name.replaceAll('发布微博内容', '发布微播内容') === feature.name
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentAvatarId = async () => {
|
||||||
|
if (!avatarStore.avatars.length) await avatarStore.loadAvatars()
|
||||||
|
return pickAvatarId(avatarStore.currentAvatarId, avatarStore.avatars)
|
||||||
|
}
|
||||||
|
|
||||||
// 从后端加载授权列表
|
|
||||||
const loadAuth = async () => {
|
const loadAuth = async () => {
|
||||||
try {
|
try {
|
||||||
if (!avatarStore.avatars.length) {
|
const avatarId = await currentAvatarId()
|
||||||
await avatarStore.loadAvatars()
|
authList.value = avatarId ? unwrapListData(await getAuthorizationList(avatarId)) : []
|
||||||
}
|
} catch (error) {
|
||||||
const avatarId = pickAvatarId(avatarStore.currentAvatarId, avatarStore.avatars)
|
console.error('加载授权失败', error)
|
||||||
if (!avatarId) {
|
|
||||||
authList.value = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const res: any = await getAuthorizationList(avatarId)
|
|
||||||
authList.value = unwrapListData(res)
|
|
||||||
} catch (e) {
|
|
||||||
console.error('加载授权失败', e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取授权图标
|
const toggleSquareAuthorization = async () => {
|
||||||
const getAuthIcon = (type: string) => {
|
const avatarId = await currentAvatarId()
|
||||||
const map: Record<string, string> = {
|
if (!avatarId || saving.value) return
|
||||||
'user': '👤',
|
saving.value = true
|
||||||
'organization': '🏢',
|
|
||||||
'application': '📱'
|
|
||||||
}
|
|
||||||
return map[type] || '🔑'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取授权类型文本
|
|
||||||
const getAuthTypeText = (type: string) => {
|
|
||||||
const map: Record<string, string> = {
|
|
||||||
'user': '用户',
|
|
||||||
'organization': '组织',
|
|
||||||
'application': '应用'
|
|
||||||
}
|
|
||||||
return map[type] || type
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取权限文本
|
|
||||||
const getPermissionText = (perm: string) => {
|
|
||||||
const map: Record<string, string> = {
|
|
||||||
'read': '读取',
|
|
||||||
'write': '写入',
|
|
||||||
'reply': '回复',
|
|
||||||
'edit': '编辑'
|
|
||||||
}
|
|
||||||
return map[perm] || perm
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换授权状态(写入后端)
|
|
||||||
const toggleAuth = async (auth: any) => {
|
|
||||||
const newStatus = auth.status === 'active' ? 'inactive' : 'active'
|
|
||||||
try {
|
try {
|
||||||
const avatarId = pickAvatarId(avatarStore.currentAvatarId, avatarStore.avatars)
|
authList.value = unwrapListData(await updateAuthorization(avatarId, {
|
||||||
if (!avatarId) return
|
id: squareAuthorization.value.id || undefined,
|
||||||
const res: any = await updateAuthorization(avatarId, {
|
targetId: SQUARE_INTERACTION_TARGET_ID,
|
||||||
id: auth.id,
|
targetName: '会会广场互动',
|
||||||
status: newStatus
|
targetType: 'application',
|
||||||
})
|
permissions: [...SQUARE_INTERACTION_PERMISSIONS],
|
||||||
authList.value = unwrapListData(res)
|
status: squareAuthorization.value.status === 'active' ? 'inactive' : 'active'
|
||||||
} catch (e) {
|
}))
|
||||||
|
} catch (error) {
|
||||||
alert('操作失败,请重试')
|
alert('操作失败,请重试')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加授权
|
const toggleExistingAuthorization = async (auth: Authorization) => {
|
||||||
const addAuthorization = () => {
|
const avatarId = await currentAvatarId()
|
||||||
alert('添加授权功能开发中...')
|
if (!avatarId || saving.value) return
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
authList.value = unwrapListData(await updateAuthorization(avatarId, {
|
||||||
|
id: auth.id,
|
||||||
|
status: auth.status === 'active' ? 'inactive' : 'active'
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
alert('操作失败,请重试')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回
|
const showUnavailableNotice = () => alert(UNAVAILABLE_MESSAGE)
|
||||||
const goBack = () => {
|
|
||||||
router.back()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
const getAuthIcon = (type: string) => ({ user: '👤', organization: '🏢', application: '📱' }[type] || '🔑')
|
||||||
loadAuth()
|
const getAuthTypeText = (type: string) => ({ user: '用户', organization: '组织', application: '应用' }[type] || type)
|
||||||
})
|
const getAuthTargetName = (name: string) => name.replaceAll('发布微博内容', '发布微播内容')
|
||||||
|
const getPermissionText = (permission: string) => ({
|
||||||
|
read: '读取', write: '写入', reply: '回复', edit: '编辑',
|
||||||
|
like: '点赞', collect: '收藏', comment: '评论'
|
||||||
|
}[permission] || permission)
|
||||||
|
const goBack = () => router.back()
|
||||||
|
|
||||||
|
onMounted(loadAuth)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.auth-manage-page {
|
.auth-manage-page { min-height: 100vh; background: #f8f9fa; padding-bottom: 80px; }
|
||||||
min-height: 100vh;
|
.page-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; background: white; border-bottom: 1px solid #edeef1; }
|
||||||
background: #F8F9FA;
|
.back-btn { background: none; border: none; font-size: 24px; cursor: pointer; padding: 4px 8px; color: #18191c; }
|
||||||
padding-bottom: 80px;
|
.page-title { font-size: 17px; font-weight: 600; margin: 0; color: #18191c; }
|
||||||
}
|
.header-spacer { width: 40px; }
|
||||||
|
.intro-card { display: flex; gap: 12px; margin: 20px 16px 0; padding: 16px; color: #7c2d12; background: #fff7ed; border: 1px solid #fed7aa; border-radius: 14px; }
|
||||||
/* 顶部导航 */
|
.intro-icon { font-size: 24px; }
|
||||||
.page-header {
|
.intro-card h2 { margin: 0 0 6px; font-size: 15px; }
|
||||||
display: flex;
|
.intro-card p { margin: 0; font-size: 12px; line-height: 1.6; color: #9a3412; }
|
||||||
align-items: center;
|
.auth-list { padding: 16px 16px 20px; display: flex; flex-direction: column; gap: 12px; }
|
||||||
justify-content: space-between;
|
.auth-card { display: flex; align-items: flex-start; gap: 12px; padding: 16px; background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, .05); }
|
||||||
padding: 16px 20px;
|
.square-card { border: 1px solid #ffedd5; }
|
||||||
background: white;
|
.unavailable-card { opacity: .78; }
|
||||||
border-bottom: 1px solid #EDEEF1;
|
.auth-icon { font-size: 24px; width: 48px; height: 48px; display: flex; align-items: center; justify-content: center; border-radius: 12px; background: #fff0e6; flex-shrink: 0; }
|
||||||
}
|
.auth-info { flex: 1; min-width: 0; }
|
||||||
|
.auth-name { font-size: 15px; font-weight: 600; margin: 0 0 4px; color: #18191c; }
|
||||||
.back-btn {
|
.auth-type { font-size: 12px; color: #9398ae; margin: 0 0 8px; }
|
||||||
background: none;
|
.auth-permissions { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||||
border: none;
|
.permission-tag { padding: 4px 8px; background: #f3f4f6; border-radius: 6px; font-size: 11px; color: #6b7280; }
|
||||||
font-size: 24px;
|
.scheduler-note { margin: 10px 0 0; color: #9398ae; font-size: 11px; line-height: 1.5; white-space: normal; overflow-wrap: anywhere; word-break: break-word; }
|
||||||
cursor: pointer;
|
.auth-actions { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; flex-shrink: 0; }
|
||||||
padding: 4px 8px;
|
.auth-status { font-size: 12px; font-weight: 500; }
|
||||||
color: #18191C;
|
.auth-status.active { color: #22c55e; }
|
||||||
}
|
.auth-status.inactive { color: #9398ae; }
|
||||||
|
.auth-toggle-btn { padding: 6px 12px; border-radius: 8px; font-size: 12px; font-weight: 500; cursor: pointer; border: none; background: #f97316; color: white; }
|
||||||
.page-title {
|
.auth-toggle-btn.revoke { color: #6b7280; background: #f3f4f6; }
|
||||||
font-size: 17px;
|
.unavailable-toggle { background: #d1d5db; color: #4b5563; }
|
||||||
font-weight: 600;
|
.auth-toggle-btn:disabled { cursor: not-allowed; opacity: .6; }
|
||||||
margin: 0;
|
|
||||||
color: #18191C;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-btn {
|
|
||||||
background: #F97316;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 50%;
|
|
||||||
font-size: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 授权列表 */
|
|
||||||
.auth-list {
|
|
||||||
padding: 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 16px;
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-icon {
|
|
||||||
font-size: 24px;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #FFF0E6;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-name {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 0 0 4px;
|
|
||||||
color: #18191C;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-type {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #9398AE;
|
|
||||||
margin: 0 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-permissions {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.permission-tag {
|
|
||||||
padding: 4px 8px;
|
|
||||||
background: #F3F4F6;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: #6B7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-status {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-status.active {
|
|
||||||
color: #22C55E;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-status.inactive {
|
|
||||||
color: #9398AE;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-toggle-btn {
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
background: #F97316;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空状态 */
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 80px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-icon {
|
|
||||||
font-size: 64px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-title {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #18191C;
|
|
||||||
margin: 0 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-desc {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #9398AE;
|
|
||||||
margin: 0 0 24px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-btn {
|
|
||||||
padding: 12px 32px;
|
|
||||||
background: #F97316;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user