gengx
This commit is contained in:
@@ -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); }
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user