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:
53
src/views/dashboard/analysis/index.vue
Normal file
53
src/views/dashboard/analysis/index.vue
Normal 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>
|
||||
61
src/views/dashboard/analysis/style.scss
Normal file
61
src/views/dashboard/analysis/style.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
134
src/views/dashboard/analysis/widget/CustomerSatisfaction.vue
Normal file
134
src/views/dashboard/analysis/widget/CustomerSatisfaction.vue
Normal 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>
|
||||
@@ -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>
|
||||
230
src/views/dashboard/analysis/widget/TargetVsReality.vue
Normal file
230
src/views/dashboard/analysis/widget/TargetVsReality.vue
Normal 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"></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"></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>
|
||||
196
src/views/dashboard/analysis/widget/TodaySales.vue
Normal file
196
src/views/dashboard/analysis/widget/TodaySales.vue
Normal 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"></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: '',
|
||||
class: 'bg-primary'
|
||||
},
|
||||
{
|
||||
label: t('analysis.todaySales.cards.totalOrder.label'),
|
||||
value: 300,
|
||||
change: t('analysis.todaySales.cards.totalOrder.change'),
|
||||
iconfont: '',
|
||||
class: 'bg-warning'
|
||||
},
|
||||
{
|
||||
label: t('analysis.todaySales.cards.productSold.label'),
|
||||
value: 56,
|
||||
change: t('analysis.todaySales.cards.productSold.change'),
|
||||
iconfont: '',
|
||||
class: 'bg-error'
|
||||
},
|
||||
{
|
||||
label: t('analysis.todaySales.cards.newCustomers.label'),
|
||||
value: 68,
|
||||
change: t('analysis.todaySales.cards.newCustomers.change'),
|
||||
iconfont: '',
|
||||
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>
|
||||
115
src/views/dashboard/analysis/widget/TopProducts.vue
Normal file
115
src/views/dashboard/analysis/widget/TopProducts.vue
Normal 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>
|
||||
129
src/views/dashboard/analysis/widget/TotalRevenue.vue
Normal file
129
src/views/dashboard/analysis/widget/TotalRevenue.vue
Normal 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>
|
||||
143
src/views/dashboard/analysis/widget/VisitorInsights.vue
Normal file
143
src/views/dashboard/analysis/widget/VisitorInsights.vue
Normal 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>
|
||||
125
src/views/dashboard/analysis/widget/VolumeServiceLevel.vue
Normal file
125
src/views/dashboard/analysis/widget/VolumeServiceLevel.vue
Normal 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>
|
||||
47
src/views/dashboard/console/index.vue
Normal file
47
src/views/dashboard/console/index.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="console">
|
||||
<CardList></CardList>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :md="12" :lg="10">
|
||||
<ActiveUser />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" :lg="14">
|
||||
<SalesOverview />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :md="24" :lg="12">
|
||||
<NewUser />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" :lg="6">
|
||||
<Dynamic />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" :lg="6">
|
||||
<TodoList />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<AboutProject />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CardList from './widget/CardList.vue'
|
||||
import ActiveUser from './widget/ActiveUser.vue'
|
||||
import SalesOverview from './widget/SalesOverview.vue'
|
||||
import NewUser from './widget/NewUser.vue'
|
||||
import Dynamic from './widget/Dynamic.vue'
|
||||
import TodoList from './widget/TodoList.vue'
|
||||
import AboutProject from './widget/AboutProject.vue'
|
||||
import { useCommon } from '@/composables/useCommon'
|
||||
|
||||
defineOptions({ name: 'Console' })
|
||||
|
||||
useCommon().scrollToTop()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './style';
|
||||
</style>
|
||||
43
src/views/dashboard/console/style.scss
Normal file
43
src/views/dashboard/console/style.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
@use '@styles/variables.scss' as *;
|
||||
|
||||
.console {
|
||||
--card-spacing: 20px;
|
||||
|
||||
// 卡片头部
|
||||
:deep(.card-header) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px 25px 5px 0;
|
||||
|
||||
.title {
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: var(--art-gray-900) !important;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 3px;
|
||||
font-size: 13px;
|
||||
color: var(--art-gray-600) !important;
|
||||
|
||||
span {
|
||||
margin-left: 10px;
|
||||
color: #52c41a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置卡片背景色、圆角、间隙
|
||||
:deep(.card-list .card),
|
||||
.card {
|
||||
margin-bottom: var(--card-spacing);
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: calc(var(--custom-radius) + 4px) !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-phone) {
|
||||
--card-spacing: 15px;
|
||||
}
|
||||
}
|
||||
139
src/views/dashboard/console/widget/AboutProject.vue
Normal file
139
src/views/dashboard/console/widget/AboutProject.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="card about-project art-custom-card">
|
||||
<div>
|
||||
<h2 class="box-title">关于项目</h2>
|
||||
<p>{{ systemName }} 是一款专注于用户体验和视觉设计的后台管理系统模版</p>
|
||||
<p>使用了 Vue3、TypeScript、Vite、Element Plus 等前沿技术</p>
|
||||
|
||||
<div class="button-wrap">
|
||||
<div class="btn art-custom-card" @click="goPage(WEB_LINKS.DOCS)">
|
||||
<span>项目官网</span>
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="btn art-custom-card" @click="goPage(WEB_LINKS.INTRODUCE)">
|
||||
<span>文档</span>
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="btn art-custom-card" @click="goPage(WEB_LINKS.GITHUB_HOME)">
|
||||
<span>Github</span>
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="btn art-custom-card" @click="goPage(WEB_LINKS.BLOG)">
|
||||
<span>博客</span>
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<img class="right-img" src="@imgs/draw/draw1.png" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppConfig from '@/config'
|
||||
import { WEB_LINKS } from '@/utils/constants'
|
||||
|
||||
const systemName = AppConfig.systemInfo.name
|
||||
|
||||
const goPage = (url: string) => {
|
||||
window.open(url)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.about-project {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 300px;
|
||||
padding: 20px;
|
||||
|
||||
h2 {
|
||||
margin-top: 10px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: var(--art-gray-900) !important;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
color: var(--art-gray-600);
|
||||
}
|
||||
}
|
||||
|
||||
.button-wrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 600px;
|
||||
margin-top: 35px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 240px;
|
||||
height: 50px;
|
||||
padding: 0 15px;
|
||||
margin: 0 15px 15px 0;
|
||||
font-size: 14px;
|
||||
color: var(--art-gray-800);
|
||||
cursor: pointer;
|
||||
background: var(--art-bg-color);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 5px 10px rgb(0 0 0 / 5%);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计 - iPad Pro及以下
|
||||
@media screen and (max-width: $device-ipad-pro) {
|
||||
.about-project {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.button-wrap {
|
||||
width: 470px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.right-img {
|
||||
width: 300px;
|
||||
height: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计 - iPad垂直及以下
|
||||
@media screen and (max-width: $device-ipad-vertical) {
|
||||
.button-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 190px;
|
||||
}
|
||||
|
||||
.right-img {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计 - 手机端
|
||||
@media screen and (max-width: $device-phone) {
|
||||
.about-project {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
176
src/views/dashboard/console/widget/ActiveUser.vue
Normal file
176
src/views/dashboard/console/widget/ActiveUser.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div class="card art-custom-card">
|
||||
<div class="chart" ref="chartRef"></div>
|
||||
<div class="text">
|
||||
<h3 class="box-title">用户概述</h3>
|
||||
<p class="subtitle">比上周 <span class="text-success">+23%</span></p>
|
||||
<p class="subtitle">我们为您创建了多个选项,可将它们组合在一起并定制为像素完美的页面</p>
|
||||
</div>
|
||||
<div class="list">
|
||||
<div v-for="(item, index) in list" :key="index">
|
||||
<p>{{ item.num }}</p>
|
||||
<p class="subtitle">{{ item.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as echarts from 'echarts'
|
||||
import { getCssVar } from '@/utils/ui'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
import { EChartsOption } from 'echarts'
|
||||
|
||||
const {
|
||||
chartRef,
|
||||
isDark,
|
||||
initChart,
|
||||
getAxisLineStyle,
|
||||
getAxisLabelStyle,
|
||||
getAxisTickStyle,
|
||||
getSplitLineStyle
|
||||
} = useChart()
|
||||
|
||||
const list = [
|
||||
{ name: '总用户量', num: '32k' },
|
||||
{ name: '总访问量', num: '128k' },
|
||||
{ name: '日访问量', num: '1.2k' },
|
||||
{ name: '周同比', num: '+5%' }
|
||||
]
|
||||
|
||||
const options: () => EChartsOption = () => {
|
||||
return {
|
||||
grid: {
|
||||
top: 15,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
containLabel: true
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
axisTick: getAxisTickStyle(),
|
||||
axisLine: getAxisLineStyle(true),
|
||||
axisLabel: getAxisLabelStyle(true)
|
||||
},
|
||||
yAxis: {
|
||||
axisLabel: getAxisLabelStyle(true),
|
||||
axisLine: getAxisLineStyle(!isDark.value),
|
||||
splitLine: getSplitLineStyle(true)
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [160, 100, 150, 80, 190, 100, 175, 120, 160],
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
borderRadius: 4,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: getCssVar('--el-color-primary-light-4')
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: getCssVar('--el-color-primary')
|
||||
}
|
||||
])
|
||||
},
|
||||
barWidth: '50%',
|
||||
animationDelay: (idx) => idx * 50 + 300,
|
||||
animationDuration: (idx) => 1500 - idx * 50,
|
||||
animationEasing: 'quarticOut' // 推荐动画: quarticOut exponentialOut quinticOut backOut
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
watch(isDark, () => {
|
||||
initChart(options())
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initChart(options())
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 420px;
|
||||
padding: 16px;
|
||||
|
||||
.chart {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
padding: 20px 0 20px 20px;
|
||||
border-radius: calc(var(--custom-radius) / 2 + 4px) !important;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 3px;
|
||||
|
||||
h3 {
|
||||
margin-top: 20px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
|
||||
&:last-of-type {
|
||||
height: 42px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-left: 3px;
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
|
||||
p {
|
||||
font-weight: 400;
|
||||
|
||||
&:first-of-type {
|
||||
font-size: 24px;
|
||||
color: var(--art-gray-900);
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.card {
|
||||
.chart {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-phone) {
|
||||
.dark {
|
||||
.card {
|
||||
.chart {
|
||||
padding: 15px 0 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
161
src/views/dashboard/console/widget/CardList.vue
Normal file
161
src/views/dashboard/console/widget/CardList.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<el-row :gutter="20" :style="{ marginTop: showWorkTab ? '0' : '10px' }" class="card-list">
|
||||
<el-col v-for="(item, index) in dataList" :key="index" :sm="12" :md="6" :lg="6">
|
||||
<div class="card art-custom-card">
|
||||
<span class="des subtitle">{{ item.des }}</span>
|
||||
<CountTo
|
||||
class="number box-title"
|
||||
:endVal="item.num"
|
||||
:duration="1000"
|
||||
separator=""
|
||||
></CountTo>
|
||||
<div class="change-box">
|
||||
<span class="change-text">较上周</span>
|
||||
<span
|
||||
class="change"
|
||||
:class="[item.change.indexOf('+') === -1 ? 'text-danger' : 'text-success']"
|
||||
>
|
||||
{{ item.change }}
|
||||
</span>
|
||||
</div>
|
||||
<i class="iconfont-sys" v-html="item.icon"></i>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
import { CountTo } from 'vue3-count-to'
|
||||
|
||||
const { showWorkTab } = storeToRefs(useSettingStore())
|
||||
|
||||
const dataList = reactive([
|
||||
{
|
||||
des: '总访问次数',
|
||||
icon: '',
|
||||
startVal: 0,
|
||||
duration: 1000,
|
||||
num: 9120,
|
||||
change: '+20%'
|
||||
},
|
||||
{
|
||||
des: '在线访客数',
|
||||
icon: '',
|
||||
startVal: 0,
|
||||
duration: 1000,
|
||||
num: 182,
|
||||
change: '+10%'
|
||||
},
|
||||
{
|
||||
des: '点击量',
|
||||
icon: '',
|
||||
startVal: 0,
|
||||
duration: 1000,
|
||||
num: 9520,
|
||||
change: '-12%'
|
||||
},
|
||||
{
|
||||
des: '新用户',
|
||||
icon: '',
|
||||
startVal: 0,
|
||||
duration: 1000,
|
||||
num: 156,
|
||||
change: '+30%'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-list {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background-color: transparent !important;
|
||||
|
||||
.art-custom-card {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
padding: 0 18px;
|
||||
list-style: none;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
$icon-size: 52px;
|
||||
|
||||
.iconfont-sys {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 20px;
|
||||
bottom: 0;
|
||||
width: $icon-size;
|
||||
height: $icon-size;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
font-size: 22px;
|
||||
line-height: $icon-size;
|
||||
color: var(--el-color-primary) !important;
|
||||
text-align: center;
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.des {
|
||||
display: block;
|
||||
height: 14px;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.number {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.change-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
|
||||
.change-text {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: var(--art-text-gray-600);
|
||||
}
|
||||
|
||||
.change {
|
||||
display: block;
|
||||
margin-left: 5px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
|
||||
&.text-success {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
&.text-danger {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.card-list {
|
||||
.art-custom-card {
|
||||
.iconfont-sys {
|
||||
background-color: #232323 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
100
src/views/dashboard/console/widget/Dynamic.vue
Normal file
100
src/views/dashboard/console/widget/Dynamic.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<div class="title">
|
||||
<h4 class="box-title">动态</h4>
|
||||
<p class="subtitle">新增<span class="text-success">+6</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list">
|
||||
<div v-for="(item, index) in list" :key="index">
|
||||
<span class="user">{{ item.username }}</span>
|
||||
<span class="type">{{ item.type }}</span>
|
||||
<span class="target">{{ item.target }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue-demi'
|
||||
|
||||
const list = reactive([
|
||||
{
|
||||
username: '中小鱼',
|
||||
type: '关注了',
|
||||
target: '誶誶淰'
|
||||
},
|
||||
{
|
||||
username: '何小荷',
|
||||
type: '发表文章',
|
||||
target: 'Vue3 + Typescript + Vite 项目实战笔记'
|
||||
},
|
||||
{
|
||||
username: '誶誶淰',
|
||||
type: '提出问题',
|
||||
target: '主题可以配置吗'
|
||||
},
|
||||
{
|
||||
username: '发呆草',
|
||||
type: '兑换了物品',
|
||||
target: '《奇特的一生》'
|
||||
},
|
||||
{
|
||||
username: '甜筒',
|
||||
type: '关闭了问题',
|
||||
target: '发呆草'
|
||||
},
|
||||
{
|
||||
username: '冷月呆呆',
|
||||
type: '兑换了物品',
|
||||
target: '《高效人士的七个习惯》'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 510px;
|
||||
padding: 0 25px;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px 0 0;
|
||||
}
|
||||
|
||||
.list {
|
||||
height: calc(100% - 100px);
|
||||
margin-top: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
> div {
|
||||
height: 70px;
|
||||
overflow: hidden;
|
||||
line-height: 70px;
|
||||
border-bottom: 1px solid var(--art-border-color);
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.user {
|
||||
font-weight: 500;
|
||||
color: var(--art-text-gray-800);
|
||||
}
|
||||
|
||||
.type {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.target {
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
176
src/views/dashboard/console/widget/NewUser.vue
Normal file
176
src/views/dashboard/console/widget/NewUser.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<div class="title">
|
||||
<h4 class="box-title">新用户</h4>
|
||||
<p class="subtitle">这个月增长<span class="text-success">+20%</span></p>
|
||||
</div>
|
||||
<el-radio-group v-model="radio2">
|
||||
<el-radio-button value="本月" label="本月"></el-radio-button>
|
||||
<el-radio-button value="上月" label="上月"></el-radio-button>
|
||||
<el-radio-button value="今年" label="今年"></el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<art-table
|
||||
class="table"
|
||||
:data="tableData"
|
||||
:pagination="false"
|
||||
size="large"
|
||||
:border="false"
|
||||
:stripe="false"
|
||||
:show-header-background="false"
|
||||
>
|
||||
<template #default>
|
||||
<el-table-column label="头像" prop="avatar" width="150px">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<img class="avatar" :src="scope.row.avatar" />
|
||||
<span class="user-name">{{ scope.row.username }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="地区" prop="province" />
|
||||
<el-table-column label="性别" prop="avatar">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<span style="margin-left: 10px">{{ scope.row.sex === 1 ? '男' : '女' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="进度" width="240">
|
||||
<template #default="scope">
|
||||
<el-progress :percentage="scope.row.pro" :color="scope.row.color" :stroke-width="4" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</art-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, reactive } from 'vue-demi'
|
||||
import avatar1 from '@/assets/img/avatar/avatar1.webp'
|
||||
import avatar2 from '@/assets/img/avatar/avatar2.webp'
|
||||
import avatar3 from '@/assets/img/avatar/avatar3.webp'
|
||||
import avatar4 from '@/assets/img/avatar/avatar4.webp'
|
||||
import avatar5 from '@/assets/img/avatar/avatar5.webp'
|
||||
import avatar6 from '@/assets/img/avatar/avatar6.webp'
|
||||
|
||||
const radio2 = ref('本月')
|
||||
|
||||
const tableData = reactive([
|
||||
{
|
||||
username: '中小鱼',
|
||||
province: '北京',
|
||||
sex: 0,
|
||||
age: 22,
|
||||
percentage: 60,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-primary)) !important',
|
||||
avatar: avatar1
|
||||
},
|
||||
{
|
||||
username: '何小荷',
|
||||
province: '深圳',
|
||||
sex: 1,
|
||||
age: 21,
|
||||
percentage: 20,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-secondary)) !important',
|
||||
avatar: avatar2
|
||||
},
|
||||
{
|
||||
username: '誶誶淰',
|
||||
province: '上海',
|
||||
sex: 1,
|
||||
age: 23,
|
||||
percentage: 60,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-warning)) !important',
|
||||
avatar: avatar3
|
||||
},
|
||||
{
|
||||
username: '发呆草',
|
||||
province: '长沙',
|
||||
sex: 0,
|
||||
age: 28,
|
||||
percentage: 50,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-info)) !important',
|
||||
avatar: avatar4
|
||||
},
|
||||
{
|
||||
username: '甜筒',
|
||||
province: '浙江',
|
||||
sex: 1,
|
||||
age: 26,
|
||||
percentage: 70,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-error)) !important',
|
||||
avatar: avatar5
|
||||
},
|
||||
{
|
||||
username: '冷月呆呆',
|
||||
province: '湖北',
|
||||
sex: 1,
|
||||
age: 25,
|
||||
percentage: 90,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-success)) !important',
|
||||
avatar: avatar6
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
addAnimation()
|
||||
})
|
||||
|
||||
const addAnimation = () => {
|
||||
setTimeout(() => {
|
||||
for (let i = 0; i < tableData.length; i++) {
|
||||
let item = tableData[i]
|
||||
tableData[i].pro = item.percentage
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.card {
|
||||
// 进度动画
|
||||
.el-progress-bar__inner {
|
||||
transition: all 1s !important;
|
||||
}
|
||||
|
||||
.el-radio-button__original-radio:checked + .el-radio-button__inner {
|
||||
color: var(--el-color-primary) !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
width: 100%;
|
||||
height: 510px;
|
||||
overflow: hidden;
|
||||
|
||||
.card-header {
|
||||
padding-left: 25px !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr:last-child td) {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
143
src/views/dashboard/console/widget/SalesOverview.vue
Normal file
143
src/views/dashboard/console/widget/SalesOverview.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<div class="title">
|
||||
<h4 class="box-title">访问量</h4>
|
||||
<p class="subtitle">今年增长<span class="text-success">+15%</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart" ref="chartRef"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as echarts from 'echarts'
|
||||
import { hexToRgba, getCssVar } from '@/utils/ui'
|
||||
import { EChartsOption } from 'echarts'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
|
||||
const {
|
||||
chartRef,
|
||||
isDark,
|
||||
initChart,
|
||||
updateChart,
|
||||
getAxisLabelStyle,
|
||||
getAxisLineStyle,
|
||||
getAxisTickStyle,
|
||||
getSplitLineStyle
|
||||
} = useChart()
|
||||
|
||||
// 定义真实数据
|
||||
const realData = [50, 25, 40, 20, 70, 35, 65, 30, 35, 20, 40, 44]
|
||||
|
||||
// 初始化动画函数
|
||||
const initChartWithAnimation = () => {
|
||||
// 首先初始化图表,数据为0
|
||||
initChart(options(true))
|
||||
updateChart(options(false))
|
||||
}
|
||||
|
||||
watch(isDark, () => {
|
||||
initChart(options())
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initChartWithAnimation()
|
||||
})
|
||||
|
||||
const options: (isInitial?: boolean) => EChartsOption = (isInitial) => {
|
||||
const isInit = isInitial || false
|
||||
return {
|
||||
// 添加动画配置
|
||||
animation: true,
|
||||
animationDuration: 0,
|
||||
animationDurationUpdate: 0,
|
||||
grid: {
|
||||
left: '2.2%',
|
||||
right: '3%',
|
||||
bottom: '0%',
|
||||
top: '5px',
|
||||
containLabel: true
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: [
|
||||
'1月',
|
||||
'2月',
|
||||
'3月',
|
||||
'4月',
|
||||
'5月',
|
||||
'6月',
|
||||
'7月',
|
||||
'8月',
|
||||
'9月',
|
||||
'10月',
|
||||
'11月',
|
||||
'12月'
|
||||
],
|
||||
axisTick: getAxisTickStyle(),
|
||||
axisLabel: getAxisLabelStyle(true),
|
||||
axisLine: getAxisLineStyle(true)
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: realData.reduce((prev, curr) => Math.max(prev, curr), 0),
|
||||
axisLabel: getAxisLabelStyle(true),
|
||||
axisLine: getAxisLineStyle(!isDark.value),
|
||||
splitLine: getSplitLineStyle(true)
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '访客',
|
||||
color: getCssVar('--main-color'),
|
||||
type: 'line',
|
||||
stack: '总量',
|
||||
data: isInit ? new Array(12).fill(0) : realData,
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
width: 2.2
|
||||
},
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: hexToRgba(getCssVar('--el-color-primary'), 0.15).rgba
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: hexToRgba(getCssVar('--el-color-primary'), 0.01).rgba
|
||||
}
|
||||
])
|
||||
},
|
||||
animationDuration: 0,
|
||||
animationDurationUpdate: 1500
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 420px;
|
||||
padding: 20px 0 30px;
|
||||
|
||||
.card-header {
|
||||
padding: 0 18px !important;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: calc(100% - 80px);
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
102
src/views/dashboard/console/widget/TodoList.vue
Normal file
102
src/views/dashboard/console/widget/TodoList.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<div class="title">
|
||||
<h4 class="box-title">代办事项</h4>
|
||||
<p class="subtitle">待处理<span class="text-danger">3</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list">
|
||||
<div v-for="(item, index) in list" :key="index">
|
||||
<p class="title">{{ item.username }}</p>
|
||||
<p class="date subtitle">{{ item.date }}</p>
|
||||
<el-checkbox v-model="item.complate" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue-demi'
|
||||
|
||||
const list = reactive([
|
||||
{
|
||||
username: '查看今天工作内容',
|
||||
date: '上午 09:30',
|
||||
complate: true
|
||||
},
|
||||
{
|
||||
username: '回复邮件',
|
||||
date: '上午 10:30',
|
||||
complate: true
|
||||
},
|
||||
{
|
||||
username: '工作汇报整理',
|
||||
date: '上午 11:00',
|
||||
complate: true
|
||||
},
|
||||
{
|
||||
username: '产品需求会议',
|
||||
date: '下午 02:00',
|
||||
complate: false
|
||||
},
|
||||
{
|
||||
username: '整理会议内容',
|
||||
date: '下午 03:30',
|
||||
complate: false
|
||||
},
|
||||
{
|
||||
username: '明天工作计划',
|
||||
date: '下午 06:30',
|
||||
complate: false
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 510px;
|
||||
padding: 0 25px;
|
||||
|
||||
.list {
|
||||
height: calc(100% - 90px);
|
||||
margin-top: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
> div {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 70px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid var(--art-border-color);
|
||||
|
||||
p {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.date {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.el-checkbox {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
80
src/views/dashboard/ecommerce/index.vue
Normal file
80
src/views/dashboard/ecommerce/index.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="ecommerce">
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :md="24" :lg="16">
|
||||
<Banner />
|
||||
</el-col>
|
||||
<el-col :sm="12" :md="12" :lg="4">
|
||||
<TotalOrderVolume />
|
||||
</el-col>
|
||||
<el-col :sm="12" :md="12" :lg="4">
|
||||
<TotalProducts />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="12" :md="12" :lg="8">
|
||||
<SalesTrend />
|
||||
</el-col>
|
||||
<el-col :sm="12" :md="12" :lg="8">
|
||||
<SalesClassification />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="24" :lg="8">
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :md="12" :lg="12">
|
||||
<ProductSales />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" :lg="12">
|
||||
<SalesGrowth />
|
||||
</el-col>
|
||||
<el-col :span="24" class="no-margin-bottom">
|
||||
<CartConversionRate />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :md="12" :lg="8">
|
||||
<HotCommodity />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" :lg="8">
|
||||
<AnnualSales />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="24" :lg="8">
|
||||
<TransactionList />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :md="24" :lg="8">
|
||||
<RecentTransaction />
|
||||
</el-col>
|
||||
<el-col :md="24" :lg="16" class="no-margin-bottom">
|
||||
<HotProductsList />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Banner from './widget/Banner.vue'
|
||||
import TotalOrderVolume from './widget/TotalOrderVolume.vue'
|
||||
import TotalProducts from './widget/TotalProducts.vue'
|
||||
import SalesTrend from './widget/SalesTrend.vue'
|
||||
import SalesClassification from './widget/SalesClassification.vue'
|
||||
import TransactionList from './widget/TransactionList.vue'
|
||||
import HotCommodity from './widget/HotCommodity.vue'
|
||||
import RecentTransaction from './widget/RecentTransaction.vue'
|
||||
import AnnualSales from './widget/AnnualSales.vue'
|
||||
import ProductSales from './widget/ProductSales.vue'
|
||||
import SalesGrowth from './widget/SalesGrowth.vue'
|
||||
import CartConversionRate from './widget/CartConversionRate.vue'
|
||||
import HotProductsList from './widget/HotProductsList.vue'
|
||||
|
||||
defineOptions({ name: 'Ecommerce' })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './style';
|
||||
</style>
|
||||
72
src/views/dashboard/ecommerce/style.scss
Normal file
72
src/views/dashboard/ecommerce/style.scss
Normal file
@@ -0,0 +1,72 @@
|
||||
.ecommerce {
|
||||
:deep(.card) {
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
background-color: var(--art-main-bg-color);
|
||||
border-radius: var(--custom-radius);
|
||||
|
||||
.card-header {
|
||||
padding-bottom: 15px;
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: var(--art-gray-900);
|
||||
|
||||
i {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--art-gray-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.icon-text-widget) {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
margin-right: 10px;
|
||||
line-height: 42px;
|
||||
color: var(--main-color);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: 8px;
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
p {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-margin-bottom {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.el-col {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
37
src/views/dashboard/ecommerce/widget/AnnualSales.vue
Normal file
37
src/views/dashboard/ecommerce/widget/AnnualSales.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="card art-custom-card yearly-card" style="height: 28.2rem">
|
||||
<div class="card-header">
|
||||
<p class="title">年度销售额</p>
|
||||
<p class="subtitle">按季度统计</p>
|
||||
</div>
|
||||
|
||||
<ArtBarChart
|
||||
:showAxisLabel="false"
|
||||
:showAxisLine="false"
|
||||
:showSplitLine="false"
|
||||
:data="[50, 80, 50, 90, 60, 70, 50]"
|
||||
barWidth="26px"
|
||||
height="16rem"
|
||||
/>
|
||||
<div class="icon-text-widget" style="margin-top: 50px">
|
||||
<div class="item">
|
||||
<div class="icon">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>¥200,858</p>
|
||||
<span>线上销售</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="icon">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>¥102,927</p>
|
||||
<span>线下销售</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
73
src/views/dashboard/ecommerce/widget/Banner.vue
Normal file
73
src/views/dashboard/ecommerce/widget/Banner.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<ArtBasicBanner
|
||||
class="banner"
|
||||
:title="`欢迎回来 ${userInfo.userName}`"
|
||||
:showButton="false"
|
||||
backgroundColor="var(--el-color-primary-light-9)"
|
||||
titleColor="var(--art-gray-900)"
|
||||
subtitleColor="#666"
|
||||
style="height: 13.3rem"
|
||||
:backgroundImage="bannerCover"
|
||||
:showDecoration="false"
|
||||
imgWidth="18rem"
|
||||
imgBottom="-7.5rem"
|
||||
:showMeteors="true"
|
||||
@click="handleBannerClick"
|
||||
>
|
||||
<div class="banner-slot">
|
||||
<div class="item">
|
||||
<p class="title">¥2,340<i class="iconfont-sys text-success"></i></p>
|
||||
<p class="subtitle">今日销售额</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<p class="title">35%<i class="iconfont-sys text-success"></i></p>
|
||||
<p class="subtitle">较昨日</p>
|
||||
</div>
|
||||
</div>
|
||||
</ArtBasicBanner>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import bannerCover from '@imgs/login/lf_icon2.webp'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
const userStore = useUserStore()
|
||||
|
||||
const userInfo = computed(() => userStore.getUserInfo)
|
||||
|
||||
const handleBannerClick = () => {}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.banner {
|
||||
.banner-slot {
|
||||
display: flex;
|
||||
|
||||
.item {
|
||||
margin-right: 30px;
|
||||
|
||||
&:first-of-type {
|
||||
padding-right: 30px;
|
||||
border-right: 1px solid var(--art-gray-300);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 30px;
|
||||
color: var(--art-gray-900) !important;
|
||||
|
||||
i {
|
||||
position: relative;
|
||||
top: -10px;
|
||||
margin-left: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
color: var(--art-gray-700) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
11
src/views/dashboard/ecommerce/widget/CartConversionRate.vue
Normal file
11
src/views/dashboard/ecommerce/widget/CartConversionRate.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<ArtLineChartCard
|
||||
class="margin-bottom-0"
|
||||
:value="2545"
|
||||
label="购物车转化率"
|
||||
:percentage="1.2"
|
||||
:height="13.5"
|
||||
:chartData="[120, 132, 101, 134, 90, 230, 210]"
|
||||
:showAreaColor="true"
|
||||
/>
|
||||
</template>
|
||||
107
src/views/dashboard/ecommerce/widget/HotCommodity.vue
Normal file
107
src/views/dashboard/ecommerce/widget/HotCommodity.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="card art-custom-card weekly-card" style="height: 28.2rem">
|
||||
<div class="card-header">
|
||||
<p class="title">热销商品</p>
|
||||
<p class="subtitle">本周销售排行</p>
|
||||
</div>
|
||||
<ArtLineChart
|
||||
:showAxisLabel="false"
|
||||
:showAxisLine="false"
|
||||
:showSplitLine="false"
|
||||
:showAreaColor="true"
|
||||
:data="[8, 40, 82, 35, 90, 52, 35]"
|
||||
height="9rem"
|
||||
/>
|
||||
<div class="content">
|
||||
<div class="item" v-for="item in weeklyList" :key="item.title">
|
||||
<div class="icon" :class="item.color">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="text">
|
||||
<p class="title">{{ item.title }}</p>
|
||||
<span class="subtitle">{{ item.subtitle }}</span>
|
||||
</div>
|
||||
<div class="value" :class="item.color">
|
||||
<span>+{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const weeklyList = [
|
||||
{
|
||||
icon: '',
|
||||
title: '智能手表Pro',
|
||||
subtitle: '电子产品',
|
||||
value: '1,286件',
|
||||
color: 'bg-primary'
|
||||
},
|
||||
{
|
||||
icon: '',
|
||||
title: '时尚连衣裙',
|
||||
subtitle: '女装服饰',
|
||||
value: '892件',
|
||||
color: 'bg-success'
|
||||
},
|
||||
{
|
||||
icon: '',
|
||||
title: '厨房小家电',
|
||||
subtitle: '家居用品',
|
||||
value: '756件',
|
||||
color: 'bg-error'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.weekly-card {
|
||||
.content {
|
||||
margin-top: 40px;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
|
||||
.icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
line-height: 42px;
|
||||
text-align: center;
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: 8px;
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 10px;
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--art-gray-800);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--art-gray-600);
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
padding: 6px 12px;
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
231
src/views/dashboard/ecommerce/widget/HotProductsList.vue
Normal file
231
src/views/dashboard/ecommerce/widget/HotProductsList.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<div class="card art-custom-card" style="height: 27.8rem">
|
||||
<div class="card-header">
|
||||
<p class="title">热销产品</p>
|
||||
<p class="subtitle">本月销售情况</p>
|
||||
</div>
|
||||
<div class="table">
|
||||
<el-scrollbar style="height: 21.55rem">
|
||||
<art-table
|
||||
:data="tableData"
|
||||
:pagination="false"
|
||||
style="margin-top: 0 !important"
|
||||
size="large"
|
||||
:border="false"
|
||||
:stripe="false"
|
||||
:show-header-background="false"
|
||||
>
|
||||
<template #default>
|
||||
<el-table-column label="产品" prop="product" width="220px">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<img class="product-image" :src="scope.row.image" />
|
||||
<div class="product-info">
|
||||
<div class="product-name">{{ scope.row.name }}</div>
|
||||
<div class="product-category">{{ scope.row.category }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="价格" prop="price">
|
||||
<template #default="scope">
|
||||
<span class="price">¥{{ scope.row.price.toLocaleString() }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" prop="stock">
|
||||
<template #default="scope">
|
||||
<div class="stock-badge" :class="getStockClass(scope.row.stock)">
|
||||
{{ getStockStatus(scope.row.stock) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="销量" prop="sales" />
|
||||
<el-table-column label="销售趋势" width="240">
|
||||
<template #default="scope">
|
||||
<el-progress
|
||||
:percentage="scope.row.pro"
|
||||
:color="scope.row.color"
|
||||
:stroke-width="4"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</art-table>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted } from 'vue'
|
||||
|
||||
// 导入产品图片
|
||||
import product1 from '@/assets/img/3d/icon1.webp'
|
||||
import product2 from '@/assets/img/3d/icon2.webp'
|
||||
import product3 from '@/assets/img/3d/icon3.webp'
|
||||
import product4 from '@/assets/img/3d/icon4.webp'
|
||||
import product5 from '@/assets/img/3d/icon5.webp'
|
||||
import product6 from '@/assets/img/3d/icon6.webp'
|
||||
|
||||
const tableData = reactive([
|
||||
{
|
||||
name: '智能手表 Pro',
|
||||
category: '电子设备',
|
||||
price: 1299,
|
||||
stock: 156,
|
||||
sales: 423,
|
||||
percentage: 75,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-primary)) !important',
|
||||
image: product1
|
||||
},
|
||||
{
|
||||
name: '无线蓝牙耳机',
|
||||
category: '音频设备',
|
||||
price: 499,
|
||||
stock: 89,
|
||||
sales: 652,
|
||||
percentage: 85,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-success)) !important',
|
||||
image: product2
|
||||
},
|
||||
{
|
||||
name: '机械键盘',
|
||||
category: '电脑配件',
|
||||
price: 399,
|
||||
stock: 12,
|
||||
sales: 238,
|
||||
percentage: 45,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-warning)) !important',
|
||||
image: product3
|
||||
},
|
||||
{
|
||||
name: '超薄笔记本电脑',
|
||||
category: '电子设备',
|
||||
price: 5999,
|
||||
stock: 0,
|
||||
sales: 126,
|
||||
percentage: 30,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-error)) !important',
|
||||
image: product4
|
||||
},
|
||||
{
|
||||
name: '智能音箱',
|
||||
category: '智能家居',
|
||||
price: 799,
|
||||
stock: 45,
|
||||
sales: 321,
|
||||
percentage: 60,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-info)) !important',
|
||||
image: product5
|
||||
},
|
||||
{
|
||||
name: '游戏手柄',
|
||||
category: '游戏配件',
|
||||
price: 299,
|
||||
stock: 78,
|
||||
sales: 489,
|
||||
percentage: 70,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-secondary)) !important',
|
||||
image: product6
|
||||
}
|
||||
])
|
||||
|
||||
// 根据库存获取状态文本
|
||||
const getStockStatus = (stock: number) => {
|
||||
if (stock === 0) return '缺货'
|
||||
if (stock < 20) return '低库存'
|
||||
if (stock < 50) return '适中'
|
||||
return '充足'
|
||||
}
|
||||
|
||||
// 根据库存获取状态类名
|
||||
const getStockClass = (stock: number) => {
|
||||
if (stock === 0) return 'out-of-stock'
|
||||
if (stock < 20) return 'low-stock'
|
||||
if (stock < 50) return 'medium-stock'
|
||||
return 'in-stock'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
addAnimation()
|
||||
})
|
||||
|
||||
const addAnimation = () => {
|
||||
setTimeout(() => {
|
||||
for (let i = 0; i < tableData.length; i++) {
|
||||
let item = tableData[i]
|
||||
tableData[i].pro = item.percentage
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.table {
|
||||
width: 100%;
|
||||
|
||||
.card-header {
|
||||
padding-left: 25px !important;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.product-category {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.price {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stock-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.in-stock {
|
||||
color: rgb(var(--art-success));
|
||||
background-color: rgba(var(--art-success-rgb), 0.1);
|
||||
}
|
||||
|
||||
.medium-stock {
|
||||
color: rgb(var(--art-info));
|
||||
background-color: rgba(var(--art-info-rgb), 0.1);
|
||||
}
|
||||
|
||||
.low-stock {
|
||||
color: rgb(var(--art-warning));
|
||||
background-color: rgba(var(--art-warning-rgb), 0.1);
|
||||
}
|
||||
|
||||
.out-of-stock {
|
||||
color: rgb(var(--art-error));
|
||||
background-color: rgba(var(--art-error-rgb), 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
19
src/views/dashboard/ecommerce/widget/ProductSales.vue
Normal file
19
src/views/dashboard/ecommerce/widget/ProductSales.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="card art-custom-card" style="height: 11rem">
|
||||
<div class="card-header">
|
||||
<p class="title" style="font-size: 24px"
|
||||
>14.5k<i class="iconfont-sys text-success"></i></p
|
||||
>
|
||||
<p class="subtitle">销售量</p>
|
||||
</div>
|
||||
|
||||
<ArtBarChart
|
||||
:showAxisLabel="false"
|
||||
:showAxisLine="false"
|
||||
:showSplitLine="false"
|
||||
:data="[50, 80, 50, 90, 60, 70, 50]"
|
||||
barWidth="16px"
|
||||
height="4rem"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
41
src/views/dashboard/ecommerce/widget/RecentTransaction.vue
Normal file
41
src/views/dashboard/ecommerce/widget/RecentTransaction.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<ArtTimelineListCard :list="timelineData" title="最近交易" subtitle="今日订单动态" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const timelineData = [
|
||||
{
|
||||
time: '上午 09:30',
|
||||
status: 'rgb(73, 190, 255)',
|
||||
content: '收到订单 #38291 支付 ¥385.90'
|
||||
},
|
||||
{
|
||||
time: '上午 10:00',
|
||||
status: 'rgb(54, 158, 255)',
|
||||
content: '新商品上架',
|
||||
code: 'SKU-3467'
|
||||
},
|
||||
{
|
||||
time: '上午 12:00',
|
||||
status: 'rgb(103, 232, 207)',
|
||||
content: '向供应商支付了 ¥6495.00'
|
||||
},
|
||||
{
|
||||
time: '下午 14:30',
|
||||
status: 'rgb(255, 193, 7)',
|
||||
content: '促销活动开始',
|
||||
code: 'PROMO-2023'
|
||||
},
|
||||
{
|
||||
time: '下午 15:45',
|
||||
status: 'rgb(255, 105, 105)',
|
||||
content: '订单取消提醒',
|
||||
code: 'ORD-9876'
|
||||
},
|
||||
{
|
||||
time: '下午 17:00',
|
||||
status: 'rgb(103, 232, 207)',
|
||||
content: '完成日销售报表'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
41
src/views/dashboard/ecommerce/widget/SalesClassification.vue
Normal file
41
src/views/dashboard/ecommerce/widget/SalesClassification.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="card art-custom-card sales-card" style="height: 26rem">
|
||||
<div class="card-header">
|
||||
<p class="title">销售分类</p>
|
||||
<p class="subtitle">按产品类别</p>
|
||||
</div>
|
||||
<ArtRingChart
|
||||
:data="[
|
||||
{ value: 30, name: '电子产品' },
|
||||
{ value: 55, name: '服装鞋包' },
|
||||
{ value: 36, name: '家居用品' }
|
||||
]"
|
||||
:color="['#4C87F3', '#EDF2FF', '#8BD8FC']"
|
||||
:radius="['70%', '80%']"
|
||||
height="16.5rem"
|
||||
:showLabel="false"
|
||||
:borderRadius="0"
|
||||
centerText="¥300,458"
|
||||
/>
|
||||
<div class="icon-text-widget">
|
||||
<div class="item">
|
||||
<div class="icon">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>¥500,458</p>
|
||||
<span>总收入</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="icon">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>¥130,580</p>
|
||||
<span>净利润</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
20
src/views/dashboard/ecommerce/widget/SalesGrowth.vue
Normal file
20
src/views/dashboard/ecommerce/widget/SalesGrowth.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="card art-custom-card" style="height: 11rem">
|
||||
<div class="card-header">
|
||||
<p class="title" style="font-size: 24px"
|
||||
>12%<i class="iconfont-sys text-success"></i></p
|
||||
>
|
||||
<p class="subtitle">增长</p>
|
||||
</div>
|
||||
|
||||
<ArtLineChart
|
||||
:showAreaColor="true"
|
||||
:showAxisLabel="false"
|
||||
:showAxisLine="false"
|
||||
:showSplitLine="false"
|
||||
:data="[50, 85, 65, 95, 75, 130, 180]"
|
||||
barWidth="16px"
|
||||
height="4rem"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
15
src/views/dashboard/ecommerce/widget/SalesTrend.vue
Normal file
15
src/views/dashboard/ecommerce/widget/SalesTrend.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="card art-custom-card" style="height: 26rem">
|
||||
<div class="card-header">
|
||||
<p class="title">销售趋势</p>
|
||||
<p class="subtitle">月度销售对比</p>
|
||||
</div>
|
||||
<ArtDualBarCompareChart
|
||||
:topData="[50, 80, 120, 90, 60]"
|
||||
:bottomData="[30, 60, 90, 70, 40]"
|
||||
:xAxisData="['一月', '二月', '三月', '四月', '五月']"
|
||||
height="18rem"
|
||||
:barWidth="16"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
20
src/views/dashboard/ecommerce/widget/TotalOrderVolume.vue
Normal file
20
src/views/dashboard/ecommerce/widget/TotalOrderVolume.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="card art-custom-card" style="height: 13.3rem">
|
||||
<div class="card-header">
|
||||
<p class="title" style="font-size: 24px">205,216</p>
|
||||
<p class="subtitle">总订单量</p>
|
||||
</div>
|
||||
<ArtRingChart
|
||||
:data="[
|
||||
{ value: 30, name: '已完成' },
|
||||
{ value: 25, name: '处理中' },
|
||||
{ value: 45, name: '待发货' }
|
||||
]"
|
||||
:color="['#4C87F3', '#93F1B4', '#8BD8FC']"
|
||||
:radius="['56%', '76%']"
|
||||
height="7rem"
|
||||
:showLabel="false"
|
||||
:borderRadius="0"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
16
src/views/dashboard/ecommerce/widget/TotalProducts.vue
Normal file
16
src/views/dashboard/ecommerce/widget/TotalProducts.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="card art-custom-card" style="height: 13.3rem">
|
||||
<div class="card-header">
|
||||
<p class="title" style="font-size: 24px">55,231</p>
|
||||
<p class="subtitle">商品总数</p>
|
||||
</div>
|
||||
<ArtBarChart
|
||||
:showAxisLabel="false"
|
||||
:showAxisLine="false"
|
||||
:showSplitLine="false"
|
||||
:data="[50, 80, 40, 90, 60, 70]"
|
||||
height="7rem"
|
||||
barWidth="18px"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
52
src/views/dashboard/ecommerce/widget/TransactionList.vue
Normal file
52
src/views/dashboard/ecommerce/widget/TransactionList.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<ArtDataListCard
|
||||
:maxCount="4"
|
||||
:list="dataList"
|
||||
title="最近活动"
|
||||
subtitle="订单处理状态"
|
||||
:showMoreButton="true"
|
||||
@more="handleMore"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const dataList = [
|
||||
{
|
||||
title: '新订单 #38291',
|
||||
status: '待处理',
|
||||
time: '5分钟',
|
||||
class: 'bg-primary',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
title: '退款申请 #12845',
|
||||
status: '处理中',
|
||||
time: '10分钟',
|
||||
class: 'bg-secondary',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
title: '客户投诉处理',
|
||||
status: '待处理',
|
||||
time: '15分钟',
|
||||
class: 'bg-warning',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
title: '库存不足提醒',
|
||||
status: '紧急',
|
||||
time: '20分钟',
|
||||
class: 'bg-danger',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
title: '订单 #29384 已发货',
|
||||
status: '已完成',
|
||||
time: '20分钟',
|
||||
class: 'bg-success',
|
||||
icon: ''
|
||||
}
|
||||
]
|
||||
|
||||
const handleMore = () => {}
|
||||
</script>
|
||||
Reference in New Issue
Block a user