第一次提交

This commit is contained in:
gxwebsoft
2023-08-04 13:32:43 +08:00
commit c02e8be49b
1151 changed files with 200453 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
<template>
<a-card :bordered="false" title="热门搜索">
<v-chart
ref="hotSearchChartRef"
:option="hotSearchChartOption"
style="height: 330px"
/>
</a-card>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue/es';
import { use } from 'echarts/core';
import type { EChartsCoreOption } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import { LineChart, BarChart } from 'echarts/charts';
import { GridComponent, TooltipComponent } from 'echarts/components';
import VChart from 'vue-echarts';
import 'echarts-wordcloud';
import { wordCloudColor } from 'ele-admin-pro/es';
import { getWordCloudList } from '@/api/dashboard/analysis';
import useEcharts from '@/utils/use-echarts';
use([CanvasRenderer, LineChart, BarChart, GridComponent, TooltipComponent]);
//
const hotSearchChartRef = ref<InstanceType<typeof VChart> | null>(null);
useEcharts([hotSearchChartRef]);
// 词云图表配置
const hotSearchChartOption: EChartsCoreOption = reactive({});
/* 获取词云数据 */
const getWordCloudData = () => {
getWordCloudList()
.then((data) => {
Object.assign(hotSearchChartOption, {
tooltip: {
show: true,
confine: true,
borderWidth: 1
},
series: [
{
type: 'wordCloud',
width: '100%',
height: '100%',
sizeRange: [12, 24],
gridSize: 6,
textStyle: {
color: wordCloudColor
},
emphasis: {
textStyle: {
shadowBlur: 8,
shadowColor: 'rgba(0, 0, 0, .15)'
}
},
data: data
}
]
});
})
.catch((e) => {
message.error(e.message);
});
};
getWordCloudData();
</script>

View File

@@ -0,0 +1,248 @@
<template>
<a-card :bordered="false" :body-style="{ padding: 0 }">
<a-tabs
size="large"
v-model:activeKey="saleSearch.type"
class="monitor-card-tabs"
@change="onSaleTypeChange"
>
<a-tab-pane tab="销售额" key="saleroom" />
<a-tab-pane tab="访问量" key="visits" />
<template #rightExtra>
<a-space
size="middle"
:class="[
'analysis-tabs-extra',
{ 'hidden-lg-and-down': styleResponsive }
]"
>
<a-radio-group v-model:value="saleSearch.dateType">
<a-radio-button value="1">今天</a-radio-button>
<a-radio-button value="2">本周</a-radio-button>
<a-radio-button value="3">本月</a-radio-button>
<a-radio-button value="4">本年</a-radio-button>
</a-radio-group>
<div style="width: 300px">
<a-range-picker
value-format="YYYY-MM-DD"
v-model:value="saleSearch.datetime"
/>
</div>
</a-space>
</template>
</a-tabs>
<div style="padding-bottom: 10px">
<a-row :gutter="16">
<a-col
v-bind="
styleResponsive ? { lg: 17, md: 15, sm: 24, xs: 24 } : { span: 17 }
"
>
<div v-if="saleSearch.type === 'saleroom'" class="demo-monitor-title">
销售量趋势
</div>
<div v-else class="demo-monitor-title">访问量趋势</div>
<v-chart
ref="saleChartRef"
:option="saleChartOption"
style="height: 320px"
/>
</a-col>
<a-col
v-bind="
styleResponsive ? { lg: 7, md: 9, sm: 24, xs: 24 } : { span: 7 }
"
>
<div v-if="saleSearch.type === 'saleroom'">
<div class="demo-monitor-title">门店销售额排名</div>
<div
v-for="(item, index) in saleroomRankData"
:key="index"
class="demo-monitor-rank-item ele-cell"
>
<ele-tag
shape="circle"
:color="index < 3 ? '#314659' : ''"
style="border: none"
>
{{ index + 1 }}
</ele-tag>
<div class="ele-cell-content ele-elip">{{ item.name }}</div>
<div class="ele-text-secondary">{{ item.value }}</div>
</div>
</div>
<div v-else>
<div class="demo-monitor-title">门店访问量排名</div>
<div
v-for="(item, index) in visitsRankData"
:key="index"
class="demo-monitor-rank-item ele-cell"
>
<ele-tag
shape="circle"
:color="index < 3 ? '#314659' : ''"
style="border: none"
>
{{ index + 1 }}
</ele-tag>
<div class="ele-cell-content ele-elip">{{ item.name }}</div>
<div class="ele-text-secondary">{{ item.value }}</div>
</div>
</div>
</a-col>
</a-row>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue/es';
import { use } from 'echarts/core';
import type { EChartsCoreOption } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import { BarChart } from 'echarts/charts';
import { GridComponent, TooltipComponent } from 'echarts/components';
import VChart from 'vue-echarts';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import { getSaleroomList } from '@/api/dashboard/analysis';
import type { SaleroomData } from '@/api/dashboard/analysis/model';
import useEcharts from '@/utils/use-echarts';
use([CanvasRenderer, BarChart, GridComponent, TooltipComponent]);
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
//
const saleChartRef = ref<InstanceType<typeof VChart> | null>(null);
useEcharts([saleChartRef]);
// 销售额柱状图配置
const saleChartOption: EChartsCoreOption = reactive({});
// 门店销售排名数据
const saleroomRankData = ref([
{ name: '工专路 1 号店', value: '323,234' },
{ name: '工专路 2 号店', value: '323,234' },
{ name: '工专路 3 号店', value: '323,234' },
{ name: '工专路 4 号店', value: '323,234' },
{ name: '工专路 5 号店', value: '323,234' },
{ name: '工专路 6 号店', value: '323,234' },
{ name: '工专路 7 号店', value: '323,234' }
]);
// 门店访问排名数据
const visitsRankData = ref([
{ name: '工专路 1 号店', value: '323,234' },
{ name: '工专路 2 号店', value: '323,234' },
{ name: '工专路 3 号店', value: '323,234' },
{ name: '工专路 4 号店', value: '323,234' },
{ name: '工专路 5 号店', value: '323,234' },
{ name: '工专路 6 号店', value: '323,234' },
{ name: '工专路 7 号店', value: '323,234' }
]);
// 销售量趋势数据
const saleroomData1 = ref<SaleroomData[]>([]);
// 访问量趋势数据
const saleroomData2 = ref<SaleroomData[]>([]);
interface SaleSearchType {
type: string;
dateType: string;
datetime: [string, string];
}
// 销售量搜索参数
const saleSearch = reactive<SaleSearchType>({
type: 'saleroom',
dateType: '1',
datetime: ['2022-01-08', '2022-02-12']
});
/* 获取销售量数据 */
const getSaleroomData = () => {
getSaleroomList()
.then((data) => {
saleroomData1.value = data.list1;
saleroomData2.value = data.list2;
onSaleTypeChange();
})
.catch((e) => {
message.error(e.message);
});
};
/* 销售量tab选择改变事件 */
const onSaleTypeChange = () => {
if (saleSearch.type === 'saleroom') {
Object.assign(saleChartOption, {
tooltip: {
trigger: 'axis'
},
xAxis: [
{
type: 'category',
data: saleroomData1.value.map((d) => d.month)
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
type: 'bar',
data: saleroomData1.value.map((d) => d.value)
}
]
});
} else {
Object.assign(saleChartOption, {
tooltip: {
trigger: 'axis'
},
xAxis: [
{
type: 'category',
data: saleroomData2.value.map((d) => d.month)
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
type: 'bar',
data: saleroomData2.value.map((d) => d.value)
}
]
});
}
};
getSaleroomData();
</script>
<style lang="less" scoped>
.monitor-card-tabs :deep(.ant-tabs-nav) {
padding: 0 16px;
}
.demo-monitor-title {
padding: 6px 20px;
}
.demo-monitor-rank-item {
padding: 0 20px;
margin-top: 18px;
}
</style>

