| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644 |
- <template>
- <div class="collapsible-toolbar" :class="{ collapsed: isCollapsed }">
- <div class="toolbar-content" v-show="!isCollapsed">
- <div class="sidebar-content">
- <div class="sidebar-item" :class="{ active: activeSubmenu === 'interactive' }" @click="toggleSubmenu('interactive')">
- <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <circle cx="12" cy="12" r="3"/>
- <path d="M12 1v6m0 6v6M5.64 5.64l4.24 4.24m4. 4.24l4.24 4.24M1 12h6m6 0h6M5.64 18.36l4.24-4.24m4.24-4.24l4kt-4.24"/>
- </svg>
- <span class="item-label">{{ lang.ssInteract }}</span>
- </div>
- <div class="sidebar-item" @click="handleToolClick('h5page')">
- <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <circle cx="12" cy="12" r="10"/>
- <path d="M2 12h20"/>
- <path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/>
- </svg>
- <span class="item-label">{{ lang.ssHPage }}</span>
- </div>
- <div class="sidebar-item" @click="handleToolClick('aiapp')">
- <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <rect x="3" y="3" width="7" height="7"/>
- <rect x="14" y="3" width="7" height="7"/>
- <rect x="14" y="14" width="7" height="7"/>
- <rect x="3" y="14" width="7" height="7"/>
- </svg>
- <span class="item-label">{{ lang.ssAiApp }}</span>
- </div>
- <div class="sidebar-item" @click="handleToolClick('video')">
- <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <rect x="3" y="4" width="18" height="16" rx="2" ry="2"/>
- <polygon points="10 9 16 12 10 15 10 9"/>
- </svg>
- <span class="item-label">{{ lang.ssVideo }}</span>
- </div>
- <div class="sidebar-item" @click="handleToolClick('creative')">
- <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <circle cx="12" cy="12" r="10"/>
- <line x1="12" y1="8" x2="12" y2="16"/>
- <line x1="8" y1="12" x2="16" y2="12"/>
- </svg>
- <span class="item-label">{{ lang.ssCreative }}</span>
- </div>
- <div class="sidebar-item" :class="{ active: activeSubmenu === 'contentlist' }" @click="toggleSubmenu('contentlist')" v-show="false">
- <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
- <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
- </svg>
- <span class="item-label">{{ lang.ssContentList }}</span>
- </div>
- </div>
- </div>
-
- <div class="submenu" :class="{ visible: activeSubmenu === 'interactive' }">
- <div class="submenu-item" @click="handleToolClick('choice')">
- <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <circle cx="12" cy="12" r="10"/>
- <path d="M12 16v-4m0-4h.01"/>
- </svg>
- <span class="submenu-label">{{ lang.ssChoiceQ }}</span>
- </div>
- <div class="submenu-item" @click="handleToolClick('qa')">
- <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>
- </svg>
- <span class="submenu-label">{{ lang.ssQandA }}</span>
- </div>
- </div>
-
- <div class="content-list-submenu" :class="{ visible: activeSubmenu === 'contentlist' }">
- <div v-if="contentList.length === 0" class="empty-state">
- <div class="empty-icon">📚</div>
- <div class="empty-title">{{ lang.ssNoLearn }}</div>
- <div class="empty-title">{{ lang.ssNeedUpload }}</div>
- </div>
- <div v-else class="content-list">
- <div
- v-for="(item, index) in contentList"
- :key="index"
- class="content-item"
- >
- <div class="content-main" @click="insertContent(item)">
- <svg class="content-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
- <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
- </svg>
- <span class="content-label">{{ item.title }}</span>
- <span class="type-tag" :class="getTypeClass(item.tool)">{{ getTypeLabel(item.tool) }}</span>
- </div>
- <div class="content-actions">
- <template v-if="item.tool === 74 || item.tool === 75">
- <div class="action-btn" @click.stop="previewVideo(item)" :title="lang.ssPreview">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <polygon points="5 3 19 12 5 21 5 3"/>
- </svg>
- </div>
- </template>
- <template v-if="item.tool !== 74 && item.tool !== 75 && item.tool !== 76">
- <div class="action-btn" @click.stop="editContent(item)" :title="lang.ssEdit">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
- <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
- </svg>
- </div>
- </template>
- <template v-if="item.tool !== 76">
- <div class="action-btn" @click.stop="copyContent(item)" :title="lang.ssCopy">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
- </svg>
- </div>
- </template>
- <div class="action-btn delete" @click.stop="deleteContent(item)" :title="lang.ssDelete">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <polyline points="3 6 5 6 21 6"/>
- <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
- </svg>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script lang="ts" setup>
- import { ref } from 'vue'
- import { storeToRefs } from 'pinia'
- import useCreateElement from '@/hooks/useCreateElement'
- import useSlideHandler from '@/hooks/useSlideHandler'
- import { useSlidesStore } from '@/store'
- import { lang } from '@/main'
- interface ContentItem {
- tool?: number
- title?: string
- url?: string
- id?: string
- }
- const props = withDefaults(defineProps<{
- defaultCollapsed?: boolean
- }>(), {
- defaultCollapsed: false
- })
- const emit = defineEmits<{
- (e: 'toggle', collapsed: boolean): void
- }>()
- const isCollapsed = ref(props.defaultCollapsed)
- const activeSubmenu = ref<string | null>(null)
- const contentList = ref<ContentItem[]>([])
- const slidesStore = useSlidesStore()
- const { currentSlide } = storeToRefs(slidesStore)
- const { createFrameElement } = useCreateElement()
- const { createSlide } = useSlideHandler()
- const toggleCollapse = () => {
- isCollapsed.value = !isCollapsed.value
- emit('toggle', isCollapsed.value)
- }
- const toggleSubmenu = (menu: string) => {
- if (activeSubmenu.value === menu) {
- activeSubmenu.value = null
- }
- else {
- activeSubmenu.value = menu
- if (menu === 'contentlist') {
- loadContentList()
- }
- }
- }
- const handleToolClick = (tool: string) => {
- interface ParentWindowWithToolList extends Window {
- addTool?: (id: number) => void;
- openVideoUploadDialog?: () => void;
- openApplicationCenter?: () => void;
- }
- const parentWindow = window.parent as ParentWindowWithToolList
- console.log('点击工具:', tool)
- if (tool === 'h5page') {
- parentWindow?.addTool?.(73)
- }
- else if (tool === 'aiapp') {
- parentWindow?.addTool?.(72)
- }
- else if (tool === 'video') {
- parentWindow?.openVideoUploadDialog?.()
- }
- else if (tool === 'creative') {
- parentWindow?.openApplicationCenter?.()
- }
- else if (tool === 'choice') {
- parentWindow?.addTool?.(45)
- }
- else if (tool === 'qa') {
- parentWindow?.addTool?.(15)
- }
- }
- const loadContentList = () => {
- try {
- interface ParentWindowWithToolList extends Window {
- pptToolList?: ContentItem[]
- }
- const parentWindow = window.parent as ParentWindowWithToolList
- contentList.value = parentWindow?.pptToolList || []
- }
- catch (error) {
- console.error('加载内容列表失败:', error)
- contentList.value = []
- }
- }
- const insertContent = (item: ContentItem) => {
- if (!item.tool || !item.url) return
- createFrameElement(item.url, item.tool)
- }
- const addContent = (data: ContentItem, type: number) => {
- // contentList.value.push(data)
- if (type === 2) {
- const elements = currentSlide.value?.elements || []
- const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15))
- if (frameElement) {
- slidesStore.updateElement({
- id: frameElement.id,
- props: { url: data.url }
- })
- }
- }
- else {
- createSlide()
- insertContent(data)
- }
- }
- Object.assign(window, { addContent, loadContentList })
- // window.loadContentList = loadContentList
- // window.addContent = addContent
- const previewVideo = (item: ContentItem) => {
- interface ParentWindowWithToolList extends Window {
- previewVideo?: (item: ContentItem) => void;
- }
- const parentWindow = window.parent as ParentWindowWithToolList
- parentWindow?.previewVideo?.(item)
- }
- const editContent = (item: ContentItem) => {
- interface ParentWindowWithToolList extends Window {
- toolBtn?: (action: number, id: string) => void;
- }
- const parentWindow = window.parent as ParentWindowWithToolList
- parentWindow?.toolBtn?.(0, item.id || '')
- }
- const copyContent = (item: ContentItem) => {
- interface ParentWindowWithToolList extends Window {
- toolBtn?: (action: number, id: string) => void;
- }
- const parentWindow = window.parent as ParentWindowWithToolList
- parentWindow?.toolBtn?.(1, item.id || '')
- }
- const deleteContent = (item: ContentItem) => {
- interface ParentWindowWithToolList extends Window {
- toolBtn?: (action: number, id: string) => void;
- }
- const parentWindow = window.parent as ParentWindowWithToolList
- parentWindow?.toolBtn?.(2, item.id || '')
- }
- const getTypeLabel = (type?: number) => {
- const typeMap: Record<number, string> = {
- 45: lang.ssChoiceQ,
- 15: lang.ssQATest,
- 72: lang.ssAiApp,
- 73: lang.ssHPage,
- 81: lang.ssHPage,
- 74: lang.ssVideo,
- 75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
- 76: lang.ssCreative,
- 77: lang.ssEnglishSpeakingTool,
- 78: lang.ssVote,
- 79: lang.ssPhoto,
- }
- return typeMap[type || 0] || lang.ssUnknown
- }
- const getTypeClass = (type?: number) => {
- const classMap: Record<number, string> = {
- 45: 'type-choice',
- 15: 'type-question',
- 72: 'type-ai',
- 73: 'type-h5',
- 74: 'type-video',
- 75: 'type-bilibili',
- 76: 'type-app-center'
- }
- return classMap[type || 0] || 'type-default'
- }
- </script>
- <style lang="scss" scoped>
- .collapsible-toolbar {
- width: 100%;
- height: 100%;
- display: flex;
- background: #fff;
- border-right: 1px solid #e5e7eb;
- transition: width 0.3s ease;
- }
- .toolbar-content {
- flex: 1;
- overflow: hidden;
- padding: 16px 8px;
- }
- .sidebar-content {
- display: flex;
- flex-direction: column;
- gap: 6px;
- width: 84px;
- position: relative;
- }
- .sidebar-item {
- width: 84px;
- padding: 12px 8px;
- border-radius: 12px;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 6px;
- cursor: pointer;
- transition: all 0.2s;
- position: relative;
- &:hover {
- background: #f3f4f6;
- }
- &:active {
- background: #e5e7eb;
- }
- &.active {
- background: #eef3ff;
- box-shadow: 0 2px 8px rgba(40, 92, 245, 0.15);
- }
- &.active::after {
- content: '';
- position: absolute;
- left: -8px;
- top: 50%;
- transform: translateY(-50%);
- width: 4px;
- height: 32px;
- background: #285cf5;
- border-radius: 0 2px 2px 0;
- }
- }
- .item-icon {
- width: 22px;
- height: 22px;
- flex-shrink: 0;
- color: #6b7280;
- }
- .sidebar-item:hover .item-icon,
- .sidebar-item.active .item-icon {
- color: #285cf5;
- }
- .item-label {
- font-size: 11px;
- font-weight: 500;
- color: #6b7280;
- text-align: center;
- }
- .sidebar-item:hover .item-label,
- .sidebar-item.active .item-label {
- color: #285cf5;
- font-weight: 600;
- }
- .submenu {
- width: 0;
- min-width: 0;
- overflow: hidden;
- transition: all 0.3s ease;
- background: #fff;
- border-radius: 12px;
- z-index: 100;
- &.visible {
- width: 160px;
- min-width: 160px;
- padding: 8px 0;
- }
- }
- .submenu-item {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 10px 16px;
- cursor: pointer;
- transition: all 0.2s ease;
- font-size: 13px;
- color: #6b7280;
- border-radius: 8px;
- margin: 4px 8px;
- &:hover {
- background-color: #f3f4f6;
- color: #285cf5;
- }
- &:active {
- background-color: #e5e7eb;
- }
- }
- .submenu-icon {
- width: 18px;
- height: 18px;
- flex-shrink: 0;
- color: #9ca3af;
- }
- .submenu-item:hover .submenu-icon {
- color: #285cf5;
- }
- .submenu-label {
- font-size: 13px;
- color: #6b7280;
- }
- .submenu-item:hover .submenu-label {
- color: #285cf5;
- }
- .content-list-submenu {
- width: 0;
- min-width: 0;
- overflow: hidden;
- transition: all 0.3s ease;
- background: #fff;
- border-radius: 12px;
- z-index: 100;
- &.visible {
- width: 380px;
- min-width: 380px;
- padding: 8px 0;
- }
- }
- .empty-state {
- text-align: center;
- padding: 20px 10px;
- color: #666;
- }
- .empty-icon {
- font-size: 32px;
- margin-bottom: 8px;
- }
- .empty-title {
- font-size: 12px;
- color: #999;
- }
- .content-list {
- max-height: 400px;
- overflow-y: auto;
- padding: 4px 0;
- &::-webkit-scrollbar {
- width: 4px;
- }
- &::-webkit-scrollbar-track {
- background: #f1f1f1;
- border-radius: 2px;
- }
- &::-webkit-scrollbar-thumb {
- background: #c1c1c1;
- border-radius: 2px;
- &:hover {
- background: #a8a8a8;
- }
- }
- }
- .content-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 8px 12px;
- border-radius: 8px;
- margin: 2px 8px;
- position: relative;
- transition: all 0.2s ease;
- &:hover {
- background-color: #f8f9fa;
- }
- }
- .content-main {
- display: flex;
- align-items: center;
- gap: 8px;
- flex: 1;
- min-width: 0;
- cursor: pointer;
- padding: 4px;
- border-radius: 6px;
- transition: all 0.2s ease;
- &:hover {
- background-color: #f3f4f6;
- }
- }
- .content-icon {
- width: 16px;
- height: 16px;
- flex-shrink: 0;
- color: #9ca3af;
- }
- .content-main:hover .content-icon {
- color: #285cf5;
- }
- .content-label {
- flex: 1;
- font-size: 13px;
- color: #6b7280;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .content-main:hover .content-label {
- color: #285cf5;
- }
- .content-actions {
- display: flex;
- align-items: center;
- gap: 4px;
- }
- .action-btn {
- width: 28px;
- height: 28px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 4px;
- cursor: pointer;
- transition: all 0.2s ease;
- color: #9ca3af;
- svg {
- width: 14px;
- height: 14px;
- }
- &:hover {
- background-color: #f3f4f6;
- color: #285cf5;
- }
- &.delete {
- &:hover {
- background-color: #fee2e2;
- color: #ef4444;
- }
- }
- }
- .type-tag {
- padding: 2px 6px;
- border-radius: 4px;
- font-size: 10px;
- font-weight: 500;
- color: #fff;
- white-space: nowrap;
- flex-shrink: 0;
- &.type-choice {
- background-color: #4caf50;
- }
- &.type-question {
- background-color: #ff9800;
- }
- &.type-ai {
- background-color: #2196f3;
- }
- &.type-h5 {
- background-color: #9c27b0;
- }
- &.type-video {
- background-color: #f44336;
- }
- &.type-bilibili {
- background-color: #fb7299;
- }
- &.type-default {
- background-color: #757575;
- }
- &.type-app-center {
- background-color: #673ab7;
- }
- }
- </style>
|