Browse Source

Merge branch 'beta'

lsc 4 days ago
parent
commit
f59c5fd944

+ 1 - 0
src/components/CollapsibleToolbar/index.vue

@@ -288,6 +288,7 @@ const getTypeLabel = (type?: number) => {
     76: lang.ssCreative,
     76: lang.ssCreative,
     77: lang.ssEnglishSpeakingTool,
     77: lang.ssEnglishSpeakingTool,
     78: lang.ssVote,
     78: lang.ssVote,
+    79: lang.ssPhoto,
   }
   }
   return typeMap[type || 0] || lang.ssUnknown
   return typeMap[type || 0] || lang.ssUnknown
 }
 }

+ 18 - 2
src/components/CollapsibleToolbar/index2.vue

@@ -232,6 +232,7 @@
         <img class="submenu-img" v-else-if="hoveredTool === 'qa'" key="qa" :src="toolAnswer" alt="">
         <img class="submenu-img" v-else-if="hoveredTool === 'qa'" key="qa" :src="toolAnswer" alt="">
         <img class="submenu-img" v-else-if="hoveredTool === 'choice'" key="choice" :src="toolChoice" alt="">
         <img class="submenu-img" v-else-if="hoveredTool === 'choice'" key="choice" :src="toolChoice" alt="">
         <img class="submenu-img" v-else-if="hoveredTool === 'vote'" key="vote" :src="toolVote" alt="">
         <img class="submenu-img" v-else-if="hoveredTool === 'vote'" key="vote" :src="toolVote" alt="">
+        <img class="submenu-img" v-else-if="hoveredTool === 'photo'" key="photo" :src="toolPhoto" alt="">
       </transition>
       </transition>
       <div class="submenu-item-box">
       <div class="submenu-item-box">
         <div class="submenu-item" @click="handleToolClick('choice')" @mouseenter="hoveredTool = 'choice'"
         <div class="submenu-item" @click="handleToolClick('choice')" @mouseenter="hoveredTool = 'choice'"
@@ -250,13 +251,21 @@
           <span class="submenu-label">{{ lang.ssQandA }}</span>
           <span class="submenu-label">{{ lang.ssQandA }}</span>
         </div>
         </div>
         <div class="submenu-item" @click="handleToolClick('vote')" @mouseenter="hoveredTool = 'vote'"
         <div class="submenu-item" @click="handleToolClick('vote')" @mouseenter="hoveredTool = 'vote'"
-          @mouseleave="hoveredTool = null" v-show="false">
+          @mouseleave="hoveredTool = null">
           <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
           <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
               <polyline points="9 11 12 14 22 4"></polyline>
               <polyline points="9 11 12 14 22 4"></polyline>
               <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
               <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
           </svg>
           </svg>
           <span class="submenu-label">{{ lang.ssVote }}</span>
           <span class="submenu-label">{{ lang.ssVote }}</span>
         </div>
         </div>
+        <div class="submenu-item" @click="handleToolClick('photo')" @mouseenter="hoveredTool = 'photo'"
+          @mouseleave="hoveredTool = null">
+          <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+              <path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"></path>
+              <circle cx="12" cy="13" r="4"></circle>
+          </svg>
+          <span class="submenu-label">{{ lang.ssPhoto }}</span>
+        </div>
         <div class="submenu-item" @click="handleToolClick('creative')" @mouseenter="hoveredTool = null"
         <div class="submenu-item" @click="handleToolClick('creative')" @mouseenter="hoveredTool = null"
           @mouseleave="hoveredTool = null">
           @mouseleave="hoveredTool = null">
           <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
           <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -555,6 +564,9 @@ import { lang } from '@/main'
 import toolChoice from '@/assets/img/tool_choice.jpeg'
 import toolChoice from '@/assets/img/tool_choice.jpeg'
 import toolAnswer from '@/assets/img/tool_answer.png'
 import toolAnswer from '@/assets/img/tool_answer.png'
 import toolVote from '@/assets/img/tool_vote.png'
 import toolVote from '@/assets/img/tool_vote.png'
