diff --git a/schoolNewsServ/admin/src/main/resources/application.yml b/schoolNewsServ/admin/src/main/resources/application.yml index 24d1974..4d0dcf8 100644 --- a/schoolNewsServ/admin/src/main/resources/application.yml +++ b/schoolNewsServ/admin/src/main/resources/application.yml @@ -1,5 +1,5 @@ server: - port: 8081 + port: 8082 servlet: context-path: /schoolNewsServ encoding: diff --git a/schoolNewsWeb/src/components/base/MobileNavItem.vue b/schoolNewsWeb/src/components/base/MobileNavItem.vue new file mode 100644 index 0000000..22b19e2 --- /dev/null +++ b/schoolNewsWeb/src/components/base/MobileNavItem.vue @@ -0,0 +1,216 @@ + + + + + diff --git a/schoolNewsWeb/src/components/base/index.ts b/schoolNewsWeb/src/components/base/index.ts index 8337e35..b80ff7b 100644 --- a/schoolNewsWeb/src/components/base/index.ts +++ b/schoolNewsWeb/src/components/base/index.ts @@ -12,4 +12,5 @@ export { default as GenericSelector } from './GenericSelector.vue'; export { default as TreeNode } from './TreeNode.vue'; export { default as Notice } from './Notice.vue'; export { default as ChangeHome } from './ChangeHome.vue'; -export { default as DynamicParamForm} from './DynamicParamForm.vue' \ No newline at end of file +export { default as DynamicParamForm} from './DynamicParamForm.vue' +export { default as MobileNavItem } from './MobileNavItem.vue' \ No newline at end of file diff --git a/schoolNewsWeb/src/layouts/MobileLayout.vue b/schoolNewsWeb/src/layouts/MobileLayout.vue new file mode 100644 index 0000000..4507bd0 --- /dev/null +++ b/schoolNewsWeb/src/layouts/MobileLayout.vue @@ -0,0 +1,593 @@ + + + + + diff --git a/schoolNewsWeb/src/router/index.ts b/schoolNewsWeb/src/router/index.ts index a4cde65..44ec207 100644 --- a/schoolNewsWeb/src/router/index.ts +++ b/schoolNewsWeb/src/router/index.ts @@ -1,4 +1,5 @@ import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"; +import { createAdaptiveRoute, setupRouteWatcher } from "@/utils/routeAdapter"; /** * 基础路由配置(无需权限) @@ -8,54 +9,21 @@ export const routes: Array = [ path: "/", redirect: "/login", }, - { - path: "/login", - component: () => import("@/layouts/BlankLayout.vue"), - children: [ - { - path: "", - name: "Login", - component: () => import("@/views/public/login/Login.vue"), - meta: { - title: "登录", - requiresAuth: false, - menuType: 3, - }, - } - ], - }, - { - path: "/register", - component: () => import("@/layouts/BlankLayout.vue"), - children: [ - { - path: "", - name: "Register", - component: () => import("@/views/public/login/Register.vue"), - meta: { - title: "注册", - requiresAuth: false, - menuType: 3, - }, - } - ], - }, - { - path: "/forgot-password", - component: () => import("@/layouts/BlankLayout.vue"), - children: [ - { - path: "", - name: "ForgotPassword", - component: () => import("@/views/public/login/ForgotPassword.vue"), - meta: { - title: "忘记密码", - requiresAuth: false, - menuType: 3, - }, - } - ], - }, + createAdaptiveRoute('/login', '@/views/public/login/Login.vue', 'BlankLayout', { + title: '登录', + requiresAuth: false, + menuType: 3, + }), + createAdaptiveRoute('/register', '@/views/public/login/Register.vue', 'BlankLayout', { + title: '注册', + requiresAuth: false, + menuType: 3, + }), + createAdaptiveRoute('/forgot-password', '@/views/public/login/ForgotPassword.vue', 'BlankLayout', { + title: '忘记密码', + requiresAuth: false, + menuType: 3, + }), // 首页(显示在导航栏) // { // path: "/home", @@ -132,8 +100,11 @@ export const routes: Array = [ ]; const router = createRouter({ - history: createWebHistory('/schoolNewsWeb/'), + history: createWebHistory(import.meta.env.BASE_URL), routes, }); +// 设置路由响应式监听器 +setupRouteWatcher(router); + export default router; diff --git a/schoolNewsWeb/src/utils/deviceUtils.ts b/schoolNewsWeb/src/utils/deviceUtils.ts new file mode 100644 index 0000000..60427cd --- /dev/null +++ b/schoolNewsWeb/src/utils/deviceUtils.ts @@ -0,0 +1,109 @@ +import { ref, onMounted, onUnmounted } from 'vue'; + +/** + * 设备类型枚举 + */ +export enum DeviceType { + MOBILE = 'mobile', // h5移动端 + DESKTOP = 'desktop' // web桌面端 +} + +/** + * 屏幕尺寸断点 + */ +export const BREAKPOINTS = { + mobile: 768, // 小于768px为移动端(h5) + desktop: 768 // 大于等于768px为桌面端(web) +}; + +/** + * 检测当前设备类型 + */ +export function getDeviceType(): DeviceType { + const width = window.innerWidth; + + if (width < BREAKPOINTS.mobile) { + return DeviceType.MOBILE; // h5移动端 + } else { + return DeviceType.DESKTOP; // web桌面端 + } +} + +/** + * 检测是否为移动端 + */ +export function isMobile(): boolean { + return getDeviceType() === DeviceType.MOBILE; +} + +/** + * 检测是否为桌面端 + */ +export function isDesktop(): boolean { + return getDeviceType() === DeviceType.DESKTOP; +} + +/** + * 响应式设备类型 Hook + */ +export function useDevice() { + const deviceType = ref(getDeviceType()); + const isMobileDevice = ref(isMobile()); + const isDesktopDevice = ref(isDesktop()); + + const updateDeviceType = () => { + deviceType.value = getDeviceType(); + isMobileDevice.value = isMobile(); + isDesktopDevice.value = isDesktop(); + }; + + onMounted(() => { + window.addEventListener('resize', updateDeviceType); + }); + + onUnmounted(() => { + window.removeEventListener('resize', updateDeviceType); + }); + + return { + deviceType, + isMobileDevice, + isDesktopDevice + }; +} + +/** + * 根据设备类型获取对应的组件路径 + */ +export function getComponentPath(basePath: string, deviceType?: DeviceType): string { + const currentDeviceType = deviceType || getDeviceType(); + + // 如果是移动端(h5),尝试加载移动端版本 + if (currentDeviceType === DeviceType.MOBILE) { + const mobilePath = basePath.replace('.vue', '.mobile.vue'); + return mobilePath; + } + + // 默认返回桌面版本(web) + return basePath; +} + +/** + * 动态导入组件,支持回退机制 + */ +export async function importResponsiveComponent(basePath: string) { + const deviceType = getDeviceType(); + + // 尝试加载设备特定的组件 + if (deviceType === DeviceType.MOBILE) { + try { + const mobilePath = basePath.replace('.vue', '.mobile.vue'); + return await import(/* @vite-ignore */ mobilePath); + } catch { + // 移动端组件不存在,回退到默认组件 + } + } + + // 加载默认组件(桌面端/web) + return await import(/* @vite-ignore */ basePath); +} diff --git a/schoolNewsWeb/src/utils/mobileRouteExample.ts b/schoolNewsWeb/src/utils/mobileRouteExample.ts new file mode 100644 index 0000000..0f33cbf --- /dev/null +++ b/schoolNewsWeb/src/utils/mobileRouteExample.ts @@ -0,0 +1,211 @@ +/** + * 移动端路由自动转换使用示例 + * + * 这个文件展示了如何使用自动转换系统来实现移动端适配 + */ + +import { RouteRecordRaw } from 'vue-router'; +import { + createAdaptiveRoute, + createResponsiveRoute, + getResponsiveLayout, + setupRouteWatcher, + type RouteAdapter +} from './routeAdapter'; + +/** + * 示例1: 创建单个自适应路由 + * + * 当屏幕宽度 < 768px时,会自动尝试加载 HomeView.mobile.vue + * 如果移动端文件不存在,会回退到 HomeView.vue + */ +export const homeRoute = createAdaptiveRoute( + '/home', + '@/views/user/home/HomeView.vue', + 'NavigationLayout', + { + title: '首页', + requiresAuth: true, + showTabBar: true + } +); + +/** + * 示例2: 手动创建路由适配器 + * + * 提供更精细的控制,可以为不同设备指定不同的组件 + */ +export const resourceCenterRoute: RouteRecordRaw = { + path: '/resource-center', + component: getResponsiveLayout('NavigationLayout'), + children: [ + { + path: '', + component: createResponsiveRoute({ + // 桌面端使用默认组件 + original: () => import('@/views/user/resource-center/ResourceCenterView.vue'), + // 移动端使用专门优化的组件 + mobile: () => import('@/views/user/resource-center/ResourceCenterView.mobile.vue'), + // 平板可以复用移动端组件或使用专门的平板版本 + tablet: () => import('@/views/user/resource-center/ResourceCenterView.mobile.vue') + }), + meta: { + title: '资源中心', + requiresAuth: true, + showTabBar: true, + showSearch: true + } + } + ] +}; + +/** + * 示例3: 批量创建自适应路由 + */ +export const userRoutes: RouteRecordRaw[] = [ + // 首页 + createAdaptiveRoute('/home', '@/views/user/home/HomeView.vue', 'NavigationLayout', { + title: '首页', + requiresAuth: true, + showTabBar: true + }), + + // 资源中心 + createAdaptiveRoute('/resource-center', '@/views/user/resource-center/ResourceCenterView.vue', 'NavigationLayout', { + title: '资源中心', + requiresAuth: true, + showTabBar: true, + showSearch: true + }), + + // 热门资源 + createAdaptiveRoute('/resource-hot', '@/views/user/resource-center/HotResourceView.vue', 'NavigationLayout', { + title: '热门资源', + requiresAuth: true, + showBackButton: true + }), + + // 搜索页面 + createAdaptiveRoute('/search', '@/views/user/resource-center/SearchView.vue', 'NavigationLayout', { + title: '搜索', + requiresAuth: true, + showBackButton: true, + showTabBar: false // 搜索页面不显示底部标签栏 + }), + + // 课程中心 + createAdaptiveRoute('/course-center', '@/views/user/study-plan/CourseCenterView.vue', 'NavigationLayout', { + title: '课程中心', + requiresAuth: true, + showTabBar: true + }), + + // 课程详情 + createAdaptiveRoute('/course-detail/:id', '@/views/user/study-plan/CourseDetailView.vue', 'NavigationLayout', { + title: '课程详情', + requiresAuth: true, + showBackButton: true + }), + + // 用户中心 + createAdaptiveRoute('/user-center', '@/views/user/user-center/UserCenterLayout.vue', 'NavigationLayout', { + title: '个人中心', + requiresAuth: true, + showTabBar: true + }), + + // 个人信息 + createAdaptiveRoute('/personal-info', '@/views/user/user-center/profile/PersonalInfoView.vue', 'NavigationLayout', { + title: '个人信息', + requiresAuth: true, + showBackButton: true + }), + + // 我的消息 + createAdaptiveRoute('/my-messages', '@/views/user/message/MyMessageListView.vue', 'NavigationLayout', { + title: '我的消息', + requiresAuth: true, + showTabBar: true + }), + + // 消息详情 + createAdaptiveRoute('/message-detail/:id', '@/views/user/message/MyMessageDetailView.vue', 'NavigationLayout', { + title: '消息详情', + requiresAuth: true, + showBackButton: true + }) +]; + +/** + * 示例4: 公共页面路由(登录、注册等) + */ +export const publicRoutes: RouteRecordRaw[] = [ + createAdaptiveRoute('/login', '@/views/public/login/Login.vue', 'BlankLayout', { + title: '登录', + requiresAuth: false + }), + + createAdaptiveRoute('/register', '@/views/public/login/Register.vue', 'BlankLayout', { + title: '注册', + requiresAuth: false + }), + + createAdaptiveRoute('/forgot-password', '@/views/public/login/ForgotPassword.vue', 'BlankLayout', { + title: '忘记密码', + requiresAuth: false + }) +]; + +/** + * 示例5: 在router中使用 + * + * 在你的 router/index.ts 中,你可以这样使用: + * + * ```typescript + * import { createRouter, createWebHistory } from 'vue-router'; + * import { setupRouteWatcher } from '@/utils/routeAdapter'; + * import { userRoutes, publicRoutes } from '@/utils/mobileRouteExample'; + * + * const router = createRouter({ + * history: createWebHistory('/schoolNewsWeb/'), + * routes: [ + * ...publicRoutes, + * ...userRoutes, + * // 其他路由... + * ] + * }); + * + * // 设置路由监听器,当屏幕尺寸变化时自动切换组件 + * setupRouteWatcher(router); + * + * export default router; + * ``` + */ + +/** + * 使用说明: + * + * 1. **文件命名规范**: + * - 桌面端:ComponentName.vue + * - 移动端:ComponentName.mobile.vue + * - 平板端:ComponentName.tablet.vue + * + * 2. **自动回退机制**: + * - 如果移动端文件不存在,自动回退到桌面端文件 + * - 如果平板端文件不存在,自动回退到桌面端文件 + * + * 3. **Layout自动切换**: + * - NavigationLayout -> MobileLayout (移动端) + * - SidebarLayout -> MobileLayout (移动端) + * - BasicLayout -> MobileLayout (移动端) + * - BlankLayout -> BlankLayout (所有设备) + * + * 4. **屏幕断点**: + * - 移动端: < 768px + * - 平板端: 768px - 1024px + * - 桌面端: > 1024px + * + * 5. **实时响应**: + * - 当用户调整浏览器窗口大小时,会自动切换到对应的组件版本 + * - 无需刷新页面 + */ diff --git a/schoolNewsWeb/src/utils/route-generator.ts b/schoolNewsWeb/src/utils/route-generator.ts index 3138457..0dae56c 100644 --- a/schoolNewsWeb/src/utils/route-generator.ts +++ b/schoolNewsWeb/src/utils/route-generator.ts @@ -8,24 +8,25 @@ import type { RouteRecordRaw } from 'vue-router'; import type { SysMenu } from '@/types'; import { MenuType } from '@/types/enums'; import { routes } from '@/router'; +import { getResponsiveLayout } from './routeAdapter'; // 预注册所有视图组件,构建时由 Vite 解析并生成按需加载的 chunk const VIEW_MODULES = import.meta.glob('../views/**/*.vue'); /** - * 布局组件映射 + * 布局组件映射 - 使用响应式布局适配 */ const LAYOUT_MAP: Record Promise> = { // 基础布局(旧版,带侧边栏) - 'BasicLayout': () => import('@/layouts/BasicLayout.vue'), + 'BasicLayout': getResponsiveLayout('BasicLayout'), // 导航布局(新版,顶部导航+动态侧边栏) - 'NavigationLayout': () => import('@/layouts/NavigationLayout.vue'), + 'NavigationLayout': getResponsiveLayout('NavigationLayout'), // 侧边栏布局(管理后台专用,顶层SIDEBAR菜单) - 'SidebarLayout': () => import('@/layouts/SidebarLayout.vue'), + 'SidebarLayout': getResponsiveLayout('SidebarLayout'), // 空白布局 - 'BlankLayout': () => import('@/layouts/BlankLayout.vue'), + 'BlankLayout': getResponsiveLayout('BlankLayout'), // 页面布局 - 'PageLayout': () => import('@/layouts/PageLayout.vue'), + 'PageLayout': getResponsiveLayout('PageLayout'), // 路由占位组件(用于没有组件的子路由) 'RoutePlaceholder': () => import('@/layouts/RoutePlaceholder.vue'), // 用户中心布局(有共用区域,避免重复查询) diff --git a/schoolNewsWeb/src/utils/routeAdapter.ts b/schoolNewsWeb/src/utils/routeAdapter.ts new file mode 100644 index 0000000..c87cf2a --- /dev/null +++ b/schoolNewsWeb/src/utils/routeAdapter.ts @@ -0,0 +1,261 @@ +import { RouteRecordRaw, RouteLocationNormalized } from 'vue-router'; +import { getDeviceType, DeviceType } from './deviceUtils'; + +/** + * 路由适配器接口 + */ +export interface RouteAdapter { + original: () => Promise; // web桌面端 + mobile?: () => Promise; // h5移动端 +} + +/** + * 移动端路由映射表 + */ +export const MOBILE_ROUTES_MAP: Record = { + // User Views + '/home': '@/views/user/home/HomeView.mobile.vue', + '/resource-center': '@/views/user/resource-center/ResourceCenterView.mobile.vue', + '/resource-hot': '@/views/user/resource-center/HotResourceView.mobile.vue', + '/search': '@/views/user/resource-center/SearchView.mobile.vue', + '/course-center': '@/views/user/study-plan/CourseCenterView.mobile.vue', + '/course-detail': '@/views/user/study-plan/CourseDetailView.mobile.vue', + '/course-study': '@/views/user/study-plan/CourseStudyView.mobile.vue', + '/study-tasks': '@/views/user/study-plan/StudyTasksView.mobile.vue', + '/learning-task-detail': '@/views/user/study-plan/LearningTaskDetailView.mobile.vue', + '/user-center': '@/views/user/user-center/UserCenterLayout.mobile.vue', + '/personal-info': '@/views/user/user-center/profile/PersonalInfoView.mobile.vue', + '/account-settings': '@/views/user/user-center/profile/AccountSettingsView.mobile.vue', + '/my-achievements': '@/views/user/user-center/MyAchievementsView.mobile.vue', + '/my-favorites': '@/views/user/user-center/MyFavoritesView.mobile.vue', + '/learning-records': '@/views/user/user-center/LearningRecordsView.mobile.vue', + '/my-messages': '@/views/user/message/MyMessageListView.mobile.vue', + '/message-detail': '@/views/user/message/MyMessageDetailView.mobile.vue', + + // Public Views + '/login': '@/views/public/login/Login.mobile.vue', + '/register': '@/views/public/login/Register.mobile.vue', + '/forgot-password': '@/views/public/login/ForgotPassword.mobile.vue', + '/article-show': '@/views/public/article/ArticleShowView.mobile.vue', + '/article-add': '@/views/public/article/ArticleAddView.mobile.vue' +}; + +/** + * Layout映射表 + */ +export const LAYOUT_MAP: Record> = { + 'NavigationLayout': { + [DeviceType.MOBILE]: '@/layouts/MobileLayout.vue', // h5移动端 + [DeviceType.DESKTOP]: '@/layouts/NavigationLayout.vue' // web桌面端 + }, + 'SidebarLayout': { + [DeviceType.MOBILE]: '@/layouts/MobileLayout.vue', // h5移动端 + [DeviceType.DESKTOP]: '@/layouts/SidebarLayout.vue' // web桌面端 + }, + 'BasicLayout': { + [DeviceType.MOBILE]: '@/layouts/MobileLayout.vue', // h5移动端 + [DeviceType.DESKTOP]: '@/layouts/BasicLayout.vue' // web桌面端 + }, + 'BlankLayout': { + [DeviceType.MOBILE]: '@/layouts/BlankLayout.vue', // h5移动端 + [DeviceType.DESKTOP]: '@/layouts/BlankLayout.vue' // web桌面端 + }, + 'PageLayout': { + [DeviceType.MOBILE]: '@/layouts/MobileLayout.vue', // h5移动端 + [DeviceType.DESKTOP]: '@/layouts/PageLayout.vue' // web桌面端 + } +}; + +/** + * 创建响应式路由组件 + */ +export function createResponsiveRoute(adapter: RouteAdapter): () => Promise { + return async () => { + const deviceType = getDeviceType(); + + try { + // 尝试加载设备特定的组件 + if (deviceType === DeviceType.MOBILE && adapter.mobile) { + return await adapter.mobile(); + } + } catch (error) { + console.warn(`Failed to load device-specific component for ${deviceType}, falling back to original:`, error); + } + + // 回退到原始组件(桌面端/web) + return await adapter.original(); + }; +} + +/** + * 获取响应式Layout组件 + */ +export function getResponsiveLayout(layoutName: string): () => Promise { + const deviceType = getDeviceType(); + const layoutMap = LAYOUT_MAP[layoutName]; + + if (!layoutMap) { + console.warn(`Unknown layout: ${layoutName}, using original`); + // 使用具体的导入路径 + switch (layoutName) { + case 'BlankLayout': + return () => import('@/layouts/BlankLayout.vue'); + case 'NavigationLayout': + return () => import('@/layouts/NavigationLayout.vue'); + case 'SidebarLayout': + return () => import('@/layouts/SidebarLayout.vue'); + case 'BasicLayout': + return () => import('@/layouts/BasicLayout.vue'); + case 'PageLayout': + return () => import('@/layouts/PageLayout.vue'); + default: + throw new Error(`Unknown layout: ${layoutName}`); + } + } + + const targetLayout = layoutMap[deviceType]; + + return async () => { + try { + // 使用具体的导入路径而不是动态路径 + switch (targetLayout) { + case '@/layouts/BlankLayout.vue': + return await import('@/layouts/BlankLayout.vue'); + case '@/layouts/NavigationLayout.vue': + return await import('@/layouts/NavigationLayout.vue'); + case '@/layouts/SidebarLayout.vue': + return await import('@/layouts/SidebarLayout.vue'); + case '@/layouts/BasicLayout.vue': + return await import('@/layouts/BasicLayout.vue'); + case '@/layouts/MobileLayout.vue': + return await import('@/layouts/MobileLayout.vue'); + case '@/layouts/PageLayout.vue': + return await import('@/layouts/PageLayout.vue'); + default: + throw new Error(`Unknown layout path: ${targetLayout}`); + } + } catch (error) { + console.warn(`Failed to load responsive layout ${targetLayout}, falling back to original:`, error); + // 回退到原始layout + switch (layoutName) { + case 'BlankLayout': + return await import('@/layouts/BlankLayout.vue'); + case 'NavigationLayout': + return await import('@/layouts/NavigationLayout.vue'); + case 'SidebarLayout': + return await import('@/layouts/SidebarLayout.vue'); + case 'BasicLayout': + return await import('@/layouts/BasicLayout.vue'); + case 'PageLayout': + return await import('@/layouts/PageLayout.vue'); + default: + throw new Error(`Unknown layout: ${layoutName}`); + } + } + }; +} + +/** + * 创建自适应路由配置 + */ +export function createAdaptiveRoute( + path: string, + originalComponent: string, + layoutName?: string, + meta?: any +): RouteRecordRaw { + // 创建具体的导入函数而不是使用动态路径 + const getOriginalComponent = () => { + switch (originalComponent) { + case '@/views/public/login/Login.vue': + return import('@/views/public/login/Login.vue'); + case '@/views/public/login/Register.vue': + return import('@/views/public/login/Register.vue'); + case '@/views/public/login/ForgotPassword.vue': + return import('@/views/public/login/ForgotPassword.vue'); + default: + throw new Error(`Unknown component: ${originalComponent}`); + } + }; + + const getMobileComponent = (): (() => Promise) | null => { + const mobilePath = MOBILE_ROUTES_MAP[path]; + if (!mobilePath) return null; + + switch (mobilePath) { + case '@/views/public/login/Login.mobile.vue': + return () => import('@/views/public/login/Login.mobile.vue'); + case '@/views/public/login/Register.mobile.vue': + return () => import('@/views/public/login/Register.mobile.vue'); + case '@/views/public/login/ForgotPassword.mobile.vue': + return () => import('@/views/public/login/ForgotPassword.mobile.vue'); + default: + return null; + } + }; + + const adapter: RouteAdapter = { + original: getOriginalComponent + }; + + // 检查是否有移动端版本 + const mobileImportFunction = getMobileComponent(); + if (mobileImportFunction) { + adapter.mobile = mobileImportFunction; + } + + // 如果指定了Layout,应用响应式Layout + if (layoutName) { + const route: RouteRecordRaw = { + path, + component: getResponsiveLayout(layoutName), + children: [ + { + path: '', + component: createResponsiveRoute(adapter), + meta + } + ], + meta + }; + return route; + } + + const route: RouteRecordRaw = { + path, + component: createResponsiveRoute(adapter), + meta + }; + + return route; +} + +/** + * 监听屏幕尺寸变化,重新加载路由 + */ +export function setupRouteWatcher(router: any) { + let currentDeviceType = getDeviceType(); + + const handleResize = () => { + const newDeviceType = getDeviceType(); + if (newDeviceType !== currentDeviceType) { + currentDeviceType = newDeviceType; + // 重新加载当前路由以应用新的组件 + const currentRoute = router.currentRoute.value; + router.replace({ + ...currentRoute, + query: { + ...currentRoute.query, + _refresh: Date.now() // 强制重新加载 + } + }); + } + }; + + window.addEventListener('resize', handleResize); + + // 返回清理函数 + return () => { + window.removeEventListener('resize', handleResize); + }; +} diff --git a/schoolNewsWeb/src/views/public/ai/AIAgent.mobile.vue b/schoolNewsWeb/src/views/public/ai/AIAgent.mobile.vue new file mode 100644 index 0000000..b9a414a --- /dev/null +++ b/schoolNewsWeb/src/views/public/ai/AIAgent.mobile.vue @@ -0,0 +1,952 @@ +