Просмотр исходного кода

✨ feat(knowledge): 添加知识详情页面功能

- 新增管理员端SilverKnowledgeDetailPage组件,支持知识详情查看、编辑和状态管理
- 新增移动端SilverWisdomDetailPage组件,实现知识详情展示与互动功能
- 添加相关路由配置,支持知识详情页面访问
- 优化管理员知识列表页查看按钮跳转逻辑,指向新详情页

✨ feat(ui): 设计移动端知识详情页UI

- 采用传统水墨风格设计语言,创建特色色彩系统
- 实现响应式布局,适配移动设备显示
- 添加阅读时间计算、日期格式化等辅助功能
- 实现点赞、收藏、分享和附件下载等互动功能
yourname 7 месяцев назад
Родитель
Сommit
ec9aff9171

+ 262 - 0
src/client/admin/pages/SilverKnowledgeDetailPage.tsx

@@ -0,0 +1,262 @@
+import React, { useState, useEffect } from 'react';
+import { Card, Descriptions, Button, Tag, Modal, Image, message, Space, Divider } from 'antd';
+import { ArrowLeftOutlined, EditOutlined, EyeOutlined, DownloadOutlined, ShareAltOutlined } from '@ant-design/icons';
+import { useParams, useNavigate } from 'react-router-dom';
+import type { InferResponseType } from 'hono/client';
+import { silverKnowledgeClient } from '@/client/api';
+
+type SilverKnowledge = InferResponseType<typeof silverKnowledgeClient[':id']['$get'], 200>;
+
+const SilverKnowledgeDetailPage: React.FC = () => {
+  const { id } = useParams<{ id: string }>();
+  const navigate = useNavigate();
+  const [knowledge, setKnowledge] = useState<SilverKnowledge | null>(null);
+  const [loading, setLoading] = useState(false);
+  const [pdfModalVisible, setPdfModalVisible] = useState(false);
+
+  useEffect(() => {
+    if (id) {
+      fetchKnowledge();
+    }
+  }, [id]);
+
+  const fetchKnowledge = async () => {
+    setLoading(true);
+    try {
+      const response = await silverKnowledgeClient[':id']['$get']({
+        param: { id: id! }
+      });
+      const result = await response.json();
+      setKnowledge(result);
+    } catch (error) {
+      message.error('获取知识详情失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleEdit = () => {
+    navigate(`/admin/silver-knowledges/${id}/edit`);
+  };
+
+  const handleBack = () => {
+    navigate('/admin/silver-knowledges');
+  };
+
+  const handleStatusChange = async (newStatus: number) => {
+    try {
+      await silverKnowledgeClient[':id']['$put']({
+        param: { id: id! },
+        json: { status: newStatus }
+      });
+      message.success('状态更新成功');
+      fetchKnowledge();
+    } catch (error) {
+      message.error('状态更新失败');
+    }
+  };
+
+  const formatDate = (date: string) => {
+    return new Date(date).toLocaleString('zh-CN');
+  };
+
+  const getReadTime = (content: string) => {
+    const wordsPerMinute = 200;
+    return Math.ceil(content.length / wordsPerMinute);
+  };
+
+  if (loading) {
+    return (
+      <div className="p-6">
+        <Card loading={true} className="shadow-sm">
+          <Card.Meta />
+        </Card>
+      </div>
+    );
+  }
+
+  if (!knowledge) {
+    return (
+      <div className="p-6">
+        <Card className="shadow-sm">
+          <div className="text-center">
+            <p>未找到知识详情</p>
+            <Button type="primary" onClick={handleBack} className="mt-4">
+              返回列表
+            </Button>
+          </div>
+        </Card>
+      </div>
+    );
+  }
+
+  const data = knowledge.data;
+
+  return (
+    <div className="p-6">
+      <Card 
+        title={
+          <div className="flex items-center justify-between">
+            <div className="flex items-center">
+              <Button 
+                icon={<ArrowLeftOutlined />} 
+                onClick={handleBack}
+                className="mr-4"
+              >
+                返回
+              </Button>
+              <span>知识详情</span>
+            </div>
+            <Space>
+              <Button 
+                type="primary" 
+                icon={<EditOutlined />}
+                onClick={handleEdit}
+              >
+                编辑
+              </Button>
+            </Space>
+          </div>
+        }
+        className="shadow-sm"
+      >
+        {/* 基本信息 */}
+        <Descriptions title="基本信息" column={2} bordered>
+          <Descriptions.Item label="ID">{data.id}</Descriptions.Item>
+          <Descriptions.Item label="状态">
+            <Tag color={data.status === 1 ? 'green' : 'orange'}>
+              {data.status === 1 ? '已发布' : '草稿'}
+            </Tag>
+          </Descriptions.Item>
+          <Descriptions.Item label="标题" span={2}>
+            {data.title}
+          </Descriptions.Item>
+          <Descriptions.Item label="作者">{data.author}</Descriptions.Item>
+          <Descriptions.Item label="分类">{data.category?.name || '-'}</Descriptions.Item>
+          <Descriptions.Item label="创建时间">{formatDate(data.createdAt)}</Descriptions.Item>
+          <Descriptions.Item label="更新时间">{formatDate(data.updatedAt)}</Descriptions.Item>
+          <Descriptions.Item label="浏览量">{data.viewCount}</Descriptions.Item>
+          <Descriptions.Item label="点赞数">{data.likeCount}</Descriptions.Item>
+          {data.tags && (
+            <Descriptions.Item label="标签" span={2}>
+              <Tag>{data.tags}</Tag>
+            </Descriptions.Item>
+          )}
+        </Descriptions>
+
+        <Divider />
+
+        {/* 封面图片 */}
+        {data.coverImage && (
+          <>
+            <h3 className="text-lg font-semibold mb-4">封面图片</h3>
+            <div className="mb-6">
+              <Image
+                width={200}
+                height={150}
+                src={data.coverImage}
+                alt="封面图片"
+                className="object-cover rounded"
+              />
+            </div>
+          </>
+        )}
+
+        {/* 内容 */}
+        <div className="mb-6">
+          <h3 className="text-lg font-semibold mb-4">知识内容</h3>
+          <div className="bg-gray-50 p-4 rounded-lg">
+            <div className="whitespace-pre-wrap">{data.content}</div>
+          </div>
+        </div>
+
+        {/* 附件 */}
+        {data.attachment && (
+          <>
+            <Divider />
+            <div className="mb-6">
+              <h3 className="text-lg font-semibold mb-4">附件资料</h3>
+              <Card 
+                size="small" 
+                className="max-w-md"
+                actions={[
+                  <Button 
+                    key="preview" 
+                    type="link" 
+                    icon={<EyeOutlined />}
+                    onClick={() => setPdfModalVisible(true)}
+                  >
+                    预览
+                  </Button>,
+                  <Button 
+                    key="download" 
+                    type="link" 
+                    icon={<DownloadOutlined />}
+                    onClick={() => window.open(data.attachment, '_blank')}
+                  >
+                    下载
+                  </Button>
+                ]}
+              >
+                <div className="flex items-center">
+                  <DownloadOutlined className="mr-2" />
+                  <span>{data.attachmentName || '附件资料'}</span>
+                </div>
+              </Card>
+            </div>
+          </>
+        )}
+
+        {/* PDF预览模态框 */}
+        {data.attachment && (
+          <Modal
+            title="PDF预览"
+            open={pdfModalVisible}
+            onCancel={() => setPdfModalVisible(false)}
+            width={800}
+            footer={null}
+          >
+            <iframe
+              src={data.attachment}
+              width="100%"
+              height="600"
+              title={data.attachmentName || 'PDF预览'}
+              className="rounded-lg"
+            />
+          </Modal>
+        )}
+
+        <Divider />
+
+        {/* 操作按钮 */}
+        <div className="flex justify-between items-center">
+          <Space>
+            <Button onClick={handleBack}>返回列表</Button>
+            {data.status === 0 && (
+              <Button 
+                type="primary" 
+                onClick={() => handleStatusChange(1)}
+              >
+                发布
+              </Button>
+            )}
+            {data.status === 1 && (
+              <Button 
+                onClick={() => handleStatusChange(0)}
+              >
+                设为草稿
+              </Button>
+            )}
+          </Space>
+          <Space>
+            <span className="text-sm text-gray-500">
+              预计阅读时间: {getReadTime(data.content)}分钟
+            </span>
+          </Space>
+        </div>
+      </Card>
+    </div>
+  );
+};
+
+export default SilverKnowledgeDetailPage;

+ 4 - 2
src/client/admin/pages/SilverKnowledges.tsx

@@ -1,4 +1,5 @@
 import React, { useState, useEffect } from 'react';
 import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
 import { Card, Table, Button, Modal, Form, Input, Select, message, Space, Tag, Popconfirm, Image } from 'antd';
 import { Card, Table, Button, Modal, Form, Input, Select, message, Space, Tag, Popconfirm, Image } from 'antd';
 import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, PaperClipOutlined } from '@ant-design/icons';
 import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, PaperClipOutlined } from '@ant-design/icons';
 import type { ColumnsType } from 'antd/es/table';
 import type { ColumnsType } from 'antd/es/table';