+import toolPhoto from '@/assets/img/tool_photo.png'
+
+
 
 
 interface ContentItem {
 interface ContentItem {
   tool?: number
   tool?: number
@@ -848,6 +860,9 @@ const handleToolClick = _.debounce((tool: string) => {
   else if (tool === 'vote') {
   else if (tool === 'vote') {
     parentWindow?.addTool?.(78)
     parentWindow?.addTool?.(78)
   }
   }
+  else if (tool === 'photo') {
+    parentWindow?.addTool?.(79)
+  } 
   else if (tool === 'qa') {
   else if (tool === 'qa') {
     parentWindow?.addTool?.(15)
     parentWindow?.addTool?.(15)
   }
   }
@@ -902,7 +917,7 @@ const addContent = (data: ContentItem, type: number) => {
   // contentList.value.push(data)
   // contentList.value.push(data)
   if (type === 2) {
   if (type === 2) {
     const elements = currentSlide.value?.elements || []
     const elements = currentSlide.value?.elements || []
-    const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15 || el.toolType === 78))
+    const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15 || el.toolType === 78 || el.toolType === 79))
     if (frameElement) {
     if (frameElement) {
       slidesStore.updateElement({
       slidesStore.updateElement({
         id: frameElement.id,
         id: frameElement.id,
@@ -963,6 +978,7 @@ const getTypeLabel = (type?: number) => {
     76: lang.ssCreative,
     76: lang.ssCreative,
     77: lang.ssEnglishSpeakingTool,
     77: lang.ssEnglishSpeakingTool,
     78: lang.ssVote,
     78: lang.ssVote,
+    79: lang.ssPhoto,
   }
   }
   return typeMap[type || 0] || lang.ssUnknown
   return typeMap[type || 0] || lang.ssUnknown
 }
 }

+ 1 - 0
src/components/ColorButton.vue

@@ -33,6 +33,7 @@ defineProps<{
 .content {
 .content {
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
+  padding: 0;
 }
 }
 .color-btn-icon {
 .color-btn-icon {
   width: 32px;
   width: 32px;

+ 143 - 0
src/components/Contextmenu2/MenuContent.vue

@@ -0,0 +1,143 @@
+<template>
+  <ul class="menu-content" 
+    :style="{width: lang.lang === 'en' ? '160px' : '120px'}"
+  >
+    <template v-for="(menu, index) in menus" :key="menu.text || index">
+      <li
+        v-if="!menu.hide"
+        class="menu-item"
+        @click.stop="handleClickMenuItem(menu)"
+        :class="{'divider': menu.divider, 'disable': menu.disable}"
+      >
+        <div 
+          class="menu-item-content" 
+          :class="{
+            'has-children': menu.children,
+            'has-handler': menu.handler,
+          }" 
+          v-if="!menu.divider"
+        >
+          <span class="text">{{menu.text}}</span>
+          <span class="sub-text" v-if="menu.subText && !menu.children">{{menu.subText}}</span>
+
+          <menu-content 
+            class="sub-menu"
+            :menus="menu.children" 
+            v-if="menu.children && menu.children.length"
+            :handleClickMenuItem="handleClickMenuItem" 
+          />
+        </div>
+      </li>
+    </template>
+  </ul>
+</template>
+
+<script lang="ts" setup>
+import type { ContextmenuItem } from './types'
+import { lang } from '@/main'
+defineProps<{
+  menus: ContextmenuItem[]
+  handleClickMenuItem: (item: ContextmenuItem) => void
+}>()
+</script>
+
+<style lang="scss" scoped>
+$menuWidth: 180px;
+$menuHeight: 30px;
+$subMenuWidth: 120px;
+
+.menu-content {
+  width: $menuWidth;
+  padding: 8px;
+  // background: #fff;
+  background: #fff;
+  box-shadow: $boxShadow;
+  border-radius: 10px;
+  list-style: none;
+  margin: 0;
+}
+.menu-item {
+  // padding: 0 20px;
+  color: #555;
+  font-size: 14px;
+  transition: all $transitionDelayFast;
+  white-space: nowrap;
+  height: 40px;
+  line-height: 40px;
+  // background-color: #fff;
+  background-color: #fff;
+  cursor: pointer;
+  border-radius: 10px;
+
+  &:not(.disable):hover > .menu-item-content > .sub-menu {
+    display: block;
+  }
+
+  &:not(.disable):hover > .has-children.has-handler::after {
+    transform: scale(1);
+  }
+
+  &:hover:not(.disable) {
+    // background-color: rgba($color: $themeColor, $alpha: .2);
+    background-color: rgba($color: #ff9300, $alpha: .2);
+  }
+
+  &.divider {
+    height: 1px;
+    overflow: hidden;
+    margin: 5px;
+    background-color: #e5e5e5;
+    line-height: 0;
+    padding: 0;
+  }
+
+  &.disable {
+    color: #b1b1b1;
+    cursor: no-drop;
+  }
+}
+.menu-item-content {
+  display: flex;
+  align-items: center;
+  // justify-content: space-between;
+  justify-content: center;
+  position: relative;
+
+  &.has-children::before {
+    content: '';
+    display: inline-block;
+    width: 8px;
+    height: 8px;
+    border-width: 1px;
+    border-style: solid;
+    border-color: #666 #666 transparent transparent;
+    position: absolute;
+    right: 0;
+    top: 50%;
+    transform: translateY(-50%) rotate(45deg);
+  }
+  &.has-children.has-handler::after {
+    content: '';
+    display: inline-block;
+    width: 1px;
+    height: 24px;
+    background-color: rgba($color: #fff, $alpha: .3);
+    position: absolute;
+    right: 18px;
+    top: 3px;
+    transform: scale(0);
+    transition: transform $transitionDelay;
+  }
+
+  .sub-text {
+    opacity: 0.6;
+  }
+  .sub-menu {
+    width: $subMenuWidth;
+    position: absolute;
+    display: none;
+    left: 112%;
+    top: -6px;
+  }
+}
+</style>

+ 80 - 0
src/components/Contextmenu2/index.vue

@@ -0,0 +1,80 @@
+<template>
+  <div 
+    class="mask"
+    @contextmenu.prevent="removeContextmenu()"
+    @mousedown.left="removeContextmenu()"
+  ></div>
+
+  <div 
+    class="contextmenu"
+    :style="{
+      left: style.left + 'px',
+      top: style.top + 'px',
+    }"
+    @contextmenu.prevent
+  >
+    <MenuContent 
+      :menus="menus"
+      :handleClickMenuItem="handleClickMenuItem" 
+    />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue'
+import type { ContextmenuItem, Axis } from './types'
+
+import MenuContent from './MenuContent.vue'
+
+const props = defineProps<{
+  axis: Axis
+  el: HTMLElement
+  menus: ContextmenuItem[]
+  removeContextmenu: () => void
+}>()
+
+const style = computed(() => {
+  const MENU_WIDTH = 180
+  const MENU_HEIGHT = 30
+  const DIVIDER_HEIGHT = 11
+  const PADDING = 5
+
+  const { x, y } = props.axis
+  const menuCount = props.menus.filter(menu => !(menu.divider || menu.hide)).length
+  const dividerCount = props.menus.filter(menu => menu.divider).length
+
+  const menuWidth = MENU_WIDTH
+  const menuHeight = menuCount * MENU_HEIGHT + dividerCount * DIVIDER_HEIGHT + PADDING * 2
+
+  const screenWidth = document.body.clientWidth
+  const screenHeight = document.body.clientHeight
+
+  return {
+    left: screenWidth <= x + menuWidth ? x - menuWidth : x,
+    top: screenHeight <= y + menuHeight ? y - menuHeight : y,
+  }
+})
+
+const handleClickMenuItem = (item: ContextmenuItem) => {
+  if (item.disable) return
+  if (item.children && !item.handler) return
+  if (item.handler) item.handler(props.el)
+  props.removeContextmenu()
+}
+</script>
+
+<style lang="scss">
+.mask {
+  position: fixed;
+  left: 0;
+  top: 0;
+  width: 100vw;
+  height: 100vh;
+  z-index: 9998;
+}
+.contextmenu {
+  position: fixed;
+  z-index: 9999;
+  user-select: none;
+}
+</style>

+ 14 - 0
src/components/Contextmenu2/types.ts

@@ -0,0 +1,14 @@
+export interface ContextmenuItem {
+  text?: string
+  subText?: string
+  divider?: boolean
+  disable?: boolean
+  hide?: boolean
+  children?: ContextmenuItem[]
+  handler?: (el: HTMLElement) => void
+}
+
+export interface Axis {
+  x: number
+  y: number
+}

+ 38 - 0
src/components/TextColorButton2.vue

@@ -0,0 +1,38 @@
+<template>
+  <Button class="text-color-btn">
+    <slot></slot>
+    <div class="text-color-block">
+      <div class="text-color-block-content" :style="{ backgroundColor: color }"></div>
+    </div>
+  </Button>
+</template>
+
+<script lang="ts" setup>
+import Button from './Button.vue'
+
+defineProps<{
+  color: string
+}>()
+</script>
+
+<style lang="scss" scoped>
+.text-color-btn {
+  width: 100%;
+  display: flex !important;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  padding: 0 15px;
+}
+.text-color-block {
+  width: 17px;
+  height: 4px;
+  margin-top: 1px;
+  background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAAXNSR0IArs4c6QAAACdJREFUGFdjfPbs2X8GBgYGSUlJEMXAiCHw//9/sIrnz59DVKALAADNxxVfaiODNQAAAABJRU5ErkJggg==);
+
+  .text-color-block-content {
+    width: 100%;
+    height: 100%;
+  }
+}
+</style>

+ 64 - 0
src/plugins/directive/contextmenu2.ts

@@ -0,0 +1,64 @@
+import { type Directive, type DirectiveBinding, createVNode, render } from 'vue'
+import ContextmenuComponent from '@/components/Contextmenu2/index.vue'
+
+const CTX_CONTEXTMENU_HANDLER = 'CTX_CONTEXTMENU_HANDLER'
+
+interface CustomHTMLElement extends HTMLElement {
+  [CTX_CONTEXTMENU_HANDLER]?: (event: MouseEvent) => void
+} 
+
+const contextmenuListener = (el: HTMLElement, event: MouseEvent, binding: DirectiveBinding) => {
+  event.stopPropagation()
+  event.preventDefault()
+
+  const menus = binding.value(el)
+  if (!menus) return
+
+  let container: HTMLDivElement | null = null
+
+  // 移除右键菜单并取消相关的事件监听
+  const removeContextmenu = () => {
+    if (container) {
+      document.body.removeChild(container)
+      container = null
+    }
+    el.classList.remove('contextmenu-active')
+    document.body.removeEventListener('scroll', removeContextmenu)  
+    window.removeEventListener('resize', removeContextmenu)
+  }
+
+  // 创建自定义菜单
+  const options = {
+    axis: { x: event.x, y: event.y },
+    el,
+    menus,
+    removeContextmenu,
+  }
+  container = document.createElement('div')
+  const vm = createVNode(ContextmenuComponent, options, null)
+  render(vm, container)
+  document.body.appendChild(container)
+
+  // 为目标节点添加菜单激活状态的className
+  el.classList.add('contextmenu-active')
+
+  // 页面变化时移除菜单
+  document.body.addEventListener('scroll', removeContextmenu)
+  window.addEventListener('resize', removeContextmenu)
+}
+
+const ContextmenuDirective: Directive = {
+  mounted(el: CustomHTMLElement, binding) {
+    el[CTX_CONTEXTMENU_HANDLER] = (event: MouseEvent) => contextmenuListener(el, event, binding)
+    el.addEventListener('contextmenu', el[CTX_CONTEXTMENU_HANDLER])
+  },
+
+  unmounted(el: CustomHTMLElement) {
+    if (el && el[CTX_CONTEXTMENU_HANDLER]) {
+      el.removeEventListener('contextmenu', el[CTX_CONTEXTMENU_HANDLER])
+      delete el[CTX_CONTEXTMENU_HANDLER]
+    }
+  },
+}
+
+export default ContextmenuDirective

+ 2 - 0
src/plugins/directive/index.ts

@@ -1,12 +1,14 @@
 import type { App } from 'vue'
 import type { App } from 'vue'
 
 
 import Contextmenu from './contextmenu'
 import Contextmenu from './contextmenu'
+import Contextmenu2 from './contextmenu2'
 import ClickOutside from './clickOutside'
 import ClickOutside from './clickOutside'
 import Tooltip from './tooltip'
 import Tooltip from './tooltip'
 
 
 export default {
 export default {
   install(app: App) {
   install(app: App) {
     app.directive('contextmenu', Contextmenu)
     app.directive('contextmenu', Contextmenu)
+    app.directive('contextmenu2', Contextmenu2)
     app.directive('click-outside', ClickOutside)
     app.directive('click-outside', ClickOutside)
     app.directive('tooltip', Tooltip)
     app.directive('tooltip', Tooltip)
   }
   }

+ 4 - 3
src/services/speaking.ts

@@ -74,10 +74,11 @@ export async function listSpeakingSessionsByConfig(
   configId: string,
   configId: string,
   userIds: string[],
   userIds: string[],
 ): Promise<ListSessionsByConfigResponse> {
 ): Promise<ListSessionsByConfigResponse> {
-  const params = new URLSearchParams({ configId, userIds: userIds.join(',') })
-  const res = await fetch(`${DIALOGUE_BASE}/sessions/by-config?${params}`, {
-    method: 'GET',
+  const res = await fetch(`${DIALOGUE_BASE}/sessions/by-config`, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
     credentials: 'include',
     credentials: 'include',
+    body: JSON.stringify({ configId, userIds }),
   })
   })
   return parse<ListSessionsByConfigResponse>(res)
   return parse<ListSessionsByConfigResponse>(res)
 }
 }

+ 5 - 4
src/tools/aiChat.ts

@@ -50,7 +50,7 @@ export const chat_no_stream = (msg: string, agentId: string, userId: string, lan
         : language === 'hk'
         : language === 'hk'
           ? 'Traditional Chinese'
           ? 'Traditional Chinese'
           : 'Chinese'
           : 'Chinese'
-      } ${msg} ${language === 'hk' ? '請用繁體中文回复' : language === 'en' ? 'Please reply in English' : '用中文回复'}`,
+      } ${msg} ${language === 'hk' ? '請用繁體中文回复' : language === 'en' ? 'Please reply in English' : '用中文回复'}`,
       uid: uuidv4(),
       uid: uuidv4(),
       stream: false,
       stream: false,
       model: agentData?.modelType || 'open-doubao',
       model: agentData?.modelType || 'open-doubao',
@@ -103,7 +103,8 @@ export const chat_stream = async (
   language: string,
   language: string,
   onMessage: (event: { type: 'message' | 'close' | 'error' | 'messageEnd'; data: string }) => void,
   onMessage: (event: { type: 'message' | 'close' | 'error' | 'messageEnd'; data: string }) => void,
   session_name?: string,
   session_name?: string,
-  file_ids?: Array<string>
+  file_ids?: Array<string>,
+  model?: string
 ): Promise<{ abort: () => void }> => {
 ): Promise<{ abort: () => void }> => {
   const agentData = await getAgentModel(agentId)
   const agentData = await getAgentModel(agentId)
   const params: ChatParams = {
   const params: ChatParams = {
@@ -115,10 +116,10 @@ export const chat_stream = async (
       : language === 'hk'
       : language === 'hk'
         ? 'Traditional Chinese'
         ? 'Traditional Chinese'
         : 'Chinese'
         : 'Chinese'
-    } ${msg}  ${language === 'hk' ? '請用繁體中文回复' : language === 'en' ? 'Please reply in English' : '用中文回复'}`,
+    } ${msg}  ${language === 'hk' ? '請用繁體中文回复' : language === 'en' ? 'Please reply in English' : '用中文回复'}`,
     uid: uuidv4(),
     uid: uuidv4(),
     stream: true,
     stream: true,
-    model: agentData?.modelType || 'open-doubao',
+    model: model || agentData?.modelType || 'open-doubao',
     userId: userId,
     userId: userId,
     tts_language: getTtsLanguage(language),
     tts_language: getTtsLanguage(language),
     session_name: session_name || uuidv4()
     session_name: session_name || uuidv4()

+ 4 - 0
src/utils/confirmDialog.ts

@@ -8,6 +8,8 @@ interface ConfirmDialogOptions {
   confirmText?: string
   confirmText?: string
   cancelText?: string
   cancelText?: string
   width?: number
   width?: number
+  onConfirm?: () => void
+  onCancel?: () => void
 }
 }
 
 
 export function showConfirmDialog(options: ConfirmDialogOptions): Promise<boolean> {
 export function showConfirmDialog(options: ConfirmDialogOptions): Promise<boolean> {
@@ -27,6 +29,7 @@ export function showConfirmDialog(options: ConfirmDialogOptions): Promise<boolea
         visible.value = false
         visible.value = false
         setTimeout(() => {
         setTimeout(() => {
           app.unmount()
           app.unmount()
+          options.onConfirm?.()
           document.body.removeChild(container)
           document.body.removeChild(container)
           resolve(true)
           resolve(true)
         }, 300)
         }, 300)
@@ -35,6 +38,7 @@ export function showConfirmDialog(options: ConfirmDialogOptions): Promise<boolea
         visible.value = false
         visible.value = false
         setTimeout(() => {
         setTimeout(() => {
           app.unmount()
           app.unmount()
+          options.onCancel?.()
           document.body.removeChild(container)
           document.body.removeChild(container)
           resolve(false)
           resolve(false)
         }, 300)
         }, 300)

+ 1 - 0
src/views/Editor/CanvasTool/WebpageInput.vue

@@ -108,6 +108,7 @@ const getTypeLabel = (type: number) => {
     76: lang.ssCreative,
     76: lang.ssCreative,
     77: lang.ssEnglishSpeakingTool,
     77: lang.ssEnglishSpeakingTool,
     78: lang.ssVote,
     78: lang.ssVote,
+    79: lang.ssPhoto,
   }
   }
   return typeMap[type] || lang.ssUnknown
   return typeMap[type] || lang.ssUnknown
 }
 }

+ 182 - 5
src/views/Editor/CanvasTool/index2.vue

@@ -17,13 +17,20 @@
             </svg>
             </svg>
             <span>{{ lang.ssQandA }}</span>
             <span>{{ lang.ssQandA }}</span>
           </div>
           </div>
-          <!-- <div class="popover-item" @click="editContent(78)" v-if="frametype != 78">
+          <div class="popover-item" @click="editContent(78)" v-if="frametype != 78">
             <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
             <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                 <polyline points="9 11 12 14 22 4"></polyline>
                 <polyline points="9 11 12 14 22 4"></polyline>
                 <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
                 <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
             </svg>
             </svg>
             <span>{{ lang.ssVote }}</span>
             <span>{{ lang.ssVote }}</span>
-          </div> -->
+          </div>
+          <div class="popover-item" @click="editContent(79)" v-if="frametype != 79">
+            <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                <path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"></path>
+                <circle cx="12" cy="13" r="4"></circle>
+            </svg>
+            <span>{{ lang.ssPhoto }}</span>
+          </div>
         </template>
         </template>
         <div class="handler-item" :class="{ active: toolVisible }">
         <div class="handler-item" :class="{ active: toolVisible }">
           <span class="svg-icon">
           <span class="svg-icon">
@@ -40,6 +47,10 @@
                 <polyline points="9 11 12 14 22 4"></polyline>
                 <polyline points="9 11 12 14 22 4"></polyline>
                 <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
                 <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
             </svg>
             </svg>
+            <svg v-if="frametype == 79" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                <path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"></path>
+                <circle cx="12" cy="13" r="4"></circle>
+            </svg>
           </span>
           </span>
           <span>{{ iframeLabel }}</span>
           <span>{{ iframeLabel }}</span>
           <svg t="1776672009773" class="xia-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
           <svg t="1776672009773" class="xia-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
@@ -115,6 +126,73 @@
           <span>{{ lang.ssShape }}</span>
           <span>{{ lang.ssShape }}</span>
         </div>
         </div>
       </Popover>
       </Popover>
+      <RichTextBase  v-if="handleElement?.type == 'text'"/>
+      <div style="display: flex; align-items: center;" v-if="handleElement?.type == 'shape'">
+        <Select 
+          style="flex: 1;" 
+          :value="fillType" 
+          @update:value="value => updateFillType(value as 'fill' | 'gradient' | 'pattern')"
+          :options="[
+            { label: lang.ssSolidFill, value: 'fill' },
+            { label: lang.ssGradFill, value: 'gradient' },
+            // { label: lang.ssImgFill, value: 'pattern' },
+          ]"
+        />
+        <Popover trigger="click" v-if="fillType === 'fill'" style="flex: 1;">
+          <template #content>
+            <ColorPicker
+              :modelValue="fill"
+              @update:modelValue="value => updateFill(value)"
+            />
+          </template>
+          <ColorButton style="width: 100px;" :color="fill" />
+        </Popover>
+        <Select 
+          style="flex: 1;" 
+          :value="gradient.type" 
+          @update:value="value => updateGradient({ type: value as GradientType })"
+          v-else-if="fillType === 'gradient'"
+          :options="[
+            { label: lang.ssLinearGrad, value: 'linear' },
+            { label: lang.ssRadialGrad, value: 'radial' },
+          ]"
+        />
+      </div>
+
+      <template v-if="fillType === 'gradient' && handleElement?.type == 'shape'">
+        <div class="row">
+          <GradientBar
+            :value="gradient.colors"
+            :index="currentGradientIndex"
+            @update:value="value => updateGradient({ colors: value })"
+            @update:index="index => currentGradientIndex = index"
+          />
+        </div>
+        <div class="row">
+          <div style="width: 40%;">{{ lang.ssCurColorBlock }}</div>
+          <Popover trigger="click" style="width: 60%;">
+            <template #content>
+              <ColorPicker
+                :modelValue="gradient.colors[currentGradientIndex].color"
+                @update:modelValue="value => updateGradientColors(value)"
+              />
+            </template>
+            <ColorButton :color="gradient.colors[currentGradientIndex].color" />
+          </Popover>
+        </div>
+        <div class="row" v-if="gradient.type === 'linear'">
+          <div style="width: 40%;">{{ lang.ssGradAngle }}</div>
+          <Slider
+            style="width: 60%;"
+            :min="0"
+            :max="360"
+            :step="15"
+            :value="gradient.rotate"
+            @update:value="value => updateGradient({ rotate: value as number })" 
+          />
+        </div>
+      </template>
+      <ElementFlip  v-if="handleElement?.type == 'shape'"/>
 
 
       <!-- 英语口语工具:重置预览 -->
       <!-- 英语口语工具:重置预览 -->
       <div
       <div
@@ -155,7 +233,7 @@
 </template>
 </template>
 
 
 <script lang="ts" setup>
 <script lang="ts" setup>
-import { ref, computed } from 'vue'
+import { ref, computed, watch } from 'vue'
 import { storeToRefs } from 'pinia'
 import { storeToRefs } from 'pinia'
 import { useMainStore, useSnapshotStore, useSlidesStore } from '@/store'
 import { useMainStore, useSnapshotStore, useSlidesStore } from '@/store'
 import { useSpeakingStore } from '@/store/speaking'
 import { useSpeakingStore } from '@/store/speaking'
@@ -182,6 +260,14 @@ import Popover from '@/components/Popover.vue'
 import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
 import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
 import Button from '@/components/Button.vue'
 import Button from '@/components/Button.vue'
 
 
+import RichTextBase from '@/views/Editor/Toolbar/common/RichTextBase2.vue'
+import Select from '@/components/Select.vue'
+import ColorPicker from '@/components/ColorPicker/index.vue'
+import GradientBar from '@/components/GradientBar.vue'
+import Slider from '@/components/Slider.vue'
+import ColorButton from '@/components/ColorButton.vue'
+import ElementFlip from '@/views/Editor/Toolbar/common/ElementFlip2.vue'
+
 const mainStore = useMainStore()
 const mainStore = useMainStore()
 const slidesStore = useSlidesStore()
 const slidesStore = useSlidesStore()
 const speakingStore = useSpeakingStore()
 const speakingStore = useSpeakingStore()
@@ -222,7 +308,7 @@ const viewMode = computed(() => getInitialViewMode())
 
 
 const hasInteractiveTool = computed(() => {
 const hasInteractiveTool = computed(() => {
   const elements = currentSlide.value?.elements || []
   const elements = currentSlide.value?.elements || []
-  return elements.some((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15 || el.toolType === 78))
+  return elements.some((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15 || el.toolType === 78 || el.toolType === 79))
 })
 })
 
 
 const iframeLabel = computed(() => {
 const iframeLabel = computed(() => {
@@ -246,6 +332,7 @@ const getTypeLabel = (type: number) => {
     76: lang.ssCreative,
     76: lang.ssCreative,
     77: lang.ssEnglishSpeakingTool,
     77: lang.ssEnglishSpeakingTool,
     78: lang.ssVote,
     78: lang.ssVote,
+    79: lang.ssPhoto,
   }
   }
   return typeMap[type] || lang.ssUnknown
   return typeMap[type] || lang.ssUnknown
 }
 }
@@ -441,12 +528,95 @@ const editContent = (toolType: number) => {
     }
     }
     const parentWindow = window.parent as ParentWindowWithToolList
     const parentWindow = window.parent as ParentWindowWithToolList
     const elements = currentSlide.value?.elements || []
     const elements = currentSlide.value?.elements || []
-    const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15 || el.toolType === 78))
+    const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15 || el.toolType === 78 || el.toolType === 79))
     parentWindow?.toolBtn2?.(0, frameElement?.url || '', toolType)
     parentWindow?.toolBtn2?.(0, frameElement?.url || '', toolType)
     toolVisible.value = false
     toolVisible.value = false
   })
   })
 }
 }
 
 
