Initial commit: One Pipe System

完整的管理系统,包含账户管理、卡片管理、套餐管理、财务管理等功能模块。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sexygoat
2026-01-22 16:35:33 +08:00
commit 222e5bb11a
495 changed files with 145440 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
<template>
<div class="analysis-dashboard">
<el-row :gutter="20">
<el-col :xl="14" :lg="15" :xs="24">
<TodaySales />
</el-col>
<el-col :xl="10" :lg="9" :xs="24">
<VisitorInsights />
</el-col>
</el-row>
<el-row :gutter="20" class="mt-20">
<el-col :xl="10" :lg="10" :xs="24">
<TotalRevenue />
</el-col>
<el-col :xl="7" :lg="7" :xs="24">
<CustomerSatisfaction />
</el-col>
<el-col :xl="7" :lg="7" :xs="24">
<TargetVsReality />
</el-col>
</el-row>
<el-row :gutter="20" class="mt-20">
<el-col :xl="10" :lg="10" :xs="24">
<TopProducts />
</el-col>
<el-col :xl="7" :lg="7" :xs="24">
<SalesMappingByCountry />
</el-col>
<el-col :xl="7" :lg="7" :xs="24">
<VolumeServiceLevel />
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import TodaySales from './widget/TodaySales.vue'
import VisitorInsights from './widget/VisitorInsights.vue'
import TotalRevenue from './widget/TotalRevenue.vue'
import CustomerSatisfaction from './widget/CustomerSatisfaction.vue'
import TargetVsReality from './widget/TargetVsReality.vue'
import TopProducts from './widget/TopProducts.vue'
import SalesMappingByCountry from './widget/SalesMappingByCountry.vue'
import VolumeServiceLevel from './widget/VolumeServiceLevel.vue'
defineOptions({ name: 'Analysis' })
</script>
<style lang="scss" scoped>
@use './style';
</style>

View File

@@ -0,0 +1,61 @@
.analysis-dashboard {
padding-bottom: 20px;
:deep(.custom-card) {
background: var(--art-main-bg-color);
border-radius: calc(var(--custom-radius) + 4px) !important;
}
// 卡片头部
:deep(.custom-card-header) {
position: relative;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 20px;
.title {
font-size: 20px;
font-weight: 400;
color: var(--art-text-gray-900);
}
.subtitle {
position: absolute;
bottom: 2px;
left: 21px;
font-size: 13px;
color: var(--art-gray-600);
}
}
.el-card {
border: 1px solid #e8ebf1;
box-shadow: none;
}
.mt-20 {
margin-top: 20px;
}
}
.dark {
.analysis-dashboard {
:deep(.custom-card) {
box-shadow: 0 4px 20px rgb(0 0 0 / 50%);
}
}
}
@media (width <= 1200px) {
.analysis-dashboard {
.mt-20 {
margin-top: 0;
}
:deep(.custom-card) {
margin-bottom: 20px;
}
}
}

View File

@@ -0,0 +1,134 @@
<template>
<div class="custom-card art-custom-card customer-satisfaction">
<div class="custom-card-header">
<span class="title">{{ t('analysis.customerSatisfaction.title') }}</span>
</div>
<div class="custom-card-body">
<div ref="chartRef" style="height: 300px; margin-top: 10px"></div>
</div>
</div>
</template>
<script setup lang="ts">
import * as echarts from 'echarts'
import { useI18n } from 'vue-i18n'
import { useChart } from '@/composables/useChart'
const { t } = useI18n()
const { chartRef, isDark, initChart } = useChart()
const options: () => echarts.EChartsOption = () => ({
grid: {
top: 30,
right: 20,
bottom: 50,
left: 20,
containLabel: true
},
tooltip: {
trigger: 'axis',
confine: true
},
legend: {
data: [
t('analysis.customerSatisfaction.legend.lastMonth'),
t('analysis.customerSatisfaction.legend.thisMonth')
],
bottom: 0,
textStyle: {
fontSize: 12,
color: isDark.value ? '#808290' : '#222B45'
}
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['Week 0', 'Week 1', 'Week 2', 'Week 3', 'Week 4', 'Week 5', 'Week 6'],
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { show: false } // 隐藏 x 轴标签
},
yAxis: {
type: 'value',
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { show: false },
splitLine: {
show: false // 将 show 设置为 false 以去除水平线条
}
},
series: [
{
name: t('analysis.customerSatisfaction.legend.lastMonth'),
type: 'line',
smooth: true,
data: [1800, 2800, 1800, 2300, 2600, 2500, 3000],
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(0,157,255,0.33)' },
{ offset: 1, color: 'rgba(255,255,255,0)' }
])
},
lineStyle: {
width: 2,
color: '#0086E1'
},
symbol: 'none',
itemStyle: {
color: '#0095FF'
}
},
{
name: t('analysis.customerSatisfaction.legend.thisMonth'),
type: 'line',
smooth: true,
data: [4000, 3500, 4300, 3700, 4500, 3500, 4000],
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(147,241,180,0.33)' },
{ offset: 1, color: 'rgba(255,255,255,0)' }
])
},
lineStyle: {
width: 2,
color: '#14DEB9'
},
symbol: 'none',
itemStyle: {
color: '#14DEB9'
}
}
]
})
watch(isDark, () => {
initChart(options())
})
onMounted(() => {
initChart(options())
})
</script>
<style lang="scss" scoped>
.custom-card {
height: 400px;
&-body {
padding: 10px 0;
}
}
@media (max-width: $device-notebook) {
.custom-card {
height: 350px;
&-body {
> div {
height: 260px !important;
}
}
}
}
</style>