View File

@@ -0,0 +1,246 @@
<!-- 统计卡片 -->
<template>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { lg: 6, md: 12, sm: 24, xs: 24 } : { span: 6 }"
>
<a-card class="analysis-chart-card" :bordered="false">
<div class="ele-text-secondary ele-cell">
<div class="ele-cell-content">总销售额</div>
<a-tooltip title="指标说明">
<question-circle-outlined />
</a-tooltip>
</div>
<h1 class="analysis-chart-card-num">¥ 126,560</h1>
<div class="analysis-chart-card-content" style="padding-top: 16px">
<a-space size="middle">
<span class="analysis-trend-text">
周同比12% <caret-up-outlined class="ele-text-danger" />
</span>
<span class="analysis-trend-text">
日同比11% <caret-down-outlined class="ele-text-success" />
</span>
</a-space>
</div>
<a-divider />
<div>日销售额 12,423</div>
</a-card>
</a-col>
<a-col
v-bind="styleResponsive ? { lg: 6, md: 12, sm: 24, xs: 24 } : { span: 6 }"
>
<a-card class="analysis-chart-card" :bordered="false">
<div class="ele-text-secondary ele-cell">
<div class="ele-cell-content">访问量</div>
<ele-tag color="red"></ele-tag>
</div>
<h1 class="analysis-chart-card-num">8,846</h1>
<v-chart
ref="visitChartRef"
:option="visitChartOption"
style="height: 40px"
/>
<a-divider />
<div>日访问量 1,234</div>
</a-card>
</a-col>
<a-col
v-bind="styleResponsive ? { lg: 6, md: 12, sm: 24, xs: 24 } : { span: 6 }"
>
<a-card class="analysis-chart-card" :bordered="false">
<div class="ele-text-secondary ele-cell">
<div class="ele-cell-content">支付笔数</div>
<ele-tag color="blue"></ele-tag>
</div>
<h1 class="analysis-chart-card-num">6,560</h1>
<v-chart
ref="payNumChartRef"
:option="payNumChartOption"
style="height: 40px"
/>
<a-divider />
<div>转化率 60%</div>
</a-card>
</a-col>
<a-col
v-bind="styleResponsive ? { lg: 6, md: 12, sm: 24, xs: 24 } : { span: 6 }"
>
<a-card class="analysis-chart-card" :bordered="false">
<div class="ele-text-secondary ele-cell">
<div class="ele-cell-content">活动运营效果</div>
<ele-tag color="green"></ele-tag>
</div>
<h1 class="analysis-chart-card-num">78%</h1>
<div class="analysis-chart-card-content" style="padding-top: 16px">
<a-progress
:percent="78"
:show-info="false"
stroke-color="#13c2c2"
status="active"
/>
</div>
<a-divider />
<a-space size="middle">
<span class="analysis-trend-text">
周同比12% <caret-up-outlined class="ele-text-danger" />
</span>
<span class="analysis-trend-text">
日同比11% <caret-down-outlined class="ele-text-success" />
</span>
</a-space>
</a-card>
</a-col>
</a-row>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue/es';
import {
QuestionCircleOutlined,
CaretUpOutlined,
CaretDownOutlined
} from '@ant-design/icons-vue';
import { use } from 'echarts/core';
import type { EChartsCoreOption } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import { LineChart, BarChart } from 'echarts/charts';
import { GridComponent, TooltipComponent } from 'echarts/components';
import VChart from 'vue-echarts';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import { getPayNumList } from '@/api/dashboard/analysis';
import useEcharts from '@/utils/use-echarts';
use([CanvasRenderer, LineChart, BarChart, GridComponent, TooltipComponent]);
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
//
const visitChartRef = ref<InstanceType<typeof VChart> | null>(null);
const payNumChartRef = ref<InstanceType<typeof VChart> | null>(null);
useEcharts([visitChartRef, payNumChartRef]);
// 访问量折线图配置
const visitChartOption: EChartsCoreOption = reactive({});
// 支付笔数柱状图配置
const payNumChartOption: EChartsCoreOption = reactive({});
/* 获取支付笔数数据 */
const getPayNumData = () => {
getPayNumList()
.then((data) => {
Object.assign(visitChartOption, {
color: '#975fe5',
tooltip: {
trigger: 'axis',
formatter:
'<i class="ele-chart-dot" style="background: #975fe5;"></i>{b0}: {c0}'
},
grid: {
top: 10,
bottom: 0,
left: 0,
right: 0
},
xAxis: [
{
show: false,
type: 'category',
boundaryGap: false,
data: data.map((d) => d.date)
}
],
yAxis: [
{
show: false,
type: 'value',
splitLine: {
show: false
}
}
],
series: [
{
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: {
opacity: 0.5
},
data: data.map((d) => d.value)
}
]
});
Object.assign(payNumChartOption, {
tooltip: {
trigger: 'axis',
formatter:
'<i class="ele-chart-dot" style="background: #5b8ff9;"></i>{b0}: {c0}'
},
grid: {
top: 10,
bottom: 0,
left: 0,
right: 0
},
xAxis: [
{
show: false,
type: 'category',
data: data.map((d) => d.date)
}
],
yAxis: [
{
show: false,
type: 'value',
splitLine: {
show: false
}
}
],
series: [
{
type: 'bar',
data: data.map((d) => d.value)
}
]
});
})
.catch((e) => {
message.error(e.message);
});
};
getPayNumData();
</script>
<style lang="less" scoped>
.analysis-chart-card {
:deep(.ant-card-body) {
padding: 16px 22px 12px 22px;
}
:deep(.ant-divider) {
margin: 12px 0;
}
}
.analysis-chart-card-num {
font-size: 30px;
}
.analysis-chart-card-content {
height: 40px;
}
.analysis-trend-text {
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<a-card
:bordered="false"
title="最近1小时访问情况"
:body-style="{ padding: '16px 6px 0 0' }"
>
<v-chart
ref="visitHourChartRef"
:option="visitHourChartOption"
style="height: 362px"
/>
</a-card>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue/es';
import { use } from 'echarts/core';
import type { EChartsCoreOption } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import { LineChart } from 'echarts/charts';
import {
GridComponent,
TooltipComponent,
LegendComponent
} from 'echarts/components';
import VChart from 'vue-echarts';
import { getVisitHourList } from '@/api/dashboard/analysis';
import useEcharts from '@/utils/use-echarts';
use([
CanvasRenderer,
LineChart,
GridComponent,
TooltipComponent,
LegendComponent
]);
//
const visitHourChartRef = ref<InstanceType<typeof VChart> | null>(null);
useEcharts([visitHourChartRef]);
// 最近 1 小时访问情况折线图配置
const visitHourChartOption: EChartsCoreOption = reactive({});
/* 获取最近 1 小时访问情况数据 */
const getVisitHourData = () => {
getVisitHourList()
.then((data) => {
Object.assign(visitHourChartOption, {
tooltip: {
trigger: 'axis'
},
legend: {
data: ['浏览量', '访问量'],
right: 20
},
xAxis: [
{
type: 'category',
boundaryGap: false,
data: data.map((d) => d.time)
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
name: '浏览量',
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: {
opacity: 0.5
},
data: data.map((d) => d.views)
},
{
name: '访问量',
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: {
opacity: 0.5
},
data: data.map((d) => d.visits)
}
]
});
})
.catch((e) => {
message.error(e.message);
});
};
getVisitHourData();
</script>

View File

@@ -0,0 +1,41 @@
<template>
<div class="ele-body ele-body-card">
<statistics-card />
<sale-card />
<a-row :gutter="16">
<a-col
v-bind="
styleResponsive ? { lg: 16, md: 14, sm: 24, xs: 24 } : { span: 16 }
"
>
<visit-hour />
</a-col>
<a-col
v-bind="
styleResponsive ? { lg: 8, md: 10, sm: 24, xs: 24 } : { span: 8 }
"
>
<hot-search />
</a-col>
</a-row>
</div>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import StatisticsCard from './components/statistics-card.vue';
import SaleCard from './components/sale-card.vue';
import VisitHour from './components/visit-hour.vue';
import HotSearch from './components/hot-search.vue';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
</script>
<script lang="ts">
export default {
name: 'DashboardAnalysis'
};
</script>

View File

@@ -0,0 +1,69 @@
<template>
<a-card :bordered="false" title="浏览器分布" :body-style="{ padding: '0px' }">
<v-chart
ref="browserChartRef"
:option="browserChartOption"
style="height: 222px"
/>
</a-card>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue/es';
import { use } from 'echarts/core';
import type { EChartsCoreOption } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import { PieChart } from 'echarts/charts';
import { TooltipComponent, LegendComponent } from 'echarts/components';
import VChart from 'vue-echarts';
import { getBrowserCountList } from '@/api/dashboard/monitor';
import useEcharts from '@/utils/use-echarts';
use([CanvasRenderer, PieChart, TooltipComponent, LegendComponent]);
//
const browserChartRef = ref<InstanceType<typeof VChart> | null>(null);
useEcharts([browserChartRef]);
// 浏览器分布饼图配置
const browserChartOption: EChartsCoreOption = reactive({});
/* 获取用户浏览器分布数据 */
const getBrowserCountData = () => {
getBrowserCountList()
.then((data) => {
Object.assign(browserChartOption, {
tooltip: {
trigger: 'item',
confine: true,
borderWidth: 1
},
legend: {
bottom: 5,
itemWidth: 10,
itemHeight: 10,
icon: 'circle',
data: data.map((d) => d.name)
},
series: [
{
type: 'pie',
radius: ['45%', '70%'],
center: ['50%', '43%'],
label: {
show: false
},
data: data
}
]
});
})
.catch((e) => {
message.error(e.message);
});
};
getBrowserCountData();
</script>

View File

@@ -0,0 +1,147 @@
<template>
<a-card :bordered="false" title="用户分布">
<a-row>
<a-col v-bind="styleResponsive ? { sm: 18, xs: 24 } : { span: 18 }">
<v-chart
ref="userCountMapChartRef"
:option="userCountMapOption"
style="height: 469px"
/>
</a-col>
<a-col v-bind="styleResponsive ? { sm: 6, xs: 24 } : { span: 6 }">
<div
v-for="item in userCountDataRank"
:key="item.name"
class="monitor-user-count-item ele-cell"
>
<div>{{ item.name }}</div>
<div class="ele-cell-content">
<a-progress
status="normal"
:show-info="false"
:percent="item.percent"
/>
</div>
<div>{{ item.value }}</div>
</div>
</a-col>
</a-row>
</a-card>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue/es';
import { use, registerMap } from 'echarts/core';
import type { EChartsCoreOption } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import { MapChart } from 'echarts/charts';
import {
VisualMapComponent,
GeoComponent,
TooltipComponent
} from 'echarts/components';
import VChart from 'vue-echarts';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import { getChinaMapData, getUserCountList } from '@/api/dashboard/monitor';
import type { UserCount } from '@/api/dashboard/monitor/model';
import useEcharts from '@/utils/use-echarts';
use([
CanvasRenderer,
MapChart,
VisualMapComponent,
GeoComponent,
TooltipComponent
]);
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
//
const userCountMapChartRef = ref<InstanceType<typeof VChart> | null>(null);
useEcharts([userCountMapChartRef]);
// 用户分布前 10 名
const userCountDataRank = ref<UserCount[]>([]);
// 用户分布地图配置
const userCountMapOption: EChartsCoreOption = reactive({});
/* 获取中国地图数据并注册地图 */
const registerChinaMap = () => {
getChinaMapData()
.then((data) => {
registerMap('china', data);
getUserCountData();
})
.catch((e) => {
message.error(e.message);
});
};
/* 获取用户分布数据 */
const getUserCountData = () => {
getUserCountList()
.then((data) => {
const temp = data.sort((a, b) => b.value - a.value);
const min = temp[temp.length - 1].value || 0;
const max = temp[0].value || 1;
//
const list = temp.length > 10 ? temp.slice(0, 15) : temp;
userCountDataRank.value = list.map((d) => {
return {
name: d.name,
value: d.value,
percent: (d.value / max) * 100
};
});
//
Object.assign(userCountMapOption, {
tooltip: {
trigger: 'item',
borderWidth: 1
},
visualMap: {
min: min,
max: max,
text: ['高', '低'],
calculable: true
},
series: [
{
name: '用户数',
label: {
show: true
},
type: 'map',
map: 'china',
data: data
}
]
});
})
.catch((e) => {
message.error(e.message);
});
};
registerChinaMap();
</script>
<style lang="less" scoped>
.monitor-user-count-item {
margin-bottom: 8px;
:deep(.ant-progress-inner) {
background: none;
}
.ele-cell-content {
padding-right: 10px;
}
}
</style>

View File

@@ -0,0 +1,70 @@
<template>
<a-card :bordered="false" title="在线人数">
<div class="monitor-online-num-card">
<div>{{ currentTime }}</div>
<div class="monitor-online-num-title">
<ele-count-up :end-val="onlineNum" />
</div>
<div class="monitor-online-num-text">在线总人数</div>
<a-badge status="processing" :text="updateTimeText" />
</div>
</a-card>
</template>
<script lang="ts" setup>
import { ref, computed, onBeforeUnmount } from 'vue';
import { toDateString } from 'ele-admin-pro/es';
// 在线人数更新定时器
let onlineNumTimer: number | null = null;
// 在线总人数倒计时
const updateTime = ref(10);
// 当前时间
const currentTime = ref(toDateString(new Date(), 'HH:mm:ss'));
// 在线人数
const onlineNum = ref(228);
// 在线人数倒计时文字
const updateTimeText = computed(() => updateTime.value + ' 秒后更新');
/* 在线人数更新倒计时 */
const startUpdateOnlineNum = () => {
onlineNumTimer = window.setInterval(() => {
currentTime.value = toDateString(new Date(), 'HH:mm:ss');
if (updateTime.value === 1) {
updateTime.value = 10;
onlineNum.value = 100 + Math.round(Math.random() * 900);
} else {
updateTime.value--;
}
}, 1000);
};
onBeforeUnmount(() => {
// 销毁定时器
if (onlineNumTimer) {
clearInterval(onlineNumTimer);
onlineNumTimer = null;
}
});
startUpdateOnlineNum();
</script>
<style lang="less" scoped>
.monitor-online-num-card {
text-align: center;
}
.monitor-online-num-title {
line-height: 1;
font-size: 50px;
margin: 22px 0 14px;
}
.monitor-online-num-text {
margin-bottom: 22px;
}
</style>

View File

@@ -0,0 +1,166 @@
<!-- 统计卡片 -->
<template>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { lg: 6, md: 12, sm: 12, xs: 24 } : { span: 6 }"
>
<a-card :bordered="false" class="monitor-count-card">
<ele-tag color="blue" shape="circle" size="large">
<eye-filled />
</ele-tag>
<h1 class="monitor-count-card-num">21.2 k</h1>
<div class="monitor-count-card-text">总访问人数</div>
<ele-avatar-list :data="visitUsers" size="small" :max="4" />
</a-card>
</a-col>
<a-col
v-bind="styleResponsive ? { lg: 6, md: 12, sm: 12, xs: 24 } : { span: 6 }"
>
<a-card :bordered="false" class="monitor-count-card">
<ele-tag color="orange" shape="circle" size="large">
<fire-filled />
</ele-tag>
<h1 class="monitor-count-card-num">1.6 k</h1>
<div class="monitor-count-card-text">点击量近30天</div>
<div class="monitor-count-card-trend ele-text-success">
<up-outlined />
<span>110.5%</span>
</div>
<a-tooltip title="指标说明">
<question-circle-outlined class="monitor-count-card-tips" />
</a-tooltip>
</a-card>
</a-col>
<a-col
v-bind="styleResponsive ? { lg: 6, md: 12, sm: 12, xs: 24 } : { span: 6 }"
>
<a-card :bordered="false" class="monitor-count-card">
<ele-tag color="red" shape="circle" size="large">
<flag-filled />
</ele-tag>
<h1 class="monitor-count-card-num">826.0</h1>
<div class="monitor-count-card-text">到达量近30天</div>
<div class="monitor-count-card-trend ele-text-danger">
<down-outlined />
<span>15.5%</span>
</div>
</a-card>
</a-col>
<a-col
v-bind="styleResponsive ? { lg: 6, md: 12, sm: 12, xs: 24 } : { span: 6 }"
>
<a-card :bordered="false" class="monitor-count-card">
<ele-tag color="green" shape="circle" size="large">
<thunderbolt-filled />
</ele-tag>
<h1 class="monitor-count-card-num">28.8 %</h1>
<div class="monitor-count-card-text">转化率近30天</div>
<div class="monitor-count-card-trend ele-text-success">
<up-outlined />
<span>65.8%</span>
</div>
<a-tooltip title="指标说明">
<question-circle-outlined class="monitor-count-card-tips" />
</a-tooltip>
</a-card>
</a-col>
</a-row>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import {
QuestionCircleOutlined,
EyeFilled,
FireFilled,
FlagFilled,
ThunderboltFilled,
UpOutlined,
DownOutlined
} from '@ant-design/icons-vue';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
interface VisitUserType {
key: string | number;
name: string;
avatar: string;
}
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
// 访问人数
const visitUsers = ref<VisitUserType[]>([
{
key: 1,
name: 'SunSmile',
avatar:
'https://cdn.eleadmin.com/20200609/c184eef391ae48dba87e3057e70238fb.jpg'
},
{
key: 2,
name: '你的名字很好听',
avatar:
'https://cdn.eleadmin.com/20200609/b6a811873e704db49db994053a5019b2.jpg'
},
{
key: 3,
name: '全村人的希望',
avatar:
'https://cdn.eleadmin.com/20200609/948344a2a77c47a7a7b332fe12ff749a.jpg'
},
{
key: 4,
name: 'Jasmine',
avatar:
'https://cdn.eleadmin.com/20200609/f6bc05af944a4f738b54128717952107.jpg'
},
{
key: 5,
name: '酷酷的大叔',
avatar:
'https://cdn.eleadmin.com/20200609/2d98970a51b34b6b859339c96b240dcd.jpg'
},
{
key: 6,
name: '管理员',
avatar: 'https://cdn.eleadmin.com/20200610/avatar.jpg'
}
]);
</script>
<style lang="less" scoped>
.monitor-count-card {
text-align: center;
.monitor-count-card-num {
margin-top: 6px;
font-size: 32px;
}
.monitor-count-card-text {
font-size: 12px;
margin: 8px 0;
opacity: 0.8;
}
.monitor-count-card-trend {
font-weight: bold;
line-height: 26px;
& > .anticon {
margin-right: 6px;
}
}
.monitor-count-card-tips {
position: absolute;
top: 16px;
right: 16px;
cursor: pointer;
opacity: 0.6;
}
}
</style>

View File

@@ -0,0 +1,75 @@
<template>
<a-card
:bordered="false"
title="用户活跃度"
:body-style="{ padding: '56px 0' }"
>
<div class="ele-cell">
<div class="ele-cell-content ele-text-center">
<div class="monitor-progress-group">
<a-progress
type="circle"
:percent="70"
stroke-color="#52c41a"
:show-info="false"
:width="161"
/>
<a-progress
type="circle"
:percent="60"
stroke-color="#1890ff"
:show-info="false"
:width="121"
:stroke-width="5"
/>
<a-progress
type="circle"
:percent="35"
stroke-color="#f5222d"
:show-info="false"
:width="91"
:stroke-width="4"
/>
</div>
</div>
<div class="monitor-progress-legends">
<div class="ele-text-small ele-elip">
<a-badge color="green" text="活跃率: 70%" />
</div>
<div class="ele-text-small ele-elip">
<a-badge color="blue" text="留存率: 60%" />
</div>
<div class="ele-text-small ele-elip">
<a-badge color="red" text="跳出率: 35%" />
</div>
</div>
</div>
</a-card>
</template>
<style lang="less" scoped>
.monitor-progress-group {
position: relative;
display: inline-block;
.ant-progress:not(:first-child) {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin-top: -1px;
}
}
.monitor-progress-legends {
padding-right: 24px;
:deep(.ant-badge-status-text) {
font-size: 12px;
}
& > div + div {
margin-top: 8px;
}
}
</style>

View File

@@ -0,0 +1,86 @@
<template>
<a-card :bordered="false" title="用户评价">
<div class="ele-cell ele-cell-align-bottom">
<div style="font-size: 51px; line-height: 1">4.5</div>
<div class="ele-cell-content">
<a-rate :value="userRate" disabled />
<span style="color: #fadb14; margin-left: 8px">很棒</span>
</div>
</div>
<div class="ele-cell" style="margin: 18px 0">
<div style="font-size: 28px; line-height: 1" class="ele-text-placeholder">
-0%
</div>
<div class="ele-cell-content ele-text-small ele-text-secondary">
当前没有评价波动
</div>
</div>
<div class="ele-cell">
<div class="ele-cell-content">
<a-progress :percent="60" stroke-color="#52c41a" :show-info="false" />
</div>
<div class="monitor-evaluate-text">
<star-filled />
<span>5 : 368 </span>
</div>
</div>
<div class="ele-cell">
<div class="ele-cell-content">
<a-progress :percent="40" stroke-color="#1890ff" :show-info="false" />
</div>
<div class="monitor-evaluate-text">
<star-filled />
<span>4 : 256 </span>
</div>
</div>
<div class="ele-cell">
<div class="ele-cell-content">
<a-progress :percent="20" stroke-color="#faad14" :show-info="false" />
</div>
<div class="monitor-evaluate-text">
<star-filled />
<span>3 : 49 </span>
</div>
</div>
<div class="ele-cell">
<div class="ele-cell-content">
<a-progress :percent="10" stroke-color="#f5222d" :show-info="false" />
</div>
<div class="monitor-evaluate-text">
<star-filled />
<span>2 : 14 </span>
</div>
</div>
<div class="ele-cell">
<div class="ele-cell-content">
<a-progress :percent="0" :show-info="false" />
</div>
<div class="monitor-evaluate-text">
<star-filled />
<span>1 : 0 </span>
</div>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { StarFilled } from '@ant-design/icons-vue';
// 用户评分
const userRate = ref(4.5);
</script>
<style lang="less" scoped>
.monitor-evaluate-text {
width: 90px;
flex-shrink: 0;
white-space: nowrap;
opacity: 0.8;
& > .anticon {
font-size: 12px;
margin: 0 6px 0 8px;
}
}
</style>

View File

@@ -0,0 +1,79 @@
<template>
<a-card :bordered="false" title="用户满意度">
<div class="ele-cell ele-text-center">
<div class="ele-cell-content" style="font-size: 24px">856</div>
<div class="ele-cell-content">
<div class="monitor-face-smile"><i></i></div>
<div class="ele-text-secondary ele-elip" style="margin-top: 8px">
正面评论
</div>
</div>
<h2 class="ele-cell-content ele-text-success">82%</h2>
</div>
<a-divider style="margin: 26px 0" />
<div class="ele-cell ele-text-center">
<div class="ele-cell-content" style="font-size: 24px">60</div>
<div class="ele-cell-content">
<div class="monitor-face-cry"><i></i></div>
<div class="ele-text-secondary ele-elip" style="margin-top: 8px">
负面评论
</div>
</div>
<h2 class="ele-cell-content ele-text-danger">9%</h2>
</div>
</a-card>
</template>
<style lang="less" scoped>
.monitor-face-smile,
.monitor-face-cry {
width: 50px;
height: 50px;
display: inline-block;
background: #fbd971;
border-radius: 50%;
position: relative;
}
.monitor-face-smile > i,
.monitor-face-smile:before,
.monitor-face-smile:after,
.monitor-face-cry > i,
.monitor-face-cry:before,
.monitor-face-cry:after {
width: 28px;
height: 28px;
border-radius: 50%;
transform: rotate(225deg);
border: 3px solid #f0c419;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
position: absolute;
bottom: 8px;
left: 11px;
}
.monitor-face-smile:before,
.monitor-face-smile:after,
.monitor-face-cry:before,
.monitor-face-cry:after {
content: '';
width: 12px;
height: 12px;
left: 8px;
top: 14px;
border-color: #f29c1f;
transform: rotate(45deg);
}
.monitor-face-smile:after,
.monitor-face-cry:after {
left: auto;
right: 8px;
}
.monitor-face-cry > i {
transform: rotate(45deg);
bottom: -6px;
}
</style>

View File

@@ -0,0 +1,91 @@
<template>
<div class="ele-body ele-body-card">
<statistics-card />
<a-row :gutter="16">
<a-col
v-bind="
styleResponsive ? { lg: 18, md: 24, sm: 24, xs: 24 } : { span: 18 }
"
>
<map-card />
</a-col>
<a-col
v-bind="
styleResponsive ? { lg: 6, md: 24, sm: 24, xs: 24 } : { span: 6 }
"
>
<a-row :gutter="16">
<a-col
v-bind="
styleResponsive
? { lg: 24, md: 12, sm: 12, xs: 24 }
: { span: 24 }
"
>
<online-num />
</a-col>
<a-col
v-bind="
styleResponsive
? { lg: 24, md: 12, sm: 12, xs: 24 }
: { span: 24 }
"
>
<browser-card />
</a-col>
</a-row>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col
v-bind="
styleResponsive
? { xl: 12, lg: 24, md: 24, sm: 24, xs: 24 }
: { span: 12 }
"
>
<user-rate />
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 12, xs: 24 }
: { span: 6 }
"
>
<user-satisfaction />
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 12, xs: 24 }
: { span: 6 }
"
>
<user-liveness />
</a-col>
</a-row>
</div>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import StatisticsCard from './components/statistics-card.vue';
import MapCard from './components/map-card.vue';
import OnlineNum from './components/online-num.vue';
import BrowserCard from './components/browser-card.vue';
import UserRate from './components/user-rate.vue';
import UserSatisfaction from './components/user-satisfaction.vue';
import UserLiveness from './components/user-liveness.vue';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
</script>
<script lang="ts">
export default {
name: 'DashboardMonitor'
};
</script>

