Compare commits

...

15 Commits

Author SHA1 Message Date
bunny 8ca3934de7 💚 📦 打包设置 2025-05-24 15:59:20 +08:00
bunny 0598597e3e 💚 📦 打包设置 2025-05-24 15:49:55 +08:00
bunny ef94848f7c Merge branch 'dev' of http://129.211.31.58:3000/Large-Visual-Screen/vehicle-monitor into dev
# Conflicts:
#	build/plugins.ts
2025-05-24 15:47:01 +08:00
bunny f60447a774 📦 打包设置 2025-05-24 15:44:38 +08:00
bunny 87fbf7e177 📦 打包设置 2025-05-24 15:38:39 +08:00
bunny aefc88fe5d CI测试 2025-05-24 15:17:55 +08:00
bunny 11f78ea88a CI测试 2025-05-24 15:15:32 +08:00
bunny 8578e6146f CI 测试 2025-05-24 15:09:56 +08:00
bunny bc368d57a1 CI 测试 2025-05-24 15:05:28 +08:00
bunny 79efc8f1fc 👷‍♂️ CI 测试 2025-05-24 14:26:14 +08:00
bunny 8a811e8445 👷‍♂️ ci测试 2025-05-24 14:24:40 +08:00
bunny 65c4880c1a 👷‍♂️ Ci测试 2025-05-24 14:20:55 +08:00
bunny 2e0b1e191f 👷‍♂️ 添加CI脚本gitlab.yml 2025-05-24 14:09:57 +08:00
bunny 28c56072d5 数据分析左侧完成 2025-05-24 13:52:02 +08:00
bunny 0592e9bd5c 🧑‍💻 feat: 修改公共头部组件 2025-05-16 22:47:46 +08:00
20 changed files with 273 additions and 63 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@ lerna-debug.log*
node_modules
dist
fakeServer
dist-ssr
*.local

54
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,54 @@
# 定义CI/CD流水线的阶段
stages:
- build # 第一阶段:构建应用程序
- build-docker # 第二阶段构建Docker镜像
- deploy # 第三阶段:部署应用程序
cache: # 缓存内容
paths:
- node_modules/
- docker/dist/
# 定义全局变量
variables:
CONTAINER_NAME: 'central-monitor' # Docker容器名称
DOCKER_TAG: '1.0.0' # Docker镜像标签版本
# 构建任务
build-job:
stage: build # 指定此任务属于build阶段
script:
# 打印编译开始信息
- echo "Compiling the code..."
# 使用Maven编译Java项目跳过测试
- npm i -g pnpm && pnpm i && pnpm build
# 打印编译完成信息
- echo "Compile complete."
# 从Docker Hub拉取OpenJDK基础镜像
- docker pull nginx:1.27.3
# 打印拉取完成信息
- echo "docker pull complete."
# 使用Dockerfile构建Docker镜像并打上标签
- cd docker && docker build -f Dockerfile -t $CONTAINER_NAME:$DOCKER_TAG .
# 打印构建成功信息
- echo "Application successfully deployed."
# 部署任务
deploy-job:
stage: deploy # 指定此任务属于deploy阶段
environment: production # 指定部署环境为production
script:
# 打印部署开始信息
- echo "Deploying application..."
# 停止正在运行的容器(如果存在),|| true确保命令失败不会中断脚本
- docker stop $CONTAINER_NAME || true
# 删除容器(如果存在)
- docker rm $CONTAINER_NAME || true
# 运行新的Docker容器
# -d: 后台运行
# -p: 端口映射7070和8000
# --name: 容器名称
# --restart always: 总是自动重启
- docker run -d -p 8800:80 --name $CONTAINER_NAME --restart always $CONTAINER_NAME:$DOCKER_TAG
# 打印部署成功信息
- echo "Application successfully deployed."

1
.npmrc Normal file
View File

@ -0,0 +1 @@
optional-depenencies=true

View File

