diff --git a/Yi.Ai.Vue3/.claude/settings.local.json b/Yi.Ai.Vue3/.claude/settings.local.json index d3606838..776c8df4 100644 --- a/Yi.Ai.Vue3/.claude/settings.local.json +++ b/Yi.Ai.Vue3/.claude/settings.local.json @@ -4,7 +4,9 @@ "Bash(npx vue-tsc --noEmit)", "Bash(timeout 60 npx vue-tsc:*)", "Bash(npm run dev:*)", - "Bash(xargs:*)" + "Bash(taskkill:*)", + "Bash(timeout /t 5 /nobreak)", + "Bash(git checkout:*)" ], "deny": [], "ask": [] diff --git a/Yi.Ai.Vue3/src/api/channel/index.ts b/Yi.Ai.Vue3/src/api/channel/index.ts index 154bf6de..ec63849f 100644 --- a/Yi.Ai.Vue3/src/api/channel/index.ts +++ b/Yi.Ai.Vue3/src/api/channel/index.ts @@ -8,6 +8,7 @@ import type { AiModelCreateInput, AiModelUpdateInput, AiModelGetListInput, + AppShortcutDto, PagedResultDto, } from './types'; @@ -103,3 +104,10 @@ export function deleteModel(id: string) { export function clearPremiumModelCache() { return post('/model/clear-premium-cache').json(); } + +// ==================== 快捷渠道 ==================== + +// 获取快捷渠道列表 +export function getAppShortcutList() { + return get('/channel/app-shortcut').json(); +} diff --git a/Yi.Ai.Vue3/src/api/channel/types.ts b/Yi.Ai.Vue3/src/api/channel/types.ts index 96fb710e..b166ffeb 100644 --- a/Yi.Ai.Vue3/src/api/channel/types.ts +++ b/Yi.Ai.Vue3/src/api/channel/types.ts @@ -117,6 +117,17 @@ export interface AiModelGetListInput { maxResultCount?: number; } +// 快捷渠道DTO +export interface AppShortcutDto { + id: string; + name: string; + endpoint: string; + extraUrl?: string; + apiKey: string; + orderNum: number; + creationTime: string; +} + // 分页结果 export interface PagedResultDto { items: T[]; diff --git a/Yi.Ai.Vue3/src/components/SystemAnnouncementDialog/index.vue b/Yi.Ai.Vue3/src/components/SystemAnnouncementDialog/index.vue index 9354a37c..30a3d978 100644 --- a/Yi.Ai.Vue3/src/components/SystemAnnouncementDialog/index.vue +++ b/Yi.Ai.Vue3/src/components/SystemAnnouncementDialog/index.vue @@ -11,6 +11,21 @@ const isLoadingData = ref(false); const activeTab = ref('activity'); +// 图片预览相关状态 +const isImageViewerVisible = ref(false); +const currentPreviewUrl = ref(''); + +// 打开图片预览 +function openImagePreview(url: string) { + currentPreviewUrl.value = url; + isImageViewerVisible.value = true; +} + +// 关闭图片预览 +function closeImagePreview() { + isImageViewerVisible.value = false; +} + // 窗口宽度响应式状态 const windowWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1920); @@ -123,9 +138,18 @@ watch(isDialogVisible, async (newValue) => { :key="index" class="activity-item" > - -
+ +
+ +
+ + + + + + +
{
+ + + + + @@ -489,15 +523,22 @@ watch(isDialogVisible, async (newValue) => { .activity-item { position: relative; - padding: 0; + padding: 16px; background: #fff; border-radius: 16px; - overflow: hidden; + overflow: visible; transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); border: 2px solid transparent; background-clip: padding-box; + // 清除浮动,确保父容器高度正确 + &::after { + content: ''; + display: table; + clear: both; + } + &::before { content: ''; position: absolute; @@ -525,9 +566,8 @@ watch(isDialogVisible, async (newValue) => { opacity: 1; } - .activity-image { - transform: scale(1.1); - filter: brightness(1.05); + .activity-image-wrapper { + box-shadow: 0 8px 24px rgba(102, 126, 234, 0.2); } .detail-link { @@ -545,10 +585,31 @@ watch(isDialogVisible, async (newValue) => { .activity-image-wrapper { position: relative; - width: 100%; - height: 220px; + float: right; + width: 160px; + height: 160px; + margin-left: 16px; + margin-bottom: 8px; + border-radius: 12px; overflow: hidden; background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); + shape-outside: inset(0); + transition: box-shadow 0.3s; + cursor: pointer; + z-index: 1; // 确保在内容之上 + + &:hover { + box-shadow: 0 4px 16px rgba(102, 126, 234, 0.3); + } + + &:hover .activity-image { + transform: scale(1.1); + } + + &:hover .zoom-icon { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } &::before { content: ''; @@ -560,6 +621,7 @@ watch(isDialogVisible, async (newValue) => { background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); z-index: 1; animation: shine 3s infinite; + pointer-events: none; // 确保不拦截鼠标事件 } &::after { @@ -587,22 +649,22 @@ watch(isDialogVisible, async (newValue) => { .activity-image { width: 100%; height: 100%; - object-fit: cover; + object-fit: contain; // 等比例缩放,完整显示图片,不裁剪 transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); } .activity-status-badge { position: absolute; - top: 16px; - right: 16px; + top: 12px; + right: 12px; z-index: 2; animation: fadeInScale 0.5s ease-out 0.3s both; :deep(.el-tag) { - border-radius: 20px; - padding: 7px 18px; + border-radius: 16px; + padding: 5px 14px; font-weight: 700; - font-size: 13px; + font-size: 11px; border: none; backdrop-filter: blur(12px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); @@ -648,24 +710,52 @@ watch(isDialogVisible, async (newValue) => { } } + .zoom-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0.8); + width: 48px; + height: 48px; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(8px); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + pointer-events: none; // 不拦截鼠标事件,让父容器接收hover + z-index: 10; // 提高层级,确保在状态标签之上 + + svg { + width: 24px; + height: 24px; + color: #fff; + stroke-width: 2; + } + } + .activity-body { - padding: 20px; + // 内容会自动环绕浮动的图片 + position: relative; + z-index: 0; // 确保在浮动图片之下 } .activity-header { display: flex; align-items: center; gap: 12px; - margin-bottom: 12px; + margin-bottom: 8px; } .activity-title { margin: 0; - font-size: 18px; + font-size: 16px; font-weight: 700; color: #1a1a1a; flex: 1; - line-height: 1.4; + line-height: 1.3; background: linear-gradient(135deg, #1a1a1a 0%, #4a5568 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; @@ -679,23 +769,23 @@ watch(isDialogVisible, async (newValue) => { .activity-content-list { padding: 0; - margin: 0 0 16px 0; + margin: 0 0 12px 0; color: #4a5568; - font-size: 14px; - line-height: 1.8; + font-size: 13px; + line-height: 1.6; .content-line { - margin: 8px 0; - padding-left: 18px; + margin: 6px 0; + padding-left: 16px; position: relative; &::before { content: ''; position: absolute; left: 0; - top: 11px; - width: 6px; - height: 6px; + top: 9px; + width: 5px; + height: 5px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 50%; } @@ -715,8 +805,9 @@ watch(isDialogVisible, async (newValue) => { justify-content: space-between; align-items: center; gap: 16px; - padding-top: 16px; + padding-top: 12px; border-top: 1px dashed #e8e9eb; + clear: both; // 清除浮动,始终在新行显示,占满100%宽度 } .activity-time-range { @@ -727,7 +818,7 @@ watch(isDialogVisible, async (newValue) => { } .activity-time { - font-size: 13px; + font-size: 12px; color: #718096; display: flex; align-items: center; @@ -735,19 +826,19 @@ watch(isDialogVisible, async (newValue) => { &:first-child::before { content: '🕐'; - font-size: 14px; + font-size: 13px; } } .detail-link { display: inline-flex; align-items: center; - gap: 8px; - padding: 10px 18px; + gap: 6px; + padding: 8px 16px; background: linear-gradient(135deg, #f0f1f3 0%, #e8eaed 100%); color: #667eea; - border-radius: 24px; - font-size: 13px; + border-radius: 20px; + font-size: 12px; font-weight: 700; text-decoration: none; transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); @@ -1095,16 +1186,27 @@ watch(isDialogVisible, async (newValue) => { .activity-item { border-radius: 10px; - } - - .activity-image-wrapper { - height: 180px; + padding: 0; } .activity-body { padding: 16px; } + .activity-image-wrapper { + float: none; + width: 100%; + height: 180px; + min-height: 180px; + margin: 0 0 16px 0; + border-radius: 10px 10px 0 0; + } + + .activity-status-badge { + top: 12px; + right: 12px; + } + .activity-header { margin-bottom: 10px; } @@ -1183,7 +1285,18 @@ watch(isDialogVisible, async (newValue) => { .activity-content { .activity-image-wrapper { + float: none; + width: 100%; height: 200px; + margin: 0 0 16px 0; + } + + .activity-item { + padding: 0; + } + + .activity-body { + padding: 18px; } } diff --git a/Yi.Ai.Vue3/src/config/permission.ts b/Yi.Ai.Vue3/src/config/permission.ts index 975246fd..e658fe28 100644 --- a/Yi.Ai.Vue3/src/config/permission.ts +++ b/Yi.Ai.Vue3/src/config/permission.ts @@ -23,7 +23,7 @@ export const PAGE_PERMISSIONS: PermissionConfig[] = [ { path: '/console/channel', allowedUsers: ['cc', 'Guo'], - description: '渠道商管理页面 - 仅限cc和Guo用户访问', + description: '号池管理页面 - 仅限cc和Guo用户访问', }, { path: '/console/system-statistics', diff --git a/Yi.Ai.Vue3/src/pages/chat/image/components/ImageGenerator.vue b/Yi.Ai.Vue3/src/pages/chat/image/components/ImageGenerator.vue index 5e328ee7..2da2ecfa 100644 --- a/Yi.Ai.Vue3/src/pages/chat/image/components/ImageGenerator.vue +++ b/Yi.Ai.Vue3/src/pages/chat/image/components/ImageGenerator.vue @@ -46,6 +46,8 @@ const generating = ref(false); const currentTaskId = ref(''); const currentTask = ref(null); const showViewer = ref(false); +const referenceImageViewerVisible = ref(false); +const referenceImagePreviewUrl = ref(''); let pollTimer: any = null; let debounceTimer: any = null; @@ -130,6 +132,17 @@ function handleRemove(file: UploadFile) { fileList.value.splice(index, 1); } +function handlePreview(file: UploadFile) { + if (file.url) { + referenceImagePreviewUrl.value = file.url; + referenceImageViewerVisible.value = true; + } +} + +function closeReferenceImageViewer() { + referenceImageViewerVisible.value = false; +} + // Handle paste event for reference images function handlePaste(event: ClipboardEvent) { const items = event.clipboardData?.items; @@ -510,6 +523,7 @@ onUnmounted(() => { :limit="2" :on-change="handleFileChange" :on-remove="handleRemove" + :on-preview="handlePreview" accept=".jpg,.jpeg,.png,.bmp,.webp" :class="{ 'hide-upload-btn': fileList.length >= 2 }" > @@ -644,6 +658,15 @@ onUnmounted(() => {
+ + + + + diff --git a/Yi.Ai.Vue3/src/pages/console/index.vue b/Yi.Ai.Vue3/src/pages/console/index.vue index a78a7108..21f3fb33 100644 --- a/Yi.Ai.Vue3/src/pages/console/index.vue +++ b/Yi.Ai.Vue3/src/pages/console/index.vue @@ -37,10 +37,10 @@ const baseNavItems = [ ]; // 根据权限动态添加菜单项 -let navItems = [...baseNavItems]; +const navItems = [...baseNavItems]; if (hasChannelPermission) { - navItems.push({ name: 'channel', label: '渠道商管理', icon: 'Setting', path: '/console/channel' }); + navItems.push({ name: 'channel', label: '号池管理', icon: 'Setting', path: '/console/channel' }); } if (hasSystemStatisticsPermission) { diff --git a/Yi.Ai.Vue3/src/routers/modules/staticRouter.ts b/Yi.Ai.Vue3/src/routers/modules/staticRouter.ts index 777991f8..d6bfdbde 100644 --- a/Yi.Ai.Vue3/src/routers/modules/staticRouter.ts +++ b/Yi.Ai.Vue3/src/routers/modules/staticRouter.ts @@ -220,7 +220,7 @@ export const layoutRouter: RouteRecordRaw[] = [ name: 'consoleChannel', component: () => import('@/pages/console/channel/index.vue'), meta: { - title: '意心Ai-渠道商管理', + title: '意心Ai-号池管理', }, }, {