+import type { GradientType, PPTShapeElement, Gradient, ShapeText } from '@/types/slides'
+const { handleElement, handleElementId } = storeToRefs(useMainStore())
+const fillType = ref('fill')
+const fill = ref<string>('#000')
+const textAlign = ref('middle')
+const pattern = ref('')
+import emitter, { EmitterEvents } from '@/utils/emitter'
+const { addHistorySnapshot } = useHistorySnapshot()
+const currentGradientIndex = ref(0)
+watch(handleElementId, () => {
+  currentGradientIndex.value = 0
+})
+
+const updateElement = (props: Partial<PPTShapeElement>) => {
+  slidesStore.updateElement({ id: handleElementId.value, props })
+  addHistorySnapshot()
+}
+
+const gradient = ref<Gradient>({
+  type: 'linear', 
+  rotate: 0,
+  colors: [
+    { pos: 0, color: '#fff' },
+    { pos: 100, color: '#fff' },
+  ],
+})
+
+watch(handleElement, () => {
+  if (!handleElement.value || handleElement.value.type !== 'shape') return
+
+  fill.value = handleElement.value.fill || '#fff'
+  const defaultGradientColor = [
+    { pos: 0, color: fill.value },
+    { pos: 100, color: '#fff' },
+  ]
+  gradient.value = handleElement.value.gradient || { type: 'linear', rotate: 0, colors: defaultGradientColor }
+  pattern.value = handleElement.value.pattern || ''
+  fillType.value = (handleElement.value.pattern !== undefined) ? 'pattern' : (handleElement.value.gradient ? 'gradient' : 'fill')
+  textAlign.value = handleElement.value?.text?.align || 'middle'
+
+  if (handleElement.value.text?.content) {
+    emitter.emit(EmitterEvents.SYNC_RICH_TEXT_ATTRS_TO_STORE)
+  }
+}, { deep: true, immediate: true })
+
+// 设置填充类型:渐变、纯色
+const updateFillType = (type: 'gradient' | 'fill' | 'pattern') => {
+  console.log('设置填充类型:', type)
+  if (type === 'fill') {
+    slidesStore.removeElementProps({ id: handleElementId.value, propName: ['gradient', 'pattern'] })
+    addHistorySnapshot()
+  }
+  else if (type === 'gradient') {
+    currentGradientIndex.value = 0
+    slidesStore.removeElementProps({ id: handleElementId.value, propName: 'pattern' })
+    updateElement({ gradient: gradient.value })
+  }
+  // else if (type === 'pattern') {
+  //   slidesStore.removeElementProps({ id: handleElementId.value, propName: 'gradient' })
+  //   updateElement({ pattern: '' })
+  // }
+}
+
+// 设置填充色
+const updateFill = (value: string) => {
+  updateElement({ fill: value })
+}
+
+// 设置渐变填充
+const updateGradient = (gradientProps: Partial<Gradient>) => {
+  if (!gradient.value) return
+  const _gradient = { ...gradient.value, ...gradientProps }
+  updateElement({ gradient: _gradient })
+}
+const updateGradientColors = (color: string) => {
+  const colors = gradient.value.colors.map((item, index) => {
+    if (index === currentGradientIndex.value) return { ...item, color }
+    return item
+  })
+  updateGradient({ colors })
+}
+
+
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
@@ -683,4 +853,11 @@ const editContent = (toolType: number) => {
     }
     }
   }
   }
 }
 }
+
+.row {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  width: 180px;
+}
 </style>
 </style>

+ 189 - 44
src/views/Editor/Thumbnails/index2.vue

@@ -5,7 +5,7 @@
     v-click-outside="() => setThumbnailsFocus(false)"
     v-click-outside="() => setThumbnailsFocus(false)"
     v-contextmenu="contextmenusThumbnails"
     v-contextmenu="contextmenusThumbnails"
   >
   >
-    <div class="add-slide">
+    <div class="add-slide" v-show="false">
       <div class="btn" @click="createSlide()"><IconPlus class="icon" /></div>
       <div class="btn" @click="createSlide()"><IconPlus class="icon" /></div>
       <!-- 添加幻灯片 <Popover trigger="click" placement="bottom-start" v-model:value="presetLayoutPopoverVisible" center>
       <!-- 添加幻灯片 <Popover trigger="click" placement="bottom-start" v-model:value="presetLayoutPopoverVisible" center>
         <template #content>
         <template #content>
@@ -56,14 +56,22 @@
               'selected': selectedSlidesIndex.includes(index),
               'selected': selectedSlidesIndex.includes(index),
             }"
             }"
             @mousedown="$event => handleClickSlideThumbnail($event, index)"
             @mousedown="$event => handleClickSlideThumbnail($event, index)"
-            @dblclick="enterScreening()"
-            v-contextmenu="contextmenusThumbnailItem"
+            v-contextmenu2="contextmenusThumbnailItem"
           >
           >
+            <!-- @dblclick="enterScreening()" -->
             <div class="label" :class="{ 'offset-left': index >= 99 }">{{ fillDigit(index + 1, 2) }}</div>
             <div class="label" :class="{ 'offset-left': index >= 99 }">{{ fillDigit(index + 1, 2) }}</div>
             <ThumbnailSlide class="thumbnail" :slide="element" :size="120" :visible="index < slidesLoadLimit" />
             <ThumbnailSlide class="thumbnail" :slide="element" :size="120" :visible="index < slidesLoadLimit" />
+            <div class="page_menu" @click.stop="$event => showPageMenu($event, index)">
+              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                  <circle cx="12" cy="12" r="1"></circle>
+                  <circle cx="12" cy="5" r="1"></circle>
+                  <circle cx="12" cy="19" r="1"></circle>
+              </svg>
+            </div>
 
 
             <div class="note-flag" v-if="element.notes && element.notes.length" @click="openNotesPanel()">{{ element.notes.length }}</div>
             <div class="note-flag" v-if="element.notes && element.notes.length" @click="openNotesPanel()">{{ element.notes.length }}</div>
           </div>
           </div>
+          <div class="add-page-between">+</div>
         </div>
         </div>
       </template>
       </template>
     </Draggable>
     </Draggable>
@@ -73,7 +81,7 @@
 </template>
 </template>
 
 
 <script lang="ts" setup>
 <script lang="ts" setup>
-import { computed, nextTick, ref, watch, useTemplateRef } from 'vue'
+import { computed, nextTick, ref, watch, useTemplateRef, createVNode, render } from 'vue'
 import { storeToRefs } from 'pinia'
 import { storeToRefs } from 'pinia'
 import { useMainStore, useSlidesStore, useKeyboardStore } from '@/store'
 import { useMainStore, useSlidesStore, useKeyboardStore } from '@/store'
 import { fillDigit } from '@/utils/common'
 import { fillDigit } from '@/utils/common'
@@ -90,6 +98,7 @@ import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
 import Templates from './Templates.vue'
 import Templates from './Templates.vue'
 import Popover from '@/components/Popover.vue'
 import Popover from '@/components/Popover.vue'
 import Draggable from 'vuedraggable'
 import Draggable from 'vuedraggable'
+import ContextmenuComponent from '@/components/Contextmenu2/index.vue'
 
 
 // 检测是否为移动设备(包括iPad和手机)
 // 检测是否为移动设备(包括iPad和手机)
 const isMobileDevice = computed(() => {
 const isMobileDevice = computed(() => {
@@ -315,54 +324,117 @@ const contextmenusThumbnails = (): ContextmenuItem[] => {
   ]
   ]
 }
 }
 
 
-const contextmenusThumbnailItem = (): ContextmenuItem[] => {
-  return [
-    {
-      text: lang.ssCut,
-      subText: 'Ctrl + X',
-      handler: cutSlide,
-    },
-    {
-      text: lang.ssCopy,
-      subText: 'Ctrl + C',
-      handler: copySlide,
-    },
-    {
-      text: lang.ssPaste,
-      subText: 'Ctrl + V',
-      handler: pasteSlide,
-    },
-    {
-      text: lang.ssSelectAll,
-      subText: 'Ctrl + A',
-      handler: selectAllSlide,
+const showPageMenu = (event: MouseEvent, index: number) => {
+  event.stopPropagation()
+  
+  const menus = contextmenusThumbnailItem()
+  if (!menus) return
+
+  let container: HTMLDivElement | null = null
+
+  const removeContextmenu = () => {
+    if (container) {
+      document.body.removeChild(container)
+      container = null
+    }
+    document.body.removeEventListener('scroll', removeContextmenu)
+    window.removeEventListener('resize', removeContextmenu)
+  }
+
+  const options = {
+    axis: { x: event.clientX - 20, y: event.clientY - 20 },
+    el: event.currentTarget as HTMLElement,
+    menus,
+    removeContextmenu,
+  }
+  container = document.createElement('div')
+  const vm = createVNode(ContextmenuComponent, options, null)
+  render(vm, container)
+  document.body.appendChild(container)
+
+  document.body.addEventListener('scroll', removeContextmenu)
+  window.addEventListener('resize', removeContextmenu)
+}
+import { showConfirmDialog } from '@/utils/confirmDialog'
+
+const confirmDeleteSlide = () => {
+  showConfirmDialog({
+    title: lang.ssConfirmDel,
+    content: lang.ssConfirmDelContent,
+    confirmText: lang.ssConfirm,
+    cancelText: lang.ssCancel,
+    width: 400,
+    onConfirm: () => {
+      deleteSlide()
     },
     },
-    { divider: true },
-    {
-      text: lang.ssNewPage,
-      subText: 'Enter',
-      handler: createSlide,
+    onCancel: () => {
+      console.log('取消删除')
     },
     },
+  })
+
+}
+
+const contextmenusThumbnailItem = (): ContextmenuItem[] => {
+  return [
+    // {
+    //   text: lang.ssCut,
+    //   subText: 'Ctrl + X',
+    //   handler: cutSlide,
+    // },
+    // {
+    //   text: lang.ssCopy,
+    //   subText: 'Ctrl + C',
+    //   handler: copySlide,
+    // },
+    // {
+    //   text: lang.ssPaste,
+    //   subText: 'Ctrl + V',
+    //   handler: pasteSlide,
+    // },
+    // {
+    //   text: lang.ssSelectAll,
+    //   subText: 'Ctrl + A',
+    //   handler: selectAllSlide,
+    // },
+    // { divider: true },
+    // {
+    //   text: lang.ssNewPage,
+    //   subText: 'Enter',
+    //   handler: createSlide,
+    // },
+    // {
+    //   text: lang.ssDupPage,
+    //   subText: 'Ctrl + D',
+    //   handler: copyAndPasteSlide,
+    // },
+    // {
+    //   text: lang.ssDelPage,
+    //   subText: 'Delete',
+    //   handler: () => deleteSlide(),
+    // },
+    // {
+    //   text: lang.ssAddSect,
+    //   handler: createSection,
+    //   disable: !!currentSlide.value.sectionTag,
+    // },
+    // { divider: true },
+    // {
+    //   text: lang.ssPlayFromCur,
+    //   subText: 'Shift + F5',
+    //   handler: enterScreening,
+    // },
+
     {
     {
-      text: lang.ssDupPage,
-      subText: 'Ctrl + D',
+      text: lang.ssDupPage2,
       handler: copyAndPasteSlide,
       handler: copyAndPasteSlide,
     },
     },
     {
     {
-      text: lang.ssDelPage,
-      subText: 'Delete',
-      handler: () => deleteSlide(),
-    },
-    {
-      text: lang.ssAddSect,
-      handler: createSection,
-      disable: !!currentSlide.value.sectionTag,
+      text: lang.ssNewPage2,
+      handler: createSlide,
     },
     },
-    { divider: true },
     {
     {
-      text: lang.ssPlayFromCur,
-      subText: 'Shift + F5',
-      handler: enterScreening,
+      text: lang.ssDelPage2,
+      handler: confirmDeleteSlide,
     },
     },
   ]
   ]
 }
 }
@@ -425,6 +497,18 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   gap: 10px;
   gap: 10px;
+
+  .thumbnail-container {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+
+    &:hover {
+      .add-page-between {
+        display: flex;
+      }
+    }
+  }
 }
 }
 .thumbnail-item {
 .thumbnail-item {
   display: flex;
   display: flex;
@@ -490,6 +574,10 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
   color: #999;
   color: #999;
   width: 20px;
   width: 20px;
   cursor: grab;
   cursor: grab;
+  position: absolute;
+  z-index: 999;
+  top: 10px;
+  left: 10px;
 
 
   &.offset-left {
   &.offset-left {
     position: relative;
     position: relative;
@@ -500,6 +588,39 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
     cursor: grabbing;
     cursor: grabbing;
   }
   }
 }
 }
+
+.page_menu{
+  width: 24px;
+  height: 24px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  cursor: pointer;
+  position: absolute;
+  z-index: 999;
+  bottom: 10px;
+  right: 10px;
+  background: rgba(255, 255, 255, 0.95);
+  border: 1px solid #e5e7eb;
+  transition: .3s;
+  border-radius: 5px;
+
+  svg{
+    color: #6b7280;
+    height: 15px;
+  }
+
+  .active,
+  &:hover{
+    border-color: $themeColor2;
+    background-color: $themeColor2;
+
+    svg{
+      color: #fff;
+    }
+  }
+}
+
 .page-number {
 .page-number {
   /* height: 100%; */
   /* height: 100%; */
   font-size: 12px;
   font-size: 12px;
@@ -557,4 +678,28 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
     font-size: 12px;
     font-size: 12px;
   }
   }
 }
 }
+
+.add-page-between {
+    width: 30px;
+    height: 30px;
+    border-radius: 50%;
+    border: 2px dashed #d1d5db;
+    background: white;
+    cursor: pointer;
+    display: none;
+    // align-items: center;
+    justify-content: center;
+    color: #9ca3af;
+    font-size: 20px;
+    transition: all 0.2s;
+    flex-shrink: 0;
+    line-height: 23px;
+
+    &:hover {
+      border-color: #F78B22;
+      border-style: solid;
+      color: #F78B22;
+      background: #FFF8F0;
+    }
+}
 </style>
 </style>

+ 1 - 0
src/views/Editor/Toolbar/ElementStylePanel/index.vue

@@ -37,6 +37,7 @@ const panelMap = {
 const { handleElement } = storeToRefs(useMainStore())
 const { handleElement } = storeToRefs(useMainStore())
 
 
 const currentPanelComponent = computed<unknown>(() => {
 const currentPanelComponent = computed<unknown>(() => {
+  console.log('handleElement.value', handleElement.value)
   return handleElement.value ? (panelMap[handleElement.value.type] || null) : null
   return handleElement.value ? (panelMap[handleElement.value.type] || null) : null
 })
 })
 </script>
 </script>

+ 59 - 0
src/views/Editor/Toolbar/common/ElementFlip2.vue

@@ -0,0 +1,59 @@
+<template>
+  <div class="element-flip">
+    <ButtonGroup class="row">
+      <CheckboxButton 
+        style="flex: 1;"
+        :checked="flipV"
+        @click="updateFlip({ flipV: !flipV })"
+      ><IconFlipVertically /> {{lang.flipVertically}}</CheckboxButton>
+      <CheckboxButton 
+        style="flex: 1;"
+        :checked="flipH"
+        @click="updateFlip({ flipH: !flipH })"
+      ><IconFlipHorizontally /> {{lang.flipHorizontally}}</CheckboxButton>
+    </ButtonGroup>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch } from 'vue'
+import { storeToRefs } from 'pinia'
+import { useMainStore, useSlidesStore } from '@/store'
+import type { ImageOrShapeFlip } from '@/types/slides'
+import useHistorySnapshot from '@/hooks/useHistorySnapshot'
+
+import CheckboxButton from '@/components/CheckboxButton.vue'
+import ButtonGroup from '@/components/ButtonGroup.vue'
+
+import { lang } from '@/main'
+
+const slidesStore = useSlidesStore()
+const { handleElement } = storeToRefs(useMainStore())
+
+const flipH = ref(false)
+const flipV = ref(false)
+
+watch(handleElement, () => {
+  if (handleElement.value && (handleElement.value.type === 'image' || handleElement.value.type === 'shape')) {
+    flipH.value = !!handleElement.value.flipH
+    flipV.value = !!handleElement.value.flipV
+  }
+}, { deep: true, immediate: true })
+
+const { addHistorySnapshot } = useHistorySnapshot()
+
+const updateFlip = (flipProps: ImageOrShapeFlip) => {
+  if (!handleElement.value) return
+  slidesStore.updateElement({ id: handleElement.value.id, props: flipProps })
+  addHistorySnapshot()
+}
+</script>
+
+<style lang="scss" scoped>
+.row {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  // margin-bottom: 10px;
+}
+</style>