View File

@@ -0,0 +1,138 @@
<!-- 最新动态 -->
<template>
<a-card :title="title" :bordered="false" :body-style="{ padding: '6px 0' }">
<template #extra>
<more-icon @remove="onRemove" @edit="onEdit" />
</template>
<div
style="height: 346px; padding: 22px 20px 0 20px"
class="ele-scrollbar-hover"
>
<a-timeline>
<a-timeline-item
v-for="item in activities"
:key="item.id"
:color="item.color"
>
<em>{{ item.time }}</em>
<em>{{ item.title }}</em>
</a-timeline-item>
</a-timeline>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import MoreIcon from './more-icon.vue';
defineProps<{
title?: string;
}>();
const emit = defineEmits<{
(e: 'remove'): void;
(e: 'edit'): void;
}>();
interface Activitie {
id: number;
title: string;
time: string;
color?: string;
}
// 最新动态数据
const activities = ref<Activitie[]>([]);
/* 查询最新动态 */
const queryActivities = () => {
activities.value = [
{
id: 1,
title: 'SunSmile 解决了bug 登录提示操作失败',
time: '20:30',
color: 'gray'
},
{
id: 2,
title: 'Jasmine 解决了bug 按钮颜色与设计不符',
time: '19:30',
color: 'gray'
},
{
id: 3,
title: '项目经理 指派了任务 解决项目一的bug',
time: '18:30'
},
{
id: 4,
title: '项目经理 指派了任务 解决项目二的bug',
time: '17:30'
},
{
id: 5,
title: '项目经理 指派了任务 解决项目三的bug',
time: '16:30'
},
{
id: 6,
title: '项目经理 指派了任务 解决项目四的bug',
time: '15:30',
color: 'gray'
},
{
id: 7,
title: '项目经理 指派了任务 解决项目五的bug',
time: '14:30',
color: 'gray'
},
{
id: 8,
title: '项目经理 指派了任务 解决项目六的bug',
time: '12:30',
color: 'gray'
},
{
id: 9,
title: '项目经理 指派了任务 解决项目七的bug',
time: '11:30'
},
{
id: 10,
title: '项目经理 指派了任务 解决项目八的bug',
time: '10:30',
color: 'gray'
},
{
id: 11,
title: '项目经理 指派了任务 解决项目九的bug',
time: '09:30',
color: 'green'
},
{
id: 12,
title: '项目经理 指派了任务 解决项目十的bug',
time: '08:30',
color: 'red'
}
];
};
const onRemove = () => {
emit('remove');
};
const onEdit = () => {
emit('edit');
};
queryActivities();
</script>
<style lang="less" scoped>
.ele-scrollbar-hover
:deep(.ant-timeline-item-last > .ant-timeline-item-content) {
min-height: auto;
}
</style>