@@ -22,6 +23,7 @@ const SilverKnowledges: React.FC = () => {
   const [currentRecord, setCurrentRecord] = useState<SilverKnowledge | null>(null);
   const [currentRecord, setCurrentRecord] = useState<SilverKnowledge | null>(null);
   const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
   const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
   const [form] = Form.useForm();
   const [form] = Form.useForm();
+  const navigate = useNavigate();
   const [searchText, setSearchText] = useState('');
   const [searchText, setSearchText] = useState('');
   const [categories, setCategories] = useState<Array<{id: number, name: string}>>([]);
   const [categories, setCategories] = useState<Array<{id: number, name: string}>>([]);
   const [categoriesLoading, setCategoriesLoading] = useState(false);
   const [categoriesLoading, setCategoriesLoading] = useState(false);
@@ -236,9 +238,9 @@ const SilverKnowledges: React.FC = () => {
           <Button
           <Button
             type="link"
             type="link"
             icon={<EyeOutlined />}
             icon={<EyeOutlined />}
-            onClick={() => window.open(`/silver-wisdom/${record.id}`, '_blank')}
+            onClick={() => navigate(`/admin/silver-knowledges/${record.id}`)}
           >
           >
-            查看
+            详情
           </Button>
           </Button>
           <Button
           <Button
             type="link"
             type="link"

+ 6 - 0
src/client/admin/routes.tsx

@@ -13,6 +13,7 @@ import { SilverJobsPage } from './pages/SilverJobs';
 import CompanyCertificationPage from './pages/CompanyCertification';
 import CompanyCertificationPage from './pages/CompanyCertification';
 import HomeIconsPage from './pages/HomeIcons';
 import HomeIconsPage from './pages/HomeIcons';
 import SilverKnowledgesPage from './pages/SilverKnowledges';
 import SilverKnowledgesPage from './pages/SilverKnowledges';
+import SilverKnowledgeDetailPage from './pages/SilverKnowledgeDetailPage';
 import KnowledgeCategories from './pages/KnowledgeCategories';
 import KnowledgeCategories from './pages/KnowledgeCategories';
 
 
 export const router = createBrowserRouter([
 export const router = createBrowserRouter([
@@ -76,6 +77,11 @@ export const router = createBrowserRouter([
         element: <SilverKnowledgesPage />,
         element: <SilverKnowledgesPage />,
         errorElement: <ErrorPage />
         errorElement: <ErrorPage />
       },
       },
+      {
+        path: 'silver-knowledges/:id',
+        element: <SilverKnowledgeDetailPage />,
+        errorElement: <ErrorPage />
+      },
       {
       {
         path: 'knowledge-categories',
         path: 'knowledge-categories',
         element: <KnowledgeCategories />,
         element: <KnowledgeCategories />,

+ 309 - 0
src/client/mobile/pages/SilverWisdomDetailPage.tsx

@@ -0,0 +1,309 @@
+import React, { useState } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { useQuery } from '@tanstack/react-query';
+import { useSilverWisdomDetail } from '@/client/mobile/hooks/useSilverWisdomData';
+import { silverKnowledgeClient } from '@/client/api';
+import { ChevronLeftIcon, HeartIcon, BookmarkIcon, ShareIcon, ArrowDownTrayIcon, EyeIcon } from '@heroicons/react/24/outline';
+import { HeartIcon as HeartSolidIcon, BookmarkIcon as BookmarkSolidIcon } from '@heroicons/react/24/solid';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+
+dayjs.locale('zh-cn');
+
+// 色彩系统
+const COLORS = {
+  ink: {
+    light: '#f5f3f0',     // 宣纸背景色
+    medium: '#d4c4a8',    // 淡墨
+    dark: '#8b7355',      // 浓墨
+    deep: '#3a2f26',      // 焦墨
+  },
+  accent: {
+    red: '#a85c5c',    // 朱砂
+    blue: '#4a6b7c',   // 花青
+    green: '#5c7c5c',  // 石绿
+  },
+  text: {
+    primary: '#2f1f0f',   // 墨色文字
+    secondary: '#5d4e3b', // 淡墨文字
+    light: '#8b7355',     // 极淡文字
+  }
+};
+
+// 字体样式
+const FONT_STYLES = {
+  title: 'font-serif text-2xl font-bold tracking-wide',
+  sectionTitle: 'font-serif text-xl font-semibold tracking-wide',
+  body: 'font-sans text-base leading-relaxed',
+  caption: 'font-sans text-sm',
+  small: 'font-sans text-xs',
+};
+
+const SilverWisdomDetailPage: React.FC = () => {
+  const { id } = useParams<{ id: string }>();
+  const navigate = useNavigate();
+  const [isLiked, setIsLiked] = useState(false);
+  const [isBookmarked, setIsBookmarked] = useState(false);
+
+  const { data, isLoading, error } = useSilverWisdomDetail(Number(id));
+
+  if (isLoading) {
+    return (
+      <div className="min-h-screen flex items-center justify-center" style={{ backgroundColor: COLORS.ink.light }}>
+        <div className="text-center">
+          <div className="animate-spin rounded-full h-8 w-8 border-b-2" style={{ borderColor: COLORS.ink.dark }}></div>
+          <p className="mt-2" style={{ color: COLORS.text.secondary }}>加载中...</p>
+        </div>
+      </div>
+    );
+  }
+
+  if (error || !data) {
+    return (
+      <div className="min-h-screen flex items-center justify-center" style={{ backgroundColor: COLORS.ink.light }}>
+        <div className="text-center">
+          <p style={{ color: COLORS.text.primary }}>获取知识详情失败</p>
+          <button 
+            onClick={() => navigate('/silver-wisdom')}
+            className="mt-4 px-4 py-2 rounded-full text-white transition-colors"
+            style={{ backgroundColor: COLORS.ink.dark }}
+          >
+            返回列表
+          </button>
+        </div>
+      </div>
+    );
+  }
+
+  const knowledge = data.data;
+
+  const handleShare = () => {
+    if (navigator.share) {
+      navigator.share({
+        title: knowledge.title,
+        text: knowledge.content.substring(0, 100) + '...',
+        url: window.location.href,
+      });
+    }
+  };
+
+  const handleDownload = () => {
+    if (knowledge.attachment) {
+      window.open(knowledge.attachment, '_blank');
+    }
+  };
+
+  const formatDate = (date: string) => {
+    return dayjs(date).format('YYYY年MM月DD日 HH:mm');
+  };
+
+  const getReadTime = (content: string) => {
+    const wordsPerMinute = 200;
+    const wordCount = content.length;
+    return Math.ceil(wordCount / wordsPerMinute);
+  };
+
+  return (
+    <div className="min-h-screen" style={{ backgroundColor: COLORS.ink.light }}>
+      {/* 头部导航 */}
+      <header 
+        className="sticky top-0 z-10 px-4 py-3 flex items-center border-b"
+        style={{ 
+          backgroundColor: COLORS.ink.light, 
+          borderColor: COLORS.ink.medium 
+        }}
+      >
+        <button 
+          onClick={() => navigate('/silver-wisdom')}
+          className="p-2 rounded-full hover:transition-colors"
+          style={{ color: COLORS.ink.dark }}
+        >
+          <ChevronLeftIcon className="w-6 h-6" />
+        </button>
+        <h1 className="flex-1 text-center text-lg font-medium" style={{ color: COLORS.text.primary }}>
+          知识详情
+        </h1>
+        <button 
+          onClick={handleShare}
+          className="p-2 rounded-full hover:transition-colors"
+          style={{ color: COLORS.ink.dark }}
+        >
+          <ShareIcon className="w-5 h-5" />
+        </button>
+      </header>
+
+      <div className="pb-20">
+        {/* 封面图片 */}
+        {knowledge.coverImage && (
+          <div className="relative h-48 overflow-hidden">
+            <img 
+              src={knowledge.coverImage} 
+              alt={knowledge.title}
+              className="w-full h-full object-cover"
+            />
+            <div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent" />
+          </div>
+        )}
+
+        {/* 内容区域 */}
+        <div className="px-4 py-6">
+          {/* 标题区域 */}
+          <div className="mb-6">
+            <h1 className={FONT_STYLES.title} style={{ color: COLORS.text.primary }}>
+              {knowledge.title}
+            </h1>
+            <div className="flex items-center mt-3 space-x-4">
+              <span className={FONT_STYLES.caption} style={{ color: COLORS.text.secondary }}>
+                作者: {knowledge.author}
+              </span>
+              <span className={FONT_STYLES.caption} style={{ color: COLORS.text.secondary }}>
+                {formatDate(knowledge.createdAt)}
+              </span>
+              <span className={FONT_STYLES.caption} style={{ color: COLORS.text.secondary }}>
+                {getReadTime(knowledge.content)}分钟阅读
+              </span>
+            </div>
+            <div className="flex items-center mt-2 space-x-2">
+              <span className="px-2 py-1 rounded-full text-xs" style={{ 
+                backgroundColor: COLORS.accent.blue, 
+                color: 'white' 
+              }}>
+                {knowledge.category?.name || '未分类'}
+              </span>
+              {knowledge.tags && (
+                <span className="px-2 py-1 rounded-full text-xs" style={{ 
+                  backgroundColor: COLORS.ink.medium, 
+                  color: COLORS.text.primary 
+                }}>
+                  {knowledge.tags}
+                </span>
+              )}
+            </div>
+          </div>
+
+          {/* 内容正文 */}
+          <div className="mb-8">
+            <div className={`${FONT_STYLES.body} whitespace-pre-wrap`} style={{ color: COLORS.text.primary }}>
+              {knowledge.content}
+            </div>
+          </div>
+
+          {/* 附件区域 */}
+          {knowledge.attachment && (
+            <div className="mb-8">
+              <h3 className={FONT_STYLES.sectionTitle} style={{ color: COLORS.text.primary }}>
+                附件资料
+              </h3>
+              <div className="mt-4 rounded-xl border p-4" 
+                style={{ 
+                  borderColor: COLORS.ink.medium,
+                  backgroundColor: 'rgba(255,255,255,0.8)'
+                }}
+              >
+                <div className="flex items-center justify-between">
+                  <div className="flex items-center">
+                    <ArrowDownTrayIcon className="w-5 h-5 mr-2" style={{ color: COLORS.ink.dark }} />
+                    <span style={{ color: COLORS.text.primary }}>{knowledge.attachmentName || '附件'}</span>
+                  </div>
+                  <div className="flex space-x-2">
+                    <button 
+                      onClick={handleDownload}
+                      className="px-3 py-1 rounded-full text-sm transition-colors"
+                      style={{ 
+                        backgroundColor: COLORS.ink.dark, 
+                        color: 'white' 
+                      }}
+                    >
+                      下载
+                    </button>
+                    <button 
+                      onClick={() => window.open(knowledge.attachment, '_blank')}
+                      className="px-3 py-1 rounded-full text-sm border transition-colors"
+                      style={{ 
+                        borderColor: COLORS.ink.medium,
+                        color: COLORS.ink.dark 
+                      }}
+                    >
+                      预览
+                    </button>
+                  </div>
+                </div>
+                
+                {/* PDF预览 */}
+                <div className="mt-4">
+                  <iframe 
+                    src={knowledge.attachment}
+                    className="w-full h-64 rounded-lg border"
+                    style={{ borderColor: COLORS.ink.medium }}
+                    title={knowledge.attachmentName || 'PDF预览'}
+                  />
+                </div>
+              </div>
+            </div>
+          )}
+
+          {/* 互动统计 */}
+          <div className="flex items-center justify-between mb-6">
+            <div className="flex items-center space-x-4">
+              <div className="flex items-center">
+                <EyeIcon className="w-5 h-5 mr-1" style={{ color: COLORS.text.secondary }} />
+                <span style={{ color: COLORS.text.secondary }}>{knowledge.viewCount}</span>
+              </div>
+              <div className="flex items-center">
+                <HeartIcon className="w-5 h-5 mr-1" style={{ color: COLORS.text.secondary }} />
+                <span style={{ color: COLORS.text.secondary }}>{knowledge.likeCount}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        {/* 底部互动栏 */}
+        <div className="fixed bottom-0 left-0 right-0 border-t p-4"
+          style={{ 
+            backgroundColor: COLORS.ink.light,
+            borderColor: COLORS.ink.medium 
+          }}
+        >
+          <div className="flex items-center justify-around max-w-md mx-auto">
+            <button 
+              onClick={() => setIsLiked(!isLiked)}
+              className="flex flex-col items-center p-2 rounded-lg transition-colors"
+              style={{ color: isLiked ? COLORS.accent.red : COLORS.text.secondary }}
+            >
+              {isLiked ? (
+                <HeartSolidIcon className="w-6 h-6" />
+              ) : (
+                <HeartIcon className="w-6 h-6" />
+              )}
+              <span className="text-xs mt-1">点赞</span>
+            </button>
+            
+            <button 
+              onClick={() => setIsBookmarked(!isBookmarked)}
+              className="flex flex-col items-center p-2 rounded-lg transition-colors"
+              style={{ color: isBookmarked ? COLORS.accent.blue : COLORS.text.secondary }}
+            >
+              {isBookmarked ? (
+                <BookmarkSolidIcon className="w-6 h-6" />
+              ) : (
+                <BookmarkIcon className="w-6 h-6" />
+              )}
+              <span className="text-xs mt-1">收藏</span>
+            </button>
+            
+            <button 
+              onClick={handleShare}
+              className="flex flex-col items-center p-2 rounded-lg transition-colors"
+              style={{ color: COLORS.text.secondary }}
+            >
+              <ShareIcon className="w-6 h-6" />
+              <span className="text-xs mt-1">分享</span>
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default SilverWisdomDetailPage;

+ 5 - 0
src/client/mobile/routes.tsx

@@ -14,6 +14,7 @@ import JobDetailPage from './pages/JobDetailPage';
 import SilverTalentsPage from './pages/SilverTalentsPage';
 import SilverTalentsPage from './pages/SilverTalentsPage';
 import TalentDetailPage from './pages/TalentDetailPage';
 import TalentDetailPage from './pages/TalentDetailPage';
 import SilverWisdomPage from './pages/SilverWisdomPage';
 import SilverWisdomPage from './pages/SilverWisdomPage';
+import SilverWisdomDetailPage from './pages/SilverWisdomDetailPage';
 import ElderlyUniversityPage from './pages/ElderlyUniversityPage';
 import ElderlyUniversityPage from './pages/ElderlyUniversityPage';
 import TimeBankPage from './pages/TimeBankPage';
 import TimeBankPage from './pages/TimeBankPage';
 import PolicyNewsPage from './pages/PolicyNewsPage';
 import PolicyNewsPage from './pages/PolicyNewsPage';
@@ -63,6 +64,10 @@ export const router = createBrowserRouter([
         path: 'silver-wisdom',
         path: 'silver-wisdom',
         element: <SilverWisdomPage />
         element: <SilverWisdomPage />
       },
       },
+      {
+        path: 'silver-wisdom/:id',
+        element: <SilverWisdomDetailPage />
+      },
       {
       {
         path: 'elderly-university',
         path: 'elderly-university',
         element: <ElderlyUniversityPage />
         element: <ElderlyUniversityPage />