36 changed files with 1518 additions and 191 deletions
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,2 @@ |
|||
// 对vue-count-to进行改造成支持vue3版本
|
|||
export { default as CountTo } from './src/index.vue'; |
|||
@ -0,0 +1,159 @@ |
|||
<template> |
|||
<span> |
|||
{{ displayValue }} |
|||
</span> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent, reactive, computed, watch, onMounted, unref, toRef } from 'vue'; |
|||
|
|||
import { countToProps } from './props'; |
|||
import { useRaf } from '/@/hooks/event/useRaf'; |
|||
import { isNumber } from '/@/utils/is'; |
|||
export default defineComponent({ |
|||
name: 'CountTo', |
|||
props: countToProps, |
|||
emits: ['mounted', 'callback'], |
|||
setup(props, { emit }) { |
|||
const { requestAnimationFrame, cancelAnimationFrame } = useRaf(); |
|||
|
|||
const state = reactive<{ |
|||
localStartVal: number; |
|||
printVal: number | null; |
|||
displayValue: string; |
|||
paused: boolean; |
|||
localDuration: number | null; |
|||
startTime: number | null; |
|||
timestamp: number | null; |
|||
rAF: any; |
|||
remaining: number | null; |
|||
}>({ |
|||
localStartVal: props.startVal, |
|||
displayValue: formatNumber(props.startVal), |
|||
printVal: null, |
|||
paused: false, |
|||
localDuration: props.duration, |
|||
startTime: null, |
|||
timestamp: null, |
|||
remaining: null, |
|||
rAF: null, |
|||
}); |
|||
onMounted(() => { |
|||
if (props.autoplay) { |
|||
start(); |
|||
} |
|||
emit('mounted'); |
|||
}); |
|||
const getCountDown = computed(() => { |
|||
return props.startVal > props.endVal; |
|||
}); |
|||
|
|||
watch([() => props.startVal, () => props.endVal], () => { |
|||
if (props.autoplay) { |
|||
start(); |
|||
} |
|||
}); |
|||
|
|||
function start() { |
|||
const { startVal, duration } = props; |
|||
state.localStartVal = startVal; |
|||
state.startTime = null; |
|||
state.localDuration = duration; |
|||
state.paused = false; |
|||
state.rAF = requestAnimationFrame(count); |
|||
} |
|||
function pauseResume() { |
|||
if (state.paused) { |
|||
resume(); |
|||
state.paused = false; |
|||
} else { |
|||
pause(); |
|||
state.paused = true; |
|||
} |
|||
} |
|||
function pause() { |
|||
cancelAnimationFrame(state.rAF); |
|||
} |
|||
|
|||
function resume() { |
|||
state.startTime = null; |
|||
state.localDuration = +(state.remaining as number); |
|||
state.localStartVal = +(state.printVal as number); |
|||
requestAnimationFrame(count); |
|||
} |
|||
|
|||
function reset() { |
|||
state.startTime = null; |
|||
cancelAnimationFrame(state.rAF); |
|||
state.displayValue = formatNumber(props.startVal); |
|||
} |
|||
|
|||
function count(timestamp: number) { |
|||
const { useEasing, easingFn, endVal } = props; |
|||
if (!state.startTime) state.startTime = timestamp; |
|||
state.timestamp = timestamp; |
|||
const progress = timestamp - state.startTime; |
|||
state.remaining = (state.localDuration as number) - progress; |
|||
if (useEasing) { |
|||
if (unref(getCountDown)) { |
|||
state.printVal = |
|||
state.localStartVal - |
|||
easingFn(progress, 0, state.localStartVal - endVal, state.localDuration as number); |
|||
} else { |
|||
state.printVal = easingFn( |
|||
progress, |
|||
state.localStartVal, |
|||
endVal - state.localStartVal, |
|||
state.localDuration as number |
|||
); |
|||
} |
|||
} else { |
|||
if (unref(getCountDown)) { |
|||
state.printVal = |
|||
state.localStartVal - |
|||
(state.localStartVal - endVal) * (progress / (state.localDuration as number)); |
|||
} else { |
|||
state.printVal = |
|||
state.localStartVal + |
|||
(endVal - state.localStartVal) * (progress / (state.localDuration as number)); |
|||
} |
|||
} |
|||
if (unref(getCountDown)) { |
|||
state.printVal = state.printVal < endVal ? endVal : state.printVal; |
|||
} else { |
|||
state.printVal = state.printVal > endVal ? endVal : state.printVal; |
|||
} |
|||
state.displayValue = formatNumber(state.printVal); |
|||
if (progress < (state.localDuration as number)) { |
|||
state.rAF = requestAnimationFrame(count); |
|||
} else { |
|||
emit('callback'); |
|||
} |
|||
} |
|||
|
|||
function formatNumber(num: number | string) { |
|||
const { decimals, decimal, separator, suffix, prefix } = props; |
|||
num = Number(num).toFixed(decimals); |
|||
num += ''; |
|||
const x = num.split('.'); |
|||
let x1 = x[0]; |
|||
const x2 = x.length > 1 ? decimal + x[1] : ''; |
|||
const rgx = /(\d+)(\d{3})/; |
|||
if (separator && !isNumber(separator)) { |
|||
while (rgx.test(x1)) { |
|||
x1 = x1.replace(rgx, '$1' + separator + '$2'); |
|||
} |
|||
} |
|||
return prefix + x1 + x2 + suffix; |
|||
} |
|||
|
|||
return { |
|||
count, |
|||
reset, |
|||
resume, |
|||
start, |
|||
pauseResume, |
|||
displayValue: toRef(state, 'displayValue'), |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -0,0 +1,62 @@ |
|||
import { PropType } from 'vue'; |
|||
export const countToProps = { |
|||
startVal: { |
|||
type: Number as PropType<number>, |
|||
required: false, |
|||
default: 0, |
|||
}, |
|||
endVal: { |
|||
type: Number as PropType<number>, |
|||
required: false, |
|||
default: 2017, |
|||
}, |
|||
duration: { |
|||
type: Number as PropType<number>, |
|||
required: false, |
|||
default: 3000, |
|||
}, |
|||
autoplay: { |
|||
type: Boolean as PropType<boolean>, |
|||
required: false, |
|||
default: true, |
|||
}, |
|||
decimals: { |
|||
type: Number as PropType<number>, |
|||
required: false, |
|||
default: 0, |
|||
validator(value: number) { |
|||
return value >= 0; |
|||
}, |
|||
}, |
|||
decimal: { |
|||
type: String as PropType<string>, |
|||
required: false, |
|||
default: '.', |
|||
}, |
|||
separator: { |
|||
type: String as PropType<string>, |
|||
required: false, |
|||
default: ',', |
|||
}, |
|||
prefix: { |
|||
type: String as PropType<string>, |
|||
required: false, |
|||
default: '', |
|||
}, |
|||
suffix: { |
|||
type: String as PropType<string>, |
|||
required: false, |
|||
default: '', |
|||
}, |
|||
useEasing: { |
|||
type: Boolean as PropType<boolean>, |
|||
required: false, |
|||
default: true, |
|||
}, |
|||
easingFn: { |
|||
type: Function as PropType<(t: number, b: number, c: number, d: number) => number>, |
|||
default(t: number, b: number, c: number, d: number) { |
|||
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b; |
|||
}, |
|||
}, |
|||
}; |
|||
@ -0,0 +1,27 @@ |
|||
// zoom-out |
|||
.zoom-out-enter-active, |
|||
.zoom-out-leave-active { |
|||
transition: opacity 0.35s ease-in-out, transform 0.45s ease-out; |
|||
} |
|||
|
|||
.zoom-out-enter-from, |
|||
.zoom-out-leave-to { |
|||
opacity: 0; |
|||
transform: scale(0); |
|||
} |
|||
|
|||
// zoom-fade |
|||
.zoom-fade-enter-active, |
|||
.zoom-fade-leave-active { |
|||
transition: transform 0.35s, opacity 0.35s ease-out; |
|||
} |
|||
|
|||
.zoom-fade-enter-from { |
|||
opacity: 0; |
|||
transform: scale(0.97); |
|||
} |
|||
|
|||
.zoom-fade-leave-to { |
|||
opacity: 0; |
|||
transform: scale(1.03); |
|||
} |
|||
@ -0,0 +1,91 @@ |
|||
<template> |
|||
<div ref="chartRef" :style="{ height, width }" /> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent, onMounted, ref, Ref } from 'vue'; |
|||
|
|||
import { useECharts } from '/@/hooks/web/useECharts'; |
|||
|
|||
import { basicProps } from './props'; |
|||
export default defineComponent({ |
|||
name: 'AnalysisLine', |
|||
props: basicProps, |
|||
setup() { |
|||
const chartRef = ref<HTMLDivElement | null>(null); |
|||
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>); |
|||
|
|||
onMounted(() => { |
|||
setOptions({ |
|||
tooltip: { |
|||
trigger: 'axis', |
|||
backgroundColor: 'rgba(0, 0, 0, .6)', |
|||
axisPointer: { |
|||
// 坐标轴指示器,坐标轴触发有效 |
|||
type: 'shadow', // 默认为直线,可选为:'line' | 'shadow' |
|||
}, |
|||
}, |
|||
legend: { |
|||
itemWidth: 15, |
|||
right: 10, |
|||
data: ['产品一', '产品二', '产品三'], |
|||
}, |
|||
grid: { |
|||
left: '3%', |
|||
right: '4%', |
|||
bottom: '3%', |
|||
containLabel: true, |
|||
}, |
|||
xAxis: [ |
|||
{ |
|||
type: 'category', |
|||
axisTick: { |
|||
inside: true, // 刻度朝内 |
|||
}, |
|||
data: ['付费用户', '免费用户', '自主'], |
|||
}, |
|||
], |
|||
yAxis: [ |
|||
{ |
|||
type: 'value', |
|||
axisTick: { |
|||
inside: true, // 刻度朝内 |
|||
}, |
|||
}, |
|||
], |
|||
series: [ |
|||
{ |
|||
name: '产品一', |
|||
type: 'bar', |
|||
itemStyle: { |
|||
color: '#3ca0f6', |
|||
}, |
|||
data: [3200, 3320, 3010], |
|||
animationDuration: 4000, |
|||
}, |
|||
{ |
|||
name: '产品二', |
|||
type: 'bar', |
|||
itemStyle: { |
|||
color: '#7dd9b9', |
|||
}, |
|||
data: [1200, 2600, 1010], |
|||
animationDuration: 4000, |
|||
}, |
|||
|
|||
{ |
|||
name: '产品三', |
|||
type: 'bar', |
|||
itemStyle: { |
|||
color: '#e6a23c', |
|||
}, |
|||
data: [862, 2500, 964], |
|||
animationDuration: 4000, |
|||
}, |
|||
], |
|||
}); |
|||
}); |
|||
|
|||
return { chartRef }; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -0,0 +1,101 @@ |
|||
<template> |
|||
<div ref="chartRef" :style="{ height, width }" /> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent, onMounted, ref, Ref } from 'vue'; |
|||
|
|||
import { useECharts } from '/@/hooks/web/useECharts'; |
|||
|
|||
import { basicProps } from './props'; |
|||
export default defineComponent({ |
|||
name: 'AnalysisLine', |
|||
props: basicProps, |
|||
setup() { |
|||
const chartRef = ref<HTMLDivElement | null>(null); |
|||
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>); |
|||
|
|||
onMounted(() => { |
|||
setOptions({ |
|||
// title: { |
|||
// text: '产品成交额', |
|||
// }, |
|||
tooltip: { |
|||
trigger: 'axis', |
|||
padding: 3, |
|||
backgroundColor: 'rgba(0, 0, 0, .6)', |
|||
borderColor: '#777', |
|||
borderWidth: 1, |
|||
}, |
|||
legend: { |
|||
icon: 'rect', |
|||
itemWidth: 15, |
|||
itemHeight: 4, |
|||
left: 80, |
|||
top: 0, |
|||
orient: 'horizontal', |
|||
data: ['产品一', '产品二'], |
|||
}, |
|||
grid: { |
|||
left: '3%', |
|||
right: '4%', |
|||
bottom: '3%', |
|||
containLabel: true, |
|||
}, |
|||
xAxis: { |
|||
type: 'category', |
|||
boundaryGap: false, |
|||
axisTick: { |
|||
inside: true, // 刻度朝内 |
|||
}, |
|||
data: [ |
|||
'一月', |
|||
'二月', |
|||
'三月', |
|||
'四月', |
|||
'五月', |
|||
'六月', |
|||
'七月', |
|||
'八月', |
|||
'九月', |
|||
'十月', |
|||
'十一月', |
|||
'十二月', |
|||
], |
|||
}, |
|||
yAxis: { |
|||
type: 'value', |
|||
axisTick: { |
|||
inside: true, // 刻度朝内 |
|||
}, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: '产品一', |
|||
type: 'line', |
|||
itemStyle: { |
|||
normal: { |
|||
color: '#5B8FF9', |
|||
}, |
|||
}, |
|||
// areaStyle: {}, |
|||
data: [330, 132, 101, 134, 90, 230, 210, 150, 232, 234, 230, 400], |
|||
animationDuration: 4000, |
|||
}, |
|||
{ |
|||
name: '产品二', |
|||
type: 'line', |
|||
itemStyle: { |
|||
normal: { |
|||
color: '#55D187', |
|||
}, |
|||
}, |
|||
data: [220, 182, 191, 234, 290, 330, 310, 330, 232, 201, 330, 190], |
|||
animationDuration: 4000, |
|||
}, |
|||
], |
|||
}); |
|||
}); |
|||
return { chartRef }; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -0,0 +1,79 @@ |
|||
<template> |
|||
<div ref="chartRef" :style="{ height, width }" /> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent, onMounted, ref, Ref } from 'vue'; |
|||
|
|||
import { useECharts } from '/@/hooks/web/useECharts'; |
|||
|
|||
import { basicProps } from './props'; |
|||
|
|||
const m2R2Data = [ |
|||
{ value: 335, name: '移动设备', itemStyle: { color: '#1b65b9' } }, |
|||
{ value: 310, name: '网页端', itemStyle: { color: '#3ca0f6' } }, |
|||
{ value: 234, name: '手表', itemStyle: { color: '#2dc0c0' } }, |
|||
{ value: 234, name: '其他', itemStyle: { color: '#7dd9b9' } }, |
|||
]; |
|||
export default defineComponent({ |
|||
name: 'AnalysisLine', |
|||
props: basicProps, |
|||
setup() { |
|||
const chartRef = ref<HTMLDivElement | null>(null); |
|||
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>); |
|||
|
|||
onMounted(() => { |
|||
setOptions({ |
|||
title: [ |
|||
{ |
|||
text: '总设备', |
|||
subtext: '1,430', |
|||
textStyle: { |
|||
fontSize: 12, |
|||
color: '#4B535E85', |
|||
}, |
|||
subtextStyle: { |
|||
fontSize: 24, |
|||
color: 'black', |
|||
}, |
|||
textAlign: 'center', |
|||
// @ts-ignore |
|||
x: '34.5%', |
|||
y: '40%', |
|||
}, |
|||
], |
|||
tooltip: { |
|||
trigger: 'item', |
|||
backgroundColor: 'rgba(0, 0, 0, .6)', |
|||
}, |
|||
legend: { |
|||
icon: 'circle', |
|||
itemHeight: 10, |
|||
type: 'scroll', |
|||
orient: 'vertical', |
|||
left: '70%', |
|||
align: 'left', |
|||
top: 'middle', |
|||
textStyle: { |
|||
color: '#8C8C8C', |
|||
}, |
|||
height: 250, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: '成交额', |
|||
type: 'pie', |
|||
center: ['35%', '50%'], |
|||
radius: ['45%', '65%'], |
|||
label: { |
|||
show: false, |
|||
}, |
|||
data: m2R2Data, |
|||
animationDuration: 3000, |
|||
}, |
|||
], |
|||
}); |
|||
}); |
|||
return { chartRef }; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -0,0 +1,82 @@ |
|||
import { defineComponent } from 'vue'; |
|||
import { Tabs, Row, Col, Progress, Divider } from 'ant-design-vue'; |
|||
import { CollapseContainer } from '/@/components/Container/index'; |
|||
import TrendLine from './TrendLine.vue'; |
|||
import './flow-ana.less'; |
|||
const prefixCls = 'flow-analysis'; |
|||
export default defineComponent({ |
|||
name: 'AnalysisFLow', |
|||
setup() { |
|||
const renderContent = () => { |
|||
return ( |
|||
<Row> |
|||
{() => ( |
|||
<> |
|||
<Col md={24} lg={8}> |
|||
{() => ( |
|||
<CollapseContainer |
|||
title="整体流量评分" |
|||
canExpan={false} |
|||
class={`${prefixCls}__left`} |
|||
> |
|||
{() => ( |
|||
<> |
|||
<div class={`${prefixCls}__score`}> |
|||
86.2<span>分</span> |
|||
</div> |
|||
<div class={`${prefixCls}__rank`}> |
|||
排名<span>前20%</span> |
|||
</div> |
|||
<Progress percent={70} showInfo={false} status="active" /> |
|||
<Divider /> |
|||
|
|||
<ul class={`${prefixCls}__rs`}> |
|||
<li> |
|||
<span>平均分</span> |
|||
<span>77.5</span> |
|||
</li> |
|||
<li> |
|||
<span>最高分</span> |
|||
<span>99.5</span> |
|||
</li> |
|||
<li> |
|||
<span>最低分</span> |
|||
<span>56.5</span> |
|||
</li> |
|||
</ul> |
|||
</> |
|||
)} |
|||
</CollapseContainer> |
|||
)} |
|||
</Col> |
|||
<Col md={24} lg={16}> |
|||
{() => ( |
|||
<CollapseContainer title="整体流量趋势" canExpan={false}> |
|||
{() => <TrendLine />} |
|||
</CollapseContainer> |
|||
)} |
|||
</Col> |
|||
</> |
|||
)} |
|||
</Row> |
|||
); |
|||
}; |
|||
return () => ( |
|||
<Tabs class={prefixCls} default-active-key="1"> |
|||
{() => ( |
|||
<> |
|||
<Tabs.TabPane key="1" tab="产品一"> |
|||
{() => renderContent()} |
|||
</Tabs.TabPane> |
|||
<Tabs.TabPane key="2" tab="产品二"> |
|||
{() => renderContent()} |
|||
</Tabs.TabPane> |
|||
<Tabs.TabPane key="3" tab="产品三"> |
|||
{() => renderContent()} |
|||
</Tabs.TabPane> |
|||
</> |
|||
)} |
|||
</Tabs> |
|||
); |
|||
}, |
|||
}); |
|||
@ -0,0 +1,116 @@ |
|||
<template> |
|||
<div class="grow-card"> |
|||
<div class="grow-card-header"> |
|||
<div class="grow-card__info"> |
|||
<p class="grow-card__title">{{ info.title }}</p> |
|||
<CountTo prefix="$" :startVal="1" :endVal="info.price" /> |
|||
</div> |
|||
<img :src="info.icon" /> |
|||
</div> |
|||
<div class="grow-card-footer" :class="{ 'is-up': info.up }"> |
|||
<Statistic :value="info.percent"> |
|||
<template #prefix> <img :src="info.up ? riseSvg : downSvg" /> </template> |
|||
</Statistic> |
|||
<span class="grow-card__mom">{{ info.mom }}</span> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent, PropType } from 'vue'; |
|||
import { Statistic } from 'ant-design-vue'; |
|||
import { CountTo } from '/@/components/CountTo/index'; |
|||
|
|||
import riseSvg from '/@/assets/svg/dashboard/analysis-rise.svg'; |
|||
import downSvg from '/@/assets/svg/dashboard/analysis-down.svg'; |
|||
import { GrowCardItem } from '../types'; |
|||
|
|||
export default defineComponent({ |
|||
components: { Statistic, CountTo }, |
|||
props: { |
|||
info: { |
|||
type: Object as PropType<GrowCardItem>, |
|||
default: null, |
|||
}, |
|||
}, |
|||
setup() { |
|||
return { |
|||
riseSvg, |
|||
downSvg, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less"> |
|||
@import (reference) '../../../../design/index.less'; |
|||
|
|||
.grow-card { |
|||
display: flex; |
|||
width: calc(100% - 12px); |
|||
height: 158px; |
|||
padding: 16px 16px 12px 16px; |
|||
// margin: 0 12px 12px 12px; |
|||
cursor: pointer; |
|||
background: @white; |
|||
border-radius: 4px; |
|||
box-shadow: 6px 6px 54px 0 rgba(0, 0, 0, 0.05); |
|||
flex-direction: column; |
|||
|
|||
&:hover { |
|||
box-shadow: 6px 6px 54px 0 rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
&-header { |
|||
display: flex; |
|||
width: 100%; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
&__title { |
|||
font-family: PingFangSC-Regular; |
|||
font-size: 16px; |
|||
letter-spacing: 0; |
|||
color: #2c3a61; |
|||
opacity: 0.7; |
|||
} |
|||
|
|||
&__info { |
|||
span { |
|||
font-family: NeoSans; |
|||
font-size: 26px; |
|||
line-height: 38px; |
|||
} |
|||
} |
|||
|
|||
&-footer { |
|||
display: flex; |
|||
width: 100%; |
|||
margin-top: 24px; |
|||
align-items: center; |
|||
|
|||
.ant-statistic-content-value { |
|||
color: @error-color; |
|||
} |
|||
|
|||
.ant-statistic-content-prefix svg { |
|||
width: 0.98rem !important; |
|||
height: 0.98rem !important; |
|||
} |
|||
|
|||
&.is-up { |
|||
.ant-statistic-content-value { |
|||
color: @success-color; |
|||
} |
|||
} |
|||
} |
|||
|
|||
&__mom { |
|||
display: inline-block; |
|||
padding-left: 10px; |
|||
font-family: PingFangSC-Regular; |
|||
font-size: 12px; |
|||
line-height: 22px; |
|||
letter-spacing: 0; |
|||
color: #606060; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,154 @@ |
|||
<template> |
|||
<div :class="prefixCls"> |
|||
<div :class="`${prefixCls}-header`"> |
|||
<div :class="`${prefixCls}__info`"> |
|||
<span :class="`${prefixCls}__title`">{{ info.title }}</span> |
|||
<span :class="`${prefixCls}__desc`">{{ info.desc }}</span> |
|||
</div> |
|||
<span :class="`${prefixCls}__tag ${info.status}`">{{ info.text }}</span> |
|||
</div> |
|||
|
|||
<div :class="`${prefixCls}-body mt-5`"> |
|||
<div :class="`${prefixCls}__process-nfo`"> |
|||
<span>进度</span> |
|||
<span>{{ info.percent }}%</span> |
|||
</div> |
|||
<Progress :percent="info.percent" :showInfo="false" :status="info.status" /> |
|||
</div> |
|||
<div :class="`${prefixCls}-footer`"> |
|||
<span :class="`${prefixCls}__date`"> |
|||
更新日期: <span>{{ info.updateTime }}</span> |
|||
</span> |
|||
<div :class="`${prefixCls}__avatar`"> |
|||
<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" /> |
|||
<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" /> |
|||
<Avatar>+3</Avatar> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { computed, defineComponent, PropType } from 'vue'; |
|||
import { Progress, Avatar } from 'ant-design-vue'; |
|||
|
|||
import { TaskItem } from '../types'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'GrowCard', |
|||
components: { Progress, Avatar }, |
|||
props: { |
|||
info: { |
|||
type: Object as PropType<TaskItem>, |
|||
default: null, |
|||
}, |
|||
}, |
|||
setup(props) { |
|||
return { |
|||
prefixCls: 'task-card', |
|||
text: computed(() => { |
|||
const { status } = props.info || {}; |
|||
return status === 'active' |
|||
? '进度正常' |
|||
: status === 'exception' |
|||
? '进度滞后' |
|||
: '项目完成'; |
|||
}), |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less" scoped> |
|||
.task-card { |
|||
display: flex; |
|||
width: calc(100% - 24px); |
|||
height: 199px; |
|||
padding: 24px 20px 12px 16px; |
|||
margin: 0 12px 12px 12px; |
|||
background: #fff; |
|||
border: 1px solid #ececf2; |
|||
border-radius: 12px; |
|||
flex-direction: column; |
|||
|
|||
&-header { |
|||
display: flex; |
|||
width: 100%; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
|
|||
&__tag { |
|||
display: inline-block; |
|||
padding: 4px 6px; |
|||
font-family: PingFangSC-Regular; |
|||
font-size: 12px; |
|||
border-radius: 6px; |
|||
|
|||
&.success { |
|||
color: #55d187; |
|||
background: rgba(85, 209, 135, 0.16); |
|||
} |
|||
|
|||
&.warn { |
|||
color: #ffa07d; |
|||
background: #ffd16416; |
|||
} |
|||
|
|||
&.done { |
|||
color: #0593ff; |
|||
background: #0593ff16; |
|||
} |
|||
} |
|||
|
|||
&__info { |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
&__title { |
|||
font-family: PingFangSC-Medium; |
|||
font-size: 16px; |
|||
line-height: 24px; |
|||
color: #2c3a61; |
|||
} |
|||
|
|||
&__desc { |
|||
font-family: PingFangSC-Regular; |
|||
font-size: 12px; |
|||
line-height: 21px; |
|||
color: #8181a5; |
|||
} |
|||
|
|||
&__process-nfo { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
|
|||
span { |
|||
font-size: 14px; |
|||
line-height: 21px; |
|||
color: #8181a5; |
|||
} |
|||
} |
|||
|
|||
&-footer { |
|||
display: flex; |
|||
width: 100%; |
|||
margin-top: 16px; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
&__date { |
|||
font-size: 12px; |
|||
line-height: 21px; |
|||
color: #2c3a61; |
|||
|
|||
span { |
|||
color: #7c8087; |
|||
} |
|||
} |
|||
|
|||
&__avatar { |
|||
display: flex; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,105 @@ |
|||
<template> |
|||
<div ref="chartRef" :style="{ height, width }" /> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent, onMounted, ref, Ref } from 'vue'; |
|||
|
|||
import { useECharts } from '/@/hooks/web/useECharts'; |
|||
|
|||
import { basicProps } from './props'; |
|||
export default defineComponent({ |
|||
name: 'AnalysisLine', |
|||
props: basicProps, |
|||
setup() { |
|||
const chartRef = ref<HTMLDivElement | null>(null); |
|||
const { setOptions, echarts } = useECharts(chartRef as Ref<HTMLDivElement>); |
|||
|
|||
onMounted(() => { |
|||
setOptions({ |
|||
// title: { |
|||
// text: '产品成交额', |
|||
// }, |
|||
tooltip: { |
|||
trigger: 'axis', |
|||
padding: 3, |
|||
backgroundColor: 'rgba(0, 0, 0, .6)', |
|||
borderColor: '#777', |
|||
borderWidth: 1, |
|||
}, |
|||
legend: { |
|||
show: false, |
|||
}, |
|||
grid: { |
|||
left: '3%', |
|||
right: '4%', |
|||
bottom: '3%', |
|||
containLabel: true, |
|||
}, |
|||
xAxis: { |
|||
type: 'category', |
|||
boundaryGap: false, |
|||
axisTick: { |
|||
inside: true, // 刻度朝内 |
|||
}, |
|||
data: [ |
|||
'一月', |
|||
'二月', |
|||
'三月', |
|||
'四月', |
|||
'五月', |
|||
'六月', |
|||
'七月', |
|||
'八月', |
|||
'九月', |
|||
'十月', |
|||
'十一月', |
|||
'十二月', |
|||
], |
|||
}, |
|||
yAxis: { |
|||
type: 'value', |
|||
axisTick: { |
|||
inside: true, // 刻度朝内 |
|||
}, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: '产品一', |
|||
type: 'line', |
|||
itemStyle: { |
|||
color: '#5B8FF9', |
|||
}, |
|||
areaStyle: { |
|||
// 线性渐变,前4个参数分别是x0,y0,x2,y2(范围0~1);相当于图形包围盒中的百分比。如果最后一个参数是‘true’,则该四个值是绝对像素位置。 |
|||
// @ts-ignore |
|||
color: new echarts.graphic.LinearGradient( |
|||
0, |
|||
0, |
|||
0, |
|||
1, |
|||
[ |
|||
{ |
|||
offset: 0, |
|||
color: '#5B8FF9', |
|||
}, |
|||
{ |
|||
offset: 1, |
|||
color: 'rgba(118,168,248, 0)', |
|||
}, |
|||
], |
|||
false |
|||
), |
|||
shadowColor: 'rgba(118,168,248, 0.9)', // 阴影颜色 |
|||
shadowBlur: 20, // shadowBlur设图形阴影的模糊大小。配合shadowColor,shadowOffsetX/Y, 设置图形的阴影效果。 |
|||
}, |
|||
// areaStyle: {}, |
|||
data: [134, 330, 132, 101, 90, 230, 210, 150, 230, 400, 232, 234], |
|||
animationDuration: 3000, |
|||
}, |
|||
], |
|||
}); |
|||
}); |
|||
return { chartRef }; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -0,0 +1,56 @@ |
|||
.flow-analysis { |
|||
width: 100%; |
|||
background: #fff; |
|||
|
|||
&__left { |
|||
padding: 10px 20px !important; |
|||
border-right: 1px solid rgba(0, 0, 0, 0.06); |
|||
border-radius: 0; |
|||
} |
|||
|
|||
&__score { |
|||
margin-top: 20px; |
|||
font-size: 30px; |
|||
line-height: 38px; |
|||
color: rgba(0, 0, 0, 0.85); |
|||
|
|||
span { |
|||
font-size: 20px; |
|||
line-height: 28px; |
|||
color: rgba(0, 0, 0, 0.85); |
|||
} |
|||
} |
|||
|
|||
&__rank { |
|||
margin: 16px 0; |
|||
font-size: 12px; |
|||
line-height: 20px; |
|||
color: #7c8087; |
|||
|
|||
span { |
|||
display: inline-block; |
|||
margin-left: 10px; |
|||
color: #1c1d21; |
|||
} |
|||
} |
|||
|
|||
&__rs { |
|||
li { |
|||
display: flex; |
|||
line-height: 28px; |
|||
justify-content: space-between; |
|||
|
|||
span { |
|||
&:nth-child(1) { |
|||
font-size: 14px; |
|||
color: #1c1d21; |
|||
} |
|||
|
|||
&:nth-child(2) { |
|||
font-size: 16px; |
|||
color: #1c1d21; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
import { PropType } from 'vue'; |
|||
|
|||
export interface BasicProps { |
|||
width: string; |
|||
height: string; |
|||
} |
|||
export const basicProps = { |
|||
width: { |
|||
type: String as PropType<string>, |
|||
default: '100%', |
|||
}, |
|||
height: { |
|||
type: String as PropType<string>, |
|||
default: '280px', |
|||
}, |
|||
}; |
|||
@ -0,0 +1,107 @@ |
|||
import { GrowCardItem, TaskItem } from './types'; |
|||
import iconSvg1 from '/@/assets/svg/dashboard/analysis-icon1.svg'; |
|||
import iconSvg2 from '/@/assets/svg/dashboard/analysis-icon2.svg'; |
|||
import iconSvg3 from '/@/assets/svg/dashboard/analysis-icon3.svg'; |
|||
import iconSvg4 from '/@/assets/svg/dashboard/analysis-icon4.svg'; |
|||
export const taskList: TaskItem[] = [ |
|||
{ |
|||
percent: 50, |
|||
title: '开发任务一', |
|||
updateTime: '2020.7.12', |
|||
desc: '开发任务一简介', |
|||
status: 'active', |
|||
}, |
|||
{ |
|||
percent: 67, |
|||
title: '开发任务二', |
|||
updateTime: '2020.3.12', |
|||
desc: '开发任务二简介', |
|||
status: 'exception', |
|||
}, |
|||
{ |
|||
percent: 100, |
|||
title: '开发任务三', |
|||
updateTime: '2020.4.12', |
|||
desc: '开发任务三简介', |
|||
|
|||
status: 'success', |
|||
}, |
|||
]; |
|||
export const growCardList: GrowCardItem[] = [ |
|||
{ |
|||
title: '总用户数', |
|||
icon: iconSvg1, |
|||
price: 80000, |
|||
up: true, |
|||
mom: '环比增长', |
|||
percent: 2.5, |
|||
}, |
|||
{ |
|||
title: '产品数量', |
|||
icon: iconSvg2, |
|||
price: 4000, |
|||
up: true, |
|||
mom: '同比增长', |
|||
percent: 3, |
|||
}, |
|||
{ |
|||
title: '总营业额', |
|||
icon: iconSvg3, |
|||
price: 3000000, |
|||
up: false, |
|||
mom: '环比降低', |
|||
percent: 2, |
|||
}, |
|||
{ |
|||
title: '总任务数', |
|||
icon: iconSvg4, |
|||
price: 10000, |
|||
up: false, |
|||
mom: '同比降低', |
|||
percent: 1, |
|||
}, |
|||
]; |
|||
export const randomizeArray = function (arg: any) { |
|||
const array = arg.slice(); |
|||
let currentIndex = array.length, |
|||
temporaryValue, |
|||
randomIndex; |
|||
|
|||
while (0 !== currentIndex) { |
|||
randomIndex = Math.floor(Math.random() * currentIndex); |
|||
currentIndex -= 1; |
|||
|
|||
temporaryValue = array[currentIndex]; |
|||
array[currentIndex] = array[randomIndex]; |
|||
array[randomIndex] = temporaryValue; |
|||
} |
|||
|
|||
return array; |
|||
}; |
|||
|
|||
export const sparklineData = [ |
|||
47, |
|||
45, |
|||
54, |
|||
38, |
|||
56, |
|||
24, |
|||
65, |
|||
31, |
|||
37, |
|||
39, |
|||
62, |
|||
51, |
|||
35, |
|||
41, |
|||
35, |
|||
27, |
|||
93, |
|||
53, |
|||
61, |
|||
27, |
|||
54, |
|||
43, |
|||
19, |
|||
46, |
|||
]; |
|||
@ -0,0 +1,83 @@ |
|||
<template> |
|||
<div class="analysis p-4"> |
|||
<Row class="pl-2"> |
|||
<template v-for="item in growCardList" :key="item.title"> |
|||
<ACol :sm="24" :md="12" :lg="6"> |
|||
<GrowCard :info="item" /> |
|||
</ACol> |
|||
</template> |
|||
</Row> |
|||
|
|||
<Row> |
|||
<ACol :md="24" :lg="17" class="my-3"> |
|||
<CollapseContainer class="mr-3" title="产品成交额" :canExpan="false"> |
|||
<AnalysisLine /> |
|||
</CollapseContainer> |
|||
<Row class="mt-3"> |
|||
<ACol :md="24" :lg="12" class="product-total"> |
|||
<CollapseContainer class="mr-3" title="产品成交额" :canExpan="false"> |
|||
<AnalysisPie /> |
|||
</CollapseContainer> |
|||
</ACol> |
|||
<ACol :md="24" :lg="12"> |
|||
<CollapseContainer class="mr-3" title="用户来源" :canExpan="false"> |
|||
<AnalysisBar /> |
|||
</CollapseContainer> |
|||
</ACol> |
|||
</Row> |
|||
</ACol> |
|||
<ACol :md="24" :lg="7"> |
|||
<CollapseContainer class="mt-3" title="项目进度" :canExpan="false"> |
|||
<template v-for="item in taskList" :key="item.title"> |
|||
<TaskCard :info="item" /> |
|||
</template> |
|||
</CollapseContainer> |
|||
</ACol> |
|||
</Row> |
|||
<Row> |
|||
<FlowAnalysis /> |
|||
</Row> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent } from 'vue'; |
|||
import GrowCard from './components/GrowCard.vue'; |
|||
import TrendLine from './components/TrendLine.vue'; |
|||
import AnalysisLine from './components/AnalysisLine.vue'; |
|||
import AnalysisPie from './components/AnalysisPie.vue'; |
|||
import AnalysisBar from './components/AnalysisBar.vue'; |
|||
import TaskCard from './components/TaskCard.vue'; |
|||
import FlowAnalysis from './components/FlowAnalysis'; |
|||
import { Row, Col } from 'ant-design-vue'; |
|||
import { CollapseContainer } from '/@/components/Container/index'; |
|||
|
|||
import { growCardList, taskList } from './data'; |
|||
export default defineComponent({ |
|||
components: { |
|||
Row, |
|||
ACol: Col, |
|||
GrowCard, |
|||
CollapseContainer, |
|||
TrendLine, |
|||
AnalysisLine, |
|||
AnalysisPie, |
|||
AnalysisBar, |
|||
TaskCard, |
|||
FlowAnalysis, |
|||
}, |
|||
setup() { |
|||
return { growCardList, taskList }; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less" scoped> |
|||
@import (reference) '../../../design/index.less'; |
|||
|
|||
.analysis { |
|||
width: 100%; |
|||
|
|||
.product-total { |
|||
.respond-to(small-and-medium, {padding-right: 0;margin-bottom: 24px;}); |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,16 @@ |
|||
export interface GrowCardItem { |
|||
icon: string; |
|||
title: string; |
|||
price: number; |
|||
up: boolean; |
|||
mom: string; |
|||
percent: number; |
|||
} |
|||
|
|||
export interface TaskItem { |
|||
percent: number; |
|||
status: 'success' | 'exception' | 'active'; |
|||
updateTime: string; |
|||
title: string; |
|||
desc: string; |
|||
} |
|||
Loading…
Reference in new issue