+ 476 - 0
src/views/Editor/Toolbar/common/RichTextBase2.vue

@@ -0,0 +1,476 @@
+<template>
+  <div class="rich-text-base">
+    <SelectGroup class="row">
+      <Select
+        style="width: 60%;"
+        :value="richTextAttrs.fontname"
+        search
+        :searchLabel="lang.searchFont"
+        @update:value="value => emitRichTextCommand('fontname', value as string)"
+        :options="FONTS"
+      >
+        <template #icon>
+          <IconFontSize />
+        </template>
+      </Select>
+      <Select
+        style="width: 40%;"
+        :value="richTextAttrs.fontsize"
+        search
+        :searchLabel="lang.searchFontSize"
+        @update:value="value => emitRichTextCommand('fontsize', value as string)"
+        :options="fontSizeOptions.map(item => ({
+          label: item, value: item
+        }))"
+      >
+        <template #icon>
+          <IconAddText />
+        </template>
+      </Select>
+    </SelectGroup>
+
+    <ButtonGroup class="row" passive>
+      <Popover trigger="click" style="width: 30%;">
+        <template #content>
+          <ColorPicker
+            :modelValue="richTextAttrs.color"
+            @update:modelValue="value => emitRichTextCommand('color', value)"
+          />
+        </template>
+        <TextColorButton first v-tooltip="lang.textColor" :color="richTextAttrs.color">
+          <IconText />
+        </TextColorButton>
+      </Popover>
+      <Popover trigger="click" style="width: 30%;">
+        <template #content>
+          <ColorPicker
+            :modelValue="richTextAttrs.backcolor"
+            @update:modelValue="value => emitRichTextCommand('backcolor', value)"
+          />
+        </template>
+        <TextColorButton v-tooltip="lang.highlight" :color="richTextAttrs.backcolor">
+          <IconHighLight />
+        </TextColorButton>
+      </Popover>
+      <Button 
+        style="display: flex;align-items: center;"
+        class="font-size-btn"
+        v-tooltip="lang.fontSizeAdd"
+        @click="emitRichTextCommand('fontsize-add')"
+      ><IconFontSize />+</Button>
+      <Button
+        style="display: flex;align-items: center;"
+        last
+        class="font-size-btn"
+        v-tooltip="lang.fontSizeReduce" 
+        @click="emitRichTextCommand('fontsize-reduce')"
+      ><IconFontSize />-</Button>
+    </ButtonGroup>
+
+    <ButtonGroup class="row">
+      <CheckboxButton 
+        style="flex: 1;"
+        :checked="richTextAttrs.bold"
+        v-tooltip="lang.bold"
+        @click="emitRichTextCommand('bold')"
+      ><IconTextBold /></CheckboxButton>
+      <CheckboxButton 
+        style="flex: 1;"
+        :checked="richTextAttrs.em"
+        v-tooltip="lang.italic"
+        @click="emitRichTextCommand('em')"
+      ><IconTextItalic /></CheckboxButton>
+      <CheckboxButton 
+        style="flex: 1;"
+        :checked="richTextAttrs.underline"
+        v-tooltip="lang.underline"
+        @click="emitRichTextCommand('underline')"
+      ><IconTextUnderline /></CheckboxButton>
+      <CheckboxButton 
+        style="flex: 1;"
+        :checked="richTextAttrs.strikethrough"
+        v-tooltip="lang.strikethrough"
+        @click="emitRichTextCommand('strikethrough')"
+      ><IconStrikethrough /></CheckboxButton>
+    </ButtonGroup>
+
+    <!-- <ButtonGroup class="row">
+      <CheckboxButton
+        style="flex: 1;"
+        :checked="richTextAttrs.superscript"
+        v-tooltip="'上标'"
+        @click="emitRichTextCommand('superscript')"
+      >A²</CheckboxButton>
+      <CheckboxButton
+        style="flex: 1;"
+        :checked="richTextAttrs.subscript"
+        v-tooltip="'下标'"
+        @click="emitRichTextCommand('subscript')"
+      >A₂</CheckboxButton>
+      <CheckboxButton
+        style="flex: 1;"
+        :checked="richTextAttrs.code"
+        v-tooltip="'行内代码'"
+        @click="emitRichTextCommand('code')"
+      ><IconCode /></CheckboxButton>
+      <CheckboxButton
+        style="flex: 1;"
+        :checked="richTextAttrs.blockquote"
+        v-tooltip="'引用'"
+        @click="emitRichTextCommand('blockquote')"
+      ><IconQuote /></CheckboxButton>
+    </ButtonGroup>
+
+    <ButtonGroup class="row" passive>
+      <Popover trigger="click" v-model:value="AIPopoverVisible" style="width: 25%;">
+        <template #content>
+          <PopoverMenuItem center @click="execAI('美化改写')">美化</PopoverMenuItem>
+          <PopoverMenuItem center @click="execAI('扩写丰富')">扩写</PopoverMenuItem>
+          <PopoverMenuItem center @click="execAI('精简提炼')">精简</PopoverMenuItem>
+        </template>
+        <CheckboxButton
+          first
+          style="width: 100%;"
+          v-tooltip="'AI辅助'"
+        ><span :class="{ 'ai-loading': isAIWriting }">{{ isAIWriting ? '' : 'AI' }}</span></CheckboxButton>
+      </Popover>
+      <CheckboxButton
+        style="flex: 1;"
+        v-tooltip="'清除格式'"
+        @click="emitRichTextCommand('clear')"
+      ><IconFormat /></CheckboxButton>
+      <CheckboxButton
+        style="flex: 1;"
+        :checked="!!textFormatPainter"
+        v-tooltip="'格式刷(双击连续使用)'"
+        @click="toggleTextFormatPainter()"
+        @dblclick="toggleTextFormatPainter(true)"
+      ><IconFormatBrush /></CheckboxButton>
+      <Popover placement="bottom-end" trigger="click" v-model:value="linkPopoverVisible" style="width: 25%;">
+        <template #content>
+          <div class="link-popover">
+            <Input v-model:value="link" placeholder="请输入超链接" />
+            <div class="btns">
+              <Button size="small" :disabled="!richTextAttrs.link" @click="removeLink()" style="margin-right: 5px;">移除</Button>
+              <Button size="small" type="primary" @click="updateLink(link)">确认</Button>
+            </div>
+          </div>
+        </template>
+        <CheckboxButton
+          last
+          style="width: 100%;"
+          :checked="!!richTextAttrs.link"
+          v-tooltip="'超链接'"
+          @click="openLinkPopover()"
+        ><IconLinkOne /></CheckboxButton>
+      </Popover>
+    </ButtonGroup>
+    <Divider />
+
+    <RadioGroup 
+      class="row" 
+      button-style="solid" 
+      :value="richTextAttrs.align"
+      @update:value="value => emitRichTextCommand('align', value)"
+    >
+      <RadioButton value="left" v-tooltip="'左对齐'" style="flex: 1;"><IconAlignTextLeft /></RadioButton>
+      <RadioButton value="center" v-tooltip="'居中'" style="flex: 1;"><IconAlignTextCenter /></RadioButton>
+      <RadioButton value="right" v-tooltip="'右对齐'" style="flex: 1;"><IconAlignTextRight /></RadioButton>
+      <RadioButton value="justify" v-tooltip="'两端对齐'" style="flex: 1;"><IconAlignTextBoth /></RadioButton>
+    </RadioGroup>
+
+    <div class="row" passive>
+      <ButtonGroup style="flex: 1;">
+        <Button
+          first
+          :type="richTextAttrs.bulletList ? 'primary' : 'default'"
+          style="flex: 1;"
+          v-tooltip="'项目符号'"
+          @click="emitRichTextCommand('bulletList')"
+        ><IconList /></Button>
+        <Popover trigger="click" v-model:value="bulletListPanelVisible">
+          <template #content>
+            <div class="list-wrap">
+              <ul class="list" 
+                v-for="item in bulletListStyleTypeOption" 
+                :key="item" 
+                :style="{ listStyleType: item }"
+                @click="emitRichTextCommand('bulletList', item)"
+              >
+                <li class="list-item" v-for="key in 3" :key="key"><span></span></li>
+              </ul>
+            </div>
+          </template>
+          <Button last class="popover-btn"><IconDown /></Button>
+        </Popover>
+      </ButtonGroup>
+      <div style="width: 10px;"></div>
+      <ButtonGroup style="flex: 1;" passive>
+        <Button
+          first
+          :type="richTextAttrs.orderedList ? 'primary' : 'default'"
+          style="flex: 1;"
+          v-tooltip="'编号'"
+          @click="emitRichTextCommand('orderedList')"
+        ><IconOrderedList /></Button>
+        <Popover trigger="click" v-model:value="orderedListPanelVisible">
+          <template #content>
+            <div class="list-wrap">
+              <ul class="list" 
+                v-for="item in orderedListStyleTypeOption" 
+                :key="item" 
+                :style="{ listStyleType: item }"
+                @click="emitRichTextCommand('orderedList', item)"
+              >
+                <li class="list-item" v-for="key in 3" :key="key"><span></span></li>
+              </ul>
+            </div>
+          </template>
+          <Button last class="popover-btn"><IconDown /></Button>
+        </Popover>
+      </ButtonGroup>
+    </div>
+
+    <div class="row">
+      <ButtonGroup style="flex: 1;" passive>
+        <Button first style="flex: 1;" v-tooltip="'减小段落缩进'" @click="emitRichTextCommand('indent', '-1')"><IconIndentLeft /></Button>
+        <Popover trigger="click" v-model:value="indentLeftPanelVisible">
+          <template #content>
+            <PopoverMenuItem center @click="emitRichTextCommand('textIndent', '-1')">减小首行缩进</PopoverMenuItem>
+          </template>
+          <Button last class="popover-btn"><IconDown /></Button>
+        </Popover>
+      </ButtonGroup>
+      <div style="width: 10px;"></div>
+      <ButtonGroup style="flex: 1;" passive>
+        <Button first style="flex: 1;" v-tooltip="'增大段落缩进'" @click="emitRichTextCommand('indent', '+1')"><IconIndentRight /></Button>
+        <Popover trigger="click" v-model:value="indentRightPanelVisible">
+          <template #content>
+            <PopoverMenuItem center @click="emitRichTextCommand('textIndent', '+1')">增大首行缩进</PopoverMenuItem>
+          </template>
+          <Button last class="popover-btn"><IconDown /></Button>
+        </Popover>
+      </ButtonGroup>
+    </div> -->
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch } from 'vue'
+import { storeToRefs } from 'pinia'
+import api from '@/services'
+import { useMainStore } from '@/store'
+import emitter, { EmitterEvents } from '@/utils/emitter'
+import { FONTS } from '@/configs/font'
+import useTextFormatPainter from '@/hooks/useTextFormatPainter'
+import message from '@/utils/message'
+import { htmlToText } from '@/utils/common'
+
+import TextColorButton from '@/components/TextColorButton2.vue'
+import CheckboxButton from '@/components/CheckboxButton.vue'
+import ColorPicker from '@/components/ColorPicker/index.vue'
+import Input from '@/components/Input.vue'
+import Button from '@/components/Button.vue'
+import ButtonGroup from '@/components/ButtonGroup.vue'
+import Select from '@/components/Select.vue'
+import SelectGroup from '@/components/SelectGroup.vue'
+import Divider from '@/components/Divider.vue'
+import Popover from '@/components/Popover.vue'
+import RadioButton from '@/components/RadioButton.vue'
+import RadioGroup from '@/components/RadioGroup.vue'
+import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
+
+import { lang } from '@/main'
+
+
+
+const { handleElement, handleElementId, richTextAttrs, textFormatPainter } = storeToRefs(useMainStore())
+
+const { toggleTextFormatPainter } = useTextFormatPainter()
+
+const fontSizeOptions = [
+  '12px', '14px', '16px', '18px', '20px', '22px', '24px', '28px', '32px',
+  '36px', '40px', '44px', '48px', '54px', '60px', '66px', '72px', '76px',
+  '80px', '88px', '96px', '104px', '112px', '120px',
+]
+
+const emitRichTextCommand = (command: string, value?: string) => {
+  emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action: { command, value } })
+}
+
+const bulletListPanelVisible = ref(false)
+const orderedListPanelVisible = ref(false)
+const indentLeftPanelVisible = ref(false)
+const indentRightPanelVisible = ref(false)
+
+const bulletListStyleTypeOption = ref(['disc', 'circle', 'square'])
+const orderedListStyleTypeOption = ref(['decimal', 'lower-roman', 'upper-roman', 'lower-alpha', 'upper-alpha', 'lower-greek'])
+
+const link = ref('')
+const linkPopoverVisible = ref(false)
+const AIPopoverVisible = ref(false)
+const isAIWriting = ref(false)
+
+watch(richTextAttrs, () => linkPopoverVisible.value = false)
+watch(handleElementId, () => {
+  if (isAIWriting.value) isAIWriting.value = false
+})
+
+const openLinkPopover = () => {
+  link.value = richTextAttrs.value.link
+}
+const updateLink = (link?: string) => {
+  const linkRegExp = /^(https?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/
+  if (!link || !linkRegExp.test(link)) return message.error('不是正确的网页链接地址')
+
+  emitRichTextCommand('link', link)
+  linkPopoverVisible.value = false
+}
+
+const removeLink = () => {
+  emitRichTextCommand('link')
+  linkPopoverVisible.value = false
+}
+
+const execAI = async (command: string) => {
+  AIPopoverVisible.value = false
+
+  if (!handleElement.value) return
+
+  let content = ''
+  if (handleElement.value.type === 'text' && handleElement.value.content) {
+    content = handleElement.value.content
+  }
+  if (handleElement.value.type === 'shape' && handleElement.value.text && handleElement.value.text.content) {
+    content = handleElement.value.text.content
+  }
+
+  if (!content) return message.error('没有可以执行的文本内容')
+
+  let resultText = ''
+
+  const stream = await api.AI_Writing({
+    content: htmlToText(content),
+    command,
+  })
+
+  isAIWriting.value = true
+
+  const reader: ReadableStreamDefaultReader = stream.body.getReader()
+  const decoder = new TextDecoder('utf-8')
+  
+  const readStream = () => {
+    reader.read().then(({ done, value }) => {
+      if (!isAIWriting.value) return
+      if (done) {
+        isAIWriting.value = false
+        return
+      }
+
+      const chunk = decoder.decode(value, { stream: true })
+      resultText += chunk
+      emitRichTextCommand('replace', resultText)
+
+      readStream()
+    })
+  }
+  readStream()
+}
+</script>
+
+<style lang="scss" scoped>
+.rich-text-base {
+  user-select: none;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+
+  ::v-deep(.ai-loading) {
+    width: 16px;
+    height: 16px;
+    display: inline-block;
+    margin-top: 8px;
+    border: 1px solid $themeColor;
+    border-top-color: transparent;
+    border-radius: 50%;
+    animation: spinner .8s linear infinite;
+  }
+}
+.row {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  // margin-bottom: 10px;
+}
+.font-size-btn {
+  padding: 0 15px;
+}
+.link-popover {
+  width: 240px;
+
+  .btns {
+    margin-top: 10px;
+    text-align: right;
+  }
+}
+.list-wrap {
+  width: 176px;
+  color: #666;
+  padding: 8px;
+  margin: -12px;
+  display: flex;
+  flex-wrap: wrap;
+  align-content: flex-start;
+}
+.list {
+  background-color: $lightGray;
+  padding: 4px 4px 4px 20px;
+  cursor: pointer;
+
+  &:not(:nth-child(3n)) {
+    margin-right: 8px;
+  }
+
+  &:nth-child(4),
+  &:nth-child(5),
+  &:nth-child(6) {
+    margin-top: 8px;
+  }
+
+  &:hover {
+    color: $themeColor;
+
+    span {
+      background-color: $themeColor;
+    }
+  }
+}
+.list-item {
+  width: 24px;
+  height: 12px;
+  position: relative;
+  font-size: 12px;
+  top: -3px;
+
+  span {
+    width: 100%;
+    height: 2px;
+    display: inline-block;
+    position: absolute;
+    top: 8px;
+    background-color: #666;
+  }
+}
+.popover-btn {
+  padding: 0 3px;
+}
+
+@keyframes spinner {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
+</style>

+ 1 - 1
src/views/Editor/index3.vue

@@ -222,7 +222,7 @@ import Modal from '@/components/Modal.vue'
 import CollapsibleToolbar from '@/components/CollapsibleToolbar/index2.vue'
 import CollapsibleToolbar from '@/components/CollapsibleToolbar/index2.vue'
 import CreateCourseDialog from '@/components/CreateCourseDialog.vue'
 import CreateCourseDialog from '@/components/CreateCourseDialog.vue'
 import api from '@/services/course'
 import api from '@/services/course'
-import lang from '../lang/cn.json'
+import { lang } from '@/main'
 
 
 
 
 interface ParentWindowWithToolList extends Window {
 interface ParentWindowWithToolList extends Window {

+ 1 - 1
src/views/Student/components/answerTheResult.vue

@@ -405,7 +405,7 @@ const changeShow = (op: any, idx: number) => {
 
 
 const lookDetail = () => {
 const lookDetail = () => {
   console.log(props.toolType)
   console.log(props.toolType)
-  if ([45, 15, 72, 73, 78].includes(props.toolType)) {
+  if ([45, 15, 72, 73, 78, 79].includes(props.toolType)) {
     emit('openChoiceQuestionDetail', props.slideIndex)
     emit('openChoiceQuestionDetail', props.slideIndex)
   }
   }
 }
 }

+ 346 - 60
src/views/Student/components/choiceQuestionDetailDialog.vue

@@ -1,10 +1,10 @@
 <template>
 <template>
   <div class="choiceQuestionDetailDialog">
   <div class="choiceQuestionDetailDialog">
-    <div class="content" :style="{
+    <div class="content" @click="clickContent(true)" :style="{
       width: slideWidth + 'px',
       width: slideWidth + 'px',
       height: slideHeight + 'px',
       height: slideHeight + 'px',
     }">
     }">
-      <span v-show="false" class="closeIcon" @click="closeSlideIndex()">
+      <span v-show="false" class="closeIcon" @click.stop="closeSlideIndex()">
         <img src="../../../assets/img/close.png" />
         <img src="../../../assets/img/close.png" />
       </span>
       </span>
 
 
@@ -20,7 +20,7 @@
                 v-if="props.showData.unsubmittedStudents.length > 0">/{{ props.showData.workArray.length +
                 v-if="props.showData.unsubmittedStudents.length > 0">/{{ props.showData.workArray.length +
                   props.showData.unsubmittedStudents.length
                   props.showData.unsubmittedStudents.length
                 }}</span></div>
                 }}</span></div>
-            <span v-if="props.showData.unsubmittedStudents.length > 0" @click="viewUnsubmittedStudents()">{{
+            <span v-if="props.showData.unsubmittedStudents.length > 0" @click.stop="viewUnsubmittedStudents()">{{
               lang.ssViewSubmitStatus2 }}</span>
               lang.ssViewSubmitStatus2 }}</span>
           </div>
           </div>
           <!--<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 <= 0}" @click="changeWorkIndex(0)">{{ lang.ssPrevQ }}</span>-->
@@ -31,7 +31,7 @@
           " v-if="props.showData.choiceQuestionListData[props.showData.workIndex] &&
           " v-if="props.showData.choiceQuestionListData[props.showData.workIndex] &&
             props.showData.choiceQuestionListData[props.showData.workIndex]
             props.showData.choiceQuestionListData[props.showData.workIndex]
               .timuList.length > 0
               .timuList.length > 0
-          " @click="previewImageToolRef.previewImage(props.showData.choiceQuestionListData[props.showData.workIndex]
+          " @click.stop="previewImageToolRef.previewImage(props.showData.choiceQuestionListData[props.showData.workIndex]
             .timuList[0].src)" />
             .timuList[0].src)" />
         <!-- <span class="c_t45_type" v-if="
         <!-- <span class="c_t45_type" v-if="
           props.showData.choiceQuestionListData[props.showData.workIndex]
           props.showData.choiceQuestionListData[props.showData.workIndex]
@@ -60,7 +60,7 @@
               </svg>{{ lang.ssAnalysis }}
               </svg>{{ lang.ssAnalysis }}
             </div>
             </div>
             <div class="ai_refresh" :class="{ 'disabled': currentAnalysis && currentAnalysis.loading }"
             <div class="ai_refresh" :class="{ 'disabled': currentAnalysis && currentAnalysis.loading }"
-              @click="aiAnalysisRefresh45()">
+              @click.stop="aiAnalysisRefresh45()">
               {{ lang.ssAIGenerate }}
               {{ lang.ssAIGenerate }}
               <svg viewBox="0 0 1024 1024" width="200" height="200">
               <svg viewBox="0 0 1024 1024" width="200" height="200">
                 <path
                 <path
@@ -82,7 +82,7 @@
           </div>
           </div>
         </div>
         </div>
         <div class="cq_changeBtn" v-if="props.showData.choiceQuestionListData.length > 1">
         <div class="cq_changeBtn" v-if="props.showData.choiceQuestionListData.length > 1">
-          <div :class="{ cq_cb_disabled: props.showData.workIndex <= 0 }" @click="changeWorkIndex(0)">
+          <div :class="{ cq_cb_disabled: props.showData.workIndex <= 0 }" @click.stop="changeWorkIndex(0)">
             <svg style="transform: rotate(-90deg);" viewBox="0 0 1024 1024" version="1.1" width="200" height="200">
             <svg style="transform: rotate(-90deg);" viewBox="0 0 1024 1024" version="1.1" width="200" height="200">
               <path
               <path
                 d="M512 330.666667c14.933333 0 29.866667 4.266667 40.533333 14.933333l277.33333399 234.666667c27.733333 23.466667 29.866667 64 8.53333301 89.6-23.466667 27.733333-64 29.866667-89.6 8.53333299L512 477.866667l-236.8 200.53333299c-27.733333 23.466667-68.266667 19.19999999-89.6-8.53333299-23.466667-27.733333-19.19999999-68.266667 8.53333301-89.6l277.33333399-234.666667c10.666667-10.666667 25.6-14.933333 40.533333-14.933333z"
                 d="M512 330.666667c14.933333 0 29.866667 4.266667 40.533333 14.933333l277.33333399 234.666667c27.733333 23.466667 29.866667 64 8.53333301 89.6-23.466667 27.733333-64 29.866667-89.6 8.53333299L512 477.866667l-236.8 200.53333299c-27.733333 23.466667-68.266667 19.19999999-89.6-8.53333299-23.466667-27.733333-19.19999999-68.266667 8.53333301-89.6l277.33333399-234.666667c10.666667-10.666667 25.6-14.933333 40.533333-14.933333z"
@@ -91,7 +91,7 @@
           </div>
           </div>
           <span>{{ props.showData.workIndex + 1 }}/{{ props.showData.choiceQuestionListData.length }}</span>
           <span>{{ props.showData.workIndex + 1 }}/{{ props.showData.choiceQuestionListData.length }}</span>
           <div :class="{ cq_cb_disabled: props.showData.workIndex >= props.showData.choiceQuestionListData.length - 1 }"
           <div :class="{ cq_cb_disabled: props.showData.workIndex >= props.showData.choiceQuestionListData.length - 1 }"
-            @click="changeWorkIndex(1)">
+            @click.stop="changeWorkIndex(1)">
             <svg style="transform: rotate(90deg);" viewBox="0 0 1024 1024" version="1.1" width="200" height="200">
             <svg style="transform: rotate(90deg);" viewBox="0 0 1024 1024" version="1.1" width="200" height="200">
               <path
               <path
                 d="M512 330.666667c14.933333 0 29.866667 4.266667 40.533333 14.933333l277.33333399 234.666667c27.733333 23.466667 29.866667 64 8.53333301 89.6-23.466667 27.733333-64 29.866667-89.6 8.53333299L512 477.866667l-236.8 200.53333299c-27.733333 23.466667-68.266667 19.19999999-89.6-8.53333299-23.466667-27.733333-19.19999999-68.266667 8.53333301-89.6l277.33333399-234.666667c10.666667-10.666667 25.6-14.933333 40.533333-14.933333z"
                 d="M512 330.666667c14.933333 0 29.866667 4.266667 40.533333 14.933333l277.33333399 234.666667c27.733333 23.466667 29.866667 64 8.53333301 89.6-23.466667 27.733333-64 29.866667-89.6 8.53333299L512 477.866667l-236.8 200.53333299c-27.733333 23.466667-68.266667 19.19999999-89.6-8.53333299-23.466667-27.733333-19.19999999-68.266667 8.53333301-89.6l277.33333399-234.666667c10.666667-10.666667 25.6-14.933333 40.533333-14.933333z"
@@ -113,7 +113,7 @@
                 v-if="props.showData.unsubmittedStudents.length > 0">/{{ props.showData.workArray.length +
                 v-if="props.showData.unsubmittedStudents.length > 0">/{{ props.showData.workArray.length +
                   props.showData.unsubmittedStudents.length
                   props.showData.unsubmittedStudents.length
                 }}</span></div>
                 }}</span></div>
-            <span v-if="props.showData.unsubmittedStudents.length > 0" @click="viewUnsubmittedStudents()">{{
+            <span v-if="props.showData.unsubmittedStudents.length > 0" @click.stop="viewUnsubmittedStudents()">{{
               lang.ssViewSubmitStatus2 }}</span>
               lang.ssViewSubmitStatus2 }}</span>
           </div>
           </div>
           <!--<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 <= 0}" @click="changeWorkIndex(0)">{{ lang.ssPrevQ }}</span>-->
@@ -124,7 +124,7 @@
           " v-if="props.showData.choiceQuestionListData[props.showData.workIndex] &&
           " v-if="props.showData.choiceQuestionListData[props.showData.workIndex] &&
             props.showData.choiceQuestionListData[props.showData.workIndex]
             props.showData.choiceQuestionListData[props.showData.workIndex]
               .timuList.length > 0
               .timuList.length > 0
-          " @click="previewImageToolRef.previewImage(props.showData.choiceQuestionListData[props.showData.workIndex]
+          " @click.stop="previewImageToolRef.previewImage(props.showData.choiceQuestionListData[props.showData.workIndex]
             .timuList[0].src)" />
             .timuList[0].src)" />
         <!-- <span class="c_t45_type" v-if="
         <!-- <span class="c_t45_type" v-if="
           props.showData.choiceQuestionListData[props.showData.workIndex]
           props.showData.choiceQuestionListData[props.showData.workIndex]
@@ -153,7 +153,7 @@
               </svg>{{ lang.ssAnalysis }}
               </svg>{{ lang.ssAnalysis }}
             </div>
             </div>
             <div class="ai_refresh" :class="{ 'disabled': currentAnalysis && currentAnalysis.loading }"
             <div class="ai_refresh" :class="{ 'disabled': currentAnalysis && currentAnalysis.loading }"
-              @click="aiAnalysisRefresh78()">
+              @click.stop="aiAnalysisRefresh78()" v-if="false">
               {{ lang.ssAIGenerate }}
               {{ lang.ssAIGenerate }}
               <svg viewBox="0 0 1024 1024" width="200" height="200">
               <svg viewBox="0 0 1024 1024" width="200" height="200">
                 <path
                 <path
@@ -175,7 +175,7 @@
           </div>
           </div>
         </div>
         </div>
         <div class="cq_changeBtn" v-if="props.showData.choiceQuestionListData.length > 1">
         <div class="cq_changeBtn" v-if="props.showData.choiceQuestionListData.length > 1">
-          <div :class="{ cq_cb_disabled: props.showData.workIndex <= 0 }" @click="changeWorkIndex(0)">
+          <div :class="{ cq_cb_disabled: props.showData.workIndex <= 0 }" @click.stop="changeWorkIndex(0)">
             <svg style="transform: rotate(-90deg);" viewBox="0 0 1024 1024" version="1.1" width="200" height="200">
             <svg style="transform: rotate(-90deg);" viewBox="0 0 1024 1024" version="1.1" width="200" height="200">
               <path
               <path
                 d="M512 330.666667c14.933333 0 29.866667 4.266667 40.533333 14.933333l277.33333399 234.666667c27.733333 23.466667 29.866667 64 8.53333301 89.6-23.466667 27.733333-64 29.866667-89.6 8.53333299L512 477.866667l-236.8 200.53333299c-27.733333 23.466667-68.266667 19.19999999-89.6-8.53333299-23.466667-27.733333-19.19999999-68.266667 8.53333301-89.6l277.33333399-234.666667c10.666667-10.666667 25.6-14.933333 40.533333-14.933333z"
                 d="M512 330.666667c14.933333 0 29.866667 4.266667 40.533333 14.933333l277.33333399 234.666667c27.733333 23.466667 29.866667 64 8.53333301 89.6-23.466667 27.733333-64 29.866667-89.6 8.53333299L512 477.866667l-236.8 200.53333299c-27.733333 23.466667-68.266667 19.19999999-89.6-8.53333299-23.466667-27.733333-19.19999999-68.266667 8.53333301-89.6l277.33333399-234.666667c10.666667-10.666667 25.6-14.933333 40.533333-14.933333z"
@@ -184,7 +184,7 @@
           </div>
           </div>
           <span>{{ props.showData.workIndex + 1 }}/{{ props.showData.choiceQuestionListData.length }}</span>
           <span>{{ props.showData.workIndex + 1 }}/{{ props.showData.choiceQuestionListData.length }}</span>
           <div :class="{ cq_cb_disabled: props.showData.workIndex >= props.showData.choiceQuestionListData.length - 1 }"
           <div :class="{ cq_cb_disabled: props.showData.workIndex >= props.showData.choiceQuestionListData.length - 1 }"
-            @click="changeWorkIndex(1)">
+            @click.stop="changeWorkIndex(1)">
             <svg style="transform: rotate(90deg);" viewBox="0 0 1024 1024" version="1.1" width="200" height="200">
             <svg style="transform: rotate(90deg);" viewBox="0 0 1024 1024" version="1.1" width="200" height="200">
               <path
               <path
                 d="M512 330.666667c14.933333 0 29.866667 4.266667 40.533333 14.933333l277.33333399 234.666667c27.733333 23.466667 29.866667 64 8.53333301 89.6-23.466667 27.733333-64 29.866667-89.6 8.53333299L512 477.866667l-236.8 200.53333299c-27.733333 23.466667-68.266667 19.19999999-89.6-8.53333299-23.466667-27.733333-19.19999999-68.266667 8.53333301-89.6l277.33333399-234.666667c10.666667-10.666667 25.6-14.933333 40.533333-14.933333z"
                 d="M512 330.666667c14.933333 0 29.866667 4.266667 40.533333 14.933333l277.33333399 234.666667c27.733333 23.466667 29.866667 64 8.53333301 89.6-23.466667 27.733333-64 29.866667-89.6 8.53333299L512 477.866667l-236.8 200.53333299c-27.733333 23.466667-68.266667 19.19999999-89.6-8.53333299-23.466667-27.733333-19.19999999-68.266667 8.53333301-89.6l277.33333399-234.666667c10.666667-10.666667 25.6-14.933333 40.533333-14.933333z"
@@ -199,7 +199,7 @@
         <div class="c_t15_title">{{ workDetail.json.answerQ }}</div>
         <div class="c_t15_title">{{ workDetail.json.answerQ }}</div>
         <span class="c_t15_type">{{ lang.ssQATest }}</span>
         <span class="c_t15_type">{{ lang.ssQATest }}</span>
         <div class="c_t15_content" v-show="!lookWorkData">
         <div class="c_t15_content" v-show="!lookWorkData">
-          <div class="c_t15_c_item" v-for="item in processedWorkArray" :key="item.id" @click="lookWork(item.id)">
+          <div class="c_t15_c_item" v-for="item in processedWorkArray" :key="item.id" @click.stop="lookWork(item.id)">
             <div class="c_t15_c_i_top">
             <div class="c_t15_c_i_top">
               <span>S</span>
               <span>S</span>
               <div>{{ item.name }}</div>
               <div>{{ item.name }}</div>
@@ -211,7 +211,8 @@
         </div>
         </div>
 
 
         <div class="aiAnalysis" style="margin-top:1rem ;"
         <div class="aiAnalysis" style="margin-top:1rem ;"
-          v-if="processedWorkArray.length > 0 && lookWorkData === null && workDetail.type === '15'">
+          v-if="processedWorkArray.length > 0 && lookWorkData === null && workDetail.type === '15'"
+          @click.stop="clickContent(false)">
           <div class="ai_header">
           <div class="ai_header">
             <div class="ai_title">
             <div class="ai_title">
               <svg viewBox="0 0 1024 1024" width="200" height="200">
               <svg viewBox="0 0 1024 1024" width="200" height="200">
@@ -224,7 +225,7 @@
               </svg>{{ lang.ssAnalysis }}
               </svg>{{ lang.ssAnalysis }}
             </div>
             </div>
             <div class="ai_refresh" :class="{ 'disabled': currentAnalysis && currentAnalysis.loading }"
             <div class="ai_refresh" :class="{ 'disabled': currentAnalysis && currentAnalysis.loading }"
-              @click="aiAnalysisRefresh15()">
+              @click.stop="aiAnalysisRefresh15()">
               {{ lang.ssAIGenerate }}
               {{ lang.ssAIGenerate }}
               <svg viewBox="0 0 1024 1024" width="200" height="200">
               <svg viewBox="0 0 1024 1024" width="200" height="200">
                 <path
                 <path
@@ -240,7 +241,7 @@
           <div class="ai_echartsData" v-if="currentAnalysis && currentAnalysis.json.keyword">
           <div class="ai_echartsData" v-if="currentAnalysis && currentAnalysis.json.keyword">
             <div class="title">{{ lang.ssKeyword }}:</div>
             <div class="title">{{ lang.ssKeyword }}:</div>
             <span v-for="(item, index) in currentAnalysis.json.keyword" :key="index">{{ item }}</span>
             <span v-for="(item, index) in currentAnalysis.json.keyword" :key="index">{{ item }}</span>
-            <div class="btn" @click="openEchatsDialog()">{{ lang.ssViewKeywordCloud }}</div>
+            <div class="btn" @click.stop="openEchatsDialog()">{{ lang.ssViewKeywordCloud }}</div>
           </div>
           </div>
           <div class="generatingContent" v-if="currentAnalysis && currentAnalysis.generatingContent">{{
           <div class="generatingContent" v-if="currentAnalysis && currentAnalysis.generatingContent">{{
             lang.ssGeneratingContent }}...</div>
             lang.ssGeneratingContent }}...</div>
@@ -249,9 +250,9 @@
         </div>
         </div>
 
 
 
 
-        <div class="c_t15_workDetail" v-if="lookWorkData">
+        <div class="c_t15_workDetail" v-if="lookWorkData" @click.stop="clickContent(false)">
           <div class="c_t15_wd_top">
           <div class="c_t15_wd_top">
-            <img src="../../../assets/img/arrow_left.png" @click="lookWork('')" />
+            <img src="../../../assets/img/arrow_left.png" @click.stop="lookWork('')" />
             <span>S</span>
             <span>S</span>
             <div>{{ lookWorkData.name }}</div>
             <div>{{ lookWorkData.name }}</div>
           </div>
           </div>
@@ -259,7 +260,62 @@
             <span v-html="lookWorkData.content.answer"></span>
             <span v-html="lookWorkData.content.answer"></span>
             <div class="c_t15_wd_c_imageList" v-if="lookWorkData.content.fileList.length > 0">
             <div class="c_t15_wd_c_imageList" v-if="lookWorkData.content.fileList.length > 0">
               <img v-for="item in lookWorkData.content.fileList" :src="item.url" :key="item.uploadTime"
               <img v-for="item in lookWorkData.content.fileList" :src="item.url" :key="item.uploadTime"
-                @click="lookImage(item.url)" />
+                @click.stop="lookImage(item.url)" />
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 拍照 -->
+      <div class="c_t15" v-if="workDetail && workDetail.type === '79' && props.showData">
+        <div class="c_t15_title">{{ workDetail.json.answerQ }}</div>
+        <span class="c_t15_type">{{ lang.ssPhoto }}</span>
+        <div class="c_t15_content" v-show="!lookWorkData">
+          <div class="c_t15_c_item" v-for="item in processedWorkArray" :key="item.id"
+            @click.stop="item.content && item.content.fileList.length > 0 ? lookWork(item.id) : ''">
+            <div class="c_t15_c_i_top">
+              <span>S</span>
+              <div>{{ item.name }}</div>
+            </div>
+            <div class="c_t73_c_i_bottom" v-if="item.content && item.content.fileList.length > 0">
+              <img :src="item.content.fileList[0].url" />
+            </div>
+            <div class="c_t73_c_i_bottom" v-else>
+              <span>{{ lang.ssNoPhoto }}</span>
+            </div>
+          </div>
+        </div>
+
+        <div class="c_t79_workDetail" v-if="lookWorkData" @click.stop="clickContent(false)">
+          <div class="c_t79_wd_top">
+            <img src="../../../assets/img/arrow_left.png" @click.stop="lookWork('')" />
+            <span>S</span>
+            <div>{{ lookWorkData.name }}</div>
+          </div>
+          <div class="c_t79_wd_content">
+            <img :src="lookWorkData.content.fileList[lookWorkIndex].url"
+              @click.stop="lookImage(lookWorkData.content.fileList[lookWorkIndex].url)" />
+          </div>
+          <!-- <div class="nextAndUpBtn" v-if="lookWorkData.content.fileList.length>1">
+            <span :class="{no_active:lookWorkIndex==0}" @click="changelookWorkIndex(0)">{{ lang.ssPrevP }}</span>
+            <span :class="{no_active:lookWorkData.content.fileList.length-1<=lookWorkIndex}"  @click="changelookWorkIndex(1)">{{ lang.ssNextP }}</span>
+          </div> -->
+          <div class="cq_changeBtn" v-if="lookWorkData.content.fileList.length > 1">
+            <div :class="{ cq_cb_disabled: lookWorkIndex <= 0 }" @click.stop="changelookWorkIndex(0)">
+              <svg style="transform: rotate(-90deg);" viewBox="0 0 1024 1024" version="1.1" width="200" height="200">
+                <path
+                  d="M512 330.666667c14.933333 0 29.866667 4.266667 40.533333 14.933333l277.33333399 234.666667c27.733333 23.466667 29.866667 64 8.53333301 89.6-23.466667 27.733333-64 29.866667-89.6 8.53333299L512 477.866667l-236.8 200.53333299c-27.733333 23.466667-68.266667 19.19999999-89.6-8.53333299-23.466667-27.733333-19.19999999-68.266667 8.53333301-89.6l277.33333399-234.666667c10.666667-10.666667 25.6-14.933333 40.533333-14.933333z"
+                  fill=""></path>
+              </svg>
+            </div>
+            <span>{{ lookWorkIndex + 1 }}/{{ lookWorkData.content.fileList.length }}</span>
+            <div :class="{ cq_cb_disabled: lookWorkIndex >= lookWorkData.content.fileList.length - 1 }"
+              @click.stop="changelookWorkIndex(1)">
+              <svg style="transform: rotate(90deg);" viewBox="0 0 1024 1024" version="1.1" width="200" height="200">
+                <path
+                  d="M512 330.666667c14.933333 0 29.866667 4.266667 40.533333 14.933333l277.33333399 234.666667c27.733333 23.466667 29.866667 64 8.53333301 89.6-23.466667 27.733333-64 29.866667-89.6 8.53333299L512 477.866667l-236.8 200.53333299c-27.733333 23.466667-68.266667 19.19999999-89.6-8.53333299-23.466667-27.733333-19.19999999-68.266667 8.53333301-89.6l277.33333399-234.666667c10.666667-10.666667 25.6-14.933333 40.533333-14.933333z"
+                  fill=""></path>
+              </svg>
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
@@ -270,7 +326,7 @@
         <div class="c_t72_title">{{ lang.ssAiApp }}</div>
         <div class="c_t72_title">{{ lang.ssAiApp }}</div>
         <span class="c_t72_type">{{ lang.ssAiApp }}</span>
         <span class="c_t72_type">{{ lang.ssAiApp }}</span>
         <div class="c_t72_content" v-show="!lookWorkData">
         <div class="c_t72_content" v-show="!lookWorkData">
-          <div class="c_t72_c_item" v-for="item in processedWorkArray" :key="item.id" @click="lookWork(item.id)">
+          <div class="c_t72_c_item" v-for="item in processedWorkArray" :key="item.id" @click.stop="lookWork(item.id)">
             <div class="c_t72_c_i_top">
             <div class="c_t72_c_i_top">
               <span>S</span>
               <span>S</span>
               <div>{{ item.name }}</div>
               <div>{{ item.name }}</div>
@@ -279,7 +335,8 @@
         </div>
         </div>
 
 
         <div class="aiAnalysis" style="margin-top:1rem ;"
         <div class="aiAnalysis" style="margin-top:1rem ;"
-          v-if="processedWorkArray.length > 0 && lookWorkData === null && props.showData.toolType === 72">
+          v-if="processedWorkArray.length > 0 && lookWorkData === null && props.showData.toolType === 72"
+          @click.stop="clickContent(false)">
           <div class="ai_header">
           <div class="ai_header">
             <div class="ai_title">
             <div class="ai_title">
               <svg viewBox="0 0 1024 1024" width="200" height="200">
               <svg viewBox="0 0 1024 1024" width="200" height="200">
@@ -292,7 +349,7 @@
               </svg>{{ lang.ssAnalysis }}
               </svg>{{ lang.ssAnalysis }}
             </div>
             </div>
             <div class="ai_refresh" :class="{ 'disabled': currentAnalysis && currentAnalysis.loading }"
             <div class="ai_refresh" :class="{ 'disabled': currentAnalysis && currentAnalysis.loading }"
-              @click="aiAnalysisRefresh72()">
+              @click.stop="aiAnalysisRefresh72()">
               {{ lang.ssAIGenerate }}
               {{ lang.ssAIGenerate }}
               <svg viewBox="0 0 1024 1024" width="200" height="200">
               <svg viewBox="0 0 1024 1024" width="200" height="200">
                 <path
                 <path
@@ -302,16 +359,15 @@
 
 
             </div>
             </div>
           </div>
           </div>
-          <div class="ai_content" v-if="currentAnalysis && currentAnalysis.json">
-            {{ currentAnalysis.json.text }}
+          <div class="ai_content" v-if="currentAnalysis && currentAnalysis.json" v-html="currentAnalysis.json.text">
           </div>
           </div>
           <div class="ai_updateTime" v-if="currentAnalysis">{{ lang.ssUpdateTime }}:{{ currentAnalysis.update_at }}
           <div class="ai_updateTime" v-if="currentAnalysis">{{ lang.ssUpdateTime }}:{{ currentAnalysis.update_at }}
           </div>
           </div>
         </div>
         </div>
 
 
-        <div class="c_t72_workDetail" v-if="lookWorkData">
+        <div class="c_t72_workDetail" v-if="lookWorkData" @click.stop="clickContent(false)">
           <div class="c_t72_wd_top">
           <div class="c_t72_wd_top">
-            <img src="../../../assets/img/arrow_left.png" @click="lookWork('')" />
+            <img src="../../../assets/img/arrow_left.png" @click.stop="lookWork('')" />
             <span>S</span>
             <span>S</span>
             <div>{{ lookWorkData.name }}</div>
             <div>{{ lookWorkData.name }}</div>
           </div>
           </div>
@@ -344,7 +400,8 @@
                           {{ item.type }}
                           {{ item.type }}
                         </div>
                         </div>
                         <div class="na_m_i_content">
                         <div class="na_m_i_content">
-                          <img @click="lookImage(item3)" style="height: 100px;width: auto;cursor: pointer;" :src="item3" />
+                          <img @click.stop="lookImage(item3)" style="height: 100px;width: auto;cursor: pointer;"
+                            :src="item3" />
                         </div>
                         </div>
                       </div>
                       </div>
                     </template>
                     </template>
@@ -361,7 +418,7 @@
         <div class="c_t73_title">{{ lang.ssPageImage }}</div>
         <div class="c_t73_title">{{ lang.ssPageImage }}</div>
         <span class="c_t73_type">{{ lang.ssHPage }}</span>
         <span class="c_t73_type">{{ lang.ssHPage }}</span>
         <div class="c_t73_content" v-show="!lookWorkData">
         <div class="c_t73_content" v-show="!lookWorkData">
-          <div class="c_t73_c_item" v-for="item in processedWorkArray" :key="item.id" @click="lookWork(item.id)">
+          <div class="c_t73_c_item" v-for="item in processedWorkArray" :key="item.id" @click.stop="lookWork(item.id)">
             <div class="c_t73_c_i_top">
             <div class="c_t73_c_i_top">
               <span>S</span>
               <span>S</span>
               <div>{{ item.name }}</div>
               <div>{{ item.name }}</div>
@@ -372,9 +429,9 @@
           </div>
           </div>
         </div>
         </div>
 
 
-        <div class="c_t73_workDetail" v-if="lookWorkData">
+        <div class="c_t73_workDetail" v-if="lookWorkData" @click.stop="clickContent(false)">
           <div class="c_t73_wd_top">
           <div class="c_t73_wd_top">
-            <img src="../../../assets/img/arrow_left.png" @click="lookWork('')" />
+            <img src="../../../assets/img/arrow_left.png" @click.stop="lookWork('')" />
             <span>S</span>
             <span>S</span>
             <div>{{ lookWorkData.name }}</div>
             <div>{{ lookWorkData.name }}</div>
           </div>
           </div>
@@ -442,7 +499,9 @@ const echartsDialogRef = ref<any>(null)
 // ai分析数据
 // ai分析数据
 const aiAnalysisData = ref<Array<any>>([])
 const aiAnalysisData = ref<Array<any>>([])
 
 
-const md = new MarkdownIt()
+const md = new MarkdownIt({
+  html: true,
+})
 const { getFile } = useImport()
 const { getFile } = useImport()
 
 
 // 判断是否是 URL 链接
 // 判断是否是 URL 链接
@@ -488,7 +547,7 @@ const processWorkContent = async (content: string, toolType: number): Promise<an
   }
   }
 
 
   try {
   try {
-    if ([45, 15, 78].includes(toolType)) {
+    if ([45, 15, 78, 79].includes(toolType)) {
       return JSON.parse(decodeURIComponent(contentToParse))
       return JSON.parse(decodeURIComponent(contentToParse))
     }
     }
     else if (toolType === 72) {
     else if (toolType === 72) {
@@ -596,11 +655,23 @@ const lookImage = (url: string) => {
   }
   }
 }
 }
 
 
+const lookWorkIndex = ref<number>(0)
 // 查看作业
 // 查看作业
 const lookWork = (id: string) => {
 const lookWork = (id: string) => {
+  lookWorkIndex.value = 0
   lookWorkDetail.value = id
   lookWorkDetail.value = id
 }
 }
 
 
+// 切换查看作业图片
+const changelookWorkIndex = (type: number) => {
+  if (type === 0 && lookWorkIndex.value > 0) {
+    lookWorkIndex.value = lookWorkIndex.value - 1
+  }
+  else if (type === 1 && lookWorkIndex.value < lookWorkData.value.content.fileList.length - 1) {
+    lookWorkIndex.value = lookWorkIndex.value + 1
+  }
+}
+
 // 选择题图表实例
 // 选择题图表实例
 const myChart = ref<any>(null)
 const myChart = ref<any>(null)
 
 
@@ -1283,29 +1354,29 @@ const getWordCloud15 = () => {
 const aiAnalysisRefresh72 = async () => {
 const aiAnalysisRefresh72 = async () => {
 
 
   let chatMsg = ``
   let chatMsg = ``
-
+  console.log('processedWorkArray.value', processedWorkArray.value)
   processedWorkArray.value.forEach((i) => {
   processedWorkArray.value.forEach((i) => {
-    i.content.forEach(j => {
-      if (j.messages) {
-        j.messages.forEach((a) => {
-          chatMsg += `\n${a.sender}:
+    if (typeof i.content === 'object') {
+      i.content.forEach(j => {
+        if (j.messages) {
+          j.messages.forEach((a) => {
+            chatMsg += `\n${a.sender}:
 ${a.content}\n`
 ${a.content}\n`
-        })
-      }
-      if (j.imageUrls) {
-        j.imageUrls.forEach((a) => {
-          chatMsg += `\n${a}\n`
-        })
-      }
-
-    })
+          })
+        }
+        if (j.imageUrls) {
+          j.imageUrls.forEach((a) => {
+            chatMsg += `\n${a}\n`
+          })
+        }
 
 
+      })
+    }
   })
   })