View File

@@ -0,0 +1,70 @@
<!-- 本月目标 -->
<template>
<a-card :title="title" :bordered="false">
<template #extra>
<more-icon @remove="onRemove" @edit="onEdit" />
</template>
<div class="workplace-goal-group">
<a-progress
:width="180"
:percent="80"
type="dashboard"
:stroke-width="4"
:show-info="false"
/>
<div class="workplace-goal-content">
<ele-tag color="blue" size="large" shape="circle">
<trophy-outlined />
</ele-tag>
<div class="workplace-goal-num">285</div>
</div>
<div class="workplace-goal-text">恭喜, 本月目标已达标!</div>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { TrophyOutlined } from '@ant-design/icons-vue';
import MoreIcon from './more-icon.vue';
defineProps<{
title?: string;
}>();
const emit = defineEmits<{
(e: 'remove'): void;
(e: 'edit'): void;
}>();
const onRemove = () => {
emit('remove');
};
const onEdit = () => {
emit('edit');
};
</script>
<style lang="less" scoped>
.workplace-goal-group {
height: 310px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
.workplace-goal-content {
position: absolute;
top: 50%;
left: 50%;
width: 180px;
margin: -50px 0 0 -90px;
text-align: center;
}
.workplace-goal-num {
font-size: 40px;
}
}
</style>

