更新
This commit is contained in:
+929
-92
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,10 +5,10 @@
|
||||
<span class="brand-badge">AI</span>
|
||||
<div class="brand-copy">
|
||||
<strong>{{ settings.siteName || 'AI Chat' }}</strong>
|
||||
<p>统一文本与图片创作</p>
|
||||
<p>TEXT · IMAGE · AGENT</p>
|
||||
</div>
|
||||
<button class="sidebar-close" type="button" aria-label="关闭会话列表" @click="chat.closeSidebar">
|
||||
<IconX :size="19" :stroke-width="1.8" />
|
||||
<IconX :size="18" :stroke-width="1.8" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<div class="conversation-list">
|
||||
<div class="conversation-heading">
|
||||
<span>最近对话</span>
|
||||
<span>{{ chat.conversations.length }}</span>
|
||||
<span>{{ chat.conversations.length.toString().padStart(2, '0') }}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -32,7 +32,9 @@
|
||||
:aria-current="conv.id === chat.currentId ? 'page' : undefined"
|
||||
@click="selectConv(conv.id)"
|
||||
>
|
||||
<IconMessageCircle class="conversation-icon" :size="17" :stroke-width="1.7" />
|
||||
<span class="conversation-icon-wrap">
|
||||
<IconMessageCircle class="conversation-icon" :size="16" :stroke-width="1.7" />
|
||||
</span>
|
||||
<span class="conv-title">{{ conv.title }}</span>
|
||||
<button
|
||||
class="delete-btn"
|
||||
@@ -40,11 +42,11 @@
|
||||
:aria-label="`删除对话:${conv.title}`"
|
||||
@click.stop="handleDelete(conv.id)"
|
||||
>
|
||||
<IconTrash :size="15" :stroke-width="1.8" />
|
||||
<IconTrash :size="14" :stroke-width="1.8" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="!chat.conversations.length" class="empty-tip">还没有会话,先创建一个新对话</p>
|
||||
<p v-if="!chat.conversations.length" class="empty-tip">还没有会话<br />点击上方按钮开始创作</p>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
@@ -54,6 +56,7 @@
|
||||
<span class="user-name">{{ auth.user?.nickname || auth.user?.username }}</span>
|
||||
<span class="user-level">{{ auth.isGuest ? '游客模式 · 自动保存' : (auth.user?.membership_name || '普通用户') }}</span>
|
||||
</div>
|
||||
<span class="user-online" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -122,45 +125,62 @@ async function handleDelete(id) {
|
||||
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border-right: 1px solid var(--border);
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: var(--sidebar-width);
|
||||
flex: 0 0 var(--sidebar-width);
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.24s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
backdrop-filter: blur(14px);
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--border);
|
||||
background:
|
||||
radial-gradient(circle at 40% -12%, rgba(183, 243, 107, 0.075), transparent 24%),
|
||||
rgba(13, 16, 21, 0.96);
|
||||
box-shadow: 18px 0 50px rgba(0, 0, 0, 0.16);
|
||||
backdrop-filter: blur(20px);
|
||||
transition: transform 260ms var(--ease-spring);
|
||||
}
|
||||
|
||||
.sidebar::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -1px;
|
||||
width: 1px;
|
||||
height: 24%;
|
||||
background: linear-gradient(to bottom, var(--accent), transparent);
|
||||
box-shadow: 0 0 16px rgba(183, 243, 107, 0.42);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.sidebar-top {
|
||||
padding: 16px 14px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
gap: 17px;
|
||||
padding: 18px 14px 13px;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
display: flex;
|
||||
min-height: 42px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 40px;
|
||||
padding: 2px;
|
||||
gap: 11px;
|
||||
padding: 1px 2px;
|
||||
}
|
||||
|
||||
.brand-badge {
|
||||
display: grid;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
flex: 0 0 auto;
|
||||
place-items: center;
|
||||
border: 1px solid rgba(183, 243, 107, 0.5);
|
||||
border-radius: 12px;
|
||||
background: var(--accent-soft);
|
||||
border: 1px solid #d9e6fb;
|
||||
color: var(--accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
flex-shrink: 0;
|
||||
background: var(--accent);
|
||||
color: #11150e;
|
||||
font-family: "Cascadia Code", Consolas, monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.04em;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5), 0 4px 0 #55782f, 0 10px 22px rgba(110, 166, 54, 0.16);
|
||||
}
|
||||
|
||||
.brand-copy {
|
||||
@@ -173,17 +193,19 @@ async function handleDelete(id) {
|
||||
overflow: hidden;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
font-weight: 690;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sidebar-brand p {
|
||||
margin-top: 2px;
|
||||
margin-top: 3px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
font-family: "Cascadia Code", Consolas, monospace;
|
||||
font-size: 8px;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.sidebar-close {
|
||||
@@ -192,81 +214,99 @@ async function handleDelete(id) {
|
||||
height: 34px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.new-chat-btn {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 43px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
padding: 0 14px;
|
||||
background: var(--accent);
|
||||
border: 1px solid rgba(183, 243, 107, 0.62);
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
background: linear-gradient(180deg, var(--accent-hover), var(--accent));
|
||||
color: #11150e;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 7px 18px rgba(45, 102, 218, 0.2);
|
||||
transition: background 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
|
||||
font-weight: 720;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.48), 0 5px 0 #577c2f, 0 13px 24px rgba(126, 183, 67, 0.13);
|
||||
transition: transform 180ms var(--ease-spring), background 160ms ease, box-shadow 180ms ease;
|
||||
}
|
||||
|
||||
.new-chat-btn:hover {
|
||||
background: var(--accent-hover);
|
||||
box-shadow: 0 9px 22px rgba(45, 102, 218, 0.24);
|
||||
background: linear-gradient(180deg, #d1ff9c, var(--accent-hover));
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.52), 0 6px 0 #5f8538, 0 15px 28px rgba(126, 183, 67, 0.18);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.new-chat-btn:active {
|
||||
transform: scale(0.98);
|
||||
transform: translateY(4px) scale(0.99);
|
||||
box-shadow: inset 0 2px 5px rgba(68, 96, 37, 0.22), 0 1px 0 #577c2f;
|
||||
}
|
||||
|
||||
.conversation-list {
|
||||
flex: 1;
|
||||
padding: 5px 10px 14px;
|
||||
overflow-y: auto;
|
||||
padding: 6px 9px 12px;
|
||||
}
|
||||
|
||||
.conversation-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 9px 7px;
|
||||
padding: 10px 9px 8px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-family: "Cascadia Code", Consolas, monospace;
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 43px;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
min-height: 42px;
|
||||
margin-bottom: 2px;
|
||||
padding: 8px 8px 8px 10px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 3px;
|
||||
padding: 7px 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 11px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: transform 170ms var(--ease-spring), border-color 160ms ease, background 160ms ease, color 160ms ease;
|
||||
}
|
||||
|
||||
.conversation-item:hover {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--border);
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
color: var(--text-primary);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.conversation-item.active {
|
||||
background: var(--accent-soft);
|
||||
box-shadow: inset 3px 0 0 var(--accent);
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
border-color: rgba(183, 243, 107, 0.25);
|
||||
background: linear-gradient(90deg, rgba(183, 243, 107, 0.13), rgba(183, 243, 107, 0.04));
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.conversation-icon-wrap {
|
||||
display: grid;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex: 0 0 auto;
|
||||
place-items: center;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
}
|
||||
|
||||
.conversation-icon {
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.conversation-item.active .conversation-icon {
|
||||
@@ -281,16 +321,15 @@ async function handleDelete(id) {
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
display: grid;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
flex: 0 0 auto;
|
||||
place-items: center;
|
||||
border-radius: 8px;
|
||||
color: var(--text-muted);
|
||||
opacity: 0;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease;
|
||||
transition: opacity 150ms ease, background 150ms ease, color 150ms ease, transform 150ms ease;
|
||||
}
|
||||
|
||||
.conversation-item:hover .delete-btn,
|
||||
@@ -299,63 +338,78 @@ async function handleDelete(id) {
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: #fff0f0;
|
||||
background: var(--danger-soft);
|
||||
color: var(--danger);
|
||||
transform: rotate(3deg);
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
padding: 26px 18px;
|
||||
margin: 18px 8px;
|
||||
padding: 25px 16px;
|
||||
border: 1px dashed var(--border-strong);
|
||||
border-radius: 13px;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
font-size: 11px;
|
||||
line-height: 1.7;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 10px 12px 12px;
|
||||
padding: 11px 13px 13px;
|
||||
border-top: 1px solid var(--border);
|
||||
background: rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px;
|
||||
padding: 6px 4px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: grid;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 11px;
|
||||
background: #eef1f6;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
flex-shrink: 0;
|
||||
flex: 0 0 auto;
|
||||
place-items: center;
|
||||
border: 1px solid rgba(183, 243, 107, 0.28);
|
||||
border-radius: 10px;
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.user-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
overflow: hidden;
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 550;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.user-level {
|
||||
margin-top: 2px;
|
||||
margin-top: 3px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.user-online {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 11px rgba(183, 243, 107, 0.58);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -363,30 +417,11 @@ async function handleDelete(id) {
|
||||
position: fixed;
|
||||
inset: 0 auto 0 0;
|
||||
z-index: 100;
|
||||
width: min(84vw, 304px);
|
||||
width: min(86vw, 310px);
|
||||
transform: translateX(-100%);
|
||||
box-shadow: 20px 0 50px rgba(21, 32, 51, 0.14);
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.sidebar-close {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
opacity: 0.68;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.sidebar,
|
||||
.new-chat-btn,
|
||||
.conversation-item,
|
||||
.delete-btn {
|
||||
transition: none;
|
||||
}
|
||||
.sidebar.open { transform: translateX(0); }
|
||||
.sidebar-close { display: flex; }
|
||||
.delete-btn { opacity: 0.66; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1082,4 +1082,36 @@ function zoomByWheel(event) { zoom.value = Math.min(3, Math.max(0.25, zoom.value
|
||||
@media(max-width:1100px){.ratio-options button{padding:0 7px}.ratio-options button>i{display:none}.workflow-panel{width:calc(100% - 24px)}.primary-workflow-button{min-width:96px;padding:0 12px}}
|
||||
@media(max-width:800px){.workflow-panel{position:absolute;right:8px;bottom:calc(46dvh + 18px);left:8px;z-index:3;width:auto;margin:0}.outpaint-panel{overflow-x:auto;justify-content:flex-start}.outpaint-frame{width:min(94vw,calc(38dvh * var(--outpaint-ratio)));height:min(38dvh,520px);max-width:94vw}.restore-overlay{padding-bottom:16px}.restore-progress{min-height:46px;padding:0 12px;font-size:12px}}
|
||||
@media(prefers-reduced-motion:reduce){.workflow-panel button:active,.tool-bar button:active{transform:none}}
|
||||
|
||||
/* Uiverse-inspired image workbench */
|
||||
.workbench{background:#090b0f;color:var(--text-primary)}
|
||||
.workbench-sidebar,.tool-bar,.canvas-footer{border-color:var(--border);background:#101319;color:var(--text-primary)}
|
||||
.sidebar-header,.ops-panel,.command-log{border-color:var(--border)}
|
||||
.sidebar-header span,.composer-footer span,.ops-title,.tool-note p,.outpaint-side-note p,.command-log strong{color:var(--text-muted)}
|
||||
.icon-button,.tool-bar>button:not(.icon-button),.canvas-footer button,.selection-tools>button,.ratio-options button{color:var(--text-secondary)}
|
||||
.icon-button:hover,.tool-bar>button:hover,.tool-bar>button.active,.canvas-footer button:hover,.selection-tools>button:hover,.selection-tools>button.active,.ratio-options button:hover,.ratio-options button.active{background:var(--accent-soft);color:var(--accent)}
|
||||
.result-strip button img{border-color:var(--border);background:var(--bg-tertiary);box-shadow:0 10px 26px rgba(0,0,0,.28)}
|
||||
.result-strip button.active img{border-color:var(--accent);box-shadow:0 0 0 3px rgba(183,243,107,.08),0 12px 28px rgba(0,0,0,.34)}
|
||||
.tool-chip{border:1px solid rgba(183,243,107,.2);background:var(--accent-soft);color:var(--accent)}
|
||||
.clear-mask,.command-tool,.status,.panel-status{color:var(--accent)}
|
||||
.outpaint-side-note,.command-log li{border-color:var(--border);background:var(--surface-inset)}
|
||||
.outpaint-side-note span{color:var(--accent)}
|
||||
.command-text{color:var(--text-secondary)}
|
||||
.suggestions button{border-color:var(--border);background:rgba(255,255,255,.02);color:var(--text-secondary)}
|
||||
.suggestions button:hover{border-color:var(--accent-line);background:var(--accent-soft);color:var(--accent)}
|
||||
.composer,.workflow-panel{border-color:var(--border-strong);background:var(--bg-secondary);box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 16px 40px rgba(0,0,0,.34)}
|
||||
.composer textarea,.workflow-command input{color:var(--text-primary)}
|
||||
.composer textarea::placeholder,.workflow-command input::placeholder{color:var(--text-muted)}
|
||||
.composer-footer button,.workflow-command>button,.primary-workflow-button{border:1px solid rgba(183,243,107,.54);background:linear-gradient(180deg,var(--accent-hover),var(--accent));color:#11150e;box-shadow:inset 0 1px 0 rgba(255,255,255,.45),0 4px 0 #577c2f}
|
||||
.composer-footer button:active,.workflow-command>button:active,.primary-workflow-button:active{transform:translateY(3px);box-shadow:0 1px 0 #577c2f}
|
||||
.workflow-command>svg{color:var(--accent)}
|
||||
.selection-tools,.panel-divider,.selection-tools>i{border-color:var(--border);background-color:var(--border)}
|
||||
.background-toggle{color:var(--text-secondary)}
|
||||
.canvas-shell{background-color:#0b0e12;background-image:radial-gradient(rgba(255,255,255,.05) .8px,transparent .8px);background-size:18px 18px}
|
||||
.canvas-stage{background-color:#11151a;background-image:linear-gradient(45deg,#191e25 25%,transparent 25%),linear-gradient(-45deg,#191e25 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#191e25 75%),linear-gradient(-45deg,transparent 75%,#191e25 75%)}
|
||||
.canvas-stage img{box-shadow:0 24px 64px rgba(0,0,0,.5)}
|
||||
.outpaint-frame{border-color:var(--border-strong);background-color:#0e1217;background-image:radial-gradient(circle,#2b332d 1px,transparent 1.1px);box-shadow:inset 0 0 0 1px rgba(255,255,255,.025)}
|
||||
.restore-progress{border-color:var(--border-strong);background:rgba(16,19,25,.96);color:var(--text-primary);box-shadow:0 18px 48px rgba(0,0,0,.48)}
|
||||
.restore-progress>svg,.restore-progress button{color:var(--accent)}
|
||||
@media(max-width:800px){.workbench-sidebar{border-color:var(--border-strong);background:var(--bg-secondary);box-shadow:0 22px 60px rgba(0,0,0,.5)}}
|
||||
</style>
|
||||
|
||||
@@ -795,4 +795,94 @@ async function copyContent() {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Uiverse-inspired message surfaces */
|
||||
.message {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
border-color: var(--border-strong);
|
||||
background: var(--surface-raised);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045), 0 7px 18px rgba(0, 0, 0, 0.22);
|
||||
}
|
||||
|
||||
.message.user .message-avatar {
|
||||
border-color: var(--border-strong);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.message.assistant .message-avatar {
|
||||
border-color: rgba(183, 243, 107, 0.28);
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 0 20px rgba(183, 243, 107, 0.06);
|
||||
}
|
||||
|
||||
.message.user .message-content {
|
||||
border-color: rgba(183, 243, 107, 0.23);
|
||||
background:
|
||||
linear-gradient(145deg, rgba(183, 243, 107, 0.12), rgba(183, 243, 107, 0.055)),
|
||||
var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 10px 28px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.message.assistant .message-content {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.message-action-btn {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.message-action-btn:hover,
|
||||
.message-action-btn:focus-visible {
|
||||
border-color: var(--border);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.message-content :deep(pre) {
|
||||
border-color: var(--border);
|
||||
background: #080a0d;
|
||||
}
|
||||
|
||||
.message.user .message-content :deep(pre),
|
||||
.message.user .message-content :deep(:not(pre) > code) {
|
||||
background: rgba(4, 7, 5, 0.46);
|
||||
}
|
||||
|
||||
.image-loading-grid i {
|
||||
background: linear-gradient(112deg, #11151a 8%, #20262c 38%, #151b1c 64%, #11151a 92%);
|
||||
background-size: 240% 100%;
|
||||
}
|
||||
|
||||
.generation-progress {
|
||||
border-color: rgba(183, 243, 107, 0.24);
|
||||
background: rgba(13, 17, 21, 0.9);
|
||||
color: var(--accent);
|
||||
box-shadow: 0 7px 20px rgba(0, 0, 0, 0.26);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.att-image,
|
||||
.att-video {
|
||||
border-color: var(--border-strong);
|
||||
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
|
||||
.att-document {
|
||||
border-color: var(--border);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 8px 22px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.att-document:hover {
|
||||
border-color: var(--accent-line);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -547,4 +547,101 @@ watch(
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Uiverse-inspired dark component treatment */
|
||||
.message-list {
|
||||
padding-top: 32px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.scroll-bottom-btn {
|
||||
border-color: var(--border-strong);
|
||||
background: rgba(18, 22, 28, 0.92);
|
||||
color: var(--text-secondary);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045), 0 12px 34px rgba(0, 0, 0, 0.36);
|
||||
}
|
||||
|
||||
.scroll-bottom-btn:hover {
|
||||
border-color: var(--accent-line);
|
||||
background: #171c22;
|
||||
color: var(--accent);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.055), 0 14px 38px rgba(0, 0, 0, 0.42);
|
||||
}
|
||||
|
||||
.welcome-panel {
|
||||
position: relative;
|
||||
width: min(100%, 680px);
|
||||
padding: 34px 26px 40px;
|
||||
}
|
||||
|
||||
.welcome-panel::before {
|
||||
display: block;
|
||||
width: 46px;
|
||||
height: 1px;
|
||||
margin: 0 auto 22px;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 14px rgba(183, 243, 107, 0.48);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.welcome-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin-bottom: 23px;
|
||||
border-color: rgba(183, 243, 107, 0.5);
|
||||
border-radius: 15px;
|
||||
background: var(--accent);
|
||||
color: #11150e;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5), 0 5px 0 #577c2f, 0 15px 30px rgba(110, 166, 54, 0.17);
|
||||
}
|
||||
|
||||
.welcome h2 {
|
||||
max-width: 590px;
|
||||
margin-right: auto;
|
||||
margin-bottom: 13px;
|
||||
margin-left: auto;
|
||||
color: var(--text-primary);
|
||||
font-size: clamp(27px, 3vw, 38px);
|
||||
font-weight: 760;
|
||||
letter-spacing: -0.055em;
|
||||
line-height: 1.16;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.welcome p {
|
||||
max-width: 500px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.loading-state span {
|
||||
border: 1px solid var(--border);
|
||||
background: linear-gradient(100deg, #15191f 15%, #20262e 38%, #15191f 62%);
|
||||
background-size: 220% 100%;
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
width: min(100%, var(--input-max-width));
|
||||
padding-left: 51px;
|
||||
}
|
||||
|
||||
.typing-indicator span {
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 9px rgba(183, 243, 107, 0.22);
|
||||
}
|
||||
|
||||
.image-preview-overlay {
|
||||
background: rgba(4, 5, 7, 0.9);
|
||||
}
|
||||
|
||||
.image-preview-close {
|
||||
border-color: var(--border-strong);
|
||||
background: rgba(18, 22, 28, 0.9);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.message-list { padding-top: 20px; }
|
||||
.welcome-panel { padding: 22px 8px 30px; }
|
||||
.welcome h2 { font-size: 25px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -289,4 +289,47 @@ async function copyDetail(item) {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Uiverse-inspired notification surfaces */
|
||||
.notification-card {
|
||||
border-color: var(--border-strong);
|
||||
background:
|
||||
radial-gradient(circle at 100% 0%, rgba(183, 243, 107, 0.05), transparent 32%),
|
||||
rgba(16, 19, 25, 0.98);
|
||||
color: var(--text-primary);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045), 0 20px 56px rgba(0, 0, 0, 0.46);
|
||||
}
|
||||
|
||||
.notification-card::before {
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 12px rgba(183, 243, 107, 0.44);
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
border: 1px solid rgba(255, 126, 121, 0.22);
|
||||
background: var(--danger-soft);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.notification-close,
|
||||
.copy-detail {
|
||||
border-color: var(--border);
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.notification-close:hover,
|
||||
.copy-detail:hover {
|
||||
border-color: var(--accent-line);
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.notification-detail,
|
||||
.detail-body,
|
||||
.detail-body pre {
|
||||
border-color: var(--border);
|
||||
background: var(--surface-inset);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="theme-toggle" role="group" aria-label="界面主题">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: theme === 'light' }"
|
||||
:aria-pressed="theme === 'light'"
|
||||
aria-label="使用浅色主题"
|
||||
title="浅色主题"
|
||||
@click="setTheme('light')"
|
||||
>
|
||||
<span>浅色</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: theme === 'dark' }"
|
||||
:aria-pressed="theme === 'dark'"
|
||||
aria-label="使用深色主题"
|
||||
title="深色主题"
|
||||
@click="setTheme('dark')"
|
||||
>
|
||||
<span>深色</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
|
||||
const { theme, setTheme } = useTheme()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.theme-toggle {
|
||||
display: inline-grid;
|
||||
grid-template-columns: repeat(2, 42px);
|
||||
gap: 0;
|
||||
padding: 2px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
button {
|
||||
display: grid;
|
||||
width: 42px;
|
||||
height: 30px;
|
||||
place-items: center;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
transition: color 160ms ease, background 160ms ease, transform 160ms ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px) scale(0.96);
|
||||
}
|
||||
|
||||
button.active {
|
||||
border-color: transparent;
|
||||
background: var(--button-primary-bg);
|
||||
color: var(--button-primary-text);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const STORAGE_KEY = 'ai-chat-theme'
|
||||
const DEFAULT_THEME = 'light'
|
||||
|
||||
function readStoredTheme() {
|
||||
try {
|
||||
return localStorage.getItem(STORAGE_KEY) === 'dark' ? 'dark' : DEFAULT_THEME
|
||||
} catch {
|
||||
return DEFAULT_THEME
|
||||
}
|
||||
}
|
||||
|
||||
const theme = ref(readStoredTheme())
|
||||
let transitionTimer
|
||||
|
||||
function applyTheme(value, animate = false) {
|
||||
const nextTheme = value === 'dark' ? 'dark' : 'light'
|
||||
const root = document.documentElement
|
||||
|
||||
if (animate) {
|
||||
root.classList.add('theme-transitioning')
|
||||
window.clearTimeout(transitionTimer)
|
||||
transitionTimer = window.setTimeout(() => root.classList.remove('theme-transitioning'), 260)
|
||||
}
|
||||
|
||||
root.dataset.theme = nextTheme
|
||||
root.style.colorScheme = nextTheme
|
||||
theme.value = nextTheme
|
||||
}
|
||||
|
||||
export function initializeTheme() {
|
||||
applyTheme(theme.value)
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const isDark = computed(() => theme.value === 'dark')
|
||||
|
||||
function setTheme(value) {
|
||||
const nextTheme = value === 'dark' ? 'dark' : 'light'
|
||||
applyTheme(nextTheme, true)
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, nextTheme)
|
||||
} catch {
|
||||
// Theme still works for the current session when storage is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
return { theme, isDark, setTheme }
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('storage', event => {
|
||||
if (event.key === STORAGE_KEY) applyTheme(event.newValue)
|
||||
})
|
||||
}
|
||||
@@ -2,8 +2,11 @@ import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { initializeTheme } from './composables/useTheme'
|
||||
import './assets/main.css'
|
||||
|
||||
initializeTheme()
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
@@ -258,6 +258,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
attachments,
|
||||
agent_id: agentId || null,
|
||||
image_tool: options.imageTool || null,
|
||||
voice_mode: !!options.voiceMode,
|
||||
stream: true
|
||||
})
|
||||
})
|
||||
@@ -288,6 +289,11 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const appendVisibleContent = (content) => {
|
||||
if (!content) return
|
||||
receivedContent += content
|
||||
try {
|
||||
options.onTextDelta?.(content)
|
||||
} catch {
|
||||
// 展示层或语音层回调失败不应中断主消息流。
|
||||
}
|
||||
|
||||
if (!isAgentStream) {
|
||||
fullContent += content
|
||||
|
||||
@@ -17,6 +17,10 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
})
|
||||
const siteName = ref('AI Chat')
|
||||
const allowRegister = ref(true)
|
||||
const voicePersona = ref({
|
||||
name: 'AI 客服',
|
||||
greeting: '您好,请问有什么可以帮您?'
|
||||
})
|
||||
const models = ref([])
|
||||
const agents = ref([])
|
||||
const loaded = ref(false)
|
||||
@@ -27,6 +31,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
features.value = data.features
|
||||
siteName.value = data.site_name
|
||||
allowRegister.value = data.allow_register
|
||||
voicePersona.value = data.voice_persona || voicePersona.value
|
||||
loaded.value = true
|
||||
}
|
||||
|
||||
@@ -40,5 +45,5 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
agents.value = res.data.data || []
|
||||
}
|
||||
|
||||
return { features, siteName, allowRegister, models, agents, loaded, loadPublic, loadModels, loadAgents }
|
||||
return { features, siteName, allowRegister, voicePersona, models, agents, loaded, loadPublic, loadModels, loadAgents }
|
||||
})
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
<main class="chat-main">
|
||||
<header class="chat-header">
|
||||
<button class="menu-btn" type="button" aria-label="打开会话列表" @click="chat.toggleSidebar">
|
||||
<IconMenu2 :size="21" :stroke-width="1.8" />
|
||||
<IconMenu2 :size="20" :stroke-width="1.8" />
|
||||
</button>
|
||||
<div class="chat-heading">
|
||||
<span class="chat-kicker">AI 创作空间</span>
|
||||
<span class="chat-kicker"><i aria-hidden="true" /> AI 创作空间</span>
|
||||
<h2 class="chat-title">{{ currentTitle }}</h2>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<ThemeToggle />
|
||||
<a
|
||||
v-if="auth.user?.role === 'admin'"
|
||||
:href="adminUrl"
|
||||
@@ -26,7 +27,7 @@
|
||||
class="btn btn-ghost btn-sm admin-link"
|
||||
aria-label="打开管理后台"
|
||||
>
|
||||
<IconSettings :size="17" :stroke-width="1.8" />
|
||||
<IconSettings :size="16" :stroke-width="1.8" />
|
||||
<span class="action-label">后台</span>
|
||||
</a>
|
||||
<button
|
||||
@@ -35,8 +36,8 @@
|
||||
:aria-label="auth.isGuest ? '登录账户' : '退出登录'"
|
||||
@click="handleAccountAction"
|
||||
>
|
||||
<IconLogin v-if="auth.isGuest" :size="17" :stroke-width="1.8" />
|
||||
<IconLogout v-else :size="17" :stroke-width="1.8" />
|
||||
<IconLogin v-if="auth.isGuest" :size="16" :stroke-width="1.8" />
|
||||
<IconLogout v-else :size="16" :stroke-width="1.8" />
|
||||
<span class="action-label">{{ auth.isGuest ? '登录' : '退出' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -65,6 +66,7 @@ import { useSettingsStore } from '@/stores/settings'
|
||||
import ChatSidebar from '@/components/ChatSidebar.vue'
|
||||
import MessageList from '@/components/MessageList.vue'
|
||||
import ChatInput from '@/components/ChatInput.vue'
|
||||
import ThemeToggle from '@/components/ThemeToggle.vue'
|
||||
import IconLogout from '@tabler/icons-vue/dist/esm/icons/IconLogout.mjs'
|
||||
import IconLogin from '@tabler/icons-vue/dist/esm/icons/IconLogin.mjs'
|
||||
import IconMenu2 from '@tabler/icons-vue/dist/esm/icons/IconMenu2.mjs'
|
||||
@@ -93,11 +95,17 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
async function handleSend({ content, attachments, agentId }) {
|
||||
async function handleSend({ content, attachments, agentId, voiceMode, signal, onTextDelta, onComplete, onCancel, onError }) {
|
||||
try {
|
||||
await chat.sendMessage(content, attachments, agentId)
|
||||
await chat.sendMessage(content, attachments, agentId, { onTextDelta, voiceMode, signal })
|
||||
onComplete?.()
|
||||
} catch (e) {
|
||||
if (e?.name === 'AbortError' || signal?.aborted) {
|
||||
onCancel?.()
|
||||
return
|
||||
}
|
||||
notification.error(e)
|
||||
onError?.(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,114 +126,129 @@ function handleAccountAction() {
|
||||
height: 100%;
|
||||
min-height: 100dvh;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #f8faff 0%, var(--bg-primary) 100%);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.chat-main {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
background: transparent;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 82% -20%, rgba(183, 243, 107, 0.075), transparent 34%),
|
||||
transparent;
|
||||
}
|
||||
|
||||
.chat-main::before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
background-image: radial-gradient(rgba(255, 255, 255, 0.055) 0.7px, transparent 0.7px);
|
||||
background-size: 17px 17px;
|
||||
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.62), transparent 74%);
|
||||
content: "";
|
||||
opacity: 0.28;
|
||||
}
|
||||
|
||||
.chat-main > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
height: var(--header-height);
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
padding: 0 22px;
|
||||
border-bottom: 1px solid rgba(225, 231, 239, 0.92);
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
backdrop-filter: blur(14px);
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: rgba(10, 12, 16, 0.76);
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.08);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.menu-btn {
|
||||
display: none;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
color: var(--text-secondary);
|
||||
transition: background 0.16s ease, color 0.16s ease, transform 0.16s ease;
|
||||
transition: background 160ms ease, border-color 160ms ease, color 160ms ease, transform 160ms ease;
|
||||
}
|
||||
|
||||
.menu-btn:hover {
|
||||
background: var(--bg-soft);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--accent-line);
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.menu-btn:active {
|
||||
transform: scale(0.96);
|
||||
transform: translateY(1px) scale(0.97);
|
||||
}
|
||||
|
||||
.chat-heading {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chat-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 1px;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.04em;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 2px;
|
||||
color: var(--text-muted);
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.11em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.chat-kicker i {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 11px rgba(183, 243, 107, 0.6);
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-size: 16px;
|
||||
font-weight: 650;
|
||||
line-height: 1.25;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 680;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.25;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
min-height: 36px;
|
||||
padding: 7px 12px;
|
||||
font-size: 13px;
|
||||
padding: 7px 11px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chat-header {
|
||||
padding: 0 10px 0 8px;
|
||||
}
|
||||
|
||||
.menu-btn {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.chat-kicker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
width: 36px;
|
||||
min-height: 36px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.action-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.menu-btn {
|
||||
transition: none;
|
||||
}
|
||||
.chat-header { padding: 0 10px 0 8px; }
|
||||
.menu-btn { display: flex; }
|
||||
.chat-kicker { display: none; }
|
||||
.chat-title { font-size: 16px; }
|
||||
.btn-sm { width: 36px; min-height: 36px; padding: 0; }
|
||||
.action-label { display: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,38 +1,51 @@
|
||||
<template>
|
||||
<div class="auth-page">
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<div class="logo">💬</div>
|
||||
<h1>{{ settings.siteName }}</h1>
|
||||
<p>登录您的账户</p>
|
||||
<ThemeToggle class="auth-theme-toggle" />
|
||||
<section class="auth-shell">
|
||||
<aside class="auth-story">
|
||||
<span class="auth-logo"><IconSparkles :size="22" :stroke-width="1.8" /></span>
|
||||
<span class="auth-eyebrow">AI CREATIVE SPACE</span>
|
||||
<h1>把想法放进来,<br />让创作自然发生。</h1>
|
||||
<p>在同一个工作区完成对话、图像生成与内容整理。</p>
|
||||
<span class="auth-note">TEXT · IMAGE · AGENT</span>
|
||||
</aside>
|
||||
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<span class="auth-status"><i /> 账户登录</span>
|
||||
<h2>{{ settings.siteName }}</h2>
|
||||
<p>欢迎回来,请登录您的账户</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleLogin">
|
||||
<div class="form-group">
|
||||
<label>账号</label>
|
||||
<input v-model="account" class="form-input" placeholder="用户名或邮箱" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input v-model="password" type="password" class="form-input" placeholder="请输入密码" required />
|
||||
</div>
|
||||
<p v-if="error" class="form-error">{{ error }}</p>
|
||||
<button type="submit" class="btn btn-primary auth-btn" :disabled="loading">
|
||||
{{ loading ? '登录中...' : '进入创作空间' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="auth-footer">
|
||||
还没有账户?
|
||||
<router-link to="/register">立即注册</router-link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleLogin">
|
||||
<div class="form-group">
|
||||
<label>账号</label>
|
||||
<input v-model="account" class="form-input" placeholder="用户名或邮箱" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input v-model="password" type="password" class="form-input" placeholder="请输入密码" required />
|
||||
</div>
|
||||
<p v-if="error" class="form-error">{{ error }}</p>
|
||||
<button type="submit" class="btn btn-primary auth-btn" :disabled="loading">
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="auth-footer">
|
||||
还没有账户?
|
||||
<router-link to="/register">立即注册</router-link>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import IconSparkles from '@tabler/icons-vue/dist/esm/icons/IconSparkles.mjs'
|
||||
import ThemeToggle from '@/components/ThemeToggle.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
|
||||
@@ -64,54 +77,166 @@ async function handleLogin() {
|
||||
|
||||
<style scoped>
|
||||
.auth-page {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
position: relative;
|
||||
min-height: 100dvh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 28px;
|
||||
overflow: auto;
|
||||
background:
|
||||
radial-gradient(circle at 72% 16%, rgba(183, 243, 107, 0.08), transparent 26%),
|
||||
var(--bg-primary);
|
||||
}
|
||||
|
||||
.auth-theme-toggle {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.auth-shell {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: min(900px, 100%);
|
||||
grid-template-columns: 1.08fr 0.92fr;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 22px;
|
||||
background: var(--bg-secondary);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045), var(--shadow);
|
||||
}
|
||||
|
||||
.auth-shell::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 16%;
|
||||
width: 36%;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, var(--accent), transparent);
|
||||
box-shadow: 0 0 17px rgba(183, 243, 107, 0.44);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.auth-story {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 550px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 54px;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--border);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(183, 243, 107, 0.07), transparent 45%),
|
||||
repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.018) 0 1px, transparent 1px 14px),
|
||||
#0c0f13;
|
||||
}
|
||||
|
||||
.auth-logo {
|
||||
display: grid;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
place-items: center;
|
||||
margin-bottom: 46px;
|
||||
border: 1px solid rgba(183, 243, 107, 0.5);
|
||||
border-radius: 14px;
|
||||
background: var(--accent);
|
||||
color: #11150e;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.52), 0 5px 0 #55782f, 0 14px 28px rgba(110, 166, 54, 0.17);
|
||||
}
|
||||
|
||||
.auth-eyebrow,
|
||||
.auth-note {
|
||||
color: var(--accent);
|
||||
font-family: "Cascadia Code", Consolas, monospace;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.16em;
|
||||
}
|
||||
|
||||
.auth-story h1 {
|
||||
margin: 15px 0 19px;
|
||||
font-size: clamp(36px, 4.7vw, 54px);
|
||||
font-weight: 760;
|
||||
letter-spacing: -0.06em;
|
||||
line-height: 1.04;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.auth-story p {
|
||||
max-width: 31ch;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.auth-note {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: 54px;
|
||||
color: var(--text-muted);
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 40px 32px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 48px 42px;
|
||||
background:
|
||||
radial-gradient(circle at 100% 0%, rgba(183, 243, 107, 0.05), transparent 28%),
|
||||
var(--bg-secondary);
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
.auth-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.auth-header h1 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
.auth-status i {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 12px rgba(183, 243, 107, 0.6);
|
||||
}
|
||||
|
||||
.auth-header p {
|
||||
.auth-header h2 {
|
||||
margin: 12px 0 7px;
|
||||
font-size: 27px;
|
||||
letter-spacing: -0.045em;
|
||||
}
|
||||
|
||||
.auth-header p,
|
||||
.auth-footer {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.auth-btn {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
padding: 14px;
|
||||
min-height: 47px;
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
margin-top: 25px;
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.auth-page { padding: 14px; }
|
||||
.auth-shell { grid-template-columns: 1fr; }
|
||||
.auth-story { display: none; }
|
||||
.auth-card { min-height: 540px; padding: 40px 25px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,42 +1,55 @@
|
||||
<template>
|
||||
<div class="auth-page">
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<div class="logo">💬</div>
|
||||
<h1>{{ settings.siteName }}</h1>
|
||||
<p>创建新账户</p>
|
||||
<ThemeToggle class="auth-theme-toggle" />
|
||||
<section class="auth-shell">
|
||||
<aside class="auth-story">
|
||||
<span class="auth-logo"><IconSparkles :size="22" :stroke-width="1.8" /></span>
|
||||
<span class="auth-eyebrow">START CREATING</span>
|
||||
<h1>一个账户,<br />连接全部创作工具。</h1>
|
||||
<p>创建账户后即可保存会话,并使用可用的模型与 Agent。</p>
|
||||
<span class="auth-note">TEXT · IMAGE · AGENT</span>
|
||||
</aside>
|
||||
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<span class="auth-status"><i /> 创建账户</span>
|
||||
<h2>{{ settings.siteName }}</h2>
|
||||
<p>填写信息,开始新的创作会话</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleRegister">
|
||||
<div class="form-group">
|
||||
<label>用户名</label>
|
||||
<input v-model="username" class="form-input" placeholder="3-50 个字符" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>邮箱</label>
|
||||
<input v-model="email" type="email" class="form-input" placeholder="your@email.com" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input v-model="password" type="password" class="form-input" placeholder="至少 6 位" required />
|
||||
</div>
|
||||
<p v-if="error" class="form-error">{{ error }}</p>
|
||||
<button type="submit" class="btn btn-primary auth-btn" :disabled="loading">
|
||||
{{ loading ? '注册中...' : '创建并进入' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="auth-footer">
|
||||
已有账户?
|
||||
<router-link to="/login">立即登录</router-link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleRegister">
|
||||
<div class="form-group">
|
||||
<label>用户名</label>
|
||||
<input v-model="username" class="form-input" placeholder="3-50 个字符" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>邮箱</label>
|
||||
<input v-model="email" type="email" class="form-input" placeholder="your@email.com" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input v-model="password" type="password" class="form-input" placeholder="至少 6 位" required />
|
||||
</div>
|
||||
<p v-if="error" class="form-error">{{ error }}</p>
|
||||
<button type="submit" class="btn btn-primary auth-btn" :disabled="loading">
|
||||
{{ loading ? '注册中...' : '注册' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="auth-footer">
|
||||
已有账户?
|
||||
<router-link to="/login">立即登录</router-link>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import IconSparkles from '@tabler/icons-vue/dist/esm/icons/IconSparkles.mjs'
|
||||
import ThemeToggle from '@/components/ThemeToggle.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
|
||||
@@ -72,54 +85,166 @@ async function handleRegister() {
|
||||
|
||||
<style scoped>
|
||||
.auth-page {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
position: relative;
|
||||
min-height: 100dvh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 28px;
|
||||
overflow: auto;
|
||||
background:
|
||||
radial-gradient(circle at 72% 16%, rgba(183, 243, 107, 0.08), transparent 26%),
|
||||
var(--bg-primary);
|
||||
}
|
||||
|
||||
.auth-theme-toggle {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.auth-shell {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: min(900px, 100%);
|
||||
grid-template-columns: 1.08fr 0.92fr;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 22px;
|
||||
background: var(--bg-secondary);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045), var(--shadow);
|
||||
}
|
||||
|
||||
.auth-shell::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 16%;
|
||||
width: 36%;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, var(--accent), transparent);
|
||||
box-shadow: 0 0 17px rgba(183, 243, 107, 0.44);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.auth-story {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 610px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 54px;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--border);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(183, 243, 107, 0.07), transparent 45%),
|
||||
repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.018) 0 1px, transparent 1px 14px),
|
||||
#0c0f13;
|
||||
}
|
||||
|
||||
.auth-logo {
|
||||
display: grid;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
place-items: center;
|
||||
margin-bottom: 46px;
|
||||
border: 1px solid rgba(183, 243, 107, 0.5);
|
||||
border-radius: 14px;
|
||||
background: var(--accent);
|
||||
color: #11150e;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.52), 0 5px 0 #55782f, 0 14px 28px rgba(110, 166, 54, 0.17);
|
||||
}
|
||||
|
||||
.auth-eyebrow,
|
||||
.auth-note {
|
||||
color: var(--accent);
|
||||
font-family: "Cascadia Code", Consolas, monospace;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.16em;
|
||||
}
|
||||
|
||||
.auth-story h1 {
|
||||
margin: 15px 0 19px;
|
||||
font-size: clamp(36px, 4.7vw, 54px);
|
||||
font-weight: 760;
|
||||
letter-spacing: -0.06em;
|
||||
line-height: 1.04;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.auth-story p {
|
||||
max-width: 31ch;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.auth-note {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: 54px;
|
||||
color: var(--text-muted);
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 40px 32px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 42px;
|
||||
background:
|
||||
radial-gradient(circle at 100% 0%, rgba(183, 243, 107, 0.05), transparent 28%),
|
||||
var(--bg-secondary);
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
margin-bottom: 27px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
.auth-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.auth-header h1 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
.auth-status i {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 12px rgba(183, 243, 107, 0.6);
|
||||
}
|
||||
|
||||
.auth-header p {
|
||||
.auth-header h2 {
|
||||
margin: 12px 0 7px;
|
||||
font-size: 27px;
|
||||
letter-spacing: -0.045em;
|
||||
}
|
||||
|
||||
.auth-header p,
|
||||
.auth-footer {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.auth-btn {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
padding: 14px;
|
||||
min-height: 47px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
margin-top: 23px;
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.auth-page { padding: 14px; }
|
||||
.auth-shell { grid-template-columns: 1fr; }
|
||||
.auth-story { display: none; }
|
||||
.auth-card { min-height: 640px; padding: 36px 25px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user