-
-
+  // if(props.userId)
 
 
   // - 未提交学生:${JSON.stringify(props.showData.unsubmittedStudents.map((item: any) => item.name))}
   // - 未提交学生:${JSON.stringify(props.showData.unsubmittedStudents.map((item: any) => item.name))}
-  const msg = `# CONTEXT #
+  let msg = `# CONTEXT #
 你是K-12阶段的AI教育课堂分析助手,基于上传的课件、逐字稿,以及当页的学生答题数据(选择题/问答题/智能体对话)进行智能分析。
 你是K-12阶段的AI教育课堂分析助手,基于上传的课件、逐字稿,以及当页的学生答题数据(选择题/问答题/智能体对话)进行智能分析。
 
 
 # OBJECTIVE #
 # OBJECTIVE #
@@ -1333,6 +1404,47 @@ ${a.content}\n`
 # EXAMPLES #
 # EXAMPLES #
 样例:
 样例:
 智能体对话显示学生对“模型训练”概念模糊,多次询问“为什么不能直接告诉机器答案”。针对概念混淆学生,补充“人类学习类比”相关解释,巩固“从数据中学习规律”核心认知。`
 智能体对话显示学生对“模型训练”概念模糊,多次询问“为什么不能直接告诉机器答案”。针对概念混淆学生,补充“人类学习类比”相关解释,巩固“从数据中学习规律”核心认知。`
+
+
+  if (["6c56ec0e-2c74-11ef-bee5-005056b86db5", "aea65da6-4399-11f1-9985-005056924926", "9c236d45-49cd-11f1-9985-005056924926"].includes(props.userId)) {
+    msg = `你是K-12阶段的AI教育课堂分析助手,基于当前课程信息以及当页的学生与单/多智能体对话数据,进行深入的智能分析。
+
+### 任务目标
+1. **总体统计分析**:
+   - 分析学生表现数据,例如全班学生互动数据,包括但不限于参与度、互动轮次、作文批改分布、作业完成情况等。
+   - 提取核心统计指标,例如发现共性问题、互动模式、分层水平分布、关键评分、完成度、优秀或需关注的学生。
+
+2. **洞察(Key Insights)**:
+   - 不是所有模块都需要的
+   - 输出的模块使用1~3句快速完成核心发现输出
+   - 保持简短、直观、易快速理解。
+3. **不输出教学建议或干预措施**。
+
+### 输出格式
+1. Markdown表格:展示关键统计指标(发言次数、互动频率、提问数量、主题分布)。
+2. ASCII条形图(可选):展示各项指标对比情况(绝对不要ASCII条形图,应该显示指标名字)
+3. 洞察与建议:分点呈现,每点对应具体统计指标或数据来源,保持精简,直观、易快速理解。
+**注意,不必输出多余的开场白(可以有非常简短的),转场,铺垫,形容词等,尽可能把输出留给有价值的发现**
+**对于重点要注意的内容,可以用 <span style="color:red"> 内容 </span> 以及 markdown的强调语法来highlight**
+
+### 注意事项
+- 输出简洁明了,重点突出。
+- 所有数字列右对齐,必要时显示百分比。
+- 避免冗长文字和详细案例描述。
+- 保持专业、友好语气,可使用 Emoji 提示情绪、课堂氛围或学生表现状态。
+- 输出保持在600字左右
+
+#INPUT#
+课程数据:
+- 课程名称:${props.courseDetail.title}
+- 课程学科:${props.courseDetail.name}
+当前页面答题数据(问答题):【分析重点】
+- AI应用
+- 对话数据:${chatMsg}
+`
+  }
+
+
   console.log('cs', msg)
   console.log('cs', msg)
   if (!currentAnalysis.value) {
   if (!currentAnalysis.value) {
     aiAnalysisData.value.push({
     aiAnalysisData.value.push({
@@ -1358,7 +1470,7 @@ ${a.content}\n`
     if (event.type === 'message') {
     if (event.type === 'message') {
       aiAnalysisData.value.find((item: any) => {
       aiAnalysisData.value.find((item: any) => {
         return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
         return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
-      }).json.text = event.data
+      }).json.text = md.render(event.data)
 
 
       aiAnalysisData.value.find((item: any) => {
       aiAnalysisData.value.find((item: any) => {
         return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
         return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
@@ -1367,7 +1479,7 @@ ${a.content}\n`
     else if (event.type === 'messageEnd') {
     else if (event.type === 'messageEnd') {
       aiAnalysisData.value.find((item: any) => {
       aiAnalysisData.value.find((item: any) => {
         return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
         return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
-      }).json.text = event.data
+      }).json.text = md.render(event.data)
       aiAnalysisData.value.find((item: any) => {
       aiAnalysisData.value.find((item: any) => {
         return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
         return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
       }).noEnd = false
       }).noEnd = false
@@ -1376,7 +1488,7 @@ ${a.content}\n`
       }).update_at = new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-')
       }).update_at = new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-')
       saveAnalysis()
       saveAnalysis()
     }
     }
