mirror of
https://github.com/fumiama/paper-manager.git
synced 2026-06-25 05:20:16 +08:00
add table visit in user.db
This commit is contained in:
@@ -384,6 +384,7 @@ func APIHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if h, ok := apimap[r.URL.Path]; ok {
|
if h, ok := apimap[r.URL.Path]; ok {
|
||||||
|
global.UserDB.VisitAPI()
|
||||||
h.handle(w, r)
|
h.handle(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ func FileHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if !utils.IsMethod("GET", w, r) {
|
if !utils.IsMethod("GET", w, r) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
token := r.Header.Get("Authorization")
|
||||||
|
user := usertokens.Get(token)
|
||||||
|
if user == nil {
|
||||||
|
writeresult(w, codeError, nil, errInvalidToken.Error(), typeError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
global.UserDB.VisitAPI()
|
||||||
if r.URL.Path[0] != '/' {
|
if r.URL.Path[0] != '/' {
|
||||||
r.URL.Path = "/" + r.URL.Path
|
r.URL.Path = "/" + r.URL.Path
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,8 +60,9 @@ const (
|
|||||||
type MessageType uint8
|
type MessageType uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
UserTableUser = "user"
|
UserTableUser = "user"
|
||||||
UserTableMessage = "msg"
|
UserTableMessage = "msg"
|
||||||
|
UserTableMonthlyAPIVisit = "visit"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -97,6 +98,10 @@ func init() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
err = UserDB.db.Create(UserTableMonthlyAPIVisit, &MonthlyAPIVisit{})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
if isinit { // 添加初始账户
|
if isinit { // 添加初始账户
|
||||||
UserDB.AddUser(&User{
|
UserDB.AddUser(&User{
|
||||||
Role: RoleSuper,
|
Role: RoleSuper,
|
||||||
@@ -720,3 +725,50 @@ func (u *UserDatabase) DelMessageByID(id int) (err error) {
|
|||||||
u.mu.Unlock()
|
u.mu.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MonthlyAPIVisit counts the api visit history
|
||||||
|
type MonthlyAPIVisit struct {
|
||||||
|
YM uint32 // YM is yyyymm
|
||||||
|
Count uint32 // visit count this mounth
|
||||||
|
}
|
||||||
|
|
||||||
|
// VisitAPI increases count of this mounth by 1
|
||||||
|
func (u *UserDatabase) VisitAPI() {
|
||||||
|
now := time.Now()
|
||||||
|
ym := uint32(now.Year())*100 + uint32(now.Month())
|
||||||
|
var v MonthlyAPIVisit
|
||||||
|
u.mu.Lock()
|
||||||
|
defer u.mu.Unlock()
|
||||||
|
_ = u.db.Find(UserTableMonthlyAPIVisit, &v, "WHERE YM="+strconv.FormatUint(uint64(ym), 10))
|
||||||
|
v.YM = ym
|
||||||
|
v.Count++
|
||||||
|
err := u.db.Insert(UserTableMonthlyAPIVisit, &v)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnln("[global.user] insert visit error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAnnualAPIVisitCount get the latest 12 mounths' count
|
||||||
|
func (u *UserDatabase) GetAnnualAPIVisitCount() (cnts [12]uint32) {
|
||||||
|
var v MonthlyAPIVisit
|
||||||
|
var yms [12]uint32
|
||||||
|
now := time.Now()
|
||||||
|
y100 := uint32(now.Year()) * 100
|
||||||
|
py100 := uint32(now.Year()-1) * 100
|
||||||
|
nm := int(now.Month())
|
||||||
|
for i := 0; i < nm; i++ {
|
||||||
|
yms[i] = y100 + uint32(i+1)
|
||||||
|
}
|
||||||
|
for i := nm; i < 12; i++ {
|
||||||
|
yms[i] = py100 + uint32(i+1)
|
||||||
|
}
|
||||||
|
u.mu.RLock()
|
||||||
|
defer u.mu.RUnlock()
|
||||||
|
i := 0
|
||||||
|
for _, ym := range yms {
|
||||||
|
_ = u.db.Find(UserTableMonthlyAPIVisit, &v, "WHERE YM="+strconv.FormatUint(uint64(ym), 10))
|
||||||
|
cnts[i] = v.Count
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ func UploadHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeresult(w, codeError, nil, errInvalidToken.Error(), typeError)
|
writeresult(w, codeError, nil, errInvalidToken.Error(), typeError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
global.UserDB.VisitAPI()
|
||||||
ff, h, err := r.FormFile("avatar")
|
ff, h, err := r.FormFile("avatar")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer ff.Close()
|
defer ff.Close()
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="md:flex">
|
|
||||||
<template>
|
|
||||||
<div>1</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
defineProps({
|
|
||||||
loading: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Card title="成交占比" :loading="loading">
|
|
||||||
<div ref="chartRef" :style="{ width, height }"></div>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Ref, ref, watch } from 'vue'
|
|
||||||
import { Card } from 'ant-design-vue'
|
|
||||||
import { useECharts } from '/@/hooks/web/useECharts'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
loading: Boolean,
|
|
||||||
width: {
|
|
||||||
type: String as PropType<string>,
|
|
||||||
default: '100%',
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
type: String as PropType<string>,
|
|
||||||
default: '300px',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const chartRef = ref<HTMLDivElement | null>(null)
|
|
||||||
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.loading,
|
|
||||||
() => {
|
|
||||||
if (props.loading) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setOptions({
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'item',
|
|
||||||
},
|
|
||||||
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: '访问来源',
|
|
||||||
type: 'pie',
|
|
||||||
radius: '80%',
|
|
||||||
center: ['50%', '50%'],
|
|
||||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
|
||||||
data: [
|
|
||||||
{ value: 500, name: '电子产品' },
|
|
||||||
{ value: 310, name: '服装' },
|
|
||||||
{ value: 274, name: '化妆品' },
|
|
||||||
{ value: 400, name: '家居' },
|
|
||||||
].sort(function (a, b) {
|
|
||||||
return a.value - b.value
|
|
||||||
}),
|
|
||||||
roseType: 'radius',
|
|
||||||
animationType: 'scale',
|
|
||||||
animationEasing: 'exponentialInOut',
|
|
||||||
animationDelay: function () {
|
|
||||||
return Math.random() * 400
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@@ -1,38 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<Card
|
<Card
|
||||||
:tab-list="tabListTitle"
|
:tab-list="[
|
||||||
v-bind="$attrs"
|
{
|
||||||
:active-tab-key="activeKey"
|
key: 'tab1',
|
||||||
@tab-change="onTabChange"
|
tab: '访问量',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
active-tab-key="tab1"
|
||||||
>
|
>
|
||||||
<p v-if="activeKey === 'tab1'">
|
<VisitAnalysisBar />
|
||||||
<VisitAnalysis />
|
|
||||||
</p>
|
|
||||||
<p v-if="activeKey === 'tab2'">
|
|
||||||
<VisitAnalysisBar />
|
|
||||||
</p>
|
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue'
|
|
||||||
import { Card } from 'ant-design-vue'
|
import { Card } from 'ant-design-vue'
|
||||||
import VisitAnalysis from './VisitAnalysis.vue'
|
|
||||||
import VisitAnalysisBar from './VisitAnalysisBar.vue'
|
import VisitAnalysisBar from './VisitAnalysisBar.vue'
|
||||||
|
|
||||||
const activeKey = ref('tab1')
|
|
||||||
|
|
||||||
const tabListTitle = [
|
|
||||||
{
|
|
||||||
key: 'tab1',
|
|
||||||
tab: '流量趋势',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'tab2',
|
|
||||||
tab: '访问量',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
function onTabChange(key) {
|
|
||||||
activeKey.value = key
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div ref="chartRef" :style="{ height, width }"></div>
|
|
||||||
</template>
|
|
||||||
<script lang="ts">
|
|
||||||
import { basicProps } from './props'
|
|
||||||
</script>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { onMounted, ref, Ref } from 'vue'
|
|
||||||
import { useECharts } from '/@/hooks/web/useECharts'
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
...basicProps,
|
|
||||||
})
|
|
||||||
const chartRef = ref<HTMLDivElement | null>(null)
|
|
||||||
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
setOptions({
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
axisPointer: {
|
|
||||||
lineStyle: {
|
|
||||||
width: 1,
|
|
||||||
color: '#019680',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
boundaryGap: false,
|
|
||||||
data: [...new Array(18)].map((_item, index) => `${index + 6}:00`),
|
|
||||||
splitLine: {
|
|
||||||
show: true,
|
|
||||||
lineStyle: {
|
|
||||||
width: 1,
|
|
||||||
type: 'solid',
|
|
||||||
color: 'rgba(226,226,226,0.5)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisTick: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
yAxis: [
|
|
||||||
{
|
|
||||||
type: 'value',
|
|
||||||
max: 80000,
|
|
||||||
splitNumber: 4,
|
|
||||||
axisTick: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
splitArea: {
|
|
||||||
show: true,
|
|
||||||
areaStyle: {
|
|
||||||
color: ['rgba(255,255,255,0.2)', 'rgba(226,226,226,0.2)'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
grid: { left: '1%', right: '1%', top: '2 %', bottom: 0, containLabel: true },
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
smooth: true,
|
|
||||||
data: [
|
|
||||||
111, 222, 4000, 18000, 33333, 55555, 66666, 33333, 14000, 36000, 66666, 44444, 22222,
|
|
||||||
11111, 4000, 2000, 500, 333, 222, 111,
|
|
||||||
],
|
|
||||||
type: 'line',
|
|
||||||
areaStyle: {},
|
|
||||||
itemStyle: {
|
|
||||||
color: '#5ab1ef',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
smooth: true,
|
|
||||||
data: [
|
|
||||||
33, 66, 88, 333, 3333, 5000, 18000, 3000, 1200, 13000, 22000, 11000, 2221, 1201, 390,
|
|
||||||
198, 60, 30, 22, 11,
|
|
||||||
],
|
|
||||||
type: 'line',
|
|
||||||
areaStyle: {},
|
|
||||||
itemStyle: {
|
|
||||||
color: '#019680',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="chartRef" :style="{ height, width }"></div>
|
<div ref="chartRef" :style="{ height, width }"></div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
|
||||||
import { basicProps } from './props'
|
|
||||||
</script>
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref, Ref } from 'vue'
|
import { onMounted, ref, Ref } from 'vue'
|
||||||
import { useECharts } from '/@/hooks/web/useECharts'
|
import { useECharts } from '/@/hooks/web/useECharts'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
...basicProps,
|
width: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: '100%',
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: '280px',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const chartRef = ref<HTMLDivElement | null>(null)
|
const chartRef = ref<HTMLDivElement | null>(null)
|
||||||
@@ -25,14 +29,13 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
grid: { left: '1%', right: '1%', top: '2 %', bottom: 0, containLabel: true },
|
grid: { left: '1%', right: '1%', top: '2%', bottom: 0, containLabel: true },
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
data: [...new Array(12)].map((_item, index) => `${index + 1}月`),
|
data: [...new Array(12)].map((_item, index) => `${index + 1}月`),
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
max: 8000,
|
|
||||||
splitNumber: 4,
|
splitNumber: 4,
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Card title="转化率" :loading="loading">
|
|
||||||
<div ref="chartRef" :style="{ width, height }"></div>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Ref, ref, watch } from 'vue'
|
|
||||||
import { Card } from 'ant-design-vue'
|
|
||||||
import { useECharts } from '/@/hooks/web/useECharts'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
loading: Boolean,
|
|
||||||
width: {
|
|
||||||
type: String as PropType<string>,
|
|
||||||
default: '100%',
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
type: String as PropType<string>,
|
|
||||||
default: '300px',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const chartRef = ref<HTMLDivElement | null>(null)
|
|
||||||
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.loading,
|
|
||||||
() => {
|
|
||||||
if (props.loading) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setOptions({
|
|
||||||
legend: {
|
|
||||||
bottom: 0,
|
|
||||||
data: ['访问', '购买'],
|
|
||||||
},
|
|
||||||
tooltip: {},
|
|
||||||
radar: {
|
|
||||||
radius: '60%',
|
|
||||||
splitNumber: 8,
|
|
||||||
indicator: [
|
|
||||||
{
|
|
||||||
name: '电脑',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '充电器',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '耳机',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '手机',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Ipad',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '耳机',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
type: 'radar',
|
|
||||||
symbolSize: 0,
|
|
||||||
areaStyle: {
|
|
||||||
shadowBlur: 0,
|
|
||||||
shadowColor: 'rgba(0,0,0,.2)',
|
|
||||||
shadowOffsetX: 0,
|
|
||||||
shadowOffsetY: 10,
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
value: [90, 50, 86, 40, 50, 20],
|
|
||||||
name: '访问',
|
|
||||||
itemStyle: {
|
|
||||||
color: '#b6a2de',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: [70, 75, 70, 76, 20, 85],
|
|
||||||
name: '购买',
|
|
||||||
itemStyle: {
|
|
||||||
color: '#5ab1ef',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Card title="访问来源" :loading="loading">
|
|
||||||
<div ref="chartRef" :style="{ width, height }"></div>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Ref, ref, watch } from 'vue'
|
|
||||||
import { Card } from 'ant-design-vue'
|
|
||||||
import { useECharts } from '/@/hooks/web/useECharts'
|
|
||||||
const props = defineProps({
|
|
||||||
loading: Boolean,
|
|
||||||
width: {
|
|
||||||
type: String as PropType<string>,
|
|
||||||
default: '100%',
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
type: String as PropType<string>,
|
|
||||||
default: '300px',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const chartRef = ref<HTMLDivElement | null>(null)
|
|
||||||
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.loading,
|
|
||||||
() => {
|
|
||||||
if (props.loading) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setOptions({
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'item',
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
bottom: '1%',
|
|
||||||
left: 'center',
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
|
||||||
name: '访问来源',
|
|
||||||
type: 'pie',
|
|
||||||
radius: ['40%', '70%'],
|
|
||||||
avoidLabelOverlap: false,
|
|
||||||
itemStyle: {
|
|
||||||
borderRadius: 10,
|
|
||||||
borderColor: '#fff',
|
|
||||||
borderWidth: 2,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
show: false,
|
|
||||||
position: 'center',
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
fontSize: '12',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
labelLine: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
data: [
|
|
||||||
{ value: 1048, name: '搜索引擎' },
|
|
||||||
{ value: 735, name: '直接访问' },
|
|
||||||
{ value: 580, name: '邮件营销' },
|
|
||||||
{ value: 484, name: '联盟广告' },
|
|
||||||
],
|
|
||||||
animationType: 'scale',
|
|
||||||
animationEasing: 'exponentialInOut',
|
|
||||||
animationDelay: function () {
|
|
||||||
return Math.random() * 100
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export interface GrowCardItem {
|
|
||||||
icon: string
|
|
||||||
title: string
|
|
||||||
value: number
|
|
||||||
total: number
|
|
||||||
color: string
|
|
||||||
action: string
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<GrowCard :loading="loading" class="enter-y" />
|
<SiteAnalysis :loading="loading" />
|
||||||
<SiteAnalysis class="!my-4 enter-y" :loading="loading" />
|
|
||||||
<div class="md:flex enter-y">
|
|
||||||
<VisitRadar class="md:w-1/3 w-full" :loading="loading" />
|
|
||||||
<VisitSource class="md:w-1/3 !md:mx-4 !md:my-0 !my-4 w-full" :loading="loading" />
|
|
||||||
<SalesProductPie class="md:w-1/3 w-full" :loading="loading" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import GrowCard from './components/GrowCard.vue'
|
|
||||||
import SiteAnalysis from './components/SiteAnalysis.vue'
|
import SiteAnalysis from './components/SiteAnalysis.vue'
|
||||||
import VisitSource from './components/VisitSource.vue'
|
|
||||||
import VisitRadar from './components/VisitRadar.vue'
|
|
||||||
import SalesProductPie from './components/SalesProductPie.vue'
|
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user