mirror of
https://github.com/Tencent/WeKnora.git
synced 2025-11-25 03:15:00 +08:00
feat(ui): Add tenant info
This commit is contained in:
@@ -8,7 +8,6 @@ server {
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: http:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; frame-ancestors 'self';" always;
|
||||
|
||||
# 错误日志配置
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
|
||||
@@ -74,8 +74,13 @@ export interface UserInfo {
|
||||
export interface TenantInfo {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
api_key: string
|
||||
status?: string
|
||||
business?: string
|
||||
owner_id: string
|
||||
storage_quota?: number
|
||||
storage_used?: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
knowledge_bases?: KnowledgeBaseInfo[]
|
||||
|
||||
@@ -3,50 +3,71 @@
|
||||
<div class="logo_box">
|
||||
<img class="logo" src="@/assets/img/weknora.png" alt="">
|
||||
</div>
|
||||
<div class="menu_box" v-for="(item, index) in menuArr" :key="index">
|
||||
<div @click="gotopage(item.path)"
|
||||
@mouseenter="mouseenteMenu(item.path)" @mouseleave="mouseleaveMenu(item.path)"
|
||||
:class="['menu_item', item.childrenPath && item.childrenPath == currentpath ? 'menu_item_c_active' : item.path == currentpath ? 'menu_item_active' : '']">
|
||||
<div class="menu_item-box">
|
||||
<div class="menu_icon">
|
||||
<img class="icon" :src="getImgSrc(item.icon == 'zhishiku' ? knowledgeIcon : item.icon == 'setting' ? settingIcon : item.icon == 'logout' ? logoutIcon : prefixIcon)" alt="">
|
||||
|
||||
<!-- 上半部分:知识库和对话 -->
|
||||
<div class="menu_top">
|
||||
<div class="menu_box" :class="{ 'has-submenu': item.children }" v-for="(item, index) in topMenuItems" :key="index">
|
||||
<div @click="gotopage(item.path)"
|
||||
@mouseenter="mouseenteMenu(item.path)" @mouseleave="mouseleaveMenu(item.path)"
|
||||
:class="['menu_item', item.childrenPath && item.childrenPath == currentpath ? 'menu_item_c_active' : item.path == currentpath ? 'menu_item_active' : '']">
|
||||
<div class="menu_item-box">
|
||||
<div class="menu_icon">
|
||||
<img class="icon" :src="getImgSrc(item.icon == 'zhishiku' ? knowledgeIcon : item.icon == 'setting' ? settingIcon : item.icon == 'logout' ? logoutIcon : item.icon == 'tenant' ? tenantIcon : prefixIcon)" alt="">
|
||||
</div>
|
||||
<span class="menu_title">{{ item.title }}</span>
|
||||
</div>
|
||||
<span class="menu_title">{{ item.title }}</span>
|
||||
<t-popup overlayInnerClassName="upload-popup" class="placement top center" content="上传知识"
|
||||
placement="top" show-arrow destroy-on-close>
|
||||
<div class="upload-file-wrap" @click="uploadFile" variant="outline"
|
||||
v-if="item.path == 'knowledgeBase'">
|
||||
<img class="upload-file-icon" :class="[item.path == currentpath ? 'active-upload' : '']"
|
||||
:src="getImgSrc(fileAddIcon)" alt="">
|
||||
</div>
|
||||
</t-popup>
|
||||
</div>
|
||||
<t-popup overlayInnerClassName="upload-popup" class="placement top center" content="上传知识"
|
||||
placement="top" show-arrow destroy-on-close>
|
||||
<div class="upload-file-wrap" @click="uploadFile" variant="outline"
|
||||
v-if="item.path == 'knowledgeBase'">
|
||||
<img class="upload-file-icon" :class="[item.path == currentpath ? 'active-upload' : '']"
|
||||
:src="getImgSrc(fileAddIcon)" alt="">
|
||||
</div>
|
||||
</t-popup>
|
||||
</div>
|
||||
<div ref="submenuscrollContainer" @scroll="handleScroll" class="submenu" v-if="item.children">
|
||||
<div class="submenu_item_p" v-for="(subitem, subindex) in item.children" :key="subindex"
|
||||
@click="gotopage(subitem.path)">
|
||||
<div :class="['submenu_item', currentSecondpath == subitem.path ? 'submenu_item_active' : '']"
|
||||
@mouseenter="mouseenteBotDownr(subindex)" @mouseleave="mouseleaveBotDown">
|
||||
<i v-if="currentSecondpath == subitem.path" class="dot"></i>
|
||||
<span class="submenu_title"
|
||||
:style="currentSecondpath == subitem.path ? 'margin-left:14px;max-width:160px;' : 'margin-left:18px;max-width:173px;'">
|
||||
{{ subitem.title }}
|
||||
</span>
|
||||
<t-popup v-model:visible="subitem.isMore" @overlay-click="delCard(subindex, subitem)"
|
||||
@visible-change="onVisibleChange" overlayClassName="del-menu-popup" trigger="click"
|
||||
destroy-on-close placement="top-left">
|
||||
<div v-if="(activeSubmenu == subindex) || (currentSecondpath == subitem.path) || subitem.isMore"
|
||||
@click.stop="openMore(subindex)" variant="outline" class="menu-more-wrap">
|
||||
<t-icon name="ellipsis" class="menu-more" />
|
||||
</div>
|
||||
<template #content>
|
||||
<span class="del_submenu">删除记录</span>
|
||||
</template>
|
||||
</t-popup>
|
||||
<div ref="submenuscrollContainer" @scroll="handleScroll" class="submenu" v-if="item.children">
|
||||
<div class="submenu_item_p" v-for="(subitem, subindex) in item.children" :key="subindex"
|
||||
@click="gotopage(subitem.path)">
|
||||
<div :class="['submenu_item', currentSecondpath == subitem.path ? 'submenu_item_active' : '']"
|
||||
@mouseenter="mouseenteBotDownr(subindex)" @mouseleave="mouseleaveBotDown">
|
||||
<i v-if="currentSecondpath == subitem.path" class="dot"></i>
|
||||
<span class="submenu_title"
|
||||
:style="currentSecondpath == subitem.path ? 'margin-left:14px;max-width:160px;' : 'margin-left:18px;max-width:173px;'">
|
||||
{{ subitem.title }}
|
||||
</span>
|
||||
<t-popup v-model:visible="subitem.isMore" @overlay-click="delCard(subindex, subitem)"
|
||||
@visible-change="onVisibleChange" overlayClassName="del-menu-popup" trigger="click"
|
||||
destroy-on-close placement="top-left">
|
||||
<div v-if="(activeSubmenu == subindex) || (currentSecondpath == subitem.path) || subitem.isMore"
|
||||
@click.stop="openMore(subindex)" variant="outline" class="menu-more-wrap">
|
||||
<t-icon name="ellipsis" class="menu-more" />
|
||||
</div>
|
||||
<template #content>
|
||||
<span class="del_submenu">删除记录</span>
|
||||
</template>
|
||||
</t-popup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下半部分:账户信息、系统设置、退出登录 -->
|
||||
<div class="menu_bottom">
|
||||
<div class="menu_box" v-for="(item, index) in bottomMenuItems" :key="'bottom-' + index">
|
||||
<div @click="gotopage(item.path)"
|
||||
@mouseenter="mouseenteMenu(item.path)" @mouseleave="mouseleaveMenu(item.path)"
|
||||
:class="['menu_item', item.childrenPath && item.childrenPath == currentpath ? 'menu_item_c_active' : item.path == currentpath ? 'menu_item_active' : '']">
|
||||
<div class="menu_item-box">
|
||||
<div class="menu_icon">
|
||||
<img class="icon" :src="getImgSrc(item.icon == 'zhishiku' ? knowledgeIcon : item.icon == 'setting' ? settingIcon : item.icon == 'logout' ? logoutIcon : item.icon == 'tenant' ? tenantIcon : prefixIcon)" alt="">
|
||||
</div>
|
||||
<span class="menu_title">{{ item.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="file" @change="upload" style="display: none" ref="uploadInput"
|
||||
accept=".pdf,.docx,.doc,.txt,.md,.jpg,.jpeg,.png" />
|
||||
</div>
|
||||
@@ -78,6 +99,19 @@ const totalPages = computed(() => Math.ceil(total.value / page_size.value));
|
||||
const hasMore = computed(() => currentPage.value < totalPages.value);
|
||||
const { menuArr } = storeToRefs(usemenuStore);
|
||||
let activeSubmenu = ref(-1);
|
||||
|
||||
// 分离上下两部分菜单
|
||||
const topMenuItems = computed(() => {
|
||||
return menuArr.value.filter(item =>
|
||||
item.path === 'knowledgeBase' || item.path === 'creatChat'
|
||||
);
|
||||
});
|
||||
|
||||
const bottomMenuItems = computed(() => {
|
||||
return menuArr.value.filter(item =>
|
||||
item.path !== 'knowledgeBase' && item.path !== 'creatChat'
|
||||
);
|
||||
});
|
||||
const loading = ref(false)
|
||||
const uploadFile = () => {
|
||||
uploadInput.value.click()
|
||||
@@ -167,12 +201,14 @@ let knowledgeIcon = ref('zhishiku-green.svg');
|
||||
let prefixIcon = ref('prefixIcon.svg');
|
||||
let settingIcon = ref('setting.svg');
|
||||
let logoutIcon = ref('logout.svg');
|
||||
let tenantIcon = ref('setting.svg'); // 暂时使用setting图标
|
||||
let pathPrefix = ref(route.name)
|
||||
const getIcon = (path) => {
|
||||
fileAddIcon.value = path == 'knowledgeBase' ? 'file-add-green.svg' : 'file-add.svg';
|
||||
knowledgeIcon.value = path == 'knowledgeBase' ? 'zhishiku-green.svg' : 'zhishiku.svg';
|
||||
prefixIcon.value = path == 'creatChat' ? 'prefixIcon-green.svg' : path == 'knowledgeBase' ? 'prefixIcon-grey.svg' : 'prefixIcon.svg';
|
||||
settingIcon.value = path == 'settings' ? 'setting-green.svg' : 'setting.svg';
|
||||
tenantIcon.value = path == 'tenant' ? 'setting-green.svg' : 'setting.svg'; // 暂时使用setting图标
|
||||
logoutIcon.value = 'logout.svg';
|
||||
}
|
||||
getIcon(route.name)
|
||||
@@ -221,6 +257,10 @@ const mouseleaveMenu = (path) => {
|
||||
padding: 8px;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.logo_box {
|
||||
height: 80px;
|
||||
@@ -250,9 +290,28 @@ const mouseleaveMenu = (path) => {
|
||||
line-height: 21.7px;
|
||||
}
|
||||
|
||||
.menu_top {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.menu_bottom {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.menu_box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.has-submenu {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -358,12 +417,11 @@ const mouseleaveMenu = (path) => {
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
height: calc(98vh - 276px);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.submenu_item_p {
|
||||
|
||||
@@ -35,6 +35,12 @@ const router = createRouter({
|
||||
component: () => import("../views/platform/index.vue"),
|
||||
meta: { requiresInit: true, requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: "tenant",
|
||||
name: "tenant",
|
||||
component: () => import("../views/tenant/TenantInfo.vue"),
|
||||
meta: { requiresInit: true, requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: "knowledgeBase",
|
||||
name: "knowledgeBase",
|
||||
|
||||
@@ -13,6 +13,7 @@ export const useMenuStore = defineStore('menuStore', {
|
||||
childrenPath: 'chat',
|
||||
children: reactive<object[]>([]),
|
||||
},
|
||||
{ title: '账户信息', icon: 'tenant', path: 'tenant' },
|
||||
{ title: '系统设置', icon: 'setting', path: 'settings' },
|
||||
{ title: '退出登录', icon: 'logout', path: 'logout' }
|
||||
]),
|
||||
@@ -31,7 +32,7 @@ export const useMenuStore = defineStore('menuStore', {
|
||||
this.menuArr[1].children?.unshift(item)
|
||||
},
|
||||
updatasessionTitle(session_id: string, title: string) {
|
||||
this.menuArr[1].children?.forEach(item => {
|
||||
this.menuArr[1].children?.forEach((item: any) => {
|
||||
if (item.id == session_id) {
|
||||
item.title = title;
|
||||
item.isNoTitle = false;
|
||||
|
||||
496
frontend/src/views/tenant/TenantInfo.vue
Normal file
496
frontend/src/views/tenant/TenantInfo.vue
Normal file
@@ -0,0 +1,496 @@
|
||||
<template>
|
||||
<div class="tenant-info-container">
|
||||
<div class="tenant-header">
|
||||
<h2>账户信息</h2>
|
||||
<p class="tenant-subtitle">查看和管理您的用户账户和租户配置信息</p>
|
||||
</div>
|
||||
|
||||
<div class="tenant-content" v-if="!loading && !error">
|
||||
<!-- 用户信息卡片 -->
|
||||
<t-card class="info-card" :bordered="false">
|
||||
<template #header>
|
||||
<div class="card-title">用户信息</div>
|
||||
</template>
|
||||
<div class="info-content">
|
||||
<t-descriptions :column="1" layout="vertical">
|
||||
<t-descriptions-item label="用户 ID">
|
||||
{{ userInfo?.id }}
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item label="用户名">
|
||||
{{ userInfo?.username }}
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item label="邮箱">
|
||||
{{ userInfo?.email }}
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item label="创建时间">
|
||||
{{ formatDate(userInfo?.created_at) }}
|
||||
</t-descriptions-item>
|
||||
</t-descriptions>
|
||||
</div>
|
||||
</t-card>
|
||||
|
||||
<!-- 租户信息卡片 -->
|
||||
<t-card class="info-card" :bordered="false">
|
||||
<template #header>
|
||||
<div class="card-title">租户信息</div>
|
||||
</template>
|
||||
<div class="info-content">
|
||||
<t-descriptions :column="1" layout="vertical">
|
||||
<t-descriptions-item label="租户 ID">
|
||||
{{ tenantInfo?.id }}
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item label="租户名称">
|
||||
{{ tenantInfo?.name }}
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item label="描述">
|
||||
{{ tenantInfo?.description || '暂无描述' }}
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item label="业务">
|
||||
{{ tenantInfo?.business || '暂无' }}
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item label="状态">
|
||||
<t-tag
|
||||
:theme="getStatusTheme(tenantInfo?.status)"
|
||||
variant="light"
|
||||
>
|
||||
{{ getStatusText(tenantInfo?.status) }}
|
||||
</t-tag>
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item label="创建时间">
|
||||
{{ formatDate(tenantInfo?.created_at) }}
|
||||
</t-descriptions-item>
|
||||
</t-descriptions>
|
||||
</div>
|
||||
</t-card>
|
||||
|
||||
<!-- API Key 卡片 -->
|
||||
<t-card class="info-card" :bordered="false">
|
||||
<template #header>
|
||||
<div class="card-header-with-actions">
|
||||
<div class="card-title">API Key</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="api-key-content">
|
||||
<t-input
|
||||
v-model="displayApiKey"
|
||||
readonly
|
||||
class="api-key-input"
|
||||
:type="showApiKey ? 'text' : 'password'"
|
||||
/>
|
||||
<t-alert theme="warning" :close="false" class="api-warning">
|
||||
<template #icon>
|
||||
<t-icon name="error-circle" />
|
||||
</template>
|
||||
请妥善保管您的 API Key,不要在公共场所或代码仓库中暴露
|
||||
</t-alert>
|
||||
</div>
|
||||
</t-card>
|
||||
|
||||
<!-- 存储信息卡片 -->
|
||||
<t-card
|
||||
class="info-card"
|
||||
:bordered="false"
|
||||
v-if="tenantInfo?.storage_quota !== undefined"
|
||||
>
|
||||
<template #header>
|
||||
<div class="card-title">存储信息</div>
|
||||
</template>
|
||||
<div class="storage-content">
|
||||
<t-descriptions :column="1" layout="vertical">
|
||||
<t-descriptions-item label="存储配额">
|
||||
{{ formatBytes(tenantInfo.storage_quota) }}
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item label="已使用">
|
||||
{{ formatBytes(tenantInfo.storage_used || 0) }}
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item label="使用率">
|
||||
<div class="usage-info">
|
||||
<span class="usage-text">{{ getUsagePercentage() }}%</span>
|
||||
<t-progress
|
||||
:percentage="getUsagePercentage()"
|
||||
:show-info="false"
|
||||
size="medium"
|
||||
:theme="getUsagePercentage() > 80 ? 'warning' : 'success'"
|
||||
/>
|
||||
</div>
|
||||
</t-descriptions-item>
|
||||
</t-descriptions>
|
||||
</div>
|
||||
</t-card>
|
||||
|
||||
<!-- API 开发文档卡片 -->
|
||||
<t-card class="info-card" :bordered="false">
|
||||
<template #header>
|
||||
<div class="card-title">API 开发文档</div>
|
||||
</template>
|
||||
<div class="doc-content">
|
||||
<p class="doc-description">使用您的 API Key 开始开发,查看完整的 API 文档和示例代码。</p>
|
||||
<t-space class="doc-actions">
|
||||
<t-button
|
||||
theme="primary"
|
||||
@click="openApiDoc"
|
||||
>
|
||||
<template #icon>
|
||||
<t-icon name="link" />
|
||||
</template>
|
||||
查看 API 文档
|
||||
</t-button>
|
||||
</t-space>
|
||||
|
||||
</div>
|
||||
</t-card>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<t-loading size="large" />
|
||||
<p class="loading-text">正在加载账户信息...</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-if="error" class="error-container">
|
||||
<t-result theme="error" title="加载失败" :description="error">
|
||||
<template #extra>
|
||||
<t-button theme="primary" @click="loadTenantInfo">重试</t-button>
|
||||
</template>
|
||||
</t-result>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { getCurrentUser, type TenantInfo, type UserInfo } from '@/api/auth'
|
||||
|
||||
// 响应式数据
|
||||
const tenantInfo = ref<TenantInfo | null>(null)
|
||||
const userInfo = ref<UserInfo | null>(null)
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
const showApiKey = ref(false)
|
||||
const showApiExample = ref(false)
|
||||
|
||||
// API 基础 URL
|
||||
const apiBaseUrl = window.location.origin
|
||||
|
||||
// 计算属性
|
||||
const displayApiKey = computed(() => {
|
||||
if (!tenantInfo.value?.api_key) return ''
|
||||
return tenantInfo.value.api_key
|
||||
})
|
||||
|
||||
// API示例代码
|
||||
const apiExampleCode = computed(() => {
|
||||
return `curl -X GET "${apiBaseUrl}/api/v1/tenants/${tenantInfo.value?.id}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "X-API-Key: ${tenantInfo.value?.api_key}"`
|
||||
})
|
||||
|
||||
// 方法
|
||||
const loadTenantInfo = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
const response = await getCurrentUser()
|
||||
if (response.success && response.data) {
|
||||
userInfo.value = response.data.user
|
||||
tenantInfo.value = response.data.tenant
|
||||
} else {
|
||||
error.value = response.message || '获取用户信息失败'
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || '网络错误,请稍后重试'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const toggleApiKeyVisibility = () => {
|
||||
showApiKey.value = !showApiKey.value
|
||||
}
|
||||
|
||||
const copyApiKey = async () => {
|
||||
if (!tenantInfo.value?.api_key) return
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(tenantInfo.value.api_key)
|
||||
// 使用TDesign的消息组件
|
||||
import('tdesign-vue-next').then(({ MessagePlugin }) => {
|
||||
MessagePlugin.success('API Key 已复制到剪贴板')
|
||||
})
|
||||
} catch (err) {
|
||||
// 降级到传统方式
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = tenantInfo.value.api_key
|
||||
document.body.appendChild(textArea)
|
||||
textArea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textArea)
|
||||
import('tdesign-vue-next').then(({ MessagePlugin }) => {
|
||||
MessagePlugin.success('API Key 已复制到剪贴板')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const openApiDoc = () => {
|
||||
window.open('https://github.com/Tencent/WeKnora/blob/main/docs/API.md', '_blank')
|
||||
}
|
||||
|
||||
const getStatusText = (status: string | undefined) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return '活跃'
|
||||
case 'inactive':
|
||||
return '未激活'
|
||||
case 'suspended':
|
||||
return '已暂停'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusTheme = (status: string | undefined) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return 'success'
|
||||
case 'inactive':
|
||||
return 'warning'
|
||||
case 'suspended':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateStr: string | undefined) => {
|
||||
if (!dateStr) return '未知'
|
||||
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
} catch {
|
||||
return '格式错误'
|
||||
}
|
||||
}
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B'
|
||||
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
const getUsagePercentage = () => {
|
||||
if (!tenantInfo.value?.storage_quota || tenantInfo.value.storage_quota === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const used = tenantInfo.value.storage_used || 0
|
||||
const percentage = (used / tenantInfo.value.storage_quota) * 100
|
||||
return Math.min(Math.round(percentage * 100) / 100, 100) // 保留两位小数,最大100%
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadTenantInfo()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tenant-info-container {
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
margin: 0 20px 20px 20px;
|
||||
height: calc(100vh - 40px);
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tenant-header {
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding-bottom: 16px;
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #000000;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.tenant-subtitle {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tenant-content {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.card-header-with-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.info-content,
|
||||
.api-key-content,
|
||||
.storage-content,
|
||||
.doc-content {
|
||||
// padding: 16px 0;
|
||||
}
|
||||
|
||||
.api-key-input {
|
||||
margin-bottom: 16px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
}
|
||||
|
||||
.api-warning {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.usage-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.usage-text {
|
||||
font-weight: 500;
|
||||
color: #000000;
|
||||
}
|
||||
}
|
||||
|
||||
.doc-description {
|
||||
margin-bottom: 16px;
|
||||
color: #666666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.doc-actions {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.api-example {
|
||||
margin-top: 20px;
|
||||
padding: 16px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
|
||||
.example-header h4 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.code-textarea {
|
||||
margin-bottom: 16px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
}
|
||||
|
||||
.example-note {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 24px;
|
||||
text-align: center;
|
||||
|
||||
.loading-text {
|
||||
margin-top: 16px;
|
||||
color: #666666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.error-container {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.tenant-info-container {
|
||||
padding: 16px;
|
||||
margin: 10px;
|
||||
height: calc(100vh - 20px);
|
||||
}
|
||||
|
||||
.tenant-header h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.card-header-with-actions {
|
||||
flex-direction: column;
|
||||
align-items: flex-start !important;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.doc-actions {
|
||||
:deep(.t-space) {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.t-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 覆盖TDesign组件样式 */
|
||||
:deep(.t-card) {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
:deep(.t-descriptions-item__label) {
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
:deep(.t-descriptions-item__content) {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
:deep(.t-input__inner) {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
:deep(.code-textarea .t-textarea__inner) {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
@@ -399,10 +399,10 @@ func (s *userService) RevokeToken(ctx context.Context, tokenString string) error
|
||||
|
||||
// GetCurrentUser gets current user from context
|
||||
func (s *userService) GetCurrentUser(ctx context.Context) (*types.User, error) {
|
||||
userID, ok := ctx.Value("user_id").(string)
|
||||
user, ok := ctx.Value("user").(*types.User)
|
||||
if !ok {
|
||||
return nil, errors.New("user not found in context")
|
||||
}
|
||||
|
||||
return s.userRepo.GetUserByID(ctx, userID)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -16,17 +16,20 @@ import (
|
||||
// Provides functionality for user registration, login, logout, and token management
|
||||
// through the REST API endpoints
|
||||
type AuthHandler struct {
|
||||
userService interfaces.UserService
|
||||
userService interfaces.UserService
|
||||
tenantService interfaces.TenantService
|
||||
}
|
||||
|
||||
// NewAuthHandler creates a new auth handler instance with the provided service
|
||||
// NewAuthHandler creates a new auth handler instance with the provided services
|
||||
// Parameters:
|
||||
// - userService: An implementation of the UserService interface for business logic
|
||||
// - tenantService: An implementation of the TenantService interface for tenant management
|
||||
//
|
||||
// Returns a pointer to the newly created AuthHandler
|
||||
func NewAuthHandler(userService interfaces.UserService) *AuthHandler {
|
||||
func NewAuthHandler(userService interfaces.UserService, tenantService interfaces.TenantService) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
userService: userService,
|
||||
userService: userService,
|
||||
tenantService: tenantService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +217,7 @@ func (h *AuthHandler) RefreshToken(c *gin.Context) {
|
||||
func (h *AuthHandler) GetCurrentUser(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
logger.Info(ctx, "Get current user info")
|
||||
logger.Debugf(ctx, "Get current user info")
|
||||
|
||||
// Get current user from service (which extracts from context)
|
||||
user, err := h.userService.GetCurrentUser(ctx)
|
||||
@@ -225,10 +228,25 @@ func (h *AuthHandler) GetCurrentUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Retrieved current user info: %s", user.Email)
|
||||
// Get tenant information
|
||||
var tenant *types.Tenant
|
||||
if user.TenantID > 0 {
|
||||
tenant, err = h.tenantService.GetTenantByID(ctx, user.TenantID)
|
||||
if err != nil {
|
||||
logger.Warnf(ctx, "Failed to get tenant info for user %s, tenant ID %d: %v", user.Email, user.TenantID, err)
|
||||
// Don't fail the request if tenant info is not available
|
||||
} else {
|
||||
logger.Debugf(ctx, "Retrieved tenant info for user %s: %s", user.Email, tenant.Name)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debugf(ctx, "Retrieved current user info: %s", user.Email)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"user": user.ToUserInfo(),
|
||||
"data": gin.H{
|
||||
"user": user.ToUserInfo(),
|
||||
"tenant": tenant,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user