Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c170b31f3a |
@ -20,6 +20,14 @@ body {
|
|||||||
height: 600px;
|
height: 600px;
|
||||||
margin-bottom: 50px;
|
margin-bottom: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端 Banner 自适应高度 */
|
||||||
|
@media screen and (max-width: 999px) {
|
||||||
|
.banner {
|
||||||
|
height: auto;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
.page {
|
.page {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@ -19,6 +19,7 @@ const props = defineProps({
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.logo {
|
.logo {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: auto;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
125
src/components/Swiper/composable.js
Normal file
125
src/components/Swiper/composable.js
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import {ref, computed, watch, nextTick, getCurrentInstance, onUnmounted} from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swiper 公用逻辑 composable
|
||||||
|
* @param {object} props - 组件 props
|
||||||
|
* @param {object} model - v-model ref (currentPage)
|
||||||
|
* @param {function} emit - emit 函数
|
||||||
|
*/
|
||||||
|
export function useSwiper(props, model, emit) {
|
||||||
|
const instance = getCurrentInstance()
|
||||||
|
const $fontSize = instance.appContext.config.globalProperties.$fontSize
|
||||||
|
const trackRef = ref(null)
|
||||||
|
let autoPlayTimer = null
|
||||||
|
|
||||||
|
/* ---------- 计算属性 ---------- */
|
||||||
|
const itemWidth = computed(() => $fontSize(props.sourceWidth))
|
||||||
|
const itemHeight = computed(() => $fontSize(props.sourceHeight))
|
||||||
|
const itemGap = computed(() => $fontSize(props.sourceGap))
|
||||||
|
const itemTotalWidth = computed(() => itemWidth.value + itemGap.value)
|
||||||
|
const viewPortWidth = computed(() => props.pageSize * itemWidth.value + (props.pageSize - 1) * itemGap.value + 'px')
|
||||||
|
const trackWidth = computed(() => props.data.length * itemTotalWidth.value - itemGap.value + 'px')
|
||||||
|
const totalPages = computed(() => props.data.length - props.pageSize + 1)
|
||||||
|
|
||||||
|
/* ---------- 方法 ---------- */
|
||||||
|
function clickTitle() {
|
||||||
|
emit('title-click')
|
||||||
|
}
|
||||||
|
|
||||||
|
function translate(page) {
|
||||||
|
if (!trackRef.value) return
|
||||||
|
trackRef.value.style.transition = `transform ${props.transitionDuration}ms ease`
|
||||||
|
trackRef.value.style.transform = `translateX(-${itemTotalWidth.value * (page - 1)}px)`
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToPage(page) {
|
||||||
|
model.value = Number(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextPage() {
|
||||||
|
const next = model.value < totalPages.value ? model.value + 1 : 1
|
||||||
|
goToPage(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevPage() {
|
||||||
|
const prev = model.value > 1 ? model.value - 1 : totalPages.value
|
||||||
|
goToPage(prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleItemClick(item, index) {
|
||||||
|
emit('item-click', item, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleItemHover(item, index) {
|
||||||
|
clearInterval(autoPlayTimer)
|
||||||
|
autoPlayTimer = null
|
||||||
|
emit('item-hover', item, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleItemHoverLeave() {
|
||||||
|
startAutoPlay()
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAutoPlay() {
|
||||||
|
if (!props.autoPlay) return
|
||||||
|
stopAutoPlay()
|
||||||
|
autoPlayTimer = setInterval(nextPage, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAutoPlay() {
|
||||||
|
if (autoPlayTimer) {
|
||||||
|
clearInterval(autoPlayTimer)
|
||||||
|
autoPlayTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端触摸事件 */
|
||||||
|
function handleItemTouchStart(item, index) {
|
||||||
|
clearInterval(autoPlayTimer)
|
||||||
|
autoPlayTimer = null
|
||||||
|
emit('item-hover', item, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleItemTouchEnd() {
|
||||||
|
startAutoPlay()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 监听器 ---------- */
|
||||||
|
watch(model, (newVal, oldVal) => {
|
||||||
|
if (newVal !== oldVal) {
|
||||||
|
nextTick(() => translate(newVal))
|
||||||
|
}
|
||||||
|
}, {immediate: true})
|
||||||
|
|
||||||
|
watch(() => props.autoPlay, (autoPlay) => {
|
||||||
|
autoPlay ? startAutoPlay() : stopAutoPlay()
|
||||||
|
}, {immediate: true})
|
||||||
|
|
||||||
|
watch(() => props.data.length, (newLen, oldLen) => {
|
||||||
|
if (newLen !== oldLen && model.value !== 1) {
|
||||||
|
model.value = 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(stopAutoPlay)
|
||||||
|
|
||||||
|
return {
|
||||||
|
trackRef,
|
||||||
|
itemWidth,
|
||||||
|
itemHeight,
|
||||||
|
itemGap,
|
||||||
|
itemTotalWidth,
|
||||||
|
viewPortWidth,
|
||||||
|
trackWidth,
|
||||||
|
totalPages,
|
||||||
|
clickTitle,
|
||||||
|
goToPage,
|
||||||
|
nextPage,
|
||||||
|
prevPage,
|
||||||
|
handleItemClick,
|
||||||
|
handleItemHover,
|
||||||
|
handleItemHoverLeave,
|
||||||
|
handleItemTouchStart,
|
||||||
|
handleItemTouchEnd,
|
||||||
|
}
|
||||||
|
}
|
||||||
183
src/components/Swiper/mobile/index.vue
Normal file
183
src/components/Swiper/mobile/index.vue
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
<script setup>
|
||||||
|
import {useSwiper} from '../composable.js'
|
||||||
|
|
||||||
|
const currentPage = defineModel({type: [Number, String], default: 1})
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: {type: String, required: true},
|
||||||
|
title: {type: String, default: ''},
|
||||||
|
data: {type: Array, default: () => [1]},
|
||||||
|
pageSize: {type: [Number, String], default: 1},
|
||||||
|
showDot: {type: Boolean, default: true},
|
||||||
|
showPagination: {type: Boolean, default: true},
|
||||||
|
showHover: {type: Boolean, default: false},
|
||||||
|
autoPlay: {type: Boolean, default: false},
|
||||||
|
transitionDuration: {type: Number, default: 300},
|
||||||
|
sourceWidth: {type: Number, default: 276},
|
||||||
|
sourceHeight: {type: Number, default: 355},
|
||||||
|
sourceGap: {type: Number, default: 20},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['page-change', 'item-click', 'item-hover', 'title-click'])
|
||||||
|
|
||||||
|
const {
|
||||||
|
trackRef,
|
||||||
|
itemWidth,
|
||||||
|
itemHeight,
|
||||||
|
itemGap,
|
||||||
|
itemTotalWidth,
|
||||||
|
viewPortWidth,
|
||||||
|
trackWidth,
|
||||||
|
totalPages,
|
||||||
|
clickTitle,
|
||||||
|
goToPage,
|
||||||
|
nextPage,
|
||||||
|
prevPage,
|
||||||
|
handleItemClick,
|
||||||
|
handleItemHover,
|
||||||
|
handleItemHoverLeave,
|
||||||
|
handleItemTouchStart,
|
||||||
|
handleItemTouchEnd,
|
||||||
|
} = useSwiper(props, currentPage, emit)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section :id="id" class="generic-carousel">
|
||||||
|
<!-- 标题 -->
|
||||||
|
<slot name="title">
|
||||||
|
<h2 v-if="title" class="carousel-title" @click="clickTitle">{{ title }}</h2>
|
||||||
|
</slot>
|
||||||
|
|
||||||
|
<!-- 分页指示器 -->
|
||||||
|
<div v-if="showDot" class="carousel-pagination">
|
||||||
|
<button
|
||||||
|
v-for="page in totalPages"
|
||||||
|
:key="page"
|
||||||
|
:class="['pagination-dot', {'is-active': page === currentPage}]"
|
||||||
|
@click="goToPage(page)"
|
||||||
|
:aria-label="`切换到第${page}页`"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 导航按钮 -->
|
||||||
|
<div v-if="showPagination && totalPages > 1" class="carousel-navigation flex j-s">
|
||||||
|
<button class="nav-btn prev-btn" @click="prevPage" aria-label="上一页"><</button>
|
||||||
|
<button class="nav-btn next-btn" @click="nextPage" aria-label="下一页">></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 轮播内容区域 -->
|
||||||
|
<div class="carousel-content">
|
||||||
|
<div class="carousel-viewport" :style="{width: viewPortWidth}">
|
||||||
|
<div
|
||||||
|
ref="trackRef"
|
||||||
|
class="carousel-track"
|
||||||
|
:style="{width: trackWidth}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in data"
|
||||||
|
:key="index"
|
||||||
|
class="carousel-item"
|
||||||
|
:style="{
|
||||||
|
width: itemWidth + 'px',
|
||||||
|
height: itemHeight + 'px',
|
||||||
|
marginRight: index < data.length - 1 ? itemGap + 'px' : '0',
|
||||||
|
}"
|
||||||
|
@click="handleItemClick(item, index)"
|
||||||
|
@mouseenter="handleItemHover(item, index)"
|
||||||
|
@mouseleave="handleItemHoverLeave(item, index)"
|
||||||
|
@touchstart="handleItemTouchStart(item, index)"
|
||||||
|
@touchend="handleItemTouchEnd"
|
||||||
|
>
|
||||||
|
<slot :item="item" :index="index" :isActive="currentPage === Math.ceil((index + 1) / pageSize)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.generic-carousel {
|
||||||
|
padding: 20px 0;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.carousel-title {
|
||||||
|
font-family: 'PingFang SC';
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-pagination {
|
||||||
|
width: 64px;
|
||||||
|
height: 4px;
|
||||||
|
margin: 12px auto 20px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.pagination-dot {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
background: #d9d9d9;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background: #0389ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-navigation {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0 8px; /* no */
|
||||||
|
z-index: 10;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.nav-btn {
|
||||||
|
pointer-events: auto;
|
||||||
|
width: 32px; /* no */
|
||||||
|
height: 32px; /* no */
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-content {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-viewport {
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-track {
|
||||||
|
display: flex;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
will-change: transform;
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-item {
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,7 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {ref, computed, watch, nextTick, getCurrentInstance, onUnmounted} from 'vue'
|
import {useSwiper} from '../composable.js'
|
||||||
|
|
||||||
/* -------------------- 组件接口定义 -------------------- */
|
|
||||||
const currentPage = defineModel({type: [Number, String], default: 1})
|
const currentPage = defineModel({type: [Number, String], default: 1})
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -21,145 +20,23 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['page-change', 'item-click', 'item-hover', 'title-click'])
|
const emit = defineEmits(['page-change', 'item-click', 'item-hover', 'title-click'])
|
||||||
|
|
||||||
/* -------------------- 响应式变量 -------------------- */
|
const {
|
||||||
const instance = getCurrentInstance()
|
trackRef,
|
||||||
const $fontSize = instance.appContext.config.globalProperties.$fontSize
|
itemWidth,
|
||||||
const trackRef = ref(null)
|
itemHeight,
|
||||||
let autoPlayTimer = null
|
itemGap,
|
||||||
|
itemTotalWidth,
|
||||||
/* -------------------- 计算属性 -------------------- */
|
viewPortWidth,
|
||||||
const itemWidth = computed(() => $fontSize(props.sourceWidth))
|
trackWidth,
|
||||||
const itemHeight = computed(() => $fontSize(props.sourceHeight))
|
totalPages,
|
||||||
const itemGap = computed(() => $fontSize(props.sourceGap))
|
clickTitle,
|
||||||
|
goToPage,
|
||||||
const itemTotalWidth = computed(() => itemWidth.value + itemGap.value)
|
nextPage,
|
||||||
const viewPortWidth = computed(() => props.pageSize * itemWidth.value + (props.pageSize - 1) * itemGap.value + 'px')
|
prevPage,
|
||||||
const trackWidth = computed(() => props.data.length * itemTotalWidth.value - itemGap.value + 'px')
|
handleItemClick,
|
||||||
const totalPages = computed(() => props.data.length - props.pageSize + 1)
|
handleItemHover,
|
||||||
|
handleItemHoverLeave,
|
||||||
/* -------------------- 方法 -------------------- */
|
} = useSwiper(props, currentPage, emit)
|
||||||
/**
|
|
||||||
* 标题点击事件
|
|
||||||
*/
|
|
||||||
const clickTitle = () => {
|
|
||||||
emit('title-click')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行平移动画
|
|
||||||
* @param {number} page - 目标页码
|
|
||||||
*/
|
|
||||||
const translate = (page) => {
|
|
||||||
if (!trackRef.value) return
|
|
||||||
trackRef.value.style.transition = `transform ${props.transitionDuration}ms ease`
|
|
||||||
trackRef.value.style.transform = `translateX(-${itemTotalWidth.value * (page - 1)}px)`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 切换到指定页码
|
|
||||||
* @param {number} page - 目标页码
|
|
||||||
*/
|
|
||||||
const goToPage = (page) => {
|
|
||||||
currentPage.value = Number(page)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 切换到下一页,循环播放
|
|
||||||
*/
|
|
||||||
const nextPage = () => {
|
|
||||||
const nextPage = currentPage.value < totalPages.value ? currentPage.value + 1 : 1
|
|
||||||
goToPage(nextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 切换到上一页,循环播放
|
|
||||||
*/
|
|
||||||
const prevPage = () => {
|
|
||||||
const prevPage = currentPage.value > 1 ? currentPage.value - 1 : totalPages.value
|
|
||||||
goToPage(prevPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理项目点击事件
|
|
||||||
* @param {Object} item - 项目数据
|
|
||||||
* @param {number} index - 项目索引
|
|
||||||
*/
|
|
||||||
const handleItemClick = (item, index) => {
|
|
||||||
emit('item-click', item, index)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理项目悬停事件
|
|
||||||
* @param {Object} item - 项目数据
|
|
||||||
* @param {number} index - 项目索引
|
|
||||||
*/
|
|
||||||
const handleItemHover = (item, index) => {
|
|
||||||
clearInterval(autoPlayTimer)
|
|
||||||
autoPlayTimer = null
|
|
||||||
emit('item-hover', item, index)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理项目鼠标移出事件
|
|
||||||
* @param {Object} item - 项目数据
|
|
||||||
* @param {number} index - 项目索引
|
|
||||||
*/
|
|
||||||
const handleItemHoverLeave = () => {
|
|
||||||
startAutoPlay()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动自动播放
|
|
||||||
*/
|
|
||||||
const startAutoPlay = () => {
|
|
||||||
if (!props.autoPlay) return
|
|
||||||
stopAutoPlay()
|
|
||||||
autoPlayTimer = setInterval(nextPage, 3000)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 停止自动播放
|
|
||||||
*/
|
|
||||||
const stopAutoPlay = () => {
|
|
||||||
if (autoPlayTimer) {
|
|
||||||
clearInterval(autoPlayTimer)
|
|
||||||
autoPlayTimer = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------- 监听器 -------------------- */
|
|
||||||
// 监听页码变化,执行平移动画
|
|
||||||
watch(
|
|
||||||
currentPage,
|
|
||||||
(newVal, oldVal) => {
|
|
||||||
if (newVal !== oldVal) {
|
|
||||||
nextTick(() => translate(newVal))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{immediate: true}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听自动播放设置变化
|
|
||||||
watch(
|
|
||||||
() => props.autoPlay,
|
|
||||||
(autoPlay) => {
|
|
||||||
autoPlay ? startAutoPlay() : stopAutoPlay()
|
|
||||||
},
|
|
||||||
{immediate: true}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听数据变化,重置到第一页
|
|
||||||
watch(
|
|
||||||
() => props.data.length,
|
|
||||||
(newLen, oldLen) => {
|
|
||||||
if (newLen !== oldLen && currentPage.value !== 1) {
|
|
||||||
currentPage.value = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 组件卸载时清理定时器
|
|
||||||
onUnmounted(stopAutoPlay)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -168,7 +45,6 @@ onUnmounted(stopAutoPlay)
|
|||||||
<slot name="title">
|
<slot name="title">
|
||||||
<h2 v-if="title" class="carousel-title" @click="clickTitle">{{ title }}</h2>
|
<h2 v-if="title" class="carousel-title" @click="clickTitle">{{ title }}</h2>
|
||||||
</slot>
|
</slot>
|
||||||
<!-- <h2 v-if="title" class="carousel-title">{{ title }}</h2> -->
|
|
||||||
|
|
||||||
<!-- 分页指示器 -->
|
<!-- 分页指示器 -->
|
||||||
<div v-if="showDot" class="carousel-pagination">
|
<div v-if="showDot" class="carousel-pagination">
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import {createWebHashHistory, createRouter} from 'vue-router'
|
import {createWebHashHistory, createRouter} from 'vue-router'
|
||||||
import mobileRoutes from './mobile.js'
|
import mobileRoutes from './mobile.js'
|
||||||
|
|
||||||
import HomeView from '@/views/homepage/index.vue'
|
import HomeView from '@/views/pc/homepage/index.vue'
|
||||||
|
|
||||||
const pcRoutes = [
|
const pcRoutes = [
|
||||||
// ============================================ 首页 ============================================
|
// ============================================ 首页 ============================================
|
||||||
@ -11,7 +11,7 @@ const pcRoutes = [
|
|||||||
{
|
{
|
||||||
path: '/product/hardwareSystem',
|
path: '/product/hardwareSystem',
|
||||||
name: 'HardwareSystem',
|
name: 'HardwareSystem',
|
||||||
component: () => import('@/views/product/hardwareSystem.vue'),
|
component: () => import('@/views/pc/product/hardwareSystem.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '低空监管体系',
|
title: '低空监管体系',
|
||||||
},
|
},
|
||||||
@ -19,7 +19,7 @@ const pcRoutes = [
|
|||||||
{
|
{
|
||||||
path: '/product/softwareSystem',
|
path: '/product/softwareSystem',
|
||||||
name: 'SoftwareSystem',
|
name: 'SoftwareSystem',
|
||||||
component: () => import('@/views/product/softwareSystem.vue'),
|
component: () => import('@/views/pc/product/softwareSystem.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '低空远程识别设备',
|
title: '低空远程识别设备',
|
||||||
},
|
},
|
||||||
@ -27,7 +27,7 @@ const pcRoutes = [
|
|||||||
{
|
{
|
||||||
path: '/product/detail',
|
path: '/product/detail',
|
||||||
name: 'ProductDetail',
|
name: 'ProductDetail',
|
||||||
component: () => import('@/views/product/detail.vue'),
|
component: () => import('@/views/pc/product/detail.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '产品详情',
|
title: '产品详情',
|
||||||
},
|
},
|
||||||
@ -37,7 +37,7 @@ const pcRoutes = [
|
|||||||
{
|
{
|
||||||
path: '/services',
|
path: '/services',
|
||||||
name: 'Services',
|
name: 'Services',
|
||||||
component: () => import('@/views/services/index.vue'),
|
component: () => import('@/views/pc/services/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '服务与支撑',
|
title: '服务与支撑',
|
||||||
},
|
},
|
||||||
@ -47,7 +47,7 @@ const pcRoutes = [
|
|||||||
{
|
{
|
||||||
path: '/news',
|
path: '/news',
|
||||||
name: 'News',
|
name: 'News',
|
||||||
component: () => import('@/views/news/index.vue'),
|
component: () => import('@/views/pc/news/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '新闻中心',
|
title: '新闻中心',
|
||||||
},
|
},
|
||||||
@ -55,7 +55,7 @@ const pcRoutes = [
|
|||||||
{
|
{
|
||||||
path: '/news/detail',
|
path: '/news/detail',
|
||||||
name: 'NewsDetail',
|
name: 'NewsDetail',
|
||||||
component: () => import('@/views/news/detail.vue'),
|
component: () => import('@/views/pc/news/detail.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '新闻详情',
|
title: '新闻详情',
|
||||||
},
|
},
|
||||||
@ -65,7 +65,7 @@ const pcRoutes = [
|
|||||||
{
|
{
|
||||||
path: '/about',
|
path: '/about',
|
||||||
name: 'About',
|
name: 'About',
|
||||||
component: () => import('@/views/about/index.vue'),
|
component: () => import('@/views/pc/about/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '关于我们',
|
title: '关于我们',
|
||||||
},
|
},
|
||||||
@ -75,7 +75,7 @@ const pcRoutes = [
|
|||||||
{
|
{
|
||||||
path: '/link',
|
path: '/link',
|
||||||
name: 'Link',
|
name: 'Link',
|
||||||
component: () => import('@/views/link/index.vue'),
|
component: () => import('@/views/pc/link/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '联系我们',
|
title: '联系我们',
|
||||||
},
|
},
|
||||||
@ -85,7 +85,7 @@ const pcRoutes = [
|
|||||||
{
|
{
|
||||||
path: '/download',
|
path: '/download',
|
||||||
name: 'Download',
|
name: 'Download',
|
||||||
component: () => import('@/views/download/index.vue'),
|
component: () => import('@/views/pc/download/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '下载中心',
|
title: '下载中心',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,26 +3,26 @@ const mobileRoutes = [
|
|||||||
{
|
{
|
||||||
path: '/m/homepage',
|
path: '/m/homepage',
|
||||||
name: 'MobileHomepage',
|
name: 'MobileHomepage',
|
||||||
component: () => import('@/views/homepage/mobile.vue'),
|
component: () => import('@/views/mobile/homepage/index.vue'),
|
||||||
},
|
},
|
||||||
|
|
||||||
// ============================================ 产品中心 ============================================
|
// ============================================ 产品中心 ============================================
|
||||||
{
|
{
|
||||||
path: '/m/product/hardwareSystem',
|
path: '/m/product/hardwareSystem',
|
||||||
name: 'MobileHardwareSystem',
|
name: 'MobileHardwareSystem',
|
||||||
component: () => import('@/views/product/mobileHardwareSystem.vue'),
|
component: () => import('@/views/mobile/product/hardwareSystem.vue'),
|
||||||
meta: {title: '低空监管体系'},
|
meta: {title: '低空监管体系'},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/m/product/softwareSystem',
|
path: '/m/product/softwareSystem',
|
||||||
name: 'MobileSoftwareSystem',
|
name: 'MobileSoftwareSystem',
|
||||||
component: () => import('@/views/product/mobileSoftwareSystem.vue'),
|
component: () => import('@/views/mobile/product/softwareSystem.vue'),
|
||||||
meta: {title: '低空远程识别设备'},
|
meta: {title: '低空远程识别设备'},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/m/product/detail',
|
path: '/m/product/detail',
|
||||||
name: 'MobileProductDetail',
|
name: 'MobileProductDetail',
|
||||||
component: () => import('@/views/product/mobileDetail.vue'),
|
component: () => import('@/views/mobile/product/detail.vue'),
|
||||||
meta: {title: '产品详情'},
|
meta: {title: '产品详情'},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ const mobileRoutes = [
|
|||||||
{
|
{
|
||||||
path: '/m/services',
|
path: '/m/services',
|
||||||
name: 'MobileServices',
|
name: 'MobileServices',
|
||||||
component: () => import('@/views/services/mobile.vue'),
|
component: () => import('@/views/mobile/services/index.vue'),
|
||||||
meta: {title: '服务与支撑'},
|
meta: {title: '服务与支撑'},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -38,13 +38,13 @@ const mobileRoutes = [
|
|||||||
{
|
{
|
||||||
path: '/m/news',
|
path: '/m/news',
|
||||||
name: 'MobileNews',
|
name: 'MobileNews',
|
||||||
component: () => import('@/views/news/mobile.vue'),
|
component: () => import('@/views/mobile/news/index.vue'),
|
||||||
meta: {title: '新闻中心'},
|
meta: {title: '新闻中心'},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/m/news/detail',
|
path: '/m/news/detail',
|
||||||
name: 'MobileNewsDetail',
|
name: 'MobileNewsDetail',
|
||||||
component: () => import('@/views/news/mobileDetail.vue'),
|
component: () => import('@/views/mobile/news/detail.vue'),
|
||||||
meta: {title: '新闻详情'},
|
meta: {title: '新闻详情'},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ const mobileRoutes = [
|
|||||||
{
|
{
|
||||||
path: '/m/about',
|
path: '/m/about',
|
||||||
name: 'MobileAbout',
|
name: 'MobileAbout',
|
||||||
component: () => import('@/views/about/mobile.vue'),
|
component: () => import('@/views/mobile/about/index.vue'),
|
||||||
meta: {title: '关于我们'},
|
meta: {title: '关于我们'},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ const mobileRoutes = [
|
|||||||
{
|
{
|
||||||
path: '/m/link',
|
path: '/m/link',
|
||||||
name: 'MobileLink',
|
name: 'MobileLink',
|
||||||
component: () => import('@/views/link/mobile.vue'),
|
component: () => import('@/views/mobile/link/index.vue'),
|
||||||
meta: {title: '联系我们'},
|
meta: {title: '联系我们'},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ const mobileRoutes = [
|
|||||||
{
|
{
|
||||||
path: '/m/download',
|
path: '/m/download',
|
||||||
name: 'MobileDownload',
|
name: 'MobileDownload',
|
||||||
component: () => import('@/views/download/mobile.vue'),
|
component: () => import('@/views/mobile/download/index.vue'),
|
||||||
meta: {title: '下载中心'},
|
meta: {title: '下载中心'},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import PcIndex from './index.vue'
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<PcIndex />
|
|
||||||
</template>
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import PcIndex from './index.vue'
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<PcIndex />
|
|
||||||
</template>
|
|
||||||
@ -151,7 +151,7 @@ const toMIIF = () => {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: $bg_color;
|
background-color: white;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import PcIndex from './index.vue'
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<PcIndex />
|
|
||||||
</template>
|
|
||||||
85
src/views/mobile/about/index.vue
Normal file
85
src/views/mobile/about/index.vue
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<script setup>
|
||||||
|
import {ref, onMounted, getCurrentInstance} from 'vue'
|
||||||
|
import Swiper from '@/components/Swiper/mobile/index.vue'
|
||||||
|
import Banner from '@/components/Banner/index.vue'
|
||||||
|
|
||||||
|
const instance = getCurrentInstance()
|
||||||
|
const $fontSize = instance.appContext.config.globalProperties.$fontSize
|
||||||
|
|
||||||
|
const introduction = '燃谷科技成立于2023年,位于南京鼓楼万谷硅巷,秉承创新、质量和合作精神的低空领域技术创新公司,专注于低空安全监管、城市飞行服务和空间数据应用的企业。公司依托低空大数据、低空物联网、智能算法引擎、空间计算模型四个核心组件,自主研发数智底座,致力于物联网、空间计算领域,助力低空经济蓬勃发展。'
|
||||||
|
|
||||||
|
const currentPage = ref(1)
|
||||||
|
|
||||||
|
const imgList = [
|
||||||
|
{id: 1, width: 140, height: 90, list: ['main/1.png','main/2.png','main/3.png','main/4.png']},
|
||||||
|
{id: 2, width: 100, height: 145, list: ['main/5.png','main/6.png','main/7.png','main/8.png','main/9.png']},
|
||||||
|
{id: 3, width: 100, height: 145, list: ['main/10.png','main/11.png','main/12.png','main/13.png','main/14.png']},
|
||||||
|
{id: 4, width: 100, height: 145, list: ['main/15.png','main/16.png','main/17.png','main/18.png','main/19.png']},
|
||||||
|
{id: 5, width: 100, height: 145, list: ['main/20.png','main/21.png']},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<Banner class="banner" img="banner/about.png" />
|
||||||
|
|
||||||
|
<!-- 关于燃谷 -->
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">关于燃谷</h2>
|
||||||
|
<div class="about-box">
|
||||||
|
<img class="about-img" src="/static/images/main/rangu.png" />
|
||||||
|
<p class="about-text">{{ introduction }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 资质证书 -->
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">资质证书</h2>
|
||||||
|
<div class="cert-grid">
|
||||||
|
<div v-for="listItem in imgList" :key="listItem.id" class="cert-row" :class="{'cert-row--few': listItem.list.length <= 4}">
|
||||||
|
<div v-for="(img, idx) in listItem.list" :key="idx">
|
||||||
|
<img
|
||||||
|
:style="{width: $fontSize(listItem.width) + 'px', height: $fontSize(listItem.height) + 'px', margin: '3px'}"
|
||||||
|
:src="`./static/images/${img}`"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page { width: 100%; }
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 0 12px;
|
||||||
|
.section-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 20px 0 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
.about-img { width: 80%; height: auto; }
|
||||||
|
.about-text { font-size: 14px; line-height: 28px; color: #333; text-align: left; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cert-grid {
|
||||||
|
.cert-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.cert-row--few {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
158
src/views/mobile/download/index.vue
Normal file
158
src/views/mobile/download/index.vue
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
<script setup>
|
||||||
|
import {ref, onMounted} from 'vue'
|
||||||
|
import Swiper from '@/components/Swiper/mobile/index.vue'
|
||||||
|
import {downloadFile, downloadList} from '@/api'
|
||||||
|
import Banner from '@/components/Banner/index.vue'
|
||||||
|
import DownloadImg from '@/components/downloadImg/index.vue'
|
||||||
|
import {ElMessage} from 'element-plus'
|
||||||
|
|
||||||
|
const currentAppPage = ref(1)
|
||||||
|
const currentFilePage = ref(1)
|
||||||
|
const list = ref(null)
|
||||||
|
const imgType = ref('ios')
|
||||||
|
const showImgType = ref(false)
|
||||||
|
|
||||||
|
onMounted(() => getList())
|
||||||
|
|
||||||
|
const transform = (items) => {
|
||||||
|
const root = {children: []}
|
||||||
|
const getOrCreateFolder = (parent, name, fullPath) => {
|
||||||
|
let folder = parent.children.find((child) => child.name === name && child.type === 'folder')
|
||||||
|
if (!folder) {
|
||||||
|
folder = {name, type: 'folder', children: [], fullPath}
|
||||||
|
parent.children.push(folder)
|
||||||
|
}
|
||||||
|
return folder
|
||||||
|
}
|
||||||
|
items.forEach((item) => {
|
||||||
|
const parts = item.Key.split('/')
|
||||||
|
if (parts.length === 1) {
|
||||||
|
root.children.push({name: parts[0], type: 'file', etag: item.ETag, size: item.Size, lastModified: item.LastModifiedDateTime, fullPath: item.Key})
|
||||||
|
} else {
|
||||||
|
let currentParent = root, currentPath = ''
|
||||||
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
|
currentPath += (currentPath ? '/' : '') + parts[i]
|
||||||
|
currentParent = getOrCreateFolder(currentParent, parts[i], currentPath + '/')
|
||||||
|
}
|
||||||
|
currentParent.children.push({name: parts[parts.length - 1], type: 'file', etag: item.ETag, size: item.Size, lastModified: item.LastModifiedDateTime, fullPath: item.Key})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return root.children
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeProps = {children: 'children', label: 'name'}
|
||||||
|
|
||||||
|
const formatSize = (bytes) => {
|
||||||
|
if (!bytes) return ''
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB']
|
||||||
|
let i = 0, size = bytes
|
||||||
|
while (size >= 1024 && i < units.length - 1) { size /= 1024; i++ }
|
||||||
|
return size.toFixed(2) + ' ' + units[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getList = () => {
|
||||||
|
downloadList({recursive: true, version: true, prefix: '', bucket: ''})
|
||||||
|
.then((res) => { if (res.code == 0 && res?.data?.length > 0) list.value = transform(res.data) })
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadFileFN = (data) => {
|
||||||
|
downloadFile({bucket: '', objname: data.fullPath, expiresMinutes: 60, isDownload: true})
|
||||||
|
.then((res) => { if (res.code === 0 && res.data) window.open(res.data) })
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const showMsg = () => ElMessage.warning('敬请期待!')
|
||||||
|
const openImg = (type) => { imgType.value = type; showImgType.value = true }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<Banner class="banner" img="banner/download.png" />
|
||||||
|
|
||||||
|
<!-- App 下载区 -->
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">资源下载</h2>
|
||||||
|
<div class="app-row">
|
||||||
|
<div class="app-card" @click="openImg('ios')">
|
||||||
|
<img src="/static/images/download/ios.png" />
|
||||||
|
</div>
|
||||||
|
<div class="app-card" @click="openImg('android')">
|
||||||
|
<img src="/static/images/download/android.png" />
|
||||||
|
</div>
|
||||||
|
<div class="app-card" @click="showMsg">
|
||||||
|
<img src="/static/images/download/windows.png" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文件下载区 -->
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">文件下载</h2>
|
||||||
|
<div class="file-list">
|
||||||
|
<el-tree v-if="list && list.length" :data="list" :props="treeProps" accordion node-key="name" default-expand-all>
|
||||||
|
<template #default="{data}">
|
||||||
|
<div class="tree-node">
|
||||||
|
<span class="node-label">{{ data.name }}</span>
|
||||||
|
<template v-if="data.type === 'file'">
|
||||||
|
<span class="file-size">{{ formatSize(data.size) }}</span>
|
||||||
|
<svg class="download-icon" @click.stop="downloadFileFN(data)" viewBox="0 0 24 24" fill="none"><path d="M12 3V15M12 15L8 11M12 15L16 11" stroke="#4A90D9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 17V19C4 20.1046 4.89543 21 6 21H18C19.1046 21 20 20.1046 20 19V17" stroke="#4A90D9" stroke-width="2" stroke-linecap="round"/></svg>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
<div v-else class="empty-tip">暂无下载资源</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DownloadImg :type="imgType" v-model="showImgType" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page { width: 100%; }
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 0 12px;
|
||||||
|
.section-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 20px 0 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
.app-card {
|
||||||
|
width: 100px;
|
||||||
|
height: 120px;
|
||||||
|
img { width: 100%; height: 100%; object-fit: contain; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 400px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
padding: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-y: auto;
|
||||||
|
.empty-tip { text-align: center; color: #999; font-size: 14px; padding: 30px 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node {
|
||||||
|
display: flex; align-items: center; width: 100%; padding: 4px 0; gap: 6px;
|
||||||
|
.node-label { flex: 1; font-size: 13px; color: #333; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.file-size { font-size: 11px; color: #999; white-space: nowrap; min-width: 60px; text-align: right; }
|
||||||
|
.download-icon { width: 18px; height: 18px; flex-shrink: 0; cursor: pointer; }
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-tree) {
|
||||||
|
background: transparent;
|
||||||
|
.el-tree-node__content { height: auto; padding: 4px 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
176
src/views/mobile/homepage/index.vue
Normal file
176
src/views/mobile/homepage/index.vue
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
<script setup>
|
||||||
|
import {ref} from 'vue'
|
||||||
|
import {useRouter} from 'vue-router'
|
||||||
|
import Swiper from '@/components/Swiper/mobile/index.vue'
|
||||||
|
import {useHomepage} from '@/views/pc/homepage/composable.js'
|
||||||
|
import {cryptoEncrypt} from '@/utils/cryptojs'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const {currentPage, videoLoaded, list, onVideoLoaded, goToFeatural} = useHomepage()
|
||||||
|
|
||||||
|
const videoRef = ref(null)
|
||||||
|
|
||||||
|
function handleVideoLoaded() {
|
||||||
|
onVideoLoaded()
|
||||||
|
if (videoRef.value) {
|
||||||
|
videoRef.value.play().catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toUrl(item) {
|
||||||
|
router.push({
|
||||||
|
path: '/m/product/detail',
|
||||||
|
query: {type: cryptoEncrypt(JSON.stringify(item))},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page" v-loading="videoLoaded">
|
||||||
|
<!-- 视频 Banner -->
|
||||||
|
<div class="banner">
|
||||||
|
<video
|
||||||
|
ref="videoRef"
|
||||||
|
autoplay
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
playsinline
|
||||||
|
webkit-playsinline
|
||||||
|
preload="auto"
|
||||||
|
disablePictureInPicture
|
||||||
|
controlslist="nodownload nofullscreen noremoteplayback"
|
||||||
|
@loadeddata="handleVideoLoaded"
|
||||||
|
>
|
||||||
|
<source src="/static/video/display.mp4" type="video/mp4" />
|
||||||
|
</video>
|
||||||
|
<div class="banner-link" @click="goToFeatural">
|
||||||
|
<span>演示系统</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 产品体系 Swiper -->
|
||||||
|
<Swiper
|
||||||
|
id="home_swiper"
|
||||||
|
title="产品体系"
|
||||||
|
v-model="currentPage"
|
||||||
|
:data="list"
|
||||||
|
:page-size="1"
|
||||||
|
:show-pagination="true"
|
||||||
|
:show-hover="false"
|
||||||
|
:source-width="280"
|
||||||
|
:source-height="320"
|
||||||
|
:auto-play="true"
|
||||||
|
>
|
||||||
|
<template #default="{item}">
|
||||||
|
<div class="card-item" @click="toUrl(item)">
|
||||||
|
<div class="card-header">{{ item.title }}</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<img :src="`./static/images/${item.imgUrl}`" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
// ==================== Banner ====================
|
||||||
|
.banner {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
background-color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
// 屏蔽移动端浏览器注入的默认播放控件
|
||||||
|
&::-webkit-media-controls {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
&::-webkit-media-controls-panel {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
&::-webkit-media-controls-play-button {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
&::-webkit-media-controls-start-playback-button {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
&::-webkit-media-controls-timeline {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
&::-webkit-media-controls-current-time-display,
|
||||||
|
&::-webkit-media-controls-time-remaining-display {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
&::-webkit-media-controls-fullscreen-button {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-link {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 16px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 8px 24px;
|
||||||
|
border: 1px solid #1fe1ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: $white;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 产品卡片 ====================
|
||||||
|
.card-item {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
height: 60px;
|
||||||
|
line-height: 60px;
|
||||||
|
background-color: #0389ff;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
66
src/views/mobile/link/index.vue
Normal file
66
src/views/mobile/link/index.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<script setup>
|
||||||
|
import {ref, onMounted} from 'vue'
|
||||||
|
import Banner from '@/components/Banner/index.vue'
|
||||||
|
|
||||||
|
const toCompany = () => {
|
||||||
|
window.open('https://www.amap.com/search?query=%E7%87%83%E8%B0%B7%E7%A7%91%E6%8A%80(%E5%8D%97%E4%BA%AC)%E6%9C%89%E9%99%90%E5%85%AC%E5%8F%B8')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<Banner class="banner" img="banner/link.png" />
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">联系我们</h2>
|
||||||
|
<div class="link-box">
|
||||||
|
<div class="info-list">
|
||||||
|
<div class="info-item name">燃谷科技(南京)有限公司</div>
|
||||||
|
<div class="info-item">电话:13222013393</div>
|
||||||
|
<div class="info-item">邮箱:company@rangutech.com</div>
|
||||||
|
<div class="info-item">地址:江苏省南京市鼓楼区万谷硅巷5F</div>
|
||||||
|
</div>
|
||||||
|
<img class="map-img" src="/static/images/link/map.png" @click="toCompany" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page { width: 100%; }
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 0 12px;
|
||||||
|
.section-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 20px 0 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.info-list {
|
||||||
|
.info-item {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 2;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
95
src/views/mobile/news/detail.vue
Normal file
95
src/views/mobile/news/detail.vue
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<script setup>
|
||||||
|
import {onMounted, ref} from 'vue'
|
||||||
|
import {useRoute, useRouter} from 'vue-router'
|
||||||
|
import {ElMessage} from 'element-plus'
|
||||||
|
import {getNewsDetail} from '@/api/index'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const detail = route.query
|
||||||
|
const showLoad = ref(true)
|
||||||
|
|
||||||
|
const newsTitle = ref('')
|
||||||
|
const newsTime = ref('')
|
||||||
|
const newsSubTitle = ref('')
|
||||||
|
const newsContent = ref(null)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
onMounted(() => getDetail())
|
||||||
|
|
||||||
|
const getDetail = () => {
|
||||||
|
getNewsDetail({slug: detail.slug})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
let {content, snapshot, datetime, title} = res.data
|
||||||
|
newsTitle.value = title
|
||||||
|
newsTime.value = datetime
|
||||||
|
newsSubTitle.value = snapshot
|
||||||
|
newsContent.value = content
|
||||||
|
showLoad.value = false
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg)
|
||||||
|
showLoad.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => { showLoad.value = false })
|
||||||
|
}
|
||||||
|
|
||||||
|
const toBack = () => {
|
||||||
|
router.push('/m/news')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page" v-loading="showLoad">
|
||||||
|
<template v-if="newsTitle">
|
||||||
|
<h2 class="title">{{ newsTitle }}</h2>
|
||||||
|
<p class="time" v-if="newsTime">{{ newsTime }}</p>
|
||||||
|
<div class="article" v-html="newsContent"></div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="no-content">暂无内容</div>
|
||||||
|
<div class="back-btn" @click="toBack">返回</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
text-align: center;
|
||||||
|
margin: 24px 0 16px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.time {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.article {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
:deep(img) { max-width: 100%; height: auto; }
|
||||||
|
}
|
||||||
|
.no-content { text-align: center; color: #999; font-size: 16px; padding: 40px 0; }
|
||||||
|
.back-btn {
|
||||||
|
margin: 32px auto 40px;
|
||||||
|
width: 120px;
|
||||||
|
padding: 10px 0;
|
||||||
|
text-align: center;
|
||||||
|
background: #0389ff;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
107
src/views/mobile/news/index.vue
Normal file
107
src/views/mobile/news/index.vue
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<script setup>
|
||||||
|
import {ref, onMounted, nextTick, onActivated, onDeactivated} from 'vue'
|
||||||
|
import Banner from '@/components/Banner/index.vue'
|
||||||
|
import {useRouter} from 'vue-router'
|
||||||
|
import {getNews} from '@/api/index'
|
||||||
|
|
||||||
|
const showLoad = ref(true)
|
||||||
|
const newList = ref([])
|
||||||
|
const page = ref({pageNum: 1, pageSize: 10, totalPages: 0})
|
||||||
|
let scrollTop = 0
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
onMounted(() => getNewList())
|
||||||
|
|
||||||
|
async function getNewList() {
|
||||||
|
getNews({status: 'published', page: page.value.pageNum, page_size: page.value.pageSize})
|
||||||
|
.then((res) => { if (res.code === 0) { newList.value = res.data; page.value.totalPages = res.pagination.total_pages } })
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => { showLoad.value = false })
|
||||||
|
}
|
||||||
|
|
||||||
|
const toDetail = (item) => {
|
||||||
|
router.push({path: '/m/news/detail', query: {id: item.id, slug: item.slug}})
|
||||||
|
}
|
||||||
|
|
||||||
|
onActivated(async () => {
|
||||||
|
await nextTick()
|
||||||
|
requestAnimationFrame(() => { window.scrollTo({top: scrollTop, behavior: 'instant'}) })
|
||||||
|
})
|
||||||
|
onDeactivated(() => { scrollTop = document.documentElement.scrollTop || document.body.scrollTop })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page" v-loading="showLoad">
|
||||||
|
<Banner class="banner" img="banner/news.png" />
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">新闻中心</h2>
|
||||||
|
<template v-if="newList.length > 0">
|
||||||
|
<div class="news-list">
|
||||||
|
<div class="news-card" v-for="item in newList" :key="item.id" @click="toDetail(item)">
|
||||||
|
<div class="news-img">
|
||||||
|
<img :src="item.cover_image" />
|
||||||
|
</div>
|
||||||
|
<div class="news-body">
|
||||||
|
<div class="news-title">{{ item.title }}</div>
|
||||||
|
<div class="news-time">{{ item?.time }}</div>
|
||||||
|
<div class="news-desc">{{ item.snapshot }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="no-content">暂无内容</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page { width: 100%; }
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 0 12px;
|
||||||
|
.section-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 20px 0 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.news-img {
|
||||||
|
width: 120px;
|
||||||
|
height: 80px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #eee;
|
||||||
|
img { width: 100%; height: 100%; object-fit: cover; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
.news-title { font-size: 14px; font-weight: 600; color: #333; margin-bottom: 4px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||||
|
.news-time { font-size: 11px; color: #999; margin-bottom: 4px; }
|
||||||
|
.news-desc { font-size: 12px; color: #666; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-content { text-align: center; color: #999; font-size: 16px; padding: 40px 0; }
|
||||||
|
</style>
|
||||||
175
src/views/mobile/product/detail.vue
Normal file
175
src/views/mobile/product/detail.vue
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
<script setup>
|
||||||
|
import {ref, onMounted} from 'vue'
|
||||||
|
import {useRoute, useRouter} from 'vue-router'
|
||||||
|
import {cryptoDecrypt} from '@/utils/cryptojs.js'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const query = JSON.parse(cryptoDecrypt(route.query.type))
|
||||||
|
const advantages = window.advantages
|
||||||
|
const videoLoaded = ref(false)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
videoLoaded.value = query.type !== 'softwareSystem' ? false : true
|
||||||
|
})
|
||||||
|
|
||||||
|
function onVideoLoaded() {
|
||||||
|
videoLoaded.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBack() {
|
||||||
|
if (query.type === 'hardwareSystem') {
|
||||||
|
router.push('/m/product/hardwareSystem')
|
||||||
|
} else if (query.type === 'softwareSystem') {
|
||||||
|
router.push('/m/product/softwareSystem')
|
||||||
|
} else {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page" v-loading="videoLoaded">
|
||||||
|
<h3 class="label">{{ query.title }} {{ query?.subTitle }}</h3>
|
||||||
|
|
||||||
|
<!-- ============== 硬件详情 ============== -->
|
||||||
|
<template v-if="query.type === 'hardwareSystem'">
|
||||||
|
<div class="hardware-section">
|
||||||
|
<img class="hardware-img" :src="`./static/images/${query.imgUrl}`" alt="" />
|
||||||
|
<div class="advantage-box">
|
||||||
|
<h4 class="advantage-title">核心优势</h4>
|
||||||
|
<div class="advantage-text" v-html="advantages"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="desc-box" v-html="query.description"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ============== 软件详情 ============== -->
|
||||||
|
<template v-else>
|
||||||
|
<div class="software-section">
|
||||||
|
<video autoplay muted loop playsinline @loadeddata="onVideoLoaded" v-show="!videoLoaded">
|
||||||
|
<source :src="`./static/video/${query.video}`" type="video/mp4" />
|
||||||
|
</video>
|
||||||
|
<div class="desc-box" v-html="query.description"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-grid" v-if="query.card">
|
||||||
|
<div class="card" v-for="item in query.card" :key="item.id">
|
||||||
|
<h4 class="card-title">{{ item.title }}</h4>
|
||||||
|
<p class="card-text">{{ item.content }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="back-btn" @click="toBack">返回</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin: 24px 0 16px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 硬件区域 ==============
|
||||||
|
.hardware-section {
|
||||||
|
.hardware-img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advantage-box {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.advantage-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advantage-text {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 软件区域 ==============
|
||||||
|
.software-section {
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 描述文案 ==============
|
||||||
|
.desc-box {
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 功能卡片 ==============
|
||||||
|
.card-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
background-color: #0389ff;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-text {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 10px;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 4;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 返回按钮 ==============
|
||||||
|
.back-btn {
|
||||||
|
margin: 32px auto 40px;
|
||||||
|
width: 120px;
|
||||||
|
padding: 10px 0;
|
||||||
|
text-align: center;
|
||||||
|
background: #0389ff;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
105
src/views/mobile/product/hardwareSystem.vue
Normal file
105
src/views/mobile/product/hardwareSystem.vue
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<script setup>
|
||||||
|
import {ref, onMounted, computed} from 'vue'
|
||||||
|
import Swiper from '@/components/Swiper/mobile/index.vue'
|
||||||
|
import Banner from '@/components/Banner/index.vue'
|
||||||
|
import {findLabelByUrl} from '@/utils'
|
||||||
|
import {cryptoEncrypt} from '@/utils/cryptojs'
|
||||||
|
import {useNavStore} from '@/store/nav'
|
||||||
|
import {useRouter} from 'vue-router'
|
||||||
|
|
||||||
|
const navStore = useNavStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const hardwareSystemList = window.hardwareSystemList
|
||||||
|
const title = computed(() => findLabelByUrl(window.nav.header, navStore.navIndex))
|
||||||
|
const currentPage = ref(1)
|
||||||
|
|
||||||
|
function titleClick(item) {
|
||||||
|
router.push({
|
||||||
|
path: '/m/product/detail',
|
||||||
|
query: {type: cryptoEncrypt(JSON.stringify(item))},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<Banner class="banner" img="banner/product.png" />
|
||||||
|
<Swiper
|
||||||
|
id="one"
|
||||||
|
:title="title"
|
||||||
|
v-model="currentPage"
|
||||||
|
:data="hardwareSystemList"
|
||||||
|
:page-size="1"
|
||||||
|
:show-pagination="true"
|
||||||
|
:show-hover="false"
|
||||||
|
:auto-play="false"
|
||||||
|
:source-width="300"
|
||||||
|
:source-height="360"
|
||||||
|
:source-gap="20"
|
||||||
|
>
|
||||||
|
<template #default="{item}">
|
||||||
|
<div class="card-item" @click="titleClick(item)">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">{{ item.title }}</div>
|
||||||
|
<div class="card-subtitle">{{ item.subTitle }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<img :src="`./static/images/${item.imgUrl}`" class="card-img" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-item {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background-color: #0389ff;
|
||||||
|
color: #fff;
|
||||||
|
padding: 12px 0;
|
||||||
|
text-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
.card-img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
103
src/views/mobile/product/softwareSystem.vue
Normal file
103
src/views/mobile/product/softwareSystem.vue
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<script setup>
|
||||||
|
import {ref, computed} from 'vue'
|
||||||
|
import Swiper from '@/components/Swiper/mobile/index.vue'
|
||||||
|
import Banner from '@/components/Banner/index.vue'
|
||||||
|
import {findLabelByUrl} from '@/utils'
|
||||||
|
import {cryptoEncrypt} from '@/utils/cryptojs'
|
||||||
|
import {useNavStore} from '@/store/nav'
|
||||||
|
import {useRouter} from 'vue-router'
|
||||||
|
|
||||||
|
const navStore = useNavStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const softwareSystemList = window.softwareSystemList
|
||||||
|
const title = computed(() => findLabelByUrl(window.nav.header, navStore.navIndex))
|
||||||
|
const currentPage = ref(1)
|
||||||
|
|
||||||
|
function titleClick(item) {
|
||||||
|
router.push({
|
||||||
|
path: '/m/product/detail',
|
||||||
|
query: {type: cryptoEncrypt(JSON.stringify(item))},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<Banner class="banner" img="banner/product.png" />
|
||||||
|
<Swiper
|
||||||
|
id="one"
|
||||||
|
:title="title"
|
||||||
|
v-model="currentPage"
|
||||||
|
:data="softwareSystemList"
|
||||||
|
:page-size="1"
|
||||||
|
:show-pagination="true"
|
||||||
|
:show-hover="false"
|
||||||
|
:auto-play="false"
|
||||||
|
:source-width="300"
|
||||||
|
:source-height="360"
|
||||||
|
:source-gap="20"
|
||||||
|
>
|
||||||
|
<template #default="{item}">
|
||||||
|
<div class="card-item" @click="titleClick(item)">
|
||||||
|
<div class="card-header">{{ item.title }}</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<img :src="`./static/images/${item.imgUrl}`" class="card-img" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-item {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background-color: #0389ff;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 12px 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
.card-img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import PcIndex from './index.vue'
|
import PcIndex from '@/views/pc/services/index.vue'
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<PcIndex />
|
<PcIndex />
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import PcIndex from './index.vue'
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<PcIndex />
|
|
||||||
</template>
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import PcPage from './detail.vue'
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<PcPage />
|
|
||||||
</template>
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {ref, onMounted, getCurrentInstance} from 'vue'
|
import {ref, onMounted, getCurrentInstance} from 'vue'
|
||||||
import Swiper from '@/components/Swiper/index.vue'
|
import Swiper from '@/components/Swiper/pc/index.vue'
|
||||||
import Banner from '@/components/Banner/index.vue'
|
import Banner from '@/components/Banner/index.vue'
|
||||||
|
|
||||||
const instance = getCurrentInstance()
|
const instance = getCurrentInstance()
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {ref, onMounted} from 'vue'
|
import {ref, onMounted} from 'vue'
|
||||||
import Swiper from '@/components/Swiper/index.vue'
|
import Swiper from '@/components/Swiper/pc/index.vue'
|
||||||
import {downloadFile, downloadList} from '@/api'
|
import {downloadFile, downloadList} from '@/api'
|
||||||
import Banner from '@/components/Banner/index.vue'
|
import Banner from '@/components/Banner/index.vue'
|
||||||
import DownloadImg from '@/components/downloadImg/index.vue'
|
import DownloadImg from '@/components/downloadImg/index.vue'
|
||||||
40
src/views/pc/homepage/composable.js
Normal file
40
src/views/pc/homepage/composable.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import {ref} from 'vue'
|
||||||
|
import {useRouter} from 'vue-router'
|
||||||
|
import {cryptoEncrypt} from '@/utils/cryptojs'
|
||||||
|
|
||||||
|
// 首页公用逻辑和数据
|
||||||
|
export function useHomepage() {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const swiperWidth = ref(400)
|
||||||
|
const swiperHeight = ref(400)
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const videoLoaded = ref(true)
|
||||||
|
const list = [...window.softwareSystemList, ...window.hardwareSystemList]
|
||||||
|
|
||||||
|
function onVideoLoaded() {
|
||||||
|
videoLoaded.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function toUrl(item) {
|
||||||
|
router.push({
|
||||||
|
path: '/product/detail',
|
||||||
|
query: {type: cryptoEncrypt(JSON.stringify(item))},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToFeatural() {
|
||||||
|
window.open(window.config.featureUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
swiperWidth,
|
||||||
|
swiperHeight,
|
||||||
|
currentPage,
|
||||||
|
videoLoaded,
|
||||||
|
list,
|
||||||
|
onVideoLoaded,
|
||||||
|
toUrl,
|
||||||
|
goToFeatural,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Swiper from '@/components/Swiper/index.vue'
|
import Swiper from '@/components/Swiper/pc/index.vue'
|
||||||
import {ref, onMounted} from 'vue'
|
import {ref, onMounted} from 'vue'
|
||||||
import {cryptoEncrypt} from '@/utils/cryptojs'
|
import {cryptoEncrypt} from '@/utils/cryptojs'
|
||||||
import {useRouter} from 'vue-router'
|
import {useRouter} from 'vue-router'
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {ref, onMounted} from 'vue'
|
import {ref, onMounted} from 'vue'
|
||||||
import Swiper from '@/components/Swiper/index.vue'
|
import Swiper from '@/components/Swiper/pc/index.vue'
|
||||||
import Banner from '@/components/Banner/index.vue'
|
import Banner from '@/components/Banner/index.vue'
|
||||||
// import Map from './map.vue'
|
// import Map from './map.vue'
|
||||||
|
|
||||||
@ -2,7 +2,7 @@
|
|||||||
defineOptions({name: 'New'})
|
defineOptions({name: 'New'})
|
||||||
import {ref, onMounted, nextTick, onActivated, onDeactivated, computed} from 'vue'
|
import {ref, onMounted, nextTick, onActivated, onDeactivated, computed} from 'vue'
|
||||||
import Banner from '@/components/Banner/index.vue'
|
import Banner from '@/components/Banner/index.vue'
|
||||||
import Swiper from '@/components/Swiper/index.vue'
|
import Swiper from '@/components/Swiper/pc/index.vue'
|
||||||
import {useRouter} from 'vue-router'
|
import {useRouter} from 'vue-router'
|
||||||
import {onBeforeRouteLeave} from 'vue-router'
|
import {onBeforeRouteLeave} from 'vue-router'
|
||||||
import {getNews} from '@/api/index'
|
import {getNews} from '@/api/index'
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {ref, onMounted, computed} from 'vue'
|
import {ref, onMounted, computed} from 'vue'
|
||||||
import Swiper from '@/components/Swiper/index.vue'
|
import Swiper from '@/components/Swiper/pc/index.vue'
|
||||||
import Banner from '@/components/Banner/index.vue'
|
import Banner from '@/components/Banner/index.vue'
|
||||||
import Project from './project.vue'
|
import Project from './project.vue'
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {ref, onMounted} from 'vue'
|
import {ref, onMounted} from 'vue'
|
||||||
import Swiper from '@/components/Swiper/index.vue'
|
import Swiper from '@/components/Swiper/pc/index.vue'
|
||||||
import Banner from '@/components/Banner/index.vue'
|
import Banner from '@/components/Banner/index.vue'
|
||||||
|
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Swiper from '@/components/Swiper/index.vue'
|
import Swiper from '@/components/Swiper/pc/index.vue'
|
||||||
import {ref, onMounted} from 'vue'
|
import {ref, onMounted} from 'vue'
|
||||||
onMounted(() => {})
|
onMounted(() => {})
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {ref, onMounted, computed} from 'vue'
|
import {ref, onMounted, computed} from 'vue'
|
||||||
import Swiper from '@/components/Swiper/index.vue'
|
import Swiper from '@/components/Swiper/pc/index.vue'
|
||||||
import Banner from '@/components/Banner/index.vue'
|
import Banner from '@/components/Banner/index.vue'
|
||||||
import {findLabelByUrl} from '@/utils'
|
import {findLabelByUrl} from '@/utils'
|
||||||
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import PcPage from './detail.vue'
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<PcPage />
|
|
||||||
</template>
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import PcPage from './hardwareSystem.vue'
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<PcPage />
|
|
||||||
</template>
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import PcPage from './softwareSystem.vue'
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<PcPage />
|
|
||||||
</template>
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import PcIndex from './index.vue'
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<PcIndex />
|
|
||||||
</template>
|
|
||||||
Reference in New Issue
Block a user