fix:增加用户中心,完成Apikey功能页,增加角色工具方法

This commit is contained in:
Gsh
2025-07-04 00:12:26 +08:00
parent e996bc2d7f
commit c637d412e6
10 changed files with 814 additions and 10 deletions

View File

@@ -21,6 +21,6 @@ VITE_SSO_SEVER_URL='https://ccnetcore.com'
# SSO单点登录项目标识
VITE_SSO_CLIENT_ID='YiXin-Ai';
# 版本号
VITE_APP_VERSION='1.0.0';
VITE_APP_VERSION='1.0.3';

View File

@@ -1,8 +1,16 @@
import type { GetSessionListVO } from './types';
import { get } from '@/utils/request';
import { get, post } from '@/utils/request';
// 获取当前用户的模型列表
export function getModelList() {
// return get<GetSessionListVO[]>('/system/model/modelList');
return get<GetSessionListVO[]>('/ai-chat/model').json();
}
// 申请ApiKey
export function applyApiKey() {
return post<any>('/token').json();
}
// 获取ApiKey
export function getApiKey() {
return get<any>('/token').json();
}

View File

@@ -0,0 +1,132 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
interface NavItem {
name: string;
label: string;
icon?: string;
}
interface Props {
modelValue: boolean;
title?: string;
width?: string;
navItems: NavItem[];
defaultActive?: string;
}
const props = withDefaults(defineProps<Props>(), {
title: '弹窗标题',
width: '800px',
defaultActive: '',
});
const emit = defineEmits(['update:modelValue', 'confirm', 'close', 'nav-change']);
const visible = ref(false);
const activeNav = ref(props.defaultActive || (props.navItems.length > 0 ? props.navItems[0].name : ''));
watch(() => props.modelValue, (val) => {
visible.value = val;
});
watch(() => props.defaultActive, (val) => {
if (val) {
activeNav.value = val;
}
});
function handleNavSelect(index: string) {
activeNav.value = index;
emit('nav-change', index);
}
function handleClose() {
visible.value = false;
emit('update:modelValue', false);
emit('close');
}
function handleConfirm() {
emit('confirm', activeNav.value);
handleClose();
}
</script>
<template>
<el-dialog
v-model="visible"
:title="title"
:width="width"
:before-close="handleClose"
class="nav-dialog"
>
<div class="dialog-container">
<!-- 左侧导航 -->
<div class="nav-side">
<el-menu
:default-active="activeNav"
class="nav-menu"
@select="handleNavSelect"
>
<el-menu-item
v-for="item in navItems"
:key="item.name"
:index="item.name"
>
<template #title>
<el-icon v-if="item.icon">
<component :is="item.icon" />
</el-icon>
<span>{{ item.label }}</span>
</template>
</el-menu-item>
</el-menu>
</div>
<!-- 右侧内容 -->
<div class="content-main">
<slot :name="activeNav" />
<div v-if="!$slots[activeNav]" class="empty-content">
<el-empty description="暂无内容" />
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleConfirm">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<style scoped>
.dialog-container {
display: flex;
height: 500px;
}
.nav-side {
width: 200px;
border-right: 1px solid #e6e6e6;
}
.nav-menu {
border-right: none;
}
.content-main {
flex: 1;
padding: 0 20px;
overflow: auto;
}
.empty-content {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -0,0 +1,562 @@
<script lang="ts" setup>
import { CircleCheck } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { applyApiKey, getApiKey } from '@/api/model/index.ts';
import { isUserVip } from '@/utils/user';
const apiKey = ref('');
const showKey = ref(false);
const loading = ref(false);
const displayedKey = ref('');
const showSuccessDialog = ref(false);
const isOpening = ref(false);
const confettis = ref<any[]>([]);
const router = useRouter();
// 获取API密钥
async function fetchApiKey() {
try {
const res = await getApiKey();
if (res.data?.apiKey) {
apiKey.value = res.data.apiKey;
displayedKey.value = res.data.apiKey;
}
}
catch (error) {
console.error('获取API密钥失败:', error);
}
}
// 领取密钥
async function handleClaim() {
if (!isUserVip) {
ElMessageBox.confirm(
`
<div class="text-center leading-relaxed">
<h3 class="text-lg font-bold mb-3">成为 YiXinAI-VIP</h3>
<p class="mb-2">
解锁所有 AI 模型,无限加速,专属客服,尽享尊贵体验。
</p>
<p class="text-sm text-gray-500">点击下方按钮,立即升级为 VIP 会员!</p>
</div>
`,
'会员尊享',
{
confirmButtonText: '前往产品页面',
cancelButtonText: '关闭',
dangerouslyUseHTMLString: true,
type: 'info',
center: true,
roundButton: true,
},
)
.then(() => {
router.push({
name: 'products', // 使用命名路由
query: { from: 'user' }, // 可选:添加来源标识
});
})
.catch(() => {
// 点击右上角关闭或“关闭”按钮,不执行任何操作
});
return;
}
if (isOpening.value)
return;
try {
isOpening.value = true;
generateConfetti();
loading.value = true;
await applyApiKey();
await fetchApiKey();
showSuccessDialog.value = true;
}
catch (error) {
console.error('领取失败:', error);
ElMessage.error('领取失败,请稍后重试');
}
finally {
loading.value = false;
setTimeout(() => isOpening.value = false, 1000);
}
}
// 重置密钥
async function handleReset() {
try {
await ElMessageBox.confirm('确定要重置API密钥吗原密钥将立即失效', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
loading.value = true;
await applyApiKey(); // 使用相同的申请接口重置
await fetchApiKey();
ElMessage.success('API密钥已重置');
}
catch (error) {
if (error !== 'cancel') {
console.error('重置失败:', error);
ElMessage.error('重置失败');
}
}
finally {
loading.value = false;
}
}
// 切换密钥显示状态
function toggleKeyVisibility() {
showKey.value = !showKey.value;
}
// 复制API密钥
function copyApiKey() {
navigator.clipboard.writeText(apiKey.value || '')
.then(() => ElMessage.success('已复制到剪贴板'))
.catch(() => ElMessage.error('复制失败'));
}
// 生成随机彩带
function generateConfetti() {
const colors = ['#ff9a9e', '#fad0c4', '#fbc2eb', '#a6c1ee', '#a18cd1', '#fdcbf1'];
const newConfettis = [];
for (let i = 0; i < 30; i++) {
newConfettis.push({
style: {
left: `${50 + (Math.random() - 0.5) * 30}%`,
backgroundColor: colors[Math.floor(Math.random() * colors.length)],
width: `${6 + Math.random() * 6}px`,
height: `${6 + Math.random() * 6}px`,
transform: `rotate(${Math.random() * 360}deg)`,
animationDuration: `${1 + Math.random() * 2}s`,
animationDelay: `${Math.random() * 0.5}s`,
},
});
}
confettis.value = newConfettis;
setTimeout(() => confettis.value = [], 2000);
}
onMounted(() => {
fetchApiKey();
});
</script>
<template>
<div class="api-key-management">
<!-- 未领取状态 -->
<div v-if="!apiKey " class="unclaimed-state">
<div class="gift-container" @click="handleClaim">
<div class="gift-box" :class="{ opening: isOpening }">
<div class="gift-lid" />
<div class="gift-body">
<div class="ribbon-horizontal" />
<div class="ribbon-vertical" />
<div class="ribbon-bow">
<div class="bow-left" />
<div class="bow-right" />
<div class="bow-center" />
</div>
</div>
</div>
<div
v-for="(confetti, index) in confettis" :key="index" class="confetti"
:style="confetti.style"
/>
</div>
<div class="claim-text">
<h3>🎁 尊享会员专属福利 🎁</h3>
<h3>
YiXinAi重磅推出专属Api接入服务
</h3>
<h3>YiXinAI-Vip 限时免费领取</h3>
<p>点击礼盒领取您的专属API密钥</p>
</div>
</div>
<!-- 已领取状态 -->
<div v-else-if="apiKey" class="claimed-state">
<h3 class="key-title">
🎉 恭喜您已获得专属API密钥 🎉
</h3>
<div class="key-display">
<el-input
v-model="displayedKey"
:type="showKey ? 'text' : 'password'"
readonly
class="key-input"
>
<template #append>
<el-button-group>
<el-button
:icon="showKey ? 'Hide' : 'View'"
@click="toggleKeyVisibility"
/>
<el-button
icon="DocumentCopy"
@click="copyApiKey"
/>
</el-button-group>
</template>
</el-input>
</div>
<div class="key-actions">
<el-button type="warning" :loading="loading" @click="handleReset">
重置密钥
</el-button>
<p class="key-hint">
重置后原密钥将立即失效
</p>
</div>
</div>
<!-- 使用说明 -->
<div class="usage-guide">
<el-divider />
<h3>使用说明</h3>
<div class="guide-content">
<p><strong>API地址</strong>https://ai.ccnetcore.com</p>
<p><strong>密钥</strong>上面申请的token</p>
<p><strong>模型</strong>聊天界面显示的模型名称</p>
</div>
<div class="guide-images">
<el-image
style="max-width: 100%; margin: 10px 0;"
src="/images/api_usage_instructions.png"
fit="contain"
/>
</div>
</div>
<!-- 领取成功弹窗 -->
<el-dialog
v-model="showSuccessDialog"
title="领取成功"
width="400px"
:show-close="false"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<div class="success-dialog">
<el-icon color="#67C23A" :size="60">
<CircleCheck />
</el-icon>
<p class="success-message">
恭喜您成功领取API密钥
</p>
<p class="success-tip">
请妥善保管您的密钥
</p>
</div>
<template #footer>
<el-button type="primary" @click="showSuccessDialog = false">
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.api-key-management {
padding: 20px;
max-width: 600px;
margin: 0 auto;
}
/* 未领取状态样式 */
.unclaimed-state {
text-align: center;
margin: 40px 0;
}
.gift-box {
position: relative;
width: 180px;
height: 180px;
margin: 0 auto 30px;
cursor: pointer;
transition: transform 0.3s;
}
.gift-box:hover {
transform: scale(1.05);
}
.gift-lid {
position: absolute;
width: 180px;
height: 40px;
background: #E74C3C;
top: -20px;
left: 0;
border-radius: 4px 4px 0 0;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.gift-body {
position: absolute;
width: 180px;
height: 140px;
background: #C0392B;
top: 20px;
left: 0;
border-radius: 0 0 4px 4px;
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
.ribbon {
position: absolute;
width: 40px;
height: 100%;
background: #F1C40F;
left: 50%;
transform: translateX(-50%);
}
.gift-label {
position: absolute;
bottom: -30px;
width: 100%;
text-align: center;
font-weight: bold;
color: #E74C3C;
}
.claim-hint {
margin-top: 20px;
font-size: 16px;
color: #E74C3C;
font-weight: bold;
}
/* 已领取状态样式 */
.claimed-state {
margin: 30px 0;
}
.key-title {
text-align: center;
color: #333;
margin-bottom: 20px;
}
.key-display {
margin: 20px 0;
}
.key-input {
font-family: monospace;
}
/* 添加按钮间距 */
.action-btn {
margin-left: 4px;
}
/* 或者直接设置最后一个按钮的左边距 */
.key-input :deep(.el-input-group__append .el-button + .el-button) {
margin-left: 4px;
}
.key-actions {
text-align: center;
margin-top: 30px;
}
.key-hint {
color: #999;
font-size: 14px;
margin-top: 10px;
}
/* 使用说明样式 */
.usage-guide {
margin-top: 40px;
}
.guide-content {
background: #f8f9fa;
padding: 15px;
border-radius: 4px;
line-height: 1.8;
}
/* 成功弹窗样式 */
.success-dialog {
text-align: center;
padding: 20px 0;
}
.success-message {
font-size: 18px;
color: #333;
margin: 15px 0 5px;
}
.success-tip {
color: #999;
font-size: 14px;
}
/* 未领取状态样式 */
.unclaimed-state {
text-align: center;
margin: 30px 0;
perspective: 600px;
}
.gift-container {
position: relative;
width: 150px;
height: 150px;
margin: 0 auto;
cursor: pointer;
}
.gift-box {
position: relative;
width: 100%;
height: 100%;
transition: all 0.3s;
transform-style: preserve-3d;
}
.gift-box:hover:not(.opening) {
transform: translateY(-5px);
}
.gift-box.opening .gift-lid {
transform: rotateX(180deg) translateY(-30px);
opacity: 0;
}
.gift-lid {
position: absolute;
width: 150px;
height: 30px;
background: linear-gradient(135deg, #ff7676, #e74c3c);
top: -15px;
left: 0;
border-radius: 8px 8px 0 0;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
transform-origin: bottom;
z-index: 2;
}
.gift-body {
position: absolute;
width: 150px;
height: 120px;
background: linear-gradient(135deg, #e74c3c, #c0392b);
top: 15px;
left: 0;
border-radius: 0 0 8px 8px;
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
z-index: 1;
}
.ribbon-horizontal {
position: absolute;
width: 100%;
height: 12px;
background: rgba(255,255,255,0.3);
top: 50%;
transform: translateY(-50%);
}
.ribbon-vertical {
position: absolute;
width: 12px;
height: 100%;
background: rgba(255,255,255,0.3);
left: 50%;
transform: translateX(-50%);
}
.ribbon-bow {
position: absolute;
top: -10px;
left: 50%;
transform: translateX(-50%);
z-index: 3;
}
.bow-left, .bow-right {
position: absolute;
width: 20px;
height: 20px;
background: linear-gradient(135deg, #f1c40f, #f39c12);
border-radius: 50%;
}
.bow-left {
left: -25px;
clip-path: polygon(0 0, 100% 0, 100% 50%, 0 50%);
}
.bow-right {
left: 5px;
clip-path: polygon(0 0, 100% 0, 100% 50%, 0 50%);
}
.bow-center {
position: absolute;
width: 12px;
height: 30px;
background: linear-gradient(135deg, #f1c40f, #f39c12);
left: -6px;
top: 10px;
border-radius: 6px;
}
.confetti {
position: absolute;
width: 8px;
height: 8px;
background: #f1c40f;
top: 50%;
left: 50%;
opacity: 0;
animation: confetti-fall 2s ease-out forwards;
z-index: 10;
}
@keyframes confetti-fall {
0% {
transform: translate(0, 0) rotate(0deg);
opacity: 1;
}
100% {
transform: translate(var(--tx), 200px) rotate(360deg);
opacity: 0;
--tx: calc((var(--random) - 0.5) * 200px);
}
}
.claim-text {
margin-top: 30px;
}
.claim-text h3 {
color: #e74c3c;
margin-bottom: 10px;
font-size: 18px;
}
.claim-text p {
color: #666;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,24 @@
<script lang="ts" setup>
interface User {
name: string;
email: string;
role: string;
}
const users: User[] = [
{ name: '张三', email: 'zhangsan@example.com', role: '管理员' },
{ name: '李四', email: 'lisi@example.com', role: '编辑' },
{ name: '王五', email: 'wangwu@example.com', role: '查看者' },
];
</script>
<template>
<div class="user-management">
<h3>用户管理</h3>
<el-table :data="users" style="width: 100%">
<el-table-column prop="name" label="姓名" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="role" label="角色" />
</el-table>
</div>
</template>

View File

@@ -32,14 +32,19 @@ const popoverRef = ref();
// 弹出面板内容
const popoverList = ref([
// {
// key: '1',
// title: '收藏夹',
// icon: 'book-mark-fill',
// },
// {
// key: '2',
// title: '设置',
// icon: 'settings-4-fill',
// },
{
key: '1',
title: '收藏夹',
icon: 'book-mark-fill',
},
{
key: '2',
title: '设置',
key: '5',
title: '用户中心',
icon: 'settings-4-fill',
},
{
@@ -53,6 +58,25 @@ const popoverList = ref([
},
]);
const dialogVisible = ref(false);
const navItems = [
// { name: 'user', label: '用户管理', icon: 'User' },
// { name: 'role', label: '角色管理', icon: 'Avatar' },
// { name: 'permission', label: '权限管理', icon: 'Key' },
{ name: 'apiKey', label: 'API密钥', icon: 'Key' },
];
function openDialog() {
dialogVisible.value = true;
}
function handleConfirm(activeNav: string) {
console.log('确认操作,当前导航:', activeNav);
ElMessage.success('操作成功');
}
function handleNavChange(nav: string) {
console.log('导航切换:', nav);
}
// 点击
function handleClick(item: any) {
switch (item.key) {
@@ -64,6 +88,10 @@ function handleClick(item: any) {
ElMessage.warning('暂未开放');
console.log('点击了设置');
break;
case '5':
console.log('点击了用户中心');
openDialog();
break;
case '4':
popoverRef.value?.hide?.();
ElMessageBox.confirm('退出登录不会丢失任何数据,你仍可以登录此账号。', '确认退出登录?', {
@@ -226,6 +254,32 @@ function openVipGuide() {
</div>
</Popover>
</div>
<nav-dialog
v-model="dialogVisible"
title="用户中心"
:nav-items="navItems"
@confirm="handleConfirm"
@nav-change="handleNavChange"
>
<!-- 用户管理内容 -->
<template #user>
<user-management />
</template>
<!-- 角色管理内容 -->
<template #role>
<!-- <role-management /> -->
</template>
<!-- 权限管理内容 -->
<template #permission>
<!-- <permission-management /> -->
</template>
<template #apiKey>
<APIKeyManagement />
</template>
</nav-dialog>
</div>
</template>

View File

@@ -16,6 +16,7 @@ const pricing = [
'无限制使用',
'售后微信群支持',
'可用优惠券,使用后29.9元',
'专属Api服务',
],
},
{
@@ -30,6 +31,7 @@ const pricing = [
'AI超级加速',
'无限制使用',
'售后微信群支持',
'专属Api服务',
],
},
{
@@ -44,6 +46,8 @@ const pricing = [
'无限制使用',
'售后微信群优先处理',
'可用优惠券,使用后49.9元',
'专属Api服务',
],
},
];

View File

@@ -0,0 +1,10 @@
import { useUserStore } from '@/stores/index.js';
const userStore = useUserStore();
// 获取用户角色信息
const userRoles = userStore.userInfo?.roles ?? [];
const isUserVip = userRoles.some(role => role.roleCode === 'YiXinAi-Vip');
export {
isUserVip,
};

View File

@@ -9,14 +9,17 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
AccountPassword: typeof import('./../src/components/LoginDialog/components/FormLogin/AccountPassword.vue')['default']
APIKeyManagement: typeof import('./../src/components/userPersonalCenter/components/APIKeyManagement.vue')['default']
DeepThinking: typeof import('./../src/components/DeepThinking/index.vue')['default']
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCard: typeof import('element-plus/es')['ElCard']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm']
@@ -26,6 +29,12 @@ declare module 'vue' {
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
FilesSelect: typeof import('./../src/components/FilesSelect/index.vue')['default']
IconSelect: typeof import('./../src/components/IconSelect/index.vue')['default']
@@ -33,12 +42,14 @@ declare module 'vue' {
LoginDialog: typeof import('./../src/components/LoginDialog/index.vue')['default']
ModeList: typeof import('./../src/components/modeList/index.vue')['default']
ModelSelect: typeof import('./../src/components/ModelSelect/index.vue')['default']
NavDialog: typeof import('./../src/components/userPersonalCenter/NavDialog.vue')['default']
Popover: typeof import('./../src/components/Popover/index.vue')['default']
QrCodeLogin: typeof import('./../src/components/LoginDialog/components/QrCodeLogin/index.vue')['default']
RegistrationForm: typeof import('./../src/components/LoginDialog/components/FormLogin/RegistrationForm.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SvgIcon: typeof import('./../src/components/SvgIcon/index.vue')['default']
UserManagement: typeof import('./../src/components/userPersonalCenter/components/UserManagement.vue')['default']
VerificationCode: typeof import('./../src/components/LoginDialog/components/FormLogin/VerificationCode.vue')['default']
WelecomeText: typeof import('./../src/components/WelecomeText/index.vue')['default']
}

View File

@@ -6,7 +6,6 @@ interface ImportMetaEnv {
readonly VITE_WEB_ENV: string;
readonly VITE_WEB_BASE_API: string;
readonly VITE_API_URL: string;
readonly VITE_BUILD_COMPRESS: string;
readonly VITE_SSO_SEVER_URL: string;
readonly VITE_SSO_CLIENT_ID: string;
readonly VITE_APP_VERSION: string;