|
@@ -0,0 +1,906 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="kwl-detail">
|
|
|
|
|
+ <!-- 头部导航 -->
|
|
|
|
|
+ <div class="detail-header">
|
|
|
|
|
+ <div class="back-btn" @click="$emit('back')">
|
|
|
|
|
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
|
+ <path d="M15 18l-6-6 6-6" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="detail-title">{{ lang.ssKwlTitle }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Banner 区域 -->
|
|
|
|
|
+ <div class="detail-banner">
|
|
|
|
|
+ <img :src="kwl" :alt="lang.ssKwlTitle" />
|
|
|
|
|
+ <div class="tool-overly"></div>
|
|
|
|
|
+ <div class="tool-icon-badge">
|
|
|
|
|
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
|
+ <path d="M9 18h6M10 22h4M12 2a7 7 0 00-4 12.7c.6.5 1 1.3 1 2.3h6c0-1 .4-1.8 1-2.3A7 7 0 0012 2z"></path>
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 描述与标签 -->
|
|
|
|
|
+ <div class="detail-desc-row">
|
|
|
|
|
+ <div class="detail-desc-text">{{ lang.ssKwlDesc }}</div>
|
|
|
|
|
+ <div class="help-icon" @mouseenter="showHelp = true" @mouseleave="showHelp = false">
|
|
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
|
+ <circle cx="12" cy="12" r="10" />
|
|
|
|
|
+ <path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3" />
|
|
|
|
|
+ <line x1="12" y1="17" x2="12.01" y2="17" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ <!-- 帮助气泡 -->
|
|
|
|
|
+ <div v-if="showHelp" class="help-tooltip">
|
|
|
|
|
+ <div class="help-title">{{ lang.ssKwlUsageScenario }}</div>
|
|
|
|
|
+ <div class="help-text">{{ lang.ssKwlScenarioText }}</div>
|
|
|
|
|
+ <div class="help-title">{{ lang.ssKwlSteps }}</div>
|
|
|
|
|
+ <div class="help-list">
|
|
|
|
|
+ <div>{{ lang.ssKwlStep1 }}</div>
|
|
|
|
|
+ <div>{{ lang.ssKwlStep2 }}</div>
|
|
|
|
|
+ <div>{{ lang.ssKwlStep3 }}</div>
|
|
|
|
|
+ <div>{{ lang.ssKwlStep4 }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="help-source">{{ lang.ssKwlSource }}</div>
|
|
|
|
|
+ <div class="help-purpose">{{ lang.ssKwlPurpose }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="detail-tags">
|
|
|
|
|
+ <span class="detail-tag">{{ lang.ssKwlGradeRange }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 工具配置区 -->
|
|
|
|
|
+ <div class="config-section">
|
|
|
|
|
+ <div class="config-title">{{ lang.ssKwlToolConfig }}</div>
|
|
|
|
|
+ <div class="config-subtitle">{{ lang.ssKwlConfigSubtitle }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 情境/任务说明 -->
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <div class="form-label">{{ lang.ssKwlTaskLabel }}</div>
|
|
|
|
|
+ <div class="form-textarea">
|
|
|
|
|
+ <textarea v-model="formData.taskDescription"
|
|
|
|
|
+ :placeholder="lang.ssKwlTaskPlaceholder"></textarea>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 添加配图/视频 -->
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <div class="upload-area" @click="triggerFileSelect">
|
|
|
|
|
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
|
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
|
|
|
+ <circle cx="8.5" cy="8.5" r="1.5" />
|
|
|
|
|
+ <polyline points="21 15 16 10 5 21" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ <span>{{ lang.ssKwlUploadMedia }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <input
|
|
|
|
|
+ ref="fileInputRef"
|
|
|
|
|
+ type="file"
|
|
|
|
|
+ accept="image/*,video/*"
|
|
|
|
|
+ multiple
|
|
|
|
|
+ style="display: none"
|
|
|
|
|
+ @change="handleFileChange"
|
|
|
|
|
+ />
|
|
|
|
|
+ <!-- 已上传预览列表 -->
|
|
|
|
|
+ <div v-if="uploadedFiles.length > 0" class="upload-preview-list">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(file, index) in uploadedFiles"
|
|
|
|
|
+ :key="index"
|
|
|
|
|
+ class="upload-preview-item"
|
|
|
|
|
+ >
|
|
|
|
|
+ <img v-if="file.type.startsWith('image/')" :src="file.url" :alt="file.name" @click="openPreview(index)" />
|
|
|
|
|
+ <video v-else :src="file.url" muted @click="openPreview(index)"></video>
|
|
|
|
|
+ <div class="preview-delete" @click="removeFile(index)">
|
|
|
|
|
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
|
+ <line x1="18" y1="6" x2="6" y2="18" />
|
|
|
|
|
+ <line x1="6" y1="6" x2="18" y2="18" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="file.uploading" class="preview-loading">
|
|
|
|
|
+ <div class="loading-spinner"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 默认展开操作提示 -->
|
|
|
|
|
+ <div class="form-row">
|
|
|
|
|
+ <div class="form-text-block">
|
|
|
|
|
+ <div class="form-label">{{ lang.ssKwlShowHowTo }}</div>
|
|
|
|
|
+ <div class="form-sublabel">{{ lang.ssKwlShowHowToDesc }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="toggle-switch" :class="{ active: formData.showHowTo }"
|
|
|
|
|
+ @click="formData.showHowTo = !formData.showHowTo">
|
|
|
|
|
+ <div class="toggle-thumb"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 高级配置 -->
|
|
|
|
|
+ <div class="advanced-section">
|
|
|
|
|
+ <div class="advanced-header" @click="showAdvanced = !showAdvanced">
|
|
|
|
|
+ <span class="advanced-title">{{ lang.ssKwlAdvanced }}</span>
|
|
|
|
|
+ <svg class="advanced-arrow" :class="{ expanded: showAdvanced }" width="16" height="16" viewBox="0 0 24 24"
|
|
|
|
|
+ fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
|
+ <polyline points="6 9 12 15 18 9" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="showAdvanced" class="advanced-body">
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <div class="form-label">{{ lang.ssKwlKPrompt }}</div>
|
|
|
|
|
+ <div class="form-input">
|
|
|
|
|
+ <input v-model="formData.kplaceholder" type="text" :placeholder="lang.ssKwlKDefault" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <div class="form-label">{{ lang.ssKwlWPrompt }}</div>
|
|
|
|
|
+ <div class="form-input">
|
|
|
|
|
+ <input v-model="formData.wplaceholder" type="text" :placeholder="lang.ssKwlWDefault" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <div class="form-label">{{ lang.ssKwlLPrompt }}</div>
|
|
|
|
|
+ <div class="form-input">
|
|
|
|
|
+ <input v-model="formData.lplaceholder" type="text" :placeholder="lang.ssKwlLDefault" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="form-row">
|
|
|
|
|
+ <div class="form-text-block">
|
|
|
|
|
+ <div class="form-label">{{ lang.ssKwlShowReflection }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="toggle-switch" :class="{ active: formData.isShowReflection }"
|
|
|
|
|
+ @click="formData.isShowReflection = !formData.isShowReflection">
|
|
|
|
|
+ <div class="toggle-thumb"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- <div class="form-group">
|
|
|
|
|
+ <div class="form-row-with-value">
|
|
|
|
|
+ <div class="form-label">{{ lang.ssKwlMinEntries }}</div>
|
|
|
|
|
+ <div class="range-value">{{ formData.minEntries }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="slider-wrapper">
|
|
|
|
|
+ <input v-model.number="formData.minEntries" type="range" min="0" max="10" step="1" class="range-slider" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div> -->
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 添加到幻灯片按钮 -->
|
|
|
|
|
+ <div class="footer-btn">
|
|
|
|
|
+ <div class="add-btn" @click="handleAddToSlide">{{ lang.ssKwlAddToSlide }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 图片预览:复用项目已有组件 -->
|
|
|
|
|
+ <PreviewImageTool ref="previewImageToolRef" />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 视频预览弹窗 -->
|
|
|
|
|
+ <Teleport to="body">
|
|
|
|
|
+ <div v-if="videoPreviewVisible" class="kwl-preview-modal" @click.self="closeVideoPreview">
|
|
|
|
|
+ <div class="kwl-preview-close" @click="closeVideoPreview">
|
|
|
|
|
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
|
+ <line x1="18" y1="6" x2="6" y2="18" />
|
|
|
|
|
+ <line x1="6" y1="6" x2="18" y2="18" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <video
|
|
|
|
|
+ :src="videoPreviewUrl"
|
|
|
|
|
+ class="kwl-preview-video"
|
|
|
|
|
+ controls
|
|
|
|
|
+ autoplay
|
|
|
|
|
+ ></video>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Teleport>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script lang="ts" setup>
|
|
|
|
|
+import { ref, reactive } from 'vue'
|
|
|
|
|
+import kwl from '@/assets/img/kwlBanner.jpg'
|
|
|
|
|
+import useImport from '@/hooks/useImport'
|
|
|
|
|
+import { lang } from '@/main'
|
|
|
|
|
+
|
|
|
|
|
+const { uploadFileToS3 } = useImport()
|
|
|
|
|
+
|
|
|
|
|
+interface UploadedFile {
|
|
|
|
|
+ name: string
|
|
|
|
|
+ url: string
|
|
|
|
|
+ type: string
|
|
|
|
|
+ uploading: boolean
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const emit = defineEmits<{
|
|
|
|
|
+ (e: 'back'): void
|
|
|
|
|
+ (e: 'add', data: any): void
|
|
|
|
|
+}>()
|
|
|
|
|
+
|
|
|
|
|
+const showHelp = ref(false)
|
|
|
|
|
+const showAdvanced = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+const fileInputRef = ref<HTMLInputElement | null>(null)
|
|
|
|
|
+const uploadedFiles = ref<UploadedFile[]>([])
|
|
|
|
|
+
|
|
|
|
|
+const triggerFileSelect = () => {
|
|
|
|
|
+ fileInputRef.value?.click()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleFileChange = async (event: Event) => {
|
|
|
|
|
+ const target = event.target as HTMLInputElement
|
|
|
|
|
+ const files = target.files
|
|
|
|
|
+ if (!files || files.length === 0) return
|
|
|
|
|
+
|
|
|
|
|
+ const validTypes = ['image/', 'video/']
|
|
|
|
|
+ const validFiles: File[] = []
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < files.length; i++) {
|
|
|
|
|
+ const file = files[i]
|
|
|
|
|
+ if (validTypes.some(type => file.type.startsWith(type))) {
|
|
|
|
|
+ validFiles.push(file)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (const file of validFiles) {
|
|
|
|
|
+ const item: UploadedFile = {
|
|
|
|
|
+ name: file.name,
|
|
|
|
|
+ url: '',
|
|
|
|
|
+ type: file.type,
|
|
|
|
|
+ uploading: true
|
|
|
|
|
+ }
|
|
|
|
|
+ uploadedFiles.value.push(item)
|
|
|
|
|
+ // 通过索引获取响应式对象进行修改
|
|
|
|
|
+ const idx = uploadedFiles.value.length - 1
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const url = await uploadFileToS3(file)
|
|
|
|
|
+ uploadedFiles.value[idx].url = url
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (err) {
|
|
|
|
|
+ console.error(lang.ssKwlUploadFail + ':', file.name, err)
|
|
|
|
|
+ uploadedFiles.value.splice(idx, 1)
|
|
|
|
|
+ }
|
|
|
|
|
+ finally {
|
|
|
|
|
+ if (uploadedFiles.value[idx]) {
|
|
|
|
|
+ uploadedFiles.value[idx].uploading = false
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 清空 input value,允许重复选择相同文件
|
|
|
|
|
+ if (fileInputRef.value) {
|
|
|
|
|
+ fileInputRef.value.value = ''
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const removeFile = (index: number) => {
|
|
|
|
|
+ uploadedFiles.value.splice(index, 1)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 预览相关:图片使用项目已有 previewImageTool 组件
|
|
|
|
|
+import PreviewImageTool from '@/views/components/tool/previewImageTool.vue'
|
|
|
|
|
+
|
|
|
|
|
+const previewImageToolRef = ref<InstanceType<typeof PreviewImageTool> | null>(null)
|
|
|
|
|
+const videoPreviewVisible = ref(false)
|
|
|
|
|
+const videoPreviewUrl = ref('')
|
|
|
|
|
+
|
|
|
|
|
+const openPreview = (index: number) => {
|
|
|
|
|
+ const file = uploadedFiles.value[index]
|
|
|
|
|
+ if (!file || file.uploading) return
|
|
|
|
|
+
|
|
|
|
|
+ if (file.type.startsWith('image/')) {
|
|
|
|
|
+ // 使用项目已有的图片预览组件
|
|
|
|
|
+ previewImageToolRef.value?.previewImage(file.url)
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
|
|
+ // 视频预览:使用简单弹窗
|
|
|
|
|
+ videoPreviewUrl.value = file.url
|
|
|
|
|
+ videoPreviewVisible.value = true
|
|
|
|
|
+ document.body.style.overflow = 'hidden'
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const closeVideoPreview = () => {
|
|
|
|
|
+ videoPreviewVisible.value = false
|
|
|
|
|
+ document.body.style.overflow = ''
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const formData = reactive({
|
|
|
|
|
+ know: [],
|
|
|
|
|
+ want: [],
|
|
|
|
|
+ learned: [],
|
|
|
|
|
+ reflection: '',
|
|
|
|
|
+ taskDescription: '',
|
|
|
|
|
+ taskUrl: [] as string[],
|
|
|
|
|
+ showHowTo: false,
|
|
|
|
|
+ kplaceholder: lang.ssKwlKDefault,
|
|
|
|
|
+ wplaceholder: lang.ssKwlWDefault,
|
|
|
|
|
+ lplaceholder: lang.ssKwlLDefault,
|
|
|
|
|
+ isShowReflection: true
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const handleAddToSlide = () => {
|
|
|
|
|
+ formData.taskUrl = uploadedFiles.value.map(f => f.url).filter(Boolean)
|
|
|
|
|
+ emit('add', { ...formData })
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+.kwl-detail {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ padding: 0 0 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 头部 */
|
|
|
|
|
+.detail-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ padding: 0 14px;
|
|
|
|
|
+ position: sticky;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ z-index: 10;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+
|
|
|
|
|
+ .back-btn {
|
|
|
|
|
+ width: 32px;
|
|
|
|
|
+ height: 32px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background: #f3f4f6;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .detail-title {
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* Banner */
|
|
|
|
|
+.detail-banner {
|
|
|
|
|
+ margin: 0 14px 14px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ // width: 100%;
|
|
|
|
|
+ height: 140px;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+
|
|
|
|
|
+ .tool-overly {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ inset: 0;
|
|
|
|
|
+ background: rgba(37, 99, 235, 0.15);
|
|
|
|
|
+ opacity: .45;
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ .tool-icon-badge {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 10px;
|
|
|
|
|
+ left: 10px;
|
|
|
|
|
+ width: 32px;
|
|
|
|
|
+ height: 32px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
|
|
|
|
|
+
|
|
|
|
|
+ svg {
|
|
|
|
|
+ width: 18px;
|
|
|
|
|
+ height: 18px;
|
|
|
|
|
+ // color: #FF9300;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .tool-icon-badge svg {
|
|
|
|
|
+ color: #2563eb;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .tool-overly {
|
|
|
|
|
+ background-color: #2563eb;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ img {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 140px;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 描述 */
|
|
|
|
|
+.detail-desc-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ padding: 0 14px;
|
|
|
|
|
+
|
|
|
|
|
+ .detail-desc-text {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #4b5563;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .help-icon {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ margin-top: 2px;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ color: #FF9300;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .help-tooltip {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 100%;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+ width: 240px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ padding: 14px;
|
|
|
|
|
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
|
|
|
+ z-index: 100;
|
|
|
|
|
+
|
|
|
|
|
+ .help-title {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+
|
|
|
|
|
+ &:first-child {
|
|
|
|
|
+ margin-top: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .help-text {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .help-list {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ line-height: 1.8;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .help-source,
|
|
|
|
|
+ .help-purpose {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-tags {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ padding: 10px 14px 0;
|
|
|
|
|
+
|
|
|
|
|
+ .detail-tag {
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ padding: 3px 10px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #3b82f6;
|
|
|
|
|
+ background: #eff6ff;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 配置区 */
|
|
|
|
|
+.config-section {
|
|
|
|
|
+ padding: 20px 14px 12px;
|
|
|
|
|
+
|
|
|
|
|
+ .config-title {
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ margin-bottom: 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .config-subtitle {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 表单 */
|
|
|
|
|
+.form-group {
|
|
|
|
|
+ padding: 0 14px;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+
|
|
|
|
|
+ .form-label {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .form-textarea {
|
|
|
|
|
+ textarea {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ min-height: 80px;
|
|
|
|
|
+ padding: 10px 12px;
|
|
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ resize: vertical;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ outline: none;
|
|
|
|
|
+ transition: border-color 0.2s ease;
|
|
|
|
|
+
|
|
|
|
|
+ &::placeholder {
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:focus {
|
|
|
|
|
+ border-color: #FF9300;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .form-input {
|
|
|
|
|
+ input {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ padding: 10px 12px;
|
|
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ outline: none;
|
|
|
|
|
+ transition: border-color 0.2s ease;
|
|
|
|
|
+
|
|
|
|
|
+ &::placeholder {
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:focus {
|
|
|
|
|
+ border-color: #FF9300;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 上传区 */
|
|
|
|
|
+.upload-area {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ padding: 12px 14px;
|
|
|
|
|
+ border: 1px dashed #d1d5db;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+
|
|
|
|
|
+ svg {
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ border-color: #FF9300;
|
|
|
|
|
+ color: #FF9300;
|
|
|
|
|
+
|
|
|
|
|
+ svg {
|
|
|
|
|
+ color: #FF9300;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 上传预览列表 */
|
|
|
|
|
+.upload-preview-list {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ margin-top: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.upload-preview-item {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ width: 80px;
|
|
|
|
|
+ height: 80px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background: #f3f4f6;
|
|
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
|
|
+
|
|
|
|
|
+ img, video {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .preview-delete {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 4px;
|
|
|
|
|
+ right: 4px;
|
|
|
|
|
+ width: 20px;
|
|
|
|
|
+ height: 20px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.55);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: background 0.2s ease;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.75);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .preview-loading {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ inset: 0;
|
|
|
|
|
+ background: rgba(255, 255, 255, 1);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .loading-spinner {
|
|
|
|
|
+ width: 24px;
|
|
|
|
|
+ height: 24px;
|
|
|
|
|
+ border: 3px solid #e5e7eb;
|
|
|
|
|
+ border-top-color: #FF9300;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ animation: kwlSpin 0.8s linear infinite;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes kwlSpin {
|
|
|
|
|
+ to { transform: rotate(360deg); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 预览弹窗 */
|
|
|
|
|
+.kwl-preview-modal {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ inset: 0;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.85);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ z-index: 9999;
|
|
|
|
|
+ cursor: zoom-out;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.kwl-preview-close {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 20px;
|
|
|
|
|
+ right: 20px;
|
|
|
|
|
+ width: 40px;
|
|
|
|
|
+ height: 40px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.15);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: background 0.2s ease;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.25);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.kwl-preview-video {
|
|
|
|
|
+ max-width: 90vw;
|
|
|
|
|
+ max-height: 85vh;
|
|
|
|
|
+ object-fit: contain;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 表单项行 */
|
|
|
|
|
+.form-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding: 0 14px;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+
|
|
|
|
|
+ .form-text-block {
|
|
|
|
|
+ .form-label {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .form-sublabel {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+ margin-top: 2px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-row-with-value {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+
|
|
|
|
|
+ .form-label {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .range-value {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #FF9300;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 开关 */
|
|
|
|
|
+.toggle-switch {
|
|
|
|
|
+ width: 44px;
|
|
|
|
|
+ height: 24px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ background: #e5e7eb;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: background 0.25s ease;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+
|
|
|
|
|
+ .toggle-thumb {
|
|
|
|
|
+ width: 20px;
|
|
|
|
|
+ height: 20px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 2px;
|
|
|
|
|
+ left: 2px;
|
|
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
|
|
|
|
+ transition: transform 0.25s ease;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.active {
|
|
|
|
|
+ background: #FF9300;
|
|
|
|
|
+
|
|
|
|
|
+ .toggle-thumb {
|
|
|
|
|
+ transform: translateX(20px);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 高级配置 */
|
|
|
|
|
+.advanced-section {
|
|
|
|
|
+ padding: 0 14px;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+
|
|
|
|
|
+ .advanced-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding: 12px 0;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+
|
|
|
|
|
+ .advanced-title {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .advanced-arrow {
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+ transition: transform 0.2s ease;
|
|
|
|
|
+
|
|
|
|
|
+ &.expanded {
|
|
|
|
|
+ transform: rotate(180deg);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .advanced-body {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ padding-top: 8px;
|
|
|
|
|
+ border-top: 1px solid #f3f4f6;
|
|
|
|
|
+
|
|
|
|
|
+ .form-group {
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 滑块 */
|
|
|
|
|
+.slider-wrapper {
|
|
|
|
|
+ padding: 8px 0;
|
|
|
|
|
+
|
|
|
|
|
+ .range-slider {
|
|
|
|
|
+ -webkit-appearance: none;
|
|
|
|
|
+ appearance: none;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 4px;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+ background: #e5e7eb;
|
|
|
|
|
+ outline: none;
|
|
|
|
|
+
|
|
|
|
|
+ &::-webkit-slider-thumb {
|
|
|
|
|
+ -webkit-appearance: none;
|
|
|
|
|
+ appearance: none;
|
|
|
|
|
+ width: 18px;
|
|
|
|
|
+ height: 18px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: #FF9300;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ box-shadow: 0 1px 3px rgba(255, 147, 0, 0.4);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &::-moz-range-thumb {
|
|
|
|
|
+ width: 18px;
|
|
|
|
|
+ height: 18px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: #FF9300;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ box-shadow: 0 1px 3px rgba(255, 147, 0, 0.4);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 底部按钮 */
|
|
|
|
|
+.footer-btn {
|
|
|
|
|
+ padding: 8px 14px 0;
|
|
|
|
|
+
|
|
|
|
|
+ .add-btn {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ padding: 14px;
|
|
|
|
|
+ background: #FF9300;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background: #f57c00;
|
|
|
|
|
+ transform: translateY(-1px);
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(255, 147, 0, 0.3);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:active {
|
|
|
|
|
+ transform: translateY(0);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|