This commit is contained in:
2026-08-05 15:56:08 +08:00
parent 01729b1e0b
commit 2d9e2376b6
106 changed files with 10007 additions and 253 deletions
+149
View File
@@ -1108,3 +1108,152 @@ html[data-theme] .restore-progress {
border-radius: 8px;
background: var(--bg-secondary);
}
/* Aurora UI — light-first blue/violet product theme */
:root {
--bg-primary: #f3f6fb;
--bg-secondary: #ffffff;
--bg-tertiary: #eef2f8;
--bg-hover: #edf1ff;
--bg-soft: #f7f9fc;
--surface-raised: #ffffff;
--surface-inset: #f7f9fc;
--surface-neutral: rgba(67, 56, 202, 0.055);
--surface-neutral-hover: rgba(67, 56, 202, 0.1);
--glass-header: rgba(255, 255, 255, 0.86);
--glass-panel: rgba(255, 255, 255, 0.94);
--text-primary: #172033;
--text-secondary: #5f6b7e;
--text-muted: #929db0;
--accent: #5457d9;
--accent-hover: #4547c4;
--accent-strong: #3436a8;
--accent-soft: #eef0ff;
--accent-line: #c7caff;
--border: #e4e9f2;
--border-strong: #d5ddea;
--danger: #dc4c64;
--danger-soft: #fff0f3;
--success: #159570;
--button-primary-bg: #5154d8;
--button-primary-hover: #4144bf;
--button-primary-text: #ffffff;
--shadow: 0 22px 60px rgba(43, 57, 91, 0.14);
--shadow-soft: 0 8px 28px rgba(43, 57, 91, 0.08);
--shadow-composer: 0 18px 46px rgba(43, 57, 91, 0.14);
--shadow-control: rgba(43, 57, 91, 0.1);
}
:root[data-theme="dark"] {
--bg-primary: #0e1422;
--bg-secondary: #151d2e;
--bg-tertiary: #1c2639;
--bg-hover: #242f46;
--bg-soft: #111929;
--surface-raised: #1a2437;
--surface-inset: #111929;
--surface-neutral: rgba(142, 147, 255, 0.08);
--surface-neutral-hover: rgba(142, 147, 255, 0.14);
--glass-header: rgba(21, 29, 46, 0.86);
--glass-panel: rgba(21, 29, 46, 0.94);
--text-primary: #f4f6fb;
--text-secondary: #aeb8ca;
--text-muted: #738097;
--accent: #979aff;
--accent-hover: #b1b3ff;
--accent-strong: #c8caff;
--accent-soft: rgba(111, 115, 255, 0.16);
--accent-line: #565b9d;
--border: #263249;
--border-strong: #34415b;
--danger-soft: rgba(220, 76, 100, 0.14);
--button-primary-bg: #777bf2;
--button-primary-hover: #8d90ff;
--button-primary-text: #ffffff;
--shadow: 0 24px 64px rgba(0, 0, 0, 0.35);
--shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.22);
--shadow-composer: 0 20px 54px rgba(0, 0, 0, 0.3);
}
html[data-theme] .chat-layout,
html[data-theme] .chat-main {
background:
radial-gradient(circle at 78% -12%, rgba(99, 102, 241, 0.11), transparent 34%),
radial-gradient(circle at 110% 82%, rgba(56, 189, 248, 0.07), transparent 30%),
var(--bg-primary) !important;
}
html[data-theme] .sidebar {
border-right: 1px solid rgba(255, 255, 255, 0.1);
background:
radial-gradient(circle at 15% -5%, rgba(125, 130, 255, 0.3), transparent 28%),
linear-gradient(180deg, #18213a 0%, #11182a 100%) !important;
box-shadow: 12px 0 34px rgba(27, 37, 66, 0.12) !important;
color: #eef2ff;
}
html[data-theme] .sidebar :is(.sidebar-brand strong, .user-info strong) { color: #f7f8ff; }
html[data-theme] .sidebar :is(.sidebar-brand p, .conversation-heading, .user-info span) { color: #8290ad; }
html[data-theme] .sidebar :is(.conversation-item, .user-row) { color: #aeb8cf; }
html[data-theme] .brand-badge {
border-radius: 12px;
background: linear-gradient(145deg, #8589ff, #5659db);
box-shadow: 0 8px 22px rgba(84, 87, 217, 0.38) !important;
}
html[data-theme] .new-chat-btn {
border: 0;
border-radius: 11px;
background: linear-gradient(135deg, #7478ed, #5054d1);
color: #ffffff;
box-shadow: 0 9px 22px rgba(70, 74, 190, 0.32);
}
html[data-theme] .new-chat-btn:hover { background: linear-gradient(135deg, #8588f5, #5b5fda); box-shadow: 0 11px 26px rgba(70, 74, 190, 0.4); transform: translateY(-1px); }
html[data-theme] .conversation-item { border-radius: 10px; }
html[data-theme] .conversation-item:hover { background: rgba(255, 255, 255, 0.07); color: #ffffff; transform: translateX(2px); }
html[data-theme] .conversation-item.active {
background: linear-gradient(90deg, rgba(129, 134, 255, 0.27), rgba(129, 134, 255, 0.1));
color: #ffffff;
box-shadow: inset 3px 0 0 #8e92ff;
}
html[data-theme] .conversation-icon-wrap { border-radius: 9px; background: rgba(255, 255, 255, 0.07); }
html[data-theme] .sidebar-footer { border-color: rgba(255, 255, 255, 0.08); background: rgba(6, 10, 20, 0.22); }
html[data-theme] .chat-header {
border-color: rgba(213, 221, 234, 0.8);
background: var(--glass-header);
box-shadow: 0 8px 28px rgba(43, 57, 91, 0.07);
backdrop-filter: blur(18px);
}
html[data-theme] .welcome-icon {
border-radius: 16px;
background: linear-gradient(145deg, #f0f1ff, #e2e5ff);
color: #5154d8;
box-shadow: 0 10px 26px rgba(81, 84, 216, 0.16);
}
html[data-theme] .welcome h2 { font-size: clamp(28px, 4vw, 44px); color: var(--text-primary); }
html[data-theme] :is(.input-wrapper, .composer, .workflow-panel) {
border-color: rgba(202, 211, 226, 0.9);
border-radius: 18px;
background: rgba(255, 255, 255, 0.94);
box-shadow: var(--shadow-composer) !important;
backdrop-filter: blur(16px);
}
:root[data-theme="dark"] :is(.input-wrapper, .composer, .workflow-panel) { border-color: var(--border-strong); background: rgba(21, 29, 46, 0.94); }
html[data-theme] .input-wrapper:focus-within { border-color: #8588ee; box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.12), var(--shadow-composer) !important; }
html[data-theme] .btn { border-radius: 10px; }
html[data-theme] .btn-primary,
html[data-theme] :is(.composer-footer button, .workflow-command > button, .primary-workflow-button) {
border-color: transparent;
border-radius: 11px;
background: linear-gradient(135deg, #6265e5, #484bc8);
color: #ffffff;
box-shadow: 0 8px 20px rgba(72, 75, 200, 0.24);
}
html[data-theme] .message.assistant .message-avatar { border-radius: 12px; background: linear-gradient(145deg, #eeefff, #dfe2ff); color: #4d50cc; }
html[data-theme] .message.user .message-content { border-radius: 18px 18px 5px 18px; background: linear-gradient(135deg, #5d61dc, #4548bb); color: #ffffff; box-shadow: 0 8px 22px rgba(69, 72, 187, 0.18); }
+11 -8
View File
@@ -155,7 +155,7 @@
/>
<button
v-if="settings.agents.length"
v-if="!auth.isGuest && settings.agents.length"
class="agent-trigger"
:class="{ active: activeAgent }"
type="button"
@@ -184,7 +184,7 @@
</button>
<button
v-if="imageModelOptions.length"
v-if="!auth.isGuest && imageModelOptions.length"
class="mode-tab"
:class="{ active: isComfyModel }"
type="button"
@@ -231,7 +231,7 @@
</button>
<button
v-if="!hasDraft"
v-if="!auth.isGuest && !hasDraft"
ref="voiceTriggerRef"
class="voice-trigger"
type="button"
@@ -267,8 +267,8 @@
<p v-else-if="activeAgent" class="input-hint">
Agent 会自动识别任务并调用合适的语言或图片生成模型
</p>
<p v-else-if="auth.isGuest && features.upload_image" class="input-hint input-hint-warn">
游客模式不支持发送图片登录后即可上传
<p v-else-if="auth.isGuest" class="input-hint input-hint-warn">
游客仅支持 qwen3.6 文本对话登录后可使用全部模型Agent文件和语音功能
</p>
<p v-else-if="imageBlockedByModel" class="input-hint input-hint-warn">当前模型不支持图片识别已自动隐藏图片上传</p>
<p v-else class="input-hint">{{ defaultInputHint }}</p>
@@ -373,18 +373,20 @@ const activeModel = computed(() => {
})
const activeAgent = computed(() => {
if (auth.isGuest) return null
const selected = settings.agents.find(item => item.id === chat.selectedAgentId)
if (selected) return selected
return chat.selectedAgentId ? settings.agents.find(item => item.id === 'auto') || null : null
})
const isComfyModel = computed(() => activeModel.value?.provider === 'comfy' && !activeAgent.value)
const imageModelOptions = computed(() => settings.models.filter(m => m.provider === 'comfy'))
const imageModelOptions = computed(() => auth.isGuest ? [] : settings.models.filter(m => m.provider === 'comfy'))
const textModelOptions = computed(() => settings.models.filter(m => m.provider !== 'comfy'))
const selectedModelValue = computed(() => chat.selectedModelId == null ? '' : String(chat.selectedModelId))
const voicePersonaName = computed(() => settings.voicePersona?.name || 'AI 客服')
const voicePersonaGreeting = computed(() => settings.voicePersona?.greeting || '您好,请问有什么可以帮您?')
const inputPlaceholder = computed(() => {
if (auth.isGuest) return '使用 qwen3.6 开始文字对话,登录后可解锁全部模型和工具'
if (isComfyModel.value) return '描述要生成或处理的图片;可上传原图后说“去水印、换背景、修复照片”'
if (activeAgent.value?.placeholder) return activeAgent.value.placeholder
return '给我一段需求,我可以继续对话,也可以帮你联动图片生成'
@@ -401,8 +403,8 @@ const modelSupportsImage = computed(() => {
})
const canUploadImage = computed(() => !auth.isGuest && features.value.upload_image && perms.value.can_upload_image && modelSupportsImage.value)
const canUploadVideo = computed(() => !isComfyModel.value && features.value.upload_video && perms.value.can_upload_video)
const canUploadFile = computed(() => !isComfyModel.value && features.value.upload_file && perms.value.can_upload_file)
const canUploadVideo = computed(() => !auth.isGuest && !isComfyModel.value && features.value.upload_video && perms.value.can_upload_video)
const canUploadFile = computed(() => !auth.isGuest && !isComfyModel.value && features.value.upload_file && perms.value.can_upload_file)
const dropOverlayTitle = computed(() => {
if (canUploadImage.value) return '松开即可添加图片'
@@ -530,6 +532,7 @@ function voiceFeatureAvailable() {
}
async function startVoiceMode() {
if (auth.isGuest) return
if (chat.sending) {
notification.show({
type: 'info',
+26
View File
@@ -1,5 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useSettingsStore } from '@/stores/settings'
const routes = [
{
@@ -19,6 +20,12 @@ const routes = [
name: 'Chat',
component: () => import('@/views/ChatView.vue'),
meta: { requiresAuth: true }
},
{
path: '/short-drama',
name: 'ShortDrama',
component: () => import('@/views/ShortDramaView.vue'),
meta: { requiresAuth: true, requiresAccount: true, shortDrama: true }
}
]
@@ -29,6 +36,7 @@ const router = createRouter({
router.beforeEach(async (to, from, next) => {
const auth = useAuthStore()
const settings = useSettingsStore()
if (!auth.initialized) {
await auth.init()
@@ -39,6 +47,24 @@ router.beforeEach(async (to, from, next) => {
return
}
if (to.meta.requiresAccount && auth.isGuest) {
next({ name: 'Login', query: { redirect: to.fullPath } })
return
}
if (to.meta.shortDrama) {
try {
if (!settings.loaded) await settings.loadPublic()
} catch {
next({ name: 'Chat' })
return
}
if (!settings.features.short_drama) {
next({ name: 'Chat' })
return
}
}
if (to.meta.guest && auth.isLoggedIn && !auth.isGuest) {
next({ name: 'Chat' })
return
+7 -2
View File
@@ -49,8 +49,13 @@ export const useAuthStore = defineStore('auth', () => {
return res.data
}
async function register(username, email, password) {
const res = await api.post('/auth/register', { username, email, password })
async function register(username, email, password, invitationCode) {
const res = await api.post('/auth/register', {
username,
email,
password,
invitation_code: invitationCode
})
setSession(res.data.data, 'account')
return res.data
}
+5 -2
View File
@@ -13,10 +13,12 @@ export const useSettingsStore = defineStore('settings', () => {
upload_image: true,
upload_video: true,
upload_file: true,
paste_image: true
paste_image: true,
short_drama: true
})
const siteName = ref('AI Chat')
const allowRegister = ref(true)
const registrationRequiresInvite = ref(true)
const voicePersona = ref({
name: 'AI 客服',
greeting: '您好,请问有什么可以帮您?'
@@ -31,6 +33,7 @@ export const useSettingsStore = defineStore('settings', () => {
features.value = data.features
siteName.value = data.site_name
allowRegister.value = data.allow_register
registrationRequiresInvite.value = data.registration_requires_invite !== false
voicePersona.value = data.voice_persona || voicePersona.value
loaded.value = true
}
@@ -45,5 +48,5 @@ export const useSettingsStore = defineStore('settings', () => {
agents.value = res.data.data || []
}
return { features, siteName, allowRegister, voicePersona, models, agents, loaded, loadPublic, loadModels, loadAgents }
return { features, siteName, allowRegister, registrationRequiresInvite, voicePersona, models, agents, loaded, loadPublic, loadModels, loadAgents }
})
+13 -1
View File
@@ -18,6 +18,15 @@
<h2 class="chat-title">{{ currentTitle }}</h2>
</div>
<div class="header-actions">
<router-link
v-if="settings.features.short_drama && !auth.isGuest"
to="/short-drama"
class="btn btn-ghost btn-sm drama-link"
aria-label="打开短剧工坊"
>
<IconMovie :size="16" :stroke-width="1.8" />
<span class="action-label">短剧</span>
</router-link>
<ThemeToggle />
<a
v-if="auth.user?.role === 'admin'"
@@ -71,6 +80,7 @@ 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'
import IconSettings from '@tabler/icons-vue/dist/esm/icons/IconSettings.mjs'
import IconMovie from '@tabler/icons-vue/dist/esm/icons/IconMovie.mjs'
const router = useRouter()
const auth = useAuthStore()
@@ -83,7 +93,9 @@ const currentTitle = computed(() => {
return conv?.title || '新对话'
})
const adminUrl = import.meta.env.VITE_ADMIN_URL || `${window.location.protocol}//${window.location.hostname}:5174`
const adminUrl = import.meta.env.VITE_ADMIN_URL || (import.meta.env.PROD
? `${window.location.origin}/admin/`
: `${window.location.protocol}//${window.location.hostname}:5174`)
onMounted(async () => {
try {
+28 -2
View File
@@ -6,7 +6,7 @@
<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>
<p>使用管理员签发的邀请码加入指定部门登录后即可使用全部模型与创作工具</p>
<span class="auth-note">TEXT · IMAGE · AGENT</span>
</aside>
@@ -30,6 +30,17 @@
<label>密码</label>
<input v-model="password" type="password" class="form-input" placeholder="至少 6 位" required />
</div>
<div class="form-group">
<label>邀请码</label>
<input
v-model.trim="invitationCode"
class="form-input invitation-input"
placeholder="例如 INV-1A2B3C4D5E"
autocomplete="one-time-code"
required
/>
<span class="field-help">邀请码仅可使用一次注册后会自动加入邀请码指定的部门</span>
</div>
<p v-if="error" class="form-error">{{ error }}</p>
<button type="submit" class="btn btn-primary auth-btn" :disabled="loading">
{{ loading ? '注册中...' : '创建并进入' }}
@@ -60,6 +71,7 @@ const settings = useSettingsStore()
const username = ref('')
const email = ref('')
const password = ref('')
const invitationCode = ref('')
const error = ref('')
const loading = ref(false)
@@ -73,7 +85,7 @@ async function handleRegister() {
error.value = ''
loading.value = true
try {
await auth.register(username.value, email.value, password.value)
await auth.register(username.value, email.value, password.value, invitationCode.value)
router.push('/')
} catch (e) {
error.value = e.message
@@ -230,6 +242,20 @@ async function handleRegister() {
font-size: 13px;
}
.invitation-input {
font-family: "Cascadia Code", Consolas, monospace;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.field-help {
display: block;
margin-top: 7px;
color: var(--text-muted);
font-size: 11px;
line-height: 1.5;
}
.auth-btn {
width: 100%;
min-height: 47px;
File diff suppressed because it is too large Load Diff