@ -31,7 +31,7 @@ export const buildEnv = (): BuildOptions => {
// 规定触发警告的 chunk 大小, 当某个代码分块的大小超过该限制时Vite 会发出警告
chunkSizeWarningLimit: 2000,
rollupOptions: {
external: ['md-editor-v3', 'echarts'],
external: ['echarts', 'fsevents'],
input: {
// @ts-ignore
index: pathResolve('../index.html', import.meta.url),

View File

@ -57,7 +57,10 @@ const useMock = (mode: string) => {
logger: true,
include: 'mock',
infixName: false,
enableProd: true, // 线上支持mock
enableDev: true,
// enableProd: true,
// build: true,
// http2: true,
})
: null;
};

View File

@ -16,13 +16,13 @@ server {
}
# 后端跨域请求
location ~/api/ {
proxy_pass http://172.17.0.1:8000;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# location ~/api/ {
# proxy_pass http://172.17.0.1:8000;
# proxy_set_header Host $http_host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
error_page 404 404.html;

59
mock/data-analyse.ts Normal file
View File

@ -0,0 +1,59 @@
import { defineFakeRoute } from 'vite-plugin-fake-server';
const randomNumber = (range: number = 100) => {
return parseInt((Math.random() * range).toFixed(0));
};
const BASE_URL = '/mock/data-analyse';
export default defineFakeRoute([
// 销售设备总量
{
url: `${BASE_URL}/device-sales-stats`,
method: 'GET',
response: () => ({
code: 200,
data: {
// 销售设备总量
totalDeviceSales: randomNumber(9999),
// 环比去年增长,如 "+15.2%"
yearlyGrowthRate: randomNumber(),
},
message: '操作成功',
}),
},
// 销售公司销售设备数量占比
{
url: `${BASE_URL}/company-sales-distribution`,
method: 'GET',
response: () => ({
code: 200,
data: [
{ name: '科技有限公司', amount: randomNumber(999999), percent: randomNumber() },
{ name: '科技有限公司', amount: randomNumber(999999), percent: randomNumber() },
{ name: '科技有限公司', amount: randomNumber(999999), percent: randomNumber() },
{ name: '科技有限公司', amount: randomNumber(999999), percent: randomNumber() },
{ name: '科技有限公司', amount: randomNumber(999999), percent: randomNumber() },
],
message: '操作成功',
}),
},
// 品牌占有率
{
url: `${BASE_URL}/brands-distribution`,
method: 'GET',
response: () => ({
code: 200,
data: [
{ value: randomNumber(), name: '品牌A' },
{ value: randomNumber(), name: '品牌B' },
{ value: randomNumber(), name: '品牌C' },
{ value: randomNumber(), name: '品牌D' },
{ value: randomNumber(), name: '品牌E' },
{ value: randomNumber(), name: '品牌F' },
{ value: randomNumber(), name: '品牌G' },
{ value: randomNumber(), name: '品牌H' },
],
message: '操作成功',
}),
},
]);

View File

@ -1,5 +1,5 @@
{
"name": "vehicle-monitor",
"name": "central-monitor",
"private": true,
"version": "1.0.0",
"type": "module",
@ -95,6 +95,18 @@
"allowedVersions": {
"eslint": "9"
}
},
"packageExtensions": {
"rollup@*": {
"optionalDependencies": {
"fsevents": "*"
}
}
}
}
},
"onlyBuiltDependencies": [
"@parcel/watcher",
"esbuild",
"vue-demi"
]
}

View File

@ -4,6 +4,8 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
packageExtensionsChecksum: sha256-P0QlM8/kU0utlq/9p9JMVRZfIyZpKq+o9SyM/Bghnd4=
importers:
.:

16
src/api/dataAnalyse.ts Normal file
View File

@ -0,0 +1,16 @@
import request from '@/api/server/requestMock';
/* 销售设备总量 */
export const getDeviceSalesStats = () => {
return request.get('data-analyse/device-sales-stats');
};
/* 销售公司销售设备数量占比 */
export const getCompanySalesDistribution = () => {
return request.get('data-analyse/company-sales-distribution');
};
/* 品牌占有率 */
export const getBrandsDistribution = () => {
return request.get('data-analyse/brands-distribution');
};

View File