View File

@@ -0,0 +1,70 @@
<!-- 本月目标 -->
<template>
<a-card :title="title" :bordered="false">
<template #extra>
<more-icon @remove="onRemove" @edit="onEdit" />
</template>
<div class="workplace-goal-group">
<a-progress
:width="180"
:percent="80"
type="dashboard"
:stroke-width="4"
:show-info="false"
/>
<div class="workplace-goal-content">
<ele-tag color="blue" size="large" shape="circle">
<trophy-outlined />
</ele-tag>
<div class="workplace-goal-num">285</div>
</div>
<div class="workplace-goal-text">恭喜, 本月目标已达标!</div>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { TrophyOutlined } from '@ant-design/icons-vue';
import MoreIcon from './more-icon.vue';
defineProps<{
title?: string;
}>();
const emit = defineEmits<{
(e: 'remove'): void;
(e: 'edit'): void;
}>();
const onRemove = () => {
emit('remove');
};
const onEdit = () => {
emit('edit');
};
</script>
<style lang="less" scoped>
.workplace-goal-group {
height: 310px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
.workplace-goal-content {
position: absolute;
top: 50%;
left: 50%;
width: 180px;
margin: -50px 0 0 -90px;
text-align: center;
}
.workplace-goal-num {
font-size: 40px;
}
}
</style>

