style(frontend): 重构全局样式并引入设计系统变量
- 引入 CSS 变量定义统一设计系统色彩、边框、圆角和阴影等 - 新增全局基础样式,使用暗色背景和统一字体颜色 - 优化滚动条样式和焦点环,提高交互体验 - 添加全局过渡动画类 - 统一输入框和按钮样式,实现一致外观和交互反馈 - 优化 Markdown 渲染样式,增强代码块和表格视觉效果 - 调整思考过程折叠样式,提升可读性和样式一致性 - 添加多种通用组件样式,如按钮、徽章等 - 服务器代理端口由8000改为7964,更新 vite 配置文件
This commit is contained in:
@@ -1,62 +1,200 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* 自定义基础样式 */
|
||||
/* ========== 设计系统 - CSS 变量 ========== */
|
||||
:root {
|
||||
/* 背景色 */
|
||||
--bg-primary: #030712; /* gray-950 */
|
||||
--bg-secondary: #111827; /* gray-900 */
|
||||
--bg-tertiary: #1f2937; /* gray-800 */
|
||||
--bg-elevated: #374151; /* gray-700 */
|
||||
/* 边框 */
|
||||
--border-primary: #1f2937; /* gray-800 */
|
||||
--border-hover: #374151; /* gray-700 */
|
||||
--border-active: rgba(99, 102, 241, 0.5); /* indigo-500/50 */
|
||||
/* 强调色 */
|
||||
--accent: #6366f1; /* indigo-500 */
|
||||
--accent-hover: #818cf8; /* indigo-400 */
|
||||
--accent-glow: rgba(99, 102, 241, 0.15);
|
||||
--accent-subtle: rgba(99, 102, 241, 0.1);
|
||||
/* 文字 */
|
||||
--text-primary: #f3f4f6; /* gray-100 */
|
||||
--text-secondary: #9ca3af; /* gray-400 */
|
||||
--text-muted: #6b7280; /* gray-500 */
|
||||
--text-dim: #4b5563; /* gray-600 */
|
||||
/* 圆角 */
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
--radius-2xl: 20px;
|
||||
/* 阴影 */
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
|
||||
--shadow-md: 0 4px 12px rgba(0,0,0,0.4);
|
||||
--shadow-lg: 0 8px 24px rgba(0,0,0,0.5);
|
||||
--shadow-glow: 0 0 20px rgba(99, 102, 241, 0.15);
|
||||
/* 过渡 */
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-base: 200ms ease;
|
||||
--transition-slow: 300ms ease;
|
||||
}
|
||||
|
||||
/* ========== 全局基础样式 ========== */
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Markdown渲染样式 */
|
||||
.markdown-body h1 { font-size: 1.5em; font-weight: 700; margin: 1em 0 0.5em; }
|
||||
.markdown-body h2 { font-size: 1.3em; font-weight: 600; margin: 1em 0 0.5em; }
|
||||
.markdown-body h3 { font-size: 1.1em; font-weight: 600; margin: 0.8em 0 0.4em; }
|
||||
.markdown-body p { margin: 1em 0; line-height: 1.8; }
|
||||
/* 文字选中颜色 */
|
||||
::selection {
|
||||
background: rgba(99, 102, 241, 0.3);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 焦点环 */
|
||||
*:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ========== 滚动条美化 ========== */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(75, 85, 99, 0.4);
|
||||
border-radius: 3px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(107, 114, 128, 0.5);
|
||||
}
|
||||
|
||||
/* ========== 全局过渡动画 ========== */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-6px); }
|
||||
}
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.2); }
|
||||
50% { box-shadow: 0 0 16px 4px rgba(99, 102, 241, 0.15); }
|
||||
}
|
||||
@keyframes breathe {
|
||||
0%, 100% { opacity: 0.6; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
.animate-fade-in { animation: fadeIn 0.3s ease; }
|
||||
.animate-slide-up { animation: slideUp 0.3s ease; }
|
||||
.animate-slide-down { animation: slideDown 0.3s ease; }
|
||||
.animate-float { animation: float 3s ease-in-out infinite; }
|
||||
.animate-glow { animation: pulse-glow 2s ease-in-out infinite; }
|
||||
.animate-breathe { animation: breathe 2s ease-in-out infinite; }
|
||||
|
||||
/* ========== 卡片悬浮效果 ========== */
|
||||
.card-hover {
|
||||
transition: transform var(--transition-base), box-shadow var(--transition-base), border-color var(--transition-base);
|
||||
}
|
||||
.card-hover:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* ========== Markdown 渲染样式 ========== */
|
||||
.markdown-body h1 { font-size: 1.5em; font-weight: 700; margin: 1.2em 0 0.6em; color: var(--text-primary); }
|
||||
.markdown-body h2 { font-size: 1.3em; font-weight: 600; margin: 1em 0 0.5em; color: var(--text-primary); }
|
||||
.markdown-body h3 { font-size: 1.1em; font-weight: 600; margin: 0.8em 0 0.4em; color: var(--text-primary); }
|
||||
.markdown-body p { margin: 0.8em 0; line-height: 1.8; }
|
||||
.markdown-body ul, .markdown-body ol { padding-left: 1.5em; margin: 0.5em 0; }
|
||||
.markdown-body li { margin: 0.3em 0; }
|
||||
.markdown-body li { margin: 0.3em 0; line-height: 1.7; }
|
||||
.markdown-body a {
|
||||
color: var(--accent-hover);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
.markdown-body a:hover {
|
||||
border-bottom-color: var(--accent-hover);
|
||||
}
|
||||
.markdown-body img {
|
||||
border-radius: var(--radius-md);
|
||||
max-width: 100%;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
.markdown-body code {
|
||||
background: rgba(127, 127, 127, 0.15);
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
color: #c7d2fe;
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
font-size: 0.88em;
|
||||
}
|
||||
.markdown-body pre {
|
||||
background: #1e1e2e;
|
||||
color: #cdd6f4;
|
||||
padding: 1em;
|
||||
border-radius: 8px;
|
||||
padding: 1em 1.2em;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid rgba(99, 102, 241, 0.1);
|
||||
overflow-x: auto;
|
||||
margin: 0.8em 0;
|
||||
}
|
||||
.markdown-body pre code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
.markdown-body table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 0.8em 0;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
.markdown-body th, .markdown-body td {
|
||||
border: 1px solid rgba(127, 127, 127, 0.3);
|
||||
padding: 0.5em 0.8em;
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
padding: 0.6em 0.8em;
|
||||
text-align: left;
|
||||
}
|
||||
.markdown-body th {
|
||||
background: rgba(127, 127, 127, 0.1);
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.markdown-body blockquote {
|
||||
border-left: 3px solid #6366f1;
|
||||
border-left: 3px solid var(--accent);
|
||||
padding-left: 1em;
|
||||
margin: 0.5em 0;
|
||||
color: rgba(127, 127, 127, 0.8);
|
||||
margin: 0.8em 0;
|
||||
color: var(--text-secondary);
|
||||
background: rgba(99, 102, 241, 0.03);
|
||||
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
||||
padding: 0.6em 1em;
|
||||
}
|
||||
.markdown-body hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, transparent, var(--border-primary), transparent);
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
|
||||
/* 思考过程折叠样式 */
|
||||
.markdown-body details {
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
border-radius: 8px;
|
||||
background: rgba(99, 102, 241, 0.06);
|
||||
border: 1px solid rgba(99, 102, 241, 0.15);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 0;
|
||||
margin: 0.5em 0 1em;
|
||||
overflow: hidden;
|
||||
@@ -68,12 +206,13 @@ body {
|
||||
color: #a5b4fc;
|
||||
user-select: none;
|
||||
font-size: 0.85em;
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
.markdown-body details summary:hover {
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
.markdown-body details[open] summary {
|
||||
border-bottom: 1px solid rgba(99, 102, 241, 0.15);
|
||||
border-bottom: 1px solid rgba(99, 102, 241, 0.12);
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.markdown-body details > *:not(summary) {
|
||||
@@ -82,3 +221,52 @@ body {
|
||||
color: #9ca3af;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ========== 通用组件样式 ========== */
|
||||
/* 输入框统一 */
|
||||
.input-base {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-hover);
|
||||
border-radius: var(--radius-lg);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
.input-base::placeholder {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.input-base:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||
}
|
||||
|
||||
/* 按钮统一 */
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: #4f46e5;
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 标签 badge */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.125rem 0.5rem;
|
||||
font-size: 0.6875rem;
|
||||
border-radius: 9999px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -20,26 +20,38 @@
|
||||
</div>
|
||||
|
||||
<!-- 主内容 -->
|
||||
<div v-else class="max-w-5xl mx-auto px-6 py-6">
|
||||
<div v-else class="max-w-5xl mx-auto px-6 py-6 animate-fade-in">
|
||||
<!-- 顶部 -->
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<div>
|
||||
<h1 class="text-lg font-bold text-gray-100">API Hub</h1>
|
||||
<h1 class="text-lg font-bold text-gray-100 flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
|
||||
API Hub
|
||||
</h1>
|
||||
<p class="text-xs text-gray-500 mt-1">团队共享 API 资源管理</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- 统计 -->
|
||||
<div class="flex items-center gap-3 text-[11px] text-gray-500 mr-2">
|
||||
<span>{{ stats.total_apis }} 个API</span>
|
||||
<span>{{ stats.total_calls }} 次调用</span>
|
||||
<span class="text-green-400">{{ stats.healthy_count }} 健康</span>
|
||||
<div class="flex items-center gap-4 mr-3">
|
||||
<div class="flex items-center gap-1.5 text-[11px] text-gray-500">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
|
||||
{{ stats.total_apis }} 个API
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 text-[11px] text-gray-500">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
||||
{{ stats.total_calls }} 次调用
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 text-[11px] text-green-400">
|
||||
<span class="w-1.5 h-1.5 bg-green-400 rounded-full animate-pulse"></span>
|
||||
{{ stats.healthy_count }} 健康
|
||||
</div>
|
||||
</div>
|
||||
<!-- 搜索 -->
|
||||
<div class="relative w-48">
|
||||
<svg class="w-3.5 h-3.5 text-gray-600 absolute left-3 top-1/2 -translate-y-1/2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||
<input v-model="keyword" @input="loadApis" placeholder="搜索API..." class="w-full pl-8 pr-3 py-1.5 bg-gray-900 border border-gray-800 rounded-lg text-xs text-gray-200 placeholder-gray-600 focus:outline-none focus:border-indigo-500" />
|
||||
<input v-model="keyword" @input="loadApis" placeholder="搜索API..." class="w-full pl-8 pr-3 py-1.5 bg-gray-900/80 border border-gray-800/60 rounded-xl text-xs text-gray-200 placeholder-gray-600 focus:outline-none focus:border-indigo-500/50 focus:shadow-[0_0_0_3px_rgba(99,102,241,0.08)] transition-all" />
|
||||
</div>
|
||||
<button @click="openApiModal()" class="flex items-center gap-1.5 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white text-xs rounded-lg transition-colors">
|
||||
<button @click="openApiModal()" class="flex items-center gap-1.5 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-500 hover:shadow-lg hover:shadow-indigo-500/20 text-white text-xs rounded-xl transition-all">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
||||
添加 API
|
||||
</button>
|
||||
@@ -62,10 +74,10 @@
|
||||
|
||||
<!-- API 卡片网格 -->
|
||||
<div v-else-if="apis.length > 0" class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div v-for="a in apis" :key="a.id" @click="selectApi(a)" class="bg-gray-900 border border-gray-800 rounded-xl px-5 py-4 cursor-pointer hover:border-indigo-600/30 transition-all group">
|
||||
<div v-for="a in apis" :key="a.id" @click="selectApi(a)" class="bg-gray-900/80 border border-gray-800/60 rounded-xl px-5 py-4 cursor-pointer hover:border-indigo-500/30 transition-all group">
|
||||
<div class="flex items-start gap-3">
|
||||
<!-- 状态灯 -->
|
||||
<div class="w-2 h-2 rounded-full mt-1.5 shrink-0" :class="a.last_check_status === 'ok' ? 'bg-green-400' : a.last_check_status === 'error' ? 'bg-red-400' : 'bg-gray-600'"></div>
|
||||
<div class="w-2.5 h-2.5 rounded-full mt-1.5 shrink-0 ring-2" :class="a.last_check_status === 'ok' ? 'bg-green-400 ring-green-400/20' : a.last_check_status === 'error' ? 'bg-red-400 ring-red-400/20' : 'bg-gray-600 ring-gray-600/20'"></div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium text-gray-200 group-hover:text-indigo-400 transition-colors truncate">{{ a.name }}</span>
|
||||
@@ -82,8 +94,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-16">
|
||||
<svg class="w-12 h-12 text-gray-700 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
|
||||
<p class="text-sm text-gray-500">暂无API,点击右上角添加</p>
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gray-800/60 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
|
||||
</div>
|
||||
<p class="text-sm text-gray-400">暂无API</p>
|
||||
<p class="text-xs text-gray-600 mt-1">点击右上角添加</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -165,7 +180,9 @@
|
||||
<div>
|
||||
<h4 class="text-xs font-medium text-gray-300 mb-3">最近日志</h4>
|
||||
<div v-if="apiLogs.length > 0" class="space-y-1">
|
||||
<!-- 日志状态码颜色条 -->
|
||||
<div v-for="log in apiLogs" :key="log.id" class="flex items-center gap-3 text-[11px] py-1.5 border-b border-gray-800/50">
|
||||
<span class="w-1 h-4 rounded-full" :class="log.response_status >= 200 && log.response_status < 400 ? 'bg-green-400' : 'bg-red-400'"></span>
|
||||
<span :class="log.response_status >= 200 && log.response_status < 400 ? 'text-green-400' : 'text-red-400'" class="font-mono w-8">{{ log.response_status || 'ERR' }}</span>
|
||||
<span class="text-gray-500">{{ log.response_time_ms }}ms</span>
|
||||
<span class="text-gray-600 truncate flex-1">{{ log.request_url }}</span>
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
<template>
|
||||
<div class="h-full flex">
|
||||
<!-- 左侧:对话列表 -->
|
||||
<div class="w-64 bg-gray-900 border-r border-gray-800 flex flex-col shrink-0">
|
||||
<div class="p-4">
|
||||
<div class="w-64 bg-gray-900/95 border-r border-gray-800/60 flex flex-col shrink-0">
|
||||
<div class="p-3">
|
||||
<button
|
||||
@click="startNewChat"
|
||||
class="w-full py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm rounded-lg transition-colors"
|
||||
class="w-full py-2.5 bg-emerald-600 hover:bg-emerald-500 text-white text-sm rounded-xl transition-all duration-200 hover:shadow-lg hover:shadow-emerald-500/20 flex items-center justify-center gap-1.5 font-medium"
|
||||
>
|
||||
+ 新建架构咨询
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
||||
新建架构咨询
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto px-2">
|
||||
<div class="flex-1 overflow-y-auto px-2 space-y-0.5">
|
||||
<div
|
||||
v-for="conv in conversations"
|
||||
:key="conv.id"
|
||||
@click="selectConversation(conv)"
|
||||
class="px-3 py-2 mb-1 rounded-lg cursor-pointer text-sm truncate flex items-center justify-between group"
|
||||
:class="currentConvId === conv.id ? 'bg-gray-800 text-white' : 'text-gray-400 hover:bg-gray-800/50'"
|
||||
class="px-3 py-2.5 rounded-lg cursor-pointer text-[13px] truncate flex items-center justify-between group transition-all duration-150"
|
||||
:class="currentConvId === conv.id ? 'bg-emerald-500/15 text-emerald-300 border-l-2 border-emerald-500' : 'text-gray-400 hover:bg-gray-800/50 border-l-2 border-transparent'"
|
||||
>
|
||||
<span class="truncate">{{ conv.title }}</span>
|
||||
<button
|
||||
@click.stop="deleteConversation(conv.id)"
|
||||
class="opacity-0 group-hover:opacity-100 text-gray-500 hover:text-red-400 ml-2 shrink-0"
|
||||
class="opacity-0 group-hover:opacity-100 text-gray-500 hover:text-red-400 ml-2 shrink-0 transition-opacity"
|
||||
>
|
||||
x
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="conversations.length === 0" class="text-gray-600 text-sm text-center py-8">
|
||||
暂无对话记录
|
||||
</p>
|
||||
<div v-if="conversations.length === 0" class="text-center py-12">
|
||||
<svg class="w-8 h-8 text-gray-700 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
|
||||
<p class="text-gray-600 text-xs">暂无对话记录</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,23 +38,26 @@
|
||||
<div class="flex-1 flex flex-col">
|
||||
<div ref="messagesContainer" class="flex-1 overflow-y-auto p-6 space-y-4">
|
||||
<!-- 欢迎提示 -->
|
||||
<div v-if="messages.length === 0" class="flex items-center justify-center h-full">
|
||||
<div v-if="messages.length === 0" class="flex items-center justify-center h-full animate-fade-in">
|
||||
<div class="text-center max-w-lg">
|
||||
<h2 class="text-2xl font-bold text-gray-300 mb-4">架构选型助手</h2>
|
||||
<p class="text-gray-500 mb-6">
|
||||
<div class="w-16 h-16 rounded-2xl bg-emerald-500/15 flex items-center justify-center mx-auto mb-6 animate-float">
|
||||
<svg class="w-8 h-8 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-gray-200 mb-3">架构选型助手</h2>
|
||||
<p class="text-gray-500 mb-8">
|
||||
描述你的项目需求和约束条件,AI帮你推荐技术栈、生成架构图、评估技术风险。
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-3 text-sm">
|
||||
<div @click="quickAsk('我想做一个电商平台,推荐技术栈')" class="bg-gray-900 border border-gray-800 rounded-lg p-3 text-gray-400 cursor-pointer hover:border-indigo-500 transition-colors">
|
||||
<div @click="quickAsk('我想做一个电商平台,推荐技术栈')" class="bg-gray-900/80 border border-gray-800/60 rounded-xl p-3.5 text-gray-400 cursor-pointer hover:border-emerald-500/30 hover:text-gray-300 transition-all duration-200">
|
||||
推荐电商平台技术栈
|
||||
</div>
|
||||
<div @click="quickAsk('React和Vue哪个更适合后台管理系统?')" class="bg-gray-900 border border-gray-800 rounded-lg p-3 text-gray-400 cursor-pointer hover:border-indigo-500 transition-colors">
|
||||
<div @click="quickAsk('React和Vue哪个更适合后台管理系统?')" class="bg-gray-900/80 border border-gray-800/60 rounded-xl p-3.5 text-gray-400 cursor-pointer hover:border-emerald-500/30 hover:text-gray-300 transition-all duration-200">
|
||||
React vs Vue对比
|
||||
</div>
|
||||
<div @click="quickAsk('帮我设计一个微服务架构图')" class="bg-gray-900 border border-gray-800 rounded-lg p-3 text-gray-400 cursor-pointer hover:border-indigo-500 transition-colors">
|
||||
<div @click="quickAsk('帮我设计一个微服务架构图')" class="bg-gray-900/80 border border-gray-800/60 rounded-xl p-3.5 text-gray-400 cursor-pointer hover:border-emerald-500/30 hover:text-gray-300 transition-all duration-200">
|
||||
生成微服务架构图
|
||||
</div>
|
||||
<div @click="quickAsk('MySQL和PostgreSQL哪个更合适?')" class="bg-gray-900 border border-gray-800 rounded-lg p-3 text-gray-400 cursor-pointer hover:border-indigo-500 transition-colors">
|
||||
<div @click="quickAsk('MySQL和PostgreSQL哪个更合适?')" class="bg-gray-900/80 border border-gray-800/60 rounded-xl p-3.5 text-gray-400 cursor-pointer hover:border-emerald-500/30 hover:text-gray-300 transition-all duration-200">
|
||||
数据库选型建议
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,11 +87,11 @@
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<div class="border-t border-gray-800 p-4 bg-gray-900/50">
|
||||
<div class="border-t border-gray-800/60 p-4 bg-gray-900/80 backdrop-blur-sm">
|
||||
<!-- 模型选择器 -->
|
||||
<div v-if="availableModels.length > 0" class="flex items-center gap-2 mb-3">
|
||||
<span class="text-xs text-gray-500">模型:</span>
|
||||
<select v-model="selectedModelId" class="px-2 py-1 bg-gray-800 border border-gray-700 rounded-lg text-xs text-gray-300 focus:outline-none focus:border-indigo-500">
|
||||
<span class="text-[11px] text-gray-500 font-medium">模型</span>
|
||||
<select v-model="selectedModelId" class="px-2.5 py-1.5 bg-gray-800/80 border border-gray-700/50 rounded-lg text-xs text-gray-300 focus:outline-none focus:border-emerald-500/50 transition-colors">
|
||||
<option :value="null">默认</option>
|
||||
<option v-for="m in availableModels" :key="m.id" :value="m.id">
|
||||
{{ m.model_name || m.model_id }}
|
||||
@@ -94,7 +99,7 @@
|
||||
<template v-if="m.is_default"> (默认)</template>
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="selectedModel?.web_search_enabled" class="text-xs text-blue-400">🌐 联网搜索</span>
|
||||
<span v-if="selectedModel?.web_search_enabled" class="text-[11px] text-blue-400 bg-blue-500/10 px-2 py-0.5 rounded-md">🌐 联网搜索</span>
|
||||
</div>
|
||||
<div class="flex gap-3 items-end">
|
||||
<textarea
|
||||
@@ -102,15 +107,15 @@
|
||||
@keydown.enter.exact="handleSend"
|
||||
placeholder="描述项目需求、技术选型问题..."
|
||||
rows="1"
|
||||
class="flex-1 px-4 py-2.5 bg-gray-800 border border-gray-700 rounded-xl text-gray-100 placeholder-gray-600 focus:outline-none focus:border-indigo-500 resize-none max-h-32 text-sm"
|
||||
class="flex-1 px-4 py-2.5 bg-gray-800/80 border border-gray-700/50 rounded-xl text-gray-100 placeholder-gray-600 focus:outline-none focus:border-emerald-500/50 focus:shadow-[0_0_0_3px_rgba(16,185,129,0.1)] resize-none max-h-32 text-sm transition-all duration-200"
|
||||
:disabled="isStreaming"
|
||||
></textarea>
|
||||
<button
|
||||
@click="handleSend"
|
||||
:disabled="isStreaming || !inputText.trim()"
|
||||
class="px-4 py-2.5 bg-indigo-600 hover:bg-indigo-700 disabled:opacity-30 text-white rounded-xl transition-colors shrink-0 text-sm"
|
||||
class="px-4 py-2.5 bg-emerald-600 hover:bg-emerald-500 disabled:opacity-30 text-white rounded-xl transition-all duration-200 shrink-0 text-sm hover:shadow-lg hover:shadow-emerald-500/20"
|
||||
>
|
||||
发送
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<template>
|
||||
<div class="h-full overflow-y-auto">
|
||||
<div class="max-w-4xl mx-auto px-6 py-8">
|
||||
<div class="max-w-4xl mx-auto px-6 py-8 animate-fade-in">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<h1 class="text-xl font-bold text-white">草稿箱</h1>
|
||||
<h1 class="text-xl font-bold text-white flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||
草稿箱
|
||||
</h1>
|
||||
<span class="text-sm text-gray-500">{{ total }} 篇草稿</span>
|
||||
</div>
|
||||
<router-link to="/post/new" class="px-4 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white text-sm rounded-lg transition-colors">
|
||||
<router-link to="/post/new" class="px-4 py-1.5 bg-indigo-600 hover:bg-indigo-500 hover:shadow-lg hover:shadow-indigo-500/20 text-white text-sm rounded-xl transition-all">
|
||||
写文章
|
||||
</router-link>
|
||||
</div>
|
||||
@@ -16,11 +19,13 @@
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else-if="drafts.length === 0" class="text-center py-20">
|
||||
<svg class="w-12 h-12 text-gray-700 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
<p class="text-gray-600 text-sm">暂无草稿</p>
|
||||
<p class="text-gray-700 text-xs mt-1">写文章时保存的草稿会显示在这里</p>
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gray-800/60 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">暂无草稿</p>
|
||||
<p class="text-gray-600 text-xs mt-1">写文章时保存的草稿会显示在这里</p>
|
||||
</div>
|
||||
|
||||
<!-- 草稿列表 -->
|
||||
@@ -28,7 +33,7 @@
|
||||
<div
|
||||
v-for="draft in drafts"
|
||||
:key="draft.id"
|
||||
class="bg-gray-900/60 border border-gray-800/60 rounded-xl p-5 hover:border-gray-700/60 transition-colors group"
|
||||
class="bg-gray-900/60 border border-gray-800/60 rounded-2xl p-5 hover:border-indigo-500/30 transition-all group"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex-1 min-w-0 cursor-pointer" @click="editDraft(draft)">
|
||||
|
||||
@@ -1,118 +1,130 @@
|
||||
<template>
|
||||
<div class="h-full overflow-y-auto">
|
||||
<div class="max-w-4xl mx-auto px-6 py-6">
|
||||
<div class="h-full overflow-y-auto" ref="scrollContainer">
|
||||
<div class="max-w-4xl mx-auto px-6 py-6 animate-fade-in">
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="relative flex-1 max-w-md">
|
||||
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||
<div class="flex items-center gap-3 mb-5">
|
||||
<div class="relative flex-1 max-w-lg group">
|
||||
<svg class="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500 transition-colors group-focus-within:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
@keydown.enter="handleSearch"
|
||||
placeholder="搜索文章..."
|
||||
class="w-full pl-9 pr-8 py-2 bg-gray-900 border border-gray-800 rounded-lg text-gray-100 placeholder-gray-600 text-sm focus:outline-none focus:border-indigo-500 transition-colors"
|
||||
placeholder="搜索文章、标签、作者..."
|
||||
class="w-full pl-10 pr-9 py-2.5 bg-gray-900/80 border border-gray-800/60 rounded-xl text-gray-100 placeholder-gray-600 text-sm focus:outline-none focus:border-indigo-500/50 focus:bg-gray-900 focus:shadow-[0_0_0_3px_rgba(99,102,241,0.1)] transition-all duration-200"
|
||||
/>
|
||||
<button v-if="searchQuery" @click="clearSearch" class="absolute right-2 top-1/2 -translate-y-1/2 p-0.5 text-gray-500 hover:text-gray-300">
|
||||
<button v-if="searchQuery" @click="clearSearch" class="absolute right-3 top-1/2 -translate-y-1/2 p-0.5 text-gray-500 hover:text-gray-300 transition-colors">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<router-link
|
||||
to="/post/new"
|
||||
class="px-4 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white text-sm rounded-xl transition-all duration-200 hover:shadow-lg hover:shadow-indigo-500/20 flex items-center gap-1.5 shrink-0 font-medium"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
||||
写经验
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- 搜索结果提示 -->
|
||||
<div v-if="isSearching" class="flex items-center gap-2 mb-4 text-sm">
|
||||
<span class="text-gray-400">搜索「<span class="text-indigo-400">{{ searchQuery }}</span>」的结果</span>
|
||||
<button @click="clearSearch" class="text-xs text-gray-500 hover:text-indigo-400 transition-colors">清除搜索</button>
|
||||
<div v-if="isSearching" class="flex items-center gap-2 mb-4 text-sm px-1">
|
||||
<span class="text-gray-400">搜索「<span class="text-indigo-400 font-medium">{{ searchQuery }}</span>」的结果</span>
|
||||
<button @click="clearSearch" class="text-xs text-gray-500 hover:text-indigo-400 transition-colors underline">清除</button>
|
||||
</div>
|
||||
|
||||
<!-- 顶部分类标签导航 -->
|
||||
<div class="flex items-center gap-1 mb-4 overflow-x-auto pb-1 scrollbar-hide">
|
||||
<div class="flex items-center gap-1.5 mb-4 overflow-x-auto pb-1 scrollbar-hide">
|
||||
<button
|
||||
v-for="cat in categories"
|
||||
:key="cat"
|
||||
@click="selectCategory(cat)"
|
||||
class="px-3.5 py-1.5 rounded-full text-sm whitespace-nowrap transition-colors shrink-0"
|
||||
class="px-3.5 py-1.5 rounded-full text-sm whitespace-nowrap transition-all duration-200 shrink-0 border"
|
||||
:class="activeCategory === cat
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'text-gray-400 hover:text-gray-200 hover:bg-gray-800/60'"
|
||||
? 'bg-indigo-600/20 text-indigo-400 border-indigo-500/30 shadow-sm shadow-indigo-500/10'
|
||||
: 'text-gray-400 hover:text-gray-200 hover:bg-gray-800/60 border-transparent'"
|
||||
>{{ cat }}</button>
|
||||
</div>
|
||||
|
||||
<!-- 二级筛选栏 -->
|
||||
<div class="flex items-center justify-between mb-5 border-b border-gray-800 pb-3">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex items-center justify-between mb-5 border-b border-gray-800/40 pb-3">
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
v-for="tab in sortTabs"
|
||||
:key="tab.key"
|
||||
@click="selectSort(tab.key)"
|
||||
class="text-sm transition-colors relative pb-1"
|
||||
class="px-3 py-1.5 text-sm transition-all duration-200 relative rounded-lg"
|
||||
:class="activeSort === tab.key
|
||||
? 'text-indigo-400 font-medium'
|
||||
: 'text-gray-500 hover:text-gray-300'"
|
||||
? 'text-indigo-400 font-medium bg-indigo-500/10'
|
||||
: 'text-gray-500 hover:text-gray-300 hover:bg-gray-800/40'"
|
||||
>
|
||||
{{ tab.label }}
|
||||
<span v-if="activeSort === tab.key" class="absolute bottom-0 left-0 right-0 h-0.5 bg-indigo-500 rounded-full"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xs text-gray-600">共 {{ total }} 条</span>
|
||||
<router-link
|
||||
to="/post/new"
|
||||
class="px-3 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white text-xs rounded-lg transition-colors"
|
||||
>+ 写经验</router-link>
|
||||
</div>
|
||||
<span class="text-xs text-gray-600">共 {{ total }} 篇</span>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="text-center py-16">
|
||||
<div class="w-6 h-6 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin mx-auto mb-2"></div>
|
||||
<p class="text-sm text-gray-600">加载中...</p>
|
||||
<div v-if="loading" class="text-center py-20">
|
||||
<div class="w-8 h-8 border-2 border-indigo-500/30 border-t-indigo-500 rounded-full animate-spin mx-auto mb-3"></div>
|
||||
<p class="text-sm text-gray-500">加载中...</p>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else-if="posts.length === 0" class="text-center py-20">
|
||||
<svg class="w-12 h-12 text-gray-700 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"/></svg>
|
||||
<p class="text-gray-500 mb-1">{{ isSearching ? '没有找到相关文章' : activeSort === 'feed' ? '关注一些人看看他们的动态吧' : '暂无内容' }}</p>
|
||||
<router-link v-if="activeSort === 'feed' && !isSearching" to="/" class="text-sm text-indigo-400 hover:text-indigo-300">看看热门内容</router-link>
|
||||
<div v-else-if="posts.length === 0" class="text-center py-24">
|
||||
<div class="w-16 h-16 rounded-2xl bg-gray-800/50 flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"/></svg>
|
||||
</div>
|
||||
<p class="text-gray-400 mb-2 font-medium">{{ isSearching ? '没有找到相关文章' : activeSort === 'feed' ? '关注一些人看看他们的动态吧' : '暂无内容' }}</p>
|
||||
<p class="text-sm text-gray-600 mb-4">{{ isSearching ? '换个关键词试试' : '成为第一个分享经验的人' }}</p>
|
||||
<router-link v-if="!isSearching" to="/post/new" class="inline-flex items-center gap-1.5 px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm rounded-lg transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
||||
写篇文章
|
||||
</router-link>
|
||||
<router-link v-else-if="activeSort === 'feed'" to="/" class="text-sm text-indigo-400 hover:text-indigo-300">看看热门内容</router-link>
|
||||
</div>
|
||||
|
||||
<!-- 帖子列表 -->
|
||||
<div v-else class="space-y-px">
|
||||
<div v-else class="space-y-0">
|
||||
<div
|
||||
v-for="post in posts"
|
||||
v-for="(post, index) in posts"
|
||||
:key="post.id"
|
||||
@click="$router.push(`/post/${post.id}`)"
|
||||
class="py-5 border-b border-gray-800/60 cursor-pointer hover:bg-gray-900/40 transition-colors -mx-2 px-2 rounded-lg"
|
||||
class="group py-5 border-b border-gray-800/40 cursor-pointer hover:bg-gray-800/20 transition-all duration-200 -mx-3 px-3 rounded-xl"
|
||||
:style="{ animationDelay: `${index * 30}ms` }"
|
||||
>
|
||||
<!-- 作者行 -->
|
||||
<div class="flex items-center gap-2 mb-2.5">
|
||||
<div
|
||||
@click.stop="$router.push(`/user/${post.author?.id}`)"
|
||||
class="w-8 h-8 rounded-full bg-gray-800 flex items-center justify-center text-xs font-bold text-indigo-400 cursor-pointer hover:ring-1 hover:ring-indigo-500 transition shrink-0 overflow-hidden"
|
||||
class="w-7 h-7 rounded-full bg-gradient-to-br from-indigo-500/20 to-purple-500/20 flex items-center justify-center text-[10px] font-bold text-indigo-400 cursor-pointer hover:ring-2 hover:ring-indigo-500/30 transition-all shrink-0 overflow-hidden"
|
||||
>
|
||||
<img v-if="post.author?.avatar" :src="post.author.avatar" class="w-full h-full object-cover" />
|
||||
<span v-else>{{ post.author?.username?.charAt(0)?.toUpperCase() || '?' }}</span>
|
||||
</div>
|
||||
<span
|
||||
@click.stop="$router.push(`/user/${post.author?.id}`)"
|
||||
class="text-sm text-gray-300 hover:text-indigo-400 cursor-pointer font-medium"
|
||||
class="text-[13px] text-gray-300 hover:text-indigo-400 cursor-pointer font-medium transition-colors"
|
||||
>{{ post.author?.username || '匿名' }}</span>
|
||||
<span class="text-xs text-gray-600">· {{ formatTime(post.created_at) }}</span>
|
||||
<span class="text-[11px] text-gray-600">· {{ formatTime(post.created_at) }}</span>
|
||||
<span v-if="post.category" class="ml-auto px-2 py-0.5 bg-indigo-500/10 text-[10px] text-indigo-400/80 rounded-full border border-indigo-500/10">{{ post.category }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 标题 + 内容 + 封面图 -->
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-[15px] font-semibold text-gray-100 mb-1.5 leading-snug">{{ post.title }}</h3>
|
||||
<h3 class="text-[15px] font-semibold text-gray-100 mb-1.5 leading-snug group-hover:text-indigo-300 transition-colors">{{ post.title }}</h3>
|
||||
<p class="text-sm text-gray-500 line-clamp-2 leading-relaxed">{{ stripContent(post.content) }}</p>
|
||||
</div>
|
||||
<!-- 封面图 -->
|
||||
<div v-if="post.cover_image" class="shrink-0">
|
||||
<img
|
||||
:src="post.cover_image"
|
||||
class="w-[120px] h-[80px] object-cover rounded-lg bg-gray-800"
|
||||
loading="lazy"
|
||||
@error="(e) => e.target.style.display = 'none'"
|
||||
/>
|
||||
<div class="w-[120px] h-[80px] rounded-xl overflow-hidden bg-gray-800 relative">
|
||||
<img
|
||||
:src="post.cover_image"
|
||||
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
@error="(e) => e.target.parentElement.style.display = 'none'"
|
||||
/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-gray-900/30 to-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -137,13 +149,12 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 ml-auto">
|
||||
<span v-if="post.category" class="px-2 py-0.5 bg-gray-800/80 text-[11px] text-gray-500 rounded">{{ post.category }}</span>
|
||||
<template v-if="parseTags(post.tags).length">
|
||||
<span
|
||||
v-for="tag in parseTags(post.tags).slice(0, 2)"
|
||||
:key="tag"
|
||||
class="px-2 py-0.5 bg-gray-800/80 text-[11px] text-gray-500 rounded"
|
||||
>{{ tag }}</span>
|
||||
class="px-2 py-0.5 bg-gray-800/60 text-[11px] text-gray-500 rounded-md hover:text-gray-400 transition-colors"
|
||||
>#{{ tag }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -151,12 +162,12 @@
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="totalPages > 1" class="flex items-center justify-center gap-1 mt-8 pb-4">
|
||||
<div v-if="totalPages > 1" class="flex items-center justify-center gap-1 mt-8 pb-6">
|
||||
<button
|
||||
@click="goPage(page - 1)"
|
||||
:disabled="page <= 1"
|
||||
class="w-8 h-8 flex items-center justify-center rounded-lg text-sm transition-colors"
|
||||
:class="page <= 1 ? 'text-gray-700 cursor-not-allowed' : 'text-gray-400 hover:bg-gray-800'"
|
||||
class="w-8 h-8 flex items-center justify-center rounded-lg text-sm transition-all duration-200"
|
||||
:class="page <= 1 ? 'text-gray-700 cursor-not-allowed' : 'text-gray-400 hover:bg-gray-800 hover:text-gray-200'"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
|
||||
</button>
|
||||
@@ -165,15 +176,15 @@
|
||||
<button
|
||||
v-else
|
||||
@click="goPage(p)"
|
||||
class="w-8 h-8 flex items-center justify-center rounded-lg text-sm transition-colors"
|
||||
:class="page === p ? 'bg-indigo-600 text-white' : 'text-gray-400 hover:bg-gray-800'"
|
||||
class="w-8 h-8 flex items-center justify-center rounded-lg text-sm transition-all duration-200"
|
||||
:class="page === p ? 'bg-indigo-600 text-white shadow-sm shadow-indigo-500/20' : 'text-gray-400 hover:bg-gray-800 hover:text-gray-200'"
|
||||
>{{ p }}</button>
|
||||
</template>
|
||||
<button
|
||||
@click="goPage(page + 1)"
|
||||
:disabled="page >= totalPages"
|
||||
class="w-8 h-8 flex items-center justify-center rounded-lg text-sm transition-colors"
|
||||
:class="page >= totalPages ? 'text-gray-700 cursor-not-allowed' : 'text-gray-400 hover:bg-gray-800'"
|
||||
class="w-8 h-8 flex items-center justify-center rounded-lg text-sm transition-all duration-200"
|
||||
:class="page >= totalPages ? 'text-gray-700 cursor-not-allowed' : 'text-gray-400 hover:bg-gray-800 hover:text-gray-200'"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
||||
</button>
|
||||
@@ -198,6 +209,7 @@ const activeCategory = ref('全部')
|
||||
const activeSort = ref('hot')
|
||||
const posts = ref([])
|
||||
const loading = ref(true)
|
||||
const scrollContainer = ref(null)
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = 20
|
||||
@@ -251,8 +263,7 @@ function goPage(p) {
|
||||
if (p < 1 || p > totalPages.value) return
|
||||
page.value = p
|
||||
loadPosts()
|
||||
// 滚动到顶部
|
||||
document.querySelector('.h-full.overflow-y-auto')?.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
scrollContainer.value?.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
async function loadPosts() {
|
||||
|
||||
@@ -31,14 +31,17 @@
|
||||
<!-- 主界面 -->
|
||||
<template v-else>
|
||||
<!-- 顶部标题栏 -->
|
||||
<div class="px-6 py-4 border-b border-gray-800 flex items-center justify-between shrink-0">
|
||||
<div class="px-6 py-4 border-b border-gray-800/60 flex items-center justify-between shrink-0">
|
||||
<div>
|
||||
<h1 class="text-lg font-bold text-white">团队知识库</h1>
|
||||
<h1 class="text-lg font-bold text-white flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>
|
||||
团队知识库
|
||||
</h1>
|
||||
<p class="text-xs text-gray-500 mt-0.5">共 {{ totalItems }} 篇知识文档</p>
|
||||
</div>
|
||||
<button
|
||||
@click="showAiPanel = !showAiPanel"
|
||||
class="px-3 py-1.5 bg-indigo-600/20 text-indigo-400 text-xs font-medium rounded-lg hover:bg-indigo-600/30 transition-colors flex items-center gap-1.5"
|
||||
class="px-3 py-1.5 bg-indigo-600/20 text-indigo-400 text-xs font-medium rounded-xl hover:bg-indigo-600/30 transition-all flex items-center gap-1.5"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
|
||||
AI 问答
|
||||
@@ -47,7 +50,7 @@
|
||||
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<!-- 左侧分类 -->
|
||||
<aside class="w-44 border-r border-gray-800 py-3 px-2 shrink-0 overflow-y-auto">
|
||||
<aside class="w-44 border-r border-gray-800/60 py-3 px-2 shrink-0 overflow-y-auto">
|
||||
<div
|
||||
@click="selectedCategory = null; fetchItems()"
|
||||
class="flex items-center justify-between px-3 py-1.5 rounded-lg text-xs cursor-pointer transition-colors mb-0.5"
|
||||
@@ -77,7 +80,7 @@
|
||||
<input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索知识库..."
|
||||
class="w-full pl-9 pr-3 py-2 bg-gray-900/50 border border-gray-800 rounded-lg text-sm text-white placeholder-gray-500 focus:outline-none focus:border-indigo-500/50"
|
||||
class="w-full pl-9 pr-3 py-2 bg-gray-900/50 border border-gray-800/60 rounded-xl text-sm text-white placeholder-gray-500 focus:outline-none focus:border-indigo-500/50 focus:shadow-[0_0_0_3px_rgba(99,102,241,0.08)] transition-all"
|
||||
@keyup.enter="fetchItems()"
|
||||
/>
|
||||
</div>
|
||||
@@ -85,13 +88,22 @@
|
||||
|
||||
<!-- 条目列表 -->
|
||||
<div class="flex-1 overflow-y-auto px-4 py-3 space-y-2">
|
||||
<div v-if="loading" class="text-center text-gray-500 text-xs py-10">加载中...</div>
|
||||
<div v-else-if="items.length === 0" class="text-center text-gray-600 text-xs py-10">暂无知识文档</div>
|
||||
<div v-if="loading" class="text-center text-gray-500 text-xs py-10">
|
||||
<div class="w-6 h-6 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin mx-auto mb-2"></div>
|
||||
加载中...
|
||||
</div>
|
||||
<div v-else-if="items.length === 0" class="text-center py-16">
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gray-800/60 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">暂无知识文档</p>
|
||||
<p class="text-gray-600 text-xs mt-1">发布文章后会自动收录到知识库</p>
|
||||
</div>
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
@click="openDetail(item)"
|
||||
class="bg-gray-900/50 border border-gray-800/50 rounded-xl p-4 cursor-pointer hover:border-gray-700 transition-colors group"
|
||||
class="bg-gray-900/50 border border-gray-800/50 rounded-xl p-4 cursor-pointer hover:border-indigo-500/30 transition-all group"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<h3 class="text-sm font-medium text-white group-hover:text-indigo-400 transition-colors">{{ item.title }}</h3>
|
||||
@@ -122,8 +134,8 @@
|
||||
</div>
|
||||
|
||||
<!-- AI 问答面板 -->
|
||||
<aside v-if="showAiPanel" class="w-96 border-l border-gray-800 flex flex-col shrink-0">
|
||||
<div class="px-4 py-3 border-b border-gray-800 flex items-center justify-between">
|
||||
<aside v-if="showAiPanel" class="w-96 border-l border-gray-800/60 flex flex-col shrink-0">
|
||||
<div class="px-4 py-3 border-b border-gray-800/60 flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium text-white">AI 知识问答</h3>
|
||||
<button @click="showAiPanel = false" class="text-gray-500 hover:text-gray-300">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
@@ -151,18 +163,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- 输入区 -->
|
||||
<div class="px-4 py-3 border-t border-gray-800">
|
||||
<div class="px-4 py-3 border-t border-gray-800/60">
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="aiQuestion"
|
||||
placeholder="输入你的问题..."
|
||||
class="flex-1 px-3 py-2 bg-gray-900 border border-gray-800 rounded-lg text-xs text-white placeholder-gray-500 focus:outline-none focus:border-indigo-500"
|
||||
class="flex-1 px-3 py-2 bg-gray-900 border border-gray-800/60 rounded-xl text-xs text-white placeholder-gray-500 focus:outline-none focus:border-indigo-500/50 focus:shadow-[0_0_0_3px_rgba(99,102,241,0.08)] transition-all"
|
||||
@keyup.enter="sendAiQuestion"
|
||||
/>
|
||||
<button
|
||||
@click="sendAiQuestion"
|
||||
:disabled="aiLoading || !aiQuestion.trim()"
|
||||
class="px-3 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-xs rounded-lg transition-colors disabled:opacity-50 shrink-0"
|
||||
class="px-3 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-xs rounded-xl transition-all disabled:opacity-50 shrink-0"
|
||||
>发送</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,94 +1,127 @@
|
||||
<template>
|
||||
<div class="h-screen bg-gray-950 text-gray-100 flex overflow-hidden">
|
||||
<!-- 移动端遮罩 -->
|
||||
<div v-if="mobileOpen" class="fixed inset-0 bg-black/60 z-40 lg:hidden" @click="mobileOpen = false"></div>
|
||||
|
||||
<!-- 侧边栏 -->
|
||||
<aside
|
||||
class="bg-gray-900 border-r border-gray-800 flex flex-col shrink-0 transition-all duration-200"
|
||||
:class="collapsed ? 'w-14' : 'w-52'"
|
||||
class="bg-gray-900/95 backdrop-blur-sm border-r border-gray-800/60 flex flex-col shrink-0 transition-all duration-300 z-50"
|
||||
:class="[
|
||||
collapsed ? 'w-[60px]' : 'w-56',
|
||||
mobileOpen ? 'fixed inset-y-0 left-0 w-56' : 'max-lg:hidden'
|
||||
]"
|
||||
>
|
||||
<!-- Logo -->
|
||||
<div class="px-3 py-4 border-b border-gray-800 flex items-center gap-2.5 h-14">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-600 flex items-center justify-center shrink-0">
|
||||
<div class="px-3 h-14 border-b border-gray-800/60 flex items-center gap-2.5 bg-gradient-to-r from-indigo-600/10 to-transparent">
|
||||
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-indigo-500 to-indigo-700 flex items-center justify-center shrink-0 shadow-lg shadow-indigo-500/20">
|
||||
<svg class="w-4.5 h-4.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
|
||||
</div>
|
||||
<span v-show="!collapsed" class="text-sm font-bold text-white whitespace-nowrap">极码 GeekCode</span>
|
||||
<span v-show="!collapsed" class="text-sm font-bold text-white whitespace-nowrap tracking-wide">极码 GeekCode</span>
|
||||
</div>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
<nav class="flex-1 py-2 px-2 space-y-0.5 overflow-y-auto">
|
||||
<router-link
|
||||
v-for="item in navItems"
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
class="flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-sm transition-colors group relative"
|
||||
:class="isActive(item.path, item.exact) ? 'bg-indigo-600/20 text-indigo-400' : 'text-gray-400 hover:text-gray-200 hover:bg-gray-800/60'"
|
||||
>
|
||||
<div class="w-5 h-5 shrink-0 flex items-center justify-center" v-html="item.icon"></div>
|
||||
<span v-show="!collapsed" class="whitespace-nowrap">{{ item.label }}</span>
|
||||
<!-- 消息角标 -->
|
||||
<span
|
||||
v-if="item.path === '/notifications' && unreadCount > 0"
|
||||
class="absolute right-2 top-1.5 min-w-[16px] h-4 px-1 bg-red-500 text-white text-[10px] rounded-full flex items-center justify-center"
|
||||
:class="collapsed ? 'right-0.5 top-0.5' : ''"
|
||||
>{{ unreadCount > 99 ? '99+' : unreadCount }}</span>
|
||||
</router-link>
|
||||
<nav class="flex-1 py-3 px-2 overflow-y-auto sidebar-scroll relative">
|
||||
<!-- 分组:核心 -->
|
||||
<div v-if="!collapsed" class="px-2.5 mb-1.5 text-[10px] font-semibold text-gray-500 uppercase tracking-wider">核心</div>
|
||||
<template v-for="(item, idx) in navItems" :key="item.path">
|
||||
<!-- 分组标题 -->
|
||||
<div v-if="item.group && !collapsed" class="px-2.5 mt-4 mb-1.5 text-[10px] font-semibold text-gray-500 uppercase tracking-wider">{{ item.group }}</div>
|
||||
<div v-else-if="item.group && collapsed" class="my-2 mx-2 border-t border-gray-800/60"></div>
|
||||
<router-link
|
||||
:to="item.path"
|
||||
class="nav-item flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-[13px] transition-all duration-200 group relative mb-0.5"
|
||||
:class="isActive(item.path, item.exact)
|
||||
? 'bg-indigo-500/15 text-indigo-400 shadow-sm shadow-indigo-500/5'
|
||||
: 'text-gray-400 hover:text-gray-200 hover:bg-gray-800/50'"
|
||||
>
|
||||
<!-- Active 指示条 -->
|
||||
<div
|
||||
v-if="isActive(item.path, item.exact)"
|
||||
class="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-4 bg-indigo-500 rounded-r-full"
|
||||
></div>
|
||||
<div class="w-5 h-5 shrink-0 flex items-center justify-center transition-transform duration-200 group-hover:scale-110" v-html="item.icon"></div>
|
||||
<span v-show="!collapsed" class="whitespace-nowrap">{{ item.label }}</span>
|
||||
<!-- 折叠 tooltip -->
|
||||
<div v-if="collapsed" class="absolute left-full ml-2 px-2 py-1 bg-gray-800 text-gray-200 text-xs rounded-md shadow-lg opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50">
|
||||
{{ item.label }}
|
||||
</div>
|
||||
<!-- 消息角标 -->
|
||||
<span
|
||||
v-if="item.path === '/notifications' && unreadCount > 0"
|
||||
class="absolute min-w-[16px] h-4 px-1 bg-red-500 text-white text-[10px] rounded-full flex items-center justify-center font-medium"
|
||||
:class="collapsed ? 'right-0.5 top-0.5' : 'right-2 top-1.5'"
|
||||
>{{ unreadCount > 99 ? '99+' : unreadCount }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<!-- 外部链接 -->
|
||||
<div v-if="!collapsed" class="px-2.5 mt-4 mb-1.5 text-[10px] font-semibold text-gray-500 uppercase tracking-wider">更多</div>
|
||||
<div v-else class="my-2 mx-2 border-t border-gray-800/60"></div>
|
||||
<a
|
||||
href="https://qoder.com/marketplace"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-sm transition-colors text-gray-400 hover:text-gray-200 hover:bg-gray-800/60 group"
|
||||
class="nav-item flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-[13px] transition-all duration-200 text-gray-400 hover:text-gray-200 hover:bg-gray-800/50 group relative mb-0.5"
|
||||
>
|
||||
<div class="w-5 h-5 shrink-0 flex items-center justify-center">
|
||||
<div class="w-5 h-5 shrink-0 flex items-center justify-center transition-transform duration-200 group-hover:scale-110">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 100 4 2 2 0 000-4z"/></svg>
|
||||
</div>
|
||||
<span v-show="!collapsed" class="whitespace-nowrap">Qoder 技能市场</span>
|
||||
<svg v-show="!collapsed" class="w-3 h-3 ml-auto text-gray-600 group-hover:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
|
||||
<svg v-show="!collapsed" class="w-3 h-3 ml-auto text-gray-600 group-hover:text-gray-400 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
|
||||
<div v-if="collapsed" class="absolute left-full ml-2 px-2 py-1 bg-gray-800 text-gray-200 text-xs rounded-md shadow-lg opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50">
|
||||
Qoder 技能市场
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="my-2 mx-1 border-t border-gray-800"></div>
|
||||
|
||||
<!-- 管理员菜单 -->
|
||||
<router-link
|
||||
v-if="isAdmin"
|
||||
to="/admin"
|
||||
class="flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-sm transition-colors"
|
||||
:class="isActive('/admin') ? 'bg-indigo-600/20 text-indigo-400' : 'text-gray-400 hover:text-gray-200 hover:bg-gray-800/60'"
|
||||
class="nav-item flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-[13px] transition-all duration-200 group relative mb-0.5"
|
||||
:class="isActive('/admin') ? 'bg-indigo-500/15 text-indigo-400' : 'text-gray-400 hover:text-gray-200 hover:bg-gray-800/50'"
|
||||
>
|
||||
<div class="w-5 h-5 shrink-0 flex items-center justify-center">
|
||||
<div v-if="isActive('/admin')" class="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-4 bg-indigo-500 rounded-r-full"></div>
|
||||
<div class="w-5 h-5 shrink-0 flex items-center justify-center transition-transform duration-200 group-hover:scale-110">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
||||
</div>
|
||||
<span v-show="!collapsed">管理</span>
|
||||
<span v-show="!collapsed">管理后台</span>
|
||||
<div v-if="collapsed" class="absolute left-full ml-2 px-2 py-1 bg-gray-800 text-gray-200 text-xs rounded-md shadow-lg opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50">管理后台</div>
|
||||
</router-link>
|
||||
|
||||
</nav>
|
||||
|
||||
<!-- 底部:折叠按钮 + 用户 -->
|
||||
<div class="border-t border-gray-800 px-2 py-2 space-y-1">
|
||||
<!-- 底部:用户 + 操作 -->
|
||||
<div class="border-t border-gray-800/60 px-2 py-2 space-y-1">
|
||||
<!-- 用户 -->
|
||||
<router-link
|
||||
to="/profile"
|
||||
class="flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-sm text-gray-400 hover:text-gray-200 hover:bg-gray-800/60 transition-colors"
|
||||
class="flex items-center gap-2.5 px-2 py-2 rounded-lg text-sm text-gray-400 hover:text-gray-200 hover:bg-gray-800/50 transition-all duration-200 group relative"
|
||||
>
|
||||
<div class="w-5 h-5 rounded-full bg-gray-700 flex items-center justify-center shrink-0 text-[10px] font-bold text-indigo-400 overflow-hidden">
|
||||
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center shrink-0 text-[11px] font-bold text-white overflow-hidden ring-2 ring-gray-800 group-hover:ring-indigo-500/30 transition-all">
|
||||
<img v-if="userStore.user?.avatar" :src="userStore.user.avatar" class="w-full h-full object-cover" />
|
||||
<span v-else>{{ userStore.user?.username?.charAt(0)?.toUpperCase() || 'U' }}</span>
|
||||
</div>
|
||||
<span v-show="!collapsed" class="truncate">{{ userStore.user?.username }}</span>
|
||||
<div v-show="!collapsed" class="flex-1 min-w-0">
|
||||
<div class="text-[13px] font-medium text-gray-200 truncate">{{ userStore.user?.username }}</div>
|
||||
<div class="text-[10px] text-gray-500">查看资料</div>
|
||||
</div>
|
||||
<div v-if="collapsed" class="absolute left-full ml-2 px-2 py-1 bg-gray-800 text-gray-200 text-xs rounded-md shadow-lg opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50">
|
||||
{{ userStore.user?.username }}
|
||||
</div>
|
||||
</router-link>
|
||||
<!-- 折叠/退出 -->
|
||||
<div class="flex items-center" :class="collapsed ? 'justify-center' : 'justify-between px-2.5'">
|
||||
<div class="flex items-center" :class="collapsed ? 'justify-center' : 'justify-between px-2'">
|
||||
<button
|
||||
@click="collapsed = !collapsed"
|
||||
class="p-1.5 text-gray-500 hover:text-gray-300 rounded transition-colors"
|
||||
class="p-1.5 text-gray-500 hover:text-gray-300 rounded-md hover:bg-gray-800/50 transition-all duration-200"
|
||||
:title="collapsed ? '展开' : '收起'"
|
||||
>
|
||||
<svg class="w-4 h-4 transition-transform" :class="collapsed ? 'rotate-180' : ''" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7"/></svg>
|
||||
<svg class="w-4 h-4 transition-transform duration-300" :class="collapsed ? 'rotate-180' : ''" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7"/></svg>
|
||||
</button>
|
||||
<button
|
||||
v-show="!collapsed"
|
||||
@click="handleLogout"
|
||||
class="p-1.5 text-gray-600 hover:text-red-400 rounded transition-colors"
|
||||
class="p-1.5 text-gray-600 hover:text-red-400 rounded-md hover:bg-red-500/10 transition-all duration-200"
|
||||
title="退出登录"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
|
||||
@@ -98,8 +131,17 @@
|
||||
</aside>
|
||||
|
||||
<!-- 主内容 -->
|
||||
<main class="flex-1 overflow-hidden">
|
||||
<router-view />
|
||||
<main class="flex-1 overflow-hidden flex flex-col">
|
||||
<!-- 移动端顶栏 -->
|
||||
<div class="h-14 border-b border-gray-800/60 flex items-center px-4 gap-3 lg:hidden bg-gray-900/80 backdrop-blur-sm shrink-0">
|
||||
<button @click="mobileOpen = true" class="p-1.5 text-gray-400 hover:text-white rounded-lg hover:bg-gray-800/50">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>
|
||||
</button>
|
||||
<span class="text-sm font-bold text-white">极码 GeekCode</span>
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<router-view />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -115,6 +157,7 @@ const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const collapsed = ref(false)
|
||||
const mobileOpen = ref(false)
|
||||
const unreadCount = ref(0)
|
||||
|
||||
const isAdmin = computed(() => userStore.user?.is_admin === true)
|
||||
@@ -129,7 +172,7 @@ const navItems = [
|
||||
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/></svg>',
|
||||
},
|
||||
{
|
||||
path: '/post/new', label: '写文章',
|
||||
path: '/post/new', label: '写文章', group: '内容',
|
||||
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>',
|
||||
},
|
||||
{
|
||||
@@ -141,7 +184,7 @@ const navItems = [
|
||||
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg>',
|
||||
},
|
||||
{
|
||||
path: '/browser', label: '浏览器',
|
||||
path: '/browser', label: '浏览器', group: '发现',
|
||||
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/></svg>',
|
||||
},
|
||||
{
|
||||
@@ -172,7 +215,6 @@ async function fetchUnread() {
|
||||
|
||||
onMounted(() => {
|
||||
fetchUnread()
|
||||
// 每30秒轮询未读数
|
||||
setInterval(fetchUnread, 30000)
|
||||
})
|
||||
|
||||
@@ -181,3 +223,8 @@ function handleLogout() {
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar-scroll::-webkit-scrollbar { width: 0; }
|
||||
.sidebar-scroll { scrollbar-width: none; }
|
||||
</style>
|
||||
|
||||
@@ -1,77 +1,186 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-950 flex items-center justify-center">
|
||||
<div class="w-full max-w-md p-8">
|
||||
<h1 class="text-3xl font-bold text-center text-indigo-400 mb-2">极码 GeekCode</h1>
|
||||
<p class="text-center text-gray-500 mb-8">需求理解 · 架构选型 · 经验沉淀</p>
|
||||
<div class="min-h-screen bg-gray-950 flex">
|
||||
<!-- 左侧品牌展示区 -->
|
||||
<div class="hidden lg:flex lg:w-1/2 relative overflow-hidden">
|
||||
<!-- 背景装饰 -->
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-indigo-950/80 via-gray-950 to-purple-950/60"></div>
|
||||
<!-- 网格背景 -->
|
||||
<div class="absolute inset-0 opacity-[0.03]" style="background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%239C92AC' fill-opacity='1'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")"></div>
|
||||
<!-- 装饰性光圈 -->
|
||||
<div class="absolute top-1/4 left-1/4 w-96 h-96 bg-indigo-500/10 rounded-full blur-3xl"></div>
|
||||
<div class="absolute bottom-1/4 right-1/4 w-80 h-80 bg-purple-500/10 rounded-full blur-3xl"></div>
|
||||
|
||||
<div class="bg-gray-900 rounded-xl p-6 border border-gray-800">
|
||||
<!-- 切换登录/注册 -->
|
||||
<div class="flex mb-6 bg-gray-800 rounded-lg p-1">
|
||||
<button
|
||||
@click="isLogin = true"
|
||||
class="flex-1 py-2 text-sm rounded-md transition-colors"
|
||||
:class="isLogin ? 'bg-indigo-600 text-white' : 'text-gray-400'"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
<button
|
||||
@click="isLogin = false"
|
||||
class="flex-1 py-2 text-sm rounded-md transition-colors"
|
||||
:class="!isLogin ? 'bg-indigo-600 text-white' : 'text-gray-400'"
|
||||
>
|
||||
注册
|
||||
</button>
|
||||
<div class="relative z-10 flex flex-col justify-center px-16 xl:px-20 w-full">
|
||||
<!-- Logo 和标语 -->
|
||||
<div class="mb-12">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="w-12 h-12 rounded-xl bg-indigo-600 flex items-center justify-center shadow-lg shadow-indigo-500/20">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
|
||||
</div>
|
||||
<span class="text-2xl font-bold text-white">极码 GeekCode</span>
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold text-white leading-tight mb-3">
|
||||
AI 驱动的<br/>
|
||||
<span class="text-transparent bg-clip-text bg-gradient-to-r from-indigo-400 to-purple-400">编程效率平台</span>
|
||||
</h2>
|
||||
<p class="text-gray-400 text-base leading-relaxed">从需求理解到架构设计,从经验沉淀到知识共享,让 AI 成为你的编程搭档</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleSubmit" autocomplete="off">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm text-gray-400 mb-1">用户名</label>
|
||||
<input
|
||||
v-model="form.username"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
placeholder="请输入用户名"
|
||||
class="w-full px-4 py-2.5 bg-gray-800 border border-gray-700 rounded-lg text-gray-100 placeholder-gray-600 focus:outline-none focus:border-indigo-500"
|
||||
required
|
||||
/>
|
||||
<!-- 特性卡片 -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center gap-4 p-4 rounded-xl bg-white/[0.03] border border-white/[0.06] backdrop-blur-sm">
|
||||
<div class="w-10 h-10 rounded-lg bg-indigo-600/20 flex items-center justify-center shrink-0">
|
||||
<svg class="w-5 h-5 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-200">需求理解助手</h4>
|
||||
<p class="text-xs text-gray-500 mt-0.5">甲方需求秒变功能清单,支持截图识别</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 p-4 rounded-xl bg-white/[0.03] border border-white/[0.06] backdrop-blur-sm">
|
||||
<div class="w-10 h-10 rounded-lg bg-emerald-600/20 flex items-center justify-center shrink-0">
|
||||
<svg class="w-5 h-5 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-200">架构选型助手</h4>
|
||||
<p class="text-xs text-gray-500 mt-0.5">智能推荐技术栈,自动生成架构设计方案</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 p-4 rounded-xl bg-white/[0.03] border border-white/[0.06] backdrop-blur-sm">
|
||||
<div class="w-10 h-10 rounded-lg bg-amber-600/20 flex items-center justify-center shrink-0">
|
||||
<svg class="w-5 h-5 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-200">经验知识库</h4>
|
||||
<p class="text-xs text-gray-500 mt-0.5">团队踩坑记录沉淀,AI 智能问答检索</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 p-4 rounded-xl bg-white/[0.03] border border-white/[0.06] backdrop-blur-sm">
|
||||
<div class="w-10 h-10 rounded-lg bg-blue-600/20 flex items-center justify-center shrink-0">
|
||||
<svg class="w-5 h-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-200">联网搜索助手</h4>
|
||||
<p class="text-xs text-gray-500 mt-0.5">实时联网搜索,智能整合最新技术资讯</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧登录表单 -->
|
||||
<div class="w-full lg:w-1/2 flex items-center justify-center px-6">
|
||||
<div class="w-full max-w-md animate-fade-in">
|
||||
<!-- 移动端 Logo -->
|
||||
<div class="lg:hidden text-center mb-8">
|
||||
<div class="w-12 h-12 rounded-xl bg-indigo-600 flex items-center justify-center mx-auto mb-4 shadow-lg shadow-indigo-500/20">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold text-white">极码 GeekCode</h1>
|
||||
<p class="text-gray-500 text-sm mt-1">需求理解 · 架构选型 · 经验沉淀</p>
|
||||
</div>
|
||||
|
||||
<!-- 欢迎语(桌面端) -->
|
||||
<div class="hidden lg:block mb-8">
|
||||
<h1 class="text-2xl font-bold text-white mb-2">欢迎回来</h1>
|
||||
<p class="text-gray-500 text-sm">登录你的账号,继续探索</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-900/80 rounded-2xl p-7 border border-gray-800/80 backdrop-blur-sm shadow-xl">
|
||||
<!-- 切换登录/注册 -->
|
||||
<div class="flex mb-6 bg-gray-800/80 rounded-xl p-1">
|
||||
<button
|
||||
@click="isLogin = true"
|
||||
class="flex-1 py-2.5 text-sm rounded-lg transition-all duration-200 font-medium"
|
||||
:class="isLogin ? 'bg-indigo-600 text-white shadow-md shadow-indigo-500/20' : 'text-gray-400 hover:text-gray-300'"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
<button
|
||||
@click="isLogin = false"
|
||||
class="flex-1 py-2.5 text-sm rounded-lg transition-all duration-200 font-medium"
|
||||
:class="!isLogin ? 'bg-indigo-600 text-white shadow-md shadow-indigo-500/20' : 'text-gray-400 hover:text-gray-300'"
|
||||
>
|
||||
注册
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 注册时显示邮箱 -->
|
||||
<div v-if="!isLogin" class="mb-4">
|
||||
<label class="block text-sm text-gray-400 mb-1">邮箱</label>
|
||||
<input
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
autocomplete="off"
|
||||
placeholder="请输入邮箱"
|
||||
class="w-full px-4 py-2.5 bg-gray-800 border border-gray-700 rounded-lg text-gray-100 placeholder-gray-600 focus:outline-none focus:border-indigo-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<form @submit.prevent="handleSubmit" autocomplete="off">
|
||||
<div class="space-y-4">
|
||||
<!-- 用户名 -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-400 mb-1.5">用户名</label>
|
||||
<div class="relative">
|
||||
<svg class="w-4.5 h-4.5 text-gray-500 absolute left-3.5 top-1/2 -translate-y-1/2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
|
||||
<input
|
||||
v-model="form.username"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
placeholder="请输入用户名"
|
||||
class="w-full pl-10 pr-4 py-2.5 bg-gray-800/60 border border-gray-700/60 rounded-xl text-gray-100 placeholder-gray-600 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 transition-all text-sm"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm text-gray-400 mb-1">密码</label>
|
||||
<input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
placeholder="请输入密码"
|
||||
class="w-full px-4 py-2.5 bg-gray-800 border border-gray-700 rounded-lg text-gray-100 placeholder-gray-600 focus:outline-none focus:border-indigo-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<!-- 注册时显示邮箱 -->
|
||||
<div v-if="!isLogin">
|
||||
<label class="block text-xs font-medium text-gray-400 mb-1.5">邮箱</label>
|
||||
<div class="relative">
|
||||
<svg class="w-4.5 h-4.5 text-gray-500 absolute left-3.5 top-1/2 -translate-y-1/2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
|
||||
<input
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
autocomplete="off"
|
||||
placeholder="请输入邮箱"
|
||||
class="w-full pl-10 pr-4 py-2.5 bg-gray-800/60 border border-gray-700/60 rounded-xl text-gray-100 placeholder-gray-600 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 transition-all text-sm"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="text-red-400 text-sm mb-4">{{ error }}</p>
|
||||
<p v-if="successMsg" class="text-green-400 text-sm mb-4 bg-green-900/20 border border-green-800/40 rounded-lg px-4 py-3">{{ successMsg }}</p>
|
||||
<!-- 密码 -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-400 mb-1.5">密码</label>
|
||||
<div class="relative">
|
||||
<svg class="w-4.5 h-4.5 text-gray-500 absolute left-3.5 top-1/2 -translate-y-1/2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg>
|
||||
<input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
placeholder="请输入密码"
|
||||
class="w-full pl-10 pr-4 py-2.5 bg-gray-800/60 border border-gray-700/60 rounded-xl text-gray-100 placeholder-gray-600 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 transition-all text-sm"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="w-full py-2.5 bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 text-white rounded-lg transition-colors"
|
||||
>
|
||||
{{ loading ? '处理中...' : (isLogin ? '登录' : '注册') }}
|
||||
</button>
|
||||
</form>
|
||||
<p v-if="error" class="text-red-400 text-sm mt-4 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
{{ error }}
|
||||
</p>
|
||||
<div v-if="successMsg" class="mt-4 flex items-start gap-2 text-green-400 text-sm bg-green-900/20 border border-green-800/40 rounded-xl px-4 py-3">
|
||||
<svg class="w-4 h-4 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
{{ successMsg }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="w-full py-3 mt-6 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white rounded-xl transition-all duration-200 font-medium text-sm hover:shadow-lg hover:shadow-indigo-500/25 active:scale-[0.98]"
|
||||
>
|
||||
<span v-if="loading" class="flex items-center justify-center gap-2">
|
||||
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
|
||||
处理中...
|
||||
</span>
|
||||
<span v-else>{{ isLogin ? '登录' : '注册' }}</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 底部版权 -->
|
||||
<p class="text-center text-xs text-gray-700 mt-8">© 2026 极码 GeekCode. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
<template>
|
||||
<div class="max-w-6xl mx-auto p-6">
|
||||
<div class="max-w-6xl mx-auto p-6 animate-fade-in">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-100">AI 模型管理</h1>
|
||||
<h1 class="text-2xl font-bold text-gray-100 flex items-center gap-2">
|
||||
<svg class="w-6 h-6 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
|
||||
AI 模型管理
|
||||
</h1>
|
||||
<p class="text-gray-500 mt-1">管理各AI服务商的模型配置和API Key</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button @click="initDefaults" class="px-4 py-2 border border-gray-600 text-gray-300 rounded-lg hover:bg-gray-800 text-sm">
|
||||
<button @click="initDefaults" class="px-4 py-2 border border-gray-700 text-gray-300 rounded-xl hover:bg-gray-800 hover:border-gray-600 text-sm transition-all">
|
||||
初始化默认配置
|
||||
</button>
|
||||
<button @click="showAddModal = true" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 text-sm">
|
||||
<button @click="showAddModal = true" class="px-4 py-2 bg-indigo-600 text-white rounded-xl hover:bg-indigo-500 hover:shadow-lg hover:shadow-indigo-500/20 text-sm transition-all">
|
||||
+ 添加模型
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务类型卡片 -->
|
||||
<div class="grid grid-cols-5 gap-4 mb-6">
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 mb-6">
|
||||
<div v-for="(label, key) in taskTypes" :key="key"
|
||||
class="bg-gray-900 rounded-lg border p-4 cursor-pointer transition-all"
|
||||
:class="filterTask === key ? 'border-indigo-500 ring-2 ring-indigo-500/20' : 'border-gray-700 hover:border-gray-600'"
|
||||
class="bg-gray-900/80 rounded-xl border p-4 cursor-pointer transition-all"
|
||||
:class="filterTask === key ? 'border-indigo-500 ring-2 ring-indigo-500/20' : 'border-gray-800/60 hover:border-gray-700'"
|
||||
@click="filterTask = filterTask === key ? '' : key">
|
||||
<div class="text-sm font-medium text-gray-200">{{ label }}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">{{ key }}</div>
|
||||
@@ -33,9 +36,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 模型列表 -->
|
||||
<div class="bg-gray-900 rounded-lg border border-gray-700 overflow-hidden">
|
||||
<div class="bg-gray-900/80 rounded-xl border border-gray-800/60 overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-800">
|
||||
<thead class="bg-gray-800/60">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-3 text-sm font-medium text-gray-400">服务商</th>
|
||||
<th class="text-left px-4 py-3 text-sm font-medium text-gray-400">模型</th>
|
||||
@@ -46,7 +49,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="model in filteredModels" :key="model.id" class="border-t border-gray-800 hover:bg-gray-800/50">
|
||||
<tr v-for="model in filteredModels" :key="model.id" class="border-t border-gray-800/60 hover:bg-gray-800/30 transition-colors">
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full" :class="providerColor(model.provider)"></span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="h-full overflow-y-auto">
|
||||
<div class="max-w-5xl mx-auto px-6 py-6">
|
||||
<div class="max-w-5xl mx-auto px-6 py-6 animate-fade-in">
|
||||
<!-- 标题区 -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
@@ -8,27 +8,24 @@
|
||||
<p class="text-xs text-gray-500 mt-1">精选开发者常用网站与工具</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- 我的提交 -->
|
||||
<button v-if="mySubmissions.length > 0" @click="showMySubmissions = true" class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-gray-400 hover:text-gray-200 border border-gray-800 rounded-lg hover:border-gray-700 transition-colors">
|
||||
<button v-if="mySubmissions.length > 0" @click="showMySubmissions = true" class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-gray-400 hover:text-gray-200 border border-gray-800/60 rounded-lg hover:border-gray-700 transition-all duration-200">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>
|
||||
我的提交
|
||||
</button>
|
||||
<!-- 推荐网站 -->
|
||||
<button @click="openSubmitModal" class="flex items-center gap-1.5 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white text-xs rounded-lg transition-colors">
|
||||
<button @click="openSubmitModal" class="flex items-center gap-1.5 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white text-xs rounded-lg transition-all duration-200 hover:shadow-lg hover:shadow-indigo-500/20">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
||||
推荐网站
|
||||
</button>
|
||||
<!-- 搜索 -->
|
||||
<div class="relative w-52">
|
||||
<svg class="w-4 h-4 text-gray-600 absolute left-3 top-1/2 -translate-y-1/2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索网站..."
|
||||
class="w-full pl-9 pr-8 py-2 bg-gray-900 border border-gray-800 rounded-lg text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-indigo-500"
|
||||
/>
|
||||
<button v-if="searchQuery" @click="searchQuery = ''" class="absolute right-2 top-1/2 -translate-y-1/2 p-0.5 text-gray-600 hover:text-gray-300">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
<div class="relative w-52 group">
|
||||
<svg class="w-4 h-4 text-gray-600 absolute left-3 top-1/2 -translate-y-1/2 transition-colors group-focus-within:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索网站..."
|
||||
class="w-full pl-9 pr-8 py-2 bg-gray-900/80 border border-gray-800/60 rounded-xl text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-indigo-500/50 focus:shadow-[0_0_0_3px_rgba(99,102,241,0.1)] transition-all duration-200"
|
||||
/>
|
||||
<button v-if="searchQuery" @click="searchQuery = ''" class="absolute right-2 top-1/2 -translate-y-1/2 p-0.5 text-gray-600 hover:text-gray-300">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,9 +36,12 @@
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else-if="filteredNav.length === 0" class="text-center py-20">
|
||||
<svg class="w-14 h-14 text-gray-700 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
|
||||
<p class="text-sm text-gray-500">{{ searchQuery ? '没有找到匹配的网站' : '暂无导航内容' }}</p>
|
||||
<div v-else-if="filteredNav.length === 0" class="text-center py-24">
|
||||
<div class="w-16 h-16 rounded-2xl bg-gray-800/50 flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
|
||||
</div>
|
||||
<p class="text-gray-400 font-medium mb-1">{{ searchQuery ? '没有找到匹配的网站' : '暂无导航内容' }}</p>
|
||||
<p class="text-sm text-gray-600">{{ searchQuery ? '换个关键词试试' : '点击“推荐网站”添加第一个' }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 分类区块 -->
|
||||
@@ -63,17 +63,20 @@
|
||||
:href="link.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="group bg-gray-900 border border-gray-800 rounded-xl px-4 py-3.5 hover:border-indigo-600/40 hover:bg-gray-900/80 transition-all cursor-pointer block"
|
||||
class="group bg-gray-900/80 border border-gray-800/60 rounded-xl px-4 py-3.5 hover:border-indigo-500/30 hover:bg-gray-800/30 transition-all duration-200 cursor-pointer block"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<!-- 图标 -->
|
||||
<div class="w-9 h-9 rounded-lg bg-gray-800 border border-gray-700 flex items-center justify-center shrink-0 group-hover:border-indigo-600/30 transition-colors">
|
||||
<div class="w-9 h-9 rounded-lg bg-gray-800/80 border border-gray-700/50 flex items-center justify-center shrink-0 group-hover:border-indigo-500/30 group-hover:shadow-sm group-hover:shadow-indigo-500/5 transition-all duration-200">
|
||||
<img v-if="link.icon" :src="link.icon" class="w-5 h-5 rounded" @error="$event.target.style.display='none'" />
|
||||
<span v-else class="text-sm font-bold text-indigo-400">{{ link.name.charAt(0).toUpperCase() }}</span>
|
||||
<img v-else :src="getFavicon(link.url)" class="w-5 h-5 rounded" @error="handleFaviconError($event, link)" />
|
||||
</div>
|
||||
<!-- 文字 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium text-gray-200 group-hover:text-indigo-400 transition-colors truncate">{{ link.name }}</div>
|
||||
<div class="text-sm font-medium text-gray-200 group-hover:text-indigo-400 transition-colors truncate flex items-center gap-1">
|
||||
{{ link.name }}
|
||||
<svg class="w-3 h-3 text-gray-700 opacity-0 group-hover:opacity-100 transition-opacity" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
|
||||
</div>
|
||||
<div v-if="link.description" class="text-[11px] text-gray-500 mt-0.5 line-clamp-2">{{ link.description }}</div>
|
||||
<div class="text-[10px] text-gray-700 mt-1 truncate">{{ getDomain(link.url) }}</div>
|
||||
</div>
|
||||
@@ -258,4 +261,37 @@ function getDomain(url) {
|
||||
return url.replace(/^https?:\/\//, '').split('/')[0]
|
||||
}
|
||||
}
|
||||
|
||||
function getFavicon(url) {
|
||||
try {
|
||||
const domain = new URL(url).hostname
|
||||
return `https://statics.dnspod.cn/proxy_favicon/_/favicon?domain=${domain}`
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function handleFaviconError(e, link) {
|
||||
const img = e.target
|
||||
const domain = getDomain(link.url)
|
||||
// 第一次失败:尝试网站自身的 favicon.ico
|
||||
if (!img.dataset.retried) {
|
||||
img.dataset.retried = '1'
|
||||
try {
|
||||
img.src = new URL(link.url).origin + '/favicon.ico'
|
||||
} catch {
|
||||
showInitial(img, link)
|
||||
}
|
||||
return
|
||||
}
|
||||
// 第二次失败:显示首字母
|
||||
showInitial(img, link)
|
||||
}
|
||||
|
||||
function showInitial(img, link) {
|
||||
const span = document.createElement('span')
|
||||
span.className = 'text-sm font-bold text-indigo-400'
|
||||
span.textContent = link.name.charAt(0).toUpperCase()
|
||||
img.replaceWith(span)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<template>
|
||||
<div class="h-full overflow-y-auto">
|
||||
<div class="max-w-2xl mx-auto px-6 py-8">
|
||||
<div class="max-w-2xl mx-auto px-6 py-8 animate-fade-in">
|
||||
<!-- 头部 -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-xl font-bold text-white">消息通知</h1>
|
||||
<h1 class="text-xl font-bold text-white flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg>
|
||||
消息通知
|
||||
</h1>
|
||||
<button
|
||||
v-if="notifications.length > 0 && hasUnread"
|
||||
@click="markAllRead"
|
||||
@@ -19,8 +22,8 @@
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
@click="activeTab = tab.value"
|
||||
class="px-3 py-1.5 text-xs rounded-lg transition-colors"
|
||||
:class="activeTab === tab.value ? 'bg-indigo-600 text-white' : 'bg-gray-800 text-gray-400 hover:text-gray-200 hover:bg-gray-700'"
|
||||
class="px-3 py-1.5 text-xs rounded-xl transition-all border"
|
||||
:class="activeTab === tab.value ? 'bg-indigo-600/20 text-indigo-400 border-indigo-500/30' : 'bg-gray-900/80 text-gray-400 border-gray-800/60 hover:text-gray-200 hover:border-gray-700'"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
@@ -33,10 +36,13 @@
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else-if="filteredNotifications.length === 0" class="text-center py-16">
|
||||
<svg class="w-12 h-12 text-gray-700 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
|
||||
</svg>
|
||||
<p class="text-gray-500 text-sm">暂无通知</p>
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gray-800/60 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">暂无通知</p>
|
||||
<p class="text-gray-600 text-xs mt-1">新的互动消息会显示在这里</p>
|
||||
</div>
|
||||
|
||||
<!-- 通知列表 -->
|
||||
@@ -45,8 +51,8 @@
|
||||
v-for="notif in filteredNotifications"
|
||||
:key="notif.id"
|
||||
@click="handleClick(notif)"
|
||||
class="flex items-start gap-3 px-4 py-3 rounded-lg cursor-pointer transition-colors"
|
||||
:class="notif.is_read ? 'hover:bg-gray-800/40' : 'bg-gray-800/60 hover:bg-gray-800'"
|
||||
class="flex items-start gap-3 px-4 py-3 rounded-xl cursor-pointer transition-all"
|
||||
:class="notif.is_read ? 'hover:bg-gray-800/40' : 'bg-indigo-600/5 border border-indigo-500/10 hover:bg-indigo-600/10'"
|
||||
>
|
||||
<!-- 未读点 -->
|
||||
<div class="pt-1.5 shrink-0">
|
||||
|
||||
@@ -1,14 +1,33 @@
|
||||
<template>
|
||||
<div class="h-full overflow-y-auto">
|
||||
<div class="max-w-4xl mx-auto p-6">
|
||||
<div v-if="post" class="bg-gray-900 border border-gray-800 rounded-xl p-6">
|
||||
<div class="max-w-4xl mx-auto p-6 animate-fade-in">
|
||||
<div v-if="post" class="bg-gray-900/80 border border-gray-800/60 rounded-2xl overflow-hidden">
|
||||
<!-- 封面图区域 -->
|
||||
<div v-if="post.cover_image" class="relative h-64 overflow-hidden">
|
||||
<img :src="post.cover_image" class="w-full h-full object-cover" />
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-gray-900 via-gray-900/40 to-transparent"></div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<!-- 标题 -->
|
||||
<h1 class="text-xl font-bold text-gray-100 mb-3">{{ post.title }}</h1>
|
||||
<div class="flex items-center gap-4 text-xs text-gray-500 mb-6">
|
||||
<span>{{ post.author_name }}</span>
|
||||
<span>{{ formatDate(post.created_at) }}</span>
|
||||
<span v-if="post.category" class="bg-gray-800 px-2 py-0.5 rounded">{{ post.category }}</span>
|
||||
<span>{{ post.view_count }} 浏览</span>
|
||||
<h1 class="text-2xl font-bold text-gray-100 mb-4 leading-tight">{{ post.title }}</h1>
|
||||
|
||||
<!-- 作者信息 -->
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-sm text-white font-bold ring-2 ring-indigo-500/20">
|
||||
{{ post.author_name?.[0]?.toUpperCase() }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-200">{{ post.author_name }}</span>
|
||||
<div class="flex items-center gap-2 text-[11px] text-gray-500">
|
||||
<span>{{ formatDate(post.created_at) }}</span>
|
||||
<span>·</span>
|
||||
<span>{{ post.view_count }} 浏览</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="post.category" class="ml-auto px-2.5 py-1 bg-indigo-600/15 text-indigo-400 text-[11px] rounded-lg border border-indigo-500/20">{{ post.category }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 内容 -->
|
||||
@@ -39,28 +58,38 @@
|
||||
<span
|
||||
v-for="tag in parseTags(post.tags)"
|
||||
:key="tag"
|
||||
class="bg-gray-800 text-gray-400 px-2 py-0.5 rounded text-xs"
|
||||
class="bg-gray-800/60 text-gray-400 px-2.5 py-0.5 rounded-lg text-xs border border-gray-700/40"
|
||||
>
|
||||
{{ tag }}
|
||||
#{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex items-center gap-4 border-t border-gray-800 pt-4">
|
||||
<div class="flex items-center gap-3 border-t border-gray-800/60 pt-4">
|
||||
<button
|
||||
@click="handleLike"
|
||||
class="flex items-center gap-1 text-sm transition-colors"
|
||||
:class="post.is_liked ? 'text-red-400' : 'text-gray-500 hover:text-red-400'"
|
||||
class="flex items-center gap-1.5 px-4 py-2 rounded-xl text-sm transition-all"
|
||||
:class="post.is_liked ? 'bg-red-500/15 text-red-400 border border-red-500/20' : 'text-gray-500 hover:text-red-400 hover:bg-red-500/10 border border-transparent'"
|
||||
>
|
||||
{{ post.is_liked ? '已赞' : '点赞' }} {{ post.like_count }}
|
||||
<svg class="w-4 h-4" :fill="post.is_liked ? 'currentColor' : 'none'" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/></svg>
|
||||
{{ post.like_count }}
|
||||
</button>
|
||||
<button
|
||||
@click="handleCollect"
|
||||
class="flex items-center gap-1 text-sm transition-colors"
|
||||
:class="post.is_collected ? 'text-yellow-400' : 'text-gray-500 hover:text-yellow-400'"
|
||||
class="flex items-center gap-1.5 px-4 py-2 rounded-xl text-sm transition-all"
|
||||
:class="post.is_collected ? 'bg-yellow-500/15 text-yellow-400 border border-yellow-500/20' : 'text-gray-500 hover:text-yellow-400 hover:bg-yellow-500/10 border border-transparent'"
|
||||
>
|
||||
{{ post.is_collected ? '已收藏' : '收藏' }} {{ post.collect_count }}
|
||||
<svg class="w-4 h-4" :fill="post.is_collected ? 'currentColor' : 'none'" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"/></svg>
|
||||
{{ post.collect_count }}
|
||||
</button>
|
||||
<button
|
||||
@click="copyLink"
|
||||
class="flex items-center gap-1.5 px-4 py-2 rounded-xl text-sm text-gray-500 hover:text-indigo-400 hover:bg-indigo-500/10 border border-transparent transition-all"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
||||
分享
|
||||
</button>
|
||||
<div class="flex-1"></div>
|
||||
<router-link
|
||||
v-if="isAuthor"
|
||||
:to="`/post/edit/${post.id}`"
|
||||
@@ -77,16 +106,20 @@
|
||||
</button>
|
||||
<button
|
||||
@click="$router.back()"
|
||||
class="ml-auto text-sm text-gray-500 hover:text-gray-300 transition-colors"
|
||||
class="text-sm text-gray-500 hover:text-gray-300 transition-colors"
|
||||
>
|
||||
返回
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评论区 -->
|
||||
<div class="mt-6">
|
||||
<h3 class="text-base font-medium text-gray-300 mb-4">评论 ({{ comments.length }})</h3>
|
||||
<h3 class="text-base font-medium text-gray-300 mb-4 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-indigo-400/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
|
||||
评论 ({{ comments.length }})
|
||||
</h3>
|
||||
|
||||
<!-- 发评论 -->
|
||||
<div class="flex gap-3 mb-6">
|
||||
@@ -94,12 +127,12 @@
|
||||
v-model="commentText"
|
||||
@keydown.enter="handleComment"
|
||||
placeholder="写下你的评论..."
|
||||
class="flex-1 px-4 py-2 bg-gray-900 border border-gray-800 rounded-lg text-gray-100 text-sm placeholder-gray-600 focus:outline-none focus:border-indigo-500"
|
||||
class="flex-1 px-4 py-2.5 bg-gray-900/80 border border-gray-800/60 rounded-xl text-gray-100 text-sm placeholder-gray-600 focus:outline-none focus:border-indigo-500/50 focus:shadow-[0_0_0_3px_rgba(99,102,241,0.1)] transition-all"
|
||||
/>
|
||||
<button
|
||||
@click="handleComment"
|
||||
:disabled="!commentText.trim()"
|
||||
class="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 disabled:opacity-30 text-white rounded-lg text-sm transition-colors"
|
||||
class="px-5 py-2.5 bg-indigo-600 hover:bg-indigo-500 hover:shadow-lg hover:shadow-indigo-500/20 disabled:opacity-30 text-white rounded-xl text-sm transition-all"
|
||||
>
|
||||
评论
|
||||
</button>
|
||||
@@ -110,13 +143,16 @@
|
||||
<div
|
||||
v-for="c in comments"
|
||||
:key="c.id"
|
||||
class="bg-gray-900 border border-gray-800 rounded-lg p-4"
|
||||
class="bg-gray-900/60 border border-gray-800/40 rounded-xl p-4"
|
||||
>
|
||||
<div class="flex items-center gap-3 mb-2 text-xs text-gray-500">
|
||||
<span class="text-gray-300">{{ c.author_name }}</span>
|
||||
<span>{{ formatDate(c.created_at) }}</span>
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-indigo-500/80 to-purple-600/80 flex items-center justify-center text-[10px] text-white font-bold">
|
||||
{{ c.author_name?.[0]?.toUpperCase() }}
|
||||
</div>
|
||||
<span class="text-sm text-gray-300">{{ c.author_name }}</span>
|
||||
<span class="text-xs text-gray-600">{{ formatDate(c.created_at) }}</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-300">{{ c.content }}</p>
|
||||
<p class="text-sm text-gray-300 pl-10">{{ c.content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -208,6 +244,11 @@ async function handleDelete() {
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
navigator.clipboard.writeText(window.location.href)
|
||||
alert('链接已复制')
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (!bytes) return '0 B'
|
||||
if (bytes < 1024) return bytes + ' B'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="h-full flex flex-col bg-gray-950">
|
||||
<div class="h-full flex flex-col bg-gray-950 animate-fade-in">
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="shrink-0 flex items-center justify-between px-6 py-3 border-b border-gray-800/60 bg-gray-950/80 backdrop-blur-sm">
|
||||
<div class="shrink-0 flex items-center justify-between px-3 sm:px-6 py-3 border-b border-gray-800/60 bg-gray-900/50 backdrop-blur-sm">
|
||||
<div class="flex items-center gap-3">
|
||||
<button @click="$router.back()" class="p-1.5 text-gray-500 hover:text-gray-300 hover:bg-gray-800 rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/></svg>
|
||||
@@ -19,21 +19,21 @@
|
||||
<button @click="editorMode = editorMode === 'edit' ? 'preview' : 'edit'" class="px-3 py-1.5 text-xs rounded-lg transition-colors" :class="editorMode === 'preview' ? 'bg-indigo-600/20 text-indigo-400' : 'text-gray-500 hover:text-gray-300 hover:bg-gray-800'">
|
||||
{{ editorMode === 'preview' ? '编辑' : '预览' }}
|
||||
</button>
|
||||
<button @click="editorMode = 'split'" class="px-3 py-1.5 text-xs rounded-lg transition-colors" :class="editorMode === 'split' ? 'bg-indigo-600/20 text-indigo-400' : 'text-gray-500 hover:text-gray-300 hover:bg-gray-800'">
|
||||
<button @click="editorMode = 'split'" class="px-3 py-1.5 text-xs rounded-lg transition-colors hidden sm:block" :class="editorMode === 'split' ? 'bg-indigo-600/20 text-indigo-400' : 'text-gray-500 hover:text-gray-300 hover:bg-gray-800'">
|
||||
分屏
|
||||
</button>
|
||||
<div class="w-px h-5 bg-gray-800 mx-1"></div>
|
||||
<button
|
||||
@click="saveDraft"
|
||||
:disabled="savingDraft"
|
||||
class="px-4 py-1.5 bg-gray-700 hover:bg-gray-600 disabled:opacity-30 disabled:cursor-not-allowed text-gray-200 text-sm rounded-lg transition-colors"
|
||||
class="hidden sm:inline px-4 py-1.5 bg-gray-800 border border-gray-700 hover:bg-gray-700 hover:border-gray-600 disabled:opacity-30 disabled:cursor-not-allowed text-gray-200 text-sm rounded-lg transition-all"
|
||||
>
|
||||
{{ savingDraft ? '保存中...' : '保存草稿' }}
|
||||
</button>
|
||||
<button
|
||||
@click="handleSubmit"
|
||||
:disabled="!form.title.trim() || !form.content.trim() || submitting"
|
||||
class="px-5 py-1.5 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-30 disabled:cursor-not-allowed text-white text-sm rounded-lg transition-colors font-medium"
|
||||
class="px-5 py-1.5 bg-indigo-600 hover:bg-indigo-500 hover:shadow-lg hover:shadow-indigo-500/20 disabled:opacity-30 disabled:cursor-not-allowed text-white text-sm rounded-lg transition-all font-medium"
|
||||
>
|
||||
{{ submitting ? '发布中...' : (isEdit ? '更新' : '发布') }}
|
||||
</button>
|
||||
@@ -46,7 +46,7 @@
|
||||
<!-- 左侧编辑区 -->
|
||||
<div class="flex-1 flex flex-col overflow-hidden">
|
||||
<!-- 标题输入 -->
|
||||
<div class="px-12 pt-8 pb-2">
|
||||
<div class="px-4 sm:px-12 pt-8 pb-2">
|
||||
<input
|
||||
v-model="form.title"
|
||||
placeholder="输入文章标题..."
|
||||
@@ -55,7 +55,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 编辑器工具栏 -->
|
||||
<div class="px-12 py-2 flex items-center gap-0.5">
|
||||
<div class="px-4 sm:px-12 py-2 flex items-center gap-0.5 overflow-x-auto">
|
||||
<template v-for="btn in toolbarBtns" :key="btn.tip">
|
||||
<div class="relative group">
|
||||
<component :is="btn.tag || 'button'" @click="btn.action?.()" :class="'w-7 h-7 flex items-center justify-center rounded text-gray-500 hover:text-indigo-400 hover:bg-gray-800/80 transition-colors' + (btn.tag === 'label' ? ' cursor-pointer' : '')">
|
||||
@@ -89,7 +89,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 编辑/预览区域 -->
|
||||
<div class="flex-1 overflow-hidden px-12 pb-6">
|
||||
<div class="flex-1 overflow-hidden px-4 sm:px-12 pb-6">
|
||||
<!-- 纯编辑模式 -->
|
||||
<textarea
|
||||
v-if="editorMode === 'edit'"
|
||||
@@ -111,19 +111,20 @@
|
||||
</div>
|
||||
|
||||
<!-- 分屏模式 -->
|
||||
<div v-else class="h-full flex gap-4">
|
||||
<div v-else class="h-full flex gap-0 relative">
|
||||
<textarea
|
||||
ref="editorRef"
|
||||
v-model="form.content"
|
||||
placeholder="开始书写..."
|
||||
class="flex-1 bg-gray-900/40 border border-gray-800/60 rounded-lg px-5 py-4 text-gray-200 text-sm leading-7 placeholder-gray-700 focus:outline-none focus:border-gray-700 resize-none font-mono"
|
||||
class="flex-1 bg-gray-900/40 border border-gray-800/60 rounded-l-lg px-5 py-4 text-gray-200 text-sm leading-7 placeholder-gray-700 focus:outline-none focus:border-indigo-500/30 resize-none font-mono"
|
||||
@paste="handlePaste"
|
||||
@drop.prevent="handleDrop"
|
||||
@dragover.prevent
|
||||
@keydown="handleEditorKeydown"
|
||||
@input="handleEditorInput"
|
||||
></textarea>
|
||||
<div class="flex-1 bg-gray-900/40 border border-gray-800/60 rounded-lg px-5 py-4 overflow-y-auto">
|
||||
<div class="w-px bg-gray-700 shrink-0 cursor-col-resize hover:bg-indigo-500/50 transition-colors" title="拖拽调整宽度"></div>
|
||||
<div class="flex-1 bg-gray-900/40 border border-gray-800/60 rounded-r-lg px-5 py-4 overflow-y-auto">
|
||||
<div class="markdown-body text-sm text-gray-200 leading-7" v-html="preview"></div>
|
||||
<p v-if="!form.content" class="text-gray-700 text-sm">预览区域</p>
|
||||
</div>
|
||||
@@ -132,13 +133,13 @@
|
||||
</div>
|
||||
|
||||
<!-- 右侧设置面板 -->
|
||||
<div class="w-72 shrink-0 border-l border-gray-800/60 overflow-y-auto bg-gray-900/30">
|
||||
<div class="w-72 shrink-0 border-l border-gray-800/60 overflow-y-auto bg-gray-900/40">
|
||||
<div class="p-5 space-y-5">
|
||||
|
||||
<!-- 发布状态 -->
|
||||
<div class="space-y-3">
|
||||
<h4 class="text-xs font-medium text-gray-400 uppercase tracking-wider">发布设置</h4>
|
||||
<label class="flex items-center justify-between py-2 px-3 bg-gray-900/60 rounded-lg cursor-pointer group">
|
||||
<h4 class="text-xs font-medium text-gray-400 uppercase tracking-wider flex items-center gap-2"><svg class="w-3.5 h-3.5 text-indigo-400/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>发布设置</h4>
|
||||
<label class="flex items-center justify-between py-2 px-3 bg-gray-900/60 border border-gray-800/40 rounded-xl cursor-pointer group hover:border-gray-700/60 transition-colors">
|
||||
<span class="text-sm text-gray-300 group-hover:text-gray-200">公开发布</span>
|
||||
<div class="relative">
|
||||
<input type="checkbox" v-model="form.is_public" class="sr-only peer" />
|
||||
@@ -151,29 +152,29 @@
|
||||
|
||||
<!-- 分类 -->
|
||||
<div class="space-y-2">
|
||||
<h4 class="text-xs font-medium text-gray-400 uppercase tracking-wider">分类</h4>
|
||||
<h4 class="text-xs font-medium text-gray-400 uppercase tracking-wider flex items-center gap-2"><svg class="w-3.5 h-3.5 text-indigo-400/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/></svg>分类</h4>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button
|
||||
v-for="cat in categories"
|
||||
:key="cat"
|
||||
@click="form.category = form.category === cat ? '' : cat"
|
||||
class="px-2.5 py-1 rounded-md text-xs transition-colors"
|
||||
class="px-2.5 py-1 rounded-lg text-xs transition-all border"
|
||||
:class="form.category === cat
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'bg-gray-800/80 text-gray-400 hover:text-gray-200 hover:bg-gray-800'"
|
||||
? 'bg-indigo-600/20 text-indigo-400 border-indigo-500/30'
|
||||
: 'bg-gray-800/80 text-gray-400 border-gray-800/60 hover:text-gray-200 hover:border-gray-700'"
|
||||
>{{ cat }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 标签 -->
|
||||
<div class="space-y-2">
|
||||
<h4 class="text-xs font-medium text-gray-400 uppercase tracking-wider">标签</h4>
|
||||
<h4 class="text-xs font-medium text-gray-400 uppercase tracking-wider flex items-center gap-2"><svg class="w-3.5 h-3.5 text-indigo-400/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"/></svg>标签</h4>
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
v-model="tagInput"
|
||||
@keydown.enter.prevent="addTag"
|
||||
placeholder="输入后回车"
|
||||
class="flex-1 px-3 py-1.5 bg-gray-900/60 border border-gray-800 rounded-lg text-gray-200 text-xs placeholder-gray-600 focus:outline-none focus:border-indigo-500"
|
||||
class="flex-1 px-3 py-1.5 bg-gray-900/60 border border-gray-800 rounded-xl text-gray-200 text-xs placeholder-gray-600 focus:outline-none focus:border-indigo-500/50 focus:shadow-[0_0_0_3px_rgba(99,102,241,0.08)] transition-all"
|
||||
/>
|
||||
<button @click="addTag" class="px-2 py-1.5 text-xs text-gray-500 hover:text-indigo-400 transition-colors">+</button>
|
||||
</div>
|
||||
@@ -193,8 +194,8 @@
|
||||
|
||||
<!-- 附件 -->
|
||||
<div class="space-y-2">
|
||||
<h4 class="text-xs font-medium text-gray-400 uppercase tracking-wider">附件</h4>
|
||||
<label class="flex items-center justify-center gap-2 px-3 py-2.5 bg-gray-900/60 border border-dashed border-gray-700 rounded-lg cursor-pointer hover:border-indigo-500/50 hover:bg-indigo-600/5 transition-colors">
|
||||
<h4 class="text-xs font-medium text-gray-400 uppercase tracking-wider flex items-center gap-2"><svg class="w-3.5 h-3.5 text-indigo-400/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"/></svg>附件</h4>
|
||||
<label class="flex items-center justify-center gap-2 px-3 py-2.5 bg-gray-900/60 border border-dashed border-gray-700 rounded-xl cursor-pointer hover:border-indigo-500/50 hover:bg-indigo-600/5 transition-all">
|
||||
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"/></svg>
|
||||
<span class="text-xs text-gray-400">{{ uploadingAttachment ? '上传中...' : '上传附件' }}</span>
|
||||
<input type="file" class="hidden" accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.zip,.rar" @change="handleAttachmentUpload" multiple :disabled="uploadingAttachment" />
|
||||
@@ -216,7 +217,7 @@
|
||||
|
||||
<!-- 模板 -->
|
||||
<div v-if="!isEdit" class="space-y-2">
|
||||
<h4 class="text-xs font-medium text-gray-400 uppercase tracking-wider">快速模板</h4>
|
||||
<h4 class="text-xs font-medium text-gray-400 uppercase tracking-wider flex items-center gap-2"><svg class="w-3.5 h-3.5 text-indigo-400/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>快速模板</h4>
|
||||
<div class="space-y-1">
|
||||
<button
|
||||
v-for="tpl in templates"
|
||||
@@ -232,17 +233,28 @@
|
||||
|
||||
<!-- 字数统计 -->
|
||||
<div class="space-y-2">
|
||||
<h4 class="text-xs font-medium text-gray-400 uppercase tracking-wider">统计</h4>
|
||||
<h4 class="text-xs font-medium text-gray-400 uppercase tracking-wider flex items-center gap-2"><svg class="w-3.5 h-3.5 text-indigo-400/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>统计</h4>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="px-3 py-2 bg-gray-900/60 rounded-lg">
|
||||
<div class="px-3 py-2 bg-gray-900/60 border border-gray-800/40 rounded-xl">
|
||||
<p class="text-lg font-semibold text-gray-200">{{ wordCount }}</p>
|
||||
<p class="text-[10px] text-gray-600">字数</p>
|
||||
</div>
|
||||
<div class="px-3 py-2 bg-gray-900/60 rounded-lg">
|
||||
<div class="px-3 py-2 bg-gray-900/60 border border-gray-800/40 rounded-xl">
|
||||
<p class="text-lg font-semibold text-gray-200">{{ readTime }}</p>
|
||||
<p class="text-[10px] text-gray-600">分钟阅读</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 字数进度条 -->
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center justify-between text-[10px] text-gray-600">
|
||||
<span>写作进度</span>
|
||||
<span>{{ Math.min(100, Math.round(wordCount / 30)) }}%</span>
|
||||
</div>
|
||||
<div class="h-1 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-gradient-to-r from-indigo-500 to-purple-500 rounded-full transition-all duration-500" :style="{ width: Math.min(100, Math.round(wordCount / 30)) + '%' }"></div>
|
||||
</div>
|
||||
<p class="text-[10px] text-gray-700">建议 3000 字以上获得更好的阅读体验</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
<template>
|
||||
<div class="h-full overflow-y-auto">
|
||||
<div class="max-w-4xl mx-auto px-6 py-6">
|
||||
<div class="max-w-4xl mx-auto px-6 py-6 animate-fade-in">
|
||||
|
||||
<!-- 用户信息卡片 -->
|
||||
<div class="bg-gray-900 border border-gray-800 rounded-xl p-6 mb-6">
|
||||
<div class="flex items-start gap-5">
|
||||
<!-- 背景 Banner + 用户信息卡片 -->
|
||||
<div class="bg-gray-900/80 border border-gray-800/60 rounded-2xl overflow-hidden mb-6">
|
||||
<!-- 渐变 Banner -->
|
||||
<div class="h-28 bg-gradient-to-r from-indigo-600/20 via-purple-600/10 to-indigo-600/20 relative">
|
||||
<div class="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,rgba(99,102,241,0.15),transparent_70%)]" />
|
||||
</div>
|
||||
|
||||
<div class="px-6 pb-6 -mt-10">
|
||||
<div class="flex items-end gap-5">
|
||||
<!-- 头像 -->
|
||||
<div class="relative group shrink-0">
|
||||
<div
|
||||
class="w-20 h-20 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-3xl text-white font-bold overflow-hidden cursor-pointer"
|
||||
class="w-20 h-20 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-3xl text-white font-bold overflow-hidden cursor-pointer ring-4 ring-gray-900 shadow-xl"
|
||||
@click="triggerAvatarUpload"
|
||||
>
|
||||
<img v-if="userStore.user?.avatar" :src="userStore.user.avatar" class="w-full h-full object-cover" />
|
||||
@@ -26,49 +32,54 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- 信息 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<h2 class="text-xl font-bold text-gray-100">{{ userStore.user?.username }}</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ userStore.user?.email }}</p>
|
||||
<div class="flex-1 min-w-0 pt-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-gray-100">{{ userStore.user?.username }}</h2>
|
||||
<p class="text-sm text-gray-500 mt-0.5">{{ userStore.user?.email }}</p>
|
||||
</div>
|
||||
<!-- 编辑按钮 -->
|
||||
<button
|
||||
@click="showEditModal = true"
|
||||
class="px-4 py-2 bg-gray-800 border border-gray-700 hover:bg-gray-700 hover:border-gray-600 text-gray-300 text-sm rounded-xl transition-all shrink-0"
|
||||
>编辑资料</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 mt-2">
|
||||
<span v-if="userStore.user?.is_admin" class="px-2 py-0.5 bg-indigo-600/20 text-indigo-400 text-[10px] rounded-full">管理员</span>
|
||||
<span v-if="userStore.user?.is_admin" class="px-2 py-0.5 bg-indigo-600/20 text-indigo-400 text-[10px] rounded-full border border-indigo-500/20">管理员</span>
|
||||
<span class="text-xs text-gray-600">加入于 {{ formatDate(userStore.user?.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 编辑按钮 -->
|
||||
<button
|
||||
@click="showEditModal = true"
|
||||
class="px-4 py-2 bg-gray-800 hover:bg-gray-700 text-gray-300 text-sm rounded-lg transition-colors shrink-0"
|
||||
>编辑资料</button>
|
||||
</div>
|
||||
|
||||
<!-- 统计数据 -->
|
||||
<div class="grid grid-cols-4 gap-4 mt-6 pt-5 border-t border-gray-800">
|
||||
<div class="text-center">
|
||||
<p class="text-2xl font-bold text-gray-100">{{ stats.post_count }}</p>
|
||||
<div class="grid grid-cols-4 gap-4 mt-6 pt-5 border-t border-gray-800/60">
|
||||
<div class="text-center group cursor-default">
|
||||
<p class="text-2xl font-bold text-gray-100 group-hover:text-indigo-400 transition-colors">{{ stats.post_count }}</p>
|
||||
<p class="text-xs text-gray-500 mt-1">文章</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="text-2xl font-bold text-gray-100">{{ stats.total_likes }}</p>
|
||||
<div class="text-center group cursor-default">
|
||||
<p class="text-2xl font-bold text-gray-100 group-hover:text-indigo-400 transition-colors">{{ stats.total_likes }}</p>
|
||||
<p class="text-xs text-gray-500 mt-1">获赞</p>
|
||||
</div>
|
||||
<div class="text-center cursor-pointer hover:text-indigo-400" @click="activeTab = 'followers'; loadFollowers()">
|
||||
<p class="text-2xl font-bold text-gray-100">{{ stats.follower_count }}</p>
|
||||
<div class="text-center cursor-pointer group" @click="activeTab = 'followers'; loadFollowers()">
|
||||
<p class="text-2xl font-bold text-gray-100 group-hover:text-indigo-400 transition-colors">{{ stats.follower_count }}</p>
|
||||
<p class="text-xs text-gray-500 mt-1">粉丝</p>
|
||||
</div>
|
||||
<div class="text-center cursor-pointer hover:text-indigo-400" @click="activeTab = 'following'; loadFollowing()">
|
||||
<p class="text-2xl font-bold text-gray-100">{{ stats.following_count }}</p>
|
||||
<div class="text-center cursor-pointer group" @click="activeTab = 'following'; loadFollowing()">
|
||||
<p class="text-2xl font-bold text-gray-100 group-hover:text-indigo-400 transition-colors">{{ stats.following_count }}</p>
|
||||
<p class="text-xs text-gray-500 mt-1">关注</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<div class="flex gap-1 bg-gray-900 rounded-lg p-1 mb-5 w-fit">
|
||||
<div class="flex gap-1 bg-gray-900/80 border border-gray-800/60 rounded-xl p-1 mb-5 w-fit">
|
||||
<button
|
||||
v-for="t in tabs" :key="t.key"
|
||||
@click="switchTab(t.key)"
|
||||
class="px-4 py-2 text-sm rounded-md transition-colors"
|
||||
:class="activeTab === t.key ? 'bg-indigo-600 text-white' : 'text-gray-400 hover:text-gray-200'"
|
||||
class="px-4 py-2 text-sm rounded-lg transition-all"
|
||||
:class="activeTab === t.key ? 'bg-indigo-600 text-white shadow-lg shadow-indigo-500/20' : 'text-gray-400 hover:text-gray-200 hover:bg-gray-800/60'"
|
||||
>{{ t.label }}</button>
|
||||
</div>
|
||||
|
||||
@@ -77,7 +88,7 @@
|
||||
<div
|
||||
v-for="post in myPosts" :key="post.id"
|
||||
@click="$router.push(`/post/${post.id}`)"
|
||||
class="bg-gray-900 border border-gray-800 rounded-xl p-5 cursor-pointer hover:border-gray-700 transition-colors group"
|
||||
class="bg-gray-900/80 border border-gray-800/60 rounded-xl p-5 cursor-pointer hover:border-indigo-500/30 transition-all group"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
@@ -102,7 +113,13 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="myPosts.length === 0" class="text-center text-gray-600 py-16 text-sm">还没有发布过文章</p>
|
||||
<div v-if="myPosts.length === 0" class="text-center py-16">
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gray-800/60 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">还没有发布过文章</p>
|
||||
<p class="text-gray-600 text-xs mt-1">开始分享你的经验吧</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 我的收藏 -->
|
||||
@@ -110,7 +127,7 @@
|
||||
<div
|
||||
v-for="post in myCollects" :key="post.id"
|
||||
@click="$router.push(`/post/${post.id}`)"
|
||||
class="bg-gray-900 border border-gray-800 rounded-xl p-5 cursor-pointer hover:border-gray-700 transition-colors group"
|
||||
class="bg-gray-900/80 border border-gray-800/60 rounded-xl p-5 cursor-pointer hover:border-indigo-500/30 transition-all group"
|
||||
>
|
||||
<h3 class="text-base font-medium text-gray-200 group-hover:text-indigo-400 transition-colors">{{ post.title }}</h3>
|
||||
<p class="text-sm text-gray-500 mt-1.5 line-clamp-2">{{ stripHtml(post.content) }}</p>
|
||||
@@ -121,7 +138,13 @@
|
||||
<span v-if="post.category" class="px-2 py-0.5 bg-gray-800 rounded">{{ post.category }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="myCollects.length === 0" class="text-center text-gray-600 py-16 text-sm">还没有收藏过文章</p>
|
||||
<div v-if="myCollects.length === 0" class="text-center py-16">
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gray-800/60 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"/></svg>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">还没有收藏过文章</p>
|
||||
<p class="text-gray-600 text-xs mt-1">浏览文章时点击收藏即可</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 粉丝列表 -->
|
||||
@@ -129,7 +152,7 @@
|
||||
<div
|
||||
v-for="u in followers" :key="u.id"
|
||||
@click="$router.push(`/user/${u.id}`)"
|
||||
class="bg-gray-900 border border-gray-800 rounded-xl p-4 flex items-center gap-3 cursor-pointer hover:border-gray-700 transition-colors"
|
||||
class="bg-gray-900/80 border border-gray-800/60 rounded-xl p-4 flex items-center gap-3 cursor-pointer hover:border-indigo-500/30 transition-all"
|
||||
>
|
||||
<div class="w-10 h-10 rounded-full bg-gray-800 flex items-center justify-center text-sm font-bold text-indigo-400 shrink-0 overflow-hidden">
|
||||
<img v-if="u.avatar" :src="u.avatar" class="w-full h-full object-cover" />
|
||||
@@ -137,7 +160,12 @@
|
||||
</div>
|
||||
<span class="text-sm text-gray-200">{{ u.username }}</span>
|
||||
</div>
|
||||
<p v-if="followers.length === 0" class="text-center text-gray-600 py-16 text-sm">暂无粉丝</p>
|
||||
<div v-if="followers.length === 0" class="text-center py-16">
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gray-800/60 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">暂无粉丝</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关注列表 -->
|
||||
@@ -145,7 +173,7 @@
|
||||
<div
|
||||
v-for="u in following" :key="u.id"
|
||||
@click="$router.push(`/user/${u.id}`)"
|
||||
class="bg-gray-900 border border-gray-800 rounded-xl p-4 flex items-center gap-3 cursor-pointer hover:border-gray-700 transition-colors"
|
||||
class="bg-gray-900/80 border border-gray-800/60 rounded-xl p-4 flex items-center gap-3 cursor-pointer hover:border-indigo-500/30 transition-all"
|
||||
>
|
||||
<div class="w-10 h-10 rounded-full bg-gray-800 flex items-center justify-center text-sm font-bold text-indigo-400 shrink-0 overflow-hidden">
|
||||
<img v-if="u.avatar" :src="u.avatar" class="w-full h-full object-cover" />
|
||||
@@ -153,14 +181,19 @@
|
||||
</div>
|
||||
<span class="text-sm text-gray-200">{{ u.username }}</span>
|
||||
</div>
|
||||
<p v-if="following.length === 0" class="text-center text-gray-600 py-16 text-sm">暂未关注任何人</p>
|
||||
<div v-if="following.length === 0" class="text-center py-16">
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gray-800/60 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"/></svg>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">暂未关注任何人</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑资料弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showEditModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60" @click.self="showEditModal = false">
|
||||
<div class="w-full max-w-md bg-gray-900 border border-gray-800 rounded-xl p-6 mx-4">
|
||||
<div class="w-full max-w-md bg-gray-900 border border-gray-800/60 rounded-2xl p-6 mx-4">
|
||||
<h3 class="text-lg font-bold text-gray-100 mb-5">编辑个人资料</h3>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
<template>
|
||||
<div class="h-full overflow-y-auto">
|
||||
<div class="max-w-5xl mx-auto px-6 py-6">
|
||||
<div class="max-w-5xl mx-auto px-6 py-6 animate-fade-in">
|
||||
<!-- 标题区 -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-lg font-bold text-gray-100">开源项目</h1>
|
||||
<p class="text-xs text-gray-500 mt-1">精选优质开源项目推荐</p>
|
||||
</div>
|
||||
<!-- 搜索 -->
|
||||
<div class="relative w-64">
|
||||
<svg class="w-4 h-4 text-gray-600 absolute left-3 top-1/2 -translate-y-1/2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||
<div class="relative w-64 group">
|
||||
<svg class="w-4 h-4 text-gray-600 absolute left-3 top-1/2 -translate-y-1/2 transition-colors group-focus-within:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
@keydown.enter="doSearch"
|
||||
:placeholder="activeTab === 'github' ? '搜索 GitHub 项目...' : '搜索项目...'"
|
||||
class="w-full pl-9 pr-8 py-2 bg-gray-900 border border-gray-800 rounded-lg text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-indigo-500"
|
||||
class="w-full pl-9 pr-8 py-2 bg-gray-900/80 border border-gray-800/60 rounded-xl text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-indigo-500/50 focus:shadow-[0_0_0_3px_rgba(99,102,241,0.1)] transition-all duration-200"
|
||||
/>
|
||||
<button v-if="searchQuery" @click="clearSearch" class="absolute right-2 top-1/2 -translate-y-1/2 p-0.5 text-gray-600 hover:text-gray-300">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
@@ -23,13 +22,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<div class="flex items-center gap-1 mb-4">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
@click="switchTab(tab.key)"
|
||||
class="text-sm pb-1 border-b-2 transition-colors"
|
||||
:class="activeTab === tab.key ? 'text-indigo-400 border-indigo-400 font-medium' : 'text-gray-500 border-transparent hover:text-gray-300'"
|
||||
class="px-3 py-1.5 text-sm rounded-lg transition-all duration-200"
|
||||
:class="activeTab === tab.key ? 'text-indigo-400 font-medium bg-indigo-500/10' : 'text-gray-500 hover:text-gray-300 hover:bg-gray-800/40'"
|
||||
>{{ tab.label }}</button>
|
||||
</div>
|
||||
|
||||
@@ -37,15 +36,15 @@
|
||||
<div v-if="categories.length > 0 && activeTab !== 'search' && activeTab !== 'github' && activeTab !== 'collects'" class="flex flex-wrap items-center gap-2 mb-5">
|
||||
<button
|
||||
@click="selectedCat = ''; loadData()"
|
||||
class="px-3 py-1 text-xs rounded-lg transition-colors"
|
||||
:class="!selectedCat ? 'bg-indigo-600 text-white' : 'bg-gray-900 border border-gray-800 text-gray-400 hover:text-gray-200'"
|
||||
class="px-3 py-1.5 text-xs rounded-lg transition-all duration-200 border"
|
||||
:class="!selectedCat ? 'bg-indigo-600/20 text-indigo-400 border-indigo-500/30' : 'bg-gray-900/80 border-gray-800/60 text-gray-400 hover:text-gray-200'"
|
||||
>全部</button>
|
||||
<button
|
||||
v-for="cat in categories"
|
||||
:key="cat.name"
|
||||
@click="selectedCat = cat.name; loadData()"
|
||||
class="px-3 py-1 text-xs rounded-lg transition-colors"
|
||||
:class="selectedCat === cat.name ? 'bg-indigo-600 text-white' : 'bg-gray-900 border border-gray-800 text-gray-400 hover:text-gray-200'"
|
||||
class="px-3 py-1.5 text-xs rounded-lg transition-all duration-200 border"
|
||||
:class="selectedCat === cat.name ? 'bg-indigo-600/20 text-indigo-400 border-indigo-500/30' : 'bg-gray-900/80 border-gray-800/60 text-gray-400 hover:text-gray-200'"
|
||||
>{{ cat.name }} <span class="text-[10px] opacity-60">{{ cat.count }}</span></button>
|
||||
</div>
|
||||
|
||||
@@ -89,9 +88,12 @@
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else-if="projectList.length === 0" class="text-center py-20">
|
||||
<svg class="w-14 h-14 text-gray-700 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
||||
<p class="text-sm text-gray-500">{{ activeTab === 'search' ? '没有找到匹配的项目' : activeTab === 'github' ? '输入关键词搜索 GitHub 项目,或点击快捷按钮' : activeTab === 'collects' ? '还没有收藏任何项目' : '暂无开源项目' }}</p>
|
||||
<div v-else-if="projectList.length === 0" class="text-center py-24">
|
||||
<div class="w-16 h-16 rounded-2xl bg-gray-800/50 flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
||||
</div>
|
||||
<p class="text-gray-400 font-medium mb-1">{{ activeTab === 'search' ? '没有找到匹配的项目' : activeTab === 'github' ? '输入关键词搜索 GitHub 项目' : activeTab === 'collects' ? '还没有收藏任何项目' : '暂无开源项目' }}</p>
|
||||
<p class="text-sm text-gray-600">{{ activeTab === 'github' ? '或点击快捷按钮探索热门项目' : '' }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 项目卡片网格 -->
|
||||
@@ -103,7 +105,7 @@
|
||||
:href="proj.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="group bg-gray-900 border border-gray-800 rounded-xl p-4 hover:border-indigo-600/40 hover:bg-gray-900/80 transition-all cursor-pointer block"
|
||||
class="group bg-gray-900/80 border border-gray-800/60 rounded-2xl p-4 hover:border-indigo-500/30 hover:bg-gray-800/20 transition-all duration-200 cursor-pointer block"
|
||||
>
|
||||
<!-- 收藏按钮 -->
|
||||
<div class="flex items-center gap-3 mb-2.5">
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
<template>
|
||||
<div class="h-full flex">
|
||||
<!-- 左侧:对话列表 -->
|
||||
<div class="w-64 bg-gray-900 border-r border-gray-800 flex flex-col shrink-0">
|
||||
<div class="p-4">
|
||||
<div class="w-64 bg-gray-900/95 border-r border-gray-800/60 flex flex-col shrink-0">
|
||||
<div class="p-3">
|
||||
<button
|
||||
@click="startNewChat"
|
||||
class="w-full py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm rounded-lg transition-colors"
|
||||
class="w-full py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white text-sm rounded-xl transition-all duration-200 hover:shadow-lg hover:shadow-indigo-500/20 flex items-center justify-center gap-1.5 font-medium"
|
||||
>
|
||||
+ 新建需求分析
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
||||
新建需求分析
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto px-2">
|
||||
<div class="flex-1 overflow-y-auto px-2 space-y-0.5">
|
||||
<div
|
||||
v-for="conv in conversations"
|
||||
:key="conv.id"
|
||||
@click="selectConversation(conv)"
|
||||
class="px-3 py-2 mb-1 rounded-lg cursor-pointer text-sm truncate flex items-center justify-between group"
|
||||
:class="currentConvId === conv.id ? 'bg-gray-800 text-white' : 'text-gray-400 hover:bg-gray-800/50'"
|
||||
class="px-3 py-2.5 rounded-lg cursor-pointer text-[13px] truncate flex items-center justify-between group transition-all duration-150"
|
||||
:class="currentConvId === conv.id ? 'bg-indigo-500/15 text-indigo-300 border-l-2 border-indigo-500' : 'text-gray-400 hover:bg-gray-800/50 border-l-2 border-transparent'"
|
||||
>
|
||||
<span class="truncate">{{ conv.title }}</span>
|
||||
<button
|
||||
@click.stop="deleteConversation(conv.id)"
|
||||
class="opacity-0 group-hover:opacity-100 text-gray-500 hover:text-red-400 ml-2 shrink-0"
|
||||
class="opacity-0 group-hover:opacity-100 text-gray-500 hover:text-red-400 ml-2 shrink-0 transition-opacity"
|
||||
>
|
||||
x
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="conversations.length === 0" class="text-gray-600 text-sm text-center py-8">
|
||||
暂无对话记录
|
||||
</p>
|
||||
<div v-if="conversations.length === 0" class="text-center py-12">
|
||||
<svg class="w-8 h-8 text-gray-700 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
|
||||
<p class="text-gray-600 text-xs">暂无对话记录</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,23 +39,26 @@
|
||||
<!-- 消息列表 -->
|
||||
<div ref="messagesContainer" class="flex-1 overflow-y-auto p-6 space-y-4">
|
||||
<!-- 欢迎提示 -->
|
||||
<div v-if="messages.length === 0" class="flex items-center justify-center h-full">
|
||||
<div v-if="messages.length === 0" class="flex items-center justify-center h-full animate-fade-in">
|
||||
<div class="text-center max-w-lg">
|
||||
<h2 class="text-2xl font-bold text-gray-300 mb-4">需求理解助手</h2>
|
||||
<p class="text-gray-500 mb-6">
|
||||
<div class="w-16 h-16 rounded-2xl bg-indigo-500/15 flex items-center justify-center mx-auto mb-6 animate-float">
|
||||
<svg class="w-8 h-8 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/></svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-gray-200 mb-3">需求理解助手</h2>
|
||||
<p class="text-gray-500 mb-8">
|
||||
把甲方发来的内容粘贴到这里(口语化文字、截图等),AI帮你整理成清晰的功能清单。
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-3 text-sm">
|
||||
<div class="bg-gray-900 border border-gray-800 rounded-lg p-3 text-gray-400">
|
||||
<div class="bg-gray-900/80 border border-gray-800/60 rounded-xl p-3.5 text-gray-400 hover:border-indigo-500/30 hover:text-gray-300 transition-all duration-200 cursor-pointer">
|
||||
粘贴微信聊天记录
|
||||
</div>
|
||||
<div class="bg-gray-900 border border-gray-800 rounded-lg p-3 text-gray-400">
|
||||
<div class="bg-gray-900/80 border border-gray-800/60 rounded-xl p-3.5 text-gray-400 hover:border-indigo-500/30 hover:text-gray-300 transition-all duration-200 cursor-pointer">
|
||||
上传需求截图
|
||||
</div>
|
||||
<div class="bg-gray-900 border border-gray-800 rounded-lg p-3 text-gray-400">
|
||||
<div class="bg-gray-900/80 border border-gray-800/60 rounded-xl p-3.5 text-gray-400 hover:border-indigo-500/30 hover:text-gray-300 transition-all duration-200 cursor-pointer">
|
||||
描述产品想法
|
||||
</div>
|
||||
<div class="bg-gray-900 border border-gray-800 rounded-lg p-3 text-gray-400">
|
||||
<div class="bg-gray-900/80 border border-gray-800/60 rounded-xl p-3.5 text-gray-400 hover:border-indigo-500/30 hover:text-gray-300 transition-all duration-200 cursor-pointer">
|
||||
导入需求文档
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,11 +100,11 @@
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<div class="border-t border-gray-800 p-4 bg-gray-900/50">
|
||||
<div class="border-t border-gray-800/60 p-4 bg-gray-900/80 backdrop-blur-sm">
|
||||
<!-- 模型选择器 -->
|
||||
<div v-if="availableModels.length > 0" class="flex items-center gap-2 mb-3">
|
||||
<span class="text-xs text-gray-500">模型:</span>
|
||||
<select v-model="selectedModelId" class="px-2 py-1 bg-gray-800 border border-gray-700 rounded-lg text-xs text-gray-300 focus:outline-none focus:border-indigo-500">
|
||||
<span class="text-[11px] text-gray-500 font-medium">模型</span>
|
||||
<select v-model="selectedModelId" class="px-2.5 py-1.5 bg-gray-800/80 border border-gray-700/50 rounded-lg text-xs text-gray-300 focus:outline-none focus:border-indigo-500/50 transition-colors">
|
||||
<option :value="null">默认</option>
|
||||
<option v-for="m in availableModels" :key="m.id" :value="m.id">
|
||||
{{ m.model_name || m.model_id }}
|
||||
@@ -107,7 +112,7 @@
|
||||
<template v-if="m.is_default"> (默认)</template>
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="selectedModel?.web_search_enabled" class="text-xs text-blue-400">🌐 联网搜索</span>
|
||||
<span v-if="selectedModel?.web_search_enabled" class="text-[11px] text-blue-400 bg-blue-500/10 px-2 py-0.5 rounded-md">🌐 联网搜索</span>
|
||||
</div>
|
||||
<!-- 图片预览 -->
|
||||
<div v-if="uploadedImages.length" class="flex gap-2 mb-3">
|
||||
@@ -137,16 +142,16 @@
|
||||
ref="textareaRef"
|
||||
placeholder="粘贴甲方需求、描述产品想法..."
|
||||
rows="3"
|
||||
class="flex-1 px-4 py-2.5 bg-gray-800 border border-gray-700 rounded-xl text-gray-100 placeholder-gray-600 focus:outline-none focus:border-indigo-500 resize-none max-h-48 text-sm"
|
||||
class="flex-1 px-4 py-2.5 bg-gray-800/80 border border-gray-700/50 rounded-xl text-gray-100 placeholder-gray-600 focus:outline-none focus:border-indigo-500/50 focus:shadow-[0_0_0_3px_rgba(99,102,241,0.1)] resize-none max-h-48 text-sm transition-all duration-200"
|
||||
:disabled="isStreaming"
|
||||
></textarea>
|
||||
<!-- 发送按钮 -->
|
||||
<button
|
||||
@click="handleSend"
|
||||
:disabled="isStreaming || (!inputText.trim() && !uploadedImages.length)"
|
||||
class="px-4 py-2.5 bg-indigo-600 hover:bg-indigo-700 disabled:opacity-30 text-white rounded-xl transition-colors shrink-0 text-sm"
|
||||
class="px-4 py-2.5 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-30 text-white rounded-xl transition-all duration-200 shrink-0 text-sm hover:shadow-lg hover:shadow-indigo-500/20"
|
||||
>
|
||||
发送
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,75 +1,93 @@
|
||||
<template>
|
||||
<div class="h-full overflow-y-auto">
|
||||
<div class="max-w-4xl mx-auto px-6 py-8">
|
||||
<h1 class="text-xl font-bold text-gray-100 mb-2">AI 工具库</h1>
|
||||
<p class="text-sm text-gray-500 mb-8">智能工具助力编程,提升开发效率</p>
|
||||
<div class="max-w-4xl mx-auto px-6 py-8 animate-fade-in">
|
||||
<!-- 欢迎 Banner -->
|
||||
<div class="relative mb-8 p-6 rounded-2xl bg-gradient-to-br from-indigo-600/10 via-gray-900 to-purple-600/10 border border-gray-800/40 overflow-hidden">
|
||||
<div class="absolute top-0 right-0 w-64 h-64 bg-indigo-500/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"></div>
|
||||
<h1 class="text-xl font-bold text-gray-100 mb-1.5 relative">AI 工具库</h1>
|
||||
<p class="text-sm text-gray-400 relative">智能工具助力编程,提升开发效率</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 需求助手 -->
|
||||
<div
|
||||
@click="$router.push('/tools/requirement')"
|
||||
class="bg-gray-900 border border-gray-800 rounded-xl p-6 cursor-pointer hover:border-indigo-600/50 hover:bg-gray-900/80 transition-all group"
|
||||
class="tool-card group relative bg-gray-900/80 border border-gray-800/60 rounded-2xl p-6 cursor-pointer hover:border-indigo-500/40 transition-all duration-300 overflow-hidden"
|
||||
>
|
||||
<div class="w-10 h-10 rounded-xl bg-indigo-600/20 flex items-center justify-center mb-4">
|
||||
<svg class="w-5 h-5 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/></svg>
|
||||
<div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-indigo-500 to-indigo-400 opacity-0 group-hover:opacity-100 transition-opacity rounded-t-2xl"></div>
|
||||
<div class="w-12 h-12 rounded-xl bg-indigo-500/15 flex items-center justify-center mb-4 group-hover:shadow-lg group-hover:shadow-indigo-500/10 transition-all duration-300">
|
||||
<svg class="w-6 h-6 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/></svg>
|
||||
</div>
|
||||
<h3 class="text-base font-medium text-gray-200 mb-2 group-hover:text-indigo-400 transition-colors">需求理解助手</h3>
|
||||
<p class="text-sm text-gray-500 leading-relaxed">把甲方发来的内容粘贴进来,AI帮你整理成清晰的功能清单、用户故事和验收标准</p>
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
<span class="px-2 py-0.5 bg-indigo-600/10 text-indigo-400 text-xs rounded">产品经理视角</span>
|
||||
<span class="px-2 py-0.5 bg-indigo-600/10 text-indigo-400 text-xs rounded">程序员视角</span>
|
||||
<h3 class="text-base font-semibold text-gray-200 mb-2 group-hover:text-indigo-400 transition-colors">需求理解助手</h3>
|
||||
<p class="text-sm text-gray-500 leading-relaxed mb-4">把甲方发来的内容粘贴进来,AI帮你整理成清晰的功能清单、用户故事和验收标准</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2.5 py-1 bg-indigo-500/10 text-indigo-400 text-[11px] rounded-lg border border-indigo-500/10">产品经理视角</span>
|
||||
<span class="px-2.5 py-1 bg-indigo-500/10 text-indigo-400 text-[11px] rounded-lg border border-indigo-500/10">程序员视角</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 架构助手 -->
|
||||
<div
|
||||
@click="$router.push('/tools/architecture')"
|
||||
class="bg-gray-900 border border-gray-800 rounded-xl p-6 cursor-pointer hover:border-emerald-600/50 hover:bg-gray-900/80 transition-all group"
|
||||
class="tool-card group relative bg-gray-900/80 border border-gray-800/60 rounded-2xl p-6 cursor-pointer hover:border-emerald-500/40 transition-all duration-300 overflow-hidden"
|
||||
>
|
||||
<div class="w-10 h-10 rounded-xl bg-emerald-600/20 flex items-center justify-center mb-4">
|
||||
<svg class="w-5 h-5 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
|
||||
<div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-emerald-500 to-emerald-400 opacity-0 group-hover:opacity-100 transition-opacity rounded-t-2xl"></div>
|
||||
<div class="w-12 h-12 rounded-xl bg-emerald-500/15 flex items-center justify-center mb-4 group-hover:shadow-lg group-hover:shadow-emerald-500/10 transition-all duration-300">
|
||||
<svg class="w-6 h-6 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
|
||||
</div>
|
||||
<h3 class="text-base font-medium text-gray-200 mb-2 group-hover:text-emerald-400 transition-colors">架构选型助手</h3>
|
||||
<p class="text-sm text-gray-500 leading-relaxed">输入项目需求,AI给出技术选型建议、数据库设计、API接口清单和系统架构图</p>
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
<span class="px-2 py-0.5 bg-emerald-600/10 text-emerald-400 text-xs rounded">技术选型</span>
|
||||
<span class="px-2 py-0.5 bg-emerald-600/10 text-emerald-400 text-xs rounded">架构设计</span>
|
||||
<h3 class="text-base font-semibold text-gray-200 mb-2 group-hover:text-emerald-400 transition-colors">架构选型助手</h3>
|
||||
<p class="text-sm text-gray-500 leading-relaxed mb-4">输入项目需求,AI给出技术选型建议、数据库设计、API接口清单和系统架构图</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2.5 py-1 bg-emerald-500/10 text-emerald-400 text-[11px] rounded-lg border border-emerald-500/10">技术选型</span>
|
||||
<span class="px-2.5 py-1 bg-emerald-500/10 text-emerald-400 text-[11px] rounded-lg border border-emerald-500/10">架构设计</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Hub -->
|
||||
<div
|
||||
@click="$router.push('/tools/api-hub')"
|
||||
class="bg-gray-900 border border-gray-800 rounded-xl p-6 cursor-pointer hover:border-amber-600/50 hover:bg-gray-900/80 transition-all group"
|
||||
class="tool-card group relative bg-gray-900/80 border border-gray-800/60 rounded-2xl p-6 cursor-pointer hover:border-amber-500/40 transition-all duration-300 overflow-hidden"
|
||||
>
|
||||
<div class="w-10 h-10 rounded-xl bg-amber-600/20 flex items-center justify-center mb-4">
|
||||
<svg class="w-5 h-5 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg>
|
||||
<div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-amber-500 to-amber-400 opacity-0 group-hover:opacity-100 transition-opacity rounded-t-2xl"></div>
|
||||
<div class="w-12 h-12 rounded-xl bg-amber-500/15 flex items-center justify-center mb-4 group-hover:shadow-lg group-hover:shadow-amber-500/10 transition-all duration-300">
|
||||
<svg class="w-6 h-6 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg>
|
||||
</div>
|
||||
<h3 class="text-base font-medium text-gray-200 mb-2 group-hover:text-amber-400 transition-colors">API Hub</h3>
|
||||
<p class="text-sm text-gray-500 leading-relaxed">团队共享API资源管理,集中管理、测试和监控各类公用API服务</p>
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
<span class="px-2 py-0.5 bg-amber-600/10 text-amber-400 text-xs rounded">密码保护</span>
|
||||
<span class="px-2 py-0.5 bg-amber-600/10 text-amber-400 text-xs rounded">在线测试</span>
|
||||
<span class="px-2 py-0.5 bg-amber-600/10 text-amber-400 text-xs rounded">健康监控</span>
|
||||
<h3 class="text-base font-semibold text-gray-200 mb-2 group-hover:text-amber-400 transition-colors">API Hub</h3>
|
||||
<p class="text-sm text-gray-500 leading-relaxed mb-4">团队共享API资源管理,集中管理、测试和监控各类公用API服务</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2.5 py-1 bg-amber-500/10 text-amber-400 text-[11px] rounded-lg border border-amber-500/10">密码保护</span>
|
||||
<span class="px-2.5 py-1 bg-amber-500/10 text-amber-400 text-[11px] rounded-lg border border-amber-500/10">在线测试</span>
|
||||
<span class="px-2.5 py-1 bg-amber-500/10 text-amber-400 text-[11px] rounded-lg border border-amber-500/10">健康监控</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 联网搜索 -->
|
||||
<div
|
||||
@click="$router.push('/tools/web-search')"
|
||||
class="bg-gray-900 border border-gray-800 rounded-xl p-6 cursor-pointer hover:border-blue-600/50 hover:bg-gray-900/80 transition-all group"
|
||||
class="tool-card group relative bg-gray-900/80 border border-gray-800/60 rounded-2xl p-6 cursor-pointer hover:border-blue-500/40 transition-all duration-300 overflow-hidden"
|
||||
>
|
||||
<div class="w-10 h-10 rounded-xl bg-blue-600/20 flex items-center justify-center mb-4">
|
||||
<svg class="w-5 h-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||
<div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-blue-500 to-blue-400 opacity-0 group-hover:opacity-100 transition-opacity rounded-t-2xl"></div>
|
||||
<div class="w-12 h-12 rounded-xl bg-blue-500/15 flex items-center justify-center mb-4 group-hover:shadow-lg group-hover:shadow-blue-500/10 transition-all duration-300">
|
||||
<svg class="w-6 h-6 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||
</div>
|
||||
<h3 class="text-base font-medium text-gray-200 mb-2 group-hover:text-blue-400 transition-colors">联网搜索助手</h3>
|
||||
<p class="text-sm text-gray-500 leading-relaxed">基于豆包大模型的联网搜索,实时获取最新信息并智能整合回答</p>
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
<span class="px-2 py-0.5 bg-blue-600/10 text-blue-400 text-xs rounded">实时搜索</span>
|
||||
<span class="px-2 py-0.5 bg-blue-600/10 text-blue-400 text-xs rounded">智能整合</span>
|
||||
<h3 class="text-base font-semibold text-gray-200 mb-2 group-hover:text-blue-400 transition-colors">联网搜索助手</h3>
|
||||
<p class="text-sm text-gray-500 leading-relaxed mb-4">基于豆包大模型的联网搜索,实时获取最新信息并智能整合回答</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2.5 py-1 bg-blue-500/10 text-blue-400 text-[11px] rounded-lg border border-blue-500/10">实时搜索</span>
|
||||
<span class="px-2.5 py-1 bg-blue-500/10 text-blue-400 text-[11px] rounded-lg border border-blue-500/10">智能整合</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tool-card {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.tool-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
<template>
|
||||
<div class="h-full overflow-y-auto">
|
||||
<div class="max-w-3xl mx-auto px-6 py-6">
|
||||
<div class="max-w-3xl mx-auto px-6 py-6 animate-fade-in">
|
||||
<!-- 用户信息卡片 -->
|
||||
<div v-if="profile" class="bg-gray-900 border border-gray-800 rounded-xl p-6 mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-14 h-14 rounded-full bg-gray-800 flex items-center justify-center text-xl font-bold text-indigo-400 shrink-0 overflow-hidden">
|
||||
<div v-if="profile" class="bg-gray-900/80 border border-gray-800/60 rounded-2xl overflow-hidden mb-6">
|
||||
<!-- 背景 Banner -->
|
||||
<div class="h-24 bg-gradient-to-r from-indigo-600/20 via-purple-600/10 to-indigo-600/20 relative">
|
||||
<div class="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,rgba(99,102,241,0.15),transparent_70%)]"></div>
|
||||
</div>
|
||||
<div class="px-6 pb-6 -mt-8">
|
||||
<div class="flex items-end gap-4">
|
||||
<div class="w-16 h-16 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-xl font-bold text-white shrink-0 overflow-hidden ring-4 ring-gray-900 shadow-xl">
|
||||
<img v-if="profile.avatar" :src="profile.avatar" class="w-full h-full object-cover" />
|
||||
<span v-else>{{ profile.username?.charAt(0)?.toUpperCase() }}</span>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex-1 pt-2">
|
||||
<h2 class="text-lg font-bold text-gray-100">{{ profile.username }}</h2>
|
||||
<p class="text-sm text-gray-500 mt-0.5">{{ profile.email }}</p>
|
||||
</div>
|
||||
<button
|
||||
v-if="!profile.is_self"
|
||||
@click="toggleFollow"
|
||||
class="px-4 py-1.5 rounded-lg text-sm transition-colors"
|
||||
:class="profile.is_following ? 'bg-gray-800 text-gray-400 hover:text-red-400' : 'bg-indigo-600 hover:bg-indigo-700 text-white'"
|
||||
class="px-4 py-1.5 rounded-xl text-sm transition-all"
|
||||
:class="profile.is_following ? 'bg-gray-800 border border-gray-700 text-gray-400 hover:text-red-400 hover:border-red-500/30' : 'bg-indigo-600 hover:bg-indigo-500 hover:shadow-lg hover:shadow-indigo-500/20 text-white'"
|
||||
>{{ profile.is_following ? '已关注' : '关注' }}</button>
|
||||
</div>
|
||||
<!-- 统计 -->
|
||||
<div class="flex gap-6 mt-4 pt-4 border-t border-gray-800">
|
||||
<div class="flex gap-8 mt-5 pt-4 border-t border-gray-800/60">
|
||||
<div class="text-center">
|
||||
<p class="text-base font-bold text-gray-200">{{ profile.post_count }}</p>
|
||||
<p class="text-xs text-gray-500">文章</p>
|
||||
@@ -35,31 +40,35 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<div class="flex gap-1 mb-4 bg-gray-900 rounded-lg p-1 w-fit">
|
||||
<div class="flex gap-1 mb-4 bg-gray-900/80 border border-gray-800/60 rounded-xl p-1 w-fit">
|
||||
<button
|
||||
@click="activeTab = 'posts'; loadContent()"
|
||||
class="px-4 py-1.5 rounded-md text-sm transition-colors"
|
||||
:class="activeTab === 'posts' ? 'bg-gray-800 text-white' : 'text-gray-500 hover:text-gray-300'"
|
||||
class="px-4 py-1.5 rounded-lg text-sm transition-all"
|
||||
:class="activeTab === 'posts' ? 'bg-indigo-600 text-white shadow-lg shadow-indigo-500/20' : 'text-gray-500 hover:text-gray-300'"
|
||||
>文章</button>
|
||||
<button
|
||||
@click="activeTab = 'collects'; loadContent()"
|
||||
class="px-4 py-1.5 rounded-md text-sm transition-colors"
|
||||
:class="activeTab === 'collects' ? 'bg-gray-800 text-white' : 'text-gray-500 hover:text-gray-300'"
|
||||
class="px-4 py-1.5 rounded-lg text-sm transition-all"
|
||||
:class="activeTab === 'collects' ? 'bg-indigo-600 text-white shadow-lg shadow-indigo-500/20' : 'text-gray-500 hover:text-gray-300'"
|
||||
>收藏</button>
|
||||
</div>
|
||||
|
||||
<!-- 帖子列表 -->
|
||||
<div v-if="postList.length === 0" class="text-center py-12">
|
||||
<p class="text-sm text-gray-600">暂无内容</p>
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gray-800/60 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||
</div>
|
||||
<p class="text-sm text-gray-400">暂无内容</p>
|
||||
</div>
|
||||
<div v-else class="space-y-3">
|
||||
<div
|
||||
v-for="post in postList"
|
||||
:key="post.id"
|
||||
@click="$router.push(`/post/${post.id}`)"
|
||||
class="bg-gray-900 border border-gray-800 rounded-xl p-5 cursor-pointer hover:border-gray-700 transition-colors"
|
||||
class="bg-gray-900/80 border border-gray-800/60 rounded-xl p-5 cursor-pointer hover:border-indigo-500/30 transition-all group"
|
||||
>
|
||||
<h3 class="text-base font-medium text-gray-200 mb-2">{{ post.title }}</h3>
|
||||
<p class="text-sm text-gray-500 line-clamp-2 mb-3">{{ post.content?.replace(/<[^>]*>/g, '').substring(0, 150) }}</p>
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
<template>
|
||||
<div class="h-full flex">
|
||||
<!-- 左侧:对话列表 -->
|
||||
<div class="w-64 bg-gray-900 border-r border-gray-800 flex flex-col shrink-0">
|
||||
<div class="p-4">
|
||||
<div class="w-64 bg-gray-900/95 border-r border-gray-800/60 flex flex-col shrink-0">
|
||||
<div class="p-3">
|
||||
<button
|
||||
@click="startNewChat"
|
||||
class="w-full py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-lg transition-colors"
|
||||
class="w-full py-2.5 bg-blue-600 hover:bg-blue-500 text-white text-sm rounded-xl transition-all duration-200 hover:shadow-lg hover:shadow-blue-500/20 flex items-center justify-center gap-1.5 font-medium"
|
||||
>
|
||||
+ 新建搜索
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
||||
新建搜索
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto px-2">
|
||||
<div class="flex-1 overflow-y-auto px-2 space-y-0.5">
|
||||
<div
|
||||
v-for="conv in conversations"
|
||||
:key="conv.id"
|
||||
@click="selectConversation(conv)"
|
||||
class="px-3 py-2 mb-1 rounded-lg cursor-pointer text-sm truncate flex items-center justify-between group"
|
||||
:class="currentConvId === conv.id ? 'bg-gray-800 text-white' : 'text-gray-400 hover:bg-gray-800/50'"
|
||||
class="px-3 py-2.5 rounded-lg cursor-pointer text-[13px] truncate flex items-center justify-between group transition-all duration-150"
|
||||
:class="currentConvId === conv.id ? 'bg-blue-500/15 text-blue-300 border-l-2 border-blue-500' : 'text-gray-400 hover:bg-gray-800/50 border-l-2 border-transparent'"
|
||||
>
|
||||
<span class="truncate">{{ conv.title }}</span>
|
||||
<button
|
||||
@click.stop="deleteConversation(conv.id)"
|
||||
class="opacity-0 group-hover:opacity-100 text-gray-500 hover:text-red-400 ml-2 shrink-0"
|
||||
class="opacity-0 group-hover:opacity-100 text-gray-500 hover:text-red-400 ml-2 shrink-0 transition-opacity"
|
||||
>
|
||||
x
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="conversations.length === 0" class="text-gray-600 text-sm text-center py-8">
|
||||
暂无搜索记录
|
||||
</p>
|
||||
<div v-if="conversations.length === 0" class="text-center py-12">
|
||||
<svg class="w-8 h-8 text-gray-700 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||
<p class="text-gray-600 text-xs">暂无搜索记录</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,39 +39,39 @@
|
||||
<!-- 消息列表 -->
|
||||
<div ref="messagesContainer" class="flex-1 overflow-y-auto p-6 space-y-4">
|
||||
<!-- 欢迎提示 -->
|
||||
<div v-if="messages.length === 0" class="flex items-center justify-center h-full">
|
||||
<div v-if="messages.length === 0" class="flex items-center justify-center h-full animate-fade-in">
|
||||
<div class="text-center max-w-lg">
|
||||
<div class="w-16 h-16 rounded-2xl bg-blue-600/20 flex items-center justify-center mx-auto mb-6">
|
||||
<div class="w-16 h-16 rounded-2xl bg-blue-500/15 flex items-center justify-center mx-auto mb-6 animate-float">
|
||||
<svg class="w-8 h-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-gray-300 mb-4">联网搜索助手</h2>
|
||||
<p class="text-gray-500 mb-6">
|
||||
<h2 class="text-2xl font-bold text-gray-200 mb-3">联网搜索助手</h2>
|
||||
<p class="text-gray-500 mb-8">
|
||||
基于豆包大模型的联网搜索,获取实时信息并智能整合回答。
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-3 text-sm">
|
||||
<div
|
||||
@click="quickSearch('今天有什么科技新闻?')"
|
||||
class="bg-gray-900 border border-gray-800 rounded-lg p-3 text-gray-400 cursor-pointer hover:border-blue-600/50 hover:text-gray-300 transition-all"
|
||||
class="bg-gray-900/80 border border-gray-800/60 rounded-xl p-3.5 text-gray-400 cursor-pointer hover:border-blue-500/30 hover:text-gray-300 transition-all duration-200"
|
||||
>
|
||||
今天科技新闻
|
||||
</div>
|
||||
<div
|
||||
@click="quickSearch('最新的前端框架趋势是什么?')"
|
||||
class="bg-gray-900 border border-gray-800 rounded-lg p-3 text-gray-400 cursor-pointer hover:border-blue-600/50 hover:text-gray-300 transition-all"
|
||||
class="bg-gray-900/80 border border-gray-800/60 rounded-xl p-3.5 text-gray-400 cursor-pointer hover:border-blue-500/30 hover:text-gray-300 transition-all duration-200"
|
||||
>
|
||||
前端框架趋势
|
||||
</div>
|
||||
<div
|
||||
@click="quickSearch('Python 3.13 有哪些新特性?')"
|
||||
class="bg-gray-900 border border-gray-800 rounded-lg p-3 text-gray-400 cursor-pointer hover:border-blue-600/50 hover:text-gray-300 transition-all"
|
||||
class="bg-gray-900/80 border border-gray-800/60 rounded-xl p-3.5 text-gray-400 cursor-pointer hover:border-blue-500/30 hover:text-gray-300 transition-all duration-200"
|
||||
>
|
||||
Python 最新版本
|
||||
</div>
|
||||
<div
|
||||
@click="quickSearch('目前最受欢迎的 AI 编程工具有哪些?')"
|
||||
class="bg-gray-900 border border-gray-800 rounded-lg p-3 text-gray-400 cursor-pointer hover:border-blue-600/50 hover:text-gray-300 transition-all"
|
||||
class="bg-gray-900/80 border border-gray-800/60 rounded-xl p-3.5 text-gray-400 cursor-pointer hover:border-blue-500/30 hover:text-gray-300 transition-all duration-200"
|
||||
>
|
||||
AI 编程工具推荐
|
||||
</div>
|
||||
@@ -110,18 +112,18 @@
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<div class="border-t border-gray-800 p-4 bg-gray-900/50">
|
||||
<div class="border-t border-gray-800/60 p-4 bg-gray-900/80 backdrop-blur-sm">
|
||||
<!-- 模型选择器 -->
|
||||
<div v-if="availableModels.length > 0" class="flex items-center gap-2 mb-3">
|
||||
<span class="text-xs text-gray-500">模型:</span>
|
||||
<select v-model="selectedModelId" class="px-2 py-1 bg-gray-800 border border-gray-700 rounded-lg text-xs text-gray-300 focus:outline-none focus:border-blue-500">
|
||||
<span class="text-[11px] text-gray-500 font-medium">模型</span>
|
||||
<select v-model="selectedModelId" class="px-2.5 py-1.5 bg-gray-800/80 border border-gray-700/50 rounded-lg text-xs text-gray-300 focus:outline-none focus:border-blue-500/50 transition-colors">
|
||||
<option :value="null">默认</option>
|
||||
<option v-for="m in availableModels" :key="m.id" :value="m.id">
|
||||
{{ m.model_name || m.model_id }}
|
||||
<template v-if="m.web_search_enabled"> 🌐</template>
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="selectedModel?.web_search_enabled" class="text-xs text-blue-400">🌐 联网搜索</span>
|
||||
<span v-if="selectedModel?.web_search_enabled" class="text-[11px] text-blue-400 bg-blue-500/10 px-2 py-0.5 rounded-md">🌐 联网搜索</span>
|
||||
</div>
|
||||
<div class="flex gap-3 items-end">
|
||||
<textarea
|
||||
@@ -131,15 +133,15 @@
|
||||
ref="textareaRef"
|
||||
placeholder="输入你想搜索的问题..."
|
||||
rows="2"
|
||||
class="flex-1 px-4 py-2.5 bg-gray-800 border border-gray-700 rounded-xl text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500 resize-none max-h-48 text-sm"
|
||||
class="flex-1 px-4 py-2.5 bg-gray-800/80 border border-gray-700/50 rounded-xl text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500/50 focus:shadow-[0_0_0_3px_rgba(59,130,246,0.1)] resize-none max-h-48 text-sm transition-all duration-200"
|
||||
:disabled="isStreaming"
|
||||
></textarea>
|
||||
<button
|
||||
@click="handleSend"
|
||||
:disabled="isStreaming || !inputText.trim()"
|
||||
class="px-4 py-2.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-30 text-white rounded-xl transition-colors shrink-0 text-sm"
|
||||
class="px-4 py-2.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-30 text-white rounded-xl transition-all duration-200 shrink-0 text-sm hover:shadow-lg hover:shadow-blue-500/20"
|
||||
>
|
||||
搜索
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,11 +10,11 @@ export default defineConfig({
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8000',
|
||||
target: 'http://127.0.0.1:7964',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/uploads': {
|
||||
target: 'http://127.0.0.1:8000',
|
||||
target: 'http://127.0.0.1:7964',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user