1
This commit is contained in:
+19
-1
@@ -3,8 +3,26 @@ import request from '@/utils/request'
|
||||
// 获取访问接口
|
||||
export function visit(data) {
|
||||
return request({
|
||||
url: '/tracking',
|
||||
url: '/tracking/',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取新闻列表
|
||||
export function getNews(params) {
|
||||
return request({
|
||||
url: '/news',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取新闻列表
|
||||
export function getNewsDetail(params) {
|
||||
return request({
|
||||
url: '/news/details',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,5 +8,7 @@ $nav_font_size: 18px;
|
||||
|
||||
$border: 2px solid red;
|
||||
|
||||
$border_radius: 8px;
|
||||
|
||||
// header nav
|
||||
$header_nav_height: 80px;
|
||||
|
||||
@@ -5,18 +5,18 @@ onMounted(() => {})
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p>ID/无人机低空监管设备 无人机定位设备/无人机飞手定位设备/无人机侦测设备/Remote ID无人机远程识别模块 Remote</p>
|
||||
<p>Remote ID无人机识别设备/符合GB-42590-2023/无人机远程识别发射模块 无人机远程识别设备/无人机RID/Remote</p>
|
||||
<p>ID/无人机低空监管设备 无人机定位设备/无人机飞手定位设备/无人机侦测设备/software ID无人机远程识别模块 software</p>
|
||||
<p>software ID无人机识别设备/符合GB-42590-2023/无人机远程识别发射模块 无人机远程识别设备/无人机RID/software</p>
|
||||
<p>ID无人机远程识别广播模块/RID无人机识别/无人机飞手远程定位设备</p>
|
||||
<p>无人机测距模块/无人机侦测监管设备/RID无人机识别/无人机定位器/无人机飞手定位器</p>
|
||||
<p>无人机侦测定位设备/迷你无人机识别模块/低空经济无人机识别设备/单兵无人机侦测识别设备</p>
|
||||
<p>无人机探测器/防撞机无人机定位设备/Remote ID无人机预警仪</p>
|
||||
<p>全向无人机探测设备/无人机定位飞手定位设备/安防安保无人机监测设备/RID无人机识别/Remote ID远程识别模块</p>
|
||||
<p>无人机探测器/防撞机无人机定位设备/software ID无人机预警仪</p>
|
||||
<p>全向无人机探测设备/无人机定位飞手定位设备/安防安保无人机监测设备/RID无人机识别/software 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无人机定位模块/software</p>
|
||||
<p>ID无人机定位跟踪设备带飞手定位飞行轨迹 自主研发无人机侦测定位模块/software</p>
|
||||
<p>ID无人机远程识别设备/飞机定位飞行轨迹飞手定位无人机识别模块源头厂家 无人机标签识别设备/software</p>
|
||||
<p>
|
||||
ID无人机侦测定位设备/远程广播模块识别 无人机远程识别地面站/RID无人机识别/无人机巡检智能识别模块/无人机定位飞手定位
|
||||
</p>
|
||||
|
||||
+30
-29
@@ -5,47 +5,48 @@ import {onMounted, ref, watch} from 'vue'
|
||||
import {visit} from '@/api/index'
|
||||
import axios from 'axios'
|
||||
|
||||
onMounted(() => {
|
||||
getUrl()
|
||||
})
|
||||
|
||||
const referrer = ref('')
|
||||
const referrer = ref(document.referrer || '')
|
||||
const ipUrl = ref('')
|
||||
|
||||
console.log('11===>', window.location)
|
||||
function getUrl() {
|
||||
referrer.value = document.referrer
|
||||
const CACHE_KEY = 'ip'
|
||||
const TIME_KEY = 'ipFetchTime'
|
||||
const CACHE_MIN = 60 * 60 * 1000 // 1h
|
||||
|
||||
axios
|
||||
.get('https://api64.ipify.org?format=json')
|
||||
.then(({data}) => {
|
||||
ipUrl.value = data.ip
|
||||
})
|
||||
.catch(console.error)
|
||||
async function getUrl() {
|
||||
const now = Date.now()
|
||||
const cachedIp = sessionStorage.getItem(CACHE_KEY)
|
||||
const cachedTime = sessionStorage.getItem(TIME_KEY)
|
||||
|
||||
if (cachedIp && cachedTime && now - Number(cachedTime) < CACHE_MIN) {
|
||||
ipUrl.value = cachedIp
|
||||
return cachedIp
|
||||
}
|
||||
|
||||
try {
|
||||
const {data} = await axios.get('https://api64.ipify.org?format=json')
|
||||
ipUrl.value = data.ip
|
||||
sessionStorage.setItem(CACHE_KEY, data.ip)
|
||||
sessionStorage.setItem(TIME_KEY, now.toString())
|
||||
postVisit()
|
||||
} catch (e) {
|
||||
console.error('获取 IP 失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function postVisit() {
|
||||
let params = {
|
||||
function postVisit() {
|
||||
const params = {
|
||||
timestamp: new Date(),
|
||||
url: window.location.href,
|
||||
referrer: referrer.value,
|
||||
real_ip: ipUrl.value,
|
||||
}
|
||||
let {code, msg} = await visit(params)
|
||||
|
||||
visit(params).then((res) => {})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [referrer.value, ipUrl.value],
|
||||
([refer, ipUrl]) => {
|
||||
if (refer && ipUrl) {
|
||||
postVisit()
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
)
|
||||
onMounted(async () => {
|
||||
await getUrl()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
+20
-6
@@ -3,20 +3,22 @@ import {createWebHashHistory, createRouter} from 'vue-router'
|
||||
import HomeView from '@/views/homepage/index.vue'
|
||||
|
||||
const routes = [
|
||||
// ============================================ 首页 ============================================
|
||||
{path: '/', component: HomeView},
|
||||
|
||||
// ============================================ 产品中心 ============================================
|
||||
{
|
||||
path: '/product/monitorSystem',
|
||||
name: 'MonitorSystem',
|
||||
component: () => import('@/views/product/monitorSystem.vue'),
|
||||
path: '/product/hardwareSystem',
|
||||
name: 'HardwareSystem',
|
||||
component: () => import('@/views/product/hardwareSystem.vue'),
|
||||
meta: {
|
||||
title: '低空监管体系',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/product/remoteDevice',
|
||||
name: 'RemoteDevice',
|
||||
component: () => import('@/views/product/remoteDevice.vue'),
|
||||
path: '/product/softwareSystem',
|
||||
name: 'SoftwareSystem',
|
||||
component: () => import('@/views/product/softwareSystem.vue'),
|
||||
meta: {
|
||||
title: '低空远程识别设备',
|
||||
},
|
||||
@@ -29,6 +31,7 @@ const routes = [
|
||||
title: '产品详情',
|
||||
},
|
||||
},
|
||||
|
||||
// ============================================ 服务与支撑 ============================================
|
||||
{
|
||||
path: '/services',
|
||||
@@ -38,6 +41,7 @@ const routes = [
|
||||
title: '服务与支撑',
|
||||
},
|
||||
},
|
||||
|
||||
// ============================================ 新闻中心 ============================================
|
||||
{
|
||||
path: '/news',
|
||||
@@ -55,6 +59,7 @@ const routes = [
|
||||
title: '新闻详情',
|
||||
},
|
||||
},
|
||||
|
||||
// ============================================ 关于我们 ============================================
|
||||
{
|
||||
path: '/about',
|
||||
@@ -64,6 +69,7 @@ const routes = [
|
||||
title: '关于我们',
|
||||
},
|
||||
},
|
||||
|
||||
// ============================================ 联系我们 ============================================
|
||||
{
|
||||
path: '/link',
|
||||
@@ -73,6 +79,7 @@ const routes = [
|
||||
title: '联系我们',
|
||||
},
|
||||
},
|
||||
|
||||
// ============================================ 下载中心 ============================================
|
||||
{
|
||||
path: '/download',
|
||||
@@ -89,6 +96,13 @@ const router = createRouter({
|
||||
routes,
|
||||
})
|
||||
|
||||
// router.beforeEach(async (to, from, next) => {
|
||||
// const {useNavStore} = await import('@/store/nav.js')
|
||||
// const navStore = useNavStore()
|
||||
// navStore.setLoad(true)
|
||||
// next()
|
||||
// })
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
// 只有路径变化时才滚动到顶部
|
||||
if (to.path !== from.path) {
|
||||
|
||||
@@ -5,6 +5,7 @@ export const useNavStore = defineStore(
|
||||
'nav',
|
||||
() => {
|
||||
const navIndex = ref('/')
|
||||
const showLoad = ref(true)
|
||||
|
||||
const changeNavIndex = (id) => {
|
||||
if (!id) {
|
||||
@@ -14,9 +15,16 @@ export const useNavStore = defineStore(
|
||||
}
|
||||
}
|
||||
|
||||
const setLoad = (status) => {
|
||||
console.log('status===>', status)
|
||||
showLoad.value = status
|
||||
}
|
||||
|
||||
return {
|
||||
navIndex,
|
||||
showLoad,
|
||||
changeNavIndex,
|
||||
setLoad,
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import CryptoJS from 'crypto-js'
|
||||
|
||||
const key = CryptoJS.enc.Utf8.parse('k+tsU5mZz1BY+P8z')
|
||||
const iv = CryptoJS.enc.Utf8.parse('k+tsU5mZz1BY+P8z')
|
||||
|
||||
// crypto-js加密
|
||||
export function cryptoEncrypt(word) {
|
||||
const srcs = CryptoJS.enc.Utf8.parse(word)
|
||||
const encrypted = CryptoJS.AES.encrypt(srcs, key, {
|
||||
iv,
|
||||
mode: CryptoJS.mode.CBC,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
})
|
||||
return encrypted.ciphertext.toString(CryptoJS.enc.Base64)
|
||||
}
|
||||
|
||||
// crypto-js解密
|
||||
export function cryptoDecrypt(word) {
|
||||
const encryptedHexStr = CryptoJS.enc.Base64.parse(word)
|
||||
const srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr)
|
||||
const decrypt = CryptoJS.AES.decrypt(srcs, key, {
|
||||
iv,
|
||||
mode: CryptoJS.mode.CBC,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
})
|
||||
const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8)
|
||||
return decryptedStr.toString()
|
||||
}
|
||||
@@ -2,9 +2,7 @@
|
||||
* axios 封装
|
||||
*/
|
||||
import axios from 'axios'
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import {tansParams} from '@/utils/index'
|
||||
import {getToken, removeToken, removeUserTenantInfo} from '@/utils/auth'
|
||||
|
||||
// 配置基础URL
|
||||
const baseURL = window.config?.baseUrl || 'http://work.rangutech.cn:9131'
|
||||
@@ -21,14 +19,6 @@ const service = axios.create({
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
// 是否需要设置 token
|
||||
const isToken = (config.headers || {}).isToken === false
|
||||
|
||||
// 添加token到请求头
|
||||
if (getToken() && !isToken) {
|
||||
config.headers.Authorization = `Bearer ${getToken()}`
|
||||
}
|
||||
|
||||
// GET请求参数处理
|
||||
if (config.method?.toLowerCase() === 'get' && config.params) {
|
||||
const urlParams = tansParams(config.params)
|
||||
@@ -48,80 +38,22 @@ service.interceptors.request.use(
|
||||
service.interceptors.response.use(
|
||||
(response) => {
|
||||
const {data, config} = response
|
||||
|
||||
// 处理二进制数据响应
|
||||
if (config.responseType === 'blob' || config.responseType === 'arraybuffer') {
|
||||
return data
|
||||
}
|
||||
|
||||
// 正常响应处理
|
||||
if (response.status === 200) {
|
||||
return data
|
||||
}
|
||||
|
||||
// 异常响应处理
|
||||
return Promise.reject(response)
|
||||
},
|
||||
(error) => {
|
||||
const {config, response, message} = error
|
||||
|
||||
console.error('Response error:', {config, response, message})
|
||||
|
||||
// 处理认证错误
|
||||
if (response && [401, 471, 472].includes(response.status)) {
|
||||
handleAuthError(config, response)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
// 处理网络错误
|
||||
handleNetworkError(message)
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 处理认证错误
|
||||
*/
|
||||
function handleAuthError(config, response) {
|
||||
const excludeUrls = ['/Token/Logout', '/Token/Access']
|
||||
|
||||
if (!excludeUrls.includes(config.url)) {
|
||||
ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
|
||||
confirmButtonText: '重新登录',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
removeToken()
|
||||
removeUserTenantInfo()
|
||||
// 这里需要导入store
|
||||
// store.dispatch('LogOut').then(() => {});
|
||||
window.location.reload() // 简单重载页面
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消操作
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理网络错误
|
||||
*/
|
||||
function handleNetworkError(message) {
|
||||
let errorMsg = message
|
||||
|
||||
if (message === 'Network Error') {
|
||||
errorMsg = '网络连接异常,请检查网络设置'
|
||||
} else if (message.includes('timeout')) {
|
||||
errorMsg = '请求超时,请稍后重试'
|
||||
}
|
||||
|
||||
ElMessage({
|
||||
message: errorMsg,
|
||||
type: 'error',
|
||||
duration: 1000,
|
||||
})
|
||||
}
|
||||
|
||||
export default service
|
||||
|
||||
@@ -1,44 +1,72 @@
|
||||
<script setup>
|
||||
import Swiper from '@/components/Swiper/index.vue'
|
||||
|
||||
import {ref, onMounted} from 'vue'
|
||||
import {cryptoEncrypt} from '@/utils/cryptojs'
|
||||
import {useRouter} from 'vue-router'
|
||||
|
||||
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]
|
||||
|
||||
onMounted(() => {})
|
||||
|
||||
function onVideoLoaded() {
|
||||
videoLoaded.value = false
|
||||
}
|
||||
|
||||
function toUrl(item) {
|
||||
router.push({
|
||||
path: '/product/detail',
|
||||
query: {type: cryptoEncrypt(JSON.stringify(item))},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="page" v-loading="videoLoaded">
|
||||
<div class="banner">
|
||||
<video autoplay muted loop>
|
||||
<source src="/static/images/main/display.mp4" type="video/mp4" />
|
||||
<video autoplay muted loop @loadeddata="onVideoLoaded">
|
||||
<source src="/static/video/display.mp4" type="video/mp4" />
|
||||
</video>
|
||||
<div class="banner-link"><span>进入演示系统</span></div>
|
||||
</div>
|
||||
<div class="swiper">
|
||||
<Swiper
|
||||
id="home_swiper"
|
||||
title="产品体系"
|
||||
v-model="currentPage"
|
||||
:data="[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
|
||||
:page-size="4"
|
||||
:show-pagination="true"
|
||||
:auto-play="true"
|
||||
>
|
||||
<template #default="{item, index, isActive}">
|
||||
<div class="my-card" :class="{active: isActive}">
|
||||
<h2>{{ item }}</h2>
|
||||
<Swiper
|
||||
id="home_swiper"
|
||||
title="产品体系"
|
||||
v-model="currentPage"
|
||||
:data="list"
|
||||
:page-size="3"
|
||||
:show-pagination="true"
|
||||
:source-width="swiperWidth"
|
||||
:source-height="swiperHeight"
|
||||
:auto-play="true"
|
||||
>
|
||||
<template #default="{item, index, isActive}">
|
||||
<div class="list" @click="toUrl(item)">
|
||||
<div class="top">
|
||||
<div class="title">{{ item.title }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</Swiper>
|
||||
</div>
|
||||
<div class="card flex column a-c j-c" :class="{active: isActive}">
|
||||
<img :class="item.type === 'hardwareSystem' ? 'm-img' : 'r-img'" :src="`./static/images/${item.imgUrl}`" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Swiper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
.banner {
|
||||
.placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.banner {
|
||||
width: 100%;
|
||||
height: 800px;
|
||||
position: relative;
|
||||
background-color: #333333;
|
||||
&-link {
|
||||
@@ -68,16 +96,31 @@ onMounted(() => {})
|
||||
video {
|
||||
padding: 3px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
.swiper {
|
||||
width: 100%;
|
||||
height: 800px;
|
||||
.my-card {
|
||||
background: #d9d9d9;
|
||||
height: 100%;
|
||||
|
||||
.list {
|
||||
height: 100%;
|
||||
border: 1px solid #eee;
|
||||
.top {
|
||||
height: 50px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
background-color: #0389ff;
|
||||
color: #fff;
|
||||
padding: 15px 0;
|
||||
}
|
||||
.card {
|
||||
height: calc(100% - 50px - 30px);
|
||||
}
|
||||
.r-img {
|
||||
width: 90%;
|
||||
height: auto;
|
||||
}
|
||||
.m-img {
|
||||
width: 70%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+54
-17
@@ -1,15 +1,46 @@
|
||||
<script setup>
|
||||
import {onMounted} from 'vue'
|
||||
import {onMounted, ref} from 'vue'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
|
||||
import {newsDetail} from './mock'
|
||||
import {getNewsDetail} from '@/api/index'
|
||||
import {ElMessage} from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const detail = route.query
|
||||
detail.artical = newsDetail.find((item) => item.id == route.query.id).article
|
||||
const showLoad = ref(true)
|
||||
const newsTitle = ref('')
|
||||
const newsSubTitle = ref('')
|
||||
const newsCoverImg = ref('')
|
||||
const newsContent = ref('')
|
||||
|
||||
const getDetail = () => {
|
||||
showLoad.value = true
|
||||
let query = {
|
||||
slug: detail.slug,
|
||||
}
|
||||
getNewsDetail(query)
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
let {content, cover_image, snapshot, title} = res.data
|
||||
newsTitle.value = title
|
||||
newsSubTitle.value = snapshot
|
||||
newsCoverImg.value = cover_image
|
||||
newsContent.value = content
|
||||
showLoad.value = false
|
||||
} else {
|
||||
ElMessage.error(res.msg)
|
||||
showLoad.value = false
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
showLoad.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
onMounted(() => {})
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
|
||||
const toBack = () => {
|
||||
router.back()
|
||||
@@ -17,19 +48,22 @@ const toBack = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page flex column a-c">
|
||||
<div class="label">
|
||||
{{ detail.label }}
|
||||
</div>
|
||||
<div class="time">
|
||||
{{ detail.time }}
|
||||
</div>
|
||||
<div class="img">
|
||||
<img :src="`./static/images/${detail.imgUrl}`" alt="" />
|
||||
</div>
|
||||
<div class="artical">
|
||||
{{ detail.artical }}
|
||||
<div class="page flex column a-c" v-loading="showLoad">
|
||||
<div v-if="newsTitle">
|
||||
<h3 class="label">
|
||||
{{ newsTitle }}
|
||||
</h3>
|
||||
<h3 class="time" v-if="newsSubTitle">
|
||||
{{ newsSubTitle }}
|
||||
</h3>
|
||||
<div class="img" v-if="newsCoverImg">
|
||||
<img :src="newsCoverImg" alt="" />
|
||||
</div>
|
||||
<div class="artical" v-if="newsContent">
|
||||
{{ newsContent }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>暂无内容</div>
|
||||
<div class="back" @click="toBack">返回</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -43,11 +77,14 @@ const toBack = () => {
|
||||
color: $black;
|
||||
}
|
||||
.time {
|
||||
font-family: 'PingFang SC';
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.img {
|
||||
width: 1200px;
|
||||
height: 600px;
|
||||
height: auto;
|
||||
margin-bottom: 40px;
|
||||
img {
|
||||
width: 100%;
|
||||
|
||||
+43
-19
@@ -5,35 +5,55 @@ import Banner from '@/components/Banner/index.vue'
|
||||
import Swiper from '@/components/Swiper/index.vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {onBeforeRouteLeave} from 'vue-router'
|
||||
import {list} from './mock'
|
||||
import {getNews} from '@/api/index'
|
||||
|
||||
const currentPage = ref(1)
|
||||
|
||||
const showLoad = ref(true)
|
||||
const containerWidth = ref(1700)
|
||||
const containerHeight = ref(1400)
|
||||
|
||||
onMounted(() => {})
|
||||
onMounted(() => {
|
||||
getNewList()
|
||||
})
|
||||
|
||||
let router = useRouter()
|
||||
|
||||
let scrollTop = 0
|
||||
|
||||
const newList = ref([])
|
||||
|
||||
const page = ref({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
totalPages: 0,
|
||||
})
|
||||
|
||||
async function getNewList() {
|
||||
let query = {
|
||||
status: 'published',
|
||||
page: page.value.pageNum,
|
||||
page_size: page.value.pageSize,
|
||||
}
|
||||
let {code, data, pagination} = await getNews(query)
|
||||
if (code === 0) {
|
||||
newList.value = data
|
||||
page.value.totalPages = pagination.total_pages
|
||||
showLoad.value = false
|
||||
} else {
|
||||
showLoad.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const toDetail = (item) => {
|
||||
router.push({
|
||||
path: '/news/detail',
|
||||
query: {
|
||||
id: item.id,
|
||||
imgUrl: item.imgUrl,
|
||||
label: item.label,
|
||||
time: item.time,
|
||||
subLabel: item?.subLabel,
|
||||
slug: item.slug,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let scrollTop = 0
|
||||
|
||||
onDeactivated(() => {
|
||||
scrollTop = document.documentElement.scrollTop || document.body.scrollTop
|
||||
})
|
||||
|
||||
onActivated(async () => {
|
||||
await nextTick()
|
||||
// 再强制等一帧布局
|
||||
@@ -42,6 +62,10 @@ onActivated(async () => {
|
||||
})
|
||||
})
|
||||
|
||||
onDeactivated(() => {
|
||||
scrollTop = document.documentElement.scrollTop || document.body.scrollTop
|
||||
})
|
||||
|
||||
onBeforeRouteLeave((to) => {
|
||||
if (to.name === 'Detail') return
|
||||
|
||||
@@ -55,7 +79,7 @@ onBeforeRouteLeave((to) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="page" v-loading="showLoad">
|
||||
<Banner class="banner" img="banner/news.png" />
|
||||
<Swiper
|
||||
id="one"
|
||||
@@ -72,19 +96,19 @@ onBeforeRouteLeave((to) => {
|
||||
<div
|
||||
class="news flex j-s"
|
||||
:class="{active: isActive}"
|
||||
v-for="item in list"
|
||||
v-for="item in newList"
|
||||
:key="item.id"
|
||||
@click="toDetail(item)"
|
||||
>
|
||||
<div class="img">
|
||||
<img :src="`./static/images/${item.imgUrl}`" />
|
||||
<img :src="item.cover_image" />
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="label">{{ item.label }}</div>
|
||||
<div class="time">{{ item.time }}</div>
|
||||
<div class="label">{{ item.title }}</div>
|
||||
<div class="time">{{ item?.time }}</div>
|
||||
<div class="sub-label">{{ item?.subLabel }}</div>
|
||||
<div class="text">{{ item.content }}</div>
|
||||
<div class="text">{{ item.snapshot }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
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>`,
|
||||
},
|
||||
]
|
||||
+101
-30
@@ -1,16 +1,24 @@
|
||||
<script setup>
|
||||
import {ref, onMounted, computed} from 'vue'
|
||||
import {ref, onMounted, shallowRef, watchEffect} from 'vue'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {advantages, monitorSystemList, remoteDeviceList} from './description.js'
|
||||
import {cryptoDecrypt} from '@/utils/cryptojs.js'
|
||||
|
||||
const advantages = window.advantages
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const detail = route.query
|
||||
const query = JSON.parse(cryptoDecrypt(route.query.type))
|
||||
|
||||
const showMonitor = computed(() => detail.type === 'monitorSystem')
|
||||
const videoLoaded = ref(false)
|
||||
|
||||
onMounted(() => {})
|
||||
onMounted(() => {
|
||||
videoLoaded.value = query.type !== 'softwareSystem' ? false : true
|
||||
})
|
||||
|
||||
function onVideoLoaded() {
|
||||
videoLoaded.value = false
|
||||
}
|
||||
|
||||
const toBack = () => {
|
||||
router.back()
|
||||
@@ -18,57 +26,73 @@ const toBack = () => {
|
||||
</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 class="page flex column a-c" v-loading="videoLoaded">
|
||||
<h3 class="label">{{ query.title }} {{ query?.subTitle }}</h3>
|
||||
|
||||
<!-- 硬件内容 -->
|
||||
<template v-if="query.type === 'hardwareSystem'">
|
||||
<div class="content flex j-s hardware">
|
||||
<img class="img" :src="`./static/images/${query.imgUrl}`" alt="" />
|
||||
<div class="text">
|
||||
<h4 class="title">核心优势:</h4>
|
||||
<div v-html="advantages"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 核心优势 -->
|
||||
<div class="text" v-if="showMonitor">
|
||||
<div class="title">核心优势:</div>
|
||||
<div v-html="advantages"></div>
|
||||
<div class="descripition">
|
||||
<div v-html="query.description"></div>
|
||||
</div>
|
||||
<div class="text" v-if="!showMonitor">
|
||||
<div v-html="remoteDeviceList[detail.id - 1].description"></div>
|
||||
</template>
|
||||
|
||||
<!-- 软件 -->
|
||||
<template v-else>
|
||||
<div class="content software flex j-s">
|
||||
<video autoplay muted loop @loadeddata="onVideoLoaded" v-show="!videoLoaded">
|
||||
<source :src="`./static/video/${query.video}`" type="video/mp4" />
|
||||
</video>
|
||||
<div class="descripition">
|
||||
<div v-html="query.description"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-list flex j-c a-c wrap">
|
||||
<div class="card" v-for="item in query.card" :key="item.id">
|
||||
<h4 class="title">{{ item.title }}</h4>
|
||||
<div class="content">{{ item.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 介绍 -->
|
||||
<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) {
|
||||
:deep(.hardware) {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 64px 0 20px 0;
|
||||
margin: 40px 0 0 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;
|
||||
}
|
||||
|
||||
.hardware {
|
||||
width: 1300px;
|
||||
.img {
|
||||
img {
|
||||
width: 550px;
|
||||
height: 550px;
|
||||
}
|
||||
width: 550px;
|
||||
height: 550px;
|
||||
}
|
||||
.text {
|
||||
font-size: 24px;
|
||||
@@ -80,12 +104,59 @@ const toBack = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.software {
|
||||
width: 1700px;
|
||||
video {
|
||||
width: 65%;
|
||||
height: auto;
|
||||
margin-right: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-list {
|
||||
width: 1200px;
|
||||
height: 100%;
|
||||
.card {
|
||||
width: 350px;
|
||||
height: 300px;
|
||||
margin: 20px;
|
||||
text-align: center;
|
||||
border-radius: $border_radius;
|
||||
transition: all 0.3s ease-in-out;
|
||||
border: 1px solid #eee;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.title {
|
||||
background-color: #0389ff;
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
color: #fff;
|
||||
font-size: 28px;
|
||||
border-radius: $border_radius $border_radius 0 0;
|
||||
}
|
||||
.content {
|
||||
font-size: 18px;
|
||||
padding: 0 20px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 6;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.descripition {
|
||||
width: 1000px;
|
||||
font-size: 20px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.back {
|
||||
margin-top: 40px;
|
||||
padding: 10px 40px;
|
||||
background: #0389ff;
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.25);
|
||||
|
||||
@@ -5,10 +5,13 @@ import Banner from '@/components/Banner/index.vue'
|
||||
import Project from './project.vue'
|
||||
|
||||
import {findLabelByUrl} from '@/utils'
|
||||
import {cryptoEncrypt} from '@/utils/cryptojs'
|
||||
|
||||
import {useNavStore} from '@/store/nav'
|
||||
const navStore = useNavStore()
|
||||
|
||||
const hardwareSystemList = window.hardwareSystemList
|
||||
|
||||
import {useRouter} from 'vue-router'
|
||||
const router = useRouter()
|
||||
|
||||
@@ -18,24 +21,11 @@ onMounted(() => {})
|
||||
|
||||
const currentPage = ref(1)
|
||||
|
||||
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,
|
||||
},
|
||||
query: {type: cryptoEncrypt(JSON.stringify(item))},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -47,11 +37,12 @@ function titleClick(item) {
|
||||
id="one"
|
||||
:title="title"
|
||||
v-model="currentPage"
|
||||
:data="swiperItem"
|
||||
:data="hardwareSystemList"
|
||||
:page-size="3"
|
||||
:show-pagination="false"
|
||||
:auto-play="false"
|
||||
:sourceHeight="400"
|
||||
:source-width="300"
|
||||
:source-height="400"
|
||||
:source-gap="80"
|
||||
>
|
||||
<template #default="{item, index, isActive}">
|
||||
@@ -60,7 +51,6 @@ function titleClick(item) {
|
||||
<div class="title">{{ item.title }}</div>
|
||||
<div class="sub-title">{{ item.subTitle }}</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<img :src="`./static/images/${item.imgUrl}`" class="img" :class="{active: isActive}" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -74,14 +64,12 @@ function titleClick(item) {
|
||||
.content {
|
||||
height: 100%;
|
||||
.top {
|
||||
background-color: #eee;
|
||||
background-color: #0389ff;
|
||||
color: #fff;
|
||||
letter-spacing: 1px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.line {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: $black;
|
||||
}
|
||||
|
||||
.img {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
@@ -55,12 +55,7 @@ const imgList = [
|
||||
>
|
||||
<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="(list, listIdx) in imgList" :index="list.id" class="flex j-c a-c wrap">
|
||||
<div v-for="(img, imgIndex) in list.list" :key="imgIndex">
|
||||
<div class="label">{{ img.label }}</div>
|
||||
<img
|
||||
|
||||
@@ -4,7 +4,12 @@ import Swiper from '@/components/Swiper/index.vue'
|
||||
import Banner from '@/components/Banner/index.vue'
|
||||
import {findLabelByUrl} from '@/utils'
|
||||
|
||||
import {cryptoEncrypt} from '@/utils/cryptojs'
|
||||
|
||||
import {useNavStore} from '@/store/nav'
|
||||
|
||||
const softwareSystemList = window.softwareSystemList
|
||||
|
||||
const navStore = useNavStore()
|
||||
|
||||
import {useRouter} from 'vue-router'
|
||||
@@ -13,31 +18,17 @@ const router = useRouter()
|
||||
const title = computed(() => findLabelByUrl(window.nav.header, navStore.navIndex))
|
||||
|
||||
const swiperWidth = ref(1300)
|
||||
const swiperHeight = ref(500)
|
||||
const swiperHeight = ref(800)
|
||||
|
||||
onMounted(() => {})
|
||||
|
||||
const currentPage = ref(1)
|
||||
|
||||
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,
|
||||
},
|
||||
query: {type: cryptoEncrypt(JSON.stringify(item))},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -55,14 +46,14 @@ function titleClick(item) {
|
||||
:source-height="swiperHeight"
|
||||
:showHover="false"
|
||||
>
|
||||
<template #default="{item, index, isActive}">
|
||||
<div class="content flex j-s a-c wrap" @click="titleClick(item)">
|
||||
<div v-for="list in swiperItem" :key="list.id" class="list">
|
||||
<template #default="{isActive}">
|
||||
<div class="content flex j-c a-c wrap">
|
||||
<div v-for="list in softwareSystemList" :key="list.id" class="list" @click="titleClick(list)">
|
||||
<div class="top">
|
||||
<div class="title">{{ list.title }}</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="my-card" :class="{active: isActive}"></div>
|
||||
<img class="img" :src="`./static/images/${list.imgUrl}`" :class="{active: isActive}" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -73,19 +64,33 @@ function titleClick(item) {
|
||||
<style lang="scss" scoped>
|
||||
.content {
|
||||
height: 100%;
|
||||
border: 1px solid red;
|
||||
.top {
|
||||
background-color: #eee;
|
||||
padding: 10px 0;
|
||||
.list {
|
||||
margin-top: 60px;
|
||||
margin: 0 15px;
|
||||
width: 400px;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
transform: translateY(-50px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
.top {
|
||||
border-radius: 8px 8px 0 0;
|
||||
background-color: #0389ff;
|
||||
color: #fff;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: $black;
|
||||
}
|
||||
.img {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user