View File

@@ -0,0 +1,152 @@
<!-- 快捷方式 -->
<template>
<a-row :gutter="16" ref="wrapRef">
<a-col v-for="item in data" :key="item.url" :lg="3" :md="6" :sm="9" :xs="8">
<a-card :bordered="false" hoverable :body-style="{ padding: 0 }">
<router-link :to="item.url" class="app-link-block">
<component
:is="item.icon"
class="app-link-icon"
:style="{ color: item.color }"
/>
<div class="app-link-title">{{ item.title }}</div>
</router-link>
</a-card>
</a-col>
</a-row>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import SortableJs from 'sortablejs';
import type { Row as ARow } from 'ant-design-vue';
const CACHE_KEY = 'workplace-links';
interface LinkItem {
icon: string;
title: string;
url: string;
color?: string;
}
// 默认顺序
const DEFAULT: LinkItem[] = [
{
icon: 'ChromeOutlined',
title: '网址导航',
url: '/apps/link'
},
{
icon: 'LaptopOutlined',
title: '项目管理',
url: '/oa/project'
},
{
icon: 'ShoppingOutlined',
title: '商城系统',
url: '/shop/goods'
},
{
icon: 'FileSearchOutlined',
title: 'CMS内容管理系统',
url: '/cms/article'
},
{
icon: 'settingOutlined',
title: '系统设置',
url: '/system/user'
},
{
icon: 'AppstoreAddOutlined',
title: '扩展插件',
url: '/system/appstore'
},
{
icon: 'TeamOutlined',
title: '用户管理',
url: '/system/user'
},
{
icon: 'DesktopOutlined',
title: '网站首页',
url: '/apps/home/index'
}
];
// 获取缓存的顺序
const cache = (() => {
const str = localStorage.getItem(CACHE_KEY);
try {
return str ? JSON.parse(str) : null;
} catch (e) {
return null;
}
})();
const data = ref<LinkItem[]>([...(cache ?? DEFAULT)]);
const wrapRef = ref<InstanceType<typeof ARow> | null>(null);
let sortableIns: SortableJs | null = null;
/* 重置布局 */
const reset = () => {
data.value = [...DEFAULT];
cacheData();
};
/* 缓存布局 */
const cacheData = () => {
localStorage.setItem(CACHE_KEY, JSON.stringify(data.value));
};
onMounted(() => {
const isTouchDevice = 'ontouchstart' in document.documentElement;
if (isTouchDevice) {
return;
}
sortableIns = new SortableJs(wrapRef.value?.$el, {
animation: 300,
onUpdate: ({ oldIndex, newIndex }) => {
if (typeof oldIndex === 'number' && typeof newIndex === 'number') {
const temp = [...data.value];
temp.splice(newIndex, 0, temp.splice(oldIndex, 1)[0]);
data.value = temp;
cacheData();
}
},
setData: () => {}
});
});
onBeforeUnmount(() => {
if (sortableIns) {
sortableIns.destroy();
}
});
defineExpose({ reset });
</script>
<script lang="ts">
import * as icons from './link-icons';
export default {
components: icons
};
</script>
<style lang="less" scoped>
.app-link-block {
padding: 12px;
text-align: center;
display: block;
color: inherit;
.app-link-icon {
color: #666666;
font-size: 30px;
margin: 6px 0 10px 0;
}
}
</style>

View File

@@ -0,0 +1,11 @@
export {
UserOutlined,
TeamOutlined,
FileSearchOutlined,
ChromeOutlined,
ShoppingOutlined,
LaptopOutlined,
AppstoreAddOutlined,
DesktopOutlined,
SettingOutlined
} from '@ant-design/icons-vue';

View File

@@ -0,0 +1,38 @@
<template>
<a-dropdown placement="bottomRight">
<more-outlined class="ele-text-secondary" style="font-size: 18px" />
<template #overlay>
<a-menu :selectable="false" @click="onClick">
<a-menu-item key="edit">
<div class="ele-cell">
<edit-outlined />
<div class="ele-cell-content">编辑</div>
</div>
</a-menu-item>
<a-menu-item key="remove">
<div class="ele-cell ele-text-danger">
<delete-outlined />
<div class="ele-cell-content">删除</div>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<script lang="ts" setup>
import {
MoreOutlined,
EditOutlined,
DeleteOutlined
} from '@ant-design/icons-vue';
const emit = defineEmits<{
(e: 'edit'): void;
(e: 'remove'): void;
}>();
const onClick = ({ key }) => {
emit(key);
};
</script>

View File

@@ -0,0 +1,112 @@
<!-- 用户信息 -->
<template>
<a-card :bordered="false" :body-style="{ padding: '20px' }">
<div class="ele-cell workplace-user-card">
<div class="ele-cell-content ele-cell">
<a-avatar :size="68" :src="loginUser.avatar">
<template v-if="!loginUser.avatar" #icon>
<user-outlined />
</template>
</a-avatar>
<div class="ele-cell-content">
<h4 class="ele-elip">
早安, {{ loginUser.nickname }}, 开始您一天的工作吧!
</h4>
<div class="ele-elip ele-text-secondary">
<cloud-outlined />
<em>今日多云转阴18 - 22出门记得穿外套哦~</em>
</div>
</div>
</div>
<div class="workplace-count-group">
<!-- <div class="workplace-count-item">-->
<!-- <div class="workplace-count-header">-->
<!-- <ele-tag color="blue" shape="circle" size="small">-->
<!-- <appstore-filled />-->
<!-- </ele-tag>-->
<!-- <span class="workplace-count-name">项目数</span>-->
<!-- </div>-->
<!-- <h2>0</h2>-->
<!-- </div>-->
<!-- <div class="workplace-count-item">-->
<!-- <div class="workplace-count-header">-->
<!-- <ele-tag color="orange" shape="circle" size="small">-->
<!-- <check-square-outlined />-->
<!-- </ele-tag>-->
<!-- <span class="workplace-count-name">待办项</span>-->
<!-- </div>-->
<!-- <h2>6 / 24</h2>-->
<!-- </div>-->
<!-- <div class="workplace-count-item">-->
<!-- <div class="workplace-count-header">-->
<!-- <ele-tag color="green" shape="circle" size="small">-->
<!-- <bell-filled />-->
<!-- </ele-tag>-->
<!-- <span class="workplace-count-name">消息</span>-->
<!-- </div>-->
<!-- <h2>0</h2>-->
<!-- </div>-->
</div>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import {
UserOutlined,
CloudOutlined,
AppstoreFilled,
CheckSquareOutlined,
BellFilled
} from '@ant-design/icons-vue';
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
// 当前登录用户信息
const loginUser = computed(() => userStore.info ?? {});
</script>
<style lang="less" scoped>
.workplace-user-card {
.ele-cell-content {
overflow: hidden;
}
h4 {
margin-bottom: 6px;
}
}
.workplace-count-group {
white-space: nowrap;
text-align: right;
flex-shrink: 0;
}
.workplace-count-item {
display: inline-block;
margin: 0 4px 0 24px;
}
.workplace-count-name {
margin-left: 8px;
}
@media screen and (max-width: 992px) {
.workplace-count-item {
margin: 0 2px 0 12px;
}
}
@media screen and (max-width: 768px) {
.workplace-user-card {
display: block;
}
.workplace-count-group {
margin-top: 8px;
}
}
</style>

