Compare commits

..

4 Commits

Author SHA1 Message Date
23ad5d6f6d 管理页面 2025-10-16 15:45:55 +08:00
05da1f7fbb 创建富文本 2025-10-15 16:47:09 +08:00
502eaff488 提交代码 2025-10-15 16:46:35 +08:00
9a01223fc9 修改 2025-10-13 18:41:08 +08:00
40 changed files with 1070 additions and 258 deletions

View File

@ -5,6 +5,11 @@
<link rel="icon" type="image/svg+xml" href="/rangu.svg" /> <link rel="icon" type="image/svg+xml" href="/rangu.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>燃谷科技(南京)有限公司</title> <title>燃谷科技(南京)有限公司</title>
<meta name="keywords" content="低空经济,低空监管,远程识别,RemoteID,RID" />
<meta
name="description"
content="燃谷科技位于南京鼓楼万谷硅巷,秉承创新、质量和合作精神的低空领域技术创新公司,专注于低空安全监管、城市飞行服务和空间数据应用的企业。"
/>
<script src="/nav/index.js"></script> <script src="/nav/index.js"></script>
<script src="/config.js"></script> <script src="/config.js"></script>
</head> </head>

View File

@ -9,14 +9,17 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@vueup/vue-quill": "^1.2.0",
"@vueuse/core": "^13.8.0", "@vueuse/core": "^13.8.0",
"axios": "^1.11.0", "axios": "^1.12.2",
"element-plus": "^2.11.1", "element-plus": "^2.11.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.5.0", "pinia-plugin-persistedstate": "^4.5.0",
"quill": "^2.0.3",
"terser": "^5.43.1", "terser": "^5.43.1",
"v-viewer": "^3.0.11",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },

View File

@ -1,2 +1,9 @@
window.GD_url = window.GD_KEYS = [
'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' '348d477ba83826e46b32d3ff10fffe82',
'ed2ea36f8564541569c370254845d93d',
'c1da03827f956a215311c0f5229bddc3',
]
window.config = {
baseUrl: 'http://192.168.3.10:9121/',
}

View File