-  }).catch(err => {
+  }, '', [], 'open-qwen-plus-latest').catch(err => {
     console.log('err', err)
     console.log('err', err)
   })
   })
 }
 }
@@ -1396,9 +1508,9 @@ const currentAnalysis = computed(() => {
   if (_result) {
   if (_result) {
     return _result
     return _result
   }
   }
-  
+
   return null
   return null
-  
+
 })
 })
 
 
 // 保存分析
 // 保存分析
@@ -1423,22 +1535,24 @@ const saveAnalysis = () => {
 }
 }
 
 
 
 
+// 点击边框
+const clickContent = (flag: boolean) => {
+  if (flag && lookWorkDetail.value && workDetail.value?.type !== '45' && workDetail.value?.type !== '78') {
+    lookWork('')
+  }
+}
+
 // 监听 props.showData.workDetail.id 变化
 // 监听 props.showData.workDetail.id 变化
 watch(
 watch(
   () => props.showData?.workDetail,
   () => props.showData?.workDetail,
   (newId, oldId) => {
   (newId, oldId) => {
-    if (newId && newId !== oldId) {
-      getAnalysis()
-    }
+    getAnalysis()
   },
   },
   { immediate: true }
   { immediate: true }
 )
 )
 
 
 
 
 
 
-// onMounted(()=>{
-//   getAnalysis()
-// })
 
 
 // 组件卸载时清理ECharts实例
 // 组件卸载时清理ECharts实例
 onUnmounted(() => {
 onUnmounted(() => {
@@ -1703,6 +1817,33 @@ onUnmounted(() => {
             -webkit-line-clamp: 2;
             -webkit-line-clamp: 2;
             -webkit-box-orient: vertical;
             -webkit-box-orient: vertical;
           }
           }
+
+          .c_t73_c_i_bottom {
+            margin-top: 15px;
+            font-weight: 300;
+            font-size: 14px;
+            // height: 40px;
+            max-width: 100%;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            display: -webkit-box;
+            -webkit-line-clamp: 2;
+            -webkit-box-orient: vertical;
+
+            img {
+              width: 100%;
+              height: 200px;
+              object-fit: cover;
+            }
+
+            span {
+              width: 100%;
+              height: 200px;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+            }
+          }
         }
         }
       }
       }
 
 