View File

@@ -0,0 +1,120 @@
<!-- 项目进度 -->
<template>
<a-card
:title="title"
:bordered="false"
:body-style="{ padding: '14px', height: '358px' }"
>
<template #extra>
<more-icon @remove="onRemove" @edit="onEdit" />
</template>
<a-table
row-key="id"
size="middle"
:pagination="false"
:data-source="projectList"
:columns="projectColumns"
:scroll="{ x: 600 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'projectName'">
<a @click="openUrl('/project?id=' + record.projectId)">{{ record.projectName }}</a>
</template>
<template v-else-if="column.key === 'status'">
<div v-for="(dict, index) in projectStatus" :key="index">
<a-tag :color="dict.comments" v-if="dict.value === record.status">{{
dict.label
}}</a-tag>
</div>
</template>
<template v-else-if="column.key === 'progress'">
<a-progress :percent="record.progress * 2" :steps="5" />
</template>
</template>
</a-table>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import MoreIcon from './more-icon.vue';
import type { ColumnsType } from 'ant-design-vue/es/table';
import { pageProject } from '@/api/oa/project';
import { message, SelectProps } from 'ant-design-vue';
import { listDictionaryData } from '@/api/system/dictionary-data';
import {getDictionaryOptions, openUrl} from '@/utils/common';
import type { Project } from '@/api/oa/project/model';
defineProps<{
title?: string;
}>();
const emit = defineEmits<{
(e: 'remove'): void;
(e: 'edit'): void;
}>();
const projectColumns = ref<ColumnsType>([
{
key: 'index',
align: 'center',
width: 38,
customRender: ({ index }) => index + 1,
fixed: 'left'
},
{
title: '项目名称',
key: 'projectName',
ellipsis: true,
minWidth: 120
},
{
title: '开始时间',
dataIndex: 'createTime',
align: 'center',
minWidth: 100,
ellipsis: true
},
{
title: '结束时间',
dataIndex: 'updateTime',
align: 'center',
minWidth: 100,
ellipsis: true
},
{
title: '状态',
key: 'status',
align: 'center',
width: 90
},
{
title: '进度',
key: 'progress',
align: 'center',
width: 180
}
]);
// 项目进度数据
const projectList = ref<Project[]>([]);
/* 获取字典数据 */
const projectStatus = getDictionaryOptions('projectStatus');
/* 查询项目进度 */
const queryProjectList = () => {
pageProject({ limit: 5, status: '1' }).then((data) => {
projectList.value = data.list;
});
};
const onRemove = () => {
emit('remove');
};
const onEdit = () => {
emit('edit');
};
queryProjectList();
</script>

View File

@@ -0,0 +1,157 @@
<!-- 我的任务 -->
<template>
<a-card
:title="title"
:bordered="false"
:body-style="{ padding: '10px', height: '358px' }"
class="workplace-table-card"
>
<template #extra>
<more-icon @remove="onRemove" @edit="onEdit" />
</template>
<div style="overflow: auto; position: relative">
<table class="ele-table" style="table-layout: fixed; min-width: 300px">
<colgroup>
<col width="38" />
<col width="65" />
<col />
<col width="70" />
</colgroup>
<thead>
<tr>
<th style="position: sticky; left: 0"></th>
<th style="text-align: center">优先级</th>
<th>任务名称</th>
<th style="text-align: center">状态</th>
</tr>
</thead>
<vue-draggable
tag="tbody"
item-key="id"
v-model="taskList"
handle=".sort-handle"
:animation="300"
:set-data="() => void 0"
>
<template #item="{ element }">
<tr>
<td style="text-align: center; position: sticky; left: 0">
<menu-outlined class="sort-handle ele-text-secondary" />
</td>
<td style="text-align: center">
<ele-tag
:color="['red', 'orange', 'blue'][element.priority - 1]"
shape="circle"
>
{{ element.priority }}
</ele-tag>
</td>
<td class="ele-elip" :title="element.taskName">
<a>{{ element.taskName }}</a>
</td>
<td style="text-align: center">
<span v-if="element.status === 0" class="ele-text-warning">
未开始
</span>
<span v-else-if="element.status === 1" class="ele-text-success">
进行中
</span>
<span v-else-if="element.status === 2" class="ele-text-info">
已完成
</span>
</td>
</tr>
</template>
</vue-draggable>
</table>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import VueDraggable from 'vuedraggable';
import { MenuOutlined } from '@ant-design/icons-vue';
import MoreIcon from './more-icon.vue';
defineProps<{
title?: string;
}>();
const emit = defineEmits<{
(e: 'remove'): void;
(e: 'edit'): void;
}>();
interface Task {
id: number;
priority: number;
taskName: string;
status: number;
}
// 我的任务数据
const taskList = ref<Task[]>([]);
/* 查询我的任务 */
const queryTaskList = () => {
taskList.value = [
{
id: 1,
priority: 1,
taskName: '解决项目一的bug',
status: 0
},
{
id: 2,
priority: 2,
taskName: '解决项目二的bug',
status: 0
},
{
id: 3,
priority: 2,
taskName: '解决项目三的bug',
status: 1
},
{
id: 4,
priority: 3,
taskName: '解决项目四的bug',
status: 1
},
{
id: 5,
priority: 3,
taskName: '解决项目五的bug',
status: 2
},
{
id: 6,
priority: 3,
taskName: '解决项目六的bug',
status: 2
}
];
};
const onRemove = () => {
emit('remove');
};
const onEdit = () => {
emit('edit');
};
queryTaskList();
</script>
<style lang="less" scoped>
.ele-table tr.sortable-chosen {
background: hsla(0, 0%, 60%, 0.1);
}
.workplace-table-card .sort-handle {
cursor: move;
}
</style>

View File