@ -18,10 +18,13 @@ defineProps({
width: 100%;
height: 11px;
border: 1px solid var(--color-primary-secondary);
border-radius: 2px;
&-inner {
height: 100%;
border-radius: 4px;
background: var(--color-primary);
transition: all 0.4s;
}
}
</style>

View File

@ -1,12 +1,12 @@
<script lang="ts" setup>
import { useIntervalFn } from '@vueuse/core';
import { onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useRoute } from 'vue-router';
import LayoutHeaderNav from '@/layout/layout-header/components/LayoutHeaderNav.vue';
import LayoutHeaderNav from '@/layout/layout-header/components/layout-nav/LayoutHeaderNav.vue';
import dayjs from '@/plugins/dayjs';
import { resetRouter } from '@/router';
const router = useRouter();
const route = useRoute();
const time = ref('');
@ -14,7 +14,7 @@ const time = ref('');
/** 显示时间 */
const displayTime = () => {
time.value = dayjs(new Date()).format('YYYY-MM-DD dddd HH:mm:ss');
setInterval(() => {
useIntervalFn(() => {
time.value = dayjs(new Date()).format('YYYY-MM-DD dddd HH:mm:ss');
}, 1000);
};
@ -26,18 +26,18 @@ onMounted(() => {
<template>
<header>
<div class="header-time c-primary">{{ time }}</div>
<div class="header-time">{{ time }}</div>
<div class="header-title">
<h1>{{ route.meta.title }}</h1>
<h1 @click="resetRouter()">{{ route.meta.title }}</h1>
<h2>{{ route.meta.subtitle }}</h2>
<img
alt="icon-setting"
class="ml-[-284px]"
src="../images/icon/icon-home.png"
@click="resetRouter()"
/>
<img alt="icon-home" class="ml-[284px]" src="../images/icon/icon-setting.png" />
<!--<img-->
<!-- alt="icon-setting"-->
<!-- class="ml-[-464px]"-->
<!-- src="../images/icon/icon-home.png"-->
<!-- @click="resetRouter()"-->
<!--/>-->
<!--<img alt="icon-home" class="ml-[464px]" src="../images/icon/icon-setting.png" />-->
</div>
<LayoutHeaderNav />
@ -47,7 +47,7 @@ onMounted(() => {
<style lang="scss" scoped>
header {
position: relative;
height: 108px;
height: 80px;
background: url('@/layout/layout-header/images/layout-header-2.png') no-repeat center;
background-size: cover;
}
@ -55,7 +55,8 @@ header {
.header-time {
position: absolute;
margin: 0 0 0 41px;
line-height: 60px;
line-height: 50px;
color: var(--color-info);
font-size: 14px;
}
@ -65,11 +66,11 @@ header {
img {
position: absolute;
bottom: 0;
left: 50%;
bottom: -14px;
transform: translateX(-50%);
width: 60px;
height: 60px;
width: 50px;
height: 50px;
object-fit: cover;
z-index: 1;
cursor: pointer;
@ -77,14 +78,15 @@ header {
h1 {
color: #fff;
font-size: 42px;
line-height: 60px;
font-size: 34px;
line-height: 50px;
cursor: pointer;
}
h2 {
color: var(--color-primary-secondary);
font-size: 24px;
line-height: 45px;
line-height: 24px;
font-weight: lighter;
}
}

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { useRoute } from 'vue-router';
import LayoutHeaderNav from '@/layout/layout-header/components/LayoutHeaderNav.vue';
import LayoutHeaderNav from '@/layout/layout-header/components/layout-nav/LayoutHeaderNav.vue';
const route = useRoute();
</script>

View File

@ -3,11 +3,11 @@
<template>
<div class="bar-op">
<ul>
<li><img alt="icon-1" src="../images/icon/icon-1.png" /></li>
<li><img alt="icon-2" src="../images/icon/icon-2.png" /></li>
<li><img alt="icon-3" src="../images/icon/icon-3.png" /></li>
<!--<li><img alt="icon-1" src="../../images/icon/icon-1.png" /></li>-->
<!--<li><img alt="icon-2" src="../../images/icon/icon-2.png" /></li>-->
<!--<li><img alt="icon-3" src="../../images/icon/icon-3.png" /></li>-->
</ul>
<span class="hover">王菠萝</span>
<span class="hover">小兔子🐇</span>
</div>
</template>
@ -16,7 +16,7 @@
display: flex;
align-items: center;
position: absolute;
top: 11px;
top: 4px;
right: 20px;
height: 36px;
@ -33,7 +33,7 @@
}
span {
color: var(--color-primary);
color: var(--color-info);
float: left;
font-size: 12px;
cursor: pointer;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -7,3 +7,5 @@ const store = createPinia();
export function setupStore(app: App<Element>) {
app.use(store);
}
export { store };

View File

@ -0,0 +1,43 @@
import { defineStore } from 'pinia';
import {
getBrandsDistribution,
getCompanySalesDistribution,
getDeviceSalesStats,
} from '@/api/dataAnalyse';
import { store } from '..';
export const useDataAnalyseStore = defineStore('dataAnalyseStore', {
state: () => ({
// 销售设备总量
deviceSalesStats: {
totalDeviceSales: 0,
yearlyGrowthRate: 0,
},
// 销售公司销售设备数量占比
companySalesDistribution: [],
// 品牌占有率
brandsDistribution: [],
}),
actions: {
/* 销售设备总量 */
async fetchDeviceSaesStats() {
this.deviceSalesStats = await getDeviceSalesStats();
},
/* 销售公司销售设备数量占比 */
async fetchCompanySalesDistribution() {
this.companySalesDistribution = await getCompanySalesDistribution();
},
/* 品牌占有率 */
async fetchBrandsDistribution() {
this.brandsDistribution = await getBrandsDistribution();
},
},
});
export function useDataAnalyseHook() {
return useDataAnalyseStore(store);
}

View File

@ -17,7 +17,7 @@ const option = ref<EChartsOption>({
{
name: '名称',
type: 'pie',
radius: [14, 100],
radius: [14, 104],
center: ['50%', '50%'],
roseType: 'area',
itemStyle: { borderRadius: 4 },

View File

@ -9,10 +9,7 @@ let myChart = null;
const option = {
tooltip: { trigger: 'item' },
legend: {
top: 'bottom',
data: ['品牌A', '品牌B', '品牌C', '品牌D', '品牌E', '品牌F'],
},
legend: { top: 'bottom', textStyle: { color: '#fff' } },
series: [
{
name: '品牌占比',
@ -27,9 +24,7 @@ const option = {
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
label: {
formatter: '{b}: {c}%',
},
label: { formatter: '{b}: {c}%', color: '#fff' },
data: [
{ value: 10.89, name: '品牌A' },
{ value: 30.89, name: '品牌B' },
@ -57,9 +52,8 @@ export const renderEcharts = (element: Ref<HTMLDivElement>) => {
};
/** 更新图表数据 */
export const updateChart = (option: Array<Array<number>>) => {
export const updateChart = (data: any) => {
const series = myChart.getOption().series;
// series[0].data = option[0];
// series[1].data = option[1];
series[0].data = data;
myChart.setOption({ series });
};

View File

@ -1,23 +1,39 @@
<script lang="ts" setup>
import { useIntervalFn } from '@vueuse/core';
import { storeToRefs } from 'pinia';
import { onMounted, ref } from 'vue';
import Progress1 from '@/components/Progress/Progress1.vue';
import { renderEcharts } from '@/views/data-analyse/charts/left-brand';
import { useDataAnalyseHook } from '@/store/modules/dataAnalyse';
import { renderEcharts, updateChart } from '@/views/data-analyse/charts/left-brand';
import PanelTitle from '@/views/data-analyse/components/PanelTitle.vue';
const brandChartRef = ref();
const deviceTotal = ref('1010');
const dataAnalyseStore = useDataAnalyseHook();
const { deviceSalesStats, companySalesDistribution, brandsDistribution } =
storeToRefs(dataAnalyseStore);
const companyList = [
{ name: '科技有限公司', amount: 18888, percent: '40' },
{ name: '科技有限公司', amount: 18888, percent: '50' },
{ name: '科技有限公司', amount: 18888, percent: '10' },
{ name: '科技有限公司', amount: 18888, percent: '80' },
{ name: '科技有限公司', amount: 18888, percent: '50' },
];
/* 初始化数据获取 */
const initAppData = async () => {
//
dataAnalyseStore.fetchDeviceSaesStats();
//
dataAnalyseStore.fetchCompanySalesDistribution();
//
await dataAnalyseStore.fetchBrandsDistribution();
//
updateChart(brandsDistribution.value);
};
const brandChartRef = ref();
onMounted(() => {
renderEcharts(brandChartRef);
initAppData();
useIntervalFn(() => {
initAppData();
}, 1000);
});
</script>
@ -28,13 +44,15 @@ onMounted(() => {
<div>
<h1 class="font-size-[21px]">销售设备总量()</h1>
<div class="data-analyse-left__top-percent">
<span>环比去年增长 52%</span>
<span>环比去年增长 {{ deviceSalesStats.yearlyGrowthRate }} %</span>
<i class="i-mdi-trending-up">s</i>
</div>
</div>
<ul class="flex">
<li v-for="(num, index) in deviceTotal" :key="index">{{ num }}</li>
<li v-for="(num, index) in deviceSalesStats.totalDeviceSales.toString()" :key="index">
{{ num }}
</li>
</ul>
</div>
@ -43,7 +61,7 @@ onMounted(() => {
<PanelTitle title="销售公司销售设备数量占比" />
<ul>
<li v-for="(item, index) in companyList" :key="index">
<li v-for="(item, index) in companySalesDistribution" :key="index">
<div class="data-analyse-left__center-info">
<span>{{ item.name }}</span>
<div class="info-left">