@@ -1772,6 +1913,104 @@ onUnmounted(() => {
           }
           }
         }
         }
       }
       }
+
+      .c_t79_workDetail {
+        width: 100%;
+        height: auto;
+        margin-top: 40px;
+        box-shadow: 4px 4px 14px 0px rgba(252, 207, 0, 0.5);
+        box-sizing: border-box;
+        padding: 16px;
+        border-radius: 12px;
+        display: flex;
+        flex-direction: column;
+
+        .c_t79_wd_top {
+          width: 100%;
+          display: flex;
+          align-items: center;
+          gap: 15px;
+
+          &>img {
+            width: 25px;
+            height: 25px;
+            cursor: pointer;
+          }
+
+          &>span {
+            display: block;
+            width: 30px;
+            height: 30px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background: rgba(252, 207, 0, 1);
+            border-radius: 4px;
+            color: rgba(255, 255, 255, 1);
+            font-weight: bold;
+            font-size: 16px;
+          }
+
+          &>div {
+            color: rgba(0, 0, 0, 0.7);
+            font-weight: 800;
+            font-size: 18px;
+          }
+        }
+
+        .c_t79_wd_content {
+          width: 100%;
+          margin-top: 20px;
+          max-height: 100%;
+          overflow: auto;
+          flex-wrap: wrap;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+
+          &>img {
+            max-width: 100%;
+            height: 300px;
+            object-fit: cover;
+          }
+        }
+      }
+
+      .cq_changeBtn {
+        display: flex;
+        align-items: center;
+        gap: 1.5rem;
+        margin: 1rem auto;
+
+        &>div {
+          padding: .6rem;
+          border-radius: .5rem;
+          border: solid 2px #F6C82B;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          cursor: pointer;
+
+          &>svg {
+            fill: #F6C82D;
+            width: 1rem;
+            height: 1rem;
+          }
+
+          &.cq_cb_disabled {
+            cursor: not-allowed !important;
+            border-color: #FEF8E9 !important;
+          }
+
+          &.cq_cb_disabled>svg {
+            fill: #A3A3A3 !important;
+          }
+        }
+
+        &>span {
+          font-weight: 500;
+        }
+      }
     }
     }
 
 
     .c_t72 {
     .c_t72 {
@@ -2211,6 +2450,23 @@ onUnmounted(() => {
   &>.ai_content {
   &>.ai_content {
     font-size: 1rem;
     font-size: 1rem;
     font-weight: 500;
     font-weight: 500;
+
+    :deep(table) {
+      border-collapse: collapse;
+      width: 100%;
+      margin: 0.5rem 0;
+
+      th,
+      td {
+        border: 1px solid #ddd;
+        padding: 0.5rem;
+        text-align: left;
+      }
+
+      th {
+        background: #f5f5f5;
+      }
+    }
   }
   }
 
 
   &>.ai_echartsData {
   &>.ai_echartsData {
@@ -2246,4 +2502,34 @@ onUnmounted(() => {
     font-weight: 500;
     font-weight: 500;
   }
   }
 }
 }
+
+.nextAndUpBtn {
+  width: 100%;
+  height: auto;
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  margin-top: 20px;
+  gap: 15px;
+
+  &>span {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 100%;
+    height: 40px;
+    border-radius: 5px;
+    font-size: 14px;
+    font-weight: 500;
+    color: #fff;
+    background: #f6c82b;
+    cursor: pointer;
+  }
+
+  &>.no_active {
+    background: #cccccc !important;
+    color: #999999 !important;
+    cursor: not-allowed !important;
+    pointer-events: none !important;
+  }
+}
 </style>
 </style>

+ 10 - 6
src/views/Student/index.vue

@@ -85,10 +85,10 @@
               <div class="homework-check-box-item-title">{{ lang.ssQuestion }}</div>
               <div class="homework-check-box-item-title">{{ lang.ssQuestion }}</div>
             </div>
             </div>
             <div class="homework-check-box-item" @click="openChoiceQuestionDetail3(slideIndex)" :class="{'active': choiceQuestionDetailDialogOpenList.includes(slideIndex)}">
             <div class="homework-check-box-item" @click="openChoiceQuestionDetail3(slideIndex)" :class="{'active': choiceQuestionDetailDialogOpenList.includes(slideIndex)}">
-              <div class="homework-check-box-item-title">{{ lang.ssAnswer }}</div>
+              <div class="homework-check-box-item-title">{{ lang.ssResult }}</div>
             </div>
             </div>
           </div>
           </div>
-          <div class="aiBtn" ref="aiBtnRef" v-if="isQuestionFrame && hasWork && props.type == '2'" 
+          <div class="aiBtn" ref="aiBtnRef" v-if="isQuestionFrame && hasWork && props.type == '2' && aiAssistant" 
             :style="{ right: aiBtnPosition.x + 'px', bottom: aiBtnPosition.y + 'px' }" @click="openAiChat">
             :style="{ right: aiBtnPosition.x + 'px', bottom: aiBtnPosition.y + 'px' }" @click="openAiChat">
             <IconComment class="aiBtn-icon" />
             <IconComment class="aiBtn-icon" />
             <span>AI对话</span>
             <span>AI对话</span>
@@ -106,7 +106,7 @@
           <ScreenSlideList :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"
           <ScreenSlideList :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"
             :animationIndex="0" :turnSlideToId="() => { }" :manualExitFullscreen="() => { }"  :slideIndex="slideIndex" v-show="!choiceQuestionDetailDialogOpenList.includes(slideIndex)"/>
             :animationIndex="0" :turnSlideToId="() => { }" :manualExitFullscreen="() => { }"  :slideIndex="slideIndex" v-show="!choiceQuestionDetailDialogOpenList.includes(slideIndex)"/>
 
 
-          <choiceQuestionDetailDialog v-if="choiceQuestionDetailDialogOpenList.includes(slideIndex)" :cid="props.cid" :workId="workId"  :userId="props.userid" :courseDetail="courseDetail" :workArray="workArray" @changeWorkIndex="changeWorkIndex" v-model:visible="choiceQuestionDetailDialogOpenList" :showData="answerTheResultRef" :slideIndex="slideIndex" :workIndex="0" :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"/>
+          <choiceQuestionDetailDialog v-if="choiceQuestionDetailDialogOpenList.includes(slideIndex) && currentSlideToolType !== 77" :cid="props.cid" :workId="workId"  :userId="props.userid" :courseDetail="courseDetail" :workArray="workArray" @changeWorkIndex="changeWorkIndex" v-model:visible="choiceQuestionDetailDialogOpenList" :showData="answerTheResultRef" :slideIndex="slideIndex" :workIndex="0" :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"/>
           <SpeakingClassPanel
           <SpeakingClassPanel
             v-else-if="choiceQuestionDetailDialogOpenList.includes(slideIndex) && currentSlideToolType === 77"
             v-else-if="choiceQuestionDetailDialogOpenList.includes(slideIndex) && currentSlideToolType === 77"
             ref="speakingPanelRef"
             ref="speakingPanelRef"
@@ -630,6 +630,7 @@ const rightPanelMode = ref<'homework' | 'dialogue' | 'choice' | ''>('homework')
 // 移除定时器相关代码,改用socket监听
 // 移除定时器相关代码,改用socket监听
 
 
 const courseDetail = ref<any>({})
 const courseDetail = ref<any>({})
+const aiAssistant = ref<boolean>(false)
 const studentArray = ref<any>([])
 const studentArray = ref<any>([])
 
 
 // 跟随模式相关状态
 // 跟随模式相关状态
@@ -1222,7 +1223,7 @@ const getWorkId = () => {
     typeof element === 'object' &&
     typeof element === 'object' &&
     ('toolType' in element) &&
     ('toolType' in element) &&
     (element as any).toolType !== undefined &&
     (element as any).toolType !== undefined &&
-    ((element as any).toolType === 45 || (element as any).toolType === 15 || (element as any).toolType === 73 || (element as any).toolType === 72 || (element as any).toolType === 78)
+    ((element as any).toolType === 45 || (element as any).toolType === 15 || (element as any).toolType === 73 || (element as any).toolType === 72 || (element as any).toolType === 78 || (element as any).toolType === 79)
   ) {
   ) {
     // 提取链接中的id参数
     // 提取链接中的id参数
     const url = (element as any).url
     const url = (element as any).url
@@ -1234,8 +1235,10 @@ const getWorkId = () => {
         id = match[1]
         id = match[1]
       }
       }
       workId.value = id
       workId.value = id
-    }
-    else {
+      if((element as any).toolType === 72){
+        workId.value = (element as any).id
+      }
+    }else{
       workId.value = ''
       workId.value = ''
     }
     }
   }
   }
@@ -2690,6 +2693,7 @@ const getCourseDetail = async () => {
     selectWorksStudent()
     selectWorksStudent()
     checkIsCreator()
     checkIsCreator()
     const pptJSONUrl = JSON.parse(courseData.chapters).pptData ? JSON.parse(courseData.chapters).pptData : ''
     const pptJSONUrl = JSON.parse(courseData.chapters).pptData ? JSON.parse(courseData.chapters).pptData : ''
+    aiAssistant.value = JSON.parse(courseData.chapters).aiAssistant ? JSON.parse(courseData.chapters).aiAssistant : false
     console.log(pptJSONUrl)
     console.log(pptJSONUrl)
     
     
     if (pptJSONUrl) { 
     if (pptJSONUrl) { 

+ 2 - 0
src/views/Student/index2.vue

@@ -547,6 +547,7 @@ const rightPanelMode = ref<'homework' | 'dialogue' | 'choice' | ''>('homework')
 // 移除定时器相关代码,改用socket监听
 // 移除定时器相关代码,改用socket监听
 
 
 const courseDetail = ref<any>({})
 const courseDetail = ref<any>({})
+const aiAssistant = ref<any>(false) // 课程助手是否打开
 const studentArray = ref<any>([])
 const studentArray = ref<any>([])
 
 
 // 跟随模式相关状态
 // 跟随模式相关状态
@@ -2432,6 +2433,7 @@ const getCourseDetail = async () => {
     selectWorksStudent()
     selectWorksStudent()
     checkIsCreator()
     checkIsCreator()
     const pptJSONUrl = JSON.parse(courseData.chapters).pptData ? JSON.parse(courseData.chapters).pptData : ''
     const pptJSONUrl = JSON.parse(courseData.chapters).pptData ? JSON.parse(courseData.chapters).pptData : ''
+    aiAssistant.value = JSON.parse(courseData.chapters).aiAssistant ? JSON.parse(courseData.chapters).aiAssistant : false
     console.log(pptJSONUrl)
     console.log(pptJSONUrl)
     
     
     if (pptJSONUrl) { 
     if (pptJSONUrl) { 

+ 1 - 0
src/views/components/element/FrameElement/BaseFrameElement.vue

@@ -174,6 +174,7 @@ const getTypeLabel = (type: number): string => {
     76: 'ssCreateSpace',
     76: 'ssCreateSpace',
     77: 'ssEnglishSpeakingTool',
     77: 'ssEnglishSpeakingTool',
     78: 'ssVote',
     78: 'ssVote',
+    79: 'ssPhoto',
   }
   }
   const key = typeMap[type]
   const key = typeMap[type]
   return (key ? lang[key] : lang.ssUnknown) as string
   return (key ? lang[key] : lang.ssUnknown) as string

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

@@ -131,7 +131,7 @@ const execCommand = ({ target, action }: RichTextCommand) => {
       addMark(editorView, mark)
       addMark(editorView, mark)
 
 
       if (item.value && !document.fonts.check(`16px ${item.value}`)) {
       if (item.value && !document.fonts.check(`16px ${item.value}`)) {
-        message.warning(lang.ssFontLoadWait)
+        // message.warning(lang.ssFontLoadWait)
       }
       }
     }
     }
     else if (item.command === 'fontsize' && item.value) {
     else if (item.command === 'fontsize' && item.value) {

+ 23 - 1
src/views/lang/cn.json

@@ -162,6 +162,8 @@
   "ssChoiceQ": "选择",
   "ssChoiceQ": "选择",
   "ssQandA": "问答",
   "ssQandA": "问答",
   "ssVote": "投票",
   "ssVote": "投票",
+  "ssPhoto": "拍照",
+  "ssNoPhoto": "暂无拍照",
   "ssNoLearn": "暂无学习内容",
   "ssNoLearn": "暂无学习内容",
   "ssNeedUpload": "请先上传或创建学习内容",
   "ssNeedUpload": "请先上传或创建学习内容",
   "ssPreview": "预览",
   "ssPreview": "预览",
@@ -235,6 +237,8 @@
   "ssClose": "关闭",
   "ssClose": "关闭",
   "ssPrevQ": "上一题",
   "ssPrevQ": "上一题",
   "ssNextQ": "下一题",
   "ssNextQ": "下一题",
+  "ssPrevP": "上一张",
+  "ssNextP": "下一张",
   "ssSingleSel": "单选题",
   "ssSingleSel": "单选题",
   "ssMultiSel": "多选题",
   "ssMultiSel": "多选题",
   "ssNodeTitle": "节点*",
   "ssNodeTitle": "节点*",
@@ -596,6 +600,9 @@
   "ssDupPage": "复制页面",
   "ssDupPage": "复制页面",
   "ssDelPage": "删除页面",
   "ssDelPage": "删除页面",
   "ssAddSect": "增加节",
   "ssAddSect": "增加节",
+  "ssDupPage2": "复制",
+  "ssDelPage2": "删除",
+  "ssNewPage2": "新增",
   "ssPlayFromCur": "从当前放映",
   "ssPlayFromCur": "从当前放映",
   "ssDblClickEdit": "双击编辑",
   "ssDblClickEdit": "双击编辑",
   "ssElText": "文本",
   "ssElText": "文本",
@@ -851,5 +858,20 @@
   "ssSpkAIGenerating": "AI 生成中…",
   "ssSpkAIGenerating": "AI 生成中…",
   "ssSpkJustNow": "刚刚生成",
   "ssSpkJustNow": "刚刚生成",
   "ssSpkSecondsAgoTpl": "{n} 秒前生成",
   "ssSpkSecondsAgoTpl": "{n} 秒前生成",
-  "ssSpkMinutesAgoTpl": "{n} 分钟前生成"
+  "ssSpkMinutesAgoTpl": "{n} 分钟前生成",
+  "ssResult": "结果",
+  "ssConfirmDel": "确认删除",
+  "ssConfirmDelContent": "此操作不可恢复,是否继续?",
+  "searchFont": "搜索字体",
+  "searchFontSize": "搜索字体大小",
+  "textColor": "文字颜色",
+  "highlight": "文字高亮",
+  "fontSizeAdd": "增加字号",
+  "fontSizeReduce": "减少字号",
+  "bold": "加粗",
+  "italic": "斜体",
+  "underline": "下划线",
+  "strikethrough": "删除线",
+  "flipVertically": "垂直翻转",
+  "flipHorizontally": "水平翻转"
 }
 }

+ 23 - 1
src/views/lang/en.json

@@ -161,6 +161,8 @@
   "ssContentList": "Content list",
   "ssContentList": "Content list",
   "ssQandA": "Q&A",
   "ssQandA": "Q&A",
   "ssVote": "Vote",
   "ssVote": "Vote",
+  "ssPhoto": "Photo",
+  "ssNoPhoto": "No photo",
   "ssNoLearn": "No learning content",
   "ssNoLearn": "No learning content",
   "ssNeedUpload": "Please upload or create learning content first",
   "ssNeedUpload": "Please upload or create learning content first",
   "ssPreview": "Preview",
   "ssPreview": "Preview",
@@ -235,6 +237,8 @@
   "ssClose": "Close",
   "ssClose": "Close",
   "ssPrevQ": "Previous question",
   "ssPrevQ": "Previous question",
   "ssNextQ": "Next question",
   "ssNextQ": "Next question",
+  "ssPrevP": "Previous photo",
+  "ssNextP": "Next photo",
   "ssSingleSel": "Single Choice",
   "ssSingleSel": "Single Choice",
   "ssMultiSel": "Multiple Choice",
   "ssMultiSel": "Multiple Choice",
   "ssNodeTitle": "Node *",
   "ssNodeTitle": "Node *",
@@ -597,6 +601,9 @@
   "ssDupPage": "Duplicate Slide",
   "ssDupPage": "Duplicate Slide",
   "ssDelPage": "Delete Slide",
   "ssDelPage": "Delete Slide",
   "ssAddSect": "Add Section",
   "ssAddSect": "Add Section",
+  "ssDupPage2": "Duplicate",
+  "ssDelPage2": "Delete",
+  "ssNewPage2": "Add",
   "ssPlayFromCur": "Present from Current Slide",
   "ssPlayFromCur": "Present from Current Slide",
   "ssDblClickEdit": "Double-click to edit",
   "ssDblClickEdit": "Double-click to edit",
   "ssElText": "Text",
   "ssElText": "Text",
@@ -851,5 +858,20 @@
   "ssSpkAIGenerating": "Generating…",
   "ssSpkAIGenerating": "Generating…",
   "ssSpkJustNow": "Just now",
   "ssSpkJustNow": "Just now",
   "ssSpkSecondsAgoTpl": "{n}s ago",
   "ssSpkSecondsAgoTpl": "{n}s ago",
-  "ssSpkMinutesAgoTpl": "{n}m ago"
+  "ssSpkMinutesAgoTpl": "{n}m ago",
+  "ssResult": "Result",
+  "ssConfirmDel": "Confirm Delete",
+  "ssConfirmDelContent": "This operation cannot be recovered, continue?",
+  "searchFont": "Search Font",
+  "searchFontSize": "Search Font Size",
+  "textColor": "Text Color",
+  "highlight": "Text Highlight Color",
+  "fontSizeAdd": "Increase Font Size",
+  "fontSizeReduce": "Reduce Font Size",
+  "bold": "Bold",
+  "italic": "Italic",
+  "underline": "Underline",
+  "strikethrough": "Strikethrough",
+  "flipVertically": "Flip Vertically",
+  "flipHorizontally": "Flip Horizontally"
 }
 }

+ 23 - 1
src/views/lang/hk.json

@@ -161,6 +161,8 @@
   "ssContentList": "內容列表",
   "ssContentList": "內容列表",
   "ssQandA": "問答",
   "ssQandA": "問答",
   "ssVote": "投票",
   "ssVote": "投票",
+  "ssPhoto": "拍照",
+  "ssNoPhoto": "暫無拍照",
   "ssNoLearn": "暫無學習內容",
   "ssNoLearn": "暫無學習內容",
   "ssNeedUpload": "請先上傳或創建學習內容",
   "ssNeedUpload": "請先上傳或創建學習內容",
   "ssPreview": "預覽",
   "ssPreview": "預覽",
@@ -235,6 +237,8 @@
   "ssClose": "關閉",
   "ssClose": "關閉",
   "ssPrevQ": "上一題",
   "ssPrevQ": "上一題",
   "ssNextQ": "下一題",
   "ssNextQ": "下一題",
+  "ssPrevP": "上一張",
+  "ssNextP": "下一張",
   "ssSingleSel": "單選題",
   "ssSingleSel": "單選題",
   "ssMultiSel": "多選題",
   "ssMultiSel": "多選題",
   "ssNodeTitle": "節點*",
   "ssNodeTitle": "節點*",
@@ -597,6 +601,9 @@
   "ssDupPage": "複製頁面",
   "ssDupPage": "複製頁面",
   "ssDelPage": "刪除頁面",
   "ssDelPage": "刪除頁面",
   "ssAddSect": "增加節",
   "ssAddSect": "增加節",
+  "ssDupPage2": "複製",
+  "ssDelPage2": "刪除",
+  "ssNewPage2": "新增",
   "ssPlayFromCur": "從當前放映",
   "ssPlayFromCur": "從當前放映",
   "ssDblClickEdit": "雙擊編輯",
   "ssDblClickEdit": "雙擊編輯",
   "ssElText": "文本",
   "ssElText": "文本",
@@ -851,5 +858,20 @@
   "ssSpkAIGenerating": "AI 生成中…",
   "ssSpkAIGenerating": "AI 生成中…",
   "ssSpkJustNow": "剛剛生成",
   "ssSpkJustNow": "剛剛生成",
   "ssSpkSecondsAgoTpl": "{n} 秒前生成",
   "ssSpkSecondsAgoTpl": "{n} 秒前生成",
-  "ssSpkMinutesAgoTpl": "{n} 分鐘前生成"
+  "ssSpkMinutesAgoTpl": "{n} 分鐘前生成",
+  "ssResult": "結果",
+  "ssConfirmDel": "確認刪除",
+  "ssConfirmDelContent": "此操作不可恢復,是否繼續?",
+  "searchFont": "搜索字型",
+  "searchFontSize": "搜索字型大小",
+  "textColor": "文字顏色",
+  "highlight": "文字高亮",
+  "fontSizeAdd": "增加字号",
+  "fontSizeReduce": "減少字号",
+  "bold": "加粗",
+  "italic": "斜體",
+  "underline": "下劃線",
+  "strikethrough": "刪除線",
+  "flipVertically": "垂直翻轉",
+  "flipHorizontally": "水平翻轉"
 }
 }