@@ -0,0 +1,84 @@
<!-- 小组成员 -->
<template>
<a-card :title="title" :bordered="false" :body-style="{ padding: '2px 0px' }">
<template #extra>
<more-icon @remove="onRemove" @edit="onEdit" />
</template>
<div
v-for="(item, index) in userList"
:key="index"
class="ele-cell user-list-item"
>
<div style="flex-shrink: 0">
<a-avatar :size="46" :src="item.avatar" />
</div>
<div class="ele-cell-content">
<span class="ele-cell-title ele-elip">{{ item.nickname }}</span>
<div class="ele-cell-desc ele-elip">{{ item.phone }}</div>
</div>
<div style="flex-shrink: 0">
<a-tag :color="['green', 'red'][item.status]">
{{ ['在线', '离线'][item.status] }}
</a-tag>
</div>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import MoreIcon from './more-icon.vue';
import { pageUsers } from '@/api/system/user';
import type { User } from '@/api/system/user/model';
defineProps<{
title?: string;
}>();
const emit = defineEmits<{
(e: 'remove'): void;
(e: 'edit'): void;
}>();
// 小组成员数据
const userList = ref<User[]>([]);
/* 查询小组成员 */
const queryUserList = () => {
pageUsers({ parentId: 11, limit: 5 }).then((data: any) => {
userList.value = data.list;
});
};
const onRemove = () => {
emit('remove');
};
const onEdit = () => {
emit('edit');
};
queryUserList();
</script>
<style lang="less" scoped>
.user-list-item {
padding: 12px 18px;
& + .user-list-item {
border-top: 1px solid hsla(0, 0%, 60%, 0.15);
}
.ele-cell-content {
overflow: hidden;
}
.ele-cell-desc {
margin-top: 0;
}
.ant-tag {
margin: 0;
}
}
</style>

View File

@@ -0,0 +1,302 @@
<template>
<div class="ele-body ele-body-card">
<profile-card />
<link-card ref="linkCardRef" />
<a-row :gutter="16" ref="wrapRef">
<a-col
v-for="(item, index) in data"
:key="item.name"
:lg="item.lg"
:md="item.md"
:sm="item.sm"
:xs="item.xs"
>
<component
:is="item.name"
:title="item.title"
@remove="onRemove(index)"
@edit="onEdit(index)"
/>
</a-col>
</a-row>
<a-card :bordered="false" :body-style="{ padding: 0 }">
<div class="ele-cell" style="line-height: 42px">
<div
class="ele-cell-content ele-text-primary workplace-bottom-btn"
@click="add"
>
<PlusCircleOutlined /> 添加视图
</div>
<a-divider type="vertical" />
<div
class="ele-cell-content ele-text-primary workplace-bottom-btn"
@click="reset"
>
<UndoOutlined /> 重置布局
</div>
</div>
</a-card>
<ele-modal
:width="680"
v-model:visible="visible"
title="未添加的视图"
:footer="null"
>
<a-row :gutter="16">
<a-col
v-for="item in notAddedData"
:key="item.name"
:md="8"
:sm="12"
:xs="24"
>
<div
class="workplace-card-item ele-border-split"
@click="addView(item)"
>
<div class="workplace-card-header ele-border-split">
{{ item.title }}
</div>
<div class="workplace-card-body ele-text-placeholder">
<plus-circle-outlined />
</div>
</div>
</a-col>
</a-row>
<a-empty v-if="!notAddedData.length" description="已添加所有视图" />
</ele-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import SortableJs from 'sortablejs';
import type { Row as ARow } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { PlusCircleOutlined, UndoOutlined } from '@ant-design/icons-vue';
import ProfileCard from './components/profile-card.vue';
import LinkCard from './components/link-card.vue';
const CACHE_KEY = 'workplace-layout';
interface ViewItem {
name: string;
title: string;
lg: number;
md: number;
sm: number;
xs: number;
}
// 默认布局
const DEFAULT: ViewItem[] = [
// {
// name: 'project-card',
// title: '项目管理',
// lg: 16,
// md: 24,
// sm: 24,
// xs: 24
// },
// {
// name: 'user-list',
// title: '小组成员',
// lg: 8,
// md: 24,
// sm: 24,
// xs: 24
// },
// {
// name: 'activities-card',
// title: '最新动态',
// lg: 8,
// md: 24,
// sm: 24,
// xs: 24
// },
// {
// name: 'task-card',
// title: '我的任务',
// lg: 8,
// md: 24,
// sm: 24,
// xs: 24
// },
// {
// name: 'goal-card',
// title: '本月目标',
// lg: 8,
// md: 24,
// sm: 24,
// xs: 24
// },
// {
// name: 'docs',
// title: '知识库',
// lg: 8,
// md: 24,
// sm: 24,
// xs: 24
// }
];
// 获取缓存的顺序
const cache = (() => {
const str = localStorage.getItem(CACHE_KEY);
try {
return str ? JSON.parse(str) : null;
} catch (e) {
return null;
}
})();
const data = ref<ViewItem[]>([...(cache ?? DEFAULT)]);
const visible = ref(false);
const linkCardRef = ref<InstanceType<typeof LinkCard> | null>(null);
const wrapRef = ref<InstanceType<typeof ARow> | null>(null);
let sortableIns: SortableJs | null = null;
// 未添加的数据
const notAddedData = computed(() => {
return DEFAULT.filter((d) => !data.value.some((t) => t.name === d.name));
});
/* 添加 */
const add = () => {
visible.value = true;
};
/* 重置布局 */
const reset = () => {
data.value = [...DEFAULT];
cacheData();
linkCardRef.value?.reset();
message.success('已重置');
};
/* 缓存布局 */
const cacheData = () => {
localStorage.setItem(CACHE_KEY, JSON.stringify(data.value));
};
/* 删除视图 */
const onRemove = (index: number) => {
data.value = data.value.filter((_d, i) => i !== index);
cacheData();
};
/* 编辑视图 */
const onEdit = (index: number) => {
data.value.map((d) => {
if (d.name == 'user-list') {
}
});
// message.info('点击了编辑');
};
/* 添加视图 */
const addView = (item) => {
data.value.push(item);
cacheData();
message.success('已添加');
};
onMounted(() => {
const isTouchDevice = 'ontouchstart' in document.documentElement;
if (isTouchDevice) {
return;
}
sortableIns = new SortableJs(wrapRef.value?.$el, {
handle: '.ant-card-head',
animation: 300,
onUpdate: ({ oldIndex, newIndex }) => {
if (typeof oldIndex === 'number' && typeof newIndex === 'number') {
const temp = [...data.value];
temp.splice(newIndex, 0, temp.splice(oldIndex, 1)[0]);
data.value = temp;
cacheData();
}
},
setData: () => {}
});
});
onBeforeUnmount(() => {
if (sortableIns) {
sortableIns.destroy();
}
});
</script>
<script lang="ts">
import ActivitiesCard from './components/activities-card.vue';
import TaskCard from './components/task-card.vue';
import GoalCard from './components/goal-card.vue';
import ProjectCard from './components/project-card.vue';
import UserList from './components/user-list.vue';
import Docs from './components/docs.vue';
export default {
name: 'DashboardWorkplace',
components: {
ProjectCard,
UserList,
ActivitiesCard,
TaskCard,
GoalCard,
Docs
}
};
</script>
<style lang="less" scoped>
.ele-body :deep(.ant-card-head) {
cursor: move;
position: relative;
}
.ele-body :deep(.ant-row > .ant-col.sortable-chosen > .ant-card) {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.2);
}
.workplace-bottom-btn {
text-align: center;
cursor: pointer;
transition: background-color 0.2s;
}
.workplace-bottom-btn:hover {
background: hsla(0, 0%, 60%, 0.05);
}
/* 添加弹窗 */
.workplace-card-item {
margin-bottom: 15px;
border-width: 1px;
border-style: solid;
border-radius: 4px;
position: relative;
cursor: pointer;
transition: box-shadow 0.2s, background-color 0.2s;
}
.workplace-card-item:hover {
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.1);
background: hsla(0, 0%, 60%, 0.05);
}
.workplace-card-item .workplace-card-header {
border-bottom-width: 1px;
border-bottom-style: solid;
padding: 8px;
}
.workplace-card-body {
font-size: 26px;
padding: 24px 10px;
text-align: center;
}
</style>