View File

@@ -0,0 +1,80 @@
<template>
<div class="custom-card art-custom-card sales-mapping-country">
<div class="custom-card-header">
<span class="title">{{ t('analysis.salesMappingCountry.title') }}</span>
</div>
<div class="custom-card-body">
<div ref="chartRef" class="sales-mapping-chart"></div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { EChartsOption } from 'echarts'
import { useChart } from '@/composables/useChart'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const { chartRef, initChart, isDark } = useChart()
const chartData = [
{ value: 1048, name: 'Beijing', itemStyle: { color: 'rgba(99, 102, 241, 0.9)' } },
{ value: 735, name: 'Shanghai', itemStyle: { color: 'rgba(134, 239, 172, 0.9)' } },
{ value: 580, name: 'Guangzhou', itemStyle: { color: 'rgba(253, 224, 71, 0.9)' } },
{ value: 484, name: 'Shenzhen', itemStyle: { color: 'rgba(248, 113, 113, 0.9)' } },
{ value: 300, name: 'Chengdu', itemStyle: { color: 'rgba(125, 211, 252, 0.9)' } }
]
const options: () => EChartsOption = () => ({
tooltip: {
trigger: 'item'
},
series: [
{
name: 'Sales Mapping',
type: 'pie',
radius: ['40%', '60%'],
avoidLabelOverlap: false,
padAngle: 5,
itemStyle: {
borderRadius: 10
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 40,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: chartData
}
]
})
watch(isDark, () => {
initChart(options())
})
onMounted(() => {
initChart(options())
})
</script>
<style lang="scss" scoped>
.sales-mapping-country {
height: 330px;
.sales-mapping-chart {
width: 100%;
height: 260px;
}
}
</style>

View File

@@ -0,0 +1,230 @@
<template>
<div class="custom-card art-custom-card target-vs-reality">
<div class="custom-card-header">
<span class="title">{{ t('analysis.targetVsReality.title') }}</span>
</div>
<div class="custom-card-body">
<div ref="chartRef" style="height: 160px"></div>
</div>
<div class="custom-card-footer">
<div class="total-item">
<div class="label">
<i class="iconfont-sys">&#xe77f;</i>
<div class="label-text">
<span>{{ t('analysis.targetVsReality.realitySales.label') }}</span>
<span>{{ t('analysis.targetVsReality.realitySales.sublabel') }}</span>
</div>
</div>
<div class="value text-color-green">8,823</div>
</div>
<div class="total-item">
<div class="label">
<i class="iconfont-sys">&#xe77c;</i>
<div class="label-text">
<span>{{ t('analysis.targetVsReality.targetSales.label') }}</span>
<span>{{ t('analysis.targetVsReality.targetSales.sublabel') }}</span>
</div>
</div>
<div class="value text-color-orange">12,122</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useChart } from '@/composables/useChart'
import { EChartsOption } from 'echarts'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const { chartRef, isDark, initChart } = useChart()
const options: () => EChartsOption = () => ({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
top: 10,
right: 0,
bottom: 0,
left: 0,
containLabel: true
},
xAxis: {
type: 'category',
data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'June', 'July'],
axisLabel: {
color: '#7B91B0'
},
axisLine: {
show: false // 隐藏 x 轴线
},
axisTick: {
show: false // 隐藏刻度线
}
},
yAxis: {
type: 'value',
axisLabel: {
show: false // 隐藏 y 轴文字
},
splitLine: {
show: false // 隐藏 y 轴分割线
},
axisLine: {
show: false // 隐藏 y 轴线
}
},
series: [
{
name: 'Reality Sales',
type: 'bar',
data: [8000, 7000, 6000, 8500, 9000, 10000, 9500],
barWidth: '15',
itemStyle: {
borderRadius: [4, 4, 0, 0],
color: '#2B8DFA'
}
},
{
name: 'Target Sales',
type: 'bar',
data: [10000, 9000, 11000, 10000, 12000, 12500, 11500],
barWidth: '15',
itemStyle: {
borderRadius: [4, 4, 4, 4],
color: '#95E0FB'
}
}
]
})
watch(isDark, () => {
initChart(options())
})
onMounted(() => {
initChart(options())
})
</script>
<style lang="scss" scoped>
.custom-card {
height: 400px;
&-body {
padding: 20px;
}
&-footer {
box-sizing: border-box;
padding: 0 20px;
margin-top: 15px;
.total-item {
display: flex;
margin-bottom: 20px;
text-align: center;
&:first-of-type .label .iconfont-sys {
color: #2b8dfa !important;
background-color: #e6f7ff !important;
}
&:last-of-type .label .iconfont-sys {
color: #1cb8fc !important;
background-color: #e6f7ff !important;
}
.label {
display: flex;
align-items: center;
justify-content: flex-start;
width: 60%;
font-size: 14px;
color: #606266;
.iconfont-sys {
width: 40px;
height: 40px;
margin-right: 12px;
font-size: 18px;
line-height: 40px;
text-align: center;
background-color: #f2f2f2;
border-radius: 6px;
}
.label-text {
display: flex;
flex-direction: column;
align-items: flex-start;
span {
&:first-of-type {
font-size: 16px;
color: var(--art-text-gray-800);
}
&:last-of-type {
margin-top: 4px;
font-size: 12px;
color: #737791;
}
}
}
}
.value {
font-size: 18px;
font-weight: 400;
&.text-color-green {
color: #2b8dfa !important;
}
&.text-color-orange {
color: #1cb8fc !important;
}
}
}
}
}
@media (max-width: $device-notebook) {
.custom-card {
height: 350px;
&-body {
padding-top: 10px;
> div {
height: 140px !important;
}
}
&-footer {
margin-top: 0;
}
}
}
.dark {
.custom-card {
&-footer {
.total-item {
&:first-of-type .label .iconfont-sys {
background-color: #222 !important;
}
&:last-of-type .label .iconfont-sys {
background-color: #222 !important;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,196 @@
<template>
<div class="custom-card art-custom-card today-sales">
<div class="custom-card-header">
<span class="title">{{ t('analysis.todaySales.title') }}</span>
<span class="subtitle">{{ t('analysis.todaySales.subtitle') }}</span>
<div class="export-btn">
<i class="iconfont-sys">&#xe6d1;</i>
<span>{{ t('analysis.todaySales.export') }}</span>
</div>
</div>
<div class="sales-summary">
<el-row :gutter="20">
<el-col :span="6" :xs="24" v-for="(item, index) in salesData" :key="index">
<div :class="['sales-card art-custom-card']">
<i class="iconfont-sys" :class="item.class" v-html="item.iconfont"></i>
<h2>
<CountTo
class="number box-title"
:endVal="item.value"
:duration="1000"
separator=""
></CountTo>
</h2>
<p>{{ item.label }}</p>
<small>{{ item.change }} {{ t('analysis.todaySales.fromYesterday') }}</small>
</div>
</el-col>
</el-row>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { CountTo } from 'vue3-count-to'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const salesData = ref([
{
label: t('analysis.todaySales.cards.totalSales.label'),
value: 999,
change: t('analysis.todaySales.cards.totalSales.change'),
iconfont: '&#xe7d9',
class: 'bg-primary'
},
{
label: t('analysis.todaySales.cards.totalOrder.label'),
value: 300,
change: t('analysis.todaySales.cards.totalOrder.change'),
iconfont: '&#xe70f',
class: 'bg-warning'
},
{
label: t('analysis.todaySales.cards.productSold.label'),
value: 56,
change: t('analysis.todaySales.cards.productSold.change'),
iconfont: '&#xe712',
class: 'bg-error'
},
{
label: t('analysis.todaySales.cards.newCustomers.label'),
value: 68,
change: t('analysis.todaySales.cards.newCustomers.change'),
iconfont: '&#xe77f',
class: 'bg-success'
}
])
</script>
<style lang="scss" scoped>
.today-sales {
height: 330px;
.export-btn {
display: flex;
align-items: center;
justify-content: center;
min-width: 66px;
padding: 6px 0;
color: var(--art-gray-600);
cursor: pointer;
border: 1px solid var(--art-border-dashed-color);
border-radius: 6px;
transition: all 0.3s;
&:hover {
color: var(--main-color);
border-color: var(--main-color);
}
.iconfont-sys {
margin-right: 5px;
font-size: 10px;
}
span {
font-size: 12px;
}
}
.sales-summary {
padding: 20px;
.sales-card {
display: flex;
flex-direction: column;
justify-content: center;
height: 220px;
padding: 0 20px;
overflow: hidden;
border-radius: calc(var(--custom-radius) / 2 + 4px) !important;
.iconfont-sys {
width: 48px;
height: 48px;
font-size: 20px;
line-height: 48px;
color: #fff;
color: var(--el-color-primary);
text-align: center;
background-color: var(--el-color-primary-light-9);
border-radius: 10px;
}
h2 {
margin-top: 10px;
font-size: 26px;
font-weight: 400;
color: var(--art-text-gray-900) !important;
}
p {
margin-top: 10px;
font-size: 16px;
color: var(--art-text-gray-700) !important;
@include ellipsis;
}
small {
display: block;
margin-top: 10px;
color: var(--art-text-gray-500) !important;
@include ellipsis;
}
}
}
}
// 暗黑模式降低颜色强度
.dark {
.today-sales {
.sales-summary {
.sales-card {
.iconfont-sys {
&.red,
&.yellow,
&.green,
&.purple {
background-color: #222 !important;
}
}
}
}
}
}
@media (max-width: $device-notebook) {
.today-sales {
height: 280px;
.sales-summary {
.sales-card {
height: 170px;
}
}
}
}
@media (width <= 768px) {
.today-sales {
height: auto;
.sales-summary {
padding-bottom: 0;
.sales-card {
margin-bottom: 20px;
}
}
}
}
</style>

View File

@@ -0,0 +1,115 @@
<template>
<div class="custom-card art-custom-card top-products">
<div class="custom-card-header">
<span class="title">{{ t('analysis.topProducts.title') }}</span>
</div>
<div class="custom-card-body">
<art-table
:data="products"
style="width: 100%"
:pagination="false"
size="large"
:border="false"
:stripe="false"
:show-header-background="false"
>
<el-table-column prop="name" :label="t('analysis.topProducts.columns.name')" width="200" />
<el-table-column prop="popularity" :label="t('analysis.topProducts.columns.popularity')">
<template #default="scope">
<el-progress
:percentage="scope.row.popularity"
:color="getColor(scope.row.popularity)"
:stroke-width="5"
:show-text="false"
/>
</template>
</el-table-column>
<el-table-column prop="sales" :label="t('analysis.topProducts.columns.sales')" width="80">
<template #default="scope">
<span
:style="{
color: getColor(scope.row.popularity),
backgroundColor: `rgba(${hexToRgb(getColor(scope.row.popularity))}, 0.08)`,
border: '1px solid',
padding: '3px 6px',
borderRadius: '4px',
fontSize: '12px'
}"
>{{ scope.row.sales }}</span
>
</template>
</el-table-column>
</art-table>
</div>
</div>
</template>
<script setup lang="ts">
import { hexToRgb } from '@/utils/ui'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
// 使用 computed 来创建响应式的产品数据
const products = computed(() => [
{
name: t('analysis.topProducts.products.homeDecor.name'),
popularity: 10,
sales: t('analysis.topProducts.products.homeDecor.sales')
},
{
name: t('analysis.topProducts.products.disneyBag.name'),
popularity: 29,
sales: t('analysis.topProducts.products.disneyBag.sales')
},
{
name: t('analysis.topProducts.products.bathroom.name'),
popularity: 65,
sales: t('analysis.topProducts.products.bathroom.sales')
},
{
name: t('analysis.topProducts.products.smartwatch.name'),
popularity: 32,
sales: t('analysis.topProducts.products.smartwatch.sales')
},
{
name: t('analysis.topProducts.products.fitness.name'),
popularity: 78,
sales: t('analysis.topProducts.products.fitness.sales')
},
{
name: t('analysis.topProducts.products.earbuds.name'),
popularity: 41,
sales: t('analysis.topProducts.products.earbuds.sales')
}
])
const getColor = (percentage: number) => {
if (percentage < 25) return '#00E096'
if (percentage < 50) return '#0095FF'
if (percentage < 75) return '#884CFF'
return '#FE8F0E'
}
</script>
<style lang="scss" scoped>
.custom-card {
height: 330px;
overflow-y: scroll;
// 隐藏滚动条
&::-webkit-scrollbar {
display: none;
}
&-body {
padding: 0 6px;
}
}
@media (width <= 1200px) {
.custom-card {
height: auto;
}
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<div class="custom-card art-custom-card total-revenue">
<div class="custom-card-header">
<span class="title">{{ t('analysis.totalRevenue.title') }}</span>
</div>
<div class="custom-card-body">
<div ref="chartRef" style="height: 300px"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { EChartsOption } from 'echarts'
import { useChart } from '@/composables/useChart'
const { t } = useI18n()
const { chartRef, isDark, initChart } = useChart()
// 创建图表选项
const options: () => EChartsOption = () => ({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
top: 20,
right: 3,
bottom: 40,
left: 3,
containLabel: true
},
legend: {
data: [
t('analysis.totalRevenue.legend.onlineSales'),
t('analysis.totalRevenue.legend.offlineSales')
],
bottom: 0,
icon: 'circle',
itemWidth: 10,
itemHeight: 10,
itemGap: 15,
textStyle: {
fontSize: 12,
color: isDark.value ? '#808290' : '#222B45'
}
},
xAxis: {
type: 'category',
data: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: isDark.value ? '#808290' : '#7B91B0'
}
},
yAxis: {
type: 'value',
axisLine: { show: false },
axisTick: { show: false },
splitLine: {
lineStyle: {
color: isDark.value ? 'rgba(255, 255, 255, 0.1)' : '#EFF1F3',
width: 0.8
}
},
axisLabel: { color: isDark.value ? '#808290' : '#7B91B0' }
},
series: [
{
name: t('analysis.totalRevenue.legend.onlineSales'),
type: 'bar',
data: [12, 13, 5, 15, 10, 15, 18],
barWidth: '15',
itemStyle: {
color: '#0095FF',
borderRadius: [4, 4, 4, 4]
}
},
{
name: t('analysis.totalRevenue.legend.offlineSales'),
type: 'bar',
data: [10, 11, 20, 5, 11, 13, 10],
barWidth: '15',
itemStyle: {
color: '#95E0FB',
borderRadius: [4, 4, 4, 4]
}
}
]
})
watch(isDark, () => {
initChart(options())
})
onMounted(() => {
initChart(options())
})
</script>
<style lang="scss" scoped>
.custom-card {
height: 400px;
&-body {
padding: 20px;
}
}
@media (max-width: $device-notebook) {
.custom-card {
height: 350px;
&-body {
> div {
height: 260px !important;
}
}
}
}
</style>

View File

@@ -0,0 +1,143 @@
<template>
<div class="custom-card art-custom-card visitor-insights">
<div class="custom-card-header">
<span class="title">{{ t('analysis.visitorInsights.title') }}</span>
</div>
<div class="card-body">
<div ref="chartRef" style="height: 250px"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useChart } from '@/composables/useChart'
import { EChartsOption } from 'echarts'
const { t } = useI18n()
const { chartRef, isDark, initChart } = useChart()
const { width } = useWindowSize()
const options: () => EChartsOption = () => {
return {
tooltip: {
trigger: 'axis'
},
grid: {
top: 20,
right: 20,
bottom: width.value < 600 ? 80 : 40,
left: 20,
containLabel: true
},
legend: {
data: [
t('analysis.visitorInsights.legend.loyalCustomers'),
t('analysis.visitorInsights.legend.newCustomers')
],
bottom: 0,
left: 'center',
itemWidth: 14,
itemHeight: 14,
textStyle: {
fontSize: 12,
color: isDark.value ? '#808290' : '#222B45'
},
icon: 'roundRect',
itemStyle: {
borderRadius: 4
}
},
xAxis: {
type: 'category',
data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { color: isDark.value ? '#808290' : '#7B91B0' }
},
yAxis: {
type: 'value',
axisLine: { show: false },
axisTick: { show: false },
splitLine: {
show: true,
lineStyle: {
color: isDark.value ? 'rgba(255, 255, 255, 0.1)' : '#EFF1F3',
width: 0.8
}
},
axisLabel: { color: isDark.value ? '#808290' : '#7B91B0' }
},
series: [
{
name: t('analysis.visitorInsights.legend.loyalCustomers'),
type: 'line',
smooth: true,
symbol: 'none',
data: [260, 200, 150, 130, 180, 270, 340, 380, 300, 220, 170, 130],
lineStyle: {
color: '#2B8DFA',
width: 3
},
itemStyle: {
color: '#2B8DFA'
}
},
{
name: t('analysis.visitorInsights.legend.newCustomers'),
type: 'line',
smooth: true,
symbol: 'none',
data: [280, 350, 300, 250, 230, 210, 240, 280, 320, 350, 300, 200],
lineStyle: {
color: '#49BEFF',
width: 3
},
itemStyle: {
color: '#49BEFF'
}
}
]
}
}
watch(isDark, () => {
initChart(options())
})
onMounted(() => {
initChart(options())
})
</script>
<style lang="scss" scoped>
.visitor-insights {
height: 330px;
}
@media (max-width: $device-notebook) {
.visitor-insights {
height: 280px;
.card-body {
> div {
height: 210px !important;
}
}
}
}
@media (max-width: $device-phone) {
.visitor-insights {
height: 315px;
.card-body {
> div {
height: 240px !important;
}
}
}
}
</style>

View File

@@ -0,0 +1,125 @@
<template>
<div class="custom-card art-custom-card volume-service-level">
<div class="custom-card-header">
<span class="title">{{ t('analysis.volumeServiceLevel.title') }}</span>
</div>
<div class="custom-card-body">
<div ref="chartRef" class="chart-container"></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
import { useChart } from '@/composables/useChart'
import { EChartsOption } from 'echarts'
const { t } = useI18n()
const { chartRef, isDark, initChart } = useChart()
// 模拟数据
const chartData = [
{ volume: 800, services: 400 },
{ volume: 1000, services: 600 },
{ volume: 750, services: 300 },
{ volume: 600, services: 250 },
{ volume: 450, services: 200 },
{ volume: 500, services: 300 }
]
const options: () => EChartsOption = () => ({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: [
t('analysis.volumeServiceLevel.legend.volume'),
t('analysis.volumeServiceLevel.legend.services')
],
bottom: 20,
itemWidth: 10,
itemHeight: 10,
icon: 'circle',
textStyle: {
fontSize: 12,
color: isDark.value ? '#808290' : '#222B45'
}
},
grid: {
left: '20',
right: '20',
bottom: '60',
top: '30',
containLabel: true
},
xAxis: {
type: 'category',
data: chartData.map((_, index) => `${index + 1}`),
axisLine: {
show: true,
lineStyle: {
color: isDark.value ? 'rgba(255, 255, 255, 0.1)' : '#EFF1F3',
width: 0.8
}
},
axisTick: { show: false },
axisLabel: { show: false }
},
yAxis: {
type: 'value',
axisLine: { show: false },
axisTick: { show: false },
splitLine: { show: false },
axisLabel: { show: false }
},
series: [
{
name: t('analysis.volumeServiceLevel.legend.volume'),
type: 'bar',
stack: 'total',
data: chartData.map((item) => item.volume),
itemStyle: {
color: '#0095FF',
borderRadius: [0, 0, 4, 4]
},
barWidth: '15'
},
{
name: t('analysis.volumeServiceLevel.legend.services'),
type: 'bar',
stack: 'total',
data: chartData.map((item) => item.services),
itemStyle: {
color: '#95E0FB',
borderRadius: [4, 4, 0, 0]
},
barWidth: '50%'
}
]
})
watch(isDark, () => {
initChart(options())
})
onMounted(() => {
initChart(options())
})
</script>
<style lang="scss" scoped>
.volume-service-level {
height: 330px;
.custom-card-body {
padding: 20px;
}
.chart-container {
height: 250px;
}
}
</style>