11wqe1 1 месяц назад
Родитель
Сommit
4fc3ff32bb
76 измененных файлов с 3424 добавлено и 1543 удалено
  1. 2 1
      src/App.vue
  2. 24 23
      src/components/CollapsibleToolbar/index.vue
  3. 4 2
      src/components/Contextmenu/MenuContent.vue
  4. 9 8
      src/components/LaTeXEditor/index.vue
  5. 2 1
      src/components/Modal.vue
  6. 61 51
      src/configs/chart.ts
  7. 14 12
      src/configs/element.ts
  8. 24 22
      src/configs/latex.ts
  9. 3 3
      src/configs/lines.ts
  10. 6 6
      src/configs/shapes.ts
  11. 4 2
      src/hooks/useCreateElement.ts
  12. 621 753
      src/hooks/useImport.ts
  13. 3 2
      src/hooks/useLink.ts
  14. 4 3
      src/hooks/useSearch.ts
  15. 9 0
      src/i18n/lang.ts
  16. 2 16
      src/main.ts
  17. 41 40
      src/views/Editor/AIPPTDialog.vue
  18. 29 37
      src/views/Editor/Canvas/EditableElement.vue
  19. 8 7
      src/views/Editor/Canvas/LinkDialog.vue
  20. 4 3
      src/views/Editor/Canvas/Operate/LinkHandler.vue
  21. 2 1
      src/views/Editor/Canvas/ShapeCreateCanvas.vue
  22. 7 6
      src/views/Editor/Canvas/WebpageLinkEditDialog.vue
  23. 15 14
      src/views/Editor/Canvas/index.vue
  24. 4 2
      src/views/Editor/CanvasTool/ChartPool.vue
  25. 11 10
      src/views/Editor/CanvasTool/MediaInput.vue
  26. 9 8
      src/views/Editor/CanvasTool/TableGenerator.vue
  27. 16 15
      src/views/Editor/CanvasTool/WebpageInput.vue
  28. 26 25
      src/views/Editor/CanvasTool/index.vue
  29. 12 12
      src/views/Editor/EditorHeader/index.vue
  30. 14 13
      src/views/Editor/ExportDialog/ExportImage.vue
  31. 3 2
      src/views/Editor/ExportDialog/ExportJSON.vue
  32. 10 9
      src/views/Editor/ExportDialog/ExportPDF.vue
  33. 15 14
      src/views/Editor/ExportDialog/ExportPPTX.vue
  34. 10 9
      src/views/Editor/ExportDialog/ExportSpecificFile.vue
  35. 6 5
      src/views/Editor/ExportDialog/index.vue
  36. 27 26
      src/views/Editor/MarkupPanel.vue
  37. 19 13
      src/views/Editor/NotesPanel.vue
  38. 2 1
      src/views/Editor/Remark/Editor.vue
  39. 10 9
      src/views/Editor/SearchPanel.vue
  40. 16 9
      src/views/Editor/SelectPanel.vue
  41. 21 20
      src/views/Editor/Thumbnails/index2.vue
  42. 2 2
      src/views/Editor/Toolbar/ElementAnimationPanel.vue
  43. 4 3
      src/views/Editor/Toolbar/ElementStylePanel/AudioStylePanel.vue
  44. 10 8
      src/views/Editor/Toolbar/ElementStylePanel/ChartStylePanel/ChartDataEditor.vue
  45. 6 5
      src/views/Editor/Toolbar/ElementStylePanel/ChartStylePanel/ThemeColorsSetting.vue
  46. 11 10
      src/views/Editor/Toolbar/ElementStylePanel/ChartStylePanel/index.vue
  47. 4 3
      src/views/Editor/Toolbar/ElementStylePanel/FrameStylePanel.vue
  48. 11 10
      src/views/Editor/Toolbar/ElementStylePanel/ImageStylePanel.vue
  49. 4 3
      src/views/Editor/Toolbar/ElementStylePanel/LatexStylePanel.vue
  50. 7 6
      src/views/Editor/Toolbar/ElementStylePanel/LineStylePanel.vue
  51. 14 13
      src/views/Editor/Toolbar/ElementStylePanel/ShapeStylePanel.vue
  52. 4 3
      src/views/Editor/Toolbar/ElementStylePanel/VideoStylePanel.vue
  53. 23 20
      src/views/Screen/BaseView.vue
  54. 2 1
      src/views/Screen/CountdownTimer.vue
  55. 12 11
      src/views/Screen/WritingBoardTool.vue
  56. 4 3
      src/views/Screen/hooks/useExecPlay.ts
  57. 2 1
      src/views/Student/components/AIWorkModal.vue
  58. 7 6
      src/views/Student/components/ChoiceStatistics.vue
  59. 8 7
      src/views/Student/components/ChoiceWorkModal.vue
  60. 20 19
      src/views/Student/components/DialoguePanel.vue
  61. 5 4
      src/views/Student/components/QAWorkModal.vue
  62. 10 9
      src/views/Student/components/ShotWorkModal.vue
  63. 10 9
      src/views/Student/components/answerTheResult.vue
  64. 12 11
      src/views/Student/components/choiceQuestionDetailDialog.vue
  65. 80 115
      src/views/Student/index.vue
  66. 2 1
      src/views/components/ThumbnailSlide/index.vue
  67. 2 1
      src/views/components/element/AudioElement/AudioPlayer.vue
  68. 14 12
      src/views/components/element/FrameElement/BaseFrameElement.vue
  69. 2 1
      src/views/components/element/ProsemirrorEditor.vue
  70. 14 13
      src/views/components/element/TableElement/EditableTable.vue
  71. 2 1
      src/views/components/element/TableElement/index.vue
  72. 4 3
      src/views/components/element/VideoElement/VideoPlayer/index.vue
  73. 6 5
      src/views/components/tool/previewImageTool.vue
  74. 657 3
      src/views/lang/cn.json
  75. 660 3
      src/views/lang/en.json
  76. 660 3
      src/views/lang/hk.json

+ 2 - 1
src/App.vue

@@ -7,7 +7,7 @@
     <Student v-else-if="viewMode === 'student'" :courseid="urlParams.courseid" :type="urlParams.type" :userid="urlParams.userid" :oid="urlParams.oid" :org="urlParams.org" :cid="urlParams.cid" />
     <Mobile v-else />
   </template>
-  <FullscreenSpin tip="数据初始化中,请稍等 ..." v-else  loading :mask="false" />
+  <FullscreenSpin :tip="lang.ssInitDataWait" v-else loading :mask="false" />
 </template>
 
 
@@ -16,6 +16,7 @@
 import { onMounted, ref, provide } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useScreenStore, useMainStore, useSnapshotStore, useSlidesStore } from '@/store'
+import { lang } from '@/main'
 import { LOCALSTORAGE_KEY_DISCARDED_DB } from '@/configs/storage'
 import { deleteDiscardedDB } from '@/utils/database'
 import { isPC } from '@/utils/common'

+ 24 - 23
src/components/CollapsibleToolbar/index.vue

@@ -7,7 +7,7 @@
             <circle cx="12" cy="12" r="3"/>
             <path d="M12 1v6m0 6v6M5.64 5.64l4.24 4.24m4. 4.24l4.24 4.24M1 12h6m6 0h6M5.64 18.36l4.24-4.24m4.24-4.24l4kt-4.24"/>
           </svg>
-          <span class="item-label">互动工具</span>
+          <span class="item-label">{{ lang.ssInteract }}</span>
         </div>
         <div class="sidebar-item" @click="handleToolClick('h5page')">
           <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -15,7 +15,7 @@
             <path d="M2 12h20"/>
             <path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/>
           </svg>
-          <span class="item-label">H5页面</span>
+          <span class="item-label">{{ lang.ssHPage }}</span>
         </div>
         <div class="sidebar-item" @click="handleToolClick('aiapp')">
           <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -24,14 +24,14 @@
             <rect x="14" y="14" width="7" height="7"/>
             <rect x="3" y="14" width="7" height="7"/>
           </svg>
-          <span class="item-label">AI应用</span>
+          <span class="item-label">{{ lang.ssAiApp }}</span>
         </div>
         <div class="sidebar-item" @click="handleToolClick('video')">
           <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
             <rect x="3" y="4" width="18" height="16" rx="2" ry="2"/>
             <polygon points="10 9 16 12 10 15 10 9"/>
           </svg>
-          <span class="item-label">视频</span>
+          <span class="item-label">{{ lang.ssVideo }}</span>
         </div>
         <div class="sidebar-item" @click="handleToolClick('creative')">
           <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -39,14 +39,14 @@
             <line x1="12" y1="8" x2="12" y2="16"/>
             <line x1="8" y1="12" x2="16" y2="12"/>
           </svg>
-          <span class="item-label">创作空间</span>
+          <span class="item-label">{{ lang.ssCreative }}</span>
         </div>
         <div class="sidebar-item" :class="{ active: activeSubmenu === 'contentlist' }" @click="toggleSubmenu('contentlist')" v-show="false">
           <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
             <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
             <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
           </svg>
-          <span class="item-label">内容列表</span>
+          <span class="item-label">{{ lang.ssContentList }}</span>
         </div>
       </div>
     </div>
@@ -57,21 +57,21 @@
           <circle cx="12" cy="12" r="10"/>
           <path d="M12 16v-4m0-4h.01"/>
         </svg>
-        <span class="submenu-label">选择题</span>
+        <span class="submenu-label">{{ lang.ssChoiceQ }}</span>
       </div>
       <div class="submenu-item" @click="handleToolClick('qa')">
         <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
           <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>
         </svg>
-        <span class="submenu-label">问答</span>
+        <span class="submenu-label">{{ lang.ssQandA }}</span>
       </div>
     </div>
     
     <div class="content-list-submenu" :class="{ visible: activeSubmenu === 'contentlist' }">
       <div v-if="contentList.length === 0" class="empty-state">
         <div class="empty-icon">📚</div>
-        <div class="empty-title">暂无学习内容</div>
-        <div class="empty-title">请先上传或创建学习内容</div>
+        <div class="empty-title">{{ lang.ssNoLearn }}</div>
+        <div class="empty-title">{{ lang.ssNeedUpload }}</div>
       </div>
       <div v-else class="content-list">
         <div 
@@ -89,14 +89,14 @@
           </div>
           <div class="content-actions">
             <template v-if="item.tool === 74 || item.tool === 75">
-              <div class="action-btn" @click.stop="previewVideo(item)" title="预览">
+              <div class="action-btn" @click.stop="previewVideo(item)" :title="lang.ssPreview">
                 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                   <polygon points="5 3 19 12 5 21 5 3"/>
                 </svg>
               </div>
             </template>
             <template v-if="item.tool !== 74 && item.tool !== 75 && item.tool !== 76">
-              <div class="action-btn" @click.stop="editContent(item)" title="编辑">
+              <div class="action-btn" @click.stop="editContent(item)" :title="lang.ssEdit">
                 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                   <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
                   <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
@@ -104,14 +104,14 @@
               </div>
             </template>
             <template v-if="item.tool !== 76">
-              <div class="action-btn" @click.stop="copyContent(item)" title="复制">
+              <div class="action-btn" @click.stop="copyContent(item)" :title="lang.ssCopy">
                 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                   <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
                   <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
                 </svg>
               </div>
             </template>
-            <div class="action-btn delete" @click.stop="deleteContent(item)" title="删除">
+            <div class="action-btn delete" @click.stop="deleteContent(item)" :title="lang.ssDelete">
               <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                 <polyline points="3 6 5 6 21 6"/>
                 <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
@@ -130,6 +130,7 @@ import { storeToRefs } from 'pinia'
 import useCreateElement from '@/hooks/useCreateElement'
 import useSlideHandler from '@/hooks/useSlideHandler'
 import { useSlidesStore } from '@/store'