@ -3,25 +3,25 @@ window.nav = {
{id: 1, label: '首页', url: '/', hasChildren: false}, {id: 1, label: '首页', url: '/', hasChildren: false},
{ {
id: 2, id: 2,
label: '产品中心', label: '解决方案',
url: '/product', url: '/product',
hasChildren: true, hasChildren: true,
children: [ children: [
{ {
id: 2.1, id: 2.1,
label: '低空监管体系', label: '空域感知矩阵',
url: '/product/monitorSystem', url: '/product/monitorSystem',
hasChildren: false, hasChildren: false,
}, },
{ {
id: 2.2, id: 2.2,
label: '低空远程识别设备', label: '低空智控中枢',
url: '/product/remoteDevice', url: '/product/remoteDevice',
hasChildren: false, hasChildren: false,
}, },
], ],
}, },
{id: 3, label: '服务与支撑', url: '/services', hasChildren: false}, // {id: 3, label: '服务与支撑', url: '/services', hasChildren: false},
{id: 4, label: '新闻中心', url: '/news', hasChildren: false}, {id: 4, label: '新闻中心', url: '/news', hasChildren: false},
{id: 5, label: '关于我们', url: '/about', hasChildren: false}, {id: 5, label: '关于我们', url: '/about', hasChildren: false},
{id: 6, label: '联系我们', url: '/link', hasChildren: false}, {id: 6, label: '联系我们', url: '/link', hasChildren: false},
@ -29,28 +29,26 @@ window.nav = {
footer: [ footer: [
{ {
id: 1, id: 1,
label: '产品中心', label: '解决方案',
url: '', url: '',
hasChildren: true, hasChildren: true,
children: [ children: [
{id: 1.1, label: '低空监管体系', url: '/product/monitorSystem'}, {id: 1.1, label: '空域感知矩阵', url: '/product/monitorSystem'},
{id: 1.2, label: '低空远程识别设备', url: '/product/remoteDevice'}, {id: 1.2, label: '低空智控中枢', url: '/product/remoteDevice'},
{id: 1.3, label: '智能加载服务', url: '/product'},
{id: 1.4, label: '智能加载服务', url: '/product'},
], ],
}, },
{ // {
id: 2, // id: 2,
label: '服务与支撑', // label: '服务与支撑',
url: '/services', // url: '/services',
hasChildren: false, // hasChildren: false,
}, // },
{ // {
id: 3, // id: 3,
label: '软件下载', // label: '软件下载',
url: '/download', // url: '/download',
hasChildren: false, // hasChildren: false,
}, // },
{id: 4, label: '新闻中心', url: '/news', hasChildren: false}, {id: 4, label: '新闻中心', url: '/news', hasChildren: false},
{ {
id: 5, id: 5,

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -1,19 +0,0 @@
import request from '@/utils/request'
// 设备设置
export function deviceSet(data) {
return request({
url: '/rangu/rid/setting',
method: 'post',
data,
})
}
// 设备设置
export function deviceGet(params) {
return request({
url: '/rangu/rid/getting',
method: 'get',
params,
})
}

10
src/api/index.js Normal file
View File

@ -0,0 +1,10 @@
import request from '@/utils/request'
// 获取访问接口
export function visit(data) {
return request({
url: '/tracking',
method: 'post',
data,
})
}

View File

@ -2,7 +2,7 @@
使用<icon-svg name="icon-微信" class="icon" /> 使用<icon-svg name="icon-微信" class="icon" />
--> -->
<script setup > <script setup>
defineProps({ defineProps({
name: { name: {
type: String, type: String,

View File

@ -0,0 +1,26 @@
<script setup>
import {ref, onMounted} from 'vue'
onMounted(() => {})
</script>
<template>
<div>
<p>ID/无人机低空监管设备 无人机定位设备/无人机飞手定位设备/无人机侦测设备/Remote ID无人机远程识别模块 Remote</p>
<p>Remote ID无人机识别设备/符合GB-42590-2023/无人机远程识别发射模块 无人机远程识别设备/无人机RID/Remote</p>
<p>ID无人机远程识别广播模块/RID无人机识别/无人机飞手远程定位设备</p>
<p>无人机测距模块/无人机侦测监管设备/RID无人机识别/无人机定位器/无人机飞手定位器</p>
<p>无人机侦测定位设备/迷你无人机识别模块/低空经济无人机识别设备/单兵无人机侦测识别设备</p>
<p>无人机探测器/防撞机无人机定位设备/Remote ID无人机预警仪</p>
<p>全向无人机探测设备/无人机定位飞手定位设备/安防安保无人机监测设备/RID无人机识别/Remote ID远程识别模块</p>
<p>无人机侦测设备飞手定位飞行轨迹/UAV无线电侦测设备/无人机GPS GNSS定位设备</p>
<p>无人机RID广播识别模块/无人机探测飞手定位飞行轨迹/专业级无人机预警侦测设备 无人机航拍防撞设备/无人机Remote</p>
<p>ID国标定位侦测设备/低空安防无人机监管 无人机识别跟踪设备/RID无人机定位模块/Remote</p>
<p>ID无人机定位跟踪设备带飞手定位飞行轨迹 自主研发无人机侦测定位模块/Remote</p>
<p>ID无人机远程识别设备/飞机定位飞行轨迹飞手定位无人机识别模块源头厂家 无人机标签识别设备/Remote</p>
<p>
ID无人机侦测定位设备/远程广播模块识别 无人机远程识别地面站/RID无人机识别/无人机巡检智能识别模块/无人机定位飞手定位
</p>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -1,12 +1,5 @@
<script setup> <script setup>
import { import {ref, computed, watch, nextTick, getCurrentInstance, onUnmounted} from 'vue'
ref,
computed,
watch,
nextTick,
getCurrentInstance,
onUnmounted,
} from 'vue'
/* -------------------- 组件接口定义 -------------------- */ /* -------------------- 组件接口定义 -------------------- */
const currentPage = defineModel({type: [Number, String], default: 1}) const currentPage = defineModel({type: [Number, String], default: 1})
@ -26,7 +19,7 @@ const props = defineProps({
sourceGap: {type: Number, default: 20}, sourceGap: {type: Number, default: 20},
}) })
const emit = defineEmits(['page-change', 'item-click', 'item-hover']) const emit = defineEmits(['page-change', 'item-click', 'item-hover', 'title-click'])
/* -------------------- 响应式变量 -------------------- */ /* -------------------- 响应式变量 -------------------- */
const instance = getCurrentInstance() const instance = getCurrentInstance()
@ -39,18 +32,18 @@ const itemWidth = computed(() => $fontSize(props.sourceWidth))
const itemHeight = computed(() => $fontSize(props.sourceHeight)) const itemHeight = computed(() => $fontSize(props.sourceHeight))
const itemGap = computed(() => $fontSize(props.sourceGap)) const itemGap = computed(() => $fontSize(props.sourceGap))
const itemTotalWidth = computed(() => itemWidth.value + itemGap.value) const itemTotalWidth = computed(() => itemWidth.value + itemGap.value)
const viewPortWidth = computed( const viewPortWidth = computed(() => props.pageSize * itemWidth.value + (props.pageSize - 1) * itemGap.value + 'px')
() => const trackWidth = computed(() => props.data.length * itemTotalWidth.value - itemGap.value + 'px')
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) const totalPages = computed(() => props.data.length - props.pageSize + 1)
/* -------------------- 方法 -------------------- */ /* -------------------- 方法 -------------------- */
/**
* 标题点击事件
*/
const clickTitle = () => {
emit('title-click')
}
/** /**
* 执行平移动画 * 执行平移动画
* @param {number} page - 目标页码 * @param {number} page - 目标页码
@ -58,9 +51,7 @@ const totalPages = computed(() => props.data.length - props.pageSize + 1)
const translate = (page) => { const translate = (page) => {
if (!trackRef.value) return if (!trackRef.value) return
trackRef.value.style.transition = `transform ${props.transitionDuration}ms ease` trackRef.value.style.transition = `transform ${props.transitionDuration}ms ease`
trackRef.value.style.transform = `translateX(-${ trackRef.value.style.transform = `translateX(-${itemTotalWidth.value * (page - 1)}px)`
itemTotalWidth.value * (page - 1)
}px)`
} }
/** /**
@ -75,8 +66,7 @@ const goToPage = (page) => {
* 切换到下一页,循环播放 * 切换到下一页,循环播放
*/ */
const nextPage = () => { const nextPage = () => {
const nextPage = const nextPage = currentPage.value < totalPages.value ? currentPage.value + 1 : 1
currentPage.value < totalPages.value ? currentPage.value + 1 : 1
goToPage(nextPage) goToPage(nextPage)
} }
@ -84,8 +74,7 @@ const nextPage = () => {
* 切换到上一页,循环播放 * 切换到上一页,循环播放
*/ */
const prevPage = () => { const prevPage = () => {
const prevPage = const prevPage = currentPage.value > 1 ? currentPage.value - 1 : totalPages.value
currentPage.value > 1 ? currentPage.value - 1 : totalPages.value
goToPage(prevPage) goToPage(prevPage)
} }
@ -175,7 +164,10 @@ onUnmounted(stopAutoPlay)
<template> <template>
<section :id="id" class="generic-carousel"> <section :id="id" class="generic-carousel">
<!-- 标题 --> <!-- 标题 -->
<h2 v-if="title" class="carousel-title">{{ title }}</h2> <slot name="title">
<h2 v-if="title" class="carousel-title" @click="clickTitle">{{ title }}</h2>
</slot>
<!-- <h2 v-if="title" class="carousel-title">{{ title }}</h2> -->
<!-- 分页指示器 --> <!-- 分页指示器 -->
<div v-if="showDot" class="carousel-pagination"> <div v-if="showDot" class="carousel-pagination">
@ -189,16 +181,9 @@ onUnmounted(stopAutoPlay)
</div> </div>
<!-- 导航按钮 --> <!-- 导航按钮 -->
<div <div v-if="showPagination && totalPages > 1" class="carousel-navigation flex j-s">
v-if="showPagination && totalPages > 1" <button class="nav-btn prev-btn" @click="prevPage" aria-label="上一页">&lt;</button>
class="carousel-navigation flex j-s" <button class="nav-btn next-btn" @click="nextPage" aria-label="下一页">&gt;</button>
>
<button class="nav-btn prev-btn" @click="prevPage" aria-label="上一页">
&lt;
</button>
<button class="nav-btn next-btn" @click="nextPage" aria-label="下一页">
&gt;
</button>
</div> </div>
<!-- 轮播内容区域 --> <!-- 轮播内容区域 -->
@ -224,11 +209,7 @@ onUnmounted(stopAutoPlay)
@mouseenter="handleItemHover(item, index)" @mouseenter="handleItemHover(item, index)"
@mouseleave="handleItemHoverLeave(item, index)" @mouseleave="handleItemHoverLeave(item, index)"
> >
<slot <slot :item="item" :index="index" :isActive="currentPage === Math.ceil((index + 1) / pageSize)" />
:item="item"
:index="index"
:isActive="currentPage === Math.ceil((index + 1) / pageSize)"
/>
</div> </div>
</div> </div>
</div> </div>
@ -247,6 +228,10 @@ onUnmounted(stopAutoPlay)
font-weight: 600; font-weight: 600;
font-size: 24px; font-size: 24px;
color: #333333; color: #333333;
&:hover {
cursor: pointer;
color: #0389ff;
}
} }
.carousel-pagination { .carousel-pagination {
@ -336,5 +321,9 @@ onUnmounted(stopAutoPlay)
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
} }
} }
.item-title {
margin-bottom: 5px;
}
} }
</style> </style>

View File

@ -1,41 +1,66 @@
<script setup> <script setup>
import Header from './views/layout/pc/header/index.vue' import Header from './views/layout/pc/header/index.vue'
import Footer from './views/layout/pc/footer/index.vue' import Footer from './views/layout/pc/footer/index.vue'
import {onMounted, ref, watch} from 'vue'
import {visit} from '@/api/index'
import axios from 'axios'
onMounted(() => {
getUrl()
})
const referrer = ref('')
const ipUrl = ref('')
console.log('11===>', window.location)
function getUrl() {
referrer.value = document.referrer
axios
.get('https://api64.ipify.org?format=json')
.then(({data}) => {
ipUrl.value = data.ip
})
.catch(console.error)
}
async function postVisit() {
let params = {
timestamp: new Date(),
url: window.location.href,
referrer: referrer.value,
real_ip: ipUrl.value,
}
let {code, msg} = await visit(params)
}
watch(
() => [referrer.value, ipUrl.value],
([refer, ipUrl]) => {
if (refer && ipUrl) {
postVisit()
}
},
{
deep: true,
immediate: true,
}
)
</script> </script>
<template> <template>
<div id="app"> <div id="app">
<Header id="header" /> <Header id="header" />
<router-view id="main" v-slot="{Component, title}">
<router-view id="main" v-slot="{Component, route}"> <keep-alive include="New">
<transition name="fade" mode="out-in"> <component :is="Component" />
<Suspense> </keep-alive>
<keep-alive include="New">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
<template #fallback>
<div class="loading-placeholder">页面加载中...</div>
</template>
</Suspense>
</transition>
</router-view> </router-view>
<Footer id="footer" /> <Footer id="footer" />
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
/* 淡入淡出效果 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
#app { #app {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -55,7 +80,7 @@ import Footer from './views/layout/pc/footer/index.vue'
} }
#footer { #footer {
width: 100%; width: 100%;
height: 500px; height: 380px;
} }
} }
</style> </style>

View File

@ -16,7 +16,7 @@ import '@/assets/styles/index.scss'
// ====================== 工具和组件 ====================== // ====================== 工具和组件 ======================
import {initRem, fontSize} from '@/utils/rem' import {initRem, fontSize} from '@/utils/rem'
import IconSvg from '_c/icon/svgIcon.vue' import IconSvg from '@/components/Icon/svgIcon.vue'
// SVG图标注册 // SVG图标注册
import 'virtual:svg-icons-register' import 'virtual:svg-icons-register'

View File

@ -1,13 +1,13 @@
import {createWebHashHistory, createRouter} from 'vue-router' import { createWebHashHistory, createRouter } from 'vue-router'
import HomeView from '@/views/homepage/index.vue' import HomeView from '@/views/homepage/index.vue'
const routes = [ const routes = [
{path: '/', component: HomeView}, { path: '/', component: HomeView },
// ============================================ 产品中心 ============================================ // ============================================ 产品中心 ============================================
{ {
path: '/product/monitorSystem', path: '/product/monitorSystem',
name: 'monitorSystem', name: 'MonitorSystem',
component: () => import('@/views/product/monitorSystem.vue'), component: () => import('@/views/product/monitorSystem.vue'),
meta: { meta: {
title: '低空监管体系', title: '低空监管体系',
@ -15,12 +15,20 @@ const routes = [
}, },
{ {
path: '/product/remoteDevice', path: '/product/remoteDevice',
name: 'remoteDevice', name: 'RemoteDevice',
component: () => import('@/views/product/remoteDevice.vue'), component: () => import('@/views/product/remoteDevice.vue'),
meta: { meta: {
title: '低空远程识别设备', title: '低空远程识别设备',
}, },
}, },
{
path: '/product/detail',
name: 'ProductDetail',
component: () => import('@/views/product/detail.vue'),
meta: {
title: '产品详情',
},
},
// ============================================ 服务与支撑 ============================================ // ============================================ 服务与支撑 ============================================
{ {
path: '/services', path: '/services',
@ -39,14 +47,6 @@ const routes = [
title: '新闻中心', title: '新闻中心',
}, },
}, },
{
path: '/news/detail',
name: 'Detail',
component: () => import('@/views/news/detail.vue'),
meta: {
title: '新闻详情',
},
},
// ============================================ 关于我们 ============================================ // ============================================ 关于我们 ============================================
{ {
path: '/about', path: '/about',
@ -74,6 +74,15 @@ const routes = [
title: '下载中心', title: '下载中心',
}, },
}, },
// ============================================ 管理页面 ============================================
{
path: '/manager',
name: 'Manager',
component: () => import('@/views/manager/index.vue'),
meta: {
title: '管理中心',
},
},
] ]
const router = createRouter({ const router = createRouter({
@ -85,7 +94,7 @@ router.afterEach((to, from) => {
// 只有路径变化时才滚动到顶部 // 只有路径变化时才滚动到顶部
if (to.path !== from.path) { if (to.path !== from.path) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
window.scrollTo({top: 0, behavior: 'instant'}) window.scrollTo({ top: 0, behavior: 'instant' })
}) })
} }
}) })

View File

@ -4,10 +4,7 @@
* @returns {number} 自适应后的尺寸 * @returns {number} 自适应后的尺寸
*/ */
export function fontSize(res) { export function fontSize(res) {
let clientWidth = let clientWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth
if (!clientWidth) return if (!clientWidth) return
let fontSize = clientWidth / 1920 let fontSize = clientWidth / 1920
return res * fontSize return res * fontSize
@ -34,3 +31,45 @@ export function randomBgColor(type = 'hex') {
.padStart(6, '0') .padStart(6, '0')
) )
} }
/**
* 根据 url 递归查找对应的 label
* @param {Array} tree - header 数组
* @param {String} path - 要匹配的 url
* @returns {String|undefined} 找到返回 label否则 undefined
*/
export function findLabelByUrl(tree, path) {
for (const node of tree) {
if (node.url === path) return node.label
if (node.hasChildren && node.children) {
const child = findLabelByUrl(node.children, path)
if (child) return child
}
}
}
/**
* get参数处理
* @param {*} params 参数
*/
export function tansParams(params) {
let result = ''
for (const propName of Object.keys(params)) {
const value = params[propName]
const part = `${encodeURIComponent(propName)}=`
if (value !== null && typeof value !== 'undefined') {
if (typeof value === 'object') {
for (const key of Object.keys(value)) {
if (value[key] !== null && typeof value[key] !== 'undefined') {
const params = `${propName}[${key}]`
const subPart = `${encodeURIComponent(params)}=`
result += `${subPart + encodeURIComponent(value[key])}&`
}
}
} else {
result += `${part + encodeURIComponent(value)}&`
}
}
}
return result
}

View File

@ -50,10 +50,7 @@ service.interceptors.response.use(
const {data, config} = response const {data, config} = response
// 处理二进制数据响应 // 处理二进制数据响应
if ( if (config.responseType === 'blob' || config.responseType === 'arraybuffer') {
config.responseType === 'blob' ||
config.responseType === 'arraybuffer'
) {
return data return data
} }
@ -90,15 +87,11 @@ function handleAuthError(config, response) {
const excludeUrls = ['/Token/Logout', '/Token/Access'] const excludeUrls = ['/Token/Logout', '/Token/Access']
if (!excludeUrls.includes(config.url)) { if (!excludeUrls.includes(config.url)) {
ElMessageBox.confirm( ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
'登录状态已过期,您可以继续留在该页面,或者重新登录', confirmButtonText: '重新登录',
'系统提示', cancelButtonText: '取消',
{ type: 'warning',
confirmButtonText: '重新登录', })
cancelButtonText: '取消',
type: 'warning',
}
)
.then(() => { .then(() => {
removeToken() removeToken()
removeUserTenantInfo() removeUserTenantInfo()
@ -127,7 +120,7 @@ function handleNetworkError(message) {
ElMessage({ ElMessage({
message: errorMsg, message: errorMsg,
type: 'error', type: 'error',
duration: 5 * 1000, duration: 1000,
}) })
} }

View File

@ -9,6 +9,9 @@ onMounted(() => {})
<template> <template>
<div class="page"> <div class="page">
<div class="banner"> <div class="banner">
<video autoplay muted loop>
<source src="/static/images/main/display.mp4" type="video/mp4" />
</video>
<div class="banner-link"><span>进入演示系统</span></div> <div class="banner-link"><span>进入演示系统</span></div>
</div> </div>
<div class="swiper"> <div class="swiper">
@ -35,11 +38,9 @@ onMounted(() => {})
.page { .page {
.banner { .banner {
width: 100%; width: 100%;
height: 800px; height: 100%;
position: relative; position: relative;
background-image: url('/static/images/main/banner.png'); background-color: #333333;
background-size: 100% 100%;
background-repeat: no-repeat;
&-link { &-link {
width: 174px; width: 174px;
height: 48px; height: 48px;
@ -64,8 +65,13 @@ onMounted(() => {})
color: $white; color: $white;
} }
} }
video {
padding: 3px;
width: 100%;
height: 100%;
border-radius: 8px;
}
} }
.swiper { .swiper {
width: 100%; width: 100%;
height: 800px; height: 800px;

View File

@ -5,10 +5,14 @@ onMounted(() => {})
<template> <template>
<div class="QR-code"> <div class="QR-code">
<img class="QR-img" :src="`./static/images/footer/QR_code.png`" /> <div class="QR-dev">
<div class="label">企业微信</div> <img class="QR-img" :src="`./static/images/footer/QR_code.png`" />
<img class="QR-img item" :src="`./static/images/footer/mark.png`" alt="" /> <div class="label">企业微信</div>
<div class="label">熊雨翔</div> </div>
<div class="QR-dev">
<img class="QR-img" :src="`./static/images/footer/mark.png`" alt="" />
<div class="label">市场联系人</div>
</div>
</div> </div>
</template> </template>
@ -16,7 +20,13 @@ onMounted(() => {})
.QR-code { .QR-code {
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: $white; display: flex;
justify-content: center;
gap: 20px;
.QR-dev {
width: 150px;
height: 200px;
}
.QR-img { .QR-img {
width: 150px; width: 150px;
height: 150px; height: 150px;
@ -26,8 +36,5 @@ onMounted(() => {})
text-align: center; text-align: center;
color: $white; color: $white;
} }
.item {
margin-top: 20px;
}
} }
</style> </style>

View File

@ -41,8 +41,8 @@ const onClick = (url) => emits('navClick', url)
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
&:hover { &:hover {
-webkit-transform: translateY(-2px); -webkit-transform: translateY(-8px);
transform: translateY(-2px); transform: translateY(-8px);
} }
} }
} }

View File

@ -26,7 +26,8 @@ const toMIIF = () => {
Copyright © 2023.Rangu Technology Co.,Ltd. All rights reserved. Copyright © 2023.Rangu Technology Co.,Ltd. All rights reserved.
燃谷科技南京有限公司 燃谷科技南京有限公司
</span> </span>
<span class="url" @click="toMIIF">苏ICP备2023030548号</span> <span class="url" @click="toMIIF">苏ICP备2023030548号-1</span>
<span class="url" @click="toMIIF">苏ICP备2023030548号-5</span>
</div> </div>
</div> </div>
</template> </template>
@ -45,7 +46,7 @@ const toMIIF = () => {
left: 190px; left: 190px;
} }
#nav { #nav {
width: calc(100% - 410px - 380px); width: calc(100% - 410px - 550px);
position: absolute; position: absolute;
left: 410px; left: 410px;
top: $top; top: $top;
@ -53,7 +54,7 @@ const toMIIF = () => {
box-sizing: border-box; box-sizing: border-box;
} }
#QR { #QR {
width: 150px; width: 320px;
height: 150px; height: 150px;
position: absolute; position: absolute;
top: $top; top: $top;

View File

@ -1,26 +1,12 @@
<script setup> <script setup>
import {useRouter} from 'vue-router'
import Nav from './nav.vue' import Nav from './nav.vue'
import {ref, onMounted} from 'vue' import {ref, onMounted} from 'vue'
import {useNavStore} from '@/store/nav'
import {storeToRefs} from 'pinia'
// Store 相关
const navStore = useNavStore()
const {navIndex} = storeToRefs(navStore)
onMounted(() => {}) onMounted(() => {})
const router = useRouter()
const toIndex = () => {
navIndex.value = '/'
router.push('/')
}
</script> </script>
<template> <template>
<div class="page flex a-c"> <div class="page flex a-c">
<div class="logo" @click="toIndex"></div> <div class="logo"></div>
<Nav class="nav"></Nav> <Nav class="nav"></Nav>
</div> </div>
</template> </template>
@ -36,7 +22,6 @@ const toIndex = () => {
height: 56px; height: 56px;
position: absolute; position: absolute;
left: 160px; left: 160px;
cursor: pointer;
} }
.nav { .nav {

View File

@ -9,7 +9,7 @@ const {navIndex} = storeToRefs(navStore)
// 响应式数据 // 响应式数据
const navList = ref([]) const navList = ref([])
const activeIndex = ref(null) const activeIndex = ref('/')
// 处理导航选择 // 处理导航选择
const handleSelect = (key) => { const handleSelect = (key) => {

View File

@ -7,7 +7,9 @@ import Banner from '@/components/Banner/index.vue'
const currentPage = ref(1) const currentPage = ref(1)
const toCompany = () => { const toCompany = () => {
window.open(window.GD_url) 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'
)
} }
onMounted(() => {}) onMounted(() => {})

425
src/views/manager/index.vue Normal file
View File

@ -0,0 +1,425 @@
<script setup>
import axios from 'axios';
import { ref, reactive, onMounted } from 'vue';
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
const API_BASE = window.config?.baseUrl
// 新闻列表
const newsList = ref([]);
// 分页与状态过滤
const page = ref(1);
const pageSize = ref(10);
const totalPages = ref(1);
const statusFilter = ref("");
const loading = ref(false);
// 编辑新闻表单
const editingNews = reactive({
title: "",
cover_image: "",
snapshot: "",
content: "",
status: "draft",
slug: "",
datetime: null,
});
const showForm = ref(false);
const isEditing = ref(false);
// 响应式引用 Quill 实例
const quillInstance = ref(null);
// 当编辑器就绪时保存实例
const onEditorReady = (quill) => {
quillInstance.value = quill;
quill.root.addEventListener('paste', handlePaste);
};
// ---------------- Quill 配置 ----------------
const editorOption= {
placeholder: "请在这里输入",
modules:{
toolbar: {
container: [
['bold', 'italic', 'underline', 'strike'],
[{ header: 1 }, { header: 2 }],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ indent: '-1' }, { indent: '+1' }],
[{ color: [] }, { background: [] }],
[{ align: [] }],
['link', 'image'],
['clean'],
],
handlers: {
image: imageHandler,
}
},
}
};
async function handlePaste(event) {
const clipboardData = event.clipboardData || window.clipboardData;
if (!clipboardData) return;
const items = clipboardData.items;
if (!items) return;
// 检查是否有图片类型
let blob = null;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') === 0) {
blob = items[i].getAsFile();
break;
}
}
if (!blob) return;
// 阻止默认粘贴行为(避免插入 base64
event.preventDefault();
try {
// 上传图片
const imageUrl = await uploadImage(blob);
// 获取当前光标位置
const quill = quillInstance.value;
const selection = quill.getSelection();
const index = selection ? selection.index : quill.getLength();
// 插入图片 URL
quill.insertEmbed(index, 'image', imageUrl);
// 可选:移动光标到图片后
quill.setSelection(index + 1);
} catch (error) {
console.error('粘贴图片上传失败:', error);
// 可选:提示用户
}
}
function imageHandler() {
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*');
input.click();
input.onchange = () => {
const file = input.files[0];
// 将图片上传到服务器
uploadImage(file)
.then((imageUrl) => {
// 将图片 URL 插入到编辑器中
const quill = quillInstance.value
const range = quill.getSelection();
quill.insertEmbed(range.index, 'image', imageUrl);
})
.catch((error) => {
console.error('图片上传失败:', error);
});
};
}
// ----------------- 获取新闻 -----------------
const fetchNews = async () => {
try {
const res = await axios.get(`${API_BASE}/news/`, {
params: {
page: page.value,
page_size: pageSize.value,
status: statusFilter.value || undefined,
},
});
if (res.data.code === 0) {
newsList.value = res.data.data;
totalPages.value = res.data.pagination.total_pages;
}
} catch (err) {
console.error("获取新闻失败", err);
}
};
// ----------------- 添加/编辑 -----------------
const saveNews = async () => {
try {
if (isEditing.value) {
await axios.post(`${API_BASE}/news/B8fpNxunbxj37x3VRcVz`, editingNews);
} else {
await axios.post(`${API_BASE}/news/VbxWW8EdJQGyWzJyvSrN`, editingNews);
}
showForm.value = false;
fetchNews();
} catch (err) {
console.error("保存新闻失败", err);
}
};
const editNews = async (news) => {
loading.value = true;
try {
const res = await axios.get(`${API_BASE}/news/details`, {
params: { slug: news.slug }
});
console.log(res.data)
if (res.data.code === 0) {
console.log(res.data.data)
Object.assign(editingNews, res.data.data);
isEditing.value = true;
showForm.value = true;
}
} finally {
loading.value = false;
}
};
const addNews = () => {
Object.assign(editingNews, {
title: "",
cover_image: "",
snapshot: "",
content: "",
status: "draft",
slug: "",
});
isEditing.value = false;
showForm.value = true;
};
// 分页
const prevPage = () => {
if (page.value > 1) {
page.value--;
fetchNews();
}
};
const nextPage = () => {
if (page.value < totalPages.value) {
page.value++;
fetchNews();
}
};
function handleImageUpload(event) {
const file = event.target.files[0];
if (!file) return;
uploadImage(file)
.then((imageUrl) => {
editingNews.cover_image = imageUrl;
})
.catch((error) => {
console.error('封面图片上传失败:', error);
});
}
function uploadImage(file) {
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append('file', file);
fetch(`${API_BASE}/update/img`, {
method: 'POST',
body: formData,
})
.then((response) => response.json())
.then((data) => {
if (data.code === 0) {
console.log(data)
resolve(`${API_BASE}/download/img/${data.data}`);
} else {
reject(data.msg);
}
})
.catch((error) => {
reject(error);
});
});
}
onMounted(() => {
fetchNews();
});
</script>
<template>
<div class="page">
<!-- 操作栏 -->
<div class="my-card flex j-s a-c toolbar">
<select v-model="statusFilter" @change="fetchNews">
<option value="">全部状态</option>
<option value="draft">草稿</option>
<option value="published">发布</option>
<option value="disable">关闭</option>
</select>
<button @click="addNews">添加新闻</button>
</div>
<!-- 新闻列表 -->
<div class="my-card">
<table>
<thead>
<tr>
<th>ID</th>
<th>标题</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="n in newsList" :key="n.slug">
<td>{{ n.id }}</td>
<td>{{ n.title }}</td>
<td>{{ n.status }}</td>
<td>
<button @click="editNews(n)">编辑</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="my-card flex j-s a-c pagination">
<button @click="prevPage" :disabled="page === 1">上一页</button>
<span>{{ page }} / {{ totalPages }}</span>
<button @click="nextPage" :disabled="page === totalPages">下一页</button>
</div>
<!-- 添加/编辑表单 -->
<div v-if="showForm" class="my-card news-form">
<h3>{{ isEditing ? "编辑新闻" : "添加新闻" }}</h3>
<label>标题: <input v-model="editingNews.title" /></label><br />
<label>发布时间:<input type="date" v-model="editingNews.datetime" /></label><br />
<label>封面图片:
<input type="file" accept="image/*" @change="handleImageUpload" />
</label>
<div v-if="editingNews.cover_image">
<img :src="editingNews.cover_image" alt="封面预览" style="max-width:200px; margin-top:10px;" />
</div>
<label>内容简介: <textarea class="snapshot" v-model="editingNews.snapshot"></textarea></label><br />
<label>内容:</label>
<quill-editor v-model:content="editingNews.content" content-type="delta"
theme="snow" style="height: 300px" :options="editorOption" @ready="onEditorReady"/>
<label>状态:
<select v-model="editingNews.status">
<option value="draft">草稿</option>
<option value="published">发布</option>
<option value="disable">关闭</option>
</select>
</label><br />
<button @click="saveNews">{{ isEditing ? "保存修改" : "添加" }}</button>
<button @click="showForm=false">取消</button>
</div>
</div>
</template>
<style lang="scss" scoped>
.my-card {
width: 80%;
padding: 20px;
margin-bottom: 20px;
margin: 20px auto;
border: 1px solid #eee;
border-radius: 8px;
background-color: #fff;
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
}
.flex {
display: flex;
}
.j-s { justify-content: space-between; }
.a-c { align-items: center; }
.toolbar select,
.toolbar button {
margin-right: 10px;
padding: 6px 12px;
font-size: 14px;
border-radius: 4px;
border: 1px solid #ccc;
background-color: #fafafa;
transition: 0.2s;
}
.toolbar button:hover {
background-color: #007BFF;
color: #fff;
border-color: #007BFF;
cursor: pointer;
}
table {
width: 100%;
border-collapse: collapse;
th, td {
padding: 10px 12px;
border: 1px solid #ddd;
text-align: left;
}
th {
background-color: #f0f0f0;
font-weight: 600;
}
tbody tr:nth-child(odd) { background-color: #fdfdfd; }
tbody tr:hover { background-color: #f5faff; }
}
button {
padding: 6px 12px;
border-radius: 4px;
border: 1px solid #ccc;
background-color: #fafafa;
transition: 0.2s;
}
button:hover {
background-color: #007BFF;
color: #fff;
border-color: #007BFF;
cursor: pointer;
}
.pagination {
gap: 10px;
button {
min-width: 80px;
text-align: center;
}
}
.snapshot{
height: 100px;
font-size: 14px;
}
.news-form input,
.news-form textarea,
.news-form select {
width: 100%;
margin-bottom: 12px;
padding: 8px 10px;
border-radius: 4px;
border: 1px solid #ccc;
font-size: 14px;
transition: 0.2s;
}
.news-form input:focus,
.news-form textarea:focus,
.news-form select:focus {
border-color: #007BFF;
outline: none;
}
.news-form button {
margin-right: 10px;
min-width: 100px;
}
.news-form h3 {
margin-bottom: 15px;
font-size: 18px;
color: #333;
}
</style>

View File

@ -5,9 +5,7 @@ import {useRoute, useRouter} from 'vue-router'
import {newsDetail} from './mock' import {newsDetail} from './mock'
const route = useRoute() const route = useRoute()
const detail = route.query const detail = route.query
detail.artical = newsDetail.find((item) => item.id == route.query.id).article detail.artical = newsDetail.find((item) => item.id == route.query.id).article
const router = useRouter() const router = useRouter()

View File

@ -0,0 +1,63 @@
export const advantages = `
<div>★ 提供业界领先的性价比解决方案。</div>
<div>★ 内置4G全网通无需外接网络即装即用。</div>
<div>★ 宽电压输入,轻松应对电压不稳等复杂情况。</div>
<div>★ 超低功耗设计,极大节约长期用电与运维成本。</div>
<div>★ 极致轻量,极大拓宽了安装位置与应用场景。</div>
<div>★ 灵活可调的侦测模式,满足从安防到巡检的全场景需求。</div>
<div>★ 具备升级能力,当前投资可平滑演进至更高专业层级。</div>
`
export const monitorSystemList = [
{
id: 1,
description: `
<div class="monitor">RGRID-Lite 是一款专为国标无人机远程识别设计的地面监测设备。它能够精准获取低空目标区域内无人机的实时位置、飞手位置、唯一SN识别码等关键信息并动态显示与记录无人机及飞手的移动轨迹。所有数据实时上传至后台系统支持长期存储与便捷回溯为空域监管提供完整的数据支撑。</div>
<div class="monitor">设备采用轻量化与一体化设计部署灵活可快速适配多种复杂安装环境。凭借超低功耗特性可搭配UPS不间断电源或太阳能供电系统实现长时间稳定运行。同时内置4G全网通模块无需依赖外部网络彻底解决供电与传输难题。</div>
<div class="monitor">RGRID-Lite 在具备卓越性能的同时,展现出优异的性价比,广泛适用于各类低空监管场景。设备支持功能定制与二次开发,可根据用户需求灵活扩展,是构建低成本、高效率无人机监管体系的理想之选。</div>
`,
},
{
id: 2,
description: `
<div class="monitor">RGRID-Ped 是一款遵循国标GB42590-2023的便携式无人机远程识别模块。它能够精准监测并获取目标空域内无人机的多项关键数据包括实时位置、飞手位置、唯一SN识别码、飞行型号、航速与高度等并实时推送至用户平台。</div>
<div class="monitor">该模块采用高度集成化设计,体积小巧、功耗极低,具备完善的接口与通信协议,可快速适配并嵌入各类无人机反制、监管或作业设备中,轻松应对有限的内部空间要求。同时,模块支持全方位的二次开发与功能定制,为各类集成应用提供可靠、灵活的低空感知核心。</div>
`,
},
{
id: 3,
description: `
<div class="monitor">RGRID-Mob 是一款符合国标 GB42590-2023 的便携式无人机远程识别设备专为机动巡检与快速部署场景设计。它可精准监测目标空域内无人机的实时位置、飞手位置、唯一SN识别码等关键信息并实时显示与记录无人机与飞手的动态轨迹。所有数据同步上传至后台支持长期存储与灵活调阅为空域执法与现场处置提供完整信息支撑。</div>
<div class="monitor">设备支持多终端协同管理,用户可通过移动端小程序实时查看数据,指挥中心亦能通过大屏系统统览全局。系统具备分级权限管理功能,支持多级账户与设备组网,实现权限分离与协同指挥。手机端集成一键导航功能,可快速定位飞手位置;指挥平台则全面监管辖区内所有设备状态与每架被侦测无人机的详细数据,真正实现“前端机动侦测—后端统一指挥”的一体化管控闭环。</div>
<div class="monitor">RGRID-Mob 重量轻、体积小可随身挂载便于野外作业与快速机动。内置4G数据传输模块开机即用无需复杂配对极大提升部署效率与操作便捷性是应对突发低空监管任务的理想移动侦测终端。</div>
`,
},
]
export const remoteDeviceList = [
{
id: 1,
description: `<div>多模态数据融合:直面不同侦测设备的数据壁垒,凭借自研融合算法,呈现深度可靠的全局态势。</div>
<div>体系化平台搭配:作为中枢神经,无缝对接与总览旗下各类专业平台,奠定一站式服务基石。</div>`,
},
{
id: 2,
description: ` <div>大量设备接入经验:具备广泛的设备兼容性,确保监管网络覆盖无死角。</div>
<div>SAAS化架构支持多租户权限管理满足不同区域、不同层级管理单位的独立运营与协同监管需求。</div>`,
},
{
id: 3,
description: `<div>大量行业内接入经验:预集成了众多厂商设备接口,极大降低了系统集成难度与成本,体现了卓越的性价比。</div>
<div>体系化平台搭配:与监管平台无缝联动,接收预警并执行指令,形成“侦-识-控”一体的完整闭环。</div>`,
},
{
id: 4,
description: `<div>多模态数据融合将飞行数据、视频流与AI识别结果深度融合输出可直接应用的业务洞察。</div>
<div>SAAS化架构支持多团队、多项目并行作业与数据隔离轻松管理大规模机队。</div>`,
},
{
id: 5,
description: ` <div>体系化平台搭配:与真实监管平台联动,是“一站式服务”中用于模拟、预测与优化的关键一环。</div>
<div>多模态数据融合:将真实地理信息、仿真数据与实时监管流融合,在虚拟空间中复现最真实的低空场景。</div>`,
},
]

View File

@ -0,0 +1,98 @@
<script setup>
import {ref, onMounted, computed} from 'vue'
import {useRoute, useRouter} from 'vue-router'
import {advantages, monitorSystemList, remoteDeviceList} from './description.js'
const route = useRoute()
const router = useRouter()
const detail = route.query
const showMonitor = computed(() => detail.type === 'monitorSystem')
onMounted(() => {})
const toBack = () => {
router.back()
}
</script>
<template>
<div class="page flex column a-c">
<div class="label">{{ detail.title }} {{ detail.subTitle }}</div>
<div class="content flex j-s">
<div class="img">
<img :src="`./static/images/${detail.imgUrl}`" alt="" />
</div>
<!-- 核心优势 -->
<div class="text" v-if="showMonitor">
<div class="title">核心优势</div>
<div v-html="advantages"></div>
</div>
<div class="text" v-if="!showMonitor">
<div v-html="remoteDeviceList[detail.id - 1].description"></div>
</div>
</div>
<!-- 介绍 -->
<div class="descripition" v-if="showMonitor">
<div v-html="monitorSystemList[detail.id - 1].description"></div>
</div>
<div class="back" @click="toBack">返回</div>
</div>
</template>
<style lang="scss" scoped>
:deep(.monitor) {
margin-bottom: 40px;
}
.label {
margin: 64px 0 20px 0;
font-family: 'PingFang SC';
font-weight: 500;
font-size: 32px;
font-weight: bold;
color: $black;
}
.time {
margin-bottom: 40px;
}
.content {
width: 1300px;
height: 550px;
margin-top: 40px;
margin-bottom: 40px;
.img {
img {
width: 550px;
height: 550px;
}
}
.text {
font-size: 24px;
line-height: 5ex;
.title {
font-size: 32px;
font-weight: bold;
margin-bottom: 10px;
}
}
}
.descripition {
width: 1000px;
font-size: 20px;
}
.back {
padding: 10px 40px;
background: #0389ff;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.25);
border-radius: 16px;
margin-bottom: 60px;
font-size: 20px;
color: $white;
cursor: pointer;
}
</style>

View File

@ -1,11 +1,43 @@
<script setup> <script setup>
import {ref, onMounted} from 'vue' import {ref, onMounted, computed} from 'vue'
import Swiper from '@/components/Swiper/index.vue' import Swiper from '@/components/Swiper/index.vue'
import Banner from '@/components/Banner/index.vue' import Banner from '@/components/Banner/index.vue'
import Project from './project.vue'
import {findLabelByUrl} from '@/utils'
import {useNavStore} from '@/store/nav'
const navStore = useNavStore()
import {useRouter} from 'vue-router'
const router = useRouter()
const title = computed(() => findLabelByUrl(window.nav.header, navStore.navIndex))
onMounted(() => {})
const currentPage = ref(1) const currentPage = ref(1)
onMounted(() => {}) const swiperItem = ref([
{id: 1, title: 'RGRID-Lite', subTitle: '无人机Remote ID远程识别设备', imgUrl: 'product/detail/6.png'},
{id: 2, title: 'RGRID-Ped', subTitle: '无人机Remote ID远程识别模块', imgUrl: 'product/detail/1.png'},
{id: 3, title: 'RGRID-Mob', subTitle: '无人机Remote ID远程识别手持设备', imgUrl: 'product/detail/4.png'},
])
// 点击标题跳转详情页
function titleClick(item) {
console.log('title clicked:', item)
router.push({
path: '/product/detail',
query: {
type: 'monitorSystem',
id: item.id,
title: item.title,
subTitle: item.subTitle,
imgUrl: item.imgUrl,
},
})
}
</script> </script>
<template> <template>
@ -13,55 +45,46 @@ onMounted(() => {})
<Banner class="banner" img="banner/product.png" /> <Banner class="banner" img="banner/product.png" />
<Swiper <Swiper
id="one" id="one"
title="产品体系" :title="title"
v-model="currentPage" v-model="currentPage"
:data="[1, 2, 3, 4]" :data="swiperItem"
:page-size="2" :page-size="3"
:show-pagination="false" :show-pagination="false"
:auto-play="false" :auto-play="false"
:sourceHeight="400"
:source-gap="80"
> >
<template #default="{item, index, isActive}"> <template #default="{item, index, isActive}">
<div class="my-card" :class="{active: isActive}"> <div class="content" @click="titleClick(item)">
<h2>{{ item }}</h2> <div class="top">
</div> <div class="title">{{ item.title }}</div>
</template> <div class="sub-title">{{ item.subTitle }}</div>
</Swiper> </div>
<Swiper <div class="line"></div>
id="two" <img :src="`./static/images/${item.imgUrl}`" class="img" :class="{active: isActive}" />
title="软件产品"
v-model="currentPage"
:data="[1, 2, 3, 4]"
:page-size="2"
:show-pagination="false"
:auto-play="false"
>
<template #default="{item, index, isActive}">
<div class="my-card" :class="{active: isActive}">
<h2>{{ item }}</h2>
</div>
</template>
</Swiper>
<Swiper
id="three"
title="硬件产品"
v-model="currentPage"
:data="[1, 2, 3, 4]"
:page-size="2"
:show-pagination="false"
:auto-play="false"
>
<template #default="{item, index, isActive}">
<div class="my-card" :class="{active: isActive}">
<h2>{{ item }}</h2>
</div> </div>
</template> </template>
</Swiper> </Swiper>
<Project />
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.my-card { .content {
background: #d9d9d9;
height: 100%; height: 100%;
.top {
background-color: #eee;
padding: 10px 0;
}
.line {
width: 100%;
height: 2px;
background-color: $black;
}
.img {
width: 300px;
height: 300px;
}
} }
</style> </style>

View File

@ -0,0 +1,95 @@
<script setup>
import Swiper from '@/components/Swiper/index.vue'
import {ref, onMounted} from 'vue'
onMounted(() => {})
// 资质证书
const certificateWidth = ref(1300)
const certificateHeight = ref(500)
const currentPage = ref(1)
const imgList = [
{
id: 1,
list: [
{id: 1, width: 290, height: 188, label: '智慧物流配送', img: 'product/1.png'},
{id: 2, width: 210, height: 280, label: '城市低空交通管理', img: 'product/5.png'},
],
},
{
id: 2,
list: [
{id: 2.1, width: 290, height: 188, label: '大型活动保障', img: 'product/2.png'},
{id: 2.2, width: 210, height: 280, label: '区域低空监管组网', img: 'product/6.png'},
],
},
{
id: 3,
list: [
{id: 3.1, width: 290, height: 188, label: '关键基础设施守护', img: 'product/3.png'},
{id: 3.2, width: 210, height: 280, label: '要地防控', img: 'product/7.png'},
],
},
{
id: 4,
list: [
{id: 4.1, width: 290, height: 188, label: '规模化无人机巡检', img: 'product/4.png'},
{id: 4.2, width: 210, height: 280, label: '移动式低空安防', img: 'product/8.png'},
],
},
]
</script>
<template>
<div class="page">
<Swiper
id="certificate"
title="应用案例"
v-model="currentPage"
:source-width="certificateWidth"
:source-height="certificateHeight"
:show-pagination="false"
:auto-play="false"
:showHover="false"
>
<template #default="{item, index, isActive}">
<div class="certificate flex j-s a-c" :class="{active: isActive}">
<div
v-for="(list, listIdx) in imgList"
:index="list.id"
class="flex j-c a-c wrap"
:class="{'last-row': listIdx === imgList.length - 1}"
>
<div v-for="(img, imgIndex) in list.list" :key="imgIndex">
<div class="label">{{ img.label }}</div>
<img
:style="{
width: $fontSize(img.width) + 'px',
height: $fontSize(img.height) + 'px',
margin: `5px ${$fontSize(10)}px`,
cursor: 'pointer',
}"
:src="`./static/images/${img.img}`"
/>
</div>
</div>
</div>
</template>
</Swiper>
</div>
</template>
<style lang="scss" scoped>
#certificate {
// margin-top: 60px;
.certificate {
margin-top: 40px;
width: 100%;
height: 100%;
.label {
font-size: 20px;
}
}
}
</style>

View File

@ -1,11 +1,45 @@
<script setup> <script setup>
import {ref, onMounted} from 'vue' import {ref, onMounted, computed} from 'vue'
import Swiper from '@/components/Swiper/index.vue' import Swiper from '@/components/Swiper/index.vue'
import Banner from '@/components/Banner/index.vue' import Banner from '@/components/Banner/index.vue'
import {findLabelByUrl} from '@/utils'
import {useNavStore} from '@/store/nav'
const navStore = useNavStore()
import {useRouter} from 'vue-router'
const router = useRouter()
const title = computed(() => findLabelByUrl(window.nav.header, navStore.navIndex))
const swiperWidth = ref(1300)
const swiperHeight = ref(500)
onMounted(() => {})
const currentPage = ref(1) const currentPage = ref(1)
onMounted(() => {}) const swiperItem = ref([
{id: 1, img: '', title: '低空全域运营中枢'},
{id: 2, img: '', title: '无人机精准识别与管控系统'},
{id: 3, img: '', title: '开放式反制协同指挥平台'},
{id: 4, img: '', title: '全自动智能巡检作业平台'},
{id: 5, img: '', title: '低空数字孪生与推演平台'},
])
// 点击标题跳转详情页
function titleClick(item) {
console.log('title clicked:', item)
router.push({
path: '/product/detail',
query: {
type: 'remoteDevice',
id: item.id,
title: item.title,
img: item.img,
},
})
}
</script> </script>
<template> <template>
@ -13,46 +47,23 @@ onMounted(() => {})
<Banner class="banner" img="banner/product.png" /> <Banner class="banner" img="banner/product.png" />
<Swiper <Swiper
id="one" id="one"
title="产品体系" :title="title"
v-model="currentPage" v-model="currentPage"
:data="[1, 2, 3, 4]"
:page-size="2"
:show-pagination="false" :show-pagination="false"
:auto-play="false" :auto-play="false"
:source-width="swiperWidth"
:source-height="swiperHeight"
:showHover="false"
> >
<template #default="{item, index, isActive}"> <template #default="{item, index, isActive}">
<div class="my-card" :class="{active: isActive}"> <div class="content flex j-s a-c wrap" @click="titleClick(item)">
<h2>{{ item }}</h2> <div v-for="list in swiperItem" :key="list.id" class="list">
</div> <div class="top">
</template> <div class="title">{{ list.title }}</div>
</Swiper> </div>
<Swiper <div class="line"></div>
id="two" <div class="my-card" :class="{active: isActive}"></div>
title="软件产品" </div>
v-model="currentPage"
:data="[1, 2, 3, 4]"
:page-size="2"
:show-pagination="false"
:auto-play="false"
>
<template #default="{item, index, isActive}">
<div class="my-card" :class="{active: isActive}">
<h2>{{ item }}</h2>
</div>
</template>
</Swiper>
<Swiper
id="three"
title="硬件产品"
v-model="currentPage"
:data="[1, 2, 3, 4]"
:page-size="2"
:show-pagination="false"
:auto-play="false"
>
<template #default="{item, index, isActive}">
<div class="my-card" :class="{active: isActive}">
<h2>{{ item }}</h2>
</div> </div>
</template> </template>
</Swiper> </Swiper>
@ -60,8 +71,21 @@ onMounted(() => {})
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.my-card { .content {
background: #d9d9d9;
height: 100%; height: 100%;
border: 1px solid red;
.top {
background-color: #eee;
padding: 10px 0;
}
.line {
width: 100%;
height: 2px;
background-color: $black;
}
.img {
width: 300px;
height: 300px;
}
} }
</style> </style>