Merge branch 'main' of https://gitee.com/zzyyymm/super_video
No files matched your search
@@ -1,13 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container" :style="containerStyle">
|
<div class="app-container" :style="containerStyle">
|
||||||
<div class="app-content" :style="scaleStyle">
|
<div class="app-content" :style="scaleStyle">
|
||||||
<router-view />
|
<router-view v-slot="{ Component }">
|
||||||
|
<KeepAlive :include="cachedNames">
|
||||||
|
<component :is="Component" />
|
||||||
|
</KeepAlive>
|
||||||
|
</router-view>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useScale } from '@/composables/useScale'
|
import { useScale } from '@/composables/useScale'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
// 使用等比例缩放hook
|
// 使用等比例缩放hook
|
||||||
const { scaleStyle } = useScale()
|
const { scaleStyle } = useScale()
|
||||||
@@ -20,6 +25,9 @@ const containerStyle = {
|
|||||||
position: 'relative' as const,
|
position: 'relative' as const,
|
||||||
background: '#000'
|
background: '#000'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 只缓存技能树页
|
||||||
|
const cachedNames = computed(() => ['SkillPage', 'Work'])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 318 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -4,7 +4,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<img class="cursor-pointer" :src="getStaticImage(image)" @click="handleClick" />
|
<img class="cursor-pointer" draggable="false" :src="getStaticImage(image)" @click="handleClick" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|||||||
@@ -0,0 +1,326 @@
|
|||||||
|
<template>
|
||||||
|
<div v-show="visible" class="sv-image-preview-root" :style="rootStyle" @click.self="onMaskClick">
|
||||||
|
<div class="sv-image-preview-toolbar">
|
||||||
|
<button class="sv-ip-btn" @click="emitClose">×</button>
|
||||||
|
<div class="sv-ip-page" v-if="images.length > 1">
|
||||||
|
{{ currentIndex + 1 }} / {{ images.length }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="sv-image-stage"
|
||||||
|
ref="stageRef"
|
||||||
|
:class="{ panning: state.isPanning }"
|
||||||
|
@wheel.prevent="onWheel"
|
||||||
|
@mousedown.prevent="onMouseDown"
|
||||||
|
@mousemove.prevent="onMouseMove"
|
||||||
|
@mouseup.prevent="onMouseUp"
|
||||||
|
@mouseleave.prevent="onMouseUp"
|
||||||
|
@touchstart.passive="onTouchStart"
|
||||||
|
@touchmove.prevent="onTouchMove"
|
||||||
|
@touchend="onTouchEnd"
|
||||||
|
>
|
||||||
|
<transition name="fade">
|
||||||
|
<img
|
||||||
|
v-if="visible"
|
||||||
|
:key="currentIndex"
|
||||||
|
class="sv-image"
|
||||||
|
:class="{ 'no-transition': state.isPanning }"
|
||||||
|
:src="images[currentIndex]"
|
||||||
|
:style="imageStyle"
|
||||||
|
@dblclick="toggleZoom"
|
||||||
|
:draggable="false"
|
||||||
|
/>
|
||||||
|
</transition>
|
||||||
|
<button v-if="images.length > 1" class="sv-ip-nav prev" @click.stop="prev">‹</button>
|
||||||
|
<button v-if="images.length > 1" class="sv-ip-nav next" @click.stop="next">›</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
images: string[]
|
||||||
|
startIndex?: number
|
||||||
|
maskClosable?: boolean
|
||||||
|
zIndex?: number
|
||||||
|
// Scale metrics compatible with useScale.ts
|
||||||
|
designWidth?: number
|
||||||
|
designHeight?: number
|
||||||
|
getScaleMetrics: () => { scale: number; offsetX: number; offsetY: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
startIndex: 0,
|
||||||
|
maskClosable: true,
|
||||||
|
zIndex: 3000,
|
||||||
|
designWidth: 1920,
|
||||||
|
designHeight: 1080
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||||
|
|
||||||
|
const visible = ref(true)
|
||||||
|
const currentIndex = ref(props.startIndex)
|
||||||
|
const stageRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
imgScale: 1,
|
||||||
|
translateX: 0,
|
||||||
|
translateY: 0,
|
||||||
|
isPanning: false,
|
||||||
|
startX: 0,
|
||||||
|
startY: 0,
|
||||||
|
pinchStartDistance: 0,
|
||||||
|
pinchStartScale: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// rAF batching for smoother pan
|
||||||
|
let rafId: number | null = null
|
||||||
|
let nextTranslateX = 0
|
||||||
|
let nextTranslateY = 0
|
||||||
|
function schedulePan(x: number, y: number) {
|
||||||
|
nextTranslateX = x
|
||||||
|
nextTranslateY = y
|
||||||
|
if (rafId !== null) return
|
||||||
|
rafId = requestAnimationFrame(() => {
|
||||||
|
state.translateX = nextTranslateX
|
||||||
|
state.translateY = nextTranslateY
|
||||||
|
rafId = null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const images = computed(() => props.images)
|
||||||
|
|
||||||
|
const rootStyle = computed(() => {
|
||||||
|
const { scale, offsetX, offsetY } = props.getScaleMetrics()
|
||||||
|
return {
|
||||||
|
position: 'fixed',
|
||||||
|
left: `${offsetX}px`,
|
||||||
|
top: `${offsetY}px`,
|
||||||
|
width: `${props.designWidth}px`,
|
||||||
|
height: `${props.designHeight}px`,
|
||||||
|
transform: `scale(${scale})`,
|
||||||
|
transformOrigin: 'top left',
|
||||||
|
zIndex: String(props.zIndex),
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.8)'
|
||||||
|
} as Record<string, string>
|
||||||
|
})
|
||||||
|
|
||||||
|
const imageStyle = computed(() => ({
|
||||||
|
transform: `translate(${state.translateX}px, ${state.translateY}px) scale(${state.imgScale})`,
|
||||||
|
maxWidth: '80%',
|
||||||
|
maxHeight: '80%',
|
||||||
|
userSelect: 'none' as const
|
||||||
|
}))
|
||||||
|
|
||||||
|
function emitClose() {
|
||||||
|
visible.value = false
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMaskClick() {
|
||||||
|
if (props.maskClosable) emitClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWheel(e: WheelEvent) {
|
||||||
|
const delta = -e.deltaY
|
||||||
|
const factor = delta > 0 ? 1.1 : 0.9
|
||||||
|
zoomAtCenter(factor)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseDown(e: MouseEvent) {
|
||||||
|
state.isPanning = true
|
||||||
|
state.startX = e.clientX - state.translateX
|
||||||
|
state.startY = e.clientY - state.translateY
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(e: MouseEvent) {
|
||||||
|
if (!state.isPanning) return
|
||||||
|
schedulePan(e.clientX - state.startX, e.clientY - state.startY)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseUp() {
|
||||||
|
state.isPanning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function distance(t1: Touch, t2: Touch) {
|
||||||
|
const dx = t1.clientX - t2.clientX
|
||||||
|
const dy = t1.clientY - t2.clientY
|
||||||
|
return Math.hypot(dx, dy)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTouchStart(e: TouchEvent) {
|
||||||
|
if (e.touches.length === 1) {
|
||||||
|
const t = e.touches[0]
|
||||||
|
state.isPanning = true
|
||||||
|
state.startX = t.clientX - state.translateX
|
||||||
|
state.startY = t.clientY - state.translateY
|
||||||
|
} else if (e.touches.length === 2) {
|
||||||
|
state.isPanning = false
|
||||||
|
state.pinchStartDistance = distance(e.touches[0], e.touches[1])
|
||||||
|
state.pinchStartScale = state.imgScale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastTap = 0
|
||||||
|
function onTouchEnd() {
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - lastTap < 300) {
|
||||||
|
toggleZoom()
|
||||||
|
}
|
||||||
|
lastTap = now
|
||||||
|
state.isPanning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTouchMove(e: TouchEvent) {
|
||||||
|
if (e.touches.length === 1 && state.isPanning) {
|
||||||
|
const t = e.touches[0]
|
||||||
|
schedulePan(t.clientX - state.startX, t.clientY - state.startY)
|
||||||
|
} else if (e.touches.length === 2) {
|
||||||
|
const d = distance(e.touches[0], e.touches[1])
|
||||||
|
const factor = d / (state.pinchStartDistance || d)
|
||||||
|
state.imgScale = clamp(state.pinchStartScale * factor, 0.5, 5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(n: number, min: number, max: number) {
|
||||||
|
return Math.max(min, Math.min(max, n))
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleZoom() {
|
||||||
|
if (state.imgScale === 1) {
|
||||||
|
state.imgScale = 2
|
||||||
|
} else {
|
||||||
|
resetTransform()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomAtCenter(factor: number) {
|
||||||
|
const next = clamp(state.imgScale * factor, 0.5, 5)
|
||||||
|
state.imgScale = next
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetTransform() {
|
||||||
|
state.imgScale = 1
|
||||||
|
state.translateX = 0
|
||||||
|
state.translateY = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function prev() {
|
||||||
|
if (images.value.length <= 1) return
|
||||||
|
currentIndex.value = (currentIndex.value - 1 + images.value.length) % images.value.length
|
||||||
|
resetTransform()
|
||||||
|
}
|
||||||
|
|
||||||
|
function next() {
|
||||||
|
if (images.value.length <= 1) return
|
||||||
|
currentIndex.value = (currentIndex.value + 1) % images.value.length
|
||||||
|
resetTransform()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close on Esc
|
||||||
|
function onKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape') emitClose()
|
||||||
|
if (e.key === 'ArrowLeft') prev()
|
||||||
|
if (e.key === 'ArrowRight') next()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('keydown', onKeydown)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('keydown', onKeydown)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.images,
|
||||||
|
() => {
|
||||||
|
currentIndex.value = clamp(props.startIndex ?? 0, 0, Math.max(0, props.images.length - 1))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sv-image-preview-root {
|
||||||
|
/* Sized and positioned by rootStyle to align with useScale */
|
||||||
|
}
|
||||||
|
.sv-image-preview-toolbar {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
left: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
color: #fff;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.sv-ip-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 34px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.sv-ip-page {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.sv-image-stage {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: grab;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.sv-image {
|
||||||
|
will-change: transform;
|
||||||
|
transition: transform 0.1s ease-out;
|
||||||
|
}
|
||||||
|
.sv-image.no-transition {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
.sv-image-stage.panning {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
.sv-ip-nav {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 42px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.sv-ip-nav.prev {
|
||||||
|
left: 16px;
|
||||||
|
}
|
||||||
|
.sv-ip-nav.next {
|
||||||
|
right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as ImagePreview } from './service.ts'
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { createVNode, render, reactive } from 'vue'
|
||||||
|
import ImagePreviewVue from './ImagePreview.vue'
|
||||||
|
|
||||||
|
type PreviewOptions = {
|
||||||
|
images: string[]
|
||||||
|
startIndex?: number
|
||||||
|
maskClosable?: boolean
|
||||||
|
zIndex?: number
|
||||||
|
// Supply scale metrics provider compatible with useScale
|
||||||
|
getScaleMetrics: () => { scale: number; offsetX: number; offsetY: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
let container: HTMLElement | null = null
|
||||||
|
|
||||||
|
export default function previewImage(options: PreviewOptions) {
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement('div')
|
||||||
|
document.body.appendChild(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = reactive({ visible: true })
|
||||||
|
|
||||||
|
const vnode = createVNode(ImagePreviewVue, {
|
||||||
|
...options,
|
||||||
|
onClose: () => {
|
||||||
|
state.visible = false
|
||||||
|
render(null, container!)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
render(vnode, container)
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<div class="equipment-bar" v-if="!isCollapsed">
|
||||||
|
<img src="@/assets/images/equipment/侧边导航栏.png" alt="" />
|
||||||
|
<div class="equipment-item">
|
||||||
|
<div v-for="(item, index) in menuItems" :key="index" class="" @click="selectItem(index)">
|
||||||
|
<div class="menu-icon">
|
||||||
|
<img v-if="activeIndex === index" src="@/assets/images/equipment/选中按钮.png" alt="" />
|
||||||
|
<img
|
||||||
|
:src="activeIndex === index ? item.activeIcon : item.inactiveIcon"
|
||||||
|
:alt="item.title"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="cursor: pointer"
|
||||||
|
:class="isCollapsed ? 'equipment-arrow-left' : 'equipment-arrow-right'"
|
||||||
|
@click="isCollapsed = !isCollapsed"
|
||||||
|
>
|
||||||
|
<img :src="isCollapsed ? arrowright : arrowleft" alt="" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, withDefaults, defineProps, defineEmits } from 'vue'
|
||||||
|
import arrowleft from '@/assets/images/equipment/按钮-收缩.png'
|
||||||
|
import arrowright from '@/assets/images/equipment/按钮-展开.png'
|
||||||
|
// 定义菜单项接口
|
||||||
|
export interface MenuItem {
|
||||||
|
activeIcon: string // 高亮状态图标路径
|
||||||
|
inactiveIcon: string // 非高亮状态图标路径
|
||||||
|
title: string // 菜单项标题
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义组件属性
|
||||||
|
interface Props {
|
||||||
|
menuItems: MenuItem[] // 菜单项数组
|
||||||
|
defaultActive?: number // 默认选中项索引
|
||||||
|
collapseIcon?: string // 折叠图标
|
||||||
|
expandIcon?: string // 展开图标
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认属性
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
defaultActive: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'item-selected', index: number): void
|
||||||
|
(e: 'toggle', collapsed: boolean): void
|
||||||
|
}>()
|
||||||
|
const isCollapsed = ref(false)
|
||||||
|
// 当前选中项索引
|
||||||
|
const activeIndex = ref(props.defaultActive)
|
||||||
|
|
||||||
|
// 是否折叠
|
||||||
|
const collapsed = ref(false)
|
||||||
|
|
||||||
|
// 选择菜单项
|
||||||
|
const selectItem = (index: number) => {
|
||||||
|
activeIndex.value = index
|
||||||
|
emit('item-selected', index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换折叠状态
|
||||||
|
const toggleCollapse = () => {
|
||||||
|
collapsed.value = !collapsed.value
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.equipment-item {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.equipment-arrow-left {
|
||||||
|
position: absolute;
|
||||||
|
top: 490px;
|
||||||
|
left: -41px;
|
||||||
|
width: 150px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-arrow-right {
|
||||||
|
position: absolute;
|
||||||
|
top: 490px;
|
||||||
|
left: 268px;
|
||||||
|
width: 150px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.menu-icon {
|
||||||
|
display: flex;
|
||||||
|
// justify-content: space-between;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -13,3 +13,6 @@ export * from './ProgressBar'
|
|||||||
|
|
||||||
// 切换组件
|
// 切换组件
|
||||||
export * from './Switcher'
|
export * from './Switcher'
|
||||||
|
|
||||||
|
// 图片预览(函数式)
|
||||||
|
export * from './ImagePreview'
|
||||||
@@ -52,6 +52,23 @@ const routes: RouteRecordRaw[] = [
|
|||||||
title: '视频'
|
title: '视频'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
//技能树
|
||||||
|
{
|
||||||
|
path: '/skill',
|
||||||
|
name: 'Skill',
|
||||||
|
component: () => import('@/views/skill/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '技能树'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/skill/video',
|
||||||
|
name: 'SkillVdieo',
|
||||||
|
component: () => import('@/views/skill/video.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '技能树视频'
|
||||||
|
}
|
||||||
|
},
|
||||||
//中期拍摄
|
//中期拍摄
|
||||||
{
|
{
|
||||||
path: '/midterm',
|
path: '/midterm',
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ const isImageLoading = ref(false)
|
|||||||
// 预加载图片映射
|
// 预加载图片映射
|
||||||
const imageMap = {
|
const imageMap = {
|
||||||
vlog: IMAGE_PATHS.ACTION.VIDEO1,
|
vlog: IMAGE_PATHS.ACTION.VIDEO1,
|
||||||
film: IMAGE_PATHS.ACTION.VIDEO2,
|
film: IMAGE_PATHS.ACTION.VIDEO3,
|
||||||
interview: IMAGE_PATHS.ACTION.VIDEO3
|
interview: IMAGE_PATHS.ACTION.VIDEO2
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预加载所有图片
|
// 预加载所有图片
|
||||||
@@ -63,6 +63,12 @@ const selectedImage = computed(() => {
|
|||||||
return getBackgroundImage(imageMap[selectedModule.value])
|
return getBackgroundImage(imageMap[selectedModule.value])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleSkill = () => {
|
||||||
|
router.push({
|
||||||
|
name: 'Skill'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 生命周期
|
// 生命周期
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
preloadImages()
|
preloadImages()
|
||||||
@@ -90,8 +96,10 @@ onMounted(() => {
|
|||||||
@click.stop="handleClick"
|
@click.stop="handleClick"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
|
<!-- 技能树 按钮 -->
|
||||||
<ImgButton
|
<ImgButton
|
||||||
v-if="status"
|
v-if="status"
|
||||||
|
@click="handleSkill"
|
||||||
:image="IMAGE_PATHS.ACTION.SKILLS_BTN"
|
:image="IMAGE_PATHS.ACTION.SKILLS_BTN"
|
||||||
class="absolute bottom-0 right-0 z-30"
|
class="absolute bottom-0 right-0 z-30"
|
||||||
/>
|
/>
|
||||||
@@ -127,16 +135,5 @@ onMounted(() => {
|
|||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <GuideSteps @complete="handleComplete" @skip="handleSkip">
|
|
||||||
<GuideStepsItem>欢迎来到短视频达人应用!我是你的导学小龙</GuideStepsItem>
|
|
||||||
<GuideStepsItem>
|
|
||||||
现在让我们一起走进
|
|
||||||
<em class="text-red-500">Vlog</em>
|
|
||||||
的世界。简单说,
|
|
||||||
<em class="text-red-500">Vlog</em>
|
|
||||||
就是用视频记录生活的一种方式--它像一本会动的日记能把我们身边的小事、瞬间的心情,通过镜头变得生动又具体。
|
|
||||||
</GuideStepsItem>
|
|
||||||
</GuideSteps> -->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -23,7 +23,7 @@ const currentHighlight = computed(() =>
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const currentStep = ref(0)
|
const currentStep = ref(1)
|
||||||
|
|
||||||
const handleComplete = () => {
|
const handleComplete = () => {
|
||||||
console.log('完成前置条件')
|
console.log('完成前置条件')
|
||||||
@@ -47,6 +47,7 @@ interface SimpleStep {
|
|||||||
const handleStepChangeGui = (_step: SimpleStep | null, index: number) => {
|
const handleStepChangeGui = (_step: SimpleStep | null, index: number) => {
|
||||||
stepIndex.value = index
|
stepIndex.value = index
|
||||||
}
|
}
|
||||||
|
const highlightShow = ref(true)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -76,19 +77,19 @@ const handleStepChangeGui = (_step: SimpleStep | null, index: number) => {
|
|||||||
<ImgButton :image="IMAGE_PATHS.VLOG.TASK_BTN" @click="handleWorkClick" />
|
<ImgButton :image="IMAGE_PATHS.VLOG.TASK_BTN" @click="handleWorkClick" />
|
||||||
</div>
|
</div>
|
||||||
<!-- 前期准备底板 -->
|
<!-- 前期准备底板 -->
|
||||||
<div class="absolute right-90 ml-[135px]">
|
<div v-if="highlightShow" class="absolute right-90 ml-[135px]">
|
||||||
<ImgButton :image="IMAGE_PATHS.VLOG.PREPARE" />
|
<ImgButton :image="IMAGE_PATHS.VLOG.PREPARE" />
|
||||||
</div>
|
</div>
|
||||||
<!-- 脚本撰写按钮 -->
|
<!-- 脚本撰写按钮 -->
|
||||||
<div class="absolute left-[13%] top-1/3">
|
<div v-if="highlightShow" class="absolute left-[13%] top-1/3">
|
||||||
<ImgButton :image="IMAGE_PATHS.VLOG.Scripting" />
|
<ImgButton :image="IMAGE_PATHS.VLOG.Scripting" />
|
||||||
</div>
|
</div>
|
||||||
<!-- 选中箭头 -->
|
<!-- 选中箭头 -->
|
||||||
<div class="absolute left-[13%] top-[37%] -translate-x-1/2">
|
<div v-if="highlightShow" class="absolute left-[13%] top-[37%] -translate-x-1/2">
|
||||||
<ImgButton :image="IMAGE_PATHS.VLOG.SelectArrow" />
|
<ImgButton :image="IMAGE_PATHS.VLOG.SelectArrow" />
|
||||||
</div>
|
</div>
|
||||||
<!-- 进度百分比 -->
|
<!-- 进度百分比 -->
|
||||||
<div class="absolute left-1/2 top-[12%] -translate-x-1/2">
|
<div v-if="highlightShow" class="absolute left-1/2 top-[12%] -translate-x-1/2">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<ImgButton :image="IMAGE_PATHS.VLOG.Progress" />
|
<ImgButton :image="IMAGE_PATHS.VLOG.Progress" />
|
||||||
<ImgButton :image="IMAGE_PATHS.VLOG.Four" />
|
<ImgButton :image="IMAGE_PATHS.VLOG.Four" />
|
||||||
@@ -101,6 +102,7 @@ const handleStepChangeGui = (_step: SimpleStep | null, index: number) => {
|
|||||||
|
|
||||||
<div class="absolute bottom-0 w-full">
|
<div class="absolute bottom-0 w-full">
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
|
v-if="highlightShow"
|
||||||
:current-step="currentStep"
|
:current-step="currentStep"
|
||||||
@complete="handleComplete"
|
@complete="handleComplete"
|
||||||
@step-change="handleStepChange"
|
@step-change="handleStepChange"
|
||||||
@@ -118,6 +120,14 @@ const handleStepChangeGui = (_step: SimpleStep | null, index: number) => {
|
|||||||
</GuideStepsItem>
|
</GuideStepsItem>
|
||||||
</GuideSteps>
|
</GuideSteps>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="highlightShow"
|
||||||
|
class="absolute right-0 top-0"
|
||||||
|
style="z-index: 998; cursor: pointer"
|
||||||
|
@click="highlightShow = false"
|
||||||
|
>
|
||||||
|
<img src="@/assets/images/midterm/叉.png" alt="" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style>
|
<style>
|
||||||
.guide-ui {
|
.guide-ui {
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-cover bg-center bg-no-repeat equipment-bg"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.EQUIPMENT.BG) }"
|
||||||
|
>
|
||||||
|
<Sidebar
|
||||||
|
:menu-items="menuItems"
|
||||||
|
:default-active="defaultActive"
|
||||||
|
@item-selected="handleMenuItemSelected"
|
||||||
|
@toggle="handleSidebarToggle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="tsx">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import Sidebar from '@/components/Sidebar/Sidebar.vue'
|
||||||
|
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
|
||||||
|
import equipmentPhone from '@/assets/images/equipment/手机-亮.png'
|
||||||
|
import equipmentPhone2 from '@/assets/images/equipment/手机-暗.png'
|
||||||
|
const menuItems = ref([
|
||||||
|
{
|
||||||
|
activeIcon: equipmentPhone,
|
||||||
|
inactiveIcon: equipmentPhone2,
|
||||||
|
title: '手机'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
activeIcon: equipmentPhone,
|
||||||
|
inactiveIcon: equipmentPhone2,
|
||||||
|
title: '手机2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
activeIcon: equipmentPhone,
|
||||||
|
inactiveIcon: equipmentPhone2,
|
||||||
|
title: '手机3'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
const defaultActive = ref(0)
|
||||||
|
const handleMenuItemSelected = (index: number) => {
|
||||||
|
defaultActive.value = index
|
||||||
|
console.log('选中了菜单项:', index)
|
||||||
|
// 根据选中的菜单项更新页面内容
|
||||||
|
}
|
||||||
|
const handleSidebarToggle = (collapsed: boolean) => {
|
||||||
|
console.log('侧边栏状态:', collapsed ? '已折叠' : '已展开')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less"></style>
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
<script lang="tsx" setup>
|
||||||
|
defineOptions({ name: 'SkillPage' })
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
|
||||||
|
import { ImgButton } from '@/components/Button'
|
||||||
|
import { GuideSteps } from '@/components/Guide'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const stepIndex = ref(0)
|
||||||
|
const selected = ref(false)
|
||||||
|
const sideStatus = ref(true)
|
||||||
|
|
||||||
|
const status = ref(false)
|
||||||
|
|
||||||
|
const selectedButton = ref('拍摄基础')
|
||||||
|
//按钮 {key: '拍摄基础', value: '拍摄基础'}
|
||||||
|
const buttons = ref([
|
||||||
|
{ key: '拍摄基础', value: '拍摄基础' },
|
||||||
|
{ key: '内容策划', value: '内容策划' },
|
||||||
|
{ key: '拍摄执行', value: '拍摄执行' },
|
||||||
|
{ key: '后期制作', value: '后期制作' },
|
||||||
|
{ key: '版权伦理意识', value: '版权伦理意识' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 预留:按钮选中状态(如需启用再恢复逻辑)
|
||||||
|
|
||||||
|
//引导步骤
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
id: 'step1',
|
||||||
|
content: '可以点击左右箭头切换到对应模块的知识树哦'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 预留:模块按钮(如需启用再补充实现)
|
||||||
|
|
||||||
|
const currentHighlight = computed(() =>
|
||||||
|
stepIndex.value === 0 ? '.img-switch-item--vlog' : '.work-border'
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectedImage = computed(() => {
|
||||||
|
return selected.value ? 'skill/画面构图选中' : 'skill/画面构图未选中'
|
||||||
|
})
|
||||||
|
|
||||||
|
interface SimpleStep {
|
||||||
|
id: string
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStepChange = (_step: SimpleStep | null, index: number) => {
|
||||||
|
stepIndex.value = index
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
selected.value = true
|
||||||
|
router.push({
|
||||||
|
name: 'SkillVdieo'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
status.value = true
|
||||||
|
}, 1000)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full relative font-semibold">
|
||||||
|
<!-- 背景图片-center -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
|
:style="{ backgroundImage: getBackgroundImage('skill/技能树背景') }"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<ImgButton class="absolute left-0 top-1/2 -translate-y-1/2" :image="'skill/左滑'"> </ImgButton>
|
||||||
|
|
||||||
|
<ImgButton
|
||||||
|
@click="sideStatus = false"
|
||||||
|
class="absolute right-0 top-1/2 -translate-y-1/2"
|
||||||
|
:image="'skill/右滑'"
|
||||||
|
>
|
||||||
|
</ImgButton>
|
||||||
|
|
||||||
|
<!-- 周围模块标签(静态绘制) -->
|
||||||
|
<div v-show="sideStatus" class="module module-left-bottom module-vertical">
|
||||||
|
<div
|
||||||
|
:class="['module-text blue-text', status ? 'text-white' : 'text-[#969696e6_!important]']"
|
||||||
|
>
|
||||||
|
拍摄基础
|
||||||
|
</div>
|
||||||
|
<div :class="['module-percent', status ? 'red-text' : 'blue-text']">0%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="sideStatus" class="module module-left-middle module-vertical">
|
||||||
|
<div
|
||||||
|
:class="['module-text blue-text', status ? 'text-white' : 'text-[#969696e6_!important]']"
|
||||||
|
>
|
||||||
|
内容策划
|
||||||
|
</div>
|
||||||
|
<div :class="['module-percent', status ? 'red-text' : 'blue-text']">0%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="!sideStatus" class="module module-right-middle module-vertical">
|
||||||
|
<div
|
||||||
|
:class="['module-text blue-text', status ? 'text-white' : 'text-[#969696e6_!important]']"
|
||||||
|
>
|
||||||
|
拍摄执行
|
||||||
|
</div>
|
||||||
|
<div :class="['module-percent', status ? 'red-text' : 'blue-text']">0%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="sideStatus" class="module module-right-upper module-vertical">
|
||||||
|
<div
|
||||||
|
:class="['module-text blue-text', status ? 'text-white' : 'text-[#969696e6_!important]']"
|
||||||
|
>
|
||||||
|
后期制作
|
||||||
|
</div>
|
||||||
|
<div :class="['module-percent', status ? 'red-text' : 'blue-text']">0%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="sideStatus" class="module module-right-far module-vertical">
|
||||||
|
<div
|
||||||
|
:class="['module-text blue-text', status ? 'text-white' : 'text-[#969696e6_!important]']"
|
||||||
|
>
|
||||||
|
版权伦理意识
|
||||||
|
</div>
|
||||||
|
<div :class="['module-percent', status ? 'red-text' : 'blue-text']">0%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 画面构图 -->
|
||||||
|
<ImgButton
|
||||||
|
v-show="!sideStatus"
|
||||||
|
@click="handleClick"
|
||||||
|
class="absolute top-[38%] left-[23%] z-10"
|
||||||
|
:image="selectedImage"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 景别 -->
|
||||||
|
<ImgButton v-show="!sideStatus" class="absolute top-[15%] left-[33%] z-10" image="skill/景别" />
|
||||||
|
|
||||||
|
<!-- 返回 -->
|
||||||
|
<ImgButton
|
||||||
|
class="absolute right-0 top-0"
|
||||||
|
:image="IMAGE_PATHS.COMPONENTS.BACK"
|
||||||
|
@click="handleClose"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 树 -->
|
||||||
|
<div
|
||||||
|
class="absolute top-28 left-[calc(50%-30px)] -translate-x-1/2 w-[786px] h-[875px] bg-cover bg-center bg-no-repeat"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: status
|
||||||
|
? getBackgroundImage('skill/技能树-亮')
|
||||||
|
: getBackgroundImage('skill/技能树-灰')
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div class="absolute bottom-28 left-1/2 -translate-x-1/2">
|
||||||
|
<span class="blue-text tracking-[18px] text-5xl">
|
||||||
|
{{ !sideStatus ? '技能树-vlog' : '技能树-新手启航' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="absolute bottom-10 left-1/2 -translate-x-1/2">
|
||||||
|
<span class="red-text text-4xl">{{
|
||||||
|
status ? '当前尚未收集任何知识点,快去学习收集知识点吧' : '当前已全部解锁'
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 引导与高亮 -->
|
||||||
|
<GuideSteps
|
||||||
|
v-if="status"
|
||||||
|
:steps="steps"
|
||||||
|
:highlight-selector="currentHighlight"
|
||||||
|
:highlight-padding="12"
|
||||||
|
:highlight-radius="20"
|
||||||
|
@step-change="handleStepChange"
|
||||||
|
position-offset-y="85%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 强发光蓝绿主标题(图2风格) */
|
||||||
|
.blue-text {
|
||||||
|
/* 文字本体采用蓝绿渐变并保持白色高亮视觉 */
|
||||||
|
background: linear-gradient(180deg, #cfffff 0%, #9ff3ff 40%, #6ee7ff 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
/* 多层发光,柔边外扩 */
|
||||||
|
text-shadow:
|
||||||
|
0 0 2px rgba(255, 255, 255, 0.9),
|
||||||
|
0 0 6px rgba(79, 241, 255, 0.9),
|
||||||
|
0 0 14px rgba(79, 241, 255, 0.8),
|
||||||
|
0 0 28px rgba(0, 199, 255, 0.6),
|
||||||
|
0 0 46px rgba(0, 199, 255, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 强发光白粉副标题(图2风格) */
|
||||||
|
.red-text {
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #ffe9e9 55%, #ffd1d1 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
text-shadow:
|
||||||
|
0 0 2px rgba(255, 255, 255, 0.9),
|
||||||
|
0 0 8px rgba(255, 135, 135, 0.85),
|
||||||
|
0 0 18px rgba(255, 135, 135, 0.65),
|
||||||
|
0 0 32px rgba(255, 110, 110, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 模块标签基础样式 */
|
||||||
|
.module {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 竖排布局 */
|
||||||
|
.module-vertical {
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
.module-vertical .module-text {
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
text-orientation: upright;
|
||||||
|
}
|
||||||
|
.module-vertical .module-percent {
|
||||||
|
transform: translateY(6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-text {
|
||||||
|
/* color: rgba(150, 150, 150, 0.9); */
|
||||||
|
font-size: 36px;
|
||||||
|
letter-spacing: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-percent {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 位置近似图2布局 */
|
||||||
|
.module-left-bottom {
|
||||||
|
left: 10%;
|
||||||
|
bottom: 22%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-left-middle {
|
||||||
|
left: 25%;
|
||||||
|
top: 48%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-right-middle {
|
||||||
|
right: 26%;
|
||||||
|
top: 52%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-right-upper {
|
||||||
|
right: 18%;
|
||||||
|
top: 42%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-right-far {
|
||||||
|
right: 8%;
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import ImgButton from '@/components/Button/ImgButton.vue'
|
||||||
|
import { IMAGE_PATHS } from '@/utils'
|
||||||
|
import six from '/video/新手导航_九种构图法.mp4'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const videoRef = ref()
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePlay = () => {
|
||||||
|
if (!videoRef.value) return
|
||||||
|
videoRef.value.muted = false
|
||||||
|
videoRef.value.volume = 1
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full relative bg-white/10" @click="handlePlay">
|
||||||
|
<!-- 返回 -->
|
||||||
|
<ImgButton
|
||||||
|
class="absolute right-0 top-0 z-10"
|
||||||
|
:image="IMAGE_PATHS.COMPONENTS.BACK"
|
||||||
|
@click="handleClose"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<video ref="videoRef" :src="six" controls></video>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
/* 核心:保持视频比例,多余部分被裁剪 */
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -27,6 +27,10 @@ const handleWorkClick = () => {
|
|||||||
router.push({ name: 'Work' })
|
router.push({ name: 'Work' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (route.query.key === 'video') {
|
if (route.query.key === 'video') {
|
||||||
currentStep.value = 1
|
currentStep.value = 1
|
||||||
@@ -42,6 +46,13 @@ onMounted(() => {
|
|||||||
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BG_CENTER) }"
|
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BG_CENTER) }"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
|
<!-- 关闭 -->
|
||||||
|
<ImgButton
|
||||||
|
class="absolute right-0 top-0 z-20"
|
||||||
|
:image="IMAGE_PATHS.COMPONENTS.CLOSE"
|
||||||
|
@click="handleClose"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 背景图片 -->
|
<!-- 背景图片 -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const handleVideoChange = () => {
|
|||||||
steps.value = [
|
steps.value = [
|
||||||
{
|
{
|
||||||
id: 'step1',
|
id: 'step1',
|
||||||
content: '已完成专业剖析,即可查看脚本'
|
content: '恭喜你,已完成专业剖析的学习,点击【查看脚本】回顾视频脚本吧!'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<script lang="tsx" setup>
|
<script lang="tsx" name="Work" setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
defineOptions({ name: 'Work' })
|
||||||
|
import { ref, computed, onMounted, onActivated } from 'vue'
|
||||||
import { getBackgroundImage, IMAGE_PATHS, getStaticImage } from '@/utils'
|
import { getBackgroundImage, IMAGE_PATHS, getStaticImage } from '@/utils'
|
||||||
import { ImgButton } from '@/components/Button'
|
import { ImgButton } from '@/components/Button'
|
||||||
import WorkSideSwitcher from './components/WorkSideSwitcher.vue'
|
import WorkSideSwitcher from './components/WorkSideSwitcher.vue'
|
||||||
import { GuideSteps } from '@/components/Guide'
|
import { GuideSteps } from '@/components/Guide'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { useScale } from '@/composables/useScale'
|
||||||
|
import { ImagePreview } from '@/components'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -91,7 +94,22 @@ const handleClose = () => {
|
|||||||
//状态是否完成前置
|
//状态是否完成前置
|
||||||
const isComplete = ref(false)
|
const isComplete = ref(false)
|
||||||
|
|
||||||
onMounted(() => {
|
const handleWorkClick = () => {
|
||||||
|
if (!isComplete.value) return
|
||||||
|
openImages([getStaticImage('work/桂花酒酿小圆子')])
|
||||||
|
}
|
||||||
|
const { scale, offsetX, offsetY } = useScale()
|
||||||
|
|
||||||
|
function openImages(urls: string[], startIndex = 0) {
|
||||||
|
ImagePreview({
|
||||||
|
images: urls,
|
||||||
|
startIndex,
|
||||||
|
maskClosable: true,
|
||||||
|
getScaleMetrics: () => ({ scale: scale.value, offsetX: offsetX.value, offsetY: offsetY.value })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
if (route.query.key === 'work') {
|
if (route.query.key === 'work') {
|
||||||
selected.value = 'profession'
|
selected.value = 'profession'
|
||||||
isComplete.value = true
|
isComplete.value = true
|
||||||
@@ -153,6 +171,7 @@ onMounted(() => {
|
|||||||
<ImgButton
|
<ImgButton
|
||||||
class="absolute right-14 bottom-10"
|
class="absolute right-14 bottom-10"
|
||||||
:image="isComplete ? IMAGE_PATHS.WORK.TASK_BTN_OK : IMAGE_PATHS.WORK.TASK_BTN"
|
:image="isComplete ? IMAGE_PATHS.WORK.TASK_BTN_OK : IMAGE_PATHS.WORK.TASK_BTN"
|
||||||
|
@click="handleWorkClick"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 引导与高亮 -->
|
<!-- 引导与高亮 -->
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ export default defineConfig(({ mode }) => {
|
|||||||
'@': resolve(__dirname, 'src')
|
'@': resolve(__dirname, 'src')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0'
|
||||||
|
},
|
||||||
build: {
|
build: {
|
||||||
assetsDir: 'assets',
|
assetsDir: 'assets',
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
|
|||||||