+import { lang } from '@/main'
 
 interface ContentItem {
   tool?: number
@@ -278,15 +279,15 @@ const deleteContent = (item: ContentItem) => {
 
 const getTypeLabel = (type?: number) => {
   const typeMap: Record<number, string> = {
-    45: '选择题',
-    15: '问答题',
-    72: 'AI应用',
-    73: 'H5页面',
-    74: '视频',
-    75: 'B站视频',
-    76: '创作空间'
-  }
-  return typeMap[type || 0] || '未知'
+    45: lang.ssChoiceQ,
+    15: lang.ssQATest,
+    72: lang.ssAiApp,
+    73: lang.ssHPage,
+    74: lang.ssVideo,
+    75: lang.ssBiliVideo,
+    76: lang.ssCreative,
+  }
+  return typeMap[type || 0] || lang.ssUnknown
 }
 
 const getTypeClass = (type?: number) => {

+ 4 - 2
src/components/Contextmenu/MenuContent.vue

@@ -1,5 +1,7 @@
 <template>
-  <ul class="menu-content">
+  <ul class="menu-content" 
+    :style="{width: lang.lang === 'en' ? '260px' : '180px'}"
+  >
     <template v-for="(menu, index) in menus" :key="menu.text || index">
       <li
         v-if="!menu.hide"
@@ -32,7 +34,7 @@
 
 <script lang="ts" setup>
 import type { ContextmenuItem } from './types'
-
+import { lang } from '@/main'
 defineProps<{
   menus: ContextmenuItem[]
   handleClickMenuItem: (item: ContextmenuItem) => void

+ 9 - 8
src/components/LaTeXEditor/index.vue

@@ -3,10 +3,10 @@
     <div class="container">
       <div class="left">
         <div class="input-area">
-          <TextArea v-model:value="latex" placeholder="输入 LaTeX 公式" ref="textAreaRef" />
+          <TextArea v-model:value="latex" :placeholder="lang.ssLatexPh" ref="textAreaRef" />
         </div>
         <div class="preview">
-          <div class="placeholder" v-if="!latex">公式预览</div>
+          <div class="placeholder" v-if="!latex">{{ lang.ssFormulaPrev }}</div>
           <div class="preview-content" v-else>
             <FormulaContent
               :width="518"
@@ -16,7 +16,7 @@
           </div>
         </div>
       </div>
-      <div class="right">
+      <div class="right" :style="{ width: lang.lang === 'en' ? '350px' : '280px' }">
         <Tabs 
           :tabs="tabs" 
           v-model:value="toolbarState" 
@@ -52,8 +52,8 @@
       </div>
     </div>
     <div class="footer">
-      <Button class="btn" @click="emit('close')">取消</Button>
-      <Button class="btn" type="primary" @click="update()">确定</Button>
+      <Button class="btn" @click="emit('close')">{{ lang.ssCancel }}</Button>
+      <Button class="btn" type="primary" @click="update()">{{ lang.ssInsert }}</Button>
     </div>
   </div>
 </template>
@@ -63,6 +63,7 @@ import { computed, onMounted, ref, useTemplateRef } from 'vue'
 import { hfmath } from './hfmath'
 import { FORMULA_LIST, SYMBOL_LIST } from '@/configs/latex'
 import message from '@/utils/message'
+import { lang } from '@/main'
 
 import FormulaContent from './FormulaContent.vue'
 import SymbolContent from './SymbolContent.vue'
@@ -76,8 +77,8 @@ interface TabItem {
 }
 
 const tabs: TabItem[] = [
-  { label: '常用符号', key: 'symbol' },
-  { label: '预置公式', key: 'formula' },
+  { label: lang.ssLatexSym, key: 'symbol' },
+  { label: lang.ssLatexPreset, key: 'formula' },
 ]
 
 interface LatexResult {
@@ -120,7 +121,7 @@ onMounted(() => {
 })
 
 const update = () => {
-  if (!latex.value) return message.error('公式不能为空')
+  if (!latex.value) return message.error(lang.ssLatexEmpty)
 
   const eq = new hfmath(latex.value)
   const pathd = eq.pathd({})

+ 2 - 1
src/components/Modal.vue

@@ -7,7 +7,7 @@
           @afterLeave="contentVisible = false"
           @before-enter="contentVisible = true"
         >
-          <div class="modal-content" v-show="visible" :style="contentStyle">
+          <div class="modal-content" v-show="visible" :style="lang.lang === 'en' ? { width: '950px' } : contentStyle">
             <span class="close-btn" v-if="closeButton" @click="close()"><IconClose /></span>
             <slot v-if="contentVisible"></slot>
           </div>
@@ -20,6 +20,7 @@
 <script lang="ts" setup>
 import { computed, nextTick, ref, watch, useTemplateRef, type CSSProperties } from 'vue'
 import { icons } from '@/plugins/icon'
+import { lang } from '@/main'
 
 const { IconClose } = icons
 

+ 61 - 51
src/configs/chart.ts

@@ -1,57 +1,67 @@
 import type { ChartData } from '@/types/slides'
+import { lang } from '@/main'
 
-export const CHART_TYPE_MAP: { [key: string]: string } = {
-  'bar': '柱状图',
-  'column': '条形图',
-  'line': '折线图',
-  'area': '面积图',
-  'scatter': '散点图',
-  'pie': '饼图',
-  'ring': '环形图',
-  'radar': '雷达图',
-}
+export const getChartTypeMap = (): { [key: string]: string } => ({
+  bar: lang.ssChartColumn,
+  column: lang.ssChartBar,
+  line: lang.ssChartLine,
+  area: lang.ssChartArea,
+  scatter: lang.ssChartScatter,
+  pie: lang.ssChartPie,
+  ring: lang.ssChartRing,
+  radar: lang.ssChartRadar,
+})
+
+const makeSeq = (pattern: string, n: number) =>
+  Array.from({ length: n }, (_, i) => pattern.replace(/\*/g, String(i + 1)))
+
+export const getChartDefaultData = (): { [key: string]: ChartData } => {
+  const cat = makeSeq(lang.ssChartCat, 5)
+  const ser = makeSeq(lang.ssChartSer, 2)
+  const coord = makeSeq(lang.ssCoord, 5)
 
-export const CHART_DEFAULT_DATA: { [key: string]: ChartData } = {
-  'bar': {
-    labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
-    legends: ['系列1', '系列2'],
-    series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
-  },
-  'column': {
-    labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
-    legends: ['系列1', '系列2'],
-    series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
-  },
-  'line': {
-    labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
-    legends: ['系列1', '系列2'],
-    series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
-  },
-  'pie': {
-    labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
-    legends: ['值'],
-    series: [[12, 19, 5, 2, 18]],
-  },
-  'ring': {
-    labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
-    legends: ['值'],
-    series: [[12, 19, 5, 2, 18]],
-  },
-  'area': {
-    labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
-    legends: ['系列1', '系列2'],
-    series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
-  },
-  'radar': {
-    labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
-    legends: ['系列1', '系列2'],
-    series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
-  },
-  'scatter': {
-    labels: ['坐标1', '坐标2', '坐标3', '坐标4', '坐标5'],
-    legends: ['X', 'Y'],
-    series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
-  },
+  return {
+    bar: {
+      labels: cat,
+      legends: ser,
+      series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
+    },
+    column: {
+      labels: cat,
+      legends: ser,
+      series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
+    },
+    line: {
+      labels: cat,
+      legends: ser,
+      series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
+    },
+    pie: {
+      labels: cat,
+      legends: [lang.ssValue],
+      series: [[12, 19, 5, 2, 18]],
+    },
+    ring: {
+      labels: cat,
+      legends: [lang.ssValue],
+      series: [[12, 19, 5, 2, 18]],
+    },
+    area: {
+      labels: cat,
+      legends: ser,
+      series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
+    },
+    radar: {
+      labels: cat,
+      legends: ser,
+      series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
+    },
+    scatter: {
+      labels: coord,
+      legends: [lang.ssX, lang.ssY],
+      series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
+    },
+  }
 }
 
 export const CHART_PRESET_THEMES = [

+ 14 - 12
src/configs/element.ts

@@ -1,15 +1,17 @@
-export const ELEMENT_TYPE_ZH: { [key: string]: string } = {
-  text: '文本',
-  image: '图片',
-  shape: '形状',
-  line: '线条',
-  chart: '图表',
-  table: '表格',
-  video: '视频',
-  audio: '音频',
-  latex: '公式',
-  frame: '网页',
-}
+import { lang } from '@/i18n/lang'
+
+export const getElementTypeZh = (): { [key: string]: string } => ({
+  text: lang.ssElText,
+  image: lang.ssElImage,
+  shape: lang.ssElShape,
+  line: lang.ssElLine,
+  chart: lang.ssElChart,
+  table: lang.ssElTable,
+  video: lang.ssElVideo,
+  audio: lang.ssElAudio,
+  latex: lang.ssElLatex,
+  frame: lang.ssElFrame,
+})
 
 export const MIN_SIZE: { [key: string]: number } = {
   text: 40,

+ 24 - 22
src/configs/latex.ts

@@ -1,74 +1,76 @@
+import { lang } from '@/main'
+
 export const FORMULA_LIST = [
   {
-    label: '高斯公式',
+    label: lang.ssGauss,
     latex: `\\int\\int\\int _ { \\Omega } \\left( \\frac { \\partial {P} } { \\partial {x} } + \\frac { \\partial {Q} } { \\partial {y} } + \\frac { \\partial {R} }{ \\partial {z} } \\right) \\mathrm { d } V = \\oint _ { \\partial \\Omega } ( P \\cos \\alpha + Q \\cos \\beta + R \\cos \\gamma ) \\mathrm{ d} S`
   },
   {
-    label: '傅里叶级数',
+    label: lang.ssFourier,
     latex: `f(x) = \\frac {a_0} 2 + \\sum_{n = 1}^\\infty {({a_n}\\cos {nx} + {b_n}\\sin {nx})}`,
   },
   {
-    label: '泰勒展开式',
+    label: lang.ssTaylor,
     latex: `e ^ { x } = 1 + \\frac { x } { 1 ! } + \\frac { x ^ { 2 } } { 2 ! } + \\frac { x ^ { 3 } } { 3 ! } + ... , \\quad - \\infty < x < \\infty`,
   },
   {
-    label: '定积分',
+    label: lang.ssDefInt,
     latex: `\\lim_ { n \\rightarrow + \\infty } \\sum _ { i = 1 } ^ { n } f \\left[ a + \\frac { i } { n } ( b - a ) \\right] \\frac { b - a } { n } = \\int _ { a } ^ { b } f ( x ) dx`,
   },
   {
-    label: '三角恒等式1',
+    label: lang.ssTrigIdOne,
     latex: `\\sin \\alpha \\pm \\sin \\beta = 2 \\sin \\frac { 1 } { 2 } ( \\alpha \\pm \\beta ) \\cos \\frac { 1 } { 2 } ( \\alpha \\mp \\beta )`,
   },
   {
-    label: '三角恒等式2',
+    label: lang.ssTrigIdTwo,
     latex: `\\cos \\alpha + \\cos \\beta = 2 \\cos \\frac { 1 } { 2 } ( \\alpha + \\beta ) \\cos \\frac { 1 } { 2 } ( \\alpha - \\beta )`,
   },
   {
-    label: '和的展开式',
+    label: lang.ssSumExpand,
     latex: `( 1 + x ) ^ { n } = 1 + \\frac { n x } { 1 ! } + \\frac { n ( n - 1 ) x ^ { 2 } } { 2 ! } + ...`,
   },
   {
-    label: '欧拉公式',
+    label: lang.ssEuler,
     latex: ` e^{ix} = \\cos {x} + i\\sin {x}`,
   },
   {
-    label: '贝努利方程',
+    label: lang.ssBernoulli,
     latex: `\\frac {dy} {dx} + P(x)y = Q(x) y^n ({n} \\not= {0,1})`,
   },
   {
-    label: '全微分方程',
+    label: lang.ssExactDE,
     latex: `du(x,y) = P(x,y)dx + Q(x,y)dy = 0`,
   },
   {
-    label: '非齐次方程',
+    label: lang.ssNonHom,
     latex: `y = (\\int Q(x) e^{\\int {P(x)dx}}dx + C)e^{-\\int {P(x)dx}}`,
   },
   {
-    label: '柯西中值定理',
+    label: lang.ssCauchyMVT,
     latex: `\\frac{{f(b) - f(a)}}{{F(b) - F(a)}} = \\frac{{f'(\\xi )}}{{F'(\\xi )}}`,
   },
   {
-    label: '拉格朗日中值定理',
+    label: lang.ssLagMVT,
     latex: `f(b) - f(a) = f'(\\xi )(b - a)`,
   },
   {
-    label: '导数公式',
+    label: lang.ssDerivForm,
     latex: `(\\arcsin x)' = \\frac{1}{{\\sqrt {1 - x^2} }}`,
   },
   {
-    label: '三角函数积分',
+    label: lang.ssTrigInt,
     latex: `\\int {tgxdx = - \\ln \\left| {\\cos x} \\right| + C}`,
   },
   {
-    label: '二次曲面',
+    label: lang.ssQuadSurf,
     latex: `\\frac{{{x^2}}}{{{a^2}}} + \\frac{{{y^2}}}{{{b^2}}} - \\frac{{{z^2}}}{{{c^2}}} = 1`,
   },
   {
-    label: '二阶微分',
+    label: lang.ssSecDiff,
     latex: `\\frac {{d^2}y} {dx^2} + P(x) \\frac {dy} {dx} + Q(x)y = f(x)`,
   },
   {
-    label: '方向导数',
+    label: lang.ssDirDeriv,
     latex: `\\frac{{\\partial f}}{{\\partial l}} = \\frac{{\\partial f}}{{\\partial x}}\\cos \\phi + \\frac{{\\partial f}}{{\\partial y}}\\sin \\phi`,
   },
 ]
@@ -76,7 +78,7 @@ export const FORMULA_LIST = [
 export const SYMBOL_LIST = [
   {
     type: 'operators',
-    label: '数学',
+    label: lang.ssMath,
     children: [
       { latex: '\\cdot' },
       { latex: '\\pm' },
@@ -152,7 +154,7 @@ export const SYMBOL_LIST = [
   },
   {
     type: 'group',
-    label: '组合',
+    label: lang.ssCombo,
     children: [
       { latex: '\\frac{a}{b}' },
       { latex: '\\frac{dx}{dx}' },
@@ -186,7 +188,7 @@ export const SYMBOL_LIST = [
   },
   {
     type: 'verbatim',
-    label: '函数',
+    label: lang.ssFunc,
     children: [
       { latex: '\\log' },
       { latex: '\\ln' },
@@ -215,7 +217,7 @@ export const SYMBOL_LIST = [
   },
   {
     type: 'greek',
-    label: '希腊字母',
+    label: lang.ssGreek,
     children: [
       { latex: '\\alpha' },
       { latex: '\\beta' },

+ 3 - 3
src/configs/lines.ts

@@ -1,5 +1,5 @@
 import type { LinePoint, LineStyleType } from '@/types/slides'
-
+import { lang } from '@/main'
 
 export interface LinePoolItem {
   path: string
@@ -18,7 +18,7 @@ interface PresetLine {
 
 export const LINE_LIST: PresetLine[] = [
   {
-    type: '直线',
+    type: lang.ssLineStraight,
     children: [
       { path: 'M 0 0 L 20 20', style: 'solid', points: ['', ''] },
       { path: 'M 0 0 L 20 20', style: 'dashed', points: ['', ''] },
@@ -28,7 +28,7 @@ export const LINE_LIST: PresetLine[] = [
     ],
   },
   {
-    type: '折线、曲线',
+    type: lang.ssLinePolyCurve,
     children: [
       { path: 'M 0 0 L 0 20 L 20 20', style: 'solid', points: ['', 'arrow'], isBroken: true },
       { path: 'M 0 0 L 10 0 L 10 20 L 20 20', style: 'solid', points: ['', 'arrow'], isBroken2: true },

+ 6 - 6
src/configs/shapes.ts

@@ -1,8 +1,8 @@
 /* eslint-disable max-lines */
 
 // 非专业设计人士可以用该应用绘制基本形状:https://github.com/pipipi-pikachu/svgPathCreator
-
 import { ShapePathFormulasKeys } from '@/types/slides'
+import { lang } from '@/i18n/lang'
 
 export interface ShapePoolItem {
   viewBox: [number, number]
@@ -247,7 +247,7 @@ export const SHAPE_PATH_FORMULAS: {
 
 export const SHAPE_LIST: ShapeListItem[] = [
   {
-    type: '矩形',
+    type: lang.ssShapeRect,
     children: [
       {
         viewBox: [200, 200],
@@ -306,7 +306,7 @@ export const SHAPE_LIST: ShapeListItem[] = [
   },
 
   {
-    type: '常用形状',
+    type: lang.ssShapeCommon,
     children: [
       {
         viewBox: [200, 200],
@@ -637,7 +637,7 @@ export const SHAPE_LIST: ShapeListItem[] = [
   },
   
   {
-    type: '箭头',
+    type: lang.ssShapeArrow,
     children: [
       {
         viewBox: [200, 200],
@@ -755,7 +755,7 @@ export const SHAPE_LIST: ShapeListItem[] = [
   },
 
   {
-    type: '其他形状',
+    type: lang.ssShapeOther,
     children: [
       {
         viewBox: [1024, 1024],
@@ -941,7 +941,7 @@ export const SHAPE_LIST: ShapeListItem[] = [
   },
 
   {
-    type: '线性',
+    type: lang.ssShapeLinear,
     children: [
       {
         viewBox: [1024, 1024],

+ 4 - 2
src/hooks/useCreateElement.ts

@@ -3,10 +3,11 @@ import { nanoid } from 'nanoid'
 import { useMainStore, useSlidesStore } from '@/store'
 import { getImageSize } from '@/utils/image'
 import message from '@/utils/message'
+import { lang } from '@/main'
 import type { PPTLineElement, PPTElement, TableCell, TableCellStyle, PPTShapeElement, ChartType } from '@/types/slides'
 import { type ShapePoolItem, SHAPE_PATH_FORMULAS } from '@/configs/shapes'
 import type { LinePoolItem } from '@/configs/lines'
-import { CHART_DEFAULT_DATA } from '@/configs/chart'
+import { getChartDefaultData } from '@/configs/chart'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
 
 interface CommonElementPosition {
@@ -88,6 +89,7 @@ export default () => {
    * @param chartType 图表类型
    */
   const createChartElement = (type: ChartType) => {
+    const CHART_DEFAULT_DATA = getChartDefaultData()
     createElement({
       type: 'chart',
       id: nanoid(10),
@@ -322,7 +324,7 @@ export default () => {
     const hasWebpage = currentSlide.value?.elements?.some(element => element.type === 'frame')
     
     if (hasWebpage) {
-      message.error('当前幻灯片已有学习内容,一个幻灯片只能插入一个学习内容')
+      message.error(lang.ssSlideLearn)
       return
     }
     

Разница между файлами не показана из-за своего большого размера
+ 621 - 753
src/hooks/useImport.ts


+ 3 - 2
src/hooks/useLink.ts

@@ -2,6 +2,7 @@ import { useSlidesStore } from '@/store'
 import type { PPTElement, PPTElementLink } from '@/types/slides'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
 import message from '@/utils/message'
+import { lang } from '@/main'
 
 export default () => {
   const slidesStore = useSlidesStore()
@@ -11,11 +12,11 @@ export default () => {
   const setLink = (handleElement: PPTElement, link: PPTElementLink) => {
     const linkRegExp = /^(https?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/
     if (link.type === 'web' && !linkRegExp.test(link.target)) {
-      message.error('不是正确的网页链接地址')
+      message.error(lang.ssWebUrlInvalid)
       return false
     }
     if (link.type === 'slide' && !link.target) {
-      message.error('请先选择链接目标')
+      message.error(lang.ssPickLinkTarget)
       return false
     }
     const props = { link }

+ 4 - 3
src/hooks/useSearch.ts

@@ -3,6 +3,7 @@ import { storeToRefs } from 'pinia'
 import { useMainStore, useSlidesStore } from '@/store'
 import type { PPTTableElement } from '@/types/slides'
 import message from '@/utils/message'
+import { lang } from '@/main'
 
 interface SearchTextResult {
   elType: 'text' | 'shape'
@@ -83,7 +84,7 @@ export default () => {
       highlightCurrentSlide()
     }
     else {
-      message.warning('未查找到匹配项')
+      message.warning(lang.ssNoMatch)
       clearMarks()
     }
   }
@@ -229,7 +230,7 @@ export default () => {
   }
   
   const searchNext = () => {
-    if (!searchWord.value) return message.warning('请先输入查找内容')
+    if (!searchWord.value) return message.warning(lang.ssFindInput)
     mainStore.setActiveElementIdList([])
     if (searchIndex.value === -1) search()
     else if (searchIndex.value < searchResults.value.length - 1) searchIndex.value += 1
@@ -238,7 +239,7 @@ export default () => {
   }
   
   const searchPrev = () => {
-    if (!searchWord.value) return message.warning('请先输入查找内容')
+    if (!searchWord.value) return message.warning(lang.ssFindInput)
     mainStore.setActiveElementIdList([])
     if (searchIndex.value === -1) search()
     else if (searchIndex.value > 0) searchIndex.value -= 1

+ 9 - 0
src/i18n/lang.ts

@@ -0,0 +1,9 @@
+import en from '@/views/lang/en.json'
+import cn from '@/views/lang/cn.json'
+import hk from '@/views/lang/hk.json'
+
+const href = window.location.href.toLowerCase()
+export const lang =
+  href.includes('cocorobo.com') ? en
+  : href.includes('cocorobo.hk') ? hk
+  : en

+ 2 - 16
src/main.ts

@@ -1,23 +1,9 @@
 import { createApp } from 'vue'
 import { createPinia } from 'pinia'
 import App from './App.vue'
-import en from './views/lang/en.json'
-import cn from './views/lang/cn.json'
-import hk from './views/lang/hk.json'
+import { lang } from '@/i18n/lang'
 
-export let lang = cn
-if (window.location.href.includes('cocorobo.cn')) {
-  lang = cn
-}
-else if (window.location.href.includes('cocorobo.hk')) {
-  lang = hk
-}
-else if (window.location.href.includes('cocorobo.com')) {
-  lang = en
-}
-else {
-  lang = cn
-}
+export { lang }
 
 // TypeScript declarations for global properties
 declare module '@vue/runtime-core' {

+ 41 - 40
src/views/Editor/AIPPTDialog.vue

@@ -2,9 +2,9 @@
   <div class="aippt-dialog">
     <div class="header">
       <span class="title">AIPPT</span>
-      <span class="subtite" v-if="step === 'template'">从下方挑选合适的模板,开始生成PPT</span>
-      <span class="subtite" v-else-if="step === 'outline'">确认下方内容大纲(点击编辑内容,右键添加/删除大纲项),开始选择模板</span>
-      <span class="subtite" v-else>在下方输入您的PPT主题,并适当补充信息,如行业、岗位、学科、用途等</span>
+      <span class="subtite" v-if="step === 'template'">{{ lang.ssAiSubTpl }}</span>
+      <span class="subtite" v-else-if="step === 'outline'">{{ lang.ssAiSubOut }}</span>
+      <span class="subtite" v-else>{{ lang.ssAiSubSet }}</span>
     </div>
     
     <template v-if="step === 'setup'">
@@ -12,12 +12,12 @@
         ref="inputRef"
         v-model:value="keyword" 
         :maxlength="50" 
-        placeholder="请输入PPT主题,如:大学生职业生涯规划" 
+        :placeholder="lang.ssAiPhTopic" 
         @enter="createOutline()"
       >
         <template #suffix>
           <span class="count">{{ keyword.length }} / 50</span>
-          <div class="submit" type="primary" @click="createOutline()"><IconSend class="icon" /> AI 生成</div>
+          <div class="submit" type="primary" @click="createOutline()"><IconSend class="icon" /> {{ lang.ssAiGen }}</div>
         </template>
       </Input>
       <div class="recommends">
@@ -25,35 +25,35 @@
       </div>
       <div class="configs">
         <div class="config-item">
-          <div class="label">语言:</div>
+          <div class="label">{{ lang.ssLangColon }}</div>
           <Select 
             class="config-content"
             style="width: 80px;"
             v-model:value="language"
             :options="[
-              { label: '中文', value: '中文' },
-              { label: '英文', value: 'English' },
-              { label: '日文', value: '日本語' },
+              { label: lang.ssLangZh, value: lang.ssLangZhV },
+              { label: lang.ssLangEn, value: 'English' },
+              { label: lang.ssLangJa, value: '日本語' },
             ]"
           />
         </div>
         <div class="config-item">
-          <div class="label">风格:</div>
+          <div class="label">{{ lang.ssStyleColon }}</div>
           <Select 
             class="config-content"
             style="width: 80px;"
             v-model:value="style"
             :options="[
-              { label: '通用', value: '通用' },
-              { label: '学术风', value: '学术风' },
-              { label: '职场风', value: '职场风' },
-              { label: '教育风', value: '教育风' },
-              { label: '营销风', value: '营销风' },
+              { label: lang.ssStyGen, value: lang.ssStyGenV },
+              { label: lang.ssStyAcad, value: lang.ssStyAcadV },
+              { label: lang.ssStyWork, value: lang.ssStyWorkV },
+              { label: lang.ssStyEdu, value: lang.ssStyEduV },
+              { label: lang.ssStyMkt, value: lang.ssStyMktV },
             ]"
           />
         </div>
         <div class="config-item">
-          <div class="label">模型:</div>
+          <div class="label">{{ lang.ssModelColon }}</div>
           <Select 
             class="config-content"
             style="width: 190px;"
@@ -67,16 +67,16 @@
           />
         </div>
         <div class="config-item">
-          <div class="label">配图:</div>
+          <div class="label">{{ lang.ssImgColon }}</div>
           <Select 
             class="config-content"
             style="width: 100px;"
             v-model:value="img"
             :options="[
-              { label: '无', value: '' },
-              { label: '模拟测试', value: 'test' },
-              { label: 'AI搜图', value: 'ai-search', disabled: true },
-              { label: 'AI生图', value: 'ai-create', disabled: true },
+              { label: lang.ssNone, value: '' },
+              { label: lang.ssMockTest, value: 'test' },
+              { label: lang.ssAiSearch, value: 'ai-search', disabled: true },
+              { label: lang.ssAiImgGen, value: 'ai-create', disabled: true },
             ]"
           />
         </div>
@@ -88,8 +88,8 @@
          <OutlineEditor v-model:value="outline" />
        </div>
       <div class="btns" v-if="!outlineCreating">
-        <Button class="btn" type="primary" @click="step = 'template'">选择模板</Button>
-        <Button class="btn" @click="outline = ''; step = 'setup'">返回重新生成</Button>
+        <Button class="btn" type="primary" @click="step = 'template'">{{ lang.ssAiChooseTpl }}</Button>
+        <Button class="btn" @click="outline = ''; step = 'setup'">{{ lang.ssAiBackRe }}</Button>
       </div>
     </div>
     <div class="select-template" v-if="step === 'template'">
@@ -104,12 +104,12 @@
         </div>
       </div>
       <div class="btns">
-        <Button class="btn" type="primary" @click="createPPT()">生成</Button>
-        <Button class="btn" @click="step = 'outline'">返回大纲</Button>
+        <Button class="btn" type="primary" @click="createPPT()">{{ lang.ssAiMake }}</Button>
+        <Button class="btn" @click="step = 'outline'">{{ lang.ssAiBackOut }}</Button>
       </div>
     </div>
 
-    <FullscreenSpin :loading="loading" tip="AI生成中,请耐心等待 ..." />
+    <FullscreenSpin :loading="loading" :tip="lang.ssAiWait" />
   </div>
 </template>
 
@@ -127,14 +127,15 @@ import Button from '@/components/Button.vue'
 import Select from '@/components/Select.vue'
 import FullscreenSpin from '@/components/FullscreenSpin.vue'
 import OutlineEditor from '@/components/OutlineEditor.vue'
+import { lang } from '@/main'
 
 const mainStore = useMainStore()
 const slideStore = useSlidesStore()
 const { templates } = storeToRefs(slideStore)
 const { AIPPT, presetImgPool, getMdContent } = useAIPPT()
 
-const language = ref('中文')
-const style = ref('通用')
+const language = ref(lang.ssLangZhV)
+const style = ref(lang.ssStyGenV)
 const img = ref('')
 const keyword = ref('')
 const outline = ref('')
@@ -147,17 +148,17 @@ const outlineRef = useTemplateRef<HTMLElement>('outlineRef')
 const inputRef = useTemplateRef<InstanceType<typeof Input>>('inputRef')
 
 const recommends = ref([
-  '2025科技前沿动态',
-  '大数据如何改变世界',
-  '餐饮市场调查与研究',
-  'AIGC在教育领域的应用',
-  '社交媒体与品牌营销',
-  '5G技术如何改变我们的生活',
-  '年度工作总结与展望',
-  '区块链技术及其应用',
-  '大学生职业生涯规划',
-  '公司年会策划方案',
-]) 
+  lang.ssAiRecA,
+  lang.ssAiRecB,
+  lang.ssAiRecC,
+  lang.ssAiRecD,
+  lang.ssAiRecE,
+  lang.ssAiRecF,
+  lang.ssAiRecG,
+  lang.ssAiRecH,
+  lang.ssAiRecI,
+  lang.ssAiRecJ,
+])
 
 onMounted(() => {
   setTimeout(() => {
@@ -171,7 +172,7 @@ const setKeyword = (value: string) => {
 }
 
 const createOutline = async () => {
-  if (!keyword.value) return message.error('请先输入PPT主题')
+  if (!keyword.value) return message.error(lang.ssAiNeedTopic)
 
   loading.value = true
   outlineCreating.value = true

+ 29 - 37
src/views/Editor/Canvas/EditableElement.vue

@@ -32,6 +32,7 @@ import useSelectElement from '@/hooks/useSelectElement'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
 import { useSlidesStore } from '@/store'
 import message from '@/utils/message'
+import { lang } from '@/main'
 
 import { ElementOrderCommands, ElementAlignCommands } from '@/types/edit'
 
@@ -82,90 +83,90 @@ const { selectAllElements } = useSelectElement()
 const contextmenus = (): ContextmenuItem[] => {
   if (props.elementInfo.lock) {
     return [{
-      text: '解锁', 
+      text: lang.ssUnlock, 
       handler: () => unlockElement(props.elementInfo),
     }]
   }
 
   const baseMenu = [
     {
-      text: '剪切',
+      text: lang.ssCut,
       subText: 'Ctrl + X',
       handler: cutElement,
     },
     {
-      text: '复制',
+      text: lang.ssCopy,
       subText: 'Ctrl + C',
       handler: copyElement,
     },
     {
-      text: '粘贴',
+      text: lang.ssPaste,
       subText: 'Ctrl + V',
       handler: pasteElement,
     },
     { divider: true },
     {
-      text: '水平居中',
+      text: lang.ssAlignHCenter,
       handler: () => alignElementToCanvas(ElementAlignCommands.HORIZONTAL),
       children: [
-        { text: '水平垂直居中', handler: () => alignElementToCanvas(ElementAlignCommands.CENTER), },
-        { text: '水平居中', handler: () => alignElementToCanvas(ElementAlignCommands.HORIZONTAL) },
-        { text: '左对齐', handler: () => alignElementToCanvas(ElementAlignCommands.LEFT) },
-        { text: '右对齐', handler: () => alignElementToCanvas(ElementAlignCommands.RIGHT) },
+        { text: lang.ssAlignHVCenter, handler: () => alignElementToCanvas(ElementAlignCommands.CENTER), },
+        { text: lang.ssAlignHCenter, handler: () => alignElementToCanvas(ElementAlignCommands.HORIZONTAL) },
+        { text: lang.ssAlignLeft, handler: () => alignElementToCanvas(ElementAlignCommands.LEFT) },
+        { text: lang.ssAlignRight, handler: () => alignElementToCanvas(ElementAlignCommands.RIGHT) },
       ],
     },
     {
-      text: '垂直居中',
+      text: lang.ssAlignVCenter,
       handler: () => alignElementToCanvas(ElementAlignCommands.VERTICAL),
       children: [
-        { text: '水平垂直居中', handler: () => alignElementToCanvas(ElementAlignCommands.CENTER) },
-        { text: '垂直居中', handler: () => alignElementToCanvas(ElementAlignCommands.VERTICAL) },
-        { text: '顶部对齐', handler: () => alignElementToCanvas(ElementAlignCommands.TOP) },
-        { text: '底部对齐', handler: () => alignElementToCanvas(ElementAlignCommands.BOTTOM) },
+        { text: lang.ssAlignHVCenter, handler: () => alignElementToCanvas(ElementAlignCommands.CENTER) },
+        { text: lang.ssAlignVCenter, handler: () => alignElementToCanvas(ElementAlignCommands.VERTICAL) },
+        { text: lang.ssAlignTop, handler: () => alignElementToCanvas(ElementAlignCommands.TOP) },
+        { text: lang.ssAlignBottom, handler: () => alignElementToCanvas(ElementAlignCommands.BOTTOM) },
       ],
     },
     { divider: true },
     {
-      text: '置于顶层',
+      text: lang.ssBringFront,
       disable: props.isMultiSelect && !props.elementInfo.groupId,
       handler: () => orderElement(props.elementInfo, ElementOrderCommands.TOP),
       children: [
-        { text: '置于顶层', handler: () => orderElement(props.elementInfo, ElementOrderCommands.TOP) },
-        { text: '上移一层', handler: () => orderElement(props.elementInfo, ElementOrderCommands.UP) },
+        { text: lang.ssBringFront, handler: () => orderElement(props.elementInfo, ElementOrderCommands.TOP) },
+        { text: lang.ssBringForward, handler: () => orderElement(props.elementInfo, ElementOrderCommands.UP) },
       ],
     },
     {
-      text: '置于底层',
+      text: lang.ssSendBack,
       disable: props.isMultiSelect && !props.elementInfo.groupId,
       handler: () => orderElement(props.elementInfo, ElementOrderCommands.BOTTOM),
       children: [
-        { text: '置于底层', handler: () => orderElement(props.elementInfo, ElementOrderCommands.BOTTOM) },
-        { text: '下移一层', handler: () => orderElement(props.elementInfo, ElementOrderCommands.DOWN) },
+        { text: lang.ssSendBack, handler: () => orderElement(props.elementInfo, ElementOrderCommands.BOTTOM) },
+        { text: lang.ssSendBackward, handler: () => orderElement(props.elementInfo, ElementOrderCommands.DOWN) },
       ],
     },
     { divider: true },
     {
-      text: '设置链接',
+      text: lang.ssSetLink,
       handler: props.openLinkDialog,
     },
     {
-      text: props.elementInfo.groupId ? '取消组合' : '组合',
+      text: props.elementInfo.groupId ? lang.ssUngroupEl : lang.ssGroupEl,
       subText: 'Ctrl + G',
       handler: props.elementInfo.groupId ? uncombineElements : combineElements,
       hide: !props.isMultiSelect,
     },
     {
-      text: '全选',
+      text: lang.ssSelectAll,
       subText: 'Ctrl + A',
       handler: selectAllElements,
     },
     {
-      text: '锁定',
+      text: lang.ssLock,
       subText: 'Ctrl + L',
       handler: lockElement,
     },
     {
-      text: '删除',
+      text: lang.ssDelete,
       subText: 'Delete',
       handler: deleteElement,
     },
@@ -174,17 +175,8 @@ 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: '在新窗口打开',
+        text: lang.ssOpenNewWin,
         handler: () => {
           const frameElement = props.elementInfo as any
           if (frameElement.url) {
@@ -193,7 +185,7 @@ const contextmenus = (): ContextmenuItem[] => {
         },
       },
       {
-        text: '复制链接',
+        text: lang.ssCopyLink,
         handler: () => {
           const frameElement = props.elementInfo as any
           if (frameElement.url) {
@@ -204,7 +196,7 @@ const contextmenus = (): ContextmenuItem[] => {
       { divider: true },
     ]
     // 为网页元素过滤掉"设置链接"功能
-    const filteredBaseMenu = baseMenu.filter(item => item.text !== '设置链接')
+    const filteredBaseMenu = baseMenu.filter(item => item.text !== lang.ssSetLink)
     return [...frameMenu, ...filteredBaseMenu]
   }
 

+ 8 - 7
src/views/Editor/Canvas/LinkDialog.vue

@@ -11,7 +11,7 @@
       ref="inputRef"
       v-if="type === 'web'" 
       v-model:value="address" 
-      placeholder="请输入网页链接地址"
+      :placeholder="lang.ssWebUrlPh"
       @enter="save()"
     />
 
@@ -23,13 +23,13 @@
     />
 
     <div class="preview" v-if="type === 'slide' && selectedSlide">
-      <div>预览:</div>
+      <div>{{ lang.ssPreview }}:</div>
       <ThumbnailSlide class="thumbnail" :slide="selectedSlide" :size="500" />
     </div>
 
     <div class="btns">
-      <Button @click="emit('close')" style="margin-right: 10px;">取消</Button>
-      <Button type="primary" @click="save()">确认</Button>
+      <Button @click="emit('close')" style="margin-right: 10px;">{{ lang.ssCancel }}</Button>
+      <Button type="primary" @click="save()">{{ lang.ssConfirm }}</Button>
     </div>
   </div>
 </template>
@@ -40,6 +40,7 @@ import { storeToRefs } from 'pinia'
 import { useMainStore, useSlidesStore } from '@/store'
 import type { ElementLinkType, PPTElementLink } from '@/types/slides'
 import useLink from '@/hooks/useLink'
+import { lang } from '@/main'
 
 import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
 import Tabs from '@/components/Tabs.vue'
@@ -68,7 +69,7 @@ const inputRef = useTemplateRef<InstanceType<typeof Input>>('inputRef')
 
 const slideOptions = computed(() => {
   return slides.value.map((item, index) => ({
-    label: `幻灯片 ${index + 1}`,
+    label: lang.ssSlideNum.replace(/\*/g, String(index + 1)),
     value: item.id,
     disabled: currentSlide.value.id === item.id,
   }))
@@ -83,8 +84,8 @@ const selectedSlide = computed(() => {
 })
 
 const tabs: TabItem[] = [
-  { key: 'web', label: '网页链接' },
-  { key: 'slide', label: '幻灯片页面' },
+  { key: 'web', label: lang.ssTabWebLink },
+  { key: 'slide', label: lang.ssTabSlidePg },
 ]
 
 const { setLink } = useLink()

+ 4 - 3
src/views/Editor/Canvas/Operate/LinkHandler.vue

@@ -1,11 +1,11 @@
 <template>
   <div class="link-handler" :style="{ top: height * canvasScale + 10 + 'px' }">
     <a class="link" v-if="link.type === 'web'" :href="link.target" target="_blank">{{link.target}}</a>
-    <a class="link" v-else @click="turnTarget(link.target)">幻灯片页面 {{link.target}}</a>
+    <a class="link" v-else @click="turnTarget(link.target)">{{ lang.ssSlidePg.replace(/\*/g, link.target) }}</a>
     <div class="btns">
-      <div class="btn" @click="openLinkDialog()">更换</div>
+      <div class="btn" @click="openLinkDialog()">{{ lang.ssReplace }}</div>
       <Divider type="vertical" />
-      <div class="btn" @click="removeLink(elementInfo)">移除</div>
+      <div class="btn" @click="removeLink(elementInfo)">{{ lang.ssRemove }}</div>
     </div>
   </div>
 </template>
@@ -16,6 +16,7 @@ import { storeToRefs } from 'pinia'
 import { useMainStore, useSlidesStore } from '@/store'
 import type { PPTElement, PPTElementLink } from '@/types/slides'
 import useLink from '@/hooks/useLink'
+import { lang } from '@/main'
 import Divider from '@/components/Divider.vue'
 
 const props = defineProps<{

+ 2 - 1
src/views/Editor/Canvas/ShapeCreateCanvas.vue

@@ -24,6 +24,7 @@ import { useKeyboardStore, useMainStore, useSlidesStore } from '@/store'
 import type { CreateCustomShapeData } from '@/types/edit'
 import { KEYS } from '@/configs/hotkey'
 import message from '@/utils/message'
+import { lang } from '@/main'
 
 const emit = defineEmits<{
   (event: 'created', payload: CreateCustomShapeData): void
@@ -165,7 +166,7 @@ const keydownListener = (e: KeyboardEvent) => {
   if (key === KEYS.ENTER) create()
 }
 onMounted(() => {
-  message.success('点击绘制任意形状,首尾闭合完成绘制,按 ESC 键或鼠标右键取消,按 ENTER 键提前完成', {
+  message.success(lang.ssShapeHint, {
     duration: 0,
   })
   document.addEventListener('keydown', keydownListener)

+ 7 - 6
src/views/Editor/Canvas/WebpageLinkEditDialog.vue

@@ -1,18 +1,18 @@
 <template>
   <div class="webpage-link-edit-dialog">
-    <div class="title">修改网页链接</div>
+    <div class="title">{{ lang.ssEditWebLink }}</div>
     
     <Input 
       class="input"
       ref="inputRef"
       v-model:value="url" 
-      placeholder="请输入网页链接地址"
+      :placeholder="lang.ssWebUrlPh"
       @enter="save()"
     />
 
     <div class="btns">
-      <Button @click="emit('close')" style="margin-right: 10px;">取消</Button>
-      <Button type="primary" @click="save()">确认</Button>
+      <Button @click="emit('close')" style="margin-right: 10px;">{{ lang.ssCancel }}</Button>
+      <Button type="primary" @click="save()">{{ lang.ssConfirm }}</Button>
     </div>
   </div>
 </template>
@@ -22,6 +22,7 @@ import { onMounted, ref, nextTick } from 'vue'
 import { useSlidesStore } from '@/store'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
 import message from '@/utils/message'
+import { lang } from '@/main'
 
 import Input from '@/components/Input.vue'
 import Button from '@/components/Button.vue'
@@ -49,7 +50,7 @@ onMounted(() => {
 
 const save = () => {
   if (!url.value) {
-    message.error('请输入网页链接地址')
+    message.error(lang.ssWebUrlReq)
     return
   }
 
@@ -58,7 +59,7 @@ const save = () => {
     new URL(url.value)
   }
   catch {
-    message.error('请输入正确的网页链接格式')
+    message.error(lang.ssWebUrlInvalid)
     return
   }
 

+ 15 - 14
src/views/Editor/Canvas/index.vue

@@ -12,7 +12,7 @@
     <div v-if="isCourseLoading" class="fullscreen-loading-overlay">
       <div class="loading-content">
         <div class="loading-spinner"></div>
-        <div class="loading-text">正在加载课程内容...</div>
+        <div class="loading-text">{{ lang.ssCourseLoading }}</div>
       </div>
     </div>
 
@@ -162,6 +162,7 @@ import Modal from '@/components/Modal.vue'
 import api from '@/services/course'
 import useImport from '@/hooks/useImport'
 import message from '@/utils/message'
+import { lang } from '@/main'
 
 
 // 定义组件props
@@ -271,7 +272,7 @@ const getCourseDetail = async () => {
     console.log(res)
     const courseDetail = res[0][0]
     emit('courseLoaded', courseDetail)
-    
+
     const pptJSONUrl = JSON.parse(courseDetail.chapters).pptData ? JSON.parse(courseDetail.chapters).pptData : ''
     console.log(pptJSONUrl)
     
@@ -293,7 +294,7 @@ const getCourseDetail = async () => {
         }
         catch (e) {
           console.error('解析pptdata.data失败:', e)
-          message.error('解析PPT数据失败')
+          message.error(lang.ssParsePptFail)
           if (typeof window !== 'undefined') {
             const win = window as any
             win.pptLoading = 2
@@ -310,7 +311,7 @@ const getCourseDetail = async () => {
   }
   catch (error) {
     console.error('获取课程详情失败:', error)
-    message.error('获取课程详情失败')
+    message.error(lang.ssFetchCourseFail)
     isCourseLoading.value = false
     if (typeof window !== 'undefined') {
       const win = window as any
@@ -417,53 +418,53 @@ const insertCustomShape = (data: CreateCustomShapeData) => {
 const contextmenus = (): ContextmenuItem[] => {
   return [
     {
-      text: '粘贴',
+      text: lang.ssPaste,
       subText: 'Ctrl + V',
       handler: pasteElement,
     },
     {
-      text: '全选',
+      text: lang.ssSelectAll,
       subText: 'Ctrl + A',
       handler: selectAllElements,
     },
     {
-      text: '标尺',
+      text: lang.ssRuler,
       subText: showRuler.value ? '√' : '',
       handler: toggleRuler,
     },
     {
-      text: '网格线',
+      text: lang.ssGridLine,
       handler: () => mainStore.setGridLineSize(gridLineSize.value ? 0 : 50),
       children: [
         {
-          text: '无',
+          text: lang.ssNone,
           subText: gridLineSize.value === 0 ? '√' : '',
           handler: () => mainStore.setGridLineSize(0),
         },
         {
-          text: '小',
+          text: lang.ssSmall,
           subText: gridLineSize.value === 25 ? '√' : '',
           handler: () => mainStore.setGridLineSize(25),
         },
         {
-          text: '中',
+          text: lang.ssMedium,
           subText: gridLineSize.value === 50 ? '√' : '',
           handler: () => mainStore.setGridLineSize(50),
         },
         {
-          text: '大',
+          text: lang.ssLarge,
           subText: gridLineSize.value === 100 ? '√' : '',
           handler: () => mainStore.setGridLineSize(100),
         },
       ],
     },
     {
-      text: '重置当前页',
+      text: lang.ssResetPage,
       handler: deleteAllElements,
     },
     { divider: true },
     {
-      text: '幻灯片放映',
+      text: lang.ssStage,
       subText: 'F5',
       handler: enterScreeningFromStart,
     },

+ 4 - 2
src/views/Editor/CanvasTool/ChartPool.vue

@@ -1,5 +1,5 @@
 <template>
-  <ul class="chart-pool">
+  <ul class="chart-pool" :style="{width: lang.lang === 'en' ? '365px' : '240px'}">
     <li class="chart-item" v-for="(chart, index) in chartList" :key="index">
       <div class="chart-content" @click="selectChart(chart)">
         <IconChartLine size="24" v-if="chart === 'line'" />
@@ -19,7 +19,9 @@
 
 <script lang="ts" setup>
 import type { ChartType } from '@/types/slides'
-import { CHART_TYPE_MAP } from '@/configs/chart'
+import { getChartTypeMap } from '@/configs/chart'
+import { lang } from '@/main'
+const CHART_TYPE_MAP = getChartTypeMap()
 
 const emit = defineEmits<{
   (event: 'select', payload: ChartType): void

+ 11 - 10
src/views/Editor/CanvasTool/MediaInput.vue

@@ -7,18 +7,18 @@
     />
 
     <template v-if="type === 'video'">
-      <Input v-model:value="videoSrc" placeholder="请输入视频地址,e.g. https://xxx.mp4"></Input>
+      <Input v-model:value="videoSrc" :placeholder="lang.ssVideoUrlPh"></Input>
       <div class="btns">
-        <Button @click="emit('close')" style="margin-right: 10px;">取消</Button>
-        <Button type="primary" @click="insertVideo()">确认</Button>
+        <Button @click="emit('close')" style="margin-right: 10px;">{{ lang.ssCancel }}</Button>
+        <Button type="primary" @click="insertVideo()">{{ lang.ssInsert }}</Button>
       </div>
     </template>
 
     <template v-if="type === 'audio'">
-      <Input v-model:value="audioSrc" placeholder="请输入音频地址,e.g. https://xxx.mp3"></Input>
+      <Input v-model:value="audioSrc" :placeholder="lang.ssAudioUrlPh"></Input>
       <div class="btns">
-        <Button @click="emit('close')" style="margin-right: 10px;">取消</Button>
-        <Button type="primary" @click="insertAudio()">确认</Button>
+        <Button @click="emit('close')" style="margin-right: 10px;">{{ lang.ssCancel }}</Button>
+        <Button type="primary" @click="insertAudio()">{{ lang.ssConfirm }}</Button>
       </div>
     </template>
   </div>
@@ -30,6 +30,7 @@ import message from '@/utils/message'
 import Tabs from '@/components/Tabs.vue'
 import Input from '@/components/Input.vue'
 import Button from '@/components/Button.vue'
+import { lang } from '@/main'
 
 type TypeKey = 'video' | 'audio'
 interface TabItem {
@@ -49,17 +50,17 @@ const videoSrc = ref('https://asset.pptist.cn/video/example.webm')
 const audioSrc = ref('https://asset.pptist.cn/audio/example.mp3')
 
 const tabs: TabItem[] = [
-  { key: 'video', label: '视频' },
-  { key: 'audio', label: '音频' },
+  { key: 'video', label: lang.ssVideo },
+  { key: 'audio', label: lang.ssAudio },
 ]
 
 const insertVideo = () => {
-  if (!videoSrc.value) return message.error('请先输入正确的视频地址')
+  if (!videoSrc.value) return message.error(lang.ssVideoUrlReq)
   emit('insertVideo', videoSrc.value)
 }
 
 const insertAudio = () => {
-  if (!audioSrc.value) return message.error('请先输入正确的音频地址')
+  if (!audioSrc.value) return message.error(lang.ssAudioUrlReq)
   emit('insertAudio', audioSrc.value)
 }
 </script>

+ 9 - 8
src/views/Editor/CanvasTool/TableGenerator.vue

@@ -1,8 +1,8 @@
 <template>
   <div class="table-generator">
     <div class="title">
-      <div class="lef">表格 {{endCell.length ? `${endCell[0]} x ${endCell[1]}` : ''}}</div>
-      <div class="right" @click="isCustom = !isCustom">{{ isCustom ? '返回' : '自定义'}}</div>
+      <div class="lef">{{ lang.ssTable }} {{endCell.length ? `${endCell[0]} x ${endCell[1]}` : ''}}</div>
+      <div class="right" @click="isCustom = !isCustom">{{ isCustom ? lang.ssBack : lang.ssCustom }}</div>
     </div>
     <table 
       @mouseleave="endCell = []" 
@@ -26,7 +26,7 @@
 
     <div class="custom" v-else>
       <div class="row">
-        <div class="label" style="width: 25%;">行数:</div>
+        <div class="label" style="width: 25%;">{{ lang.ssRowCnt }}</div>
         <NumberInput
           :min="1"
           :max="20"
@@ -35,7 +35,7 @@
         />
       </div>
       <div class="row">
-        <div class="label" style="width: 25%;">列数:</div>
+        <div class="label" style="width: 25%;">{{ lang.ssColCnt }}</div>
         <NumberInput
           :min="1"
           :max="20"
@@ -44,8 +44,8 @@
         />
       </div>
       <div class="btns">
-        <Button class="btn" @click="close()">取消</Button>
-        <Button class="btn" type="primary" @click="insertCustomTable()">确认</Button>
+        <Button class="btn" @click="close()">{{ lang.ssCancel }}</Button>
+        <Button class="btn" type="primary" @click="insertCustomTable()">{{ lang.ssInsert }}</Button>
       </div>
     </div>
   </div>
@@ -56,6 +56,7 @@ import { ref } from 'vue'
 import message from '@/utils/message'
 import Button from '@/components/Button.vue'
 import NumberInput from '@/components/NumberInput.vue'
+import { lang } from '@/main'
 
 interface InsertData {
   row: number
@@ -79,8 +80,8 @@ const handleClickTable = () => {
 }
 
 const insertCustomTable = () => {
-  if (customRow.value < 1 || customRow.value > 20) return message.warning('行数/列数必须在0~20之间!')
-  if (customCol.value < 1 || customCol.value > 20) return message.warning('行数/列数必须在0~20之间!')
+  if (customRow.value < 1 || customRow.value > 20) return message.warning(lang.ssTblRangeWarn)
+  if (customCol.value < 1 || customCol.value > 20) return message.warning(lang.ssTblRangeWarn)
   emit('insert', { row: customRow.value, col: customCol.value })
   isCustom.value = false
 }

+ 16 - 15
src/views/Editor/CanvasTool/WebpageInput.vue

@@ -1,13 +1,13 @@
 <template>
   <div class="webpage-input">
-    <div class="title">插入学习内容</div>
-    <div class="description">请选择要嵌入的学习内容</div>
+    <div class="title">{{ lang.ssInsertLearn }}</div>
+    <div class="description">{{ lang.ssPickLearn }}</div>
     
     <!-- 当列表为空时显示上传提示 -->
     <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 class="empty-title">{{ lang.ssNoLearn }}</div>
+      <div class="empty-desc">{{ lang.ssNeedUpload }}</div>
     </div>
     
     <!-- 当有内容时显示列表 -->
@@ -33,14 +33,14 @@
     </div>
     
     <div class="btns">
-      <Button @click="emit('close')" style="margin-right: 10px;">取消</Button>
+      <Button @click="emit('close')" style="margin-right: 10px;">{{ lang.ssCancel }}</Button>
       <Button 
         v-if="webpageList.length > 0"
         type="primary" 
         @click="insertWebpage()"
         :disabled="selectedIndex === null"
       >
-        确认
+        {{ lang.ssConfirm }}
       </Button>
     </div>
   </div>
@@ -50,6 +50,7 @@
 import { ref } from 'vue'
 import message from '@/utils/message'
 import Button from '@/components/Button.vue'
+import { lang } from '@/main'
 
 interface Webpage {
   type: number
@@ -85,7 +86,7 @@ const toggleWebpageSelection = (index: number) => {
 
 const insertWebpage = () => {
   if (selectedIndex.value === null) {
-    return message.error('请先选择一个网页')
+    return message.error(lang.ssPickWebFirst)
   }
   
   // 根据选中的index获取对应的链接
@@ -98,15 +99,15 @@ const insertWebpage = () => {
 // 获取类型标签
 const getTypeLabel = (type: number) => {
   const typeMap: Record<number, string> = {
-    45: '选择题',
-    15: '问答题',
-    72: 'AI应用',
-    73: 'H5页面',
-    74: '视频',
-    75: 'B站视频',
-    76: '创作空间'
+    45: lang.ssChoiceQ,
+    15: lang.ssQATest,
+    72: lang.ssAiApp,
+    73: lang.ssHPage,
+    74: lang.ssVideo,
+    75: lang.ssBiliVideo,
+    76: lang.ssCreative
   }
-  return typeMap[type] || '未知'
+  return typeMap[type] || lang.ssUnknown
 }
 
 // 获取类型样式类

+ 26 - 25
src/views/Editor/CanvasTool/index.vue

@@ -1,37 +1,37 @@
 <template>
   <div class="canvas-tool">
     <div class="left-handler">
-      <IconBack class="handler-item" :class="{ 'disable': !canUndo }" v-tooltip="'撤销(Ctrl + Z)'" @click="undo()" />
-      <IconNext class="handler-item" :class="{ 'disable': !canRedo }" v-tooltip="'重做(Ctrl + Y)'" @click="redo()" />
+      <IconBack class="handler-item" :class="{ 'disable': !canUndo }" v-tooltip="lang.ssUndoTip" @click="undo()" />
+      <IconNext class="handler-item" :class="{ 'disable': !canRedo }" v-tooltip="lang.ssRedoTip" @click="redo()" />
       <div class="more">
         <Divider type="vertical" style="height: 20px;" />
         <Popover class="more-icon" trigger="click" v-model:value="moreVisible" :offset="10">
           <template #content>
-            <PopoverMenuItem center @click="toggleNotesPanel(); moreVisible = false">批注面板</PopoverMenuItem>
-            <PopoverMenuItem center @click="toggleSelectPanel(); moreVisible = false">选择窗格</PopoverMenuItem>
-            <PopoverMenuItem center @click="toggleSraechPanel(); moreVisible = false">查找替换</PopoverMenuItem>
+            <PopoverMenuItem center @click="toggleNotesPanel(); moreVisible = false">{{ lang.ssNotePanel }}</PopoverMenuItem>
+            <PopoverMenuItem center @click="toggleSelectPanel(); moreVisible = false">{{ lang.ssSelectPane }}</PopoverMenuItem>
+            <PopoverMenuItem center @click="toggleSraechPanel(); moreVisible = false">{{ lang.ssSearchReplace }}</PopoverMenuItem>
           </template>
           <IconMore class="handler-item" />
         </Popover>
-        <IconComment class="handler-item" :class="{ 'active': showNotesPanel }" v-tooltip="'批注面板'" @click="toggleNotesPanel()" />
-        <IconMoveOne class="handler-item" :class="{ 'active': showSelectPanel }" v-tooltip="'选择窗格'" @click="toggleSelectPanel()" />
-        <IconSearch class="handler-item" :class="{ 'active': showSearchPanel }" v-tooltip="'查找/替换(Ctrl + F)'" @click="toggleSraechPanel()" />
+        <IconComment class="handler-item" :class="{ 'active': showNotesPanel }" v-tooltip="lang.ssNotePanel" @click="toggleNotesPanel()" />
+        <IconMoveOne class="handler-item" :class="{ 'active': showSelectPanel }" v-tooltip="lang.ssSelectPane" @click="toggleSelectPanel()" />
+        <IconSearch class="handler-item" :class="{ 'active': showSearchPanel }" v-tooltip="lang.ssSearchTip" @click="toggleSraechPanel()" />
       </div>
     </div>
 
     <div class="add-element-handler">
-      <div class="handler-item group-btn" v-tooltip="'插入文字'">
+      <div class="handler-item group-btn" v-tooltip="lang.ssInsertText">
         <IconFontSize class="icon" :class="{ 'active': creatingElement?.type === 'text' }" @click="drawText()" />
         
         <Popover trigger="click" v-model:value="textTypeSelectVisible" style="height: 100%;" :offset="10">
           <template #content>
-            <PopoverMenuItem center @click="() => { drawText(); textTypeSelectVisible = false }"><IconTextRotationNone /> 横向文本框</PopoverMenuItem>
-            <PopoverMenuItem center @click="() => { drawText(true); textTypeSelectVisible = false }"><IconTextRotationDown /> 竖向文本框</PopoverMenuItem>
+            <PopoverMenuItem center @click="() => { drawText(); textTypeSelectVisible = false }"><IconTextRotationNone /> {{ lang.ssTextHorizontal }}</PopoverMenuItem>
+            <PopoverMenuItem center @click="() => { drawText(true); textTypeSelectVisible = false }"><IconTextRotationDown /> {{ lang.ssTextVertical }}</PopoverMenuItem>
           </template>
           <IconDown class="arrow" />
         </Popover>
       </div>
-      <div class="handler-item group-btn" v-tooltip="'插入形状'" :offset="10">
+      <div class="handler-item group-btn" v-tooltip="lang.ssInsertShape" :offset="10">
         <Popover trigger="click" style="height: 100%;" v-model:value="shapePoolVisible" :offset="10">
           <template #content>
             <ShapePool @select="shape => drawShape(shape)" />
@@ -41,25 +41,25 @@
         
         <Popover trigger="click" v-model:value="shapeMenuVisible" style="height: 100%;" :offset="10">
           <template #content>
-            <PopoverMenuItem center @click="() => { drawCustomShape(); shapeMenuVisible = false }">自由绘制</PopoverMenuItem>
+            <PopoverMenuItem center @click="() => { drawCustomShape(); shapeMenuVisible = false }">{{ lang.ssFreeDraw }}</PopoverMenuItem>
           </template>
           <IconDown class="arrow" />
         </Popover>
       </div>
       <FileInput @change="files => insertImageElement(files)">
-        <IconPicture class="handler-item" v-tooltip="'插入图片'" />
+        <IconPicture class="handler-item" v-tooltip="lang.ssInsertImage" />
       </FileInput>
       <Popover trigger="click" v-model:value="linePoolVisible" :offset="10">
         <template #content>
           <LinePool @select="line => drawLine(line)" />
         </template>
-        <IconConnection class="handler-item" :class="{ 'active': creatingElement?.type === 'line' }" v-tooltip="'插入线条'" />
+        <IconConnection class="handler-item" :class="{ 'active': creatingElement?.type === 'line' }" v-tooltip="lang.ssInsertLine" />
       </Popover>
       <Popover trigger="click" v-model:value="chartPoolVisible" :offset="10">
         <template #content>
           <ChartPool @select="chart => { createChartElement(chart); chartPoolVisible = false }" />
         </template>
-        <IconChartProportion class="handler-item" v-tooltip="'插入图表'" />
+        <IconChartProportion class="handler-item" v-tooltip="lang.ssInsertChart" />
       </Popover>
       <Popover trigger="click" v-model:value="tableGeneratorVisible" :offset="10">
         <template #content>
@@ -68,9 +68,9 @@
             @insert="({ row, col }) => { createTableElement(row, col); tableGeneratorVisible = false }"
           />
         </template>
-        <IconInsertTable class="handler-item" v-tooltip="'插入表格'" />
+        <IconInsertTable class="handler-item" v-tooltip="lang.ssInsertTable" />
       </Popover>
-      <IconFormula class="handler-item" v-tooltip="'插入公式'" @click="latexEditorVisible = true" />
+      <IconFormula class="handler-item" v-tooltip="lang.ssInsertFormula" @click="latexEditorVisible = true" />
       <Popover v-if="viewMode !== 'editor2'" trigger="manual" v-model:value="webpageInputVisible" :offset="10">
         <template #content>
           <WebpageInput 
@@ -79,7 +79,7 @@
             @insertWebpage="({ url, type }) => { createFrameElement(url, type); webpageInputVisible = false }"
           />
         </template>
-        <IconLinkOne class="handler-item" v-tooltip="'插入学习内容'" @click="handleInsertLearningContent" />
+        <IconLinkOne class="handler-item" v-tooltip="lang.ssInsertLearn" @click="handleInsertLearningContent" />
       </Popover>
       <Popover trigger="click" v-model:value="mediaInputVisible" :offset="10">
         <template #content>
@@ -89,13 +89,13 @@
             @insertAudio="src => { createAudioElement(src); mediaInputVisible = false }"
           />
         </template>
-        <IconVideoTwo class="handler-item" v-tooltip="'插入音视频'" />
+        <IconVideoTwo class="handler-item" v-tooltip="lang.ssInsertMedia" />
       </Popover>
     </div>
 
     <div class="right-handler">
-      <div v-if="hasInteractiveTool" class="handler-item viewport-size edit-tool-btn" @click="editTool">编辑工具</div>
-      <IconMinus class="handler-item viewport-size" v-tooltip="'画布缩小(Ctrl + -)'" @click="scaleCanvas('-')" />
+      <div v-if="hasInteractiveTool" class="handler-item viewport-size edit-tool-btn" @click="editTool">{{ lang.ssEditTool }}</div>
+      <IconMinus class="handler-item viewport-size" v-tooltip="lang.ssZoomOutTip" @click="scaleCanvas('-')" />
       <Popover trigger="click" v-model:value="canvasScaleVisible">
         <template #content>
           <PopoverMenuItem
@@ -104,12 +104,12 @@
             :key="item" 
             @click="applyCanvasPresetScale(item)"
           >{{item}}%</PopoverMenuItem>
-          <PopoverMenuItem center @click="resetCanvas(); canvasScaleVisible = false">适应屏幕</PopoverMenuItem>
+          <PopoverMenuItem center @click="resetCanvas(); canvasScaleVisible = false">{{ lang.ssFitScreen }}</PopoverMenuItem>
         </template>
         <span class="text">{{canvasScalePercentage}}</span>
       </Popover>
-      <IconPlus class="handler-item viewport-size" v-tooltip="'画布放大(Ctrl + =)'" @click="scaleCanvas('+')" />
-      <IconFullScreen class="handler-item viewport-size-adaptation" v-tooltip="'适应屏幕(Ctrl + 0)'" @click="resetCanvas()" />
+      <IconPlus class="handler-item viewport-size" v-tooltip="lang.ssZoomInTip" @click="scaleCanvas('+')" />
+      <IconFullScreen class="handler-item viewport-size-adaptation" v-tooltip="lang.ssFitScreenTip" @click="resetCanvas()" />
     </div>
 
     <Modal
@@ -134,6 +134,7 @@ import type { LinePoolItem } from '@/configs/lines'
 import useScaleCanvas from '@/hooks/useScaleCanvas'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
 import useCreateElement from '@/hooks/useCreateElement'
+import { lang } from '@/main'
 
 import ShapePool from './ShapePool.vue'
 import LinePool from './LinePool.vue'

+ 12 - 12
src/views/Editor/EditorHeader/index.vue

@@ -8,7 +8,7 @@
             importPPTXFile(files)
             mainMenuVisible = false
           }">
-            <PopoverMenuItem>导入 PPTX 文件</PopoverMenuItem>
+            <PopoverMenuItem>{{ lang.ssImportPptx }}</PopoverMenuItem>
           </FileInput>
           <!-- <FileInput accept=".json"  @change="files => {
             importJSON(files)
@@ -23,18 +23,18 @@
             <PopoverMenuItem>导入 pptist 文件</PopoverMenuItem>
           </FileInput> -->
           <!-- <PopoverMenuItem @click="setDialogForExport('pptx')">导出文件</PopoverMenuItem> -->
-          <PopoverMenuItem @click="resetSlides(); mainMenuVisible = false">重置幻灯片</PopoverMenuItem>
+          <PopoverMenuItem @click="resetSlides(); mainMenuVisible = false">{{ lang.ssResetSlides }}</PopoverMenuItem>
           <!-- <PopoverMenuItem @click="openMarkupPanel(); mainMenuVisible = false">幻灯片类型标注</PopoverMenuItem> -->
           <!-- <PopoverMenuItem @click="goLink('https://github.com/pipipi-pikachu/PPTist/issues')">意见反馈</PopoverMenuItem> -->
           <!-- <PopoverMenuItem @click="goLink('https://github.com/pipipi-pikachu/PPTist/blob/master/doc/Q&A.md')">常见问题</PopoverMenuItem> -->
-          <PopoverMenuItem @click="mainMenuVisible = false; hotkeyDrawerVisible = true">快捷操作</PopoverMenuItem>
+          <PopoverMenuItem @click="mainMenuVisible = false; hotkeyDrawerVisible = true">{{ lang.ssHotkeys }}</PopoverMenuItem>
         </template>
         <div class="menu-item"  v-show="false"><IconHamburgerButton class="icon" /></div>
       </Popover>
       <FileInput accept="application/vnd.openxmlformats-officedocument.presentationml.presentation" @change="files => {
         importPPTXFile(files)
       }">
-        <div class="menu-item"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>上传 PPTX 文件</div>
+        <div class="menu-item"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>{{ lang.ssUploadPptx }}</div>
       </FileInput>
 
       <div class="title" v-show="false">
@@ -56,13 +56,13 @@
 
     <div class="right">
       <div class="group-menu-item">
-        <div class="menu-item" v-tooltip="'幻灯片放映(F5)'" @click="enterScreening()">
+        <div class="menu-item" v-tooltip="lang.ssStage" @click="enterScreening()">
           <IconPpt class="icon" />
         </div>
         <Popover trigger="click" center>
           <template #content>
-            <PopoverMenuItem @click="enterScreeningFromStart()">从头开始</PopoverMenuItem>
-            <PopoverMenuItem @click="enterScreening()">从当前页开始</PopoverMenuItem>
+            <PopoverMenuItem @click="enterScreeningFromStart()">{{ lang.ssFromStart }}</PopoverMenuItem>
+            <PopoverMenuItem @click="enterScreening()">{{ lang.ssFromCurrent }}</PopoverMenuItem>
           </template>
           <div class="arrow-btn"><IconDown class="arrow" /></div>
         </Popover>
@@ -73,7 +73,7 @@
       <!-- <div class="menu-item" v-tooltip="'AI生成PPT'" @click="openAIPPTDialog(); mainMenuVisible = false">
         <span class="text ai">AI</span>
       </div> -->
-      <div class="menu-item" v-tooltip="'导出'" @click="setDialogForExport('pptx')">
+      <div class="menu-item" v-tooltip="lang.ssExport" @click="setDialogForExport('pptx')">
         <IconDownload class="icon" />
       </div>
       <!-- <a class="github-link" v-tooltip="'Copyright © 2020-PRESENT pipipi-pikachu'" href="https://github.com/pipipi-pikachu/PPTist" target="_blank">
@@ -87,22 +87,22 @@
       placement="right"
     >
       <HotkeyDoc />
-      <template v-slot:title>快捷操作</template>
+      <template v-slot:title>{{ lang.ssHotkeys }}</template>
     </Drawer>
 
-    <FullscreenSpin :loading="exporting" tip="正在导入..." />
+    <FullscreenSpin :loading="exporting" :tip="lang.ssImporting" />
   </div>
 </template>
 
 <script lang="ts" setup>
-import { nextTick, ref, useTemplateRef, inject } from 'vue'
+import { nextTick, ref, useTemplateRef, inject, onMounted } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useMainStore, useSlidesStore } from '@/store'
 import useScreening from '@/hooks/useScreening'
 import useImport from '@/hooks/useImport'
 import useSlideHandler from '@/hooks/useSlideHandler'
 import type { DialogForExportTypes } from '@/types/export'
-
+import { lang } from '@/main'
 import HotkeyDoc from './HotkeyDoc.vue'
 import FileInput from '@/components/FileInput.vue'
 import FullscreenSpin from '@/components/FullscreenSpin.vue'

+ 14 - 13
src/views/Editor/ExportDialog/ExportImage.vue

@@ -11,9 +11,9 @@
         />
       </div>
     </div>
-    <div class="configs">
+    <div class="configs" :style="{ width: lang.lang === 'en' ? '475px' : '350px' }">
       <div class="row">
-        <div class="title">导出格式:</div>
+        <div class="title">{{ lang.ssExpFormat }}</div>
         <RadioGroup
           class="config-item"
           v-model:value="format"
@@ -23,18 +23,18 @@
         </RadioGroup>
       </div>
       <div class="row">
-        <div class="title">导出范围:</div>
+        <div class="title">{{ lang.ssExpRange }}</div>
         <RadioGroup
           class="config-item"
           v-model:value="rangeType"
         >
-          <RadioButton style="width: 33.33%;" value="all">全部</RadioButton>
-          <RadioButton style="width: 33.33%;" value="current">当前页</RadioButton>
-          <RadioButton style="width: 33.33%;" value="custom">自定义</RadioButton>
+          <RadioButton style="width: 33.33%;" value="all">{{ lang.ssShowAll }}</RadioButton>
+          <RadioButton style="width: 33.33%;" value="current">{{ lang.ssCurPage }}</RadioButton>
+          <RadioButton style="width: 33.33%;" value="custom">{{ lang.ssCustom }}</RadioButton>
         </RadioGroup>
       </div>
       <div class="row" v-if="rangeType === 'custom'">
-        <div class="title" :data-range="`(${range[0]} ~ ${range[1]})`">自定义范围:</div>
+        <div class="title" :data-range="`(${range[0]} ~ ${range[1]})`">{{ lang.ssCustomRange }}</div>
         <Slider
           class="config-item"
           range
@@ -46,7 +46,7 @@
       </div>
 
       <div class="row">
-        <div class="title">图片质量:</div>
+        <div class="title">{{ lang.ssImgQuality }}</div>
         <Slider
           class="config-item"
           :min="0"
@@ -57,19 +57,19 @@
       </div>
 
       <div class="row">
-        <div class="title">忽略在线字体:</div>
+        <div class="title">{{ lang.ssIgnoreWebfont }}</div>
         <div class="config-item">
-          <Switch v-model:value="ignoreWebfont" v-tooltip="'导出时默认忽略在线字体,若您在幻灯片中使用了在线字体,且希望导出后保留相关样式,可选择关闭【忽略在线字体】选项,但要注意这将会增加导出用时。'" />
+          <Switch v-model:value="ignoreWebfont" v-tooltip="lang.ssIgnoreWebfontTip" />
         </div>
       </div>
     </div>
 
     <div class="btns">
-      <Button class="btn export" type="primary" @click="expImage()">导出图片</Button>
-      <Button class="btn close" @click="emit('close')">关闭</Button>
+      <Button class="btn export" type="primary" @click="expImage()">{{ lang.ssExportImage }}</Button>
+      <Button class="btn close" @click="emit('close')">{{ lang.ssClose }}</Button>
     </div>
 
-    <FullscreenSpin :loading="exporting" tip="正在导出..." />
+    <FullscreenSpin :loading="exporting" :tip="lang.ssExporting" />
   </div>
 </template>
 
@@ -78,6 +78,7 @@ import { computed, ref, useTemplateRef } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useSlidesStore } from '@/store'
 import useExport from '@/hooks/useExport'
+import { lang } from '@/main'
 
 import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
 import FullscreenSpin from '@/components/FullscreenSpin.vue'

+ 3 - 2
src/views/Editor/ExportDialog/ExportJSON.vue

@@ -5,8 +5,8 @@
     </div>
 
     <div class="btns">
-      <Button class="btn export" type="primary" @click="exportJSON()">导出 JSON</Button>
-      <Button class="btn close" @click="emit('close')">关闭</Button>
+      <Button class="btn export" type="primary" @click="exportJSON()">{{ lang.ssExportJSON }}</Button>
+      <Button class="btn close" @click="emit('close')">{{ lang.ssClose }}</Button>
     </div>
   </div>
 </template>
@@ -16,6 +16,7 @@ import { computed } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useSlidesStore } from '@/store'
 import useExport from '@/hooks/useExport'
+import { lang } from '@/main'
 import Button from '@/components/Button.vue'
 
 const emit = defineEmits<{

+ 10 - 9
src/views/Editor/ExportDialog/ExportPDF.vue

@@ -20,19 +20,19 @@
         </template>
       </div>
     </div>
-    <div class="configs">
+    <div class="configs" :style="{ width: lang.lang === 'en' ? '475px' : '350px' }">
       <div class="row">
-        <div class="title">导出范围:</div>
+        <div class="title">{{ lang.ssExpRange }}</div>
         <RadioGroup
           class="config-item"
           v-model:value="rangeType"
         >
-          <RadioButton style="width: 50%;" value="all">全部</RadioButton>
-          <RadioButton style="width: 50%;" value="current">当前页</RadioButton>
+          <RadioButton style="width: 50%;" value="all">{{ lang.ssShowAll }}</RadioButton>
+          <RadioButton style="width: 50%;" value="current">{{ lang.ssCurPage }}</RadioButton>
         </RadioGroup>
       </div>
       <div class="row">
-        <div class="title">每页数量:</div>
+        <div class="title">{{ lang.ssPerPage }}</div>
         <Select 
           class="config-item"
           v-model:value="count"
@@ -44,19 +44,19 @@
         />
       </div>
       <div class="row">
-        <div class="title">边缘留白:</div>
+        <div class="title">{{ lang.ssEdgePad }}</div>
         <div class="config-item">
           <Switch v-model:value="padding" />
         </div>
       </div>
       <div class="tip">
-        提示:若打印预览与实际样式不一致,请在弹出的打印窗口中勾选【背景图形】选项。
+        {{ lang.ssPrintBgTip }}
       </div>
     </div>
 
     <div class="btns">
-      <Button class="btn export" type="primary" @click="expPDF()">打印 / 导出 PDF</Button>
-      <Button class="btn close" @click="emit('close')">关闭</Button>
+      <Button class="btn export" type="primary" @click="expPDF()">{{ lang.ssPrintPdf }}</Button>
+      <Button class="btn close" @click="emit('close')">{{ lang.ssClose }}</Button>
     </div>
   </div>
 </template>
@@ -66,6 +66,7 @@ import { ref, useTemplateRef } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useSlidesStore } from '@/store'
 import { print } from '@/utils/print'
+import { lang } from '@/main'
 
 import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
 import Switch from '@/components/Switch.vue'

+ 15 - 14
src/views/Editor/ExportDialog/ExportPPTX.vue

@@ -1,19 +1,19 @@
 <template>
   <div class="export-pptx-dialog">
-    <div class="configs">
+    <div class="configs" :style="{ width: lang.lang === 'en' ? '475px' : '350px' }">
       <div class="row">
-        <div class="title">导出范围:</div>
+        <div class="title">{{ lang.ssExpRange }}</div>
         <RadioGroup
           class="config-item"
           v-model:value="rangeType"
         >
-          <RadioButton style="width: 33.33%;" value="all">全部</RadioButton>
-          <RadioButton style="width: 33.33%;" value="current">当前页</RadioButton>
-          <RadioButton style="width: 33.33%;" value="custom">自定义</RadioButton>
+          <RadioButton style="width: 33.33%;" value="all">{{ lang.ssShowAll }}</RadioButton>
+          <RadioButton style="width: 33.33%;" value="current">{{ lang.ssCurPage }}</RadioButton>
+          <RadioButton style="width: 33.33%;" value="custom">{{ lang.ssCustom }}</RadioButton>
         </RadioGroup>
       </div>
       <div class="row" v-if="rangeType === 'custom'">
-        <div class="title" :data-range="`(${range[0]} ~ ${range[1]})`">自定义范围:</div>
+        <div class="title" :data-range="`(${range[0]} ~ ${range[1]})`">{{ lang.ssCustomRange }}</div>
         <Slider
           class="config-item"
           range
@@ -24,28 +24,28 @@
         />
       </div>
       <div class="row">
-        <div class="title">忽略音频/视频:</div>
+        <div class="title">{{ lang.ssIgnoreMedia }}</div>
         <div class="config-item">
-          <Switch v-model:value="ignoreMedia" v-tooltip="'导出时默认忽略音视频,若您的幻灯片中存在音视频元素,且希望将其导出到PPTX文件中,可选择关闭【忽略音视频】选项,但要注意这将会大幅增加导出用时。'" />
+          <Switch v-model:value="ignoreMedia" v-tooltip="lang.ssIgnoreMediaTip" />
         </div>
       </div>
       <div class="row">
-        <div class="title">覆盖默认母版:</div>
+        <div class="title">{{ lang.ssMastOver }}</div>
         <div class="config-item">
           <Switch v-model:value="masterOverwrite" />
         </div>
       </div>
 
       <div class="tip" v-if="!ignoreMedia">
-        提示:1. 支持导出格式:avi、mp4、mov、wmv、mp3、wav;2. 跨域资源无法导出。
+        {{ lang.ssPptxTip }}
       </div>
     </div>
     <div class="btns">
-      <Button class="btn export" type="primary" @click="exportPPTX(selectedSlides, masterOverwrite, ignoreMedia)">导出 PPTX</Button>
-      <Button class="btn close" @click="emit('close')">关闭</Button>
+      <Button class="btn export" type="primary" @click="exportPPTX(selectedSlides, masterOverwrite, ignoreMedia)">{{ lang.ssExportPptx }}</Button>
+      <Button class="btn close" @click="emit('close')">{{ lang.ssClose }}</Button>
     </div>
 
-    <FullscreenSpin :loading="exporting" tip="正在导出..." />
+    <FullscreenSpin :loading="exporting" :tip="lang.ssExporting" />
   </div>
 </template>
 
@@ -54,6 +54,7 @@ import { computed, ref } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useSlidesStore } from '@/store'
 import useExport from '@/hooks/useExport'
+import { lang } from '@/main'
 
 import FullscreenSpin from '@/components/FullscreenSpin.vue'
 import Switch from '@/components/Switch.vue'
@@ -110,7 +111,7 @@ const selectedSlides = computed(() => {
   }
 
   .title {
-    width: 100px;
+    width: 110px;
     position: relative;
 
     &::after {

+ 10 - 9
src/views/Editor/ExportDialog/ExportSpecificFile.vue

@@ -1,19 +1,19 @@
 <template>
   <div class="export-pptist-dialog">
-    <div class="configs">
+    <div class="configs" :style="{ width: lang.lang === 'en' ? '475px' : '350px' }">
       <div class="row">
-        <div class="title">导出范围:</div>
+        <div class="title">{{ lang.ssExpRange }}</div>
         <RadioGroup
           class="config-item"
           v-model:value="rangeType"
         >
-          <RadioButton style="width: 33.33%;" value="all">全部</RadioButton>
-          <RadioButton style="width: 33.33%;" value="current">当前页</RadioButton>
-          <RadioButton style="width: 33.33%;" value="custom">自定义</RadioButton>
+          <RadioButton style="width: 33.33%;" value="all">{{ lang.ssShowAll }}</RadioButton>
+          <RadioButton style="width: 33.33%;" value="current">{{ lang.ssCurPage }}</RadioButton>
+          <RadioButton style="width: 33.33%;" value="custom">{{ lang.ssCustom }}</RadioButton>
         </RadioGroup>
       </div>
       <div class="row" v-if="rangeType === 'custom'">
-        <div class="title" :data-range="`(${range[0]} ~ ${range[1]})`">自定义范围:</div>
+        <div class="title" :data-range="`(${range[0]} ~ ${range[1]})`">{{ lang.ssCustomRange }}</div>
         <Slider
           class="config-item"
           range
@@ -24,12 +24,12 @@
         />
       </div>
       <div class="tip">
-        提示:.pptist 是本应用的特有文件后缀,支持将该类型的文件导入回应用中。
+        {{ lang.ssPptistTip }}
       </div>
     </div>
     <div class="btns">
-      <Button class="btn export" type="primary" @click="exportSpecificFile(selectedSlides)">导出 .pptist 文件</Button>
-      <Button class="btn close" @click="emit('close')">关闭</Button>
+      <Button class="btn export" type="primary" @click="exportSpecificFile(selectedSlides)">{{ lang.ssExportPptist }}</Button>
+      <Button class="btn close" @click="emit('close')">{{ lang.ssClose }}</Button>
     </div>
   </div>
 </template>
@@ -39,6 +39,7 @@ import { computed, ref } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useSlidesStore } from '@/store'
 import useExport from '@/hooks/useExport'
+import { lang } from '@/main'
 
 import Slider from '@/components/Slider.vue'
 import Button from '@/components/Button.vue'

+ 6 - 5
src/views/Editor/ExportDialog/index.vue

@@ -17,6 +17,7 @@ import { computed } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useMainStore } from '@/store'
 import type { DialogForExportTypes } from '@/types/export'
+import { lang } from '@/main'
 
 import ExportImage from './ExportImage.vue'
 import ExportJSON from './ExportJSON.vue'
@@ -36,11 +37,11 @@ const { dialogForExport } = storeToRefs(mainStore)
 const setDialogForExport = mainStore.setDialogForExport
 
 const tabs: TabItem[] = [
-  { key: 'pptist', label: '导出 pptist 文件' },
-  { key: 'pptx', label: '导出 PPTX' },
-  { key: 'image', label: '导出图片' },
-  { key: 'json', label: '导出 JSON' },
-  { key: 'pdf', label: '打印 / 导出 PDF' },
+  { key: 'pptist', label: lang.ssExpPptist },
+  { key: 'pptx', label: lang.ssExpPptx },
+  { key: 'image', label: lang.ssExpImage },
+  { key: 'json', label: lang.ssExpJson },
+  { key: 'pdf', label: lang.ssPrintPdf },
 ]
 
 const currentDialogComponent = computed<unknown>(() => {

+ 27 - 26
src/views/Editor/MarkupPanel.vue

@@ -3,14 +3,14 @@
     class="notes-panel" 
     :width="300" 
     :height="130" 
-    title="幻灯片类型标注" 
+    :title="lang.ssMarkupTitle" 
     :left="-270" 
     :top="90"
     @close="close()"
   >
     <div class="container">
       <div class="row">
-        <div style="width: 40%;">当前页面类型:</div>
+        <div style="width: 40%;">{{ lang.ssCurPageType }}</div>
         <Select
           style="width: 60%;"
           :value="slideType"
@@ -19,7 +19,7 @@
         />
       </div>
       <div class="row" v-if="handleElement && (handleElement.type === 'text' || (handleElement.type === 'shape' && handleElement.text))">
-        <div style="width: 40%;">当前文本类型:</div>
+        <div style="width: 40%;">{{ lang.ssCurTextType }}</div>
         <Select
           style="width: 60%;"
           :value="textType"
@@ -28,7 +28,7 @@
         />
       </div>
       <div class="row" v-else-if="handleElement && handleElement.type === 'image'">
-        <div style="width: 40%;">当前图片类型:</div>
+        <div style="width: 40%;">{{ lang.ssCurImgType }}</div>
         <Select
           style="width: 60%;"
           :value="imageType"
@@ -36,7 +36,7 @@
           :options="imageTypeOptions"
         />
       </div>
-      <div class="placeholder" v-else>选中图片、文字、带文字的形状,标记类型</div>
+      <div class="placeholder" v-else>{{ lang.ssMarkupHint }}</div>
     </div>
   </MoveablePanel>
 </template>
@@ -46,6 +46,7 @@ import { computed, ref } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useMainStore, useSlidesStore } from '@/store'
 import type { ImageType, SlideType, TextType } from '@/types/slides'
+import { lang } from '@/main'
 
 import MoveablePanel from '@/components/MoveablePanel.vue'
 import Select from '@/components/Select.vue'
@@ -56,33 +57,33 @@ const { currentSlide } = storeToRefs(slidesStore)
 const { handleElement, handleElementId } = storeToRefs(mainStore)
 
 const slideTypeOptions = ref<{ label: string; value: SlideType | '' }[]>([
-  { label: '未标记类型', value: '' },
-  { label: '封面页', value: 'cover' },
-  { label: '目录页', value: 'contents' },
-  { label: '过渡页', value: 'transition' },
-  { label: '内容页', value: 'content' },
-  { label: '结束页', value: 'end' },
+  { label: lang.ssUnmarkedType, value: '' },
+  { label: lang.ssCoverPage, value: 'cover' },
+  { label: lang.ssTocPage, value: 'contents' },
+  { label: lang.ssTransPage, value: 'transition' },
+  { label: lang.ssContentPage, value: 'content' },
+  { label: lang.ssEndPage, value: 'end' },
 ])
 
 const textTypeOptions = ref<{ label: string; value: TextType | '' }[]>([
-  { label: '未标记类型', value: '' },
-  { label: '标题', value: 'title' },
-  { label: '副标题', value: 'subtitle' },
-  { label: '正文', value: 'content' },
-  { label: '列表项目', value: 'item' },
-  { label: '列表项标题', value: 'itemTitle' },
-  { label: '注释', value: 'notes' },
-  { label: '页眉', value: 'header' },
-  { label: '页脚', value: 'footer' },
-  { label: '节编号', value: 'partNumber' },
-  { label: '项目编号', value: 'itemNumber' },
+  { label: lang.ssUnmarkedType, value: '' },
+  { label: lang.ssTxtTitle, value: 'title' },
+  { label: lang.ssTxtSubttl, value: 'subtitle' },
+  { label: lang.ssTxtBody, value: 'content' },
+  { label: lang.ssTxtItem, value: 'item' },
+  { label: lang.ssTxtItemTtl, value: 'itemTitle' },
+  { label: lang.ssTxtNotes, value: 'notes' },
+  { label: lang.ssTxtHeader, value: 'header' },
+  { label: lang.ssTxtFooter, value: 'footer' },
+  { label: lang.ssTxtPartNo, value: 'partNumber' },
+  { label: lang.ssTxtItemNo, value: 'itemNumber' },
 ])
 
 const imageTypeOptions = ref<{ label: string; value: ImageType | '' }[]>([
-  { label: '未标记类型', value: '' },
-  { label: '页面插图', value: 'pageFigure' },
-  { label: '项目插图', value: 'itemFigure' },
-  { label: '背景图', value: 'background' },
+  { label: lang.ssUnmarkedType, value: '' },
+  { label: lang.ssImgPageFig, value: 'pageFigure' },
+  { label: lang.ssImgItemFig, value: 'itemFigure' },
+  { label: lang.ssImgBg, value: 'background' },
 ])
 
 const slideType = computed(() => currentSlide.value?.type || '')

+ 19 - 13
src/views/Editor/NotesPanel.vue

@@ -3,7 +3,7 @@
     class="notes-panel" 
     :width="300" 
     :height="560" 
-    :title="`幻灯片${slideIndex + 1}的批注`" 
+    :title="lang.ssSlideNote.replace(/\*/g, String(slideIndex + 1))" 
     :left="-270" 
     :top="90"
     :minWidth="300"
@@ -25,8 +25,8 @@
               </div>
             </div>
             <div class="btns">
-              <div class="btn reply" @click="replyNoteId = note.id">回复</div>
-              <div class="btn delete" @click.stop="deleteNote(note.id)">删除</div>
+              <div class="btn reply" @click="replyNoteId = note.id">{{ lang.ssReply }}</div>
+              <div class="btn delete" @click.stop="deleteNote(note.id)">{{ lang.ssDelete }}</div>
             </div>
           </div>
           <div class="content">{{ note.content }}</div>
@@ -41,35 +41,35 @@
                   </div>
                 </div>
                 <div class="btns">
-                  <div class="btn delete" @click.stop="deleteReply(note.id, reply.id)">删除</div>
+                  <div class="btn delete" @click.stop="deleteReply(note.id, reply.id)">{{ lang.ssDelete }}</div>
                 </div>
               </div>
               <div class="content">{{ reply.content }}</div>
             </div>
           </div>
           <div class="note-reply" v-if="replyNoteId === note.id">
-            <TextArea :padding="6" v-model:value="replyContent" placeholder="输入回复内容" :rows="1" @enter.prevent="createNoteReply()" />
+            <TextArea :padding="6" v-model:value="replyContent" :placeholder="lang.ssReplyInput" :rows="1" @enter.prevent="createNoteReply()" />
             <div class="reply-btns">
-              <Button class="btn" size="small" @click="replyNoteId = ''">取消</Button>
-              <Button class="btn" size="small" type="primary" @click="createNoteReply()">回复</Button>
+              <Button class="btn" size="small" @click="replyNoteId = ''">{{ lang.ssCancel }}</Button>
+              <Button class="btn" size="small" type="primary" @click="createNoteReply()">{{ lang.ssReply }}</Button>
             </div>
           </div>
         </div>
-        <div class="empty" v-if="!notes.length">本页暂无批注</div>
+        <div class="empty" v-if="!notes.length">{{ lang.ssNoNotes }}</div>
       </div>
       <div class="send">
         <TextArea 
           ref="textAreaRef"
           v-model:value="content"
           :padding="6"
-          :placeholder="`输入批注(为${handleElementId ? '选中元素' : '当前页幻灯片' })`"
+          :placeholder="notePlaceholder"
           :rows="2"
           @focus="replyNoteId = ''; activeNoteId = ''"
           @enter.prevent="createNote()"
         />
         <div class="footer">
-          <IconDelete class="btn icon" v-tooltip="'清空本页批注'" style="flex: 1" @click="clear()" />
-          <Button type="primary" class="btn" style="flex: 12" @click="createNote()">添加批注</Button>
+          <IconDelete class="btn icon" v-tooltip="lang.ssClearNotes" style="flex: 1" @click="clear()" />
+          <Button type="primary" class="btn" style="flex: 12" @click="createNote()">{{ lang.ssAddNote }}</Button>
         </div>
       </div>
     </div>
@@ -86,6 +86,7 @@ import type { Note } from '@/types/slides'
 import MoveablePanel from '@/components/MoveablePanel.vue'
 import TextArea from '@/components/TextArea.vue'
 import Button from '@/components/Button.vue'
+import { lang } from '@/main'
 
 const slidesStore = useSlidesStore()
 const mainStore = useMainStore()
@@ -100,6 +101,11 @@ const replyNoteId = ref('')
 const textAreaRef = useTemplateRef<InstanceType<typeof TextArea>>('textAreaRef')
 const notesRef = useTemplateRef<HTMLElement>('notesRef')
 
+const notePlaceholder = computed(() => {
+  const target = handleElementId.value ? lang.ssSelEl : lang.ssCurSlide
+  return lang.lang == 'en' ? 'Add a note for this slide...' :lang.ssNoteInput.replace(/\*/g, target)
+})
+
 watch(slideIndex, () => {
   activeNoteId.value = ''
   replyNoteId.value = ''
@@ -121,7 +127,7 @@ const createNote = () => {
     id: nanoid(),
     content: content.value,
     time: new Date().getTime(),
-    user: '测试用户',
+    user: lang.ssTestUser,
   }
   if (handleElementId.value) newNote.elId = handleElementId.value
 
@@ -153,7 +159,7 @@ const createNoteReply = () => {
       id: nanoid(),
       content: replyContent.value,
       time: new Date().getTime(),
-      user: '测试用户',
+      user: lang.ssTestUser,
     },
   ]
   const newNote: Note = {

+ 2 - 1
src/views/Editor/Remark/Editor.vue

@@ -38,6 +38,7 @@ import { initProsemirrorEditor, createDocument } from '@/utils/prosemirror'
 import { addMark, autoSelectAll, getTextAttrs, type TextAttrs } from '@/utils/prosemirror/utils'
 import { toggleList } from '@/utils/prosemirror/commands/toggleList'
 import tippy, { type Instance } from 'tippy.js'
+import { lang } from '@/main'
 
 import ColorPicker from '@/components/ColorPicker/index.vue'
 import Popover from '@/components/Popover.vue'
@@ -177,7 +178,7 @@ onMounted(() => {
       input: handleInput,
     },
   }, {
-    placeholder: '点击输入演讲者备注',
+    placeholder: lang.ssSpeakerNotePh,
   })
 
   menuInstance.value = tippy(editorViewRef.value!, {

+ 10 - 9
src/views/Editor/SearchPanel.vue

@@ -13,24 +13,24 @@
     />
 
     <div class="content" :class="type" @mousedown.stop>
-      <Input class="input" v-model:value="searchWord" placeholder="输入查找内容" @enter="searchNext()" ref="searchInpRef">
+      <Input class="input" v-model:value="searchWord" :placeholder="lang.ssFindInput" @enter="searchNext()" ref="searchInpRef">
         <template #suffix>
           <span class="count">{{searchIndex + 1}}/{{searchResults.length}}</span>
           <Divider type="vertical" />
           <span class="ignore-case"
             :class="{ 'active': modifiers === 'g' }"
-            v-tooltip="'忽略大小写'"
+            v-tooltip="lang.ssIgnoreCase"
             @click="toggleModifiers()"
           >Aa</span>
           <Divider type="vertical" />
-          <IconLeft class="next-btn left" @click="searchPrev()" v-tooltip="'上一个'" />
-          <IconRight class="next-btn right" @click="searchNext()" v-tooltip="'下一个'" />
+          <IconLeft class="next-btn left" @click="searchPrev()" v-tooltip="lang.ssPrevOne" />
+          <IconRight class="next-btn right" @click="searchNext()" v-tooltip="lang.ssNextOne" />
         </template>
       </Input>
-      <Input class="input" v-model:value="replaceWord" placeholder="输入替换内容" @enter="replace()" v-if="type === 'replace'"></Input>
+      <Input class="input" v-model:value="replaceWord" :placeholder="lang.ssReplInput" @enter="replace()" v-if="type === 'replace'"></Input>
       <div class="footer" v-if="type === 'replace'">
-        <Button :disabled="!searchWord" style="margin-left: 5px;" @click="replace()">替换</Button>
-        <Button :disabled="!searchWord" type="primary" style="margin-left: 5px;" @click="replaceAll()">全部替换</Button>
+        <Button :disabled="!searchWord" style="margin-left: 5px;" @click="replace()">{{ lang.ssReplace }}</Button>
+        <Button :disabled="!searchWord" type="primary" style="margin-left: 5px;" @click="replaceAll()">{{ lang.ssReplaceAll }}</Button>
       </div>
     </div>
   </MoveablePanel>
@@ -45,6 +45,7 @@ import Tabs from '@/components/Tabs.vue'
 import Divider from '@/components/Divider.vue'
 import Input from '@/components/Input.vue'
 import Button from '@/components/Button.vue'
+import { lang } from '@/main'
 
 type TypeKey = 'search' | 'replace'
 interface TabItem {
@@ -69,8 +70,8 @@ const {
 
 const type = ref<TypeKey>('search')
 const tabs: TabItem[] = [
-  { key: 'search', label: '查找' },
-  { key: 'replace', label: '替换' },
+  { key: 'search', label: lang.ssFindTab },
+  { key: 'replace', label: lang.ssReplTab },
 ]
 
 const close = () => {

+ 16 - 9
src/views/Editor/SelectPanel.vue

@@ -3,15 +3,15 @@
     class="select-panel" 
     :width="200" 
     :height="360" 
-    :title="`选择(${activeElementIdList.length}/${currentSlide.elements.length})`" 
+    :title="selectTitle"
     :left="-270" 
     :top="90"
     @close="close()"
   >
     <div class="handler" v-if="elements.length">
       <div class="btns">
-        <Button size="small" style="margin-right: 5px;" @click="showAllElements()">全部显示</Button>
-        <Button size="small" @click="hideAllElements()">全部隐藏</Button>
+        <Button size="small" style="margin-right: 5px;" @click="showAllElements()">{{ lang.ssShowAll }}</Button>
+        <Button size="small" @click="hideAllElements()">{{ lang.ssHideAll }}</Button>
       </div>
       <div class="icon-btns" v-if="handleElement">
         <IconDown class="icon-btn" @click="orderElement(handleElement!, ElementOrderCommands.UP)" />
@@ -21,7 +21,7 @@
     <div class="element-list">
       <template v-for="item in elements" :key="item.id">
         <div class="group-els" v-if="item.type === 'group'">
-          <div class="group-title">组合</div>
+          <div class="group-title">{{ lang.ssGroup }}</div>
           <div 
             class="item" 
             :class="{
@@ -35,14 +35,14 @@
           >
             <input 
               :id="`select-panel-input-${groupItem.id}`" 
-              :value="groupItem.name || ELEMENT_TYPE_ZH[groupItem.type]" 
+              :value="groupItem.name || getElementTypeZh()[groupItem.type]" 
               class="input" 
               type="text" 
               v-if="editingElId === groupItem.id" 
               @blur="$event => saveElementName($event, groupItem.id)"
               @keydown.enter="$event => saveElementName($event, groupItem.id)"
             >
-            <div v-else class="name">{{groupItem.name || ELEMENT_TYPE_ZH[groupItem.type]}}</div>
+            <div v-else class="name">{{ groupItem.name || getElementTypeZh()[groupItem.type] }}</div>
             <div class="icons">
               <IconPreviewClose style="font-size: 17px;" @click.stop="toggleHideElement(groupItem.id)" v-if="hiddenElementIdList.includes(groupItem.id)" />
               <IconPreviewOpen style="font-size: 17px;" @click.stop="toggleHideElement(groupItem.id)" v-else />
@@ -58,14 +58,14 @@
         >
           <input 
             :id="`select-panel-input-${item.id}`" 
-            :value="item.name || ELEMENT_TYPE_ZH[item.type]" 
+            :value="item.name || getElementTypeZh()[item.type]" 
             class="input" 
             type="text" 
             v-if="editingElId === item.id" 
             @blur="$event => saveElementName($event, item.id)"
             @keydown.enter="$event => saveElementName($event, item.id)"
           >
-          <div v-else class="name">{{item.name || ELEMENT_TYPE_ZH[item.type]}}</div>
+          <div v-else class="name">{{ item.name || getElementTypeZh()[item.type] }}</div>
           <div class="icons">
             <IconPreviewClose style="font-size: 17px;" @click.stop="toggleHideElement(item.id)" v-if="hiddenElementIdList.includes(item.id)" />
             <IconPreviewOpen style="font-size: 17px;" @click.stop="toggleHideElement(item.id)" v-else />
@@ -81,11 +81,12 @@ import { computed, nextTick, ref } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useSlidesStore, useMainStore } from '@/store'
 import type { PPTElement } from '@/types/slides'
-import { ELEMENT_TYPE_ZH } from '@/configs/element'
+import { getElementTypeZh } from '@/configs/element'
 import useOrderElement from '@/hooks/useOrderElement'
 import useHideElement from '@/hooks/useHideElement'
 import useSelectElement from '@/hooks/useSelectElement'
 import { ElementOrderCommands } from '@/types/edit'
+import { lang } from '@/main'
 
 import MoveablePanel from '@/components/MoveablePanel.vue'
 import Button from '@/components/Button.vue'
@@ -99,6 +100,12 @@ const { orderElement } = useOrderElement()
 const { selectElement } = useSelectElement()
 const { toggleHideElement, showAllElements, hideAllElements } = useHideElement()
 
+const selectTitle = computed(() => {
+  const values = [activeElementIdList.value.length, currentSlide.value.elements.length]
+  let i = 0
+  return lang.ssSelectCnt.replace(/\*/g, () => String(values[i++] ?? ''))
+})
+
 interface GroupElements {
   type: 'group'
   id: string

+ 21 - 20
src/views/Editor/Thumbnails/index2.vue

@@ -40,13 +40,13 @@
               :id="`section-title-input-${element?.sectionTag?.id || 'default'}`" 
               type="text"
               :value="element?.sectionTag?.title || ''"
-              placeholder="输入节名称"
+              :placeholder="lang.ssSectName"
               @blur="$event => saveSection($event)"
               @keydown.enter.stop="$event => saveSection($event)"
               v-if="editingSectionId === element?.sectionTag?.id || (index === 0 && editingSectionId === 'default')"
             >
             <span class="text" v-else>
-              <div class="text-content">{{ element?.sectionTag ? (element?.sectionTag?.title || '无标题节') : '默认节' }}</div>
+              <div class="text-content">{{ element?.sectionTag ? (element?.sectionTag?.title || lang.ssUntitledSec) : lang.ssDefSec }}</div>
             </span>
           </div>
           <div
@@ -68,7 +68,7 @@
       </template>
     </Draggable>
 
-    <div class="page-number">幻灯片 {{slideIndex + 1}} / {{slides.length}}</div>
+    <div class="page-number">{{ lang.ssSlidePage }} {{slideIndex + 1}} / {{slides.length}}</div>
   </div>
 </template>
 
@@ -85,6 +85,7 @@ import useScreening from '@/hooks/useScreening'
 import useLoadSlides from '@/hooks/useLoadSlides'
 import useAddSlidesOrElements from '@/hooks/useAddSlidesOrElements'
 import type { Slide } from '@/types/slides'
+import { lang } from '@/main'
 
 import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
 import Templates from './Templates.vue'
@@ -261,22 +262,22 @@ const contextmenusSection = (el: HTMLElement): ContextmenuItem[] => {
 
   return [
     {
-      text: '删除节',
+      text: lang.ssDelSect,
       handler: () => removeSection(sectionId),
     },
     {
-      text: '删除节和幻灯片',
+      text: lang.ssDelSectSlides,
       handler: () => {
         mainStore.setActiveElementIdList([])
         removeSectionSlides(sectionId)
       },
     },
     {
-      text: '删除所有节',
+      text: lang.ssDelAllSect,
       handler: removeAllSection,
     },
     {
-      text: '重命名节',
+      text: lang.ssRenSect,
       handler: () => editSection(sectionId),
     },
   ]
@@ -287,22 +288,22 @@ const { enterScreening, enterScreeningFromStart } = useScreening()
 const contextmenusThumbnails = (): ContextmenuItem[] => {
   return [
     {
-      text: '粘贴',
+      text: lang.ssPaste,
       subText: 'Ctrl + V',
       handler: pasteSlide,
     },
     {
-      text: '全选',
+      text: lang.ssSelectAll,
       subText: 'Ctrl + A',
       handler: selectAllSlide,
     },
     {
-      text: '新建页面',
+      text: lang.ssNewPage,
       subText: 'Enter',
       handler: createSlide,
     },
     {
-      text: '幻灯片放映',
+      text: lang.ssSlideShow,
       subText: 'F5',
       handler: enterScreeningFromStart,
     },
@@ -312,49 +313,49 @@ const contextmenusThumbnails = (): ContextmenuItem[] => {
 const contextmenusThumbnailItem = (): ContextmenuItem[] => {
   return [
     {
-      text: '剪切',
+      text: lang.ssCut,
       subText: 'Ctrl + X',
       handler: cutSlide,
     },
     {
-      text: '复制',
+      text: lang.ssCopy,
       subText: 'Ctrl + C',
       handler: copySlide,
     },
     {
-      text: '粘贴',
+      text: lang.ssPaste,
       subText: 'Ctrl + V',
       handler: pasteSlide,
     },
     {
-      text: '全选',
+      text: lang.ssSelectAll,
       subText: 'Ctrl + A',
       handler: selectAllSlide,
     },
     { divider: true },
     {
-      text: '新建页面',
+      text: lang.ssNewPage,
       subText: 'Enter',
       handler: createSlide,
     },
     {
-      text: '复制页面',
+      text: lang.ssDupPage,
       subText: 'Ctrl + D',
       handler: copyAndPasteSlide,
     },
     {
-      text: '删除页面',
+      text: lang.ssDelPage,
       subText: 'Delete',
       handler: () => deleteSlide(),
     },
     {
-      text: '增加节',
+      text: lang.ssAddSect,
       handler: createSection,
       disable: !!currentSlide.value.sectionTag,
     },
     { divider: true },
     {
-      text: '从当前放映',
+      text: lang.ssPlayFromCur,
       subText: 'Shift + F5',
       handler: enterScreening,
     },

+ 2 - 2
src/views/Editor/Toolbar/ElementAnimationPanel.vue

@@ -131,7 +131,7 @@ import {
   ANIMATION_DEFAULT_TRIGGER,
   ANIMATION_CLASS_PREFIX,
 } from '@/configs/animation'
-import { ELEMENT_TYPE_ZH } from '@/configs/element'
+import { getElementTypeZh } from '@/configs/element'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
 
 import Tabs from '@/components/Tabs.vue'
@@ -197,7 +197,7 @@ const animationSequence = computed(() => {
       const el = currentSlide.value.elements.find(el => el.id === animation.elId)
       if (!el) continue
 
-      const elType = ELEMENT_TYPE_ZH[el.type]
+      const elType = getElementTypeZh()[el.type]
       const animationEffect = animationEffects[animation.effect]
       animationSequence.push({
         ...animation,

+ 4 - 3
src/views/Editor/Toolbar/ElementStylePanel/AudioStylePanel.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="audio-style-panel">
     <div class="row">
-      <div style="width: 40%;">图标颜色:</div>
+      <div style="width: 40%;">{{ lang.ssIconColor }}</div>
       <Popover trigger="click" style="width: 60%;">
         <template #content>
           <ColorPicker
@@ -14,7 +14,7 @@
     </div>
 
     <div class="row switch-row">
-      <div style="width: 40%;">自动播放:</div>
+      <div style="width: 40%;">{{ lang.ssAutoplay }}</div>
       <div class="switch-wrapper" style="width: 60%;">
         <Switch 
           :value="handleAudioElement.autoplay" 
@@ -24,7 +24,7 @@
     </div>
 
     <div class="row switch-row">
-      <div style="width: 40%;">循环播放:</div>
+      <div style="width: 40%;">{{ lang.ssLoopPlay }}</div>
       <div class="switch-wrapper" style="width: 60%;">
         <Switch 
           :value="handleAudioElement.loop" 
@@ -41,6 +41,7 @@ import { storeToRefs } from 'pinia'
 import { useMainStore, useSlidesStore } from '@/store'
 import type { PPTAudioElement } from '@/types/slides'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
+import { lang } from '@/main'
 
 import ColorButton from '@/components/ColorButton.vue'
 import ColorPicker from '@/components/ColorPicker/index.vue'

+ 10 - 8
src/views/Editor/Toolbar/ElementStylePanel/ChartStylePanel/ChartDataEditor.vue

@@ -68,7 +68,7 @@
 
     <div class="btns">
       <div class="left">
-        图表类型:{{ CHART_TYPE_MAP[chartType] }}
+        {{ lang.ssChartType }}{{ CHART_TYPE_MAP[chartType] }}
         <Popover trigger="click" placement="top" v-model:value="chartTypeSelectVisible">
           <template #content>
             <PopoverMenuItem
@@ -78,13 +78,13 @@
               @click="chartType = item; chartTypeSelectVisible = false"
             >{{CHART_TYPE_MAP[item]}}</PopoverMenuItem>
           </template>
-          <span class="change">点击更换</span>
+          <span class="change">{{ lang.ssClickChange }}</span>
         </Popover>
       </div>
       <div class="right">
-        <Button class="btn" @click="closeEditor()">取消</Button>
-        <Button class="btn" @click="clear()">清空数据</Button>
-        <Button type="primary" class="btn" @click="getTableData()">确认</Button>
+        <Button class="btn" @click="closeEditor()">{{ lang.ssCancel }}</Button>
+        <Button class="btn" @click="clear()">{{ lang.ssClearData }}</Button>
+        <Button type="primary" class="btn" @click="getTableData()">{{ lang.ssApply }}</Button>
       </div>
     </div>
   </div>
@@ -94,8 +94,9 @@
 import { computed, onMounted, onUnmounted, ref } from 'vue'
 import type { ChartData, ChartType } from '@/types/slides'
 import { KEYS } from '@/configs/hotkey'
-import { CHART_TYPE_MAP } from '@/configs/chart'
+import { getChartTypeMap } from '@/configs/chart'
 import { pasteCustomClipboardString, pasteExcelClipboardString, pasteHTMLTableClipboardString } from '@/utils/clipboard'
+import { lang } from '@/main'
 import Button from '@/components/Button.vue'
 import Popover from '@/components/Popover.vue'
 import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
@@ -123,6 +124,7 @@ const selectedRange = ref([0, 0])
 const tempRangeSize = ref({ width: 0, height: 0 })
 const focusCell = ref<[number, number] | null>(null)
 const chartType = ref<ChartType>('bar')
+const CHART_TYPE_MAP = getChartTypeMap()
 
 // 当前选区的边框线条位置
 const rangeLines = computed(() => {
@@ -206,13 +208,13 @@ const getTableData = () => {
 
   // 第一行为系列名,第一列为项目名,实际数据从第二行第二列开始
   for (let rowIndex = 1; rowIndex < row; rowIndex++) {
-    let labelsItem = `类别${rowIndex}`
+    let labelsItem = lang.ssChartCat.replace(/\*/g, String(rowIndex))
     const labelInputRef = document.querySelector(`#cell-${rowIndex}-0`) as HTMLInputElement
     if (labelInputRef && labelInputRef.value) labelsItem = labelInputRef.value
     labels.push(labelsItem)
   }
   for (let colIndex = 1; colIndex < col; colIndex++) {
-    let legendsItem = `系列${colIndex}`
+    let legendsItem = lang.ssChartSer.replace(/\*/g, String(colIndex))
     const labelInputRef = document.querySelector(`#cell-0-${colIndex}`) as HTMLInputElement
     if (labelInputRef && labelInputRef.value) legendsItem = labelInputRef.value
     legends.push(legendsItem)

+ 6 - 5
src/views/Editor/Toolbar/ElementStylePanel/ChartStylePanel/ThemeColorsSetting.vue

@@ -1,10 +1,10 @@
 <template>
   <div class="theme-colors-setting">
-    <div class="title">图表主题配色</div>
+    <div class="title">{{ lang.ssChartThemeColors }}</div>
 
     <div class="list">
       <div class="row" v-for="(item, index) in themeColors" :key="index">
-        <div class="label" style="width: 40%;">主题配色{{ index + 1 }}</div>
+        <div class="label" style="width: 40%;">{{ lang.ssThemeColorNo.replace(/\*/g, String(index + 1)) }}</div>
         <Popover trigger="click" style="width: 60%;">
           <template #content>
             <ColorPicker
@@ -14,7 +14,7 @@
           </template>
           <div class="color-btn-wrap" style="width: 100%;">
             <ColorButton :color="item" />
-            <div class="delete-color-btn" v-tooltip="'删除'" @click.stop="deleteThemeColor(index)" v-if="index !== 0"><IconCloseSmall /></div>
+            <div class="delete-color-btn" v-tooltip="lang.ssDelete" @click.stop="deleteThemeColor(index)" v-if="index !== 0"><IconCloseSmall /></div>
           </div>
         </Popover>
       </div>
@@ -23,16 +23,17 @@
         :disabled="themeColors.length >= 10"
         @click="addThemeColor()"
       >
-        <IconPlus class="btn-icon" /> 添加主题色
+        <IconPlus class="btn-icon" /> {{ lang.ssAddThemeColor }}
       </Button>
     </div>
 
-    <Button class="btn" type="primary" @click="setThemeColors()">确认</Button>
+    <Button class="btn" type="primary" @click="setThemeColors()">{{ lang.ssConfirm }}</Button>
   </div>
 </template>
 
 <script lang="ts" setup>
 import { ref, onMounted } from 'vue'
+import { lang } from '@/main'
 import Popover from '@/components/Popover.vue'
 import ColorPicker from '@/components/ColorPicker/index.vue'
 import ColorButton from '@/components/ColorButton.vue'

+ 11 - 10
src/views/Editor/Toolbar/ElementStylePanel/ChartStylePanel/index.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="chart-style-panel">
     <Button class="full-width-btn" @click="chartDataEditorVisible = true">
-      <IconEdit class="btn-icon" /> 编辑图表
+      <IconEdit class="btn-icon" /> {{ lang.ssEditChart }}
     </Button>
 
     <Divider />
@@ -12,20 +12,20 @@
           @update:value="value => updateOptions({ stack: value })" 
           :value="stack"
           style="flex: 2;"
-        >堆叠样式</Checkbox>
+        >{{ lang.ssStackStyle }}</Checkbox>
         <Checkbox 
           v-if="handleChartElement.chartType === 'line'"
           @update:value="value => updateOptions({ lineSmooth: value })" 
           :value="lineSmooth"
           style="flex: 3;"
-        >使用平滑曲线</Checkbox>
+        >{{ lang.ssSmoothLine }}</Checkbox>
       </div>
   
       <Divider />
     </template>
 
     <div class="row">
-      <div style="width: 40%;">背景填充:</div>
+      <div style="width: 40%;">{{ lang.ssBgFill }}</div>
       <Popover trigger="click" style="width: 60%;">
         <template #content>
           <ColorPicker
@@ -37,7 +37,7 @@
       </Popover>
     </div>
     <div class="row">
-      <div style="width: 40%;">坐标与文字:</div>
+      <div style="width: 40%;">{{ lang.ssAxisText }}</div>
       <Popover trigger="click" style="width: 60%;">
         <template #content>
           <ColorPicker
@@ -49,7 +49,7 @@
       </Popover>
     </div>
     <div class="row">
-      <div style="width: 40%;">网格颜色:</div>
+      <div style="width: 40%;">{{ lang.ssGridColor }}</div>
       <Popover trigger="click" style="width: 60%;">
         <template #content>
           <ColorPicker
@@ -62,11 +62,11 @@
     </div>
 
     <div class="row">
-      <div style="width: 40%;">主题配色:</div>
+      <div style="width: 40%;">{{ lang.ssChartTheme }}</div>
       <Popover trigger="click" v-model:value="themesVisible" style="width: 60%;">
         <template #content>
           <div class="themes">
-            <div class="label">预置图表主题:</div>
+            <div class="label">{{ lang.ssPresetChartTheme }}</div>
             <div class="preset-themes">
               <div class="preset-theme" v-for="(item, index) in CHART_PRESET_THEMES" :key="index" @click="setThemeColors(item)">
                 <div 
@@ -77,7 +77,7 @@
                 ></div>
               </div>
             </div>
-            <div class="label">幻灯片主题:</div>
+            <div class="label">{{ lang.ssSlideTheme }}</div>
             <div class="preset-themes" :style="{ marginBottom: '-10px' }">
               <div class="preset-theme" @click="setThemeColors(theme.themeColors)">
                 <div 
@@ -89,7 +89,7 @@
               </div>
             </div>
             <Divider :margin="10" />
-            <Button class="full-width-btn" @click="themesVisible = false; themeColorsSettingVisible = true">自定义配色</Button>
+            <Button class="full-width-btn" @click="themesVisible = false; themeColorsSettingVisible = true">{{ lang.ssCustomColors }}</Button>
           </div>
         </template>
         <ColorListButton :colors="themeColors" />
@@ -130,6 +130,7 @@ import type { ChartData, ChartOptions, ChartType, PPTChartElement } from '@/type
 import emitter, { EmitterEvents } from '@/utils/emitter'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
 import { CHART_PRESET_THEMES } from '@/configs/chart'
+import { lang } from '@/main'
 
 import ElementOutline from '../../common/ElementOutline.vue'
 import ChartDataEditor from './ChartDataEditor.vue'

+ 4 - 3
src/views/Editor/Toolbar/ElementStylePanel/FrameStylePanel.vue

@@ -1,9 +1,9 @@
 <template>
   <div class="frame-style-panel">
     <div class="row">
-      <div>网页链接:</div>
-      <Input v-model:value="url" placeholder="请输入网页链接" />
-      <Button @click="updateURL()">确定</Button>
+      <div>{{ lang.ssTabWebLink }}:</div>
+      <Input v-model:value="url" :placeholder="lang.ssWebUrlPh" />
+      <Button @click="updateURL()">{{ lang.ssConfirm }}</Button>
     </div>
   </div>
 </template>
@@ -13,6 +13,7 @@ import { ref, watch, computed } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useMainStore, useSlidesStore } from '@/store'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
+import { lang } from '@/main'
 import Input from '@/components/Input.vue'
 import Button from '@/components/Button.vue'
 

+ 11 - 10
src/views/Editor/Toolbar/ElementStylePanel/ImageStylePanel.vue

@@ -8,11 +8,11 @@
     <ElementFlip />
 
     <ButtonGroup class="row" passive>
-      <Button first style="width: calc(100% / 6 * 5);" @click="clipImage()"><IconTailoring class="btn-icon" /> 裁剪图片</Button>
+      <Button first style="width: calc(100% / 6 * 5);" @click="clipImage()"><IconTailoring class="btn-icon" /> {{ lang.ssClipImage }}</Button>
       <Popover trigger="click" v-model:value="clipPanelVisible" style="width: calc(100% / 6);">
         <template #content>
           <div class="clip">
-            <div class="title">按形状:</div>
+            <div class="title">{{ lang.ssByShape }}</div>
             <div class="shape-clip">
               <div 
                 class="shape-clip-item" 
@@ -25,7 +25,7 @@
             </div>
 
             <template v-for="typeItem in ratioClipOptions" :key="typeItem.label">
-              <div class="title" v-if="typeItem.label">{{typeItem.label}}</div>
+              <div class="title" v-if="typeItem.label">{{ lang.ssByRatio.replace(/\*/g, typeItem.label) }}</div>
               <ButtonGroup class="row">
                 <Button 
                   style="flex: 1;"
@@ -42,7 +42,7 @@
     </ButtonGroup>
     
     <div class="row">
-      <div style="width: 40%;">圆角半径:</div>
+      <div style="width: 40%;">{{ lang.ssRadius }}</div>
       <NumberInput 
         :value="handleImageElement.radius || 0" 
         @update:value="value => updateImage({ radius: value })" 
@@ -61,10 +61,10 @@
     <Divider />
     
     <FileInput @change="files => replaceImage(files)">
-      <Button class="full-width-btn"><IconTransform class="btn-icon" /> 替换图片</Button>
+      <Button class="full-width-btn"><IconTransform class="btn-icon" /> {{ lang.ssReplaceImage }}</Button>
     </FileInput>
-    <Button class="full-width-btn" @click="resetImage()"><IconUndo class="btn-icon" /> 重置样式</Button>
-    <Button class="full-width-btn" @click="setBackgroundImage()"><IconTheme class="btn-icon" /> 设为背景</Button>
+    <Button class="full-width-btn" @click="resetImage()"><IconUndo class="btn-icon" /> {{ lang.ssResetStyle }}</Button>
+    <Button class="full-width-btn" @click="setBackgroundImage()"><IconTheme class="btn-icon" /> {{ lang.ssSetAsBg }}</Button>
   </div>
 </template>
 
@@ -76,6 +76,7 @@ import type { PPTImageElement, SlideBackground } from '@/types/slides'
 import { CLIPPATHS } from '@/configs/imageClip'
 import { getImageDataURL, getImageSize } from '@/utils/image'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
+import { lang } from '@/main'
 
 import ElementOutline from '../common/ElementOutline.vue'
 import ElementShadow from '../common/ElementShadow.vue'
@@ -92,13 +93,13 @@ import NumberInput from '@/components/NumberInput.vue'
 const shapeClipPathOptions = CLIPPATHS
 const ratioClipOptions = [
   {
-    label: '纵横比(正方形)',
+    label: lang.ssRatioSquare,
     children: [
       { key: '1:1', ratio: 1 / 1 },
     ],
   },
   {
-    label: '纵横比(纵向)',
+    label: lang.ssRatioPortrait,
     children: [
       { key: '2:3', ratio: 3 / 2 },
       { key: '3:4', ratio: 4 / 3 },
@@ -107,7 +108,7 @@ const ratioClipOptions = [
     ],
   },
   {
-    label: '纵横比(横向)',
+    label: lang.ssRatioLandscape,
     children: [
       { key: '3:2', ratio: 2 / 3 },
       { key: '4:3', ratio: 3 / 4 },

+ 4 - 3
src/views/Editor/Toolbar/ElementStylePanel/LatexStylePanel.vue

@@ -1,13 +1,13 @@
 <template>
   <div class="latex-style-panel">
     <div class="row">
-      <Button style="flex: 1;" @click="latexEditorVisible = true">编辑 LaTeX</Button>
+      <Button style="flex: 1;" @click="latexEditorVisible = true">{{ lang.ssEditLatex }}</Button>
     </div>
 
     <Divider />
 
     <div class="row">
-      <div style="width: 40%;">颜色:</div>
+      <div style="width: 40%;">{{ lang.ssColor }}</div>
       <Popover trigger="click" style="width: 60%;">
         <template #content>
           <ColorPicker
@@ -19,7 +19,7 @@
       </Popover>
     </div>
     <div class="row">
-      <div style="width: 40%;">粗细:</div>
+      <div style="width: 40%;">{{ lang.ssStrokeWidth }}</div>
       <NumberInput 
         :min="1"
         :max="3"
@@ -49,6 +49,7 @@ import { useMainStore, useSlidesStore } from '@/store'
 import type { PPTLatexElement } from '@/types/slides'
 import emitter, { EmitterEvents } from '@/utils/emitter'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
+import { lang } from '@/main'
 
 import ColorButton from '@/components/ColorButton.vue'
 import LaTeXEditor from '@/components/LaTeXEditor/index.vue'

+ 7 - 6
src/views/Editor/Toolbar/ElementStylePanel/LineStylePanel.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="line-style-panel">
     <div class="row">
-      <div style="width: 40%;">线条样式:</div>
+      <div style="width: 40%;">{{ lang.ssLineStyle }}</div>
       <SelectCustom style="width: 60%;">
         <template #options>
           <div class="option" v-for="item in lineStyleOptions" :key="item" @click="updateLine({ style: item })">
@@ -14,7 +14,7 @@
       </SelectCustom>
     </div>
     <div class="row">
-      <div style="width: 40%;">线条颜色:</div>
+      <div style="width: 40%;">{{ lang.ssLineColor }}</div>
       <Popover trigger="click" style="width: 60%;">
         <template #content>
           <ColorPicker
@@ -26,7 +26,7 @@
       </Popover>
     </div>
     <div class="row">
-      <div style="width: 40%;">线条宽度:</div>
+      <div style="width: 40%;">{{ lang.ssLineWidth }}</div>
       <NumberInput 
         :value="handleLineElement.width" 
         @update:value="value => updateLine({ width: value })" 
@@ -35,7 +35,7 @@
     </div>
     
     <div class="row">
-      <div style="width: 40%;">起点样式:</div>
+      <div style="width: 40%;">{{ lang.ssStartStyle }}</div>
       <SelectCustom style="width: 60%;">
         <template #options>
           <div class="option" v-for="item in lineMarkerOptions" :key="item" @click="updateLine({ points: [item, handleLineElement.points[1]] })">
@@ -48,7 +48,7 @@
       </SelectCustom>
     </div>
     <div class="row">
-      <div style="width: 40%;">终点样式:</div>
+      <div style="width: 40%;">{{ lang.ssEndStyle }}</div>
       <SelectCustom style="width: 60%;">
         <template #options>
           <div class="option" v-for="item in lineMarkerOptions" :key="item" @click="updateLine({ points: [handleLineElement.points[0], item] })">
@@ -64,7 +64,7 @@
     <Divider />
 
     <div class="row">
-      <Button style="flex: 1;" @click="updateLine({ start: handleLineElement.end, end: handleLineElement.start })"><IconSwitch /> 交换方向</Button>
+      <Button style="flex: 1;" @click="updateLine({ start: handleLineElement.end, end: handleLineElement.start })"><IconSwitch /> {{ lang.ssSwapDir }}</Button>
     </div>
 
     <Divider />
@@ -78,6 +78,7 @@ import { storeToRefs } from 'pinia'
 import { useMainStore, useSlidesStore } from '@/store'
 import type { LinePoint, LineStyleType, PPTLineElement } from '@/types/slides'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
+import { lang } from '@/main'
 
 import ElementShadow from '../common/ElementShadow.vue'
 import SVGLine from '../common/SVGLine.vue'

+ 14 - 13
src/views/Editor/Toolbar/ElementStylePanel/ShapeStylePanel.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="shape-style-panel">
     <div class="title">
-      <span>点击替换形状</span>
+      <span>{{ lang.ssClickReplaceShape }}</span>
       <IconDown />
     </div>
     <div class="shape-pool">
@@ -24,9 +24,9 @@
         :value="fillType" 
         @update:value="value => updateFillType(value as 'fill' | 'gradient' | 'pattern')"
         :options="[
-          { label: '纯色填充', value: 'fill' },
-          { label: '渐变填充', value: 'gradient' },
-          { label: '图片填充', value: 'pattern' },
+          { label: lang.ssSolidFill, value: 'fill' },
+          { label: lang.ssGradFill, value: 'gradient' },
+          { label: lang.ssImgFill, value: 'pattern' },
         ]"
       />
       <div style="width: 10px;" v-if="fillType !== 'pattern'"></div>
@@ -45,8 +45,8 @@
         @update:value="value => updateGradient({ type: value as GradientType })"
         v-else-if="fillType === 'gradient'"
         :options="[
-          { label: '线性渐变', value: 'linear' },
-          { label: '径向渐变', value: 'radial' },
+          { label: lang.ssLinearGrad, value: 'linear' },
+          { label: lang.ssRadialGrad, value: 'radial' },
         ]"
       />
     </div>
@@ -61,7 +61,7 @@
         />
       </div>
       <div class="row">
-        <div style="width: 40%;">当前色块:</div>
+        <div style="width: 40%;">{{ lang.ssCurColorBlock }}</div>
         <Popover trigger="click" style="width: 60%;">
           <template #content>
             <ColorPicker
@@ -73,7 +73,7 @@
         </Popover>
       </div>
       <div class="row" v-if="gradient.type === 'linear'">
-        <div style="width: 40%;">渐变角度:</div>
+        <div style="width: 40%;">{{ lang.ssGradAngle }}</div>
         <Slider
           style="width: 60%;"
           :min="0"
@@ -111,9 +111,9 @@
         :value="textAlign"
         @update:value="value => updateTextAlign(value as 'top' | 'middle' | 'bottom')"
       >
-        <RadioButton value="top" v-tooltip="'顶对齐'" style="flex: 1;"><IconAlignTextTopOne /></RadioButton>
-        <RadioButton value="middle" v-tooltip="'居中'" style="flex: 1;"><IconAlignTextMiddleOne /></RadioButton>
-        <RadioButton value="bottom" v-tooltip="'底对齐'" style="flex: 1;"><IconAlignTextBottomOne /></RadioButton>
+        <RadioButton value="top" v-tooltip="lang.ssAlignTop" style="flex: 1;"><IconAlignTextTopOne /></RadioButton>
+        <RadioButton value="middle" v-tooltip="lang.ssAlignMiddle" style="flex: 1;"><IconAlignTextMiddleOne /></RadioButton>
+        <RadioButton value="bottom" v-tooltip="lang.ssAlignBottom" style="flex: 1;"><IconAlignTextBottomOne /></RadioButton>
       </RadioGroup>
 
       <Divider />
@@ -128,12 +128,12 @@
 
     <div class="row">
       <CheckboxButton
-        v-tooltip="'双击连续使用'"
+        v-tooltip="lang.ssFormatPainter"
         style="flex: 1;"
         :checked="!!shapeFormatPainter"
         @click="toggleShapeFormatPainter()"
         @dblclick="toggleShapeFormatPainter(true)"
-      ><IconFormatBrush /> 形状格式刷</CheckboxButton>
+      ><IconFormatBrush /> {{ lang.ssShapePainter }}</CheckboxButton>
     </div>
   </div>
 </template>
@@ -148,6 +148,7 @@ import { getImageDataURL } from '@/utils/image'
 import emitter, { EmitterEvents } from '@/utils/emitter'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
 import useShapeFormatPainter from '@/hooks/useShapeFormatPainter'
+import { lang } from '@/main'
 
 import ElementOpacity from '../common/ElementOpacity.vue'
 import ElementOutline from '../common/ElementOutline.vue'

+ 4 - 3
src/views/Editor/Toolbar/ElementStylePanel/VideoStylePanel.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="video-style-panel">
-    <div class="title">视频预览封面</div>
+    <div class="title">{{ lang.ssVideoPoster }}</div>
     <div class="background-image-wrapper">
       <FileInput @change="files => setVideoPoster(files)">
         <div class="background-image">
@@ -11,11 +11,11 @@
       </FileInput>
     </div>
     <div class="row">
-      <Button style="flex: 1;" @click="updateVideo({ poster: '' })">重置封面</Button>
+      <Button style="flex: 1;" @click="updateVideo({ poster: '' })">{{ lang.ssResetPoster }}</Button>
     </div>
 
     <div class="row switch-row">
-      <div style="width: 40%;">自动播放:</div>
+      <div style="width: 40%;">{{ lang.ssAutoplay }}</div>
       <div class="switch-wrapper" style="width: 60%;">
         <Switch 
           :value="handleVideoElement.autoplay" 
@@ -33,6 +33,7 @@ import { useMainStore, useSlidesStore } from '@/store'
 import type { PPTVideoElement } from '@/types/slides'
 import { getImageDataURL } from '@/utils/image'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
+import { lang } from '@/main'
 
 import FileInput from '@/components/FileInput.vue'
 import Button from '@/components/Button.vue'

+ 23 - 20
src/views/Screen/BaseView.vue

@@ -42,14 +42,16 @@
       @mouseenter="rightToolsVisible = true"
     >
       <div class="content">
-        <div class="tool-btn page-number" @click="slideThumbnailModelVisible = true">幻灯片 {{slideIndex + 1}} / {{slides.length}}</div>
-        <IconWrite class="tool-btn" v-tooltip="'画笔工具'" @click="writingBoardToolVisible = true" />
-        <IconMagic class="tool-btn" v-tooltip="'激光笔'" :class="{ 'active': laserPen }" @click="laserPen = !laserPen" />
-        <IconStopwatchStart class="tool-btn" v-tooltip="'计时器'" :class="{ 'active': timerlVisible }" @click="timerlVisible = !timerlVisible" />
-        <IconListView class="tool-btn" v-tooltip="'演讲者视图'" @click="changeViewMode('presenter')" />
-        <IconOffScreenOne class="tool-btn" v-tooltip="'退出全屏'" v-if="fullscreenState" @click="manualExitFullscreen()" />
-        <IconFullScreenOne class="tool-btn" v-tooltip="'进入全屏'" v-else @click="enterFullscreen()" />
-        <IconPower class="tool-btn" v-tooltip="'结束放映'" @click="exitScreening()" />
+        <div class="tool-btn page-number" @click="slideThumbnailModelVisible = true">
+          {{ lang.ssSlidePage }} {{slideIndex + 1}} / {{slides.length}}
+        </div>
+        <IconWrite class="tool-btn" v-tooltip="lang.ssPenTool" @click="writingBoardToolVisible = true" />
+        <IconMagic class="tool-btn" v-tooltip="lang.ssLaserPen" :class="{ 'active': laserPen }" @click="laserPen = !laserPen" />
+        <IconStopwatchStart class="tool-btn" v-tooltip="lang.ssTimer" :class="{ 'active': timerlVisible }" @click="timerlVisible = !timerlVisible" />
+        <IconListView class="tool-btn" v-tooltip="lang.ssPresenterView" @click="changeViewMode('presenter')" />
+        <IconOffScreenOne class="tool-btn" v-tooltip="lang.ssExitFull" v-if="fullscreenState" @click="manualExitFullscreen()" />
+        <IconFullScreenOne class="tool-btn" v-tooltip="lang.ssEnterFull" v-else @click="enterFullscreen()" />
+        <IconPower class="tool-btn" v-tooltip="lang.ssEndPlay" @click="exitScreening()" />
       </div>
     </div>
 
@@ -61,6 +63,7 @@
 import { ref } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useSlidesStore } from '@/store'
+import { lang } from '@/main'
 import type { ContextmenuItem } from '@/components/Contextmenu/types'
 import { enterFullscreen } from '@/utils/fullscreen'
 import useScreening from '@/hooks/useScreening'
@@ -114,30 +117,30 @@ const laserPen = ref(false)
 const contextmenus = (): ContextmenuItem[] => {
   return [
     {
-      text: '上一页',
+      text: lang.ssPrevPage,
       subText: '↑ ←',
       disable: slideIndex.value <= 0,
       handler: () => turnPrevSlide(),
     },
     {
-      text: '下一页',
+      text: lang.ssNextPage,
       subText: '↓ →',
       disable: slideIndex.value >= slides.value.length - 1,
       handler: () => turnNextSlide(),
     },
     {
-      text: '第一页',
+      text: lang.ssFirstSlide,
       disable: slideIndex.value === 0,
       handler: () => turnSlideToIndex(0),
     },
     {
-      text: '最后一页',
+      text: lang.ssLastSlide,
       disable: slideIndex.value === slides.value.length - 1,
       handler: () => turnSlideToIndex(slides.value.length - 1),
     },
     { divider: true },
     {
-      text: autoPlayTimer.value ? '取消自动放映' : '自动放映',
+      text: autoPlayTimer.value ? lang.ssCancelAuto : lang.ssAutoPlayShow,
       handler: autoPlayTimer.value ? closeAutoPlay : autoPlay,
       children: [
         {
@@ -163,35 +166,35 @@ const contextmenus = (): ContextmenuItem[] => {
       ],
     },
     {
-      text: '循环放映',
+      text: lang.ssLoopShow,
       subText: loopPlay.value ? '√' : '',
       handler: () => setLoopPlay(!loopPlay.value),
     },
     { divider: true },
     {
-      text: '显示工具栏',
+      text: lang.ssShowToolbar,
       handler: () => rightToolsVisible.value = true,
     },
     {
-      text: '查看所有幻灯片',
+      text: lang.ssAllSlides,
       handler: () => slideThumbnailModelVisible.value = true,
     },
     {
-      text: '触底显示缩略图',
+      text: lang.ssBottomThumb,
       subText: bottomThumbnailsVisible.value ? '√' : '',
       handler: () => bottomThumbnailsVisible.value = !bottomThumbnailsVisible.value,
     },
     {
-      text: '画笔工具',
+      text: lang.ssPenTool,
       handler: () => writingBoardToolVisible.value = true,
     },
     {
-      text: '演讲者视图',
+      text: lang.ssPresenterView,
       handler: () => props.changeViewMode('presenter'),
     },
     { divider: true },
     {
-      text: '结束放映',
+      text: lang.ssEndPlay,
       subText: 'ESC',
       handler: exitScreening,
     },

+ 2 - 1
src/views/Screen/CountdownTimer.vue

@@ -53,7 +53,7 @@
         </div>
       </div>
       <div class="divider"></div>
-      <button class="primary-btn" @click="toggle()">{{ inTiming ? '暂停' : '开始计时' }}</button>
+      <button class="primary-btn" @click="toggle()">{{ inTiming ? lang.ssPause : lang.ssStartTimer }}</button>
     </div>
     <!-- <div class="close-btn" @click="emit('close')"><IconClose class="icon" /></div> -->
   </MoveablePanel>
@@ -63,6 +63,7 @@
 import { onUnmounted, ref, watch } from 'vue'
 
 import MoveablePanel from '@/components/MoveablePanel2.vue'
+import { lang } from '@/main'
 
 withDefaults(defineProps<{
   left?: number

+ 12 - 11
src/views/Screen/WritingBoardTool.vue

@@ -35,11 +35,11 @@
           <Popover placement="top" trigger="manual" :value="sizePopoverType === 'pen'" @hide="sizePopoverType = ''">
             <template #content>
               <div class="setting">
-                <div class="label">墨迹粗细:</div>
+                <div class="label">{{ lang.ssInkWidth }}</div>
                 <Slider class="size-slider" :min="4" :max="10" :step="2" v-model:value="penSize" />
               </div>
             </template>
-            <div class="btn" :class="{ 'active': writingBoardModel === 'pen' }" v-tooltip="'画笔'" @click="changeModel('pen')">
+            <div class="btn" :class="{ 'active': writingBoardModel === 'pen' }" v-tooltip="lang.ssPenTool" @click="changeModel('pen')">
               <IconWrite class="icon" />
             </div>
           </Popover>
@@ -52,40 +52,40 @@
                   <IconArrowRight class="icon" :class="{ 'active': shapeType === 'arrow' }" @click="shapeType = 'arrow'" />
                 </div>
                 <Divider type="vertical" />
-                <div class="label">墨迹粗细:</div>
+                <div class="label">{{ lang.ssInkWidth }}</div>
                 <Slider class="size-slider" :min="2" :max="8" :step="2" v-model:value="shapeSize" />
               </div>
             </template>
-            <div class="btn" :class="{ 'active': writingBoardModel === 'shape' }" v-tooltip="'形状'" @click="changeModel('shape')">
+            <div class="btn" :class="{ 'active': writingBoardModel === 'shape' }" v-tooltip="lang.ssShape" @click="changeModel('shape')">
               <IconGraphicDesign class="icon" />
             </div>
           </Popover>
           <Popover placement="top" trigger="manual" :value="sizePopoverType === 'mark'" @hide="sizePopoverType = ''">
             <template #content>
               <div class="setting">
-                <div class="label">墨迹粗细:</div>
+                <div class="label">{{ lang.ssInkWidth }}</div>
                 <Slider class="size-slider" :min="16" :max="40" :step="4" v-model:value="markSize" />
               </div>
             </template>
-            <div class="btn" :class="{ 'active': writingBoardModel === 'mark' }" v-tooltip="'荧光笔'" @click="changeModel('mark')">
+            <div class="btn" :class="{ 'active': writingBoardModel === 'mark' }" v-tooltip="lang.ssHighlight" @click="changeModel('mark')">
               <IconHighLight class="icon" />
             </div>
           </Popover>
           <Popover placement="top" trigger="manual" :value="sizePopoverType === 'eraser'" @hide="sizePopoverType = ''">
             <template #content>
               <div class="setting">
-                <div class="label">橡皮大小:</div>
+                <div class="label">{{ lang.ssEraserSz }}</div>
                 <Slider class="size-slider" :min="20" :max="200" :step="20" v-model:value="rubberSize" />
               </div>
             </template>
-            <div class="btn" :class="{ 'active': writingBoardModel === 'eraser' }" v-tooltip="'橡皮擦'" @click="changeModel('eraser')">
+            <div class="btn" :class="{ 'active': writingBoardModel === 'eraser' }" v-tooltip="lang.ssEraser" @click="changeModel('eraser')">
               <IconErase class="icon" />
             </div>
           </Popover>
-          <div class="btn" v-tooltip="'清除墨迹'" @click="clearCanvas()">
+          <div class="btn" v-tooltip="lang.ssClearInk" @click="clearCanvas()">
             <IconClear class="icon" />
           </div>
-          <div class="btn" :class="{ 'active': blackboard }" v-tooltip="'黑板'" @click="handleBlackboardToggle">
+          <div class="btn" :class="{ 'active': blackboard }" v-tooltip="lang.ssBlackboard" @click="handleBlackboardToggle">
             <IconFill class="icon" />
           </div>
           <div class="colors">
@@ -99,7 +99,7 @@
             ></div>
           </div>
         </div>
-        <div class="btn close" v-tooltip="'关闭画笔'" @click="closeWritingBoard()">
+        <div class="btn close" v-tooltip="lang.ssClosePen" @click="closeWritingBoard()">
           <IconClose class="icon" />
         </div>
       </div>
@@ -118,6 +118,7 @@ import MoveablePanel from '@/components/MoveablePanel.vue'
 import Slider from '@/components/Slider.vue'
 import Popover from '@/components/Popover.vue'
 import Divider from '@/components//Divider.vue'
+import { lang } from '@/main'
 
 const writingBoardColors = ['#000000', '#ffffff', '#1e497b', '#4e81bb', '#e2534d', '#9aba60', '#8165a0', '#47acc5', '#f9974c', '#ffff3a']
 

+ 4 - 3
src/views/Screen/hooks/useExecPlay.ts

@@ -2,6 +2,7 @@ import { onMounted, onUnmounted, ref } from 'vue'
 import { throttle } from 'lodash'
 import { storeToRefs } from 'pinia'
 import { useSlidesStore } from '@/store'
+import { lang } from '@/main'
 import { KEYS } from '@/configs/hotkey'
 import { ANIMATION_CLASS_PREFIX } from '@/configs/animation'
 import message from '@/utils/message'
@@ -135,7 +136,7 @@ export default () => {
     }
     else {
       if (loopPlay.value) turnSlideToIndex(slides.value.length - 1)
-      else throttleMassage('已经是第一页了')
+      else throttleMassage(lang.ssFirstPage)
     }
     inAnimation.value = false
   }
@@ -151,7 +152,7 @@ export default () => {
     else {
       if (loopPlay.value) turnSlideToIndex(0)
       else {
-        throttleMassage('已经是最后一页了')
+        throttleMassage(lang.ssLastPage)
         closeAutoPlay()
       }
       inAnimation.value = false
@@ -162,7 +163,7 @@ export default () => {
   const autoPlayInterval = ref(2500)
   const autoPlay = () => {
     closeAutoPlay()
-    message.success('开始自动放映')
+    message.success(lang.ssAutoPlayStart)
     autoPlayTimer.value = setInterval(execNext, autoPlayInterval.value)
   }
 

+ 2 - 1
src/views/Student/components/AIWorkModal.vue

@@ -4,7 +4,7 @@
       <template v-for="(item,index) in messageList" :key="item.id">
         <div v-if="item.messages || item.imageUrls">
           <div class="messageNode">
-            <div class="mn_title">节点{{ index+1 }}</div>
+            <div class="mn_title">{{ lang.ssNodeTitle.replace(/\*/g, String(index + 1)) }}</div>
             <div class="mn_content">
               <template v-for="(item2,index2) in item.messages" :key="`${index}-${index2}`">
                 <div>
@@ -43,6 +43,7 @@
 <script lang="ts" setup>
 import { computed, watch, ref} from 'vue'
 import Modal from '@/components/Modal.vue'
+import { lang } from '@/main'
 import MarkdownIt from 'markdown-it'
 import useImport from '@/hooks/useImport'
 // md.render(_addText)

+ 7 - 6
src/views/Student/components/ChoiceStatistics.vue

@@ -1,14 +1,14 @@
 <template>
   <div class="choice-statistics-panel">
     <div v-if="isChoiceQuestion && statistics" class="statistics-content">
-      <div class="statistics-title">选择题统计</div>
+      <div class="statistics-title">{{ lang.ssChoiceStat }}</div>
       <div class="statistics-summary">
-        总提交:{{ statistics.totalSubmissions }}
+        {{ lang.ssTotalSub.replace(/\*/g, String(statistics.totalSubmissions)) }}
       </div>
     </div>
     <div v-else class="statistics-empty">
-      <div v-if="!isChoiceQuestion">当前页面不是选择题</div>
-      <div v-else-if="!statistics">暂无统计数据</div>
+      <div v-if="!isChoiceQuestion">{{ lang.ssNotChoice }}</div>
+      <div v-else-if="!statistics">{{ lang.ssNoStat }}</div>
     </div>
 
 		<div class="statistics-chart">
@@ -19,7 +19,7 @@
 							<div>{{ item.title }}</div>
 						</div>
             <div class="t45_btn">
-              <span @click="lookStudent(item.checkList)">查看学生</span>
+              <span @click="lookStudent(item.checkList)">{{ lang.ssViewStu }}</span>
             </div>
 						<div class="t45_list">
 							<div class="t45_l_item" v-for="(i,index2) in item.checkList" :key="index+'_'+index2">
@@ -56,7 +56,7 @@
 			<div class="ub_item" v-for="(item,index) in userCheckList" :key="index">
 				<div class="ub_i_options">{{ item.options }}</div>
         <div class="ub_i_user">
-          <span>选择同学:</span>
+          <span>{{ lang.ssSelStu }}</span>
           <div class="ub_i_u_item" v-for="(item2,index2) in item.user" :key="index+'_'+index2">{{ item2 }}</div>
         </div>
 			</div>
@@ -68,6 +68,7 @@
 import { computed, reactive, ref } from 'vue'
 import { ElementTypes } from '@/types/slides'
 import Modal from '@/components/Modal.vue'
+import { lang } from '@/main'
 
 interface Props {
   workArray: any[]

+ 8 - 7
src/views/Student/components/ChoiceWorkModal.vue

@@ -66,8 +66,8 @@
       <span>{{ workData.testJson[showIndex].teststitle }}</span>
 
       <div class="cq_changeBtnArea" v-if="workData.testJson.length>1">
-        <span :class="{cq_cba_disabled: showIndex == 0}" @click="changeQuestion('prev')">上一题</span>
-        <span :class="{cq_cba_disabled: showIndex == workData.testJson.length - 1}" @click="changeQuestion('next')">下一题</span>
+        <span :class="{cq_cba_disabled: showIndex == 0}" @click="changeQuestion('prev')">{{ lang.ssPrevQ }}</span>
+        <span :class="{cq_cba_disabled: showIndex == workData.testJson.length - 1}" @click="changeQuestion('next')">{{ lang.ssNextQ }}</span>
       </div>
     </div>
 
@@ -75,7 +75,7 @@
     <img class="cq_image" v-if="workData.testJson[showIndex].timuList.length>0" :src="workData.testJson[showIndex].timuList[0].src" @click="openPreview(workData.testJson[showIndex].timuList[0])">
 
     <div class="cq_type" v-if="workData.testJson[showIndex]">
-      {{ workData.testJson[showIndex].type == 1 ? "单选题" : "多选题" }}
+      {{ workData.testJson[showIndex].type == 1 ? lang.ssSingleSel : lang.ssMultiSel }}
       <span v-if="workData.testJson.length>1">({{showIndex+1}}/{{workData.testJson.length}})</span>
 
     </div>
@@ -137,11 +137,11 @@
       <div class="image-preview__toolbar">
         <button @click.stop="zoomOut">-</button>
         <button @click.stop="zoomIn">+</button>
-        <button @click.stop="resetTransform">重置</button>
+        <button @click.stop="resetTransform">{{ lang.ssReset }}</button>
         <button @click.stop="rotateLeft">⟲</button>
         <button @click.stop="rotateRight">⟳</button>
-        <button @click.stop="toggleFit">{{ fitMode ? '实际大小' : '适应屏幕' }}</button>
-        <button @click.stop="closePreview">关闭</button>
+        <button @click.stop="toggleFit">{{ fitMode ? lang.ssActualSize : lang.ssFitScreen }}</button>
+        <button @click.stop="closePreview">{{ lang.ssClose }}</button>
       </div>
       <div class="image-preview__stage"
            @mousedown="onDragStart"
@@ -149,7 +149,7 @@
            @mouseup="onDragEnd"
            @mouseleave="onDragEnd"
            @dblclick.stop="toggleZoom">
-        <img :src="imageUrl" alt="预览" class="image-preview__img"
+        <img :src="imageUrl" :alt="lang.ssPreview" class="image-preview__img"
              :style="imgStyle" draggable="false" />
       </div>
     </div>
@@ -159,6 +159,7 @@
 <script lang="ts" setup>
 import { computed, ref, watch } from 'vue'
 import Modal from '@/components/Modal.vue'
+import { lang } from '@/main'
 import 'katex/dist/katex.min.css'
 import katex from 'katex'
 

+ 20 - 19
src/views/Student/components/DialoguePanel.vue

@@ -36,7 +36,7 @@
 			<div class="dp_clear_btn" 
 					v-if="!sendMessageLoading && !dialogueLoading"
 					@click.stop="openClearConfirm"
-					title="清空聊天记录">
+					:title="lang.ssClearChat">
 				  <svg
               width="20"
               height="20"
@@ -82,7 +82,7 @@
 						<textarea
 							v-model="inputText"
 							type="text"
-							placeholder="请输入"
+							:placeholder="lang.ssChatPhInput"
 							@keydown.enter.exact.prevent="sendMessage"
 						></textarea>
 					</div>
@@ -155,13 +155,13 @@
       @update:visible="val => clearConfirmVisible = val"
     >
       <div class="clear-confirm">
-        <div class="clear-confirm__title">清空聊天记录</div>
+        <div class="clear-confirm__title">{{ lang.ssClearChat }}</div>
         <div class="clear-confirm__content">
-          确定要清空所有聊天记录吗?此操作不可恢复。
+          {{ lang.ssClearChatTip }}
         </div>
         <div class="clear-confirm__footer">
-          <Button type="default" @click="clearConfirmVisible = false">取消</Button>
-          <Button type="primary" @click="handleConfirmClear">确认清空</Button>
+          <Button type="default" @click="clearConfirmVisible = false">{{ lang.ssCancel }}</Button>
+          <Button type="primary" @click="handleConfirmClear">{{ lang.ssConfirmClear }}</Button>
         </div>
       </div>
     </Modal>
@@ -177,6 +177,7 @@ import api from '../../../services/course'
 import message from '@/utils/message'
 import Modal from '@/components/Modal.vue'
 import Button from '@/components/Button.vue'
+import { lang } from '@/main'
 
 
 
@@ -205,10 +206,10 @@ const messageList = reactive<Message[]>([
   {
     id: '1',
     userContent: '',
-    aiContent: '你好!我是你的学习助手,有什么可以帮助你的吗?', // 你好!我是你的学习助手,有什么可以帮助你的吗?
+    aiContent: lang.ssAiHello,
     createTime: new Date().toLocaleString().replace(/\//g, '-'),
-    userName: '老师',
-    aiName: 'AI助手',
+    userName: lang.ssTeacher,
+    aiName: lang.ssAiHelper,
     loading: false,
   },
 ])
@@ -258,8 +259,8 @@ const sendMessage = () => {
     userContent: userInput,
     aiContent: '',
     createTime: new Date().toLocaleString().replace(/\//g, '-'),
-    userName: userName.value || '老师',
-    aiName: 'AI助手',
+    userName: userName.value || lang.ssTeacher,
+    aiName: lang.ssAiHelper,
     loading: true,
   }
   messageList.push(newMessage)
@@ -331,7 +332,7 @@ const sendMessage = () => {
         sendMessageLoading.value = false
         const errorMsgItem = messageList.find((item) => item.id === _uid)
         if (errorMsgItem) {
-          errorMsgItem.aiContent = '网络错误'
+          errorMsgItem.aiContent = lang.ssNetErrShort
         }
       },
     }
@@ -407,8 +408,8 @@ const getMessageList = () => {
             userContent: item.problem,
             aiContent: item.answer,
             createTime: item.createtime,
-            userName: userName.value || '老师',
-            aiName: 'AI助手',
+            userName: userName.value || lang.ssTeacher,
+            aiName: lang.ssAiHelper,
             loading: false,
           }
           messageList.push(oldMessage)
@@ -452,15 +453,15 @@ const clearChatHistory = async () => {
       messageList[0] = {
         id: '1',
         userContent: '',
-        aiContent: '你好!我是你的学习助手,有什么可以帮助你的吗?',
+        aiContent: lang.ssAiHello,
         createTime: new Date().toLocaleString().replace(/\//g, '-'),
-        userName: '老师',
-        aiName: 'AI助手',
+        userName: lang.ssTeacher,
+        aiName: lang.ssAiHelper,
         loading: false,
       }
 
       console.log('聊天记录清空成功')
-      message.success('聊天记录已清空')
+      message.success(lang.ssChatCleared)
     }
     else {
       throw new Error(`HTTP error! status: ${response.status}`)
@@ -468,7 +469,7 @@ const clearChatHistory = async () => {
   }
   catch (error) {
     console.error('清空聊天记录失败:', error)
-    message.error('清空聊天记录失败,请重试')
+    message.error(lang.ssClearFail)
   }
   finally {
     dialogueLoading.value = false

+ 5 - 4
src/views/Student/components/QAWorkModal.vue

@@ -19,7 +19,7 @@
       </div> -->
 
       <div class="qaa_title">{{ workData.answerQ }}</div>
-    <div class="qaa_type">问答题</div>
+    <div class="qaa_type">{{ lang.ssQATest }}</div>
     <div class="qaa_editArea">
       <wangEnduit
         ref="wangEnduitRef"
@@ -37,11 +37,11 @@
       <div class="image-preview__toolbar">
         <button @click.stop="zoomOut">-</button>
         <button @click.stop="zoomIn">+</button>
-        <button @click.stop="resetTransform">重置</button>
+        <button @click.stop="resetTransform">{{ lang.ssReset }}</button>
         <button @click.stop="rotateLeft">⟲</button>
         <button @click.stop="rotateRight">⟳</button>
-        <button @click.stop="toggleFit">{{ fitMode ? '实际大小' : '适应屏幕' }}</button>
-        <button @click.stop="closePreview">关闭</button>
+        <button @click.stop="toggleFit">{{ fitMode ? lang.ssActualSize : lang.ssFitScreen }}</button>
+        <button @click.stop="closePreview">{{ lang.ssClose }}</button>
       </div>
       <div class="image-preview__stage"
            @mousedown="onDragStart"
@@ -60,6 +60,7 @@
 import { computed, watch, ref } from 'vue'
 import wangEnduit from "./wangEnduit.vue";
 import Modal from '@/components/Modal.vue'
+import { lang } from '@/main'
 
 const props = defineProps<{
   visible: boolean

+ 10 - 9
src/views/Student/components/ShotWorkModal.vue

@@ -1,25 +1,25 @@
-<template>
+<template>
   <Modal :visible="visible" :width="800" :closeButton="true" @update:visible="val => emit('update:visible', val)">
     <div class="shot-wrap">
       <div v-if="imageUrl" class="image-container">
         <!-- Loading 状态 -->
         <div v-if="imageLoading" class="image-loading">
           <div class="loading-spinner"></div>
-          <div class="loading-text">图片加载中...</div>
+          <div class="loading-text">{{ lang.ssLoading }}</div>
         </div>
         
         <!-- 图片 -->
         <img 
           v-show="!imageLoading"
           :src="imageUrl" 
-          alt="截图作业" 
+          :alt="lang.ssShotWork" 
           class="shot-img" 
           @click="openPreview"
           @load="onImageLoad"
           @error="onImageError"
         />
       </div>
-      <div v-else class="shot-empty">暂无图片</div>
+      <div v-else class="shot-empty">{{ lang.ssNoImage }}</div>
     </div>
   </Modal>
 
@@ -29,11 +29,11 @@
       <div class="image-preview__toolbar">
         <button @click.stop="zoomOut">-</button>
         <button @click.stop="zoomIn">+</button>
-        <button @click.stop="resetTransform">重置</button>
+        <button @click.stop="resetTransform">{{ lang.ssReset }}</button>
         <button @click.stop="rotateLeft">⟲</button>
         <button @click.stop="rotateRight">⟳</button>
-        <button @click.stop="toggleFit">{{ fitMode ? '实际大小' : '适应屏幕' }}</button>
-        <button @click.stop="closePreview">关闭</button>
+        <button @click.stop="toggleFit">{{ fitMode ? lang.ssActualSize : lang.ssFitScreen }}</button>
+        <button @click.stop="closePreview">{{ lang.ssClose }}</button>
       </div>
       <div class="image-preview__stage"
            @mousedown="onDragStart"
@@ -44,13 +44,13 @@
         <!-- 预览中的 Loading 状态 -->
         <div v-if="previewImageLoading" class="preview-loading">
           <div class="loading-spinner"></div>
-          <div class="loading-text">图片加载中...</div>
+          <div class="loading-text">{{ lang.ssLoading }}</div>
         </div>
         
         <img 
           v-show="!previewImageLoading"
           :src="imageUrl" 
-          alt="预览" 
+          :alt="lang.ssPreview" 
           class="image-preview__img"
           :style="imgStyle" 
           draggable="false"
@@ -65,6 +65,7 @@
 <script lang="ts" setup>
 import { computed, ref, watch } from 'vue'
 import Modal from '@/components/Modal.vue'
+import { lang } from '@/main'
 
 const props = defineProps<{
   visible: boolean

+ 10 - 9
src/views/Student/components/answerTheResult.vue

@@ -1,9 +1,9 @@
 <template>
 	<div class="answerTheResult">
 		<div class="atr_detail">
-			<div class="atr_d_btn" @click="lookDetail()">{{isShowDialog ? '查看题目' : '查看结果'}}</div>
+			<div class="atr_d_btn" @click="lookDetail()">{{ isShowDialog ? lang.ssViewQ : lang.ssViewRes }}</div>
 			<div class="atr_d_msg">
-				<div>参与人数</div>
+				<div>{{ lang.ssParticipants }}</div>
 				<span
 					>{{ workArrayLength }}/{{
 						workArrayLength + unsubmittedStudentsLength
@@ -12,18 +12,18 @@
 			</div>
 
 			<div class="atr_d_msg" v-if="workDetail && workDetail.type === '45'">
-				<div>正确率</div>
+				<div>{{ lang.ssAccuracy }}</div>
 				<span v-if="choiceQuestionListData[workIndex]">{{choiceQuestionListData[workIndex].accuracyRate}}%({{choiceQuestionListData[workIndex].yes}}/{{choiceQuestionListData[workIndex].all}})</span>
 			</div>
 
 			<div class="atr_d_msg" v-if="choiceQuestionAnswer">
-				<div>正确答案</div>
+				<div>{{ lang.ssCorrectAns }}</div>
 				<span style="color: #03ae2b">{{ choiceQuestionAnswer }}</span>
 			</div>
 			<span class="atr_d_line" v-if="props.unsubmittedStudents && props.unsubmittedStudents?.length > 0"></span>
 
 			<div class="no_submit" v-if="props.unsubmittedStudents && props.unsubmittedStudents?.length > 0">
-				<div>未提交人员</div>
+				<div>{{ lang.ssUnsubStu }}</div>
 				<img
 					@click="showNoSubmitDetail = !showNoSubmitDetail"
 					:class="{ no_submit_active: !showNoSubmitDetail }"
@@ -47,7 +47,7 @@
       <span class="atr_d_line" v-if="props.workArray && props.workArray?.length > 0"></span>
 
       <div class="is_submit" v-if="props.workArray && props.workArray?.length > 0">
-				<div>已提交人员</div>
+				<div>{{ lang.ssSubStu }}</div>
 				<img
 					@click="showIsSubmitDetail = !showIsSubmitDetail"
 					:class="{ is_submit_active: !showIsSubmitDetail }"
@@ -103,7 +103,7 @@
 								<span v-else>{{ op.option }}</span>
 								
 							</div>
-							<span v-if="op.isAnswer">正确</span>
+							<span v-if="op.isAnswer">{{ lang.ssCorrectTag }}</span>
 						</div>
 						<img
 							@click="changeShow(op, idx)"
@@ -129,8 +129,8 @@
 			</template>
 
 			<div class="nextAndUpBtn" v-if="choiceQuestionListData.length>1">
-				<span :class="{no_active:workIndex==0}" @click="changeWorkIndex(0)">上一题</span>
-				<span :class="{no_active:choiceQuestionListData.length-1<=workIndex}"  @click="changeWorkIndex(1)">下一题</span>
+				<span :class="{no_active:workIndex==0}" @click="changeWorkIndex(0)">{{ lang.ssPrevQ }}</span>
+				<span :class="{no_active:choiceQuestionListData.length-1<=workIndex}"  @click="changeWorkIndex(1)">{{ lang.ssNextQ }}</span>
 			</div>
 		</div>
 	</div>
@@ -143,6 +143,7 @@
 import { ref, computed, watch, inject, type Ref } from 'vue'
 import previewImageTool from '../../components/tool/previewImageTool.vue'
 import api from '../../../services/course'
+import { lang } from '@/main'
 interface Props {
 	workArray?: object[] | null;
 	unsubmittedStudents?: object[] | null;

+ 12 - 11
src/views/Student/components/choiceQuestionDetailDialog.vue

@@ -21,8 +21,8 @@
             props.showData.choiceQuestionListData[props.showData.workIndex]
               .teststitle
           }}</div>
-          <span class="c_t45_t_btn" :class="{'c_t45_t_btn_noActive': props.showData.workIndex <= 0}" @click="changeWorkIndex(0)">上一题</span>
-          <span class="c_t45_t_btn" :class="{'c_t45_t_btn_noActive': props.showData.workIndex >= props.showData.choiceQuestionListData.length - 1}" @click="changeWorkIndex(1)">下一题</span>
+          <span class="c_t45_t_btn" :class="{'c_t45_t_btn_noActive': props.showData.workIndex <= 0}" @click="changeWorkIndex(0)">{{ lang.ssPrevQ }}</span>
+          <span class="c_t45_t_btn" :class="{'c_t45_t_btn_noActive': props.showData.workIndex >= props.showData.choiceQuestionListData.length - 1}" @click="changeWorkIndex(1)">{{ lang.ssNextQ }}</span>
         </div>
         <img
           class="c_t45_img"
@@ -41,7 +41,7 @@
             props.showData.choiceQuestionListData[props.showData.workIndex]
               .type === '1'
           "
-          >单选题</span
+          >{{ lang.ssSingleSel }}</span
         >
         <span
           class="c_t45_type"
@@ -49,7 +49,7 @@
             props.showData.choiceQuestionListData[props.showData.workIndex]
               .type === '2'
           "
-          >多选项</span
+          >{{ lang.ssMultiOpt }}</span
         >
         <div
           class="c_t45_echarts"
@@ -67,7 +67,7 @@
         v-if="workDetail && workDetail.type === '15' && props.showData"
       >
         <div class="c_t15_title">{{ workDetail.json.answerQ }}</div>
-        <span class="c_t15_type">问答题</span>
+        <span class="c_t15_type">{{ lang.ssQATest }}</span>
         <div class="c_t15_content" v-show="!lookWorkData">
           <div
             class="c_t15_c_item"
@@ -116,8 +116,8 @@
         class="c_t72"
         v-if="props.showData && props.showData.toolType === 72"
       >
-        <div class="c_t72_title">AI应用</div>
-        <span class="c_t72_type">AI应用</span>
+        <div class="c_t72_title">{{ lang.ssAiApp }}</div>
+        <span class="c_t72_type">{{ lang.ssAiApp }}</span>
         <div class="c_t72_content" v-show="!lookWorkData">
           <div
             class="c_t72_c_item"
@@ -147,8 +147,8 @@
               :key="item.id"
             >
               <div class="messageNodeArea" v-if="item.messages || item.imageUrls">
-                <div class="messageNode">
-                  <div class="mn_title">节点{{ index + 1 }}</div>
+                  <div class="messageNode">
+                  <div class="mn_title">{{ lang.ssNodeTitle.replace(/\*/g, String(index + 1)) }}</div>
                   <div class="mn_content">
                     <template
                       v-for="(item2, index2) in item.messages"
@@ -204,8 +204,8 @@
         class="c_t73"
         v-if="props.showData && props.showData.toolType === 73"
       >
-        <div class="c_t73_title">页面图片</div>
-        <span class="c_t73_type">H5页面</span>
+        <div class="c_t73_title">{{ lang.ssPageImage }}</div>
+        <span class="c_t73_type">{{ lang.ssHPage }}</span>
         <div class="c_t73_content" v-show="!lookWorkData">
           <div
             class="c_t73_c_item"
@@ -248,6 +248,7 @@ import * as echarts from 'echarts'
 import previewImageTool from '../../components/tool/previewImageTool.vue'
 import MarkdownIt from 'markdown-it'
 import useImport from '@/hooks/useImport'
+import { lang } from '@/main'
 const props = defineProps<{
   visible: number[];
   workIndex: number;

+ 80 - 115
src/views/Student/index.vue

@@ -4,15 +4,15 @@
     <div v-if="isLoading" class="loading-overlay">
       <div class="loading-content">
         <div class="loading-spinner"></div>
-        <div class="loading-text">正在加载课程内容...</div>
+        <div class="loading-text">{{ lang.ssCourseLoading }}</div>
       </div>
     </div>
     <!-- 左侧导航栏 -->
     <div class="layout-content-left" v-show="type == '1' || (type == '2' && !isFollowModeActive)" :class="{ collapsed: slidePanelCollapsed }">
       <div class="thumbnails">
         <div class="viewer-header slide-header">
-          <h3 v-show="!slidePanelCollapsed">课程大纲</h3>
-          <button class="collapse-btn" @click="slidePanelCollapsed = !slidePanelCollapsed" :title="slidePanelCollapsed ? '展开' : '收起'" style="right: 8px;">
+          <h3 v-show="!slidePanelCollapsed">{{ lang.ssCourseOutline }}</h3>
+          <button class="collapse-btn" @click="slidePanelCollapsed = !slidePanelCollapsed" :title="slidePanelCollapsed ? lang.ssExpand : lang.ssCollapse" style="right: 8px;">
             <span v-if="slidePanelCollapsed">
               <img src="@/assets/img/arrow.svg">
             </span>
@@ -36,18 +36,18 @@
     <!-- 中间放映区域 -->
     <div class="layout-content-center">
       <div class="viewer-header" :class="{ 'hidden': isFullscreen }" style="display: none;">
-        <div class="slide-title">幻灯片 {{ slideIndex + 1 }}</div>
+        <div class="slide-title">{{ slideTitleText }}</div>
         <div class="viewer-controls">
-          <button @click="previousSlide" :disabled="slideIndex === 0" title="上一页" v-if="!isFollowModeActive || props.type == '1'">
+          <button @click="previousSlide" :disabled="slideIndex === 0" :title="lang.ssPrevPage" v-if="!isFollowModeActive || props.type == '1'">
             <IconLeftTwo class="control-icon" />
           </button>
-          <button @click="nextSlide" :disabled="slideIndex === slides.length - 1" title="下一页" v-if="!isFollowModeActive || props.type == '1'">
+          <button @click="nextSlide" :disabled="slideIndex === slides.length - 1" :title="lang.ssNextPage" v-if="!isFollowModeActive || props.type == '1'">
             <IconRightTwo class="control-icon" />
           </button>
           <!-- <button @click="resetZoom" title="重置缩放">
                         <IconUndo class="control-icon" />
                     </button> -->
-          <button @click="enterFullscreen" title="全屏">
+          <button @click="enterFullscreen" :title="lang.ssFullscreen">
             <IconFullScreenOne class="control-icon" />
           </button>
           <!-- 只有创建人才显示跟随模式按钮 -->
@@ -56,7 +56,7 @@
             @click="toggleFollowMode" 
             :class="{ 'follow-active': isFollowModeActive }"
           >
-            {{ isFollowModeActive ? '关闭跟随模式' : '开启跟随模式' }}
+            {{ isFollowModeActive ? lang.ssFollowOff : lang.ssFollowOn }}
           </button>
           <!-- <button @click="backToEditor" class="back-btn" title="返回编辑">
                         <IconEdit class="control-icon" />
@@ -105,13 +105,13 @@
                 </div>
               </div>
               <div class="slide-bottom-right" v-if="!isFullscreen">
-                <Refresh class="tool-btn" v-tooltip="'刷新'" @click="handleRefreshPage" v-if="currentSlideHasIframe"/>
-                <UpTwo @click="handleHomeworkSubmit" v-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo && !isSubmitting" class="tool-btn upBtn" v-tooltip="'提交作业'"/>
-                <IconLoading v-else-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo" class="tool-btn loading" v-tooltip="'提交中...'"></IconLoading>
-                <IconStopwatchStart v-if="props.type == '1' && courseDetail.userid == props.userid && isFollowModeActive" class="tool-btn" v-tooltip="'计时器'" @click="timerlVisible = !timerlVisible"  />
-                <IconWrite v-if="isFollowModeActive && props.type == '1' && courseDetail.userid == props.userid" class="tool-btn" v-tooltip="'画笔工具'" @click="writingBoardToolVisible = true"  />
-                <IconMagic v-if="isFollowModeActive && props.type == '1' && courseDetail.userid == props.userid" class="tool-btn" v-tooltip="'激光笔'" :class="{ 'active': laserPen }" @click="toggleLaserPen"  />
-                <IconFullScreenOne class="tool-btn" v-tooltip="'打开全屏'" @click="enterFullscreen" />
+                <Refresh class="tool-btn" v-tooltip="lang.ssRefresh" @click="handleRefreshPage" v-if="currentSlideHasIframe"/>
+                <UpTwo @click="handleHomeworkSubmit" v-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo && !isSubmitting" class="tool-btn upBtn" v-tooltip="lang.ssSubmitHW"/>
+                <IconLoading v-else-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo" class="tool-btn loading" v-tooltip="lang.ssSubmitting"></IconLoading>
+                <IconStopwatchStart v-if="props.type == '1' && courseDetail.userid == props.userid && isFollowModeActive" class="tool-btn" v-tooltip="lang.ssTimer" @click="timerlVisible = !timerlVisible"  />
+                <IconWrite v-if="isFollowModeActive && props.type == '1' && courseDetail.userid == props.userid" class="tool-btn" v-tooltip="lang.ssPenTool" @click="writingBoardToolVisible = true"  />
+                <IconMagic v-if="isFollowModeActive && props.type == '1' && courseDetail.userid == props.userid" class="tool-btn" v-tooltip="lang.ssLaserPen" :class="{ 'active': laserPen }" @click="toggleLaserPen"  />
+                <IconFullScreenOne class="tool-btn" v-tooltip="lang.ssOpenFull" @click="enterFullscreen" />
               </div>
           </div>
         </div>
@@ -124,10 +124,10 @@
         <!-- 作业提交按钮 - 当当前幻灯片包含iframe时显示(排除B站视频) -->
         <div v-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo && isFullscreen" class="homework-submit-btn" :class="{ 'submitting': isSubmitting }"
           :style="{ right: getHomeworkButtonRight() + 'px' }" @click="handleHomeworkSubmit"
-          v-tooltip="isSubmitting ? '作业提交中...' : '作业提交'">
+          v-tooltip="isSubmitting ? lang.ssHwSubmitting : lang.ssHwSubmit">
           <!-- <IconEdit v-if="!isSubmitting" class="tool-btn" />
             <div v-else class="loading-spinner"></div> -->
-          <span class="btn-text">{{ isSubmitting ? '提交中...' : '提交' }}</span>
+          <span class="btn-text">{{ isSubmitting ? lang.ssSubmittingEll : lang.ssSubmit }}</span>
         </div>
 
         <!-- 刷新iframe按钮 -->
@@ -135,9 +135,9 @@
           v-if="currentSlideHasIframe && isFullscreen"
           :style="{ right: getRefreshButtonRight() + 'px' }" 
           @click="handleRefreshPage"
-          v-tooltip="'刷新iframe内容'">
+          v-tooltip="lang.ssRefreshIframe">
           <Refresh class="tool-btn" />
-          <span class="btn-text">刷新</span>
+          <span class="btn-text">{{ lang.ssRefresh }}</span>
         </div>
 
         <!-- 功能组件 -->
@@ -170,12 +170,13 @@
         <div v-if="isFullscreen && (!isFollowModeActive || props.type == '1')" class="tools-right" :class="{ 'visible': rightToolsVisible }"
           @mouseleave="rightToolsVisible = false" @mouseenter="rightToolsVisible = true">
           <div class="content">
-            <div class="tool-btn page-number" @click="slideThumbnailModelVisible = true">幻灯片 {{ slideIndex +
-              1 }} / {{ slides.length }}</div>
-            <IconWrite class="tool-btn" v-if="isFollowModeActive && props.type == '1' && courseDetail.userid == props.userid" v-tooltip="'画笔工具'" @click="writingBoardToolVisible = true" />
-            <IconMagic class="tool-btn" v-if="isFollowModeActive && props.type == '1' && courseDetail.userid == props.userid" v-tooltip="'激光笔'" :class="{ 'active': laserPen }" @click="toggleLaserPen" />
-            <IconStopwatchStart v-if="(props.type == '1' && courseDetail.userid == props.userid && isFollowModeActive)" class="tool-btn" v-tooltip="'计时器'" @click="timerlVisible = !timerlVisible" />
-            <IconOffScreenOne class="tool-btn" v-tooltip="'退出全屏'" @click="enterFullscreen" />
+            <div class="tool-btn page-number" @click="slideThumbnailModelVisible = true">
+              {{ lang.ssSlidePage }} {{slideIndex + 1}} / {{slides.length}}
+            </div>
+            <IconWrite class="tool-btn" v-if="isFollowModeActive && props.type == '1' && courseDetail.userid == props.userid" v-tooltip="lang.ssPenTool" @click="writingBoardToolVisible = true" />
+            <IconMagic class="tool-btn" v-if="isFollowModeActive && props.type == '1' && courseDetail.userid == props.userid" v-tooltip="lang.ssLaserPen" :class="{ 'active': laserPen }" @click="toggleLaserPen" />
+            <IconStopwatchStart v-if="(props.type == '1' && courseDetail.userid == props.userid && isFollowModeActive)" class="tool-btn" v-tooltip="lang.ssTimer" @click="timerlVisible = !timerlVisible" />
+            <IconOffScreenOne class="tool-btn" v-tooltip="lang.ssExitFull" @click="enterFullscreen" />
           </div>
         </div>
       </div>
@@ -183,7 +184,7 @@
     <div class="layout-content-right" v-show="type == '1'" :class="{ collapsed: workPanelCollapsed }">
       <div class="thumbnails">
         <div class="viewer-header right-panel-header">
-          <button class="collapse-btn" @click="workPanelCollapsed = !workPanelCollapsed" :title="workPanelCollapsed ? '展开' : '收起'" v-if="rightPanelMode != ''" style="left: 8px;">
+          <button class="collapse-btn" @click="workPanelCollapsed = !workPanelCollapsed" :title="workPanelCollapsed ? lang.ssExpand : lang.ssCollapse" v-if="rightPanelMode != ''" style="left: 8px;">
             <span v-if="workPanelCollapsed">
               <img src="@/assets/img/arrow.svg" style="transform: rotate(180deg);">
             </span>
@@ -199,17 +200,17 @@
               class="tab-btn" 
               :class="{ active: rightPanelMode === 'homework' }"
               @click="switchToHomework"
-              title="回答结果"
+              :title="lang.ssAnswerRes"
             >
-              回答结果
+              {{ lang.ssAnswerRes }}
             </button>
             <button 
               class="tab-btn" 
               :class="{ active: rightPanelMode === 'dialogue' }"
               @click="switchToDialogue"
-              title="对话区"
+              :title="lang.ssDialogArea"
             >
-              对话区
+              {{ lang.ssDialogArea }}
             </button>
             <!-- <button 
               v-if="isChoiceQuestion"
@@ -259,7 +260,7 @@
         
         <!-- 回答结果内容 -->
         <div v-show="!workPanelCollapsed && rightPanelMode === 'homework'" class="panel-content">
-          <div v-if="workLoading" class="homework-loading">正在加载作业...</div>
+          <div v-if="workLoading" class="homework-loading">{{ lang.ssHwLoading }}</div>
           <answerTheResult :toolType="toolType" :workId="workId" :workArray="workArray" :unsubmittedStudents="unsubmittedStudents" :slideIndex="slideIndex" v-else ref="answerTheResultRef" @openChoiceQuestionDetail="openChoiceQuestionDetail" @openWorkModal="openWorkModal"/>
           <!--<div class="homework-title">已提交</div>
           <div v-if="workLoading" class="homework-loading">正在加载作业...</div>
@@ -337,20 +338,20 @@
   <!-- 在适当位置添加连接状态指示器 -->
   <div class="connection-status" v-if="connectionStatus !== 'connected'">
     <div class="status-indicator" :class="connectionStatus">
-      <span v-if="connectionStatus === 'connecting'">连接中...</span>
-      <span v-else-if="connectionStatus === 'disconnected'">连接断开</span>
+      <span v-if="connectionStatus === 'connecting'">{{ lang.ssConnecting }}</span>
+      <span v-else-if="connectionStatus === 'disconnected'">{{ lang.ssConnLost }}</span>
     </div>
     <button v-if="connectionStatus === 'disconnected'" @click="manualReconnect" class="reconnect-btn">
-      重新连接
+      {{ lang.ssReconnect }}
     </button>
   </div>
 
   <div class="connection-status" v-if="false">
     <div class="status-indicator" :class="'disconnected'">
-      <span>连接断开</span>
+      <span>{{ lang.ssConnLost }}</span>
     </div>
     <button @click="manualReconnect" class="reconnect-btn">
-      重新连接
+      {{ lang.ssReconnect }}
     </button>
   </div>
 </template>
@@ -433,6 +434,8 @@ const props = withDefaults(defineProps<Props>(), {
 const slidesStore = useSlidesStore()
 const { slides, slideIndex, currentSlide, viewportSize, viewportRatio } = storeToRefs(slidesStore)
 
+const slideTitleText = computed(() => lang.ssSlideNum.replace(/\*/g, String(slideIndex.value + 1)))
+
 // 添加容器引用,用于计算幻灯片尺寸
 const viewerCanvasRef = ref<HTMLElement>()
 
@@ -594,7 +597,6 @@ const connectionStatus = ref<'disconnected' | 'connecting' | 'connected'>('disco
 // 认证 token 相关变量
 const authToken = ref<string | null>(null)
 const authTokenUpdateTimer = ref<NodeJS.Timeout | null>(null)
-const socketCheckTimer = ref<NodeJS.Timeout | null>(null)
 
 // 同步数据最大保留时间(40分钟)
 const SYNC_DATA_MAX_AGE = 40 * 60 * 1000 // 40分钟 = 40 * 60 * 1000毫秒
@@ -693,7 +695,7 @@ const openWorkModal = (work: WorkItem) => {
     visibleAI.value = true
   }
   else {
-    message.info('暂不支持的作业类型')
+    message.info(lang.ssHwTypeUnsup)
   }
 }
 
@@ -1395,11 +1397,11 @@ const processIframeLinks = async () => {
                     let newHash = hashPart
                     if (newHash.includes('?')) {
                       // 如果hash中已经有查询参数,添加&
-                      newHash += `&courseid=${props.courseid || ''}&layout=laptop`
+                      newHash += `&courseid=${props.courseid || ''}`
                     } 
                     else {
                       // 如果hash中没有查询参数,添加?
-                      newHash += `?courseid=${props.courseid || ''}&layout=laptop`
+                      newHash += `?courseid=${props.courseid || ''}`
                     }
 
                     // 构建新的URL
@@ -1696,7 +1698,7 @@ const uploadFile = (file: File): Promise<string> => {
         }
 
         const options = {
-          partSize: 5 * 1024 * 1024, // 2GB分片
+          partSize: 2048 * 1024 * 1024, // 2GB分片
           queueSize: 2,
           leavePartsOnError: true
         }
@@ -1711,7 +1713,7 @@ const uploadFile = (file: File): Promise<string> => {
           .send((err: any, data: any) => {
             if (err) {
               console.error('Upload failed:', err)
-              message.error('文件上传失败')
+              message.error(lang.ssFileUploadFail)
               reject(err)
             }
             else {
@@ -1741,7 +1743,7 @@ const handleHomeworkSubmit = async () => {
   }
 
   isSubmitting.value = true
-  let homeworkContent: string = '作业提交' // 默认作业内容
+  let homeworkContent: string = lang.ssHwSubmit // 默认作业内容
   let hasSubmitWork = false // 标记是否成功提交作业
 
   try {
@@ -1782,9 +1784,9 @@ const handleHomeworkSubmit = async () => {
               homeworkContent = String(result)
             }
             else {
-              homeworkContent = 'workPage作业提交'
+              homeworkContent = lang.ssHwSubmitWp
             }
-            message.success('作业提交成功')
+            message.success(lang.ssHwSubmitSucc)
             hasSubmitWork = true
             
             // 发送作业提交成功的socket消息
@@ -1813,11 +1815,6 @@ const handleHomeworkSubmit = async () => {
         // 由于TS类型检查,需通过 any 绕过类型限制
         const iframeWindow = iframe.contentWindow as any
         if (iframeWindow && iframeWindow.exposed_outputs) {
-          if (iframeWindow.exposed_outputs.length === 0) {
-            message.warning('没有找到作业内容')
-            hasSubmitWork = true
-            continue
-          }
           console.log('执行iframe中的submitWork方法,参数可变')
           const iframeSlideIndex = slideIndex.value
           const jsonString = JSON.stringify(iframeWindow.exposed_outputs)
@@ -1832,7 +1829,7 @@ const handleHomeworkSubmit = async () => {
             
             // 使用上传后的链接提交作业
             await submitWork(iframeSlideIndex, '72', fileUrl, '20')
-            message.success('作业提交成功')
+            message.success(lang.ssHwSubmitSucc)
             hasSubmitWork = true
             
             // 发送作业提交成功的socket消息
@@ -1846,7 +1843,7 @@ const handleHomeworkSubmit = async () => {
           catch (error) {
             console.error('文件上传失败:', error)
             isSubmitting.value = false
-            message.error('作业提交失败,请重试')
+            message.error(lang.ssHwSubmitRetry)
           }
         }
       }
@@ -1948,7 +1945,7 @@ const handleHomeworkSubmit = async () => {
             }
             catch (htmlToImageError) {
               console.log('html-to-image也失败了,使用canvas绘制方案:', htmlToImageError)
-              message.error('截图提交失败')
+              message.error(lang.ssShotFail)
               return
               /*
               // 最后的备用方案:使用canvas绘制
@@ -2041,7 +2038,7 @@ const handleHomeworkSubmit = async () => {
           homeworkContent = imageUrl // 保存截图URL作为作业内容
           // 提交截图
           await submitWork(slideIndex.value, '73', imageUrl, '1') // 73表示截图工具,21表示图片类型
-          message.success('页面截图提交成功')
+          message.success(lang.ssShotSucc)
           hasSubmitWork = true
           
           // 发送作业提交成功的socket消息
@@ -2055,7 +2052,7 @@ const handleHomeworkSubmit = async () => {
         catch (error) {
           console.error('截图提交失败:', error)
           isSubmitting.value = false
-          message.error('截图提交失败')
+          message.error(lang.ssShotFail)
         }
       }
       else {
@@ -2129,14 +2126,14 @@ const handleHomeworkSubmit = async () => {
     }
 
     if (!hasSubmitWork) {
-      message.info('未找到可用的作业提交功能')
+      message.info(lang.ssHwNoFunc)
     }
     isSubmitting.value = false
 
   }
   catch (error) {
     console.error('作业提交过程中出错:', error)
-    message.error('作业提交失败')
+    message.error(lang.ssHwSubmitFail)
     isSubmitting.value = false
     addOp3(1, new Date().getTime(), { courseid: props.courseid, homeworkContent }, 'error')
   }
@@ -2153,7 +2150,7 @@ const handleHomeworkSubmit = async () => {
 }
 
 const successSubmit = () => {
-  message.success('作业提交成功')
+  message.success(lang.ssHwSubmitSucc)
   sendMessage({
     type: 'homework_submitted',
     courseid: props.courseid,
@@ -2174,7 +2171,7 @@ const handleRefreshPage = () => {
     console.log('找到iframe元素数量:', iframes.length)
 
     if (iframes.length === 0) {
-      message.warning('当前页面没有找到iframe元素')
+      message.warning(lang.ssNoIframe)
       return
     }
 
@@ -2208,7 +2205,7 @@ const handleRefreshPage = () => {
     }
 
     if (refreshedCount > 0) {
-      message.success(`刷新完成`)
+      message.success(lang.ssRefreshDone)
       
       // 如果当前页面有iframe,重新获取作业数据
       if (currentSlideHasIframe.value && props.type == '1') {
@@ -2220,12 +2217,12 @@ const handleRefreshPage = () => {
       isSubmitting.value = false
     }
     else {
-      message.info('没有找到可刷新的iframe')
+      message.info(lang.ssNoIframeRef)
     }
   }
   catch (error) {
     console.error('刷新iframe时出错:', error)
-    message.error('刷新iframe失败')
+    message.error(lang.ssRefreshFail)
   }
 }
 
@@ -2390,7 +2387,7 @@ const checkPPTFile = async (jsonObj: any) => {
     pptJsonFileid.value = data1[0].fileid
   }
   else {
-    const pptJsonFile = new File([jsonObj], courseDetail.value.title + '.json', { type: 'application/json' })
+    const pptJsonFile = new File([jsonObj], courseDetail.value.courseName + '.json', { type: 'application/json' })
     uploadFile2(pptJsonFile, props.courseid as string)
   }
 }
@@ -2430,7 +2427,7 @@ const getCourseDetail = async () => {
   }
   catch (error) {
     console.error('获取课程详情失败:', error)
-    message.error('获取课程详情失败')
+    message.error(lang.ssFetchCourseFail)
     isLoading.value = false
   }
   finally {
@@ -2527,7 +2524,7 @@ const getWork = async (isUpdate = false) => {
   catch (error) {
     console.error('getWork 执行失败:', error)
     if (!isUpdate) {
-      message.error('获取作业信息失败')
+      message.error(lang.ssWorkInfoFail)
     }
     getWorkLoading.value = false
   }
@@ -2555,7 +2552,7 @@ const selectWorksStudent = async () => {
   }
   catch (error) {
     console.error('获取学生信息失败:', error)
-    message.error('获取学生信息失败')
+    message.error(lang.ssStuInfoFail)
   }
   finally {
     studentLoading.value = false
@@ -2601,7 +2598,7 @@ const selectCourseSLook = async (type = 2) => {
     }
   }
   if (props.type == '2') {
-    message.success(isFollowModeActive.value ? '跟随模式已开启' : '自由模式已开启')
+    message.success(isFollowModeActive.value ? lang.ssFollowOnTip : lang.ssFreeOnTip)
   }
   
   checkParentMode()
@@ -2620,7 +2617,7 @@ const toggleFollowMode = async () => {
     
     if (res) {
       isFollowModeActive.value = newFollowState
-      message.success(newFollowState ? '跟随模式已开启' : '自由模式已开启')
+      message.success(newFollowState ? lang.ssFollowOnTip : lang.ssFreeOnTip)
       
       // 如果开启跟随模式,设置当前幻灯片为跟随目标
       if (newFollowState) {
@@ -2633,13 +2630,13 @@ const toggleFollowMode = async () => {
       handleWritingBoardClose() 
     }
     else {
-      message.error('操作失败,请重试')
+      message.error(lang.ssOpFailRetry)
     }
     checkParentMode()
   }
   catch (error) {
     console.error('切换跟随模式失败:', error)
-    message.error('操作失败,请重试')
+    message.error(lang.ssOpFailRetry)
   }
 }
 
@@ -3417,17 +3414,11 @@ onUnmounted(() => {
     authTokenUpdateTimer.value = null
   }
 
-  // 清理 socket 连接检查定时器
-  if (socketCheckTimer.value) {
-    clearInterval(socketCheckTimer.value)
-    socketCheckTimer.value = null
+  if (providerSocket.value) {
+    providerSocket.value.destroy()
+    providerSocket.value = null
   }
 
-  // if (providerSocket.value) {
-  //   providerSocket.value.destroy()
-  //   providerSocket.value = null
-  // }
-
   // 清理画图延迟发送定时器
   if (drawingDelayTimer.value) {
     clearTimeout(drawingDelayTimer.value)
@@ -3541,7 +3532,7 @@ const manualReconnect = () => {
 }
 
 // 创建WebSocket连接
-const createWebSocketConnection = async (type = 1) => {
+const createWebSocketConnection = async () => {
   if (!api.yweb_socket || isConnecting.value) return
   
   isConnecting.value = true
@@ -3549,10 +3540,10 @@ const createWebSocketConnection = async (type = 1) => {
   
   try {
     // 清理之前的连接
-    // if (providerSocket.value && type == 1) {
-    //   providerSocket.value.destroy()
-    //   providerSocket.value = null
-    // }
+    if (providerSocket.value) {
+      providerSocket.value.destroy()
+      providerSocket.value = null
+    }
     
     // 清理之前的 token 更新定时器
     if (authTokenUpdateTimer.value) {
@@ -3656,7 +3647,7 @@ const createWebSocketConnection = async (type = 1) => {
         console.log('👉 WebSocket连接断开')
         connectionStatus.value = 'disconnected'
         isConnecting.value = false
-        createWebSocketConnection(2)
+        handleDisconnection()
       }
     })
     
@@ -3665,7 +3656,7 @@ const createWebSocketConnection = async (type = 1) => {
       console.error('👉 WebSocket连接错误:', error)
       connectionStatus.value = 'disconnected'
       isConnecting.value = false
-      createWebSocketConnection(2)
+      handleDisconnection()
     })
     
   }
@@ -3673,11 +3664,8 @@ const createWebSocketConnection = async (type = 1) => {
     console.error('👉 创建WebSocket连接失败:', error)
     connectionStatus.value = 'disconnected'
     isConnecting.value = false
-    createWebSocketConnection(2)
+    handleDisconnection()
   }
-
-  // 启动 socket 连接检查定时器
-  startSocketCheckTimer()
 }
 
 // 处理连接断开
@@ -3688,36 +3676,13 @@ const handleDisconnection = () => {
     
     reconnectTimer.value = setTimeout(() => {
       createWebSocketConnection()
-    }, reconnectInterval.value) as unknown as NodeJS.Timeout
+    }, reconnectInterval.value)
   }
   else {
     console.error('👉 WebSocket重连次数已达上限,停止重连')
     // 可以在这里显示用户提示
-    message.error('网络连接异常,请检查网络后刷新页面')
-  }
-}
-
-// 启动 socket 连接检查定时器
-const startSocketCheckTimer = () => {
-  // 清理之前的定时器
-  if (socketCheckTimer.value) {
-    clearInterval(socketCheckTimer.value)
-    socketCheckTimer.value = null
+    message.error(lang.ssNetError)
   }
-  
-  // 每10秒检查一次 socket 连接状态
-  socketCheckTimer.value = setInterval(() => {
-    if (providerSocket.value) {
-      // 直接检查 providerSocket 的连接状态
-      // WebsocketProvider 有一个 connected 属性来表示连接状态
-      const isConnected = (providerSocket.value as any).ws.readyState
-      console.log('🔍 定时器检查 socket 连接状态:', isConnected)
-      if (isConnected !== 1) {
-        console.log('🔍 定时器检查发现 socket 未连接,执行重连')
-        createWebSocketConnection(2)
-      }
-    }
-  }, 10000) as unknown as NodeJS.Timeout
 }
 
 // 工具函数:格式化时间

+ 2 - 1
src/views/components/ThumbnailSlide/index.vue

@@ -27,7 +27,7 @@
       <!-- 遮罩层:确保iframe不会阻止缩略图点击事件 -->
       <div class="thumbnail-mask" @click="handleMaskClick"></div>
     </div>
-    <div class="placeholder" v-else>加载中 ...</div>
+    <div class="placeholder" v-else>{{ lang.ssLoading }}</div>
   </div>
 </template>
 
@@ -38,6 +38,7 @@ import { useSlidesStore } from "@/store";
 import type { Slide } from "@/types/slides";
 import { injectKeySlideScale } from "@/types/injectKey";
 import useSlideBackgroundStyle from "@/hooks/useSlideBackgroundStyle";
+import { lang } from "@/main";
 
 import ThumbnailElement from "./ThumbnailElement.vue";
 

+ 2 - 1
src/views/components/element/AudioElement/AudioPlayer.vue

@@ -75,6 +75,7 @@
 <script lang="ts" setup>
 import { computed, ref, useTemplateRef } from 'vue'
 import message from '@/utils/message'
+import { lang } from '@/main'
 
 const props = withDefaults(defineProps<{
   src: string
@@ -184,7 +185,7 @@ const handleProgress = () => {
   loaded.value = audioRef.value?.buffered.length ? audioRef.value.buffered.end(audioRef.value.buffered.length - 1) : 0
 }
 
-const handleError = () => message.error('视频加载失败')
+const handleError = () => message.error(lang.ssAudioLoadFail)
 
 const thumbMove = (e: MouseEvent | TouchEvent) => {
   if (!audioRef.value || !playBarWrapRef.value) return

+ 14 - 12
src/views/components/element/FrameElement/BaseFrameElement.vue

@@ -73,14 +73,14 @@
         <div v-else-if="!isThumbnail && !isVisible" class="iframe-placeholder">
           <div class="placeholder-content">
             <div class="placeholder-icon">🌐</div>
-            <div class="placeholder-text">互动工具</div>
+            <div class="placeholder-text">{{ lang.ssInteract }}</div>
             <div class="placeholder-type">({{ getTypeLabel(Number(elementInfo.toolType)) }})</div>
           </div>
         </div>
         <!-- 缩略图模式 -->
         <div v-else-if="isThumbnail" class="thumbnail-content">
           <div class="thumbnail-content-inner">
-            <div>互动工具</div>
+            <div>{{ lang.ssInteract }}</div>
             <div>({{ getTypeLabel(Number(elementInfo.toolType)) }})</div>
           </div>
         </div>
@@ -94,6 +94,7 @@
 <script lang="ts" setup>
 import type { PropType } from 'vue'
 import type { PPTFrameElement } from '@/types/slides'
+import { lang } from '@/main'
 import { ref, watch, nextTick } from 'vue'
 
 const props = defineProps({
@@ -123,17 +124,18 @@ watch(() => props.elementInfo.url, (newUrl, oldUrl) => {
 })
 
 // 获取类型标签
-const getTypeLabel = (type: number) => {
-  const typeMap: Record<number, string> = {
-    45: '选择题',
-    15: '问答题',
-    72: 'AI应用',
-    73: 'H5页面',
-    74: '视频',
-    75: 'B站视频',
-    76: '创作空间'
+const getTypeLabel = (type: number): string => {
+  const typeMap: Record<number, keyof typeof lang> = {
+    45: 'ssChoiceQ',
+    15: 'ssEssayQ',
+    72: 'ssAIApp',
+    73: 'ssH5Page',
+    74: 'ssVideo',
+    75: 'ssBilibili',
+    76: 'ssCreateSpace'
   }
-  return typeMap[type] || '未知'
+  const key = typeMap[type]
+  return (key ? lang[key] : lang.ssUnknown) as string
 }
 
 // 处理iframe加载完成事件

+ 2 - 1
src/views/components/element/ProsemirrorEditor.vue

@@ -24,6 +24,7 @@ import { setListStyle } from '@/utils/prosemirror/commands/setListStyle'
 import { replaceText } from '@/utils/prosemirror/commands/replaceText'
 import type { TextFormatPainterKeys } from '@/types/edit'
 import message from '@/utils/message'
+import { lang } from '@/main'
 import { KEYS } from '@/configs/hotkey'
 
 const props = withDefaults(defineProps<{
@@ -130,7 +131,7 @@ const execCommand = ({ target, action }: RichTextCommand) => {
       addMark(editorView, mark)
 
       if (item.value && !document.fonts.check(`16px ${item.value}`)) {
-        message.warning('字体需要等待加载下载后生效,请稍等')
+        message.warning(lang.ssFontLoadWait)
       }
     }
     else if (item.command === 'fontsize' && item.value) {

+ 14 - 13
src/views/components/element/TableElement/EditableTable.vue

@@ -72,6 +72,7 @@ import { debounce, isEqual } from 'lodash'
 import { storeToRefs } from 'pinia'
 import { nanoid } from 'nanoid'
 import { useMainStore } from '@/store'
+import { lang } from '@/main'
 import type { PPTElementOutline, TableCell, TableTheme } from '@/types/slides'
 import type { ContextmenuItem } from '@/components/Contextmenu/types'
 import { KEYS } from '@/configs/hotkey'
@@ -692,51 +693,51 @@ const contextmenus = (el: HTMLElement): ContextmenuItem[] => {
 
   return [
     {
-      text: '插入列',
+      text: lang.ssInsertCol,
       children: [
-        { text: '到左侧', handler: () => insertCol(colIndex) },
-        { text: '到右侧', handler: () => insertCol(colIndex + 1) },
+        { text: lang.ssToLeft, handler: () => insertCol(colIndex) },
+        { text: lang.ssToRight, handler: () => insertCol(colIndex + 1) },
       ],
     },
     {
-      text: '插入行',
+      text: lang.ssInsertRow,
       children: [
-        { text: '到上方', handler: () => insertRow(rowIndex) },
-        { text: '到下方', handler: () => insertRow(rowIndex + 1) },
+        { text: lang.ssAbove, handler: () => insertRow(rowIndex) },
+        { text: lang.ssBelow, handler: () => insertRow(rowIndex + 1) },
       ],
     },
     {
-      text: '删除列',
+      text: lang.ssDeleteCol,
       disable: !canDeleteCol,
       handler: () => deleteCol(colIndex),
     },
     {
-      text: '删除行',
+      text: lang.ssDeleteRow,
       disable: !canDeleteRow,
       handler: () => deleteRow(rowIndex),
     },
     { divider: true },
     {
-      text: '合并单元格',
+      text: lang.ssMergeCells,
       disable: !canMerge,
       handler: mergeCells,
     },
     {
-      text: '取消合并单元格',
+      text: lang.ssUnmergeCells,
       disable: !canSplit,
       handler: () => splitCells(rowIndex, colIndex),
     },
     { divider: true },
     {
-      text: '选中当前列',
+      text: lang.ssSelectCurCol,
       handler: () => selectCol(colIndex),
     },
     {
-      text: '选中当前行',
+      text: lang.ssSelectCurRow,
       handler: () => selectRow(rowIndex),
     },
     {
-      text: '选中全部单元格',
+      text: lang.ssSelectAllCells,
       handler: selectAll,
     },
   ]

+ 2 - 1
src/views/components/element/TableElement/index.vue

@@ -38,7 +38,7 @@
           @mousedown="$event => handleSelectElement($event)"
           @touchstart="$event => handleSelectElement($event)"
         >
-          <div class="mask-tip" v-if="handleElementId === elementInfo.id" :style="{ transform: `scale(${ 1 / canvasScale })` }">双击编辑</div>
+          <div class="mask-tip" v-if="handleElementId === elementInfo.id" :style="{ transform: `scale(${ 1 / canvasScale })` }">{{ lang.ssDblClickEdit }}</div>
         </div>
       </div>
     </div>
@@ -49,6 +49,7 @@
 import { nextTick, onMounted, onUnmounted, ref, watch, useTemplateRef } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useMainStore, useSlidesStore } from '@/store'
+import { lang } from '@/main'
 import type { PPTTableElement, TableCell } from '@/types/slides'
 import type { ContextmenuItem } from '@/components/Contextmenu/types'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'

+ 4 - 3
src/views/components/element/VideoElement/VideoPlayer/index.vue

@@ -11,7 +11,7 @@
     @click="autoHideController()"
   >
     <div class="video-wrap" @click="toggle()">
-      <div class="load-error" v-if="loadError">视频加载失败</div>
+      <div class="load-error" v-if="loadError">{{ lang.ssVideoLoadFail }}</div>
 
       <canvas ref="bgCanvasRef" class="bg-canvas"></canvas>
       <video
@@ -81,7 +81,7 @@
       <div class="icons icons-right">
         <div class="speed">
           <div class="icon speed-icon">
-            <span class="icon-content" @click="speedMenuVisible = !speedMenuVisible">{{playbackRate === 1 ? '倍速' : (playbackRate + 'x')}}</span>
+            <span class="icon-content" @click="speedMenuVisible = !speedMenuVisible">{{ playbackRate === 1 ? lang.ssPlaybackRate : (playbackRate + 'x') }}</span>
             <div class="speed-menu" v-if="speedMenuVisible" @mouseleave="speedMenuVisible = false">
               <div 
                 class="speed-menu-item" 
@@ -95,7 +95,7 @@
         </div>
         <div class="loop" @click="toggleLoop()">
           <div class="icon loop-icon" :class="{ 'active': loop }">
-            <span class="icon-content">循环{{loop ? '开' : '关'}}</span>
+            <span class="icon-content">{{ lang.ssLoop }} {{ loop ? lang.ssOn : lang.ssOff }}</span>
           </div>
         </div>
       </div>
@@ -123,6 +123,7 @@
 
 <script lang="ts" setup>
 import { computed, ref, useTemplateRef, onMounted } from 'vue'
+import { lang } from '@/main'
 import useMSE from './useMSE'
 
 const props = withDefaults(defineProps<{

+ 6 - 5
src/views/components/tool/previewImageTool.vue

@@ -10,13 +10,13 @@
       <div class="image-preview__toolbar">
         <button @click.stop="zoomOut">-</button>
         <button @click.stop="zoomIn">+</button>
-        <button @click.stop="resetTransform">重置</button>
+        <button @click.stop="resetTransform">{{ lang.ssReset }}</button>
         <button @click.stop="rotateLeft">⟲</button>
         <button @click.stop="rotateRight">⟳</button>
         <button @click.stop="toggleFit">
-          {{ fitMode ? "实际大小" : "适应屏幕" }}
+          {{ fitMode ? lang.ssActualSize : lang.ssFitScreen }}
         </button>
-        <button @click.stop="closePreview">关闭</button>
+        <button @click.stop="closePreview">{{ lang.ssClose }}</button>
       </div>
       <div
         class="image-preview__stage"
@@ -29,13 +29,13 @@
         <!-- 预览中的 Loading 状态 -->
         <div v-if="previewImageLoading" class="preview-loading">
           <div class="loading-spinner"></div>
-          <div class="loading-text">图片加载中...</div>
+          <div class="loading-text">{{ lang.ssImgLoading }}</div>
         </div>
 
         <img
           v-show="!previewImageLoading"
           :src="imageUrl"
-          alt="预览"
+          :alt="lang.ssPreview"
           class="image-preview__img"
           :style="imgStyle"
           draggable="false"
@@ -49,6 +49,7 @@
 
 <script setup lang="ts">
 import { ref, watch, computed, defineExpose } from 'vue'
+import { lang } from '@/main'
 const imageUrl = ref<string>('')
 // Loading 状态
 const imageLoading = ref(false)

+ 657 - 3
src/views/lang/cn.json

@@ -1,3 +1,657 @@
- {
-  "lang": "cn"
- }
+{
+  "lang": "cn",
+  "ssImportPptx": "导入 PPTX 文件",
+  "ssResetSlides": "重置幻灯片",
+  "ssHotkeys": "快捷操作",
+  "ssUploadPptx": "上传 PPTX 文件",
+  "ssStage": "幻灯片放映(F5)",
+  "ssFromStart": "从头开始",
+  "ssFromCurrent": "从当前页开始",
+  "ssExport": "导出",
+  "ssImporting": "正在导入...",
+  "ssCourseLoading": "正在加载课程内容...",
+  "ssParsePptFail": "解析PPT数据失败",
+  "ssFetchCourseFail": "获取课程详情失败",
+  "ssPaste": "粘贴",
+  "ssSelectAll": "全选",
+  "ssRuler": "标尺",
+  "ssGridLine": "网格线",
+  "ssNone": "无",
+  "ssSmall": "小",
+  "ssMedium": "中",
+  "ssLarge": "大",
+  "ssResetPage": "重置当前页",
+  "ssUndoTip": "撤销(Ctrl + Z)",
+  "ssRedoTip": "重做(Ctrl + Y)",
+  "ssNotePanel": "批注面板",
+  "ssSelectPane": "选择窗格",
+  "ssSearchReplace": "查找替换",
+  "ssSearchTip": "查找/替换(Ctrl + F)",
+  "ssInsertText": "插入文字",
+  "ssTextHorizontal": "横向文本框",
+  "ssTextVertical": "竖向文本框",
+  "ssInsertShape": "插入形状",
+  "ssFreeDraw": "自由绘制",
+  "ssInsertImage": "插入图片",
+  "ssInsertLine": "插入线条",
+  "ssInsertChart": "插入图表",
+  "ssInsertTable": "插入表格",
+  "ssInsertFormula": "插入公式",
+  "ssInsertLearn": "插入学习内容",
+  "ssInsertMedia": "插入音视频",
+  "ssEditTool": "编辑工具",
+  "ssZoomOutTip": "画布缩小(Ctrl + -)",
+  "ssFitScreen": "适应屏幕",
+  "ssZoomInTip": "画布放大(Ctrl + =)",
+  "ssFitScreenTip": "适应屏幕(Ctrl + 0)",
+  "ssFileReadFail": "无法正确读取 / 解析该文件",
+  "ssCoord": "坐标*",
+  "ssSectName": "输入节名称",
+  "ssUntitledSec": "无标题节",
+  "ssDefSec": "默认节",
+  "ssSlidePage": "幻灯片",
+  "ssStyle": "样式",
+  "ssSymbol": "符号",
+  "ssPosition": "位置",
+  "ssAnim": "动画",
+  "ssDesign": "设计",
+  "ssSwitch": "切换",
+  "ssStyleMulti": "样式(多选)",
+  "ssPosMulti": "位置(多选)",
+  "ssExpPptist": "导出 pptist 文件",
+  "ssExpPptx": "导出 PPTX",
+  "ssExpImage": "导出图片",
+  "ssExpJson": "导出 JSON",
+  "ssPrintPdf": "打印 / 导出 PDF",
+  "ssSelectCnt": "选择(*/*)",
+  "ssShowAll": "全部显示",
+  "ssHideAll": "全部隐藏",
+  "ssGroup": "组合",
+  "ssFindInput": "输入查找内容",
+  "ssReplInput": "输入替换内容",
+  "ssIgnoreCase": "忽略大小写",
+  "ssPrevOne": "上一个",
+  "ssNextOne": "下一个",
+  "ssReplace": "替换",
+  "ssReplaceAll": "全部替换",
+  "ssFindTab": "查找",
+  "ssReplTab": "替换",
+  "ssSlideNote": "幻灯片*的批注",
+  "ssReply": "回复",
+  "ssDelete": "删除",
+  "ssReplyInput": "输入回复内容",
+  "ssCancel": "取消",
+  "ssNoNotes": "本页暂无批注",
+  "ssSelEl": "选中元素",
+  "ssCurSlide": "当前页幻灯片",
+  "ssNoteInput": "输入批注(为*)",
+  "ssClearNotes": "清空本页批注",
+  "ssAddNote": "添加批注",
+  "ssTestUser": "测试用户",
+  "ssMarkupTitle": "幻灯片类型标注",
+  "ssCurPageType": "当前页面类型:",
+  "ssCurTextType": "当前文本类型:",
+  "ssCurImgType": "当前图片类型:",
+  "ssMarkupHint": "选中图片、文字、带文字的形状,标记类型",
+  "ssUnmarkedType": "未标记类型",
+  "ssCoverPage": "封面页",
+  "ssTocPage": "目录页",
+  "ssTransPage": "过渡页",
+  "ssContentPage": "内容页",
+  "ssEndPage": "结束页",
+  "ssTxtTitle": "标题",
+  "ssTxtSubttl": "副标题",
+  "ssTxtBody": "正文",
+  "ssTxtItem": "列表项目",
+  "ssTxtItemTtl": "列表项标题",
+  "ssTxtNotes": "注释",
+  "ssTxtHeader": "页眉",
+  "ssTxtFooter": "页脚",
+  "ssTxtPartNo": "节编号",
+  "ssTxtItemNo": "项目编号",
+  "ssImgPageFig": "页面插图",
+  "ssImgItemFig": "项目插图",
+  "ssImgBg": "背景图",
+  "ssAiSubTpl": "从下方挑选合适的模板,开始生成PPT",
+  "ssAiSubOut": "确认下方内容大纲(点击编辑内容,右键添加/删除大纲项),开始选择模板",
+  "ssAiSubSet": "在下方输入您的PPT主题,并适当补充信息,如行业、岗位、学科、用途等",
+  "ssAiPhTopic": "请输入PPT主题,如:大学生职业生涯规划",
+  "ssAiGen": "AI 生成",
+  "ssLangColon": "语言:",
+  "ssStyleColon": "风格:",
+  "ssModelColon": "模型:",
+  "ssImgColon": "配图:",
+  "ssLangZh": "中文",
+  "ssLangZhV": "中文",
+  "ssLangEn": "英文",
+  "ssLangJa": "日文",
+  "ssStyGen": "通用",
+  "ssStyGenV": "通用",
+  "ssStyAcad": "学术风",
+  "ssStyAcadV": "学术风",
+  "ssStyWork": "职场风",
+  "ssStyWorkV": "职场风",
+  "ssStyEdu": "教育风",
+  "ssStyEduV": "教育风",
+  "ssStyMkt": "营销风",
+  "ssStyMktV": "营销风",
+  "ssMockTest": "模拟测试",
+  "ssAiSearch": "AI搜图",
+  "ssAiImgGen": "AI生图",
+  "ssAiChooseTpl": "选择模板",
+  "ssAiBackRe": "返回重新生成",
+  "ssAiMake": "生成",
+  "ssAiBackOut": "返回大纲",
+  "ssAiWait": "AI生成中,请耐心等待 ...",
+  "ssAiNeedTopic": "请先输入PPT主题",
+  "ssAiRecA": "2025科技前沿动态",
+  "ssAiRecB": "大数据如何改变世界",
+  "ssAiRecC": "餐饮市场调查与研究",
+  "ssAiRecD": "AIGC在教育领域的应用",
+  "ssAiRecE": "社交媒体与品牌营销",
+  "ssAiRecF": "5G技术如何改变我们的生活",
+  "ssAiRecG": "年度工作总结与展望",
+  "ssAiRecH": "区块链技术及其应用",
+  "ssAiRecI": "大学生职业生涯规划",
+  "ssAiRecJ": "公司年会策划方案",
+  "ssInteract": "互动工具",
+  "ssHPage": "H5页面",
+  "ssAiApp": "AI应用",
+  "ssVideo": "视频",
+  "ssCreative": "创作空间",
+  "ssContentList": "内容列表",
+  "ssChoiceQ": "选择题",
+  "ssQandA": "问答",
+  "ssNoLearn": "暂无学习内容",
+  "ssNeedUpload": "请先上传或创建学习内容",
+  "ssPreview": "预览",
+  "ssEdit": "编辑",
+  "ssQATest": "问答题",
+  "ssBiliVideo": "B站视频",
+  "ssUnknown": "未知",
+  "ssCourseOutline": "课程大纲",
+  "ssExpand": "展开",
+  "ssCollapse": "收起",
+  "ssSlideNum": "幻灯片 *",
+  "ssPrevPage": "上一页",
+  "ssNextPage": "下一页",
+  "ssFullscreen": "全屏",
+  "ssFollowOn": "开启跟随模式",
+  "ssFollowOff": "关闭跟随模式",
+  "ssRefresh": "刷新",
+  "ssSubmitHW": "提交作业",
+  "ssSubmitting": "提交中...",
+  "ssTimer": "计时器",
+  "ssPenTool": "画笔工具",
+  "ssLaserPen": "激光笔",
+  "ssOpenFull": "打开全屏",
+  "ssExitFull": "退出全屏",
+  "ssHwSubmit": "作业提交",
+  "ssHwSubmitting": "作业提交中...",
+  "ssSubmittingEll": "提交中...",
+  "ssSubmit": "提交",
+  "ssRefreshIframe": "刷新iframe内容",
+  "ssHwLoading": "正在加载作业...",
+  "ssAnswerRes": "回答结果",
+  "ssDialogArea": "对话区",
+  "ssConnecting": "连接中...",
+  "ssConnLost": "连接断开",
+  "ssReconnect": "重新连接",
+  "ssHwTypeUnsup": "暂不支持的作业类型",
+  "ssFileUploadFail": "文件上传失败",
+  "ssHwSubmitWp": "workPage作业提交",
+  "ssHwSubmitSucc": "作业提交成功",
+  "ssHwSubmitRetry": "作业提交失败,请重试",
+  "ssShotFail": "截图提交失败",
+  "ssShotSucc": "页面截图提交成功",
+  "ssHwNoFunc": "未找到可用的作业提交功能",
+  "ssHwSubmitFail": "作业提交失败",
+  "ssNoIframe": "当前页面没有找到iframe元素",
+  "ssRefreshDone": "刷新完成",
+  "ssNoIframeRef": "没有找到可刷新的iframe",
+  "ssRefreshFail": "刷新iframe失败",
+  "ssWorkInfoFail": "获取作业信息失败",
+  "ssStuInfoFail": "获取学生信息失败",
+  "ssFollowOnTip": "跟随模式已开启",
+  "ssFreeOnTip": "自由模式已开启",
+  "ssOpFailRetry": "操作失败,请重试",
+  "ssNetError": "网络连接异常,请检查网络后刷新页面",
+  "ssLoading": "加载中 ...",
+  "ssInkWidth": "墨迹粗细:",
+  "ssShape": "形状",
+  "ssHighlight": "荧光笔",
+  "ssEraserSz": "橡皮大小:",
+  "ssEraser": "橡皮擦",
+  "ssClearInk": "清除墨迹",
+  "ssBlackboard": "黑板",
+  "ssClosePen": "关闭画笔",
+  "ssPause": "暂停",
+  "ssStartTimer": "开始计时",
+  "ssJsonReadFail": "无法正确读取 / 解析该JSON数据",
+  "ssNoImage": "暂无图片",
+  "ssShotWork": "截图作业",
+  "ssActualSize": "实际大小",
+  "ssReset": "重置",
+  "ssClose": "关闭",
+  "ssPrevQ": "上一题",
+  "ssNextQ": "下一题",
+  "ssSingleSel": "单选题",
+  "ssMultiSel": "多选题",
+  "ssNodeTitle": "节点*",
+  "ssClearChat": "清空聊天记录",
+  "ssChatPhInput": "请输入",
+  "ssClearChatTip": "确定要清空所有聊天记录吗?此操作不可恢复。",
+  "ssConfirmClear": "确认清空",
+  "ssAiHello": "你好!我是你的学习助手,有什么可以帮助你的吗?",
+  "ssTeacher": "老师",
+  "ssAiHelper": "AI助手",
+  "ssNetErrShort": "网络错误",
+  "ssChatCleared": "聊天记录已清空",
+  "ssClearFail": "清空聊天记录失败,请重试",
+  "ssChoiceStat": "选择题统计",
+  "ssTotalSub": "总提交:* 人",
+  "ssNotChoice": "当前页面不是选择题",
+  "ssNoStat": "暂无统计数据",
+  "ssViewStu": "查看学生",
+  "ssSelStu": "选择同学:",
+  "ssViewQ": "查看题目",
+  "ssViewRes": "查看结果",
+  "ssParticipants": "参与人数",
+  "ssAccuracy": "正确率",
+  "ssCorrectAns": "正确答案",
+  "ssUnsubStu": "未提交人员",
+  "ssSubStu": "已提交人员",
+  "ssCorrectTag": "正确",
+  "ssMultiOpt": "多选项",
+  "ssPageImage": "页面图片",
+  "ssSlideLearn": "当前幻灯片已有学习内容,一个幻灯片只能插入一个学习内容",
+  "ssCut": "剪切",
+  "ssCopy": "复制",
+  "ssAlignHCenter": "水平居中",
+  "ssAlignHVCenter": "水平垂直居中",
+  "ssAlignLeft": "左对齐",
+  "ssAlignRight": "右对齐",
+  "ssAlignVCenter": "垂直居中",
+  "ssAlignTop": "顶部对齐",
+  "ssAlignBottom": "底部对齐",
+  "ssBringFront": "置于顶层",
+  "ssBringForward": "上移一层",
+  "ssSendBack": "置于底层",
+  "ssSendBackward": "下移一层",
+  "ssSetLink": "设置链接",
+  "ssGroupEl": "组合",
+  "ssUngroupEl": "取消组合",
+  "ssLock": "锁定",
+  "ssUnlock": "解锁",
+  "ssOpenNewWin": "在新窗口打开",
+  "ssCopyLink": "复制链接",
+  "ssShapeHint": "点击绘制任意形状,首尾闭合完成绘制,按 ESC 键或鼠标右键取消,按 ENTER 键提前完成",
+  "ssWebUrlPh": "请输入网页链接地址",
+  "ssConfirm": "确认",
+  "ssTabWebLink": "网页链接",
+  "ssTabSlidePg": "幻灯片页面",
+  "ssEditWebLink": "修改网页链接",
+  "ssWebUrlReq": "请输入网页链接地址",
+  "ssWebUrlInvalid": "请输入正确的网页链接格式",
+  "ssTable": "表格",
+  "ssBack": "返回",
+  "ssCustom": "自定义",
+  "ssRowCnt": "行数:",
+  "ssColCnt": "列数:",
+  "ssTblRangeWarn": "行数/列数必须在0~20之间!",
+  "ssAudio": "音频",
+  "ssVideoUrlPh": "请输入视频地址,e.g. https://xxx.mp4",
+  "ssAudioUrlPh": "请输入音频地址,e.g. https://xxx.mp3",
+  "ssVideoUrlReq": "请先输入正确的视频地址",
+  "ssAudioUrlReq": "请先输入正确的音频地址",
+  "ssPickLearn": "请选择要嵌入的学习内容",
+  "ssPickWebFirst": "请先选择一个网页",
+  "ssLatexPh": "输入 LaTeX 公式",
+  "ssFormulaPrev": "公式预览",
+  "ssLatexSym": "常用符号",
+  "ssLatexPreset": "预置公式",
+  "ssLatexEmpty": "公式不能为空",
+  "ssExpFormat": "导出格式:",
+  "ssExpRange": "导出范围:",
+  "ssCurPage": "当前页",
+  "ssCustomRange": "自定义范围:",
+  "ssImgQuality": "图片质量:",
+  "ssIgnoreWebfont": "忽略在线字体:",
+  "ssIgnoreWebfontTip": "导出时默认忽略在线字体,若您在幻灯片中使用了在线字体,且希望导出后保留相关样式,可选择关闭【忽略在线字体】选项,但要注意这将会增加导出用时。",
+  "ssExporting": "正在导出...",
+  "ssEssayQ": "问答题",
+  "ssAIApp": "AI应用",
+  "ssH5Page": "H5页面",
+  "ssBilibili": "B站视频",
+  "ssCreateSpace": "创作空间",
+  "ssInsertCol": "插入列",
+  "ssToLeft": "到左侧",
+  "ssToRight": "到右侧",
+  "ssInsertRow": "插入行",
+  "ssAbove": "到上方",
+  "ssBelow": "到下方",
+  "ssDeleteCol": "删除列",
+  "ssDeleteRow": "删除行",
+  "ssMergeCells": "合并单元格",
+  "ssUnmergeCells": "取消合并单元格",
+  "ssSelectCurCol": "选中当前列",
+  "ssSelectCurRow": "选中当前行",
+  "ssSelectAllCells": "选中全部单元格",
+  "ssVideoLoadFail": "视频加载失败",
+  "ssPlaybackRate": "倍速",
+  "ssLoop": "循环",
+  "ssOn": "开",
+  "ssOff": "关",
+  "ssFontLoadWait": "字体需要等待加载下载后生效,请稍等",
+  "ssImgLoading": "图片加载中...",
+  "ssPickLinkTarget": "请先选择链接目标",
+  "ssNoMatch": "未查找到匹配项",
+  "ssAudioLoadFail": "音频加载失败",
+  "ssSlidePg": "幻灯片页面 *",
+  "ssRemove": "移除",
+  "ssPerPage": "每页数量:",
+  "ssEdgePad": "边缘留白:",
+  "ssPrintBgTip": "提示:若打印预览与实际样式不一致,请在弹出的打印窗口中勾选【背景图形】选项。",
+  "ssIgnoreMedia": "忽略音频/视频:",
+  "ssIgnoreMediaTip": "导出时默认忽略音视频,若您的幻灯片中存在音视频元素,且希望将其导出到PPTX文件中,可选择关闭【忽略音视频】选项,但要注意这将会大幅增加导出用时。",
+  "ssMastOver": "覆盖默认母版:",
+  "ssPptxTip": "提示:1. 支持导出格式:avi、mp4、mov、wmv、mp3、wav;2. 跨域资源无法导出。",
+  "ssPptistTip": "提示:.pptist 是本应用的特有文件后缀,支持将该类型的文件导入回应用中。",
+  "ssSpeakerNotePh": "点击输入演讲者备注",
+  "ssInsertAllTpl": "插入全部",
+  "ssInsertTpl": "插入模板",
+  "ssTplAll": "全部",
+  "ssTplCover": "封面",
+  "ssTplDir": "目录",
+  "ssTplTrans": "过渡",
+  "ssTplContent": "内容",
+  "ssTplEnd": "结束",
+  "ssColorMask": "着色(蒙版):",
+  "ssMaskColor": "蒙版颜色:",
+  "ssEnableFilter": "启用滤镜:",
+  "ssFlipV": "垂直翻转",
+  "ssFlipH": "水平翻转",
+  "ssBlur": "模糊",
+  "ssBrightness": "亮度",
+  "ssContrast": "对比度",
+  "ssGrayscale": "灰度",
+  "ssSaturate": "饱和度",
+  "ssHueRotate": "色相",
+  "ssSepia": "褐色",
+  "ssInvert": "反转",
+  "ssOpacity": "不透明度",
+  "ssPresetBW": "黑白",
+  "ssPresetRetro": "复古",
+  "ssPresetSharp": "锐化",
+  "ssPresetSoft": "柔和",
+  "ssPresetWarm": "暖色",
+  "ssPresetBright": "明亮",
+  "ssPresetVivid": "鲜艳",
+  "ssPresetBlur": "模糊",
+  "ssPresetInvert": "反转",
+  "ssEnableOutline": "启用边框:",
+  "ssOutlineStyle": "边框样式:",
+  "ssOutlineColor": "边框颜色:",
+  "ssOutlineWidth": "边框粗细:",
+  "ssEnableShadow": "启用阴影:",
+  "ssShadowH": "水平阴影:",
+  "ssShadowV": "垂直阴影:",
+  "ssBlurDist": "模糊距离:",
+  "ssShadowColor": "阴影颜色:",
+  "ssSearchFont": "搜索字体",
+  "ssSearchFontSize": "搜索字号",
+  "ssTextColor": "文字颜色",
+  "ssTextHighlight": "文字高亮",
+  "ssFontSizeUp": "增大字号",
+  "ssFontSizeDown": "减小字号",
+  "ssBold": "加粗",
+  "ssItalic": "斜体",
+  "ssUnderline": "下划线",
+  "ssStrike": "删除线",
+  "ssSup": "上标",
+  "ssSub": "下标",
+  "ssInlineCode": "行内代码",
+  "ssQuote": "引用",
+  "ssAIAssist": "AI辅助",
+  "ssAIBeautify": "美化",
+  "ssAIExpand": "扩写",
+  "ssAISimplify": "精简",
+  "ssClearFormat": "清除格式",
+  "ssFormatPainter": "格式刷(双击连续使用)",
+  "ssLink": "超链接",
+  "ssLinkPh": "请输入超链接",
+  "ssAlignJustify": "两端对齐",
+  "ssBulletList": "项目符号",
+  "ssOrderedList": "编号",
+  "ssIndentLess": "减小段落缩进",
+  "ssIndentMore": "增大段落缩进",
+  "ssTextIndentLess": "减小首行缩进",
+  "ssTextIndentMore": "增大首行缩进",
+  "ssNoTextToAI": "没有可以执行的文本内容",
+  "ssChartType": "图表类型:",
+  "ssClickChange": "点击更换",
+  "ssClearData": "清空数据",
+  "ssChartCat": "类别*",
+  "ssChartSer": "系列*",
+  "ssEditChart": "编辑图表",
+  "ssStackStyle": "堆叠样式",
+  "ssSmoothLine": "使用平滑曲线",
+  "ssBgFill": "背景填充:",
+  "ssAxisText": "坐标与文字:",
+  "ssGridColor": "网格颜色:",
+  "ssChartTheme": "主题配色:",
+  "ssPresetChartTheme": "预置图表主题:",
+  "ssSlideTheme": "幻灯片主题:",
+  "ssCustomColors": "自定义配色",
+  "ssChartThemeColors": "图表主题配色",
+  "ssThemeColorNo": "主题配色*:",
+  "ssAddThemeColor": "添加主题色",
+  "ssIconColor": "图标颜色:",
+  "ssAutoplay": "自动播放:",
+  "ssLoopPlay": "循环播放:",
+  "ssClipImage": "裁剪图片",
+  "ssByShape": "按形状:",
+  "ssByRatio": "按*:",
+  "ssRadius": "圆角半径:",
+  "ssReplaceImage": "替换图片",
+  "ssResetStyle": "重置样式",
+  "ssSetAsBg": "设为背景",
+  "ssRatioSquare": "纵横比(正方形)",
+  "ssRatioPortrait": "纵横比(纵向)",
+  "ssRatioLandscape": "纵横比(横向)",
+  "ssEditLatex": "编辑 LaTeX",
+  "ssColor": "颜色:",
+  "ssStrokeWidth": "粗细:",
+  "ssLineStyle": "线条样式:",
+  "ssLineColor": "线条颜色:",
+  "ssLineWidth": "线条宽度:",
+  "ssStartStyle": "起点样式:",
+  "ssEndStyle": "终点样式:",
+  "ssSwapDir": "交换方向",
+  "ssClickReplaceShape": "点击替换形状",
+  "ssSolidFill": "纯色填充",
+  "ssGradFill": "渐变填充",
+  "ssImgFill": "图片填充",
+  "ssLinearGrad": "线性渐变",
+  "ssRadialGrad": "径向渐变",
+  "ssCurColorBlock": "当前色块:",
+  "ssGradAngle": "渐变角度:",
+  "ssAlignMiddle": "居中",
+  "ssShapePainter": "形状格式刷",
+  "ssCellFill": "单元格填充",
+  "ssEnableThemeTbl": "启用主题表格:",
+  "ssHeaderRow": "标题行",
+  "ssFooterRow": "汇总行",
+  "ssFirstCol": "第一列",
+  "ssLastCol": "最后一列",
+  "ssThemeColorLbl": "主题颜色:",
+  "ssLineHeight": "行间距:",
+  "ssParaSpace": "段间距:",
+  "ssWordSpace": "字间距:",
+  "ssTextBoxFill": "文本框填充:",
+  "ssTimes": "倍",
+  "ssBigTitle": "大标题",
+  "ssSmallTitle": "小标题",
+  "ssBody": "正文",
+  "ssBodySmall": "正文[小]",
+  "ssVideoPoster": "视频预览封面",
+  "ssResetPoster": "重置封面",
+  "ssBgFillTit": "背景填充",
+  "ssBgContain": "缩放",
+  "ssBgRepeat": "拼贴",
+  "ssBgCover": "缩放铺满",
+  "ssApplyBgAll": "应用背景到全部",
+  "ssWideSixNine": "宽屏 16 : 9",
+  "ssWideSixTen": "宽屏 16 : 10",
+  "ssStdFourThree": "标准 4 : 3",
+  "ssPaperAThreeFour": "纸张 A3 / A4",
+  "ssPortAThreeFour": "竖向 A3 / A4",
+  "ssCanvasSize": "画布尺寸:",
+  "ssGlobalTheme": "全局主题",
+  "ssMore": "更多",
+  "ssFont": "字体:",
+  "ssFontColor": "字体颜色:",
+  "ssBgColor": "背景颜色:",
+  "ssThemeColor": "主题色:",
+  "ssApplyThemeAll": "应用主题到全部",
+  "ssExtractTheme": "从幻灯片提取主题",
+  "ssPresetThemeTit": "预置主题",
+  "ssTextAa": "文字 Aa",
+  "ssSet": "设置",
+  "ssSetApply": "设置并应用",
+  "ssEditThemeColor": "编辑主题色",
+  "ssSlideThemeClr": "幻灯片主题色*:",
+  "ssChoose": "选择",
+  "ssApplyToTheme": "应用到主题",
+  "ssThemePickTip": "点击色块排除不要的颜色",
+  "ssSaveSelTheme": "将选中配置保存为主题",
+  "ssExtCurPage": "从当前页中提取",
+  "ssExtAllSlides": "从全部幻灯片提取",
+  "ssThemeClrLimit": "主题色超出数量限制,已自动选取前6个",
+  "ssAddAnim": "添加动画",
+  "ssPickElAnim": "选中画布中的元素添加动画",
+  "ssAnimDur": "持续时长:",
+  "ssTrigMode": "触发方式:",
+  "ssTrigManual": "主动触发",
+  "ssTrigWithPrev": "与上一动画同时",
+  "ssTrigAfterPrev": "上一动画之后",
+  "ssChangeAnim": "更换动画",
+  "ssStopPreview": "停止预览",
+  "ssPreviewAll": "预览全部",
+  "ssAnimIn": "入场",
+  "ssAnimOut": "退场",
+  "ssAnimEmph": "强调",
+  "ssLayer": "层级:",
+  "ssToTop": "置顶",
+  "ssToBottom": "置底",
+  "ssMoveUp": "上移",
+  "ssMoveDown": "下移",
+  "ssAlignLbl": "对齐:",
+  "ssPosH": "水平:",
+  "ssPosV": "垂直:",
+  "ssWidth": "宽度:",
+  "ssHeight": "高度:",
+  "ssRotate": "旋转:",
+  "ssRatioUnlock": "解除宽高比锁定",
+  "ssRatioLock": "宽高比锁定",
+  "ssUniDistH": "水平均匀分布",
+  "ssUniDistV": "垂直均匀分布",
+  "ssFillColor": "填充颜色:",
+  "ssApplyAll": "应用到全部",
+  "ssAppliedAll": "已应用到全部",
+  "ssLayout": "布局",
+  "ssUndo": "撤销",
+  "ssRedo": "重做",
+  "ssExitEdit": "退出编辑",
+  "ssNewSlide": "新幻灯片",
+  "ssText": "文字",
+  "ssImage": "图片",
+  "ssRect": "矩形",
+  "ssCircle": "圆形",
+  "ssExitPlay": "退出播放",
+  "ssPlay": "播放",
+  "ssFirstSlide": "第一页",
+  "ssLastSlide": "最后一页",
+  "ssFirstPage": "已经是第一页了",
+  "ssLastPage": "已经是最后一页了",
+  "ssAutoPlayShow": "自动放映",
+  "ssCancelAuto": "取消自动放映",
+  "ssAutoPlayStart": "开始自动放映",
+  "ssPresenterView": "演讲者视图",
+  "ssEnterFull": "进入全屏",
+  "ssEndPlay": "结束放映",
+  "ssStart": "开始",
+  "ssCountdown": "倒计时",
+  "ssLoopShow": "循环放映",
+  "ssShowToolbar": "显示工具栏",
+  "ssAllSlides": "查看所有幻灯片",
+  "ssBottomThumb": "触底显示缩略图",
+  "ssInitDataWait": "数据初始化中,请稍等 ...",
+  "ssDelSect": "删除节",
+  "ssDelSectSlides": "删除节和幻灯片",
+  "ssDelAllSect": "删除所有节",
+  "ssRenSect": "重命名节",
+  "ssNewPage": "新建页面",
+  "ssSlideShow": "幻灯片放映",
+  "ssDupPage": "复制页面",
+  "ssDelPage": "删除页面",
+  "ssAddSect": "增加节",
+  "ssPlayFromCur": "从当前放映",
+  "ssDblClickEdit": "双击编辑",
+  "ssElText": "文本",
+  "ssElImage": "图片",
+  "ssElShape": "形状",
+  "ssElLine": "线条",
+  "ssElChart": "图表",
+  "ssElTable": "表格",
+  "ssElVideo": "视频",
+  "ssElAudio": "音频",
+  "ssElLatex": "公式",
+  "ssElFrame": "网页",
+  "ssShapeRect": "矩形",
+  "ssShapeCommon": "常用形状",
+  "ssShapeArrow": "箭头",
+  "ssShapeOther": "其他形状",
+  "ssShapeLinear": "线性",
+  "ssLineStraight": "直线",
+  "ssLinePolyCurve": "折线、曲线",
+  "ssChartBar": "柱状图",
+  "ssChartColumn": "条形图",
+  "ssChartLine": "折线图",
+  "ssChartArea": "面积图",
+  "ssChartScatter": "散点图",
+  "ssChartPie": "饼图",
+  "ssChartRing": "环形图",
+  "ssChartRadar": "雷达图",
+  "ssValue": "值",
+  "ssX": "X",
+  "ssY": "Y",
+  "ssApply":"确认",
+  "ssInsert":"确认",
+  "ssGauss": "高斯公式",
+  "ssFourier": "傅里叶级数",
+  "ssTaylor": "泰勒展开式",
+  "ssDefInt": "定积分",
+  "ssTrigIdOne": "三角恒等式1",
+  "ssTrigIdTwo": "三角恒等式2",
+  "ssSumExpand": "和的展开式",
+  "ssEuler": "欧拉公式",
+  "ssBernoulli": "贝努利方程",
+  "ssExactDE": "全微分方程",
+  "ssNonHom": "非齐次方程",
+  "ssCauchyMVT": "柯西中值定理",
+  "ssLagMVT": "拉格朗日中值定理",
+  "ssDerivForm": "导数公式",
+  "ssTrigInt": "三角函数积分",
+  "ssQuadSurf": "二次曲面",
+  "ssSecDiff": "二阶微分",
+  "ssDirDeriv": "方向导数",
+  "ssMath": "数学",
+  "ssCombo": "组合",
+  "ssFunc": "函数",
+  "ssGreek": "希腊字母",
+  "ssExportPptist": "导出 .pptist 文件",
+  "ssExportPptx": "导出 PPTX ",
+  "ssExportImage": "导出图片",
+  "ssExportJSON": "导出 JSON"
+}

+ 660 - 3
src/views/lang/en.json

@@ -1,3 +1,660 @@
- {
-  "lang": "en"
- }
+{
+  "lang": "en",
+  "ssImportPptx": "Import PPTX file",
+  "ssResetSlides": "Reset slides",
+  "ssHotkeys": "Shortcuts",
+  "ssUploadPptx": "Upload PPTX file",
+  "ssStage": "Slideshow (F5)",
+  "ssFromStart": "From Beginning",
+  "ssFromCurrent": "From Current Slide",
+  "ssExport": "Export",
+  "ssImporting": "Importing...",
+  "ssCourseLoading": "Loading course content...",
+  "ssParsePptFail": "Failed to parse PPT data",
+  "ssFetchCourseFail": "Failed to get course details",
+  "ssPaste": "Paste",
+  "ssSelectAll": "Select All",
+  "ssRuler": "Ruler",
+  "ssGridLine": "Grid lines",
+  "ssNone": "None",
+  "ssSmall": "Small",
+  "ssMedium": "Medium",
+  "ssLarge": "Large",
+  "ssResetPage": "Reset current page",
+  "ssUndoTip": "Undo (Ctrl + Z)",
+  "ssRedoTip": "Redo (Ctrl + Y)",
+  "ssNotePanel": "Notes",
+  "ssSelectPane": "Select",
+  "ssSearchReplace": "Find & Replace",
+  "ssSearchTip": "Find/Replace (Ctrl + F)",
+  "ssInsertText": "Insert Text",
+  "ssTextHorizontal": "Horizontal Text box",
+  "ssTextVertical": "Vertical Text box",
+  "ssInsertShape": "Insert Shapes",
+  "ssFreeDraw": "Freefrom",
+  "ssInsertImage": "Insert Image",
+  "ssInsertLine": "Insert Line",
+  "ssInsertChart": "Insert Chart",
+  "ssInsertTable": "Insert Table",
+  "ssInsertFormula": "Insert Formula",
+  "ssInsertLearn": "Insert learning content",
+  "ssInsertMedia": "Insert Media",
+  "ssEditTool": "Edit tool",
+  "ssZoomOutTip": "Zoom Out (Ctrl + -)",
+  "ssFitScreen": "Fit to Screen",
+  "ssZoomInTip": "Zoom In (Ctrl + =)",
+  "ssFitScreenTip": "Fit to Screen (Ctrl + 0)",
+  "ssFileReadFail": "Failed to read/parse the file",
+  "ssCoord": "Coordinates *",
+  "ssSectName": "Enter section name",
+  "ssUntitledSec": "Untitled Section",
+  "ssDefSec": "Default Section",
+  "ssSlidePage": "Slide",
+  "ssStyle": "Style",
+  "ssSymbol": "Symbol",
+  "ssPosition": "Position",
+  "ssAnim": "Animation",
+  "ssDesign": "Design",
+  "ssSwitch": "Transition",
+  "ssStyleMulti": "Style (Multi)",
+  "ssPosMulti": "Position (Multi)",
+  "ssExpPptist": "Export as .pptist",
+  "ssExpPptx": "Export as PPTX",
+  "ssExpImage": "Export as Images",
+  "ssExpJson": "Export as JSON",
+  "ssPrintPdf": "Print / Export as PDF",
+  "ssSelectCnt": "Select (*/*)",
+  "ssShowAll": "All Slides",
+  "ssHideAll": "Hide All",
+  "ssGroup": "Group",
+  "ssFindInput": "Search...",
+  "ssReplInput": "Replace with...",
+  "ssIgnoreCase": "Case Insensitive",
+  "ssPrevOne": "Previous",
+  "ssNextOne": "Next",
+  "ssReplace": "Replace",
+  "ssReplaceAll": "Replace all",
+  "ssFindTab": "Find",
+  "ssReplTab": "Replace",
+  "ssSlideNote": "Notes for Slide *",
+  "ssReply": "Reply",
+  "ssDelete": "Delete",
+  "ssReplyInput": "Enter reply",
+  "ssCancel": "Cancel",
+  "ssNoNotes": "No notes for this slide",
+  "ssSelEl": "selected element",
+  "ssCurSlide": "current slide",
+  "ssNoteInput": "Enter note (for *)",
+  "ssClearNotes": "Clear notes for this slide",
+  "ssAddNote": "Add Note",
+  "ssTestUser": "Test user",
+  "ssMarkupTitle": "Slide type tagging",
+  "ssCurPageType": "Current slide type:",
+  "ssCurTextType": "Current text type:",
+  "ssCurImgType": "Current image type:",
+  "ssMarkupHint": "Select an image, text, or a shape with text to tag its type",
+  "ssUnmarkedType": "Unmarked",
+  "ssCoverPage": "Cover",
+  "ssTocPage": "Contents",
+  "ssTransPage": "Transition",
+  "ssContentPage": "Content",
+  "ssEndPage": "End",
+  "ssTxtTitle": "Title",
+  "ssTxtSubttl": "Subtitle",
+  "ssTxtBody": "Body",
+  "ssTxtItem": "List item",
+  "ssTxtItemTtl": "Item title",
+  "ssTxtNotes": "Notes",
+  "ssTxtHeader": "Header",
+  "ssTxtFooter": "Footer",
+  "ssTxtPartNo": "Section number",
+  "ssTxtItemNo": "Item number",
+  "ssImgPageFig": "Slide illustration",
+  "ssImgItemFig": "Item illustration",
+  "ssImgBg": "Background",
+  "ssAiSubTpl": "Pick a template below to start generating your PPT",
+  "ssAiSubOut": "Review the outline below (click to edit; right-click to add/delete items), then choose a template",
+  "ssAiSubSet": "Enter your PPT topic below and add details such as industry, role, subject, purpose, etc.",
+  "ssAiPhTopic": "Enter PPT topic, e.g., Career planning for college students",
+  "ssAiGen": "AI Generate",
+  "ssLangColon": "Language:",
+  "ssStyleColon": "Style:",
+  "ssModelColon": "Model:",
+  "ssImgColon": "Images:",
+  "ssLangZh": "Chinese",
+  "ssLangZhV": "中文",
+  "ssLangEn": "English",
+  "ssLangJa": "Japanese",
+  "ssStyGen": "General",
+  "ssStyGenV": "通用",
+  "ssStyAcad": "Academic",
+  "ssStyAcadV": "学术风",
+  "ssStyWork": "Workplace",
+  "ssStyWorkV": "职场风",
+  "ssStyEdu": "Education",
+  "ssStyEduV": "教育风",
+  "ssStyMkt": "Marketing",
+  "ssStyMktV": "营销风",
+  "ssMockTest": "Mock test",
+  "ssAiSearch": "AI image search",
+  "ssAiImgGen": "AI image generation",
+  "ssAiChooseTpl": "Choose template",
+  "ssAiBackRe": "Back & regenerate",
+  "ssAiMake": "Generate",
+  "ssAiBackOut": "Back to outline",
+  "ssAiWait": "Generating with AI, please wait ...",
+  "ssAiNeedTopic": "Please enter a PPT topic first",
+  "ssAiRecA": "Tech frontiers 2025",
+  "ssAiRecB": "How big data is changing the world",
+  "ssAiRecC": "Food & beverage market research",
+  "ssAiRecD": "Applications of AIGC in education",
+  "ssAiRecE": "Social media & brand marketing",
+  "ssAiRecF": "How 5G is changing our lives",
+  "ssAiRecG": "Annual work summary and outlook",
+  "ssAiRecH": "Blockchain technology and applications",
+  "ssAiRecI": "Career planning for college students",
+  "ssAiRecJ": "Company annual meeting plan",
+  "ssInteract": "Interactive tools",
+  "ssHPage": "H5 page",
+  "ssAiApp": "AI apps",
+  "ssVideo": "Video",
+  "ssCreative": "Creative space",
+  "ssContentList": "Content list",
+  "ssQandA": "Q&A",
+  "ssNoLearn": "No learning content",
+  "ssNeedUpload": "Please upload or create learning content first",
+  "ssPreview": "Preview",
+  "ssEdit": "Edit",
+  "ssCopy": "Copy",
+  "ssQATest": "Q&A",
+  "ssBiliVideo": "Bilibili video",
+  "ssUnknown": "Unknown",
+  "ssCourseOutline": "Course outline",
+  "ssExpand": "Expand",
+  "ssCollapse": "Collapse",
+  "ssSlideNum": "Slide *",
+  "ssPrevPage": "Previous page",
+  "ssNextPage": "Next page",
+  "ssFullscreen": "Fullscreen",
+  "ssFollowOn": "Enable follow mode",
+  "ssFollowOff": "Disable follow mode",
+  "ssRefresh": "Refresh",
+  "ssSubmitHW": "Submit homework",
+  "ssSubmitting": "Submitting...",
+  "ssTimer": "Timer",
+  "ssPenTool": "Pen tool",
+  "ssLaserPen": "Laser pointer",
+  "ssOpenFull": "Enter fullscreen",
+  "ssExitFull": "Exit fullscreen",
+  "ssHwSubmit": "Homework submission",
+  "ssHwSubmitting": "Submitting homework...",
+  "ssSubmittingEll": "Submitting...",
+  "ssSubmit": "Submit",
+  "ssRefreshIframe": "Refresh iframe content",
+  "ssHwLoading": "Loading homework...",
+  "ssAnswerRes": "Responses",
+  "ssDialogArea": "Chat",
+  "ssConnecting": "Connecting...",
+  "ssConnLost": "Disconnected",
+  "ssReconnect": "Reconnect",
+  "ssHwTypeUnsup": "Homework type not supported yet",
+  "ssFileUploadFail": "File upload failed",
+  "ssHwSubmitWp": "workPage submission",
+  "ssHwSubmitSucc": "Homework submitted successfully",
+  "ssHwSubmitRetry": "Homework submission failed, please try again",
+  "ssShotFail": "Screenshot submission failed",
+  "ssShotSucc": "Page screenshot submitted successfully",
+  "ssHwNoFunc": "No available homework submission feature found",
+  "ssHwSubmitFail": "Homework submission failed",
+  "ssNoIframe": "No iframe element found on this page",
+  "ssRefreshDone": "Refresh completed",
+  "ssNoIframeRef": "No iframe found to refresh",
+  "ssRefreshFail": "Failed to refresh iframe",
+  "ssWorkInfoFail": "Failed to fetch homework information",
+  "ssStuInfoFail": "Failed to fetch student information",
+  "ssFollowOnTip": "Follow mode enabled",
+  "ssFreeOnTip": "Free mode enabled",
+  "ssOpFailRetry": "Operation failed, please try again",
+  "ssNetError": "Network error, please check your connection and refresh the page",
+  "ssLoading": "Loading ...",
+  "ssInkWidth": "Stroke width:",
+  "ssShape": "Shape",
+  "ssHighlight": "Highlighter",
+  "ssEraserSz": "Eraser size:",
+  "ssEraser": "Eraser",
+  "ssClearInk": "Clear strokes",
+  "ssBlackboard": "Blackboard",
+  "ssClosePen": "Close pen",
+  "ssPause": "Pause",
+  "ssStartTimer": "Start timer",
+  "ssJsonReadFail": "Failed to read/parse this JSON data",
+  "ssNoImage": "No image",
+  "ssShotWork": "Screenshot homework",
+  "ssActualSize": "Actual size",
+  "ssReset": "Reset",
+  "ssClose": "Close",
+  "ssPrevQ": "Previous question",
+  "ssNextQ": "Next question",
+  "ssSingleSel": "Single choice",
+  "ssMultiSel": "Multiple choice",
+  "ssNodeTitle": "Node *",
+  "ssClearChat": "Clear chat history",
+  "ssChatPhInput": "Please enter",
+  "ssClearChatTip": "Are you sure you want to clear all chat history? This action cannot be undone.",
+  "ssConfirmClear": "Confirm clear",
+  "ssAiHello": "Hello! I am your learning assistant. How can I help you?",
+  "ssTeacher": "Teacher",
+  "ssAiHelper": "AI Assistant",
+  "ssNetErrShort": "Network error",
+  "ssChatCleared": "Chat history has been cleared",
+  "ssClearFail": "Failed to clear chat history, please try again",
+  "ssChoiceStat": "Multiple choice stats",
+  "ssTotalSub": "Total submissions: *",
+  "ssNotChoice": "The current page is not a choice question",
+  "ssNoStat": "No statistics available",
+  "ssViewStu": "View students",
+  "ssSelStu": "Selected students:",
+  "ssViewQ": "View question",
+  "ssViewRes": "View result",
+  "ssParticipants": "Participants",
+  "ssAccuracy": "Accuracy",
+  "ssCorrectAns": "Correct answer",
+  "ssUnsubStu": "Not submitted",
+  "ssSubStu": "Submitted",
+  "ssCorrectTag": "Correct",
+  "ssMultiOpt": "Multiple options",
+  "ssPageImage": "Page image",
+  "ssSlideLearn": "This slide already has learning content. Only one learning content can be inserted per slide.",
+  "ssCut": "Cut",
+  "ssAlignHCenter": "Align center horizontally",
+  "ssAlignHVCenter": "Align center both",
+  "ssAlignLeft": "Align left",
+  "ssAlignRight": "Align right",
+  "ssAlignVCenter": "Align center vertically",
+  "ssAlignTop": "Align top",
+  "ssAlignBottom": "Align bottom",
+  "ssBringFront": "Bring to front",
+  "ssBringForward": "Bring forward",
+  "ssSendBack": "Send to back",
+  "ssSendBackward": "Send backward",
+  "ssSetLink": "Set link",
+  "ssGroupEl": "Group",
+  "ssUngroupEl": "Ungroup",
+  "ssLock": "Lock",
+  "ssUnlock": "Unlock",
+  "ssOpenNewWin": "Open in new window",
+  "ssCopyLink": "Copy link",
+  "ssShapeHint": "Click to draw a shape. Connect the endpoints to close the path. Press Esc or right-click to cancel. Press Enter to finish early.",
+  "ssWebUrlPh": "Please enter a webpage URL",
+  "ssConfirm": "Confirm",
+  "ssTabWebLink": "Web link",
+  "ssTabSlidePg": "Slide page",
+  "ssEditWebLink": "Edit web link",
+  "ssWebUrlReq": "Please enter a webpage URL",
+  "ssWebUrlInvalid": "Please enter a valid webpage URL",
+  "ssTable": "Table",
+  "ssBack": "Back",
+  "ssCustom": "Custom",
+  "ssRowCnt": "Rows:",
+  "ssColCnt": "Columns:",
+  "ssTblRangeWarn": "Rows/columns must be between 0 and 20!",
+  "ssAudio": "Audio",
+  "ssVideoUrlPh": "Please enter a video URL, e.g. https://xxx.mp4",
+  "ssAudioUrlPh": "Please enter an audio URL, e.g. https://xxx.mp3",
+  "ssVideoUrlReq": "Please enter a valid video URL first",
+  "ssAudioUrlReq": "Please enter a valid audio URL first",
+  "ssPickLearn": "Please choose learning content to embed",
+  "ssPickWebFirst": "Please select a webpage first",
+  "ssLatexPh": "Enter LaTeX formula...",
+  "ssFormulaPrev": "Formula preview",
+  "ssLatexSym": "Common Symbols",
+  "ssLatexPreset": "Preset Formulas",
+  "ssLatexEmpty": "Formula cannot be empty",
+  "ssExpFormat": "file format:",
+  "ssExpRange": "Export range:",
+  "ssCurPage": "Current Slide",
+  "ssCustomRange": "Custom range:",
+  "ssImgQuality": "Image quality:",
+  "ssIgnoreWebfont": "Exclude web fonts:",
+  "ssIgnoreWebfontTip": "Web fonts are ignored by default when exporting. If you use web fonts and want to keep their styles, you can turn off [Ignore web fonts]; note this may increase export time.",
+  "ssExporting": "Exporting...",
+  "ssChoiceQ": "Multiple choice",
+  "ssEssayQ": "Essay",
+  "ssAIApp": "AI app",
+  "ssH5Page": "H5 page",
+  "ssBilibili": "Bilibili video",
+  "ssCreateSpace": "Create space",
+  "ssInsertCol": "Insert column",
+  "ssToLeft": "To left",
+  "ssToRight": "To right",
+  "ssInsertRow": "Insert row",
+  "ssAbove": "Above",
+  "ssBelow": "Below",
+  "ssDeleteCol": "Delete column",
+  "ssDeleteRow": "Delete row",
+  "ssMergeCells": "Merge cells",
+  "ssUnmergeCells": "Unmerge cells",
+  "ssSelectCurCol": "Select current column",
+  "ssSelectCurRow": "Select current row",
+  "ssSelectAllCells": "Select all cells",
+  "ssVideoLoadFail": "Video failed to load",
+  "ssPlaybackRate": "Speed",
+  "ssLoop": "Loop",
+  "ssOn": "On",
+  "ssOff": "Off",
+  "ssFontLoadWait": "Font will take effect after loading, please wait",
+  "ssImgLoading": "Loading image...",
+  "ssPickLinkTarget": "Please select a link target first",
+  "ssNoMatch": "No matches found",
+  "ssAudioLoadFail": "Audio failed to load",
+  "ssSlidePg": "Slide page *",
+  "ssRemove": "Remove",
+  "ssPerPage": "Slides per page:",
+  "ssEdgePad": "Page margins:",
+  "ssPrintBgTip": "Note: If the print preview doesn't match the actual appearance, enable the Background graphics option in the print dialog.",
+  "ssIgnoreMedia": "Exclude audio/video:",
+  "ssIgnoreMediaTip": "Audio and video are ignored by default when exporting. If your slides contain audio/video elements and you want to export them into the PPTX file, you can turn off [Ignore audio/video], but note this will significantly increase export time.",
+  "ssMastOver": "Override default master slide:",
+  "ssPptxTip": "Tip: 1) Supported formats: avi, mp4, mov, wmv, mp3, wav; 2) Cross-origin resources cannot be exported.",
+  "ssPptistTip": "Note: .pptist is the native file format for this app. Files in this format can be re-imported back into the application.",
+  "ssSpeakerNotePh": "Click to enter speaker notes",
+  "ssInsertAllTpl": "Insert all",
+  "ssInsertTpl": "Insert template",
+  "ssTplAll": "All",
+  "ssTplCover": "Cover",
+  "ssTplDir": "Contents",
+  "ssTplTrans": "Transition",
+  "ssTplContent": "Content",
+  "ssTplEnd": "End",
+  "ssColorMask": "Tint (mask):",
+  "ssMaskColor": "Mask color:",
+  "ssEnableFilter": "Enable filter:",
+  "ssFlipV": "Flip vertically",
+  "ssFlipH": "Flip horizontally",
+  "ssBlur": "Blur",
+  "ssBrightness": "Brightness",
+  "ssContrast": "Contrast",
+  "ssGrayscale": "Grayscale",
+  "ssSaturate": "Saturation",
+  "ssHueRotate": "Hue",
+  "ssSepia": "Sepia",
+  "ssInvert": "Invert",
+  "ssOpacity": "Opacity",
+  "ssPresetBW": "Black & white",
+  "ssPresetRetro": "Retro",
+  "ssPresetSharp": "Sharp",
+  "ssPresetSoft": "Soft",
+  "ssPresetWarm": "Warm",
+  "ssPresetBright": "Bright",
+  "ssPresetVivid": "Vivid",
+  "ssPresetBlur": "Blur",
+  "ssPresetInvert": "Invert",
+  "ssEnableOutline": "Enable outline:",
+  "ssOutlineStyle": "Outline style:",
+  "ssOutlineColor": "Outline color:",
+  "ssOutlineWidth": "Outline width:",
+  "ssEnableShadow": "Enable shadow:",
+  "ssShadowH": "Horizontal shadow:",
+  "ssShadowV": "Vertical shadow:",
+  "ssBlurDist": "Blur distance:",
+  "ssShadowColor": "Shadow color:",
+  "ssSearchFont": "Search fonts",
+  "ssSearchFontSize": "Search font sizes",
+  "ssTextColor": "Text color",
+  "ssTextHighlight": "Text highlight",
+  "ssFontSizeUp": "Increase font size",
+  "ssFontSizeDown": "Decrease font size",
+  "ssBold": "Bold",
+  "ssItalic": "Italic",
+  "ssUnderline": "Underline",
+  "ssStrike": "Strikethrough",
+  "ssSup": "Superscript",
+  "ssSub": "Subscript",
+  "ssInlineCode": "Inline code",
+  "ssQuote": "Quote",
+  "ssAIAssist": "AI assist",
+  "ssAIBeautify": "Beautify",
+  "ssAIExpand": "Expand",
+  "ssAISimplify": "Simplify",
+  "ssClearFormat": "Clear formatting",
+  "ssFormatPainter": "Format painter (double-click to keep active)",
+  "ssLink": "Link",
+  "ssLinkPh": "Enter a link",
+  "ssAlignJustify": "Justify",
+  "ssBulletList": "Bullets",
+  "ssOrderedList": "Numbering",
+  "ssIndentLess": "Decrease paragraph indent",
+  "ssIndentMore": "Increase paragraph indent",
+  "ssTextIndentLess": "Decrease first-line indent",
+  "ssTextIndentMore": "Increase first-line indent",
+  "ssNoTextToAI": "No text content to process",
+  "ssChartType": "Chart type: ",
+  "ssClickChange": "Click to change",
+  "ssClearData": "Clear Data",
+  "ssChartCat": "Category *",
+  "ssChartSer": "Series *",
+  "ssEditChart": "Edit chart",
+  "ssStackStyle": "Stack",
+  "ssSmoothLine": "Smooth line",
+  "ssBgFill": "Background fill:",
+  "ssAxisText": "Axes & text:",
+  "ssGridColor": "Grid color:",
+  "ssChartTheme": "Theme colors:",
+  "ssPresetChartTheme": "Preset chart themes:",
+  "ssSlideTheme": "Slide theme:",
+  "ssCustomColors": "Custom colors",
+  "ssChartThemeColors": "Chart theme colors",
+  "ssThemeColorNo": "Theme color *:",
+  "ssAddThemeColor": "Add theme color",
+  "ssIconColor": "Icon color:",
+  "ssAutoplay": "Autoplay:",
+  "ssLoopPlay": "Loop:",
+  "ssClipImage": "Crop image",
+  "ssByShape": "By shape:",
+  "ssByRatio": "By *:",
+  "ssRadius": "Corner radius:",
+  "ssReplaceImage": "Replace image",
+  "ssResetStyle": "Reset style",
+  "ssSetAsBg": "Set as background",
+  "ssRatioSquare": "Aspect ratio (square)",
+  "ssRatioPortrait": "Aspect ratio (portrait)",
+  "ssRatioLandscape": "Aspect ratio (landscape)",
+  "ssEditLatex": "Edit LaTeX",
+  "ssColor": "Color:",
+  "ssStrokeWidth": "Stroke width:",
+  "ssLineStyle": "Line style:",
+  "ssLineColor": "Line color:",
+  "ssLineWidth": "Line width:",
+  "ssStartStyle": "Start style:",
+  "ssEndStyle": "End style:",
+  "ssSwapDir": "Swap direction",
+  "ssClickReplaceShape": "Click to replace shape",
+  "ssSolidFill": "Solid fill",
+  "ssGradFill": "Gradient fill",
+  "ssImgFill": "Image fill",
+  "ssLinearGrad": "Linear gradient",
+  "ssRadialGrad": "Radial gradient",
+  "ssCurColorBlock": "Current color:",
+  "ssGradAngle": "Gradient angle:",
+  "ssAlignMiddle": "Center",
+  "ssShapePainter": "Shape format painter",
+  "ssCellFill": "Cell fill",
+  "ssEnableThemeTbl": "Enable themed table:",
+  "ssHeaderRow": "Header row",
+  "ssFooterRow": "Footer row",
+  "ssFirstCol": "First column",
+  "ssLastCol": "Last column",
+  "ssThemeColorLbl": "Theme color:",
+  "ssLineHeight": "Line spacing:",
+  "ssParaSpace": "Paragraph spacing:",
+  "ssWordSpace": "Letter spacing:",
+  "ssTextBoxFill": "Text box fill:",
+  "ssTimes": "x",
+  "ssBigTitle": "Title",
+  "ssSmallTitle": "Subtitle",
+  "ssBody": "Body",
+  "ssBodySmall": "Body (small)",
+  "ssVideoPoster": "Video poster",
+  "ssResetPoster": "Reset poster",
+  "ssBgFillTit": "Background fill",
+  "ssBgContain": "Contain",
+  "ssBgRepeat": "Tile",
+  "ssBgCover": "Cover",
+  "ssApplyBgAll": "Apply background to all",
+  "ssWideSixNine": "Widescreen 16 : 9",
+  "ssWideSixTen": "Widescreen 16 : 10",
+  "ssStdFourThree": "Standard 4 : 3",
+  "ssPaperAThreeFour": "Paper A3 / A4",
+  "ssPortAThreeFour": "Portrait A3 / A4",
+  "ssCanvasSize": "Canvas size: ",
+  "ssGlobalTheme": "Global theme",
+  "ssMore": "More",
+  "ssFont": "Font:",
+  "ssFontColor": "Font color:",
+  "ssBgColor": "Background color:",
+  "ssThemeColor": "Theme color:",
+  "ssApplyThemeAll": "Apply theme to all",
+  "ssExtractTheme": "Extract theme from slide",
+  "ssPresetThemeTit": "Preset themes",
+  "ssTextAa": "Text Aa",
+  "ssSet": "Set",
+  "ssSetApply": "Set & apply",
+  "ssEditThemeColor": "Edit theme colors",
+  "ssSlideThemeClr": "Slide theme color *:",
+  "ssChoose": "Select",
+  "ssApplyToTheme": "Apply to theme",
+  "ssThemePickTip": "Click a color block to exclude it",
+  "ssSaveSelTheme": "Save selected settings as theme",
+  "ssExtCurPage": "Extract from current slide",
+  "ssExtAllSlides": "Extract from all slides",
+  "ssThemeClrLimit": "Theme colors exceed the limit; the first 6 have been selected automatically",
+  "ssAddAnim": "Add animation",
+  "ssPickElAnim": "Select an element on the canvas to add animation",
+  "ssAnimDur": "Duration:",
+  "ssTrigMode": "Trigger:",
+  "ssTrigManual": "On click",
+  "ssTrigWithPrev": "With previous",
+  "ssTrigAfterPrev": "After previous",
+  "ssChangeAnim": "Change animation",
+  "ssStopPreview": "Stop preview",
+  "ssPreviewAll": "Preview all",
+  "ssAnimIn": "Entrance",
+  "ssAnimOut": "Exit",
+  "ssAnimEmph": "Emphasis",
+  "ssLayer": "Layer:",
+  "ssToTop": "Bring to front",
+  "ssToBottom": "Send to back",
+  "ssMoveUp": "Move up",
+  "ssMoveDown": "Move down",
+  "ssAlignLbl": "Align:",
+  "ssPosH": "Horizontal:",
+  "ssPosV": "Vertical:",
+  "ssWidth": "Width:",
+  "ssHeight": "Height:",
+  "ssRotate": "Rotate:",
+  "ssRatioUnlock": "Unlock aspect ratio",
+  "ssRatioLock": "Lock aspect ratio",
+  "ssUniDistH": "Distribute horizontally",
+  "ssUniDistV": "Distribute vertically",
+  "ssFillColor": "Fill color:",
+  "ssApplyAll": "Apply to all",
+  "ssAppliedAll": "Applied to all",
+  "ssLayout": "Layout",
+  "ssUndo": "Undo",
+  "ssRedo": "Redo",
+  "ssExitEdit": "Exit edit",
+  "ssNewSlide": "New slide",
+  "ssText": "Text",
+  "ssImage": "Image",
+  "ssRect": "Rectangle",
+  "ssCircle": "Circle",
+  "ssExitPlay": "Exit playback",
+  "ssPlay": "Play",
+  "ssFirstSlide": "First slide",
+  "ssLastSlide": "Last slide",
+  "ssFirstPage": "Already on the first slide",
+  "ssLastPage": "Already on the last slide",
+  "ssAutoPlayShow": "Auto play",
+  "ssCancelAuto": "Cancel auto play",
+  "ssAutoPlayStart": "Auto play started",
+  "ssPresenterView": "Presenter view",
+  "ssEnterFull": "Enter fullscreen",
+  "ssEndPlay": "End slideshow",
+  "ssStart": "Start",
+  "ssCountdown": "Countdown",
+  "ssLoopShow": "Loop slideshow",
+  "ssShowToolbar": "Show toolbar",
+  "ssAllSlides": "View all slides",
+  "ssBottomThumb": "Show bottom thumbnails",
+  "ssInitDataWait": "Initializing data, please wait...",
+  "ssDelSect": "Remove Section",
+  "ssDelSectSlides": "Remove Section and Slides",
+  "ssDelAllSect": "Remove all Sections",
+  "ssRenSect": "Rename Section",
+  "ssNewPage": "New Slide",
+  "ssSlideShow": "Slideshow",
+  "ssDupPage": "Duplicate Slide",
+  "ssDelPage": "Delete Slide",
+  "ssAddSect": "Add Section",
+  "ssPlayFromCur": "Present from Current Slide",
+  "ssDblClickEdit": "Double-click to edit",
+  "ssElText": "Text",
+  "ssElImage": "Image",
+  "ssElShape": "Shape",
+  "ssElLine": "Line",
+  "ssElChart": "Chart",
+  "ssElTable": "Table",
+  "ssElVideo": "Media",
+  "ssElAudio": "Audio",
+  "ssElLatex": "Formula",
+  "ssElFrame": "Web page",
+  "ssShapeRect": "Rectangle",
+  "ssShapeCommon": "Basic Shapes",
+  "ssShapeArrow": "Arrows",
+  "ssShapeOther": "More Shapes",
+  "ssShapeLinear": "Lines",
+  "ssLineStraight": "Lines",
+  "ssLinePolyCurve": "Connectors & Curves",
+  "ssChartBar": "Bar Chart",
+  "ssChartColumn": "Column Chart",
+  "ssChartLine": "Line Chart",
+  "ssChartArea": "Area Chart",
+  "ssChartScatter": "Scatter Chart",
+  "ssChartPie": "Pie Chart",
+  "ssChartRing": "Donut Chart",
+  "ssChartRadar": "Radar Chart",
+  "ssValue": "Value",
+  "ssX": "X",
+  "ssY": "Y",
+  "ssApply":"Apply",
+  "ssInsert":"Insert",
+  "ssGauss": "Gauss's Formula",
+  "ssFourier": "Fourier Series",
+  "ssTaylor": "Taylor Series Expansion",
+  "ssDefInt": "Definite Integral",
+  "ssTrigIdOne": "Trigonometric Identity I",
+  "ssTrigIdTwo": "Trigonometric Identity II",
+  "ssSumExpand": "Summation Expansion",
+  "ssEuler": "Euler's Formula",
+  "ssBernoulli": "Bernoulli Equation",
+  "ssExactDE": "Total Differential Equation",
+  "ssNonHom": "Non-homogeneous Equation",
+  "ssCauchyMVT": "Cauchy's Mean Value Theorem",
+  "ssLagMVT": "Lagrange's Mean Value Theorem",
+  "ssDerivForm": "Derivative Formulas",
+  "ssTrigInt": "Trigonometric Integrals",
+  "ssQuadSurf": "Quadric Surface",
+  "ssSecDiff": "Second-Order Differential",
+  "ssDirDeriv": "Directional Derivative",
+  "ssMath": "Math",
+  "ssCombo": "Combinations",
+  "ssFunc": "Functions",
+  "ssGreek": "Greek letters",
+  "ssExportPptist": "Export .pptist File",
+  "ssExportPptx": "Export PPTX",
+  "ssExportImage": "Export Images",
+  "ssExportJSON": "Export JSON"
+
+
+
+}

+ 660 - 3
src/views/lang/hk.json

@@ -1,3 +1,660 @@
- {
-  "lang": "hk"
- }
+{
+  "lang": "hk",
+  "ssImportPptx": "導入 PPTX 檔案",
+  "ssResetSlides": "重置投影片",
+  "ssHotkeys": "快捷操作",
+  "ssUploadPptx": "上傳 PPTX 檔案",
+  "ssStage": "投影片放映(F5)",
+  "ssFromStart": "從頭開始",
+  "ssFromCurrent": "從目前頁開始",
+  "ssExport": "匯出",
+  "ssImporting": "正在匯入...",
+  "ssCourseLoading": "正在載入課程內容...",
+  "ssParsePptFail": "解析 PPT 資料失敗",
+  "ssFetchCourseFail": "取得課程詳情失敗",
+  "ssPaste": "貼上",
+  "ssSelectAll": "全選",
+  "ssRuler": "標尺",
+  "ssGridLine": "網格線",
+  "ssNone": "無",
+  "ssSmall": "小",
+  "ssMedium": "中",
+  "ssLarge": "大",
+  "ssResetPage": "重置當前頁",
+  "ssUndoTip": "復原(Ctrl + Z)",
+  "ssRedoTip": "重做(Ctrl + Y)",
+  "ssNotePanel": "批註面板",
+  "ssSelectPane": "選擇窗格",
+  "ssSearchReplace": "查找替換",
+  "ssSearchTip": "查找/替換(Ctrl + F)",
+  "ssInsertText": "插入文字",
+  "ssTextHorizontal": "橫向文本框",
+  "ssTextVertical": "豎向文本框",
+  "ssInsertShape": "插入形狀",
+  "ssFreeDraw": "自由繪製",
+  "ssInsertImage": "插入圖片",
+  "ssInsertLine": "插入線條",
+  "ssInsertChart": "插入圖表",
+  "ssInsertTable": "插入表格",
+  "ssInsertFormula": "插入公式",
+  "ssInsertLearn": "插入學習內容",
+  "ssInsertMedia": "插入音視頻",
+  "ssEditTool": "編輯工具",
+  "ssZoomOutTip": "畫布縮小(Ctrl + -)",
+  "ssFitScreen": "適應螢幕",
+  "ssZoomInTip": "畫布放大(Ctrl + =)",
+  "ssFitScreenTip": "適應螢幕(Ctrl + 0)",
+  "ssFileReadFail": "無法正確讀取/解析該檔案",
+  "ssCoord": "座標*",
+  "ssSectName": "輸入節名稱",
+  "ssUntitledSec": "無標題節",
+  "ssDefSec": "預設節",
+  "ssSlidePage": "投影片",
+  "ssStyle": "樣式",
+  "ssSymbol": "符號",
+  "ssPosition": "位置",
+  "ssAnim": "動畫",
+  "ssDesign": "設計",
+  "ssSwitch": "切換",
+  "ssStyleMulti": "樣式(多選)",
+  "ssPosMulti": "位置(多選)",
+  "ssExpPptist": "匯出 pptist 檔案",
+  "ssExpPptx": "匯出 PPTX",
+  "ssExpImage": "匯出圖片",
+  "ssExpJson": "匯出 JSON",
+  "ssPrintPdf": "列印 / 匯出 PDF",
+  "ssSelectCnt": "選擇(*/*)",
+  "ssShowAll": "全部顯示",
+  "ssHideAll": "全部隱藏",
+  "ssGroup": "組合",
+  "ssFindInput": "輸入查找內容",
+  "ssReplInput": "輸入替換內容",
+  "ssIgnoreCase": "忽略大小寫",
+  "ssPrevOne": "上一個",
+  "ssNextOne": "下一個",
+  "ssReplace": "替換",
+  "ssReplaceAll": "全部替換",
+  "ssFindTab": "查找",
+  "ssReplTab": "替換",
+  "ssSlideNote": "投影片*的批註",
+  "ssReply": "回覆",
+  "ssDelete": "刪除",
+  "ssReplyInput": "輸入回覆內容",
+  "ssCancel": "取消",
+  "ssNoNotes": "本頁暫無批註",
+  "ssSelEl": "選中元素",
+  "ssCurSlide": "當前頁投影片",
+  "ssNoteInput": "輸入批註(為*)",
+  "ssClearNotes": "清空本頁批註",
+  "ssAddNote": "添加批註",
+  "ssTestUser": "測試用戶",
+  "ssMarkupTitle": "投影片類型標註",
+  "ssCurPageType": "當前頁面類型:",
+  "ssCurTextType": "當前文本類型:",
+  "ssCurImgType": "當前圖片類型:",
+  "ssMarkupHint": "選中圖片、文字、帶文字的形狀,標記類型",
+  "ssUnmarkedType": "未標記類型",
+  "ssCoverPage": "封面頁",
+  "ssTocPage": "目錄頁",
+  "ssTransPage": "過渡頁",
+  "ssContentPage": "內容頁",
+  "ssEndPage": "結束頁",
+  "ssTxtTitle": "標題",
+  "ssTxtSubttl": "副標題",
+  "ssTxtBody": "正文",
+  "ssTxtItem": "列表項目",
+  "ssTxtItemTtl": "列表項標題",
+  "ssTxtNotes": "註釋",
+  "ssTxtHeader": "頁眉",
+  "ssTxtFooter": "頁腳",
+  "ssTxtPartNo": "節編號",
+  "ssTxtItemNo": "項目編號",
+  "ssImgPageFig": "頁面插圖",
+  "ssImgItemFig": "項目插圖",
+  "ssImgBg": "背景圖",
+  "ssAiSubTpl": "從下方挑選合適的模板,開始生成PPT",
+  "ssAiSubOut": "確認下方內容大綱(點擊編輯內容,右鍵添加/刪除大綱項),開始選擇模板",
+  "ssAiSubSet": "在下方輸入您的PPT主題,並適當補充資訊,如行業、崗位、學科、用途等",
+  "ssAiPhTopic": "請輸入PPT主題,如:大學生職業生涯規劃",
+  "ssAiGen": "AI 生成",
+  "ssLangColon": "語言:",
+  "ssStyleColon": "風格:",
+  "ssModelColon": "模型:",
+  "ssImgColon": "配圖:",
+  "ssLangZh": "中文",
+  "ssLangZhV": "中文",
+  "ssLangEn": "英文",
+  "ssLangJa": "日文",
+  "ssStyGen": "通用",
+  "ssStyGenV": "通用",
+  "ssStyAcad": "學術風",
+  "ssStyAcadV": "学术风",
+  "ssStyWork": "職場風",
+  "ssStyWorkV": "职场风",
+  "ssStyEdu": "教育風",
+  "ssStyEduV": "教育风",
+  "ssStyMkt": "營銷風",
+  "ssStyMktV": "营销风",
+  "ssMockTest": "模擬測試",
+  "ssAiSearch": "AI搜圖",
+  "ssAiImgGen": "AI生圖",
+  "ssAiChooseTpl": "選擇模板",
+  "ssAiBackRe": "返回重新生成",
+  "ssAiMake": "生成",
+  "ssAiBackOut": "返回大綱",
+  "ssAiWait": "AI生成中,請耐心等待 ...",
+  "ssAiNeedTopic": "請先輸入PPT主題",
+  "ssAiRecA": "2025科技前沿動態",
+  "ssAiRecB": "大數據如何改變世界",
+  "ssAiRecC": "餐飲市場調查與研究",
+  "ssAiRecD": "AIGC在教育領域的應用",
+  "ssAiRecE": "社交媒體與品牌營銷",
+  "ssAiRecF": "5G技術如何改變我們的生活",
+  "ssAiRecG": "年度工作總結與展望",
+  "ssAiRecH": "區塊鏈技術及其應用",
+  "ssAiRecI": "大學生職業生涯規劃",
+  "ssAiRecJ": "公司年會策劃方案",
+  "ssInteract": "互動工具",
+  "ssHPage": "H5頁面",
+  "ssAiApp": "AI應用",
+  "ssVideo": "視頻",
+  "ssCreative": "創作空間",
+  "ssContentList": "內容列表",
+  "ssQandA": "問答",
+  "ssNoLearn": "暫無學習內容",
+  "ssNeedUpload": "請先上傳或創建學習內容",
+  "ssPreview": "預覽",
+  "ssEdit": "編輯",
+  "ssCopy": "複製",
+  "ssQATest": "問答題",
+  "ssBiliVideo": "B站視頻",
+  "ssUnknown": "未知",
+  "ssCourseOutline": "課程大綱",
+  "ssExpand": "展開",
+  "ssCollapse": "收起",
+  "ssSlideNum": "投影片 *",
+  "ssPrevPage": "上一頁",
+  "ssNextPage": "下一頁",
+  "ssFullscreen": "全屏",
+  "ssFollowOn": "開啟跟隨模式",
+  "ssFollowOff": "關閉跟隨模式",
+  "ssRefresh": "刷新",
+  "ssSubmitHW": "提交作業",
+  "ssSubmitting": "提交中...",
+  "ssTimer": "計時器",
+  "ssPenTool": "畫筆工具",
+  "ssLaserPen": "激光筆",
+  "ssOpenFull": "打開全屏",
+  "ssExitFull": "退出全屏",
+  "ssHwSubmit": "作業提交",
+  "ssHwSubmitting": "作業提交中...",
+  "ssSubmittingEll": "提交中...",
+  "ssSubmit": "提交",
+  "ssRefreshIframe": "刷新iframe內容",
+  "ssHwLoading": "正在載入作業...",
+  "ssAnswerRes": "回答結果",
+  "ssDialogArea": "對話區",
+  "ssConnecting": "連接中...",
+  "ssConnLost": "連接斷開",
+  "ssReconnect": "重新連接",
+  "ssHwTypeUnsup": "暫不支持的作業類型",
+  "ssFileUploadFail": "文件上傳失敗",
+  "ssHwSubmitWp": "workPage作業提交",
+  "ssHwSubmitSucc": "作業提交成功",
+  "ssHwSubmitRetry": "作業提交失敗,請重試",
+  "ssShotFail": "截圖提交失敗",
+  "ssShotSucc": "頁面截圖提交成功",
+  "ssHwNoFunc": "未找到可用的作業提交功能",
+  "ssHwSubmitFail": "作業提交失敗",
+  "ssNoIframe": "當前頁面沒有找到iframe元素",
+  "ssRefreshDone": "刷新完成",
+  "ssNoIframeRef": "沒有找到可刷新的iframe",
+  "ssRefreshFail": "刷新iframe失敗",
+  "ssWorkInfoFail": "獲取作業信息失敗",
+  "ssStuInfoFail": "獲取學生信息失敗",
+  "ssFollowOnTip": "跟隨模式已開啟",
+  "ssFreeOnTip": "自由模式已開啟",
+  "ssOpFailRetry": "操作失敗,請重試",
+  "ssNetError": "網絡連接異常,請檢查網絡後刷新頁面",
+  "ssLoading": "加載中 ...",
+  "ssInkWidth": "墨跡粗細:",
+  "ssShape": "形狀",
+  "ssHighlight": "熒光筆",
+  "ssEraserSz": "橡皮大小:",
+  "ssEraser": "橡皮擦",
+  "ssClearInk": "清除墨跡",
+  "ssBlackboard": "黑板",
+  "ssClosePen": "關閉畫筆",
+  "ssPause": "暫停",
+  "ssStartTimer": "開始計時",
+  "ssJsonReadFail": "無法正確讀取 / 解析該JSON數據",
+  "ssNoImage": "暫無圖片",
+  "ssShotWork": "截圖作業",
+  "ssActualSize": "實際大小",
+  "ssReset": "重置",
+  "ssClose": "關閉",
+  "ssPrevQ": "上一題",
+  "ssNextQ": "下一題",
+  "ssSingleSel": "單選題",
+  "ssMultiSel": "多選題",
+  "ssNodeTitle": "節點*",
+  "ssClearChat": "清空聊天記錄",
+  "ssChatPhInput": "請輸入",
+  "ssClearChatTip": "確定要清空所有聊天記錄嗎?此操作不可恢復。",
+  "ssConfirmClear": "確認清空",
+  "ssAiHello": "你好!我是你的學習助手,有什麼可以幫助你嗎?",
+  "ssTeacher": "老師",
+  "ssAiHelper": "AI助手",
+  "ssNetErrShort": "網絡錯誤",
+  "ssChatCleared": "聊天記錄已清空",
+  "ssClearFail": "清空聊天記錄失敗,請重試",
+  "ssChoiceStat": "選擇題統計",
+  "ssTotalSub": "總提交:* 人",
+  "ssNotChoice": "當前頁面不是選擇題",
+  "ssNoStat": "暫無統計數據",
+  "ssViewStu": "查看學生",
+  "ssSelStu": "選擇同學:",
+  "ssViewQ": "查看題目",
+  "ssViewRes": "查看結果",
+  "ssParticipants": "參與人數",
+  "ssAccuracy": "正確率",
+  "ssCorrectAns": "正確答案",
+  "ssUnsubStu": "未提交人員",
+  "ssSubStu": "已提交人員",
+  "ssCorrectTag": "正確",
+  "ssMultiOpt": "多選項",
+  "ssPageImage": "頁面圖片",
+  "ssSlideLearn": "當前投影片已有學習內容,一個投影片只能插入一個學習內容",
+  "ssCut": "剪切",
+  "ssAlignHCenter": "水平置中",
+  "ssAlignHVCenter": "水平垂直置中",
+  "ssAlignLeft": "靠左對齊",
+  "ssAlignRight": "靠右對齊",
+  "ssAlignVCenter": "垂直置中",
+  "ssAlignTop": "靠上對齊",
+  "ssAlignBottom": "靠下對齊",
+  "ssBringFront": "置於頂層",
+  "ssBringForward": "上移一層",
+  "ssSendBack": "置於底層",
+  "ssSendBackward": "下移一層",
+  "ssSetLink": "設置連結",
+  "ssGroupEl": "組合",
+  "ssUngroupEl": "取消組合",
+  "ssLock": "鎖定",
+  "ssUnlock": "解鎖",
+  "ssOpenNewWin": "在新視窗打開",
+  "ssCopyLink": "複製連結",
+  "ssShapeHint": "點擊繪製任意形狀,首尾閉合完成繪製,按 ESC 鍵或滑鼠右鍵取消,按 ENTER 鍵提前完成",
+  "ssWebUrlPh": "請輸入網頁連結地址",
+  "ssConfirm": "確認",
+  "ssTabWebLink": "網頁連結",
+  "ssTabSlidePg": "投影片頁面",
+  "ssEditWebLink": "修改網頁連結",
+  "ssWebUrlReq": "請輸入網頁連結地址",
+  "ssWebUrlInvalid": "請輸入正確的網頁連結格式",
+  "ssTable": "表格",
+  "ssBack": "返回",
+  "ssCustom": "自定義",
+  "ssRowCnt": "行數:",
+  "ssColCnt": "列數:",
+  "ssTblRangeWarn": "行數/列數必須在0~20之間!",
+  "ssAudio": "音頻",
+  "ssVideoUrlPh": "請輸入視頻地址,e.g. https://xxx.mp4",
+  "ssAudioUrlPh": "請輸入音頻地址,e.g. https://xxx.mp3",
+  "ssVideoUrlReq": "請先輸入正確的視頻地址",
+  "ssAudioUrlReq": "請先輸入正確的音頻地址",
+  "ssPickLearn": "請選擇要嵌入的學習內容",
+  "ssPickWebFirst": "請先選擇一個網頁",
+  "ssLatexPh": "輸入 LaTeX 公式",
+  "ssFormulaPrev": "公式預覽",
+  "ssLatexSym": "常用符號",
+  "ssLatexPreset": "預置公式",
+  "ssLatexEmpty": "公式不能為空",
+  "ssExpFormat": "匯出格式:",
+  "ssExpRange": "匯出範圍:",
+  "ssCurPage": "當前頁",
+  "ssCustomRange": "自定義範圍:",
+  "ssImgQuality": "圖片質量:",
+  "ssIgnoreWebfont": "忽略在線字體:",
+  "ssIgnoreWebfontTip": "匯出時預設忽略在線字體。若在幻燈片中使用了在線字體且希望匯出後保留樣式,可關閉【忽略在線字體】選項,但會增加匯出用時。",
+  "ssExporting": "正在匯出...",
+  "ssChoiceQ": "選擇題",
+  "ssEssayQ": "問答題",
+  "ssAIApp": "AI應用",
+  "ssH5Page": "H5頁面",
+  "ssBilibili": "B站視頻",
+  "ssCreateSpace": "創作空間",
+  "ssInsertCol": "插入列",
+  "ssToLeft": "到左側",
+  "ssToRight": "到右側",
+  "ssInsertRow": "插入行",
+  "ssAbove": "到上方",
+  "ssBelow": "到下方",
+  "ssDeleteCol": "刪除列",
+  "ssDeleteRow": "刪除行",
+  "ssMergeCells": "合併單元格",
+  "ssUnmergeCells": "取消合併單元格",
+  "ssSelectCurCol": "選中當前列",
+  "ssSelectCurRow": "選中當前行",
+  "ssSelectAllCells": "選中全部單元格",
+  "ssVideoLoadFail": "視頻加載失敗",
+  "ssPlaybackRate": "倍速",
+  "ssLoop": "循環",
+  "ssOn": "開",
+  "ssOff": "關",
+  "ssFontLoadWait": "字體需要等待加載下載後生效,請稍等",
+  "ssImgLoading": "圖片加載中...",
+  "ssPickLinkTarget": "請先選擇連結目標",
+  "ssNoMatch": "未查找到匹配項",
+  "ssAudioLoadFail": "音頻加載失敗",
+  "ssSlidePg": "投影片頁面 *",
+  "ssRemove": "移除",
+  "ssPerPage": "每頁數量:",
+  "ssEdgePad": "邊緣留白:",
+  "ssPrintBgTip": "提示:若打印預覽與實際樣式不一致,請在彈出的打印視窗中勾選【背景圖形】選項。",
+  "ssIgnoreMedia": "忽略音頻/視頻:",
+  "ssIgnoreMediaTip": "匯出時預設忽略音視頻。若您的投影片中存在音視頻元素且希望將其匯出到 PPTX 檔案中,可關閉【忽略音視頻】選項,但要注意這將會大幅增加匯出用時。",
+  "ssMastOver": "覆蓋預設母版:",
+  "ssPptxTip": "提示:1. 支援匯出格式:avi、mp4、mov、wmv、mp3、wav;2. 跨域資源無法匯出。",
+  "ssPptistTip": "提示:.pptist 是本應用的特有檔案後綴,支援將該類型的檔案匯入回應用中。",
+  "ssSpeakerNotePh": "點擊輸入演講者備註",
+  "ssInsertAllTpl": "插入全部",
+  "ssInsertTpl": "插入模板",
+  "ssTplAll": "全部",
+  "ssTplCover": "封面",
+  "ssTplDir": "目錄",
+  "ssTplTrans": "過渡",
+  "ssTplContent": "內容",
+  "ssTplEnd": "結束",
+  "ssColorMask": "著色(蒙版):",
+  "ssMaskColor": "蒙版顏色:",
+  "ssEnableFilter": "啟用濾鏡:",
+  "ssFlipV": "垂直翻轉",
+  "ssFlipH": "水平翻轉",
+  "ssBlur": "模糊",
+  "ssBrightness": "亮度",
+  "ssContrast": "對比度",
+  "ssGrayscale": "灰度",
+  "ssSaturate": "飽和度",
+  "ssHueRotate": "色相",
+  "ssSepia": "褐色",
+  "ssInvert": "反轉",
+  "ssOpacity": "不透明度",
+  "ssPresetBW": "黑白",
+  "ssPresetRetro": "復古",
+  "ssPresetSharp": "銳化",
+  "ssPresetSoft": "柔和",
+  "ssPresetWarm": "暖色",
+  "ssPresetBright": "明亮",
+  "ssPresetVivid": "鮮豔",
+  "ssPresetBlur": "模糊",
+  "ssPresetInvert": "反轉",
+  "ssEnableOutline": "啟用邊框:",
+  "ssOutlineStyle": "邊框樣式:",
+  "ssOutlineColor": "邊框顏色:",
+  "ssOutlineWidth": "邊框粗細:",
+  "ssEnableShadow": "啟用陰影:",
+  "ssShadowH": "水平陰影:",
+  "ssShadowV": "垂直陰影:",
+  "ssBlurDist": "模糊距離:",
+  "ssShadowColor": "陰影顏色:",
+  "ssSearchFont": "搜索字體",
+  "ssSearchFontSize": "搜索字號",
+  "ssTextColor": "文字顏色",
+  "ssTextHighlight": "文字高亮",
+  "ssFontSizeUp": "增大字號",
+  "ssFontSizeDown": "減小字號",
+  "ssBold": "加粗",
+  "ssItalic": "斜體",
+  "ssUnderline": "下劃線",
+  "ssStrike": "刪除線",
+  "ssSup": "上標",
+  "ssSub": "下標",
+  "ssInlineCode": "行內代碼",
+  "ssQuote": "引用",
+  "ssAIAssist": "AI 輔助",
+  "ssAIBeautify": "美化",
+  "ssAIExpand": "擴寫",
+  "ssAISimplify": "精簡",
+  "ssClearFormat": "清除格式",
+  "ssFormatPainter": "格式刷(雙擊連續使用)",
+  "ssLink": "超連結",
+  "ssLinkPh": "請輸入超連結",
+  "ssAlignJustify": "兩端對齊",
+  "ssBulletList": "項目符號",
+  "ssOrderedList": "編號",
+  "ssIndentLess": "減小段落縮進",
+  "ssIndentMore": "增大段落縮進",
+  "ssTextIndentLess": "減小首行縮進",
+  "ssTextIndentMore": "增大首行縮進",
+  "ssNoTextToAI": "沒有可以執行的文本內容",
+  "ssChartType": "圖表類型:",
+  "ssClickChange": "點擊更換",
+  "ssClearData": "清空數據",
+  "ssChartCat": "類別*",
+  "ssChartSer": "系列*",
+  "ssEditChart": "編輯圖表",
+  "ssStackStyle": "堆疊樣式",
+  "ssSmoothLine": "使用平滑曲線",
+  "ssBgFill": "背景填充:",
+  "ssAxisText": "座標與文字:",
+  "ssGridColor": "網格顏色:",
+  "ssChartTheme": "主題配色:",
+  "ssPresetChartTheme": "預置圖表主題:",
+  "ssSlideTheme": "投影片主題:",
+  "ssCustomColors": "自定義配色",
+  "ssChartThemeColors": "圖表主題配色",
+  "ssThemeColorNo": "主題配色*:",
+  "ssAddThemeColor": "添加主題色",
+  "ssIconColor": "圖標顏色:",
+  "ssAutoplay": "自動播放:",
+  "ssLoopPlay": "循環播放:",
+  "ssClipImage": "裁剪圖片",
+  "ssByShape": "按形狀:",
+  "ssByRatio": "按*:",
+  "ssRadius": "圓角半徑:",
+  "ssReplaceImage": "替換圖片",
+  "ssResetStyle": "重置樣式",
+  "ssSetAsBg": "設為背景",
+  "ssRatioSquare": "縱橫比(正方形)",
+  "ssRatioPortrait": "縱橫比(縱向)",
+  "ssRatioLandscape": "縱橫比(橫向)",
+  "ssEditLatex": "編輯 LaTeX",
+  "ssColor": "顏色:",
+  "ssStrokeWidth": "粗細:",
+  "ssLineStyle": "線條樣式:",
+  "ssLineColor": "線條顏色:",
+  "ssLineWidth": "線條寬度:",
+  "ssStartStyle": "起點樣式:",
+  "ssEndStyle": "終點樣式:",
+  "ssSwapDir": "交換方向",
+  "ssClickReplaceShape": "點擊替換形狀",
+  "ssSolidFill": "純色填充",
+  "ssGradFill": "漸變填充",
+  "ssImgFill": "圖片填充",
+  "ssLinearGrad": "線性漸變",
+  "ssRadialGrad": "徑向漸變",
+  "ssCurColorBlock": "當前色塊:",
+  "ssGradAngle": "漸變角度:",
+  "ssAlignMiddle": "居中",
+  "ssShapePainter": "形狀格式刷",
+  "ssCellFill": "單元格填充",
+  "ssEnableThemeTbl": "啟用主題表格:",
+  "ssHeaderRow": "標題行",
+  "ssFooterRow": "匯總行",
+  "ssFirstCol": "第一列",
+  "ssLastCol": "最後一列",
+  "ssThemeColorLbl": "主題顏色:",
+  "ssLineHeight": "行間距:",
+  "ssParaSpace": "段間距:",
+  "ssWordSpace": "字間距:",
+  "ssTextBoxFill": "文本框填充:",
+  "ssTimes": "倍",
+  "ssBigTitle": "大標題",
+  "ssSmallTitle": "小標題",
+  "ssBody": "正文",
+  "ssBodySmall": "正文[小]",
+  "ssVideoPoster": "影片預覽封面",
+  "ssResetPoster": "重置封面",
+  "ssBgFillTit": "背景填充",
+  "ssBgContain": "縮放",
+  "ssBgRepeat": "拼貼",
+  "ssBgCover": "縮放鋪滿",
+  "ssApplyBgAll": "應用背景到全部",
+  "ssWideSixNine": "寬屏 16 : 9",
+  "ssWideSixTen": "寬屏 16 : 10",
+  "ssStdFourThree": "標準 4 : 3",
+  "ssPaperAThreeFour": "紙張 A3 / A4",
+  "ssPortAThreeFour": "豎向 A3 / A4",
+  "ssCanvasSize": "畫布尺寸:",
+  "ssGlobalTheme": "全局主題",
+  "ssMore": "更多",
+  "ssFont": "字體:",
+  "ssFontColor": "字體顏色:",
+  "ssBgColor": "背景顏色:",
+  "ssThemeColor": "主題色:",
+  "ssApplyThemeAll": "應用主題到全部",
+  "ssExtractTheme": "從幻燈片提取主題",
+  "ssPresetThemeTit": "預置主題",
+  "ssTextAa": "文字 Aa",
+  "ssSet": "設置",
+  "ssSetApply": "設置並應用",
+  "ssEditThemeColor": "編輯主題色",
+  "ssSlideThemeClr": "幻燈片主題色*:",
+  "ssChoose": "選擇",
+  "ssApplyToTheme": "應用到主題",
+  "ssThemePickTip": "點擊色塊排除不要的顏色",
+  "ssSaveSelTheme": "將選中配置保存為主題",
+  "ssExtCurPage": "從當前頁中提取",
+  "ssExtAllSlides": "從全部幻燈片提取",
+  "ssThemeClrLimit": "主題色超出數量限制,已自動選取前6個",
+  "ssAddAnim": "添加動畫",
+  "ssPickElAnim": "選中畫布中的元素添加動畫",
+  "ssAnimDur": "持續時長:",
+  "ssTrigMode": "觸發方式:",
+  "ssTrigManual": "主動觸發",
+  "ssTrigWithPrev": "與上一動畫同時",
+  "ssTrigAfterPrev": "上一動畫之後",
+  "ssChangeAnim": "更換動畫",
+  "ssStopPreview": "停止預覽",
+  "ssPreviewAll": "預覽全部",
+  "ssAnimIn": "入場",
+  "ssAnimOut": "退場",
+  "ssAnimEmph": "強調",
+  "ssLayer": "層級:",
+  "ssToTop": "置頂",
+  "ssToBottom": "置底",
+  "ssMoveUp": "上移",
+  "ssMoveDown": "下移",
+  "ssAlignLbl": "對齊:",
+  "ssPosH": "水平:",
+  "ssPosV": "垂直:",
+  "ssWidth": "寬度:",
+  "ssHeight": "高度:",
+  "ssRotate": "旋轉:",
+  "ssRatioUnlock": "解除寬高比鎖定",
+  "ssRatioLock": "寬高比鎖定",
+  "ssUniDistH": "水平平均分佈",
+  "ssUniDistV": "垂直平均分佈",
+  "ssFillColor": "填充顏色:",
+  "ssApplyAll": "應用到全部",
+  "ssAppliedAll": "已應用到全部",
+  "ssLayout": "佈局",
+  "ssUndo": "撤銷",
+  "ssRedo": "重做",
+  "ssExitEdit": "退出編輯",
+  "ssNewSlide": "新幻燈片",
+  "ssText": "文字",
+  "ssImage": "圖片",
+  "ssRect": "矩形",
+  "ssCircle": "圓形",
+  "ssExitPlay": "退出播放",
+  "ssPlay": "播放",
+  "ssFirstSlide": "第一頁",
+  "ssLastSlide": "最後一頁",
+  "ssFirstPage": "已經是第一頁了",
+  "ssLastPage": "已經是最後一頁了",
+  "ssAutoPlayShow": "自動放映",
+  "ssCancelAuto": "取消自動放映",
+  "ssAutoPlayStart": "開始自動放映",
+  "ssPresenterView": "演講者視圖",
+  "ssEnterFull": "進入全屏",
+  "ssEndPlay": "結束放映",
+  "ssStart": "開始",
+  "ssCountdown": "倒計時",
+  "ssLoopShow": "循環放映",
+  "ssShowToolbar": "顯示工具欄",
+  "ssAllSlides": "查看所有投影片",
+  "ssBottomThumb": "觸底顯示縮略圖",
+  "ssInitDataWait": "數據初始化中,請稍等 ...",
+  "ssDelSect": "刪除節",
+  "ssDelSectSlides": "刪除節和幻燈片",
+  "ssDelAllSect": "刪除所有節",
+  "ssRenSect": "重命名節",
+  "ssNewPage": "新建頁面",
+  "ssSlideShow": "幻燈片放映",
+  "ssDupPage": "複製頁面",
+  "ssDelPage": "刪除頁面",
+  "ssAddSect": "增加節",
+  "ssPlayFromCur": "從當前放映",
+  "ssDblClickEdit": "雙擊編輯",
+  "ssElText": "文本",
+  "ssElImage": "圖片",
+  "ssElShape": "形狀",
+  "ssElLine": "線條",
+  "ssElChart": "圖表",
+  "ssElTable": "表格",
+  "ssElVideo": "視頻",
+  "ssElAudio": "音頻",
+  "ssElLatex": "公式",
+  "ssElFrame": "網頁",
+  "ssShapeRect": "矩形",
+  "ssShapeCommon": "常用形狀",
+  "ssShapeArrow": "箭頭",
+  "ssShapeOther": "其他形狀",
+  "ssShapeLinear": "線性",
+  "ssLineStraight": "直線",
+  "ssLinePolyCurve": "折線、曲線",
+  "ssChartBar": "柱狀圖",
+  "ssChartColumn": "條形圖",
+  "ssChartLine": "折線圖",
+  "ssChartArea": "面積圖",
+  "ssChartScatter": "散點圖",
+  "ssChartPie": "餅圖",
+  "ssChartRing": "環形圖",
+  "ssChartRadar": "雷達圖",
+  "ssValue": "值",
+  "ssX": "X",
+  "ssY": "Y",
+  "ssApply":"確認",
+  "ssInsert":"確認",
+  "ssGauss": "高斯公式",
+  "ssFourier": "傅里葉級數",
+  "ssTaylor": "泰勒展開式",
+  "ssDefInt": "定積分",
+  "ssTrigIdOne": "三角恆等式1",
+  "ssTrigIdTwo": "三角恆等式2",
+  "ssSumExpand": "和的展開式",
+  "ssEuler": "歐拉公式",
+  "ssBernoulli": "貝努利方程",
+  "ssExactDE": "全微分方程",
+  "ssNonHom": "非齊次方程",
+  "ssCauchyMVT": "柯西中值定理",
+  "ssLagMVT": "拉格朗日中值定理",
+  "ssDerivForm": "導數公式",
+  "ssTrigInt": "三角函數積分",
+  "ssQuadSurf": "二次曲面",
+  "ssSecDiff": "二階微分",
+  "ssDirDeriv": "方向導數",
+  "ssMath": "數學",
+  "ssCombo": "組合",
+  "ssFunc": "函數",
+  "ssGreek": "希臘字母",
+  "ssExportPptist": "導出 .pptist 文件",
+  "ssExportPptx": "導出 PPTX ",
+  "ssExportImage": "導出圖片",
+  "ssExportJSON": "導出 JSON"
+
+
+
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов