index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. <template>
  2. <div class="collapsible-toolbar" :class="{ collapsed: isCollapsed }">
  3. <div class="toolbar-content" v-show="!isCollapsed">
  4. <div class="sidebar-content">
  5. <div class="sidebar-item" :class="{ active: activeSubmenu === 'interactive' }" @click="toggleSubmenu('interactive')">
  6. <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  7. <circle cx="12" cy="12" r="3"/>
  8. <path d="M12 1v6m0 6v6M5.64 5.64l4.24 4.24m4. 4.24l4.24 4.24M1 12h6m6 0h6M5.64 18.36l4.24-4.24m4.24-4.24l4kt-4.24"/>
  9. </svg>
  10. <span class="item-label">{{ lang.ssInteract }}</span>
  11. </div>
  12. <div class="sidebar-item" @click="handleToolClick('h5page')">
  13. <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  14. <circle cx="12" cy="12" r="10"/>
  15. <path d="M2 12h20"/>
  16. <path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/>
  17. </svg>
  18. <span class="item-label">{{ lang.ssHPage }}</span>
  19. </div>
  20. <div class="sidebar-item" @click="handleToolClick('aiapp')">
  21. <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  22. <rect x="3" y="3" width="7" height="7"/>
  23. <rect x="14" y="3" width="7" height="7"/>
  24. <rect x="14" y="14" width="7" height="7"/>
  25. <rect x="3" y="14" width="7" height="7"/>
  26. </svg>
  27. <span class="item-label">{{ lang.ssAiApp }}</span>
  28. </div>
  29. <div class="sidebar-item" @click="handleToolClick('video')">
  30. <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  31. <rect x="3" y="4" width="18" height="16" rx="2" ry="2"/>
  32. <polygon points="10 9 16 12 10 15 10 9"/>
  33. </svg>
  34. <span class="item-label">{{ lang.ssVideo }}</span>
  35. </div>
  36. <div class="sidebar-item" @click="handleToolClick('creative')">
  37. <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  38. <circle cx="12" cy="12" r="10"/>
  39. <line x1="12" y1="8" x2="12" y2="16"/>
  40. <line x1="8" y1="12" x2="16" y2="12"/>
  41. </svg>
  42. <span class="item-label">{{ lang.ssCreative }}</span>
  43. </div>
  44. <div class="sidebar-item" :class="{ active: activeSubmenu === 'contentlist' }" @click="toggleSubmenu('contentlist')" v-show="false">
  45. <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  46. <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
  47. <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
  48. </svg>
  49. <span class="item-label">{{ lang.ssContentList }}</span>
  50. </div>
  51. </div>
  52. </div>
  53. <div class="submenu" :class="{ visible: activeSubmenu === 'interactive' }">
  54. <div class="submenu-item" @click="handleToolClick('choice')">
  55. <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  56. <circle cx="12" cy="12" r="10"/>
  57. <path d="M12 16v-4m0-4h.01"/>
  58. </svg>
  59. <span class="submenu-label">{{ lang.ssChoiceQ }}</span>
  60. </div>
  61. <div class="submenu-item" @click="handleToolClick('qa')">
  62. <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  63. <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>
  64. </svg>
  65. <span class="submenu-label">{{ lang.ssQandA }}</span>
  66. </div>
  67. </div>
  68. <div class="content-list-submenu" :class="{ visible: activeSubmenu === 'contentlist' }">
  69. <div v-if="contentList.length === 0" class="empty-state">
  70. <div class="empty-icon">📚</div>
  71. <div class="empty-title">{{ lang.ssNoLearn }}</div>
  72. <div class="empty-title">{{ lang.ssNeedUpload }}</div>
  73. </div>
  74. <div v-else class="content-list">
  75. <div
  76. v-for="(item, index) in contentList"
  77. :key="index"
  78. class="content-item"
  79. >
  80. <div class="content-main" @click="insertContent(item)">
  81. <svg class="content-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  82. <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
  83. <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
  84. </svg>
  85. <span class="content-label">{{ item.title }}</span>
  86. <span class="type-tag" :class="getTypeClass(item.tool)">{{ getTypeLabel(item.tool) }}</span>
  87. </div>
  88. <div class="content-actions">
  89. <template v-if="item.tool === 74 || item.tool === 75">
  90. <div class="action-btn" @click.stop="previewVideo(item)" :title="lang.ssPreview">
  91. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  92. <polygon points="5 3 19 12 5 21 5 3"/>
  93. </svg>
  94. </div>
  95. </template>
  96. <template v-if="item.tool !== 74 && item.tool !== 75 && item.tool !== 76">
  97. <div class="action-btn" @click.stop="editContent(item)" :title="lang.ssEdit">
  98. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  99. <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
  100. <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
  101. </svg>
  102. </div>
  103. </template>
  104. <template v-if="item.tool !== 76">
  105. <div class="action-btn" @click.stop="copyContent(item)" :title="lang.ssCopy">
  106. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  107. <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
  108. <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
  109. </svg>
  110. </div>
  111. </template>
  112. <div class="action-btn delete" @click.stop="deleteContent(item)" :title="lang.ssDelete">
  113. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  114. <polyline points="3 6 5 6 21 6"/>
  115. <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
  116. </svg>
  117. </div>
  118. </div>
  119. </div>
  120. </div>
  121. </div>
  122. </div>
  123. </template>
  124. <script lang="ts" setup>
  125. import { ref } from 'vue'
  126. import { storeToRefs } from 'pinia'
  127. import useCreateElement from '@/hooks/useCreateElement'
  128. import useSlideHandler from '@/hooks/useSlideHandler'
  129. import { useSlidesStore } from '@/store'
  130. import { lang } from '@/main'
  131. import { ARTICLE_READING_TOOL_TYPE } from '@/configs/englishSpeakingTools'
  132. interface ContentItem {
  133. tool?: number
  134. title?: string
  135. url?: string
  136. id?: string
  137. }
  138. const props = withDefaults(defineProps<{
  139. defaultCollapsed?: boolean
  140. }>(), {
  141. defaultCollapsed: false
  142. })
  143. const emit = defineEmits<{
  144. (e: 'toggle', collapsed: boolean): void
  145. }>()
  146. const isCollapsed = ref(props.defaultCollapsed)
  147. const activeSubmenu = ref<string | null>(null)
  148. const contentList = ref<ContentItem[]>([])
  149. const slidesStore = useSlidesStore()
  150. const { currentSlide } = storeToRefs(slidesStore)
  151. const { createFrameElement } = useCreateElement()
  152. const { createSlide } = useSlideHandler()
  153. const toggleCollapse = () => {
  154. isCollapsed.value = !isCollapsed.value
  155. emit('toggle', isCollapsed.value)
  156. }
  157. const toggleSubmenu = (menu: string) => {
  158. if (activeSubmenu.value === menu) {
  159. activeSubmenu.value = null
  160. }
  161. else {
  162. activeSubmenu.value = menu
  163. if (menu === 'contentlist') {
  164. loadContentList()
  165. }
  166. }
  167. }
  168. const handleToolClick = (tool: string) => {
  169. interface ParentWindowWithToolList extends Window {
  170. addTool?: (id: number) => void;
  171. openVideoUploadDialog?: () => void;
  172. openApplicationCenter?: () => void;
  173. }
  174. const parentWindow = window.parent as ParentWindowWithToolList
  175. console.log('点击工具:', tool)
  176. if (tool === 'h5page') {
  177. parentWindow?.addTool?.(73)
  178. }
  179. else if (tool === 'aiapp') {
  180. parentWindow?.addTool?.(72)
  181. }
  182. else if (tool === 'video') {
  183. parentWindow?.openVideoUploadDialog?.()
  184. }
  185. else if (tool === 'creative') {
  186. parentWindow?.openApplicationCenter?.()
  187. }
  188. else if (tool === 'choice') {
  189. parentWindow?.addTool?.(45)
  190. }
  191. else if (tool === 'qa') {
  192. parentWindow?.addTool?.(15)
  193. }
  194. }
  195. const loadContentList = () => {
  196. try {
  197. interface ParentWindowWithToolList extends Window {
  198. pptToolList?: ContentItem[]
  199. }
  200. const parentWindow = window.parent as ParentWindowWithToolList
  201. contentList.value = parentWindow?.pptToolList || []
  202. }
  203. catch (error) {
  204. console.error('加载内容列表失败:', error)
  205. contentList.value = []
  206. }
  207. }
  208. const insertContent = (item: ContentItem) => {
  209. if (!item.tool || !item.url) return
  210. createFrameElement(item.url, item.tool)
  211. }
  212. const addContent = (data: ContentItem, type: number) => {
  213. // contentList.value.push(data)
  214. if (type === 2) {
  215. const elements = currentSlide.value?.elements || []
  216. const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15))
  217. if (frameElement) {
  218. slidesStore.updateElement({
  219. id: frameElement.id,
  220. props: { url: data.url }
  221. })
  222. }
  223. }
  224. else {
  225. createSlide()
  226. insertContent(data)
  227. }
  228. }
  229. Object.assign(window, { addContent, loadContentList })
  230. // window.loadContentList = loadContentList
  231. // window.addContent = addContent
  232. const previewVideo = (item: ContentItem) => {
  233. interface ParentWindowWithToolList extends Window {
  234. previewVideo?: (item: ContentItem) => void;
  235. }
  236. const parentWindow = window.parent as ParentWindowWithToolList
  237. parentWindow?.previewVideo?.(item)
  238. }
  239. const editContent = (item: ContentItem) => {
  240. interface ParentWindowWithToolList extends Window {
  241. toolBtn?: (action: number, id: string) => void;
  242. }
  243. const parentWindow = window.parent as ParentWindowWithToolList
  244. parentWindow?.toolBtn?.(0, item.id || '')
  245. }
  246. const copyContent = (item: ContentItem) => {
  247. interface ParentWindowWithToolList extends Window {
  248. toolBtn?: (action: number, id: string) => void;
  249. }
  250. const parentWindow = window.parent as ParentWindowWithToolList
  251. parentWindow?.toolBtn?.(1, item.id || '')
  252. }
  253. const deleteContent = (item: ContentItem) => {
  254. interface ParentWindowWithToolList extends Window {
  255. toolBtn?: (action: number, id: string) => void;
  256. }
  257. const parentWindow = window.parent as ParentWindowWithToolList
  258. parentWindow?.toolBtn?.(2, item.id || '')
  259. }
  260. const getTypeLabel = (type?: number) => {
  261. const typeMap: Record<number, string> = {
  262. 45: lang.ssChoiceQ,
  263. 15: lang.ssQATest,
  264. 72: lang.ssAiApp,
  265. 73: lang.ssHPage,
  266. 81: lang.ssHPage,
  267. 74: lang.ssVideo,
  268. 75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
  269. 76: lang.ssCreative,
  270. 77: lang.ssEnglishSpeakingTool,
  271. [ARTICLE_READING_TOOL_TYPE]: lang.ssArticleReadingTool,
  272. 78: lang.ssVote,
  273. 79: lang.ssPhoto,
  274. }
  275. return typeMap[type || 0] || lang.ssUnknown
  276. }
  277. const getTypeClass = (type?: number) => {
  278. const classMap: Record<number, string> = {
  279. 45: 'type-choice',
  280. 15: 'type-question',
  281. 72: 'type-ai',
  282. 73: 'type-h5',
  283. 74: 'type-video',
  284. 75: 'type-bilibili',
  285. 76: 'type-app-center'
  286. }
  287. return classMap[type || 0] || 'type-default'
  288. }
  289. </script>
  290. <style lang="scss" scoped>
  291. .collapsible-toolbar {
  292. width: 100%;
  293. height: 100%;
  294. display: flex;
  295. background: #fff;
  296. border-right: 1px solid #e5e7eb;
  297. transition: width 0.3s ease;
  298. }
  299. .toolbar-content {
  300. flex: 1;
  301. overflow: hidden;
  302. padding: 16px 8px;
  303. }
  304. .sidebar-content {
  305. display: flex;
  306. flex-direction: column;
  307. gap: 6px;
  308. width: 84px;
  309. position: relative;
  310. }
  311. .sidebar-item {
  312. width: 84px;
  313. padding: 12px 8px;
  314. border-radius: 12px;
  315. display: flex;
  316. flex-direction: column;
  317. align-items: center;
  318. justify-content: center;
  319. gap: 6px;
  320. cursor: pointer;
  321. transition: all 0.2s;
  322. position: relative;
  323. &:hover {
  324. background: #f3f4f6;
  325. }
  326. &:active {
  327. background: #e5e7eb;
  328. }
  329. &.active {
  330. background: #eef3ff;
  331. box-shadow: 0 2px 8px rgba(40, 92, 245, 0.15);
  332. }
  333. &.active::after {
  334. content: '';
  335. position: absolute;
  336. left: -8px;
  337. top: 50%;
  338. transform: translateY(-50%);
  339. width: 4px;
  340. height: 32px;
  341. background: #285cf5;
  342. border-radius: 0 2px 2px 0;
  343. }
  344. }
  345. .item-icon {
  346. width: 22px;
  347. height: 22px;
  348. flex-shrink: 0;
  349. color: #6b7280;
  350. }
  351. .sidebar-item:hover .item-icon,
  352. .sidebar-item.active .item-icon {
  353. color: #285cf5;
  354. }
  355. .item-label {
  356. font-size: 11px;
  357. font-weight: 500;
  358. color: #6b7280;
  359. text-align: center;
  360. }
  361. .sidebar-item:hover .item-label,
  362. .sidebar-item.active .item-label {
  363. color: #285cf5;
  364. font-weight: 600;
  365. }
  366. .submenu {
  367. width: 0;
  368. min-width: 0;
  369. overflow: hidden;
  370. transition: all 0.3s ease;
  371. background: #fff;
  372. border-radius: 12px;
  373. z-index: 100;
  374. &.visible {
  375. width: 160px;
  376. min-width: 160px;
  377. padding: 8px 0;
  378. }
  379. }
  380. .submenu-item {
  381. display: flex;
  382. align-items: center;
  383. gap: 12px;
  384. padding: 10px 16px;
  385. cursor: pointer;
  386. transition: all 0.2s ease;
  387. font-size: 13px;
  388. color: #6b7280;
  389. border-radius: 8px;
  390. margin: 4px 8px;
  391. &:hover {
  392. background-color: #f3f4f6;
  393. color: #285cf5;
  394. }
  395. &:active {
  396. background-color: #e5e7eb;
  397. }
  398. }
  399. .submenu-icon {
  400. width: 18px;
  401. height: 18px;
  402. flex-shrink: 0;
  403. color: #9ca3af;
  404. }
  405. .submenu-item:hover .submenu-icon {
  406. color: #285cf5;
  407. }
  408. .submenu-label {
  409. font-size: 13px;
  410. color: #6b7280;
  411. }
  412. .submenu-item:hover .submenu-label {
  413. color: #285cf5;
  414. }
  415. .content-list-submenu {
  416. width: 0;
  417. min-width: 0;
  418. overflow: hidden;
  419. transition: all 0.3s ease;
  420. background: #fff;
  421. border-radius: 12px;
  422. z-index: 100;
  423. &.visible {
  424. width: 380px;
  425. min-width: 380px;
  426. padding: 8px 0;
  427. }
  428. }
  429. .empty-state {
  430. text-align: center;
  431. padding: 20px 10px;
  432. color: #666;
  433. }
  434. .empty-icon {
  435. font-size: 32px;
  436. margin-bottom: 8px;
  437. }
  438. .empty-title {
  439. font-size: 12px;
  440. color: #999;
  441. }
  442. .content-list {
  443. max-height: 400px;
  444. overflow-y: auto;
  445. padding: 4px 0;
  446. &::-webkit-scrollbar {
  447. width: 4px;
  448. }
  449. &::-webkit-scrollbar-track {
  450. background: #f1f1f1;
  451. border-radius: 2px;
  452. }
  453. &::-webkit-scrollbar-thumb {
  454. background: #c1c1c1;
  455. border-radius: 2px;
  456. &:hover {
  457. background: #a8a8a8;
  458. }
  459. }
  460. }
  461. .content-item {
  462. display: flex;
  463. align-items: center;
  464. justify-content: space-between;
  465. padding: 8px 12px;
  466. border-radius: 8px;
  467. margin: 2px 8px;
  468. position: relative;
  469. transition: all 0.2s ease;
  470. &:hover {
  471. background-color: #f8f9fa;
  472. }
  473. }
  474. .content-main {
  475. display: flex;
  476. align-items: center;
  477. gap: 8px;
  478. flex: 1;
  479. min-width: 0;
  480. cursor: pointer;
  481. padding: 4px;
  482. border-radius: 6px;
  483. transition: all 0.2s ease;
  484. &:hover {
  485. background-color: #f3f4f6;
  486. }
  487. }
  488. .content-icon {
  489. width: 16px;
  490. height: 16px;
  491. flex-shrink: 0;
  492. color: #9ca3af;
  493. }
  494. .content-main:hover .content-icon {
  495. color: #285cf5;
  496. }
  497. .content-label {
  498. flex: 1;
  499. font-size: 13px;
  500. color: #6b7280;
  501. white-space: nowrap;
  502. overflow: hidden;
  503. text-overflow: ellipsis;
  504. }
  505. .content-main:hover .content-label {
  506. color: #285cf5;
  507. }
  508. .content-actions {
  509. display: flex;
  510. align-items: center;
  511. gap: 4px;
  512. }
  513. .action-btn {
  514. width: 28px;
  515. height: 28px;
  516. display: flex;
  517. align-items: center;
  518. justify-content: center;
  519. border-radius: 4px;
  520. cursor: pointer;
  521. transition: all 0.2s ease;
  522. color: #9ca3af;
  523. svg {
  524. width: 14px;
  525. height: 14px;
  526. }
  527. &:hover {
  528. background-color: #f3f4f6;
  529. color: #285cf5;
  530. }
  531. &.delete {
  532. &:hover {
  533. background-color: #fee2e2;
  534. color: #ef4444;
  535. }
  536. }
  537. }
  538. .type-tag {
  539. padding: 2px 6px;
  540. border-radius: 4px;
  541. font-size: 10px;
  542. font-weight: 500;
  543. color: #fff;
  544. white-space: nowrap;
  545. flex-shrink: 0;
  546. &.type-choice {
  547. background-color: #4caf50;
  548. }
  549. &.type-question {
  550. background-color: #ff9800;
  551. }
  552. &.type-ai {
  553. background-color: #2196f3;
  554. }
  555. &.type-h5 {
  556. background-color: #9c27b0;
  557. }
  558. &.type-video {
  559. background-color: #f44336;
  560. }
  561. &.type-bilibili {
  562. background-color: #fb7299;
  563. }
  564. &.type-default {
  565. background-color: #757575;
  566. }
  567. &.type-app-center {
  568. background-color: #673ab7;
  569. }
  570. }
  571. </style>