Quellcode durchsuchen

AI学习工具的用户端显示和跳转

liyanbo vor 1 Monat
Ursprung
Commit
f9e68c2846
3 geänderte Dateien mit 267 neuen und 10 gelöschten Zeilen
  1. 19 10
      src/components/LeftPanel.vue
  2. 6 0
      src/router/index.js
  3. 242 0
      src/views/miniprogramtools/index.vue

+ 19 - 10
src/components/LeftPanel.vue

@@ -51,6 +51,7 @@ import en from '@/assets/icon/en.png'
 import canal from '@/assets/icon/canal.png'
 import plant from '@/assets/icon/plant.png'
 import poetry from '@/assets/icon/poetry.png'
+import blockly from '@/assets/icon/blockly.png'
 
 
 // 黑色图标
@@ -64,6 +65,7 @@ import en02 from '@/assets/icon/en02.png'
 import canal02 from '@/assets/icon/canal02.png'
 import plant02 from '@/assets/icon/plant02.png'
 import poetry02 from '@/assets/icon/poetry02.png'
+import blockly02 from '@/assets/icon/blockly02.png'
 
 const router = useRouter()
 const route = useRoute()
@@ -83,7 +85,7 @@ const originalGroupList = ref([
     index: '0'
   },
   {
-    icon: painting, 
+    icon: painting,
     hoverIcon: painting02,
     title: '智能绘画',
     path: '/ai-painting',
@@ -144,6 +146,13 @@ const originalGroupList = ref([
     title: '虚拟实验室',
     path: '/virtual-laboratory',
     index: '9'
+  },
+  {
+    icon: blockly,
+    hoverIcon: blockly02,
+    title: 'AI学习工具',
+    path: '/mini-program-tools',
+    index: '10'
   }
 ])
 
@@ -201,7 +210,7 @@ const personData = ref([])
 // 跳转智能问答
 const navigateToAI = async (group) => {
   const routePath = group.path
-  
+
   if (group.title === '智能问答') {
     try {
       const grade = route.query.grade || localStorage.getItem('selectedGrade')
@@ -210,7 +219,7 @@ const navigateToAI = async (group) => {
       const aiPerson = juniorAIRes.data.list.find(
         person => person.name === '小智'
       )
-      
+
       if (aiPerson) {
         personData.value = {
           id: aiPerson.id,
@@ -219,12 +228,12 @@ const navigateToAI = async (group) => {
           message: aiPerson.systemMessage,
           default: aiPerson.questTip
         }
-        
+
         const newState = {
           ...personData.value,
           category: grade + 'AI'
         }
-        
+
         // 先 push 再手动更新 history.state(确保原生 state 同步)
         router
           .push({
@@ -287,23 +296,23 @@ defineExpose({
   height: 100%;
   background: linear-gradient(to bottom, #001169, #8a78d0);
   overflow-y: auto;
-  
+
   // 自定义滚动条样式
   &::-webkit-scrollbar {
     width: rpx(0); // 滚动条宽度
   }
-  
+
   &::-webkit-scrollbar-track {
     background-color: rgba(255, 255, 255, 0.1); // 滚动条轨道背景色
     border-radius: rpx(2); // 滚动条轨道圆角
   }
-  
+
   &::-webkit-scrollbar-thumb {
     background-color: rgba(255, 255, 255, 0.3); // 滚动条滑块颜色
     border-radius: rpx(2); // 滚动条滑块圆角
     transition: background-color 0.3s ease; // 滑块颜色过渡效果
   }
-  
+
   &::-webkit-scrollbar-thumb:hover {
     background-color: rgba(255, 255, 255, 0.5); // 鼠标悬停时的滑块颜色
   }
@@ -355,4 +364,4 @@ defineExpose({
   height: rpx(11);
   margin-right: rpx(2);
 }
-</style>
+</style>

+ 6 - 0
src/router/index.js

@@ -140,6 +140,11 @@ const routes = [
     path: '/virtual-laboratory',
     component: () => import('../views/virtuallaboratory/index.vue')
   },
+  // 小程序工具
+  {
+    path: '/mini-program-tools',
+    component: () => import('../views/miniprogramtools/index.vue')
+  },
   // 智能台灯
   { path: '/smartDeskLamp', component: () => import('../views/virtuallaboratory/blockly/SmartDeskLamp.vue') },
   // 智能家居
@@ -247,6 +252,7 @@ const homeRoutes = {
     '/ai-grandcanal',
     '/ai-plantexperts',
     '/virtual-laboratory',
+    '/mini-program-tools',
     '/smartHome',
     '/smartDeskLamp',
     '/ai-poetry',

+ 242 - 0
src/views/miniprogramtools/index.vue

@@ -0,0 +1,242 @@
+<template>
+  <div class="home-container">
+    <div
+      class="icon-expand"
+      :style="{
+        backgroundColor: drawerVisible ? '#44449c' : '#7F70C840',
+        left: drawerVisible ? '18%' : '0'
+      }"
+      @click="toggleDrawer"
+    >
+      <span class="vertical-lines" :style="{ color: drawerVisible ? '#8a78d0' : 'white' }">||</span>
+    </div>
+
+    <LeftPanel ref="leftPanelRef" v-show="drawerVisible" />
+
+    <div class="tool-content">
+      <div class="title-box">
+        <div class="box-icon" @click="goBack">
+          <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
+          返回首页|小程序工具
+        </div>
+      </div>
+
+      <div v-loading="loading" class="content-box">
+        <div v-for="tool in toolList" :key="tool.id" class="small-box">
+          <button
+            class="tool-card"
+            type="button"
+            :title="`打开${tool.name}`"
+            @click="openTool(tool)"
+          >
+            <img v-if="tool.cover" :src="tool.cover" :alt="tool.name" />
+            <div v-else class="cover-placeholder">暂无封面</div>
+          </button>
+          <div class="additional-text">{{ tool.name }}</div>
+        </div>
+
+        <el-empty v-if="!loading && toolList.length === 0" description="暂无可用的小程序工具" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, ref } from 'vue'
+import { ArrowLeftBold } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { useRouter } from 'vue-router'
+import LeftPanel from '@/components/LeftPanel.vue'
+import { getMiniProgramToolList } from '@/api/ai/miniProgramTool.js'
+import { homeRoutes } from '@/router/index.js'
+
+const leftPanelRef = ref(null)
+const drawerVisible = ref(true)
+const loading = ref(false)
+const toolList = ref([])
+const router = useRouter()
+
+const toggleDrawer = () => {
+  drawerVisible.value = !drawerVisible.value
+}
+
+const goBack = () => {
+  router.push(homeRoutes.home)
+}
+
+const normalizeTool = (tool) => ({
+  id: tool.id ?? tool.toolId ?? tool.url,
+  name: tool.name ?? tool.title ?? tool.toolName ?? '未命名工具',
+  cover: tool.image ?? tool.cover ?? tool.coverUrl ?? tool.imageUrl ?? '',
+  url: tool.url ?? tool.linkUrl ?? tool.routeUrl ?? tool.targetUrl ?? ''
+})
+
+const fetchToolList = async () => {
+  loading.value = true
+  try {
+    const response = await getMiniProgramToolList()
+    const list = response?.data?.list ?? response?.data ?? []
+    toolList.value = Array.isArray(list) ? list.map(normalizeTool) : []
+  } catch (error) {
+    console.error('获取小程序工具列表失败:', error)
+    toolList.value = []
+    ElMessage.error('获取小程序工具列表失败,请稍后重试')
+  } finally {
+    loading.value = false
+  }
+}
+
+const openTool = (tool) => {
+  if (!tool.url) {
+    ElMessage.warning('该工具暂未配置访问地址')
+    return
+  }
+
+  let targetUrl
+  try {
+    targetUrl = new URL(tool.url, window.location.origin)
+  } catch (error) {
+    ElMessage.error('该工具的访问地址无效')
+    return
+  }
+
+  if (!['http:', 'https:'].includes(targetUrl.protocol)) {
+    ElMessage.error('仅支持打开 HTTP 或 HTTPS 地址')
+    return
+  }
+
+  const newWindow = window.open(targetUrl.href, '_blank', 'noopener,noreferrer')
+  if (newWindow) newWindow.opener = null
+}
+
+onMounted(fetchToolList)
+</script>
+
+<style scoped lang="scss">
+@use 'sass:math';
+
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+
+.home-container {
+  position: fixed;
+  inset: 0;
+  display: flex;
+  background: linear-gradient(to bottom, #e2ddfc, #f1effd);
+}
+
+.icon-expand {
+  width: rpx(8);
+  height: rpx(35);
+  position: absolute;
+  top: 50%;
+  z-index: 9999;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  clip-path: polygon(0 0, 100% 15%, 100% 85%, 0 100%);
+  transform: translateY(-50%);
+  transition: all .3s ease;
+}
+
+.vertical-lines {
+  font-size: rpx(10);
+}
+
+.tool-content {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  min-width: 0;
+}
+
+.title-box {
+  height: rpx(35);
+}
+
+.box-icon {
+  display: flex;
+  align-items: center;
+  height: 100%;
+  padding-left: rpx(15);
+  color: #000;
+  font-size: rpx(10);
+  cursor: pointer;
+}
+
+.left-icon {
+  margin: 0 rpx(5) 0 rpx(10);
+}
+
+.content-box {
+  display: flex;
+  flex: 1;
+  flex-wrap: wrap;
+  align-content: flex-start;
+  overflow-y: auto;
+}
+
+.content-box :deep(.el-empty) {
+  width: 100%;
+}
+
+.small-box {
+  display: flex;
+  flex: 0 0 calc(33.333% - rpx(10));
+  flex-direction: column;
+  align-items: center;
+  margin: rpx(3) 0 0 rpx(10);
+  color: #000;
+  font-size: rpx(8);
+}
+
+.tool-card {
+  width: rpx(150);
+  height: rpx(80);
+  margin-top: rpx(5);
+  padding: 0;
+  overflow: hidden;
+  border: 1px solid #fff;
+  border-radius: rpx(10);
+  background: #d9d5ef;
+  cursor: pointer;
+}
+
+.tool-card:hover,
+.tool-card:focus-visible {
+  box-shadow: 0 4px 8px rgb(0 0 0 / 50%);
+  outline: none;
+}
+
+.tool-card img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.cover-placeholder {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+  color: #666;
+  font-size: rpx(9);
+}
+
+.additional-text {
+  margin: rpx(2) 0 rpx(4);
+  font-size: rpx(8);
+}
+
+.content-box::-webkit-scrollbar {
+  width: rpx(2);
+}
+
+.content-box::-webkit-scrollbar-thumb {
+  border-radius: rpx(3);
+  background: linear-gradient(to bottom, hsl(230 100% 21%), #8a78d0);
+}
+</style>