lsc 6 일 전
부모
커밋
ee12741a13

+ 12 - 1
src/App.vue

@@ -1,7 +1,7 @@
 <template>
   <template v-if="slides.length">
     <Screen v-if="screening" />
-    <Student v-else-if="viewMode === 'student'" />
+    <Student v-else-if="viewMode === 'student'" :courseid="urlParams.courseid" :type="urlParams.type" />
     <Editor v-else-if="_isPC" />
     <Mobile v-else />
   </template>
@@ -55,6 +55,17 @@ const getInitialViewMode = () => {
   return 'editor'
 }
 
+// 获取URL参数中的courseid和type
+const getUrlParams = () => {
+  const urlParams = new URLSearchParams(window.location.search)
+  return {
+    courseid: urlParams.get('courseid'),
+    type: urlParams.get('type')
+  }
+}
+
+const urlParams = getUrlParams()
+
 const viewMode = ref(getInitialViewMode())
 
 // 全局切换视图模式的函数

+ 1 - 1
src/hooks/useCreateElement.ts

@@ -322,7 +322,7 @@ export default () => {
     const hasWebpage = currentSlide.value?.elements?.some(element => element.type === 'frame')
     
     if (hasWebpage) {
-      message.error('当前幻灯片已包含网页元素,一个幻灯片只能插入一个网页')
+      message.error('当前幻灯片已有学习内容,一个幻灯片只能插入一个学习内容')
       return
     }
     

+ 9 - 9
src/views/Editor/Canvas/EditableElement.vue

@@ -174,15 +174,15 @@ const contextmenus = (): ContextmenuItem[] => {
   // 为网页元素添加特殊菜单项
   if (props.elementInfo.type === ElementTypes.FRAME) {
     const frameMenu = [
-      {
-        text: '修改链接',
-        handler: () => {
-          const frameElement = props.elementInfo as any
-          if (frameElement.url) {
-            props.openWebpageLinkEditDialog(frameElement.id, frameElement.url)
-          }
-        },
-      },
+      // {
+      //   text: '修改链接',
+      //   handler: () => {
+      //     const frameElement = props.elementInfo as any
+      //     if (frameElement.url) {
+      //       props.openWebpageLinkEditDialog(frameElement.id, frameElement.url)
+      //     }
+      //   },
+      // },
       {
         text: '在新窗口打开',
         handler: () => {

+ 234 - 18
src/views/Editor/CanvasTool/WebpageInput.vue

@@ -1,17 +1,47 @@
 <template>
   <div class="webpage-input">
-    <div class="title">插入网页</div>
-    <div class="description">请输入要嵌入的网页链接地址</div>
+    <div class="title">插入学习内容</div>
+    <div class="description">请选择要嵌入的学习内容</div>
     
-    <Input 
-      v-model:value="webpageUrl" 
-      placeholder="请输入网页链接,e.g. https://example.com"
-      style="margin: 15px 0;"
-    />
+    <!-- 当列表为空时显示上传提示 -->
+    <div v-if="webpageList.length === 0" class="empty-state">
+      <div class="empty-icon">📚</div>
+      <div class="empty-title">暂无学习内容</div>
+      <div class="empty-desc">请先上传或创建学习内容</div>
+    </div>
+    
+    <!-- 当有内容时显示列表 -->
+    <div v-else class="webpage-list">
+      <div 
+        v-for="(webpage, index) in webpageList" 
+        :key="index"
+        class="webpage-item"
+        :class="{ active: selectedIndex === index }"
+        @click="toggleWebpageSelection(index)"
+      >
+        <div class="webpage-info">
+          <div class="webpage-title">
+            <span class="type-tag" :class="getTypeClass(webpage.type)">{{ getTypeLabel(webpage.type) }}</span>
+            {{ webpage.title }}
+          </div>
+          <div class="webpage-url">{{ webpage.url }}</div>
+        </div>
+        <div class="webpage-status">
+          <div v-if="selectedIndex === index" class="selected-icon">✓</div>
+        </div>
+      </div>
+    </div>
     
     <div class="btns">
       <Button @click="emit('close')" style="margin-right: 10px;">取消</Button>
-      <Button type="primary" @click="insertWebpage()">确认</Button>
+      <Button 
+        v-if="webpageList.length > 0"
+        type="primary" 
+        @click="insertWebpage()"
+        :disabled="selectedIndex === null"
+      >
+        确认
+      </Button>
     </div>
   </div>
 </template>
@@ -19,28 +49,72 @@
 <script lang="ts" setup>
 import { ref } from 'vue'
 import message from '@/utils/message'
-import Input from '@/components/Input.vue'
 import Button from '@/components/Button.vue'
 
+interface Webpage {
+  type: number
+  title: string
+  url: string
+}
+
+interface Props {
+  webpageList: Webpage[]
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  webpageList: () => []
+})
+
 const emit = defineEmits<{
   (event: 'insertWebpage', payload: string): void
   (event: 'close'): void
 }>()
 
-const webpageUrl = ref('https://aichat.cocorobo.cn/#/?id=4e826675-b8e3-44af-9ef3-8cebd302f702&type=agent')
+const selectedIndex = ref<number | null>(null)
+
+const toggleWebpageSelection = (index: number) => {
+  if (selectedIndex.value === index) {
+    // 如果点击已选中的网页,则取消选择
+    selectedIndex.value = null
+  }
+  else {
+    // 否则选择新网页
+    selectedIndex.value = index
+  }
+}
 
 const insertWebpage = () => {
-  if (!webpageUrl.value) return message.error('请先输入正确的网页链接')
+  if (selectedIndex.value === null) {
+    return message.error('请先选择一个网页')
+  }
   
-  // 简单的URL验证
-  try {
-    new URL(webpageUrl.value)
+  // 根据选中的index获取对应的链接
+  const selectedWebpage = props.webpageList[selectedIndex.value]
+  if (selectedWebpage) {
+    emit('insertWebpage', selectedWebpage.url)
   }
-  catch {
-    return message.error('请输入正确的网页链接格式')
+}
+
+// 获取类型标签
+const getTypeLabel = (type: number) => {
+  const typeMap: Record<number, string> = {
+    45: '选择题',
+    15: '问答题',
+    72: 'AI应用',
+    73: 'H5页面'
   }
-  
-  emit('insertWebpage', webpageUrl.value)
+  return typeMap[type] || '未知'
+}
+
+// 获取类型样式类
+const getTypeClass = (type: number) => {
+  const classMap: Record<number, string> = {
+    45: 'type-choice',
+    15: 'type-question',
+    72: 'type-ai',
+    73: 'type-h5'
+  }
+  return classMap[type] || 'type-default'
 }
 </script>
 
@@ -63,6 +137,148 @@ const insertWebpage = () => {
   margin-bottom: 15px;
 }
 
+.empty-state {
+  text-align: center;
+  padding: 40px 20px;
+  color: #666;
+}
+
+.empty-icon {
+  font-size: 48px;
+  margin-bottom: 16px;
+}
+
+.empty-title {
+  font-size: 16px;
+  font-weight: 500;
+  color: #333;
+  margin-bottom: 8px;
+}
+
+.empty-desc {
+  font-size: 14px;
+  color: #999;
+  margin-bottom: 24px;
+}
+
+.webpage-list {
+  max-height: 250px; /* 设置最大高度,大约显示5个项目 */
+  overflow-y: auto;
+  margin: 15px 0;
+  border: 1px solid #e0e0e0;
+  border-radius: 6px;
+  
+  /* 自定义滚动条样式 */
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
+  
+  &::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 3px;
+  }
+  
+  &::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: 3px;
+    
+    &:hover {
+      background: #a8a8a8;
+    }
+  }
+}
+
+.webpage-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 12px 16px;
+  border-bottom: 1px solid #f0f0f0;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  height: 50px; /* 固定每个项目的高度 */
+  box-sizing: border-box;
+  
+  &:last-child {
+    border-bottom: none;
+  }
+  
+  &:hover {
+    background-color: #f8f9fa;
+  }
+  
+  &.active {
+    background-color: #e3f2fd;
+    border-left: 3px solid #2196f3;
+  }
+}
+
+.webpage-info {
+  flex: 1;
+  min-width: 0;
+}
+
+.webpage-title {
+  font-size: 14px;
+  font-weight: 500;
+  color: #333;
+  margin-bottom: 4px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.type-tag {
+  padding: 2px 6px;
+  border-radius: 4px;
+  font-size: 11px;
+  font-weight: 500;
+  color: #fff;
+  white-space: nowrap;
+  flex-shrink: 0;
+  
+  &.type-choice {
+    background-color: #4caf50;
+  }
+  
+  &.type-question {
+    background-color: #ff9800;
+  }
+  
+  &.type-ai {
+    background-color: #2196f3;
+  }
+  
+  &.type-h5 {
+    background-color: #9c27b0;
+  }
+  
+  &.type-default {
+    background-color: #757575;
+  }
+}
+
+.webpage-url {
+  font-size: 12px;
+  color: #666;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.webpage-status {
+  margin-left: 12px;
+}
+
+.selected-icon {
+  color: #2196f3;
+  font-weight: bold;
+  font-size: 16px;
+}
+
 .btns {
   margin-top: 15px;
   text-align: right;

+ 40 - 2
src/views/Editor/CanvasTool/index.vue

@@ -71,14 +71,15 @@
         <IconInsertTable class="handler-item" v-tooltip="'插入表格'" />
       </Popover>
       <IconFormula class="handler-item" v-tooltip="'插入公式'" @click="latexEditorVisible = true" />
-      <Popover trigger="click" v-model:value="webpageInputVisible" :offset="10">
+      <Popover trigger="manual" v-model:value="webpageInputVisible" :offset="10">
         <template #content>
           <WebpageInput 
+            :webpageList="webpageList"
             @close="webpageInputVisible = false"
             @insertWebpage="url => { createFrameElement(url); webpageInputVisible = false }"
           />
         </template>
-        <IconLinkOne class="handler-item" v-tooltip="'插入网页'" />
+        <IconLinkOne class="handler-item" v-tooltip="'插入学习内容'" @click="handleInsertLearningContent" />
       </Popover>
       <Popover trigger="click" v-model:value="mediaInputVisible" :offset="10">
         <template #content>
@@ -189,6 +190,43 @@ const chartPoolVisible = ref(false)
 const tableGeneratorVisible = ref(false)
 const mediaInputVisible = ref(false)
 const webpageInputVisible = ref(false)
+
+// 预设的网页列表
+const webpageList = ref<Array<{type: number, title: string, url: string}>>([])
+
+// 点击插入学习内容时获取数据
+const handleInsertLearningContent = () => {
+  try {
+    // 获取父窗口的工具列表,如果没有则使用空数组
+    // 定义父窗口的类型,添加pptToolList属性
+    interface ParentWindowWithToolList extends Window {
+      pptToolList?: Array<{ tool?: number; title?: string; url?: string }>
+    }
+    const parentWindow = window.parent as ParentWindowWithToolList
+    const pptToolList = parentWindow?.pptToolList || []
+    // 转换父窗口的工具列表格式
+    webpageList.value = pptToolList.map((item: any) => ({
+      type: item.tool || 0,
+      title: item.title || '未知工具',
+      url: item.url || '#'
+    })).filter((item: any) => item.url !== '#') // 过滤掉无效的URL
+    
+    console.log('学习内容列表加载完成:', webpageList.value.length, '个项目')
+    
+    // 显示弹窗
+    webpageInputVisible.value = true
+    
+  }
+  catch (error) {
+    console.error('加载学习内容失败:', error)
+    
+    // 发生错误时使用空数组
+    webpageList.value = []
+    
+    // 显示弹窗
+    webpageInputVisible.value = true
+  }
+}
 const latexEditorVisible = ref(false)
 const textTypeSelectVisible = ref(false)
 const shapeMenuVisible = ref(false)

+ 6 - 0
src/views/Editor/Toolbar/index.vue

@@ -45,6 +45,12 @@ const elementTabs = computed<ElementTabs[]>(() => {
       { label: '动画', key: ToolbarStates.EL_ANIMATION },
     ]
   }
+  if (handleElement.value?.type === 'frame') {
+    return [
+      { label: '位置', key: ToolbarStates.EL_POSITION },
+      { label: '动画', key: ToolbarStates.EL_ANIMATION },
+    ]
+  }
   return [
     { label: '样式', key: ToolbarStates.EL_STYLE },
     { label: '位置', key: ToolbarStates.EL_POSITION },

+ 1 - 1
src/views/Screen/ScreenSlideList.vue

@@ -22,7 +22,7 @@
           width: slideWidth + 'px',
           height: slideHeight + 'px',
         }"
-        v-if="Math.abs(slideIndex - index) < 2 || slide.animations?.length"
+        v-show="Math.abs(slideIndex - index) < 2 || slide.animations?.length"
       >
         <ScreenSlide 
           :slide="slide" 

+ 159 - 23
src/views/Student/index.vue

@@ -1,7 +1,7 @@
 <template>
     <div class="pptist-student-viewer" :class="{ 'fullscreen': isFullscreen, 'laser-pen': laserPen }">
         <!-- 左侧导航栏 -->
-        <div class="layout-content-left">
+        <div class="layout-content-left" v-show="type == '1'">
             <div class="thumbnails">
                 <div class="viewer-header">
                     <h3>幻灯片导航</h3>
@@ -16,12 +16,10 @@
                     </div>
                 </div>
 
-                <div class="page-number">幻灯片 {{ slideIndex + 1 }} / {{ slides.length }}</div>
-
-                <!-- 进度条 -->
+                <!-- <div class="page-number">幻灯片 {{ slideIndex + 1 }} / {{ slides.length }}</div>
                 <div class="progress-bar">
                     <div class="progress-fill" :style="{ width: `${((slideIndex + 1) / slides.length) * 100}%` }"></div>
-                </div>
+                </div> -->
             </div>
         </div>
 
@@ -50,41 +48,47 @@
 
             <div class="viewer-canvas" ref="viewerCanvasRef">
                 <!-- 全屏时:使用放映功能 -->
-                <ScreenSlideList v-if="isFullscreen && showSlideList" :slideWidth="slideWidth"
+                <!-- <ScreenSlideList :slideWidth="slideWidth"
                     :slideHeight="slideHeight" :animationIndex="0" :turnSlideToId="() => { }"
-                    :manualExitFullscreen="() => { }" />
+                    :manualExitFullscreen="() => { }" /> -->
 
                 <!-- 不全屏时:使用编辑模式的显示比例和居中逻辑 -->
-                <div v-else-if="showSlideList" class="slide-list-wrap" :style="{
-                    width: (slideWidth * canvasScale) + 'px',
-                    height: (slideHeight * canvasScale) + 'px',
-                    left: `${(containerWidth - slideWidth * canvasScale) / 2}px`,
-                    top: `${(containerHeight - slideHeight * canvasScale) / 2}px`
+                <div class="slide-list-wrap" :style="{
+                    width: isFullscreen ? '100%' : (slideWidth * canvasScale) + 'px',
+                    height: isFullscreen ? '100%' : (slideHeight * canvasScale) + 'px',
+                    left: isFullscreen ? '0' : `${(containerWidth - slideWidth * canvasScale) / 2}px`,
+                    top: isFullscreen ? '0' : `${(containerHeight - slideHeight * canvasScale) / 2}px`
                 }">
                     <div class="viewport" v-if="false">
-                        <!-- 背景 -->
                         <div class="background" :style="backgroundStyle"></div>
 
-                        <!-- 使用ScreenElement组件显示元素 -->
                         <ScreenElement v-for="(element, index) in elementList" :key="element.id" :elementInfo="element"
                             :elementIndex="index + 1" :animationIndex="0" :turnSlideToId="() => { }"
                             :manualExitFullscreen="() => { }" />
                     </div>
 
-                    <!-- 加载状态提示 -->
-                    <ScreenSlideList v-if="showSlideList" :slideWidth="slideWidth * canvasScale"
+                    <ScreenSlideList :slideWidth="slideWidth * canvasScale"
                         :slideHeight="slideHeight * canvasScale" :animationIndex="0" :turnSlideToId="() => { }"
                         :manualExitFullscreen="() => { }" />
                 </div>
-                <div v-else-if="!showSlideList" class="loading-indicator">
-                    <div class="loading-text">正在重新加载...</div>
-                </div>
                 <!-- 全屏时的左右下角工具按钮 -->
                 <div v-if="isFullscreen" class="tools-left">
                     <IconLeftTwo class="tool-btn" theme="two-tone" :fill="['#111', '#fff']" @click="previousSlide" />
                     <IconRightTwo class="tool-btn" theme="two-tone" :fill="['#111', '#fff']" @click="nextSlide" />
                 </div>
 
+                <!-- 作业提交按钮 - 当当前幻灯片包含iframe时显示 -->
+                <div 
+                    v-if="currentSlideHasIframe" 
+                    class="homework-submit-btn"
+                    :style="{ right: getHomeworkButtonRight() + 'px' }"
+                    @click="handleHomeworkSubmit"
+                    v-tooltip="'作业提交'"
+                >
+                    <IconEdit class="tool-btn" />
+                    <span class="btn-text">作业提交</span>
+                </div>
+
                 <!-- 功能组件 -->
                 <SlideThumbnails v-if="slideThumbnailModelVisible" :turnSlideToIndex="goToSlide"
                     @close="slideThumbnailModelVisible = false" />
@@ -109,6 +113,13 @@
                 </div>
             </div>
         </div>
+        <div class="layout-content-right" v-show="type == '1'">
+            <div class="thumbnails">
+                <div class="viewer-header">
+                    <h3>左侧导航</h3>
+                </div>
+            </div>
+        </div>
     </div>
 </template>
 
@@ -116,6 +127,7 @@
 import { computed, ref, onMounted, onUnmounted, nextTick, inject } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useSlidesStore } from '@/store'
+import { ElementTypes } from '@/types/slides'
 import { fillDigit } from '@/utils/common'
 import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
 import ScreenSlideList from '@/views/Screen/ScreenSlideList.vue'
@@ -125,6 +137,18 @@ import WritingBoardTool from '@/views/Screen/WritingBoardTool.vue'
 import CountdownTimer from '@/views/Screen/CountdownTimer.vue'
 import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
 import useImport from '@/hooks/useImport'
+import message from '@/utils/message'
+
+// 定义组件props
+interface Props {
+  courseid?: string | null
+  type?: string | null
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  courseid: null,
+  type: null
+})
 
 // 图标组件通过全局注册,无需导入
 
@@ -154,7 +178,7 @@ const slideHeight = ref(0)
 
 // 计算幻灯片尺寸的函数
 const calculateSlideSize = () => {
-  const slideWrapRef = viewerCanvasRef.value || document.body
+  const slideWrapRef = isFullscreen.value ? document.body : viewerCanvasRef.value
   const winWidth = slideWrapRef.clientWidth
   const winHeight = slideWrapRef.clientHeight
 
@@ -212,7 +236,7 @@ const calculateScale = () => {
     const scale = Math.min(scaleX, scaleY) * 0.9
 
     console.log('最终缩放比例:', scale)
-    canvasScale.value = scale
+    canvasScale.value = isFullscreen.value ? 1 : props.type == '1' ? 0.9 : 0.95
   }
   else {
     console.error('找不到容器元素')
@@ -233,6 +257,11 @@ const elementList = computed(() => {
   return currentSlide.value?.elements || []
 })
 
+// 检测当前幻灯片是否包含iframe元素
+const currentSlideHasIframe = computed(() => {
+  return elementList.value.some(element => element.type === ElementTypes.FRAME)
+})
+
 // 跳转到指定幻灯片
 const goToSlide = (index: number) => {
   console.log('goToSlide 被调用,目标索引:', index)
@@ -365,6 +394,24 @@ const backToEditor = () => {
   window.location.href = '/'
 }
 
+// 作业提交功能
+const handleHomeworkSubmit = () => {
+  console.log('作业提交按钮被点击')
+  // 这里可以跳转到作业提交页面,并传递当前幻灯片的ID
+  // 例如:window.location.href = `/course/${props.courseid}/homework/${currentSlide.value?.id}`
+  message.info('作业提交功能暂未实现')
+}
+
+// 获取作业提交按钮的右侧位置
+const getHomeworkButtonRight = () => {
+  if (isFullscreen.value) {
+    return 30 // 全屏时按钮在右侧30px
+  }
+  if (props.type === '1') {
+    return 230 // type=1时(有左侧导航栏)按钮在右侧230px
+  }
+  return 30 // type=2时按钮在右侧30px
+}
 
 
 // 键盘快捷键
@@ -428,6 +475,23 @@ const handleViewportSizeUpdated = (event: any) => {
 onMounted(() => {
   document.addEventListener('keydown', handleKeydown)
 
+  // 处理URL参数
+  if (props.courseid || props.type) {
+    console.log('收到URL参数:', { courseid: props.courseid, type: props.type })
+    
+    // 这里可以根据courseid和type进行相应的处理
+    // 比如加载特定的课程数据、设置特定的显示模式等
+    if (props.courseid) {
+      console.log('课程ID:', props.courseid)
+      // TODO: 根据courseid加载对应的课程数据
+    }
+    
+    if (props.type) {
+      console.log('类型:', props.type)
+      // TODO: 根据type设置特定的显示模式或功能
+    }
+  }
+
   // 计算初始缩放比例
   nextTick(() => {
     calculateScale()
@@ -456,10 +520,14 @@ onMounted(() => {
     previousSlide,
     nextSlide,
     enterFullscreen,
-    toggleLaserPen
+    toggleLaserPen,
+    // 添加URL参数到全局对象中
+    courseid: props.courseid,
+    type: props.type
   }
 
   console.log('PPTist Student View 已加载,可通过 window.PPTistStudent 访问功能')
+  console.log('URL参数:', { courseid: props.courseid, type: props.type })
 })
 
 onUnmounted(() => {
@@ -492,6 +560,9 @@ onUnmounted(() => {
         .layout-content-left {
             display: none; // 全屏时隐藏左侧导航栏
         }
+        .layout-content-right {
+            display: none; // 全屏时隐藏左侧导航栏
+        }
 
         .viewer-header {
             display: none; // 全屏时隐藏顶部标题栏
@@ -512,8 +583,16 @@ onUnmounted(() => {
     overflow-y: auto;
 }
 
+.layout-content-right {
+    width: 200px;
+    height: 100%;
+    background-color: #fff;
+    border-left: 1px solid #e0e0e0;
+    overflow-y: auto;
+}
+
 .thumbnails {
-    padding: 16px;
+    padding: 0;
 
     .viewer-header {
         margin-bottom: 16px;
@@ -527,6 +606,9 @@ onUnmounted(() => {
     }
 
     .thumbnail-list {
+        width: 100%;
+        padding: 0 16px;
+        box-sizing: border-box;
         .thumbnail-item {
             position: relative;
             margin-bottom: 12px;
@@ -685,6 +767,11 @@ onUnmounted(() => {
     background-color: #fff;
     box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.01), 0 0 12px 0 rgba(0, 0, 0, 0.1);
     border-radius: 8px;
+    
+    /* 全屏时去掉圆角 */
+    .pptist-student-viewer.fullscreen & {
+        border-radius: 0;
+    }
 }
 
 .loading-indicator {
@@ -813,6 +900,10 @@ onUnmounted(() => {
         width: 160px;
     }
 
+    .layout-content-right {
+        width: 160px;
+    }
+
     .viewer-header {
         padding: 0 16px;
 
@@ -826,4 +917,49 @@ onUnmounted(() => {
         }
     }
 }
+
+/* 作业提交按钮样式 */
+.homework-submit-btn {
+    position: fixed;
+    bottom: 30px;
+    z-index: 100;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    padding: 12px 20px;
+    border-radius: 25px;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
+    transition: all 0.3s ease;
+    font-size: 14px;
+    font-weight: 500;
+
+    &:hover {
+        transform: translateY(-2px);
+        box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
+    }
+
+    &:active {
+        transform: translateY(0);
+    }
+
+    .btn-text {
+        white-space: nowrap;
+    }
+
+    .tool-btn {
+        background: transparent;
+        color: white;
+        width: 20px;
+        height: 20px;
+        font-size: 16px;
+        
+        &:hover {
+            background: transparent;
+            transform: none;
+        }
+    }
+}
 </style>