From 42edd4c2305f8ae98329ea9cbd36bc2d0d3d2d8a Mon Sep 17 00:00:00 2001 From: Gsh <15170702455@163.com> Date: Sat, 3 Jan 2026 17:03:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B7=AF=E7=94=B1=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E6=9D=83=E9=99=90=E6=8E=A7=E5=88=B6=E3=80=81=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E5=B9=BF=E5=9C=BA=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/CardFlipActivity.vue | 230 +++++++++++++++++- Yi.Ai.Vue3/src/config/index.ts | 2 +- Yi.Ai.Vue3/src/config/permission.ts | 110 +++++++++ .../chat/image/components/ImageGenerator.vue | 145 ++++++----- .../chat/image/components/ImagePlaza.vue | 36 ++- .../pages/chat/image/components/MyImages.vue | 140 +++++++++-- Yi.Ai.Vue3/src/pages/chat/image/index.vue | 76 +++--- Yi.Ai.Vue3/src/pages/console/index.vue | 17 +- Yi.Ai.Vue3/src/pages/payResult/index.vue | 3 + Yi.Ai.Vue3/src/routers/index.ts | 12 + Yi.Ai.Vue3/src/styles/var.scss | 34 +-- 11 files changed, 660 insertions(+), 145 deletions(-) create mode 100644 Yi.Ai.Vue3/src/config/permission.ts diff --git a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/CardFlipActivity.vue b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/CardFlipActivity.vue index 666f5900..14e47109 100644 --- a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/CardFlipActivity.vue +++ b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/CardFlipActivity.vue @@ -408,7 +408,10 @@ async function handleFlipCard(record: CardFlipRecord) { await new Promise(resolve => requestAnimationFrame(resolve)); // 4. 移动克隆卡片到屏幕中心并放大(考虑边界限制) - const scale = Math.min(1.8, window.innerWidth / rect.width * 0.6); // 动态计算缩放比例 + // 移动端使用更小的缩放比例 + const isMobile = window.innerWidth <= 768; + const maxScale = isMobile ? 1.5 : 1.8; + const scale = Math.min(maxScale, window.innerWidth / rect.width * (isMobile ? 0.5 : 0.6)); const scaledWidth = rect.width * scale; const scaledHeight = rect.height * scale; @@ -416,8 +419,8 @@ async function handleFlipCard(record: CardFlipRecord) { let centerX = window.innerWidth / 2; let centerY = window.innerHeight / 2; - // 边界检查:确保卡片完全在视口内(留20px边距) - const margin = 20; + // 边界检查:确保卡片完全在视口内(移动端留更多边距) + const margin = isMobile ? 30 : 20; const minX = scaledWidth / 2 + margin; const maxX = window.innerWidth - scaledWidth / 2 - margin; const minY = scaledHeight / 2 + margin; @@ -1253,6 +1256,11 @@ function getCardClass(record: CardFlipRecord): string[] { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 16px; + @media (max-width: 768px) { + padding: 12px; + border-radius: 12px; + } + /* 自定义滚动条 */ &::-webkit-scrollbar { width: 6px; @@ -1277,15 +1285,36 @@ function getCardClass(record: CardFlipRecord): string[] { .lucky-float-ball { position: fixed; left: 50%; - /* left: 20px; */ - /* top: 20px; */ + transform: translateX(-50%); z-index: 999; bottom: 20px; - transition: all 0.3s - cubic-bezier(0.4, 0, 0.2, 1); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; + &:hover { - transform: scale(1.1); + transform: translateX(-50%) scale(1.1); + } + + @media (max-width: 768px) { + bottom: 15px; + + .lucky-circle { + width: 70px; + height: 70px; + } + + .lucky-content .lucky-icon { + font-size: 20px; + } + + .lucky-content .lucky-text { + font-size: 12px; + } + + .lucky-label { + font-size: 11px; + margin-top: 4px; + } } &.lucky-full { @@ -1408,6 +1437,12 @@ function getCardClass(record: CardFlipRecord): string[] { animation: slideIn 0.5s ease; flex-wrap: wrap; + @media (max-width: 768px) { + padding: 8px 10px; + gap: 8px; + margin-bottom: 10px; + } + .compact-stats { display: flex; align-items: center; @@ -1497,6 +1532,11 @@ function getCardClass(record: CardFlipRecord): string[] { border-radius: 12px; backdrop-filter: blur(10px); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + + @media (max-width: 768px) { + padding: 12px 20px; + border-radius: 8px; + } } .shuffle-text { @@ -1505,6 +1545,15 @@ function getCardClass(record: CardFlipRecord): string[] { color: #fff; text-shadow: 0 2px 8px rgba(255, 215, 0, 0.6); animation: textPulse 1.5s ease-in-out infinite; + white-space: nowrap; + + @media (max-width: 768px) { + font-size: 14px; + } + + @media (max-width: 480px) { + font-size: 12px; + } } } @@ -1515,8 +1564,13 @@ function getCardClass(record: CardFlipRecord): string[] { max-width: 100%; @media (max-width: 768px) { - grid-template-columns: repeat(5, 1fr); - gap: 6px; + grid-template-columns: repeat(3, 1fr); + gap: 10px; + } + + @media (max-width: 480px) { + grid-template-columns: repeat(2, 1fr); + gap: 12px; } // 洗牌阶段样式 @@ -1683,6 +1737,11 @@ function getCardClass(record: CardFlipRecord): string[] { background: rgba(0, 0, 0, 0.2); padding: 2px 6px; border-radius: 4px; + + @media (max-width: 768px) { + font-size: 9px; + padding: 2px 5px; + } } .card-content { @@ -1691,6 +1750,10 @@ function getCardClass(record: CardFlipRecord): string[] { align-items: center; gap: 8px; z-index: 1; + + @media (max-width: 768px) { + gap: 6px; + } } // 系统logo样式(优化为居中圆形,更丰富的效果) @@ -1709,6 +1772,12 @@ function getCardClass(record: CardFlipRecord): string[] { z-index: 3; filter: brightness(1.1); + @media (max-width: 768px) { + width: 40px; + height: 40px; + border: 3px solid rgba(255, 255, 255, 1); + } + // 外层光晕效果 &::before { content: ''; @@ -1741,6 +1810,10 @@ function getCardClass(record: CardFlipRecord): string[] { font-weight: bold; color: #fff; text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + + @media (max-width: 768px) { + font-size: 24px; + } } .card-type-badge { @@ -1753,6 +1826,12 @@ function getCardClass(record: CardFlipRecord): string[] { border-radius: 10px; backdrop-filter: blur(4px); z-index: 2; + + @media (max-width: 768px) { + font-size: 9px; + padding: 2px 6px; + bottom: 4px; + } } .card-shine { @@ -1827,6 +1906,10 @@ function getCardClass(record: CardFlipRecord): string[] { position: relative; z-index: 1; + @media (max-width: 768px) { + padding: 8px; + } + // logo水印样式 .result-watermark { position: absolute; @@ -1869,6 +1952,10 @@ function getCardClass(record: CardFlipRecord): string[] { z-index: 1; filter: drop-shadow(0 4px 8px rgba(255, 215, 0, 0.5)); margin-bottom: 4px; + + @media (max-width: 768px) { + font-size: 36px; + } } .result-text { @@ -1883,6 +1970,11 @@ function getCardClass(record: CardFlipRecord): string[] { letter-spacing: 2px; position: relative; + @media (max-width: 768px) { + font-size: 16px; + letter-spacing: 1px; + } + // 文字外发光 &::after { content: attr(data-text); @@ -1907,6 +1999,11 @@ function getCardClass(record: CardFlipRecord): string[] { position: relative; filter: drop-shadow(0 3px 10px rgba(255, 215, 0, 0.6)); + @media (max-width: 768px) { + font-size: 32px; + margin: 8px 0; + } + // 金色光效边框 &::before { content: attr(data-amount); @@ -1932,6 +2029,11 @@ function getCardClass(record: CardFlipRecord): string[] { letter-spacing: 3px; text-transform: uppercase; margin-top: 4px; + + @media (max-width: 768px) { + font-size: 14px; + letter-spacing: 2px; + } } } @@ -1953,6 +2055,10 @@ function getCardClass(record: CardFlipRecord): string[] { margin-bottom: 6px; filter: drop-shadow(0 2px 6px rgba(147, 112, 219, 0.3)); animation: gentleBounce 2s ease-in-out infinite; + + @media (max-width: 768px) { + font-size: 36px; + } } .result-text { @@ -1964,6 +2070,10 @@ function getCardClass(record: CardFlipRecord): string[] { margin: 8px 0; z-index: 1; letter-spacing: 1px; + + @media (max-width: 768px) { + font-size: 15px; + } } .result-tip { @@ -1972,6 +2082,10 @@ function getCardClass(record: CardFlipRecord): string[] { z-index: 1; margin-top: 6px; font-weight: 500; + + @media (max-width: 768px) { + font-size: 12px; + } } } @@ -1994,6 +2108,11 @@ function getCardClass(record: CardFlipRecord): string[] { z-index: 1; filter: drop-shadow(0 4px 12px rgba(255, 215, 0, 0.8)); margin: 10px 0; + + @media (max-width: 768px) { + font-size: 42px; + margin: 8px 0; + } } .mystery-text { @@ -2004,6 +2123,11 @@ function getCardClass(record: CardFlipRecord): string[] { z-index: 1; letter-spacing: 4px; margin: 8px 0; + + @media (max-width: 768px) { + font-size: 18px; + letter-spacing: 2px; + } } .mystery-hint { @@ -2012,6 +2136,11 @@ function getCardClass(record: CardFlipRecord): string[] { z-index: 1; letter-spacing: 2px; margin-top: 6px; + + @media (max-width: 768px) { + font-size: 12px; + letter-spacing: 1px; + } } .mystery-stars { @@ -2060,6 +2189,11 @@ function getCardClass(record: CardFlipRecord): string[] { margin-bottom: 16px; text-align: center; line-height: 1.6; + + @media (max-width: 768px) { + font-size: 13px; + margin-bottom: 12px; + } } .code-input { @@ -2078,10 +2212,19 @@ function getCardClass(record: CardFlipRecord): string[] { border: 1px solid #bae6fd; border-radius: 12px; + @media (max-width: 768px) { + padding: 20px 12px; + } + .filled-icon { font-size: 48px; display: block; margin-bottom: 12px; + + @media (max-width: 768px) { + font-size: 40px; + margin-bottom: 10px; + } } .filled-text { @@ -2090,6 +2233,10 @@ function getCardClass(record: CardFlipRecord): string[] { line-height: 1.6; margin: 0; font-weight: 500; + + @media (max-width: 768px) { + font-size: 14px; + } } } } @@ -2128,12 +2275,22 @@ function getCardClass(record: CardFlipRecord): string[] { border-radius: 12px; margin-bottom: 16px; + @media (max-width: 768px) { + padding: 14px; + margin-bottom: 12px; + } + .code-text { font-size: 28px; font-weight: bold; color: #fff; letter-spacing: 6px; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + + @media (max-width: 768px) { + font-size: 24px; + letter-spacing: 4px; + } } } @@ -2142,8 +2299,17 @@ function getCardClass(record: CardFlipRecord): string[] { gap: 8px; margin-bottom: 12px; + @media (max-width: 768px) { + flex-direction: column; + gap: 10px; + } + .el-button { flex: 1; + + @media (max-width: 768px) { + width: 100%; + } } } @@ -2153,6 +2319,10 @@ function getCardClass(record: CardFlipRecord): string[] { line-height: 1.5; margin: 0 0 12px 0; + @media (max-width: 768px) { + font-size: 12px; + } + strong { color: #f56c6c; font-weight: 600; @@ -2199,12 +2369,20 @@ function getCardClass(record: CardFlipRecord): string[] { padding: 12px; text-align: left; + @media (max-width: 768px) { + padding: 10px; + } + .share-preview-title { font-size: 14px; font-weight: bold; color: #303133; margin-bottom: 8px; text-align: center; + + @media (max-width: 768px) { + font-size: 13px; + } } .share-preview-content { @@ -2218,6 +2396,12 @@ function getCardClass(record: CardFlipRecord): string[] { max-height: 200px; overflow-y: auto; + @media (max-width: 768px) { + font-size: 12px; + padding: 8px; + max-height: 150px; + } + /* 自定义滚动条 */ &::-webkit-scrollbar { width: 4px; @@ -2276,9 +2460,17 @@ function getCardClass(record: CardFlipRecord): string[] { display: inline-block; margin-bottom: 20px; + @media (max-width: 768px) { + margin-bottom: 16px; + } + .double-icon { font-size: 64px; animation: bounce 1s infinite; + + @media (max-width: 768px) { + font-size: 52px; + } } .double-sparkle { @@ -2287,6 +2479,10 @@ function getCardClass(record: CardFlipRecord): string[] { right: -10px; font-size: 32px; animation: spin 2s linear infinite; + + @media (max-width: 768px) { + font-size: 26px; + } } } @@ -2295,6 +2491,11 @@ function getCardClass(record: CardFlipRecord): string[] { font-weight: bold; color: #303133; margin-bottom: 16px; + + @media (max-width: 768px) { + font-size: 18px; + margin-bottom: 12px; + } } .double-text { @@ -2303,10 +2504,19 @@ function getCardClass(record: CardFlipRecord): string[] { color: #606266; margin-bottom: 24px; + @media (max-width: 768px) { + font-size: 14px; + margin-bottom: 20px; + } + .highlight { color: #f56c6c; font-weight: bold; font-size: 16px; + + @media (max-width: 768px) { + font-size: 15px; + } } } diff --git a/Yi.Ai.Vue3/src/config/index.ts b/Yi.Ai.Vue3/src/config/index.ts index 5f054135..7d60d8fc 100644 --- a/Yi.Ai.Vue3/src/config/index.ts +++ b/Yi.Ai.Vue3/src/config/index.ts @@ -12,4 +12,4 @@ export const COLLAPSE_THRESHOLD: number = 600; export const SIDE_BAR_WIDTH: number = 280; // 路由白名单地址[本地存在的路由 staticRouter.ts 中] -export const ROUTER_WHITE_LIST: string[] = ['/chat', '/chat/not_login', '/products', '/model-library', '/403', '/404']; +export const ROUTER_WHITE_LIST: string[] = ['/chat', '/chat/conversation', '/chat/image', '/chat/video', '/model-library', '/403', '/404']; diff --git a/Yi.Ai.Vue3/src/config/permission.ts b/Yi.Ai.Vue3/src/config/permission.ts new file mode 100644 index 00000000..c1c3c64a --- /dev/null +++ b/Yi.Ai.Vue3/src/config/permission.ts @@ -0,0 +1,110 @@ +/** + * 权限配置文件 + * 用于配置特定页面的访问权限 + */ + +/** + * 权限配置接口 + */ +export interface PermissionConfig { + /** 路由路径 */ + path: string; + /** 允许访问的用户名列表 */ + allowedUsers: string[]; + /** 权限描述 */ + description?: string; +} + +/** + * 页面权限配置列表 + * 在这里配置需要特殊权限控制的页面 + */ +export const PAGE_PERMISSIONS: PermissionConfig[] = [ + { + path: '/console/channel', + allowedUsers: ['cc', 'Guo'], + description: '渠道商管理页面 - 仅限cc和Guo用户访问', + }, + // 可以在这里继续添加其他需要权限控制的页面 + // { + // path: '/console/admin', + // allowedUsers: ['admin', 'superadmin'], + // description: '管理员页面', + // }, +]; + +/** + * 检查用户是否有权限访问指定路径 + * @param path 路由路径 + * @param userName 用户名 + * @returns 是否有权限 + */ +export function checkPagePermission(path: string, userName: string | undefined): boolean { + // 如果没有用户名,返回false + if (!userName) { + return false; + } + + // 查找该路径的权限配置 + const permissionConfig = PAGE_PERMISSIONS.find(config => config.path === path); + + // 如果没有配置权限,说明该页面不需要特殊权限,返回true + if (!permissionConfig) { + return true; + } + + // 检查用户名是否在允许列表中(不区分大小写) + return permissionConfig.allowedUsers.some( + allowedUser => allowedUser.toLowerCase() === userName.toLowerCase(), + ); +} + +/** + * 获取用户无权访问的路由列表 + * @param userName 用户名 + * @returns 无权访问的路由路径数组 + */ +export function getRestrictedRoutes(userName: string | undefined): string[] { + if (!userName) { + return PAGE_PERMISSIONS.map(config => config.path); + } + + return PAGE_PERMISSIONS.filter( + config => !config.allowedUsers.some( + allowedUser => allowedUser.toLowerCase() === userName.toLowerCase(), + ), + ).map(config => config.path); +} + +/** + * 检查路由是否需要权限控制 + * @param path 路由路径 + * @returns 是否需要权限控制 + */ +export function isRestrictedRoute(path: string): boolean { + return PAGE_PERMISSIONS.some(config => config.path === path); +} + +/** + * 过滤菜单路由,移除用户无权访问的菜单项 + * @param routes 路由配置数组 + * @param userName 用户名 + * @returns 过滤后的路由配置数组 + */ +export function filterMenuRoutes(routes: any[], userName: string | undefined): any[] { + return routes.filter((route) => { + // 检查当前路由是否有权限 + const hasPermission = checkPagePermission(route.path, userName); + + if (!hasPermission) { + return false; + } + + // 如果有子路由,递归过滤 + if (route.children && route.children.length > 0) { + route.children = filterMenuRoutes(route.children, userName); + } + + return true; + }); +} 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 3f50855d..2145c693 100644 --- a/Yi.Ai.Vue3/src/pages/chat/image/components/ImageGenerator.vue +++ b/Yi.Ai.Vue3/src/pages/chat/image/components/ImageGenerator.vue @@ -12,10 +12,17 @@ import { ZoomIn, } from '@element-plus/icons-vue'; import { ElMessage } from 'element-plus'; -import { computed, onMounted, onUnmounted, ref } from 'vue'; +import { computed, onMounted, onUnmounted, ref, watch } from 'vue'; import { getSelectableTokenInfo } from '@/api'; import { generateImage, getImageModels, getTaskStatus } from '@/api/aiImage'; +const props = defineProps({ + isActive: { + type: Boolean, + default: true, + }, +}); + const emit = defineEmits(['task-created']); // State @@ -41,6 +48,19 @@ const canGenerate = computed(() => { return selectedModelId.value && prompt.value && !generating.value; }); +// Watch isActive to manage polling +watch(() => props.isActive, (active) => { + if (active) { + // Resume polling if we have a processing task + if (currentTaskId.value && currentTask.value?.taskStatus === 'Processing') { + startPolling(currentTaskId.value); + } + } + else { + stopPolling(); + } +}); + // Methods async function fetchTokens() { tokenLoading.value = true; @@ -197,6 +217,12 @@ function startPolling(taskId: string) { } async function pollStatus(taskId: string) { + // Double check active status before polling (though timer should be cleared) + if (!props.isActive) { + stopPolling(); + return; + } + try { const res = await getTaskStatus(taskId); // Handle response structure if needed @@ -257,7 +283,8 @@ async function downloadImage() { link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); - } catch (e) { + } + catch (e) { console.error('Download failed', e); // Fallback window.open(currentTask.value.storeUrl, '_blank'); @@ -324,14 +351,15 @@ onUnmounted(() => { 配置 - -
- + + + { - + + { -
- -
-
- - - - - 清空 - -
- -
- - -
- -
- -
- - - - 点击上传 + + + + + + + + +
+ +
+ + + + 点击上传 +
+
+
+ 最多2张,< 5MB (支持 JPG/PNG/WEBP) +
- -
- 最多2张,< 5MB (支持 JPG/PNG/WEBP) -
-
-
+ +
@@ -450,7 +481,7 @@ onUnmounted(() => {
- +
{ :deep(.hide-upload-btn .el-upload--picture-card) { display: none; } +/* 隐藏默认的标签 */ +:deep(.prompt-form-item .el-form-item__label){ + display: flex; +} diff --git a/Yi.Ai.Vue3/src/pages/chat/image/components/ImagePlaza.vue b/Yi.Ai.Vue3/src/pages/chat/image/components/ImagePlaza.vue index 6d632e2d..da28d0bb 100644 --- a/Yi.Ai.Vue3/src/pages/chat/image/components/ImagePlaza.vue +++ b/Yi.Ai.Vue3/src/pages/chat/image/components/ImagePlaza.vue @@ -55,18 +55,22 @@ class="w-full h-full" :preview-src-list="[currentTask.storeUrl]" /> + +
+ +
-
-
-

+
+
+

提示词

-
+
{{ currentTask.prompt }}
-
+
创建时间 {{ formatTime(currentTask.creationTime) }} @@ -118,7 +122,7 @@ import TaskCard from './TaskCard.vue'; import { format } from 'date-fns'; import { ElMessage } from 'element-plus'; import { useClipboard } from '@vueuse/core'; -import { Picture, Loading, MagicStick, CopyDocument } from '@element-plus/icons-vue'; +import { Picture, Loading, MagicStick, CopyDocument, Download } from '@element-plus/icons-vue'; const emit = defineEmits(['use-prompt', 'use-reference']); @@ -197,6 +201,24 @@ const copyPrompt = async (text: string) => { await copy(text); ElMessage.success('提示词已复制'); }; + +const downloadImage = async (url: string) => { + try { + const response = await fetch(url); + const blob = await response.blob(); + const blobUrl = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = blobUrl; + link.download = `image-${Date.now()}.png`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(blobUrl); + } catch (e) { + console.error('Download failed', e); + window.open(url, '_blank'); + } +};