fix:增加用户中心,完成Apikey功能页,增加角色工具方法
This commit is contained in:
132
Yi.Ai.Vue3/src/components/userPersonalCenter/NavDialog.vue
Normal file
132
Yi.Ai.Vue3/src/components/userPersonalCenter/NavDialog.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user