main
commit
24b28d428c
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
|
||||
|
||||
VITE_API_BASE_URL = '/api'
|
||||
|
||||
VITE_NFT_BASE_URL = 'http://store-manage.hmily.club'
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
|
||||
|
||||
VITE_API_BASE_URL = '/api'
|
||||
|
||||
VITE_NFT_BASE_URL = 'http://store-manage.hmily.club'
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Vue 3 + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (previously Volar) and disable Vetur
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Vue</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "sotre-manage-echarts",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"ant-design-vue": "^4.2.1",
|
||||
"axios": "^1.6.2",
|
||||
"echarts": "^5.3.2",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"unocss": "^0.59.4",
|
||||
"vite": "^5.2.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,8 @@
|
|||
<template>
|
||||
<div>
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
|
|
@ -0,0 +1,122 @@
|
|||
<template>
|
||||
<span :style="{ color }">
|
||||
{{ value }}
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
import {
|
||||
defineComponent,
|
||||
ref,
|
||||
computed,
|
||||
watchEffect,
|
||||
unref,
|
||||
onMounted,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { useTransition, TransitionPresets } from '@vueuse/core'
|
||||
|
||||
const isNumber = (val) => typeof val === 'number'
|
||||
|
||||
const props = {
|
||||
startVal: { type: Number, default: 0 },
|
||||
endVal: { type: Number, default: 2021 },
|
||||
duration: { type: Number, default: 1500 },
|
||||
autoplay: { type: Boolean, default: true },
|
||||
decimals: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
validator(value) {
|
||||
return value >= 0
|
||||
},
|
||||
},
|
||||
prefix: { type: String, default: '' },
|
||||
suffix: { type: String, default: '' },
|
||||
separator: { type: String, default: ',' },
|
||||
decimal: { type: String, default: '.' },
|
||||
/**
|
||||
* font color
|
||||
*/
|
||||
color: { type: String },
|
||||
/**
|
||||
* Turn on digital animation
|
||||
*/
|
||||
useEasing: { type: Boolean, default: true },
|
||||
/**
|
||||
* Digital animation
|
||||
*/
|
||||
transition: { type: String, default: 'linear' },
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CountTo',
|
||||
props,
|
||||
emits: ['onStarted', 'onFinished'],
|
||||
setup(props, { emit }) {
|
||||
const source = ref(props.startVal)
|
||||
const disabled = ref(false)
|
||||
let outputValue = useTransition(source)
|
||||
|
||||
const value = computed(() => formatNumber(unref(outputValue)))
|
||||
|
||||
watchEffect(() => {
|
||||
source.value = props.startVal
|
||||
})
|
||||
|
||||
watch([() => props.startVal, () => props.endVal], () => {
|
||||
if (props.autoplay) {
|
||||
start()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
props.autoplay && start()
|
||||
})
|
||||
|
||||
function start() {
|
||||
run()
|
||||
source.value = props.endVal
|
||||
}
|
||||
|
||||
function reset() {
|
||||
source.value = props.startVal
|
||||
run()
|
||||
}
|
||||
|
||||
function run() {
|
||||
outputValue = useTransition(source, {
|
||||
disabled,
|
||||
duration: props.duration,
|
||||
onFinished: () => emit('onFinished'),
|
||||
onStarted: () => emit('onStarted'),
|
||||
...(props.useEasing
|
||||
? { transition: TransitionPresets[props.transition] }
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
|
||||
function formatNumber(num) {
|
||||
if (!num && num !== 0) {
|
||||
return ''
|
||||
}
|
||||
const { decimals, decimal, separator, suffix, prefix } = props
|
||||
num = Number(num).toFixed(decimals)
|
||||
num = parseFloat(num)
|
||||
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 { value, start, reset }
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
msg: String,
|
||||
})
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Install
|
||||
<a href="https://github.com/vuejs/language-tools" target="_blank">Volar</a>
|
||||
in your IDE for a better DX
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import ScaleScreen from './src/ScaleScreen.vue'
|
||||
|
||||
export default ScaleScreen
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
<template>
|
||||
<div>
|
||||
<transition name="slide">
|
||||
<div
|
||||
v-if="isScroll"
|
||||
v-show="showTop"
|
||||
class="fixed topTool border border-[#396684] bg-[#1c2c34cc] top-0 z-99999 flex text-white py-4px px-10px"
|
||||
>
|
||||
<div class="px-10px cursor-pointer" @click="handleScrollLeft">左</div>
|
||||
<div class="px-10px cursor-pointer" @click="handleScrollCenter">中</div>
|
||||
<div class="px-10px cursor-pointer" @click="handleScrollRight">右</div>
|
||||
</div>
|
||||
</transition>
|
||||
<section
|
||||
ref="screenRef"
|
||||
:style="{ ...styles.box, ...boxStyle }"
|
||||
class="v-screen-box"
|
||||
>
|
||||
<div
|
||||
:style="{ ...styles.wrapper, ...wrapperStyle }"
|
||||
class="screen-wrapper"
|
||||
ref="screenWrapper"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import {
|
||||
defineComponent,
|
||||
nextTick,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
reactive,
|
||||
ref,
|
||||
toRef,
|
||||
} from 'vue'
|
||||
/**
|
||||
* 防抖函数
|
||||
* @param {Function} fn
|
||||
* @param {number} delay
|
||||
* @returns {() => void}
|
||||
*/
|
||||
function debounce(fn, delay) {
|
||||
let timer
|
||||
return function (...args) {
|
||||
if (timer) clearTimeout(timer)
|
||||
timer = setTimeout(
|
||||
() => {
|
||||
typeof fn === 'function' && fn.apply(null, args)
|
||||
clearTimeout(timer)
|
||||
},
|
||||
delay > 0 ? delay : 100
|
||||
)
|
||||
}
|
||||
}
|
||||
export default defineComponent({
|
||||
name: 'ScaleScreen',
|
||||
props: {
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: 1920,
|
||||
},
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: 1080,
|
||||
},
|
||||
fullScreen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
autoScale: {
|
||||
type: [Object, Boolean],
|
||||
default: true,
|
||||
},
|
||||
delay: {
|
||||
type: Number,
|
||||
default: 500,
|
||||
},
|
||||
boxStyle: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
wrapperStyle: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const state = reactive({
|
||||
width: 0,
|
||||
height: 0,
|
||||
originalWidth: 0,
|
||||
originalHeight: 0,
|
||||
observer: null,
|
||||
showTop: false,
|
||||
isScroll: false,
|
||||
})
|
||||
const screenRef = ref()
|
||||
const styles = reactive({
|
||||
box: {
|
||||
overflow: 'hidden',
|
||||
backgroundSize: `100% 100%`,
|
||||
background: `#000`,
|
||||
width: `100vw`,
|
||||
height: `100vh`,
|
||||
},
|
||||
wrapper: {
|
||||
transitionProperty: `all`,
|
||||
transitionTimingFunction: `cubic-bezier(0.4, 0, 0.2, 1)`,
|
||||
transitionDuration: `500ms`,
|
||||
position: `relative`,
|
||||
overflow: `hidden`,
|
||||
zIndex: 100,
|
||||
transformOrigin: `left top`,
|
||||
display: 'inline-block',
|
||||
},
|
||||
})
|
||||
const screenWrapper = ref()
|
||||
/**
|
||||
* 初始化大屏容器宽高
|
||||
*/
|
||||
const initSize = () => {
|
||||
return new Promise((resolve) => {
|
||||
nextTick(() => {
|
||||
var _a, _b
|
||||
// region 获取大屏真实尺寸
|
||||
if (props.width && props.height) {
|
||||
state.width = props.width
|
||||
state.height = props.height
|
||||
} else {
|
||||
state.width =
|
||||
(_a = screenWrapper.value) === null || _a === void 0
|
||||
? void 0
|
||||
: _a.clientWidth
|
||||
state.height =
|
||||
(_b = screenWrapper.value) === null || _b === void 0
|
||||
? void 0
|
||||
: _b.clientHeight
|
||||
}
|
||||
// endregion
|
||||
// region 获取画布尺寸
|
||||
if (!state.originalHeight || !state.originalWidth) {
|
||||
state.originalWidth = window.screen.width
|
||||
state.originalHeight = window.screen.height
|
||||
}
|
||||
// endregion
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 更新大屏容器宽高
|
||||
*/
|
||||
const updateSize = () => {
|
||||
if (state.width && state.height) {
|
||||
screenWrapper.value.style.width = `${state.width}px`
|
||||
screenWrapper.value.style.height = `${state.height}px`
|
||||
} else {
|
||||
screenWrapper.value.style.width = `${state.originalWidth}px`
|
||||
screenWrapper.value.style.height = `${state.originalHeight}px`
|
||||
}
|
||||
}
|
||||
const autoScale = (scale) => {
|
||||
if (!props.autoScale) return
|
||||
const domWidth = screenWrapper.value.clientWidth
|
||||
const domHeight = screenWrapper.value.clientHeight
|
||||
const currentWidth = document.body.clientWidth
|
||||
const currentHeight = document.body.clientHeight
|
||||
screenWrapper.value.style.transform = `scale(${scale},${scale})`
|
||||
let mx = Math.max((currentWidth - domWidth * scale) / 2, 0)
|
||||
let my = Math.max((currentHeight - domHeight * scale) / 2, 0)
|
||||
if (typeof props.autoScale === 'object') {
|
||||
!props.autoScale.x && (mx = 0)
|
||||
!props.autoScale.y && (my = 0)
|
||||
}
|
||||
screenWrapper.value.style.margin = `${my}px ${mx}px`
|
||||
}
|
||||
const updateScale = () => {
|
||||
// 获取真实视口尺寸
|
||||
const currentWidth = document.body.clientWidth
|
||||
let currentHeight = document.body.clientHeight
|
||||
if (currentWidth < 3000) currentHeight = currentHeight - 10
|
||||
// 获取大屏最终的宽高
|
||||
const realWidth = state.width || state.originalWidth
|
||||
const realHeight = state.height || state.originalHeight
|
||||
// 计算缩放比例
|
||||
const widthScale = currentWidth / +realWidth
|
||||
const heightScale = currentHeight / +realHeight
|
||||
// 若要铺满全屏,则按照各自比例缩放
|
||||
if (props.fullScreen) {
|
||||
screenWrapper.value.style.transform = `scale(${widthScale},${heightScale})`
|
||||
return false
|
||||
}
|
||||
// 按照宽高最小比例进行缩放
|
||||
let scale = Math.min(widthScale, heightScale)
|
||||
if (currentWidth < 3000) {
|
||||
scale = heightScale
|
||||
state.isScroll = true
|
||||
styles.box['overflow-x'] = 'auto'
|
||||
} else {
|
||||
state.isScroll = false
|
||||
styles.box.overflow = 'hidden'
|
||||
}
|
||||
autoScale(scale)
|
||||
}
|
||||
const onResize = debounce(async () => {
|
||||
await initSize()
|
||||
updateSize()
|
||||
updateScale()
|
||||
}, props.delay)
|
||||
const initMutationObserver = () => {
|
||||
const observer = (state.observer = new MutationObserver(() => {
|
||||
onResize()
|
||||
}))
|
||||
observer.observe(screenWrapper.value, {
|
||||
attributes: true,
|
||||
attributeFilter: ['style'],
|
||||
attributeOldValue: true,
|
||||
})
|
||||
}
|
||||
const onMouseUpdate = (e) => {
|
||||
if (e.pageY <= 30) state.showTop = true
|
||||
else state.showTop = false
|
||||
}
|
||||
onMounted(() => {
|
||||
nextTick(async () => {
|
||||
await initSize()
|
||||
updateSize()
|
||||
updateScale()
|
||||
window.addEventListener('resize', onResize)
|
||||
window.addEventListener('mousemove', onMouseUpdate)
|
||||
initMutationObserver()
|
||||
})
|
||||
})
|
||||
onUnmounted(() => {
|
||||
var _a
|
||||
window.removeEventListener('resize', onResize)
|
||||
window.removeEventListener('mousemove', onMouseUpdate)
|
||||
;(_a = state.observer) === null || _a === void 0
|
||||
? void 0
|
||||
: _a.disconnect()
|
||||
})
|
||||
const handleScrollLeft = () => {
|
||||
nextTick(() => {
|
||||
var _a
|
||||
;(_a = screenRef.value) === null || _a === void 0
|
||||
? void 0
|
||||
: _a.scrollTo({ left: 0, behavior: 'smooth' })
|
||||
})
|
||||
}
|
||||
const handleScrollCenter = () => {
|
||||
nextTick(() => {
|
||||
var _a, _b, _c
|
||||
;(_a = screenRef.value) === null || _a === void 0
|
||||
? void 0
|
||||
: _a.scrollTo({
|
||||
left:
|
||||
((_c =
|
||||
(_b = screenRef.value) === null || _b === void 0
|
||||
? void 0
|
||||
: _b.scrollWidth) !== null && _c !== void 0
|
||||
? _c
|
||||
: 0) /
|
||||
2 -
|
||||
document.body.clientWidth / 2,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
})
|
||||
}
|
||||
const handleScrollRight = () => {
|
||||
nextTick(() => {
|
||||
var _a, _b
|
||||
;(_a = screenRef.value) === null || _a === void 0
|
||||
? void 0
|
||||
: _a.scrollTo({
|
||||
left:
|
||||
(_b = screenRef.value) === null || _b === void 0
|
||||
? void 0
|
||||
: _b.scrollWidth,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
})
|
||||
}
|
||||
const showTop = toRef(state, 'showTop')
|
||||
const isScroll = toRef(state, 'isScroll')
|
||||
return {
|
||||
screenWrapper,
|
||||
styles,
|
||||
isScroll,
|
||||
showTop,
|
||||
screenRef,
|
||||
handleScrollLeft,
|
||||
handleScrollCenter,
|
||||
handleScrollRight,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.topTool {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import * as echarts from 'echarts/core'
|
||||
|
||||
import {
|
||||
BarChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
MapChart,
|
||||
PictorialBarChart,
|
||||
RadarChart,
|
||||
ScatterChart,
|
||||
} from 'echarts/charts'
|
||||
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
PolarComponent,
|
||||
AriaComponent,
|
||||
ParallelComponent,
|
||||
LegendComponent,
|
||||
RadarComponent,
|
||||
ToolboxComponent,
|
||||
DataZoomComponent,
|
||||
VisualMapComponent,
|
||||
TimelineComponent,
|
||||
CalendarComponent,
|
||||
GraphicComponent,
|
||||
} from 'echarts/components'
|
||||
|
||||
import { SVGRenderer } from 'echarts/renderers'
|
||||
|
||||
echarts.use([
|
||||
LegendComponent,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
PolarComponent,
|
||||
AriaComponent,
|
||||
ParallelComponent,
|
||||
BarChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
MapChart,
|
||||
RadarChart,
|
||||
SVGRenderer,
|
||||
PictorialBarChart,
|
||||
RadarComponent,
|
||||
ToolboxComponent,
|
||||
DataZoomComponent,
|
||||
VisualMapComponent,
|
||||
TimelineComponent,
|
||||
CalendarComponent,
|
||||
GraphicComponent,
|
||||
ScatterChart,
|
||||
])
|
||||
|
||||
export default echarts
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export function isFunction(val) {
|
||||
return typeof val === 'function'
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import { ref, computed, unref } from 'vue';
|
||||
import { useEventListener } from '/@/hooks/event/useEventListener';
|
||||
import { screenMap, sizeEnum, screenEnum } from '/@/enums/breakpointEnum';
|
||||
let globalScreenRef;
|
||||
let globalWidthRef;
|
||||
let globalRealWidthRef;
|
||||
export function useBreakpoint() {
|
||||
return {
|
||||
screenRef: computed(() => unref(globalScreenRef)),
|
||||
widthRef: globalWidthRef,
|
||||
screenEnum,
|
||||
realWidthRef: globalRealWidthRef,
|
||||
};
|
||||
}
|
||||
// Just call it once
|
||||
export function createBreakpointListen(fn) {
|
||||
const screenRef = ref(sizeEnum.XL);
|
||||
const realWidthRef = ref(window.innerWidth);
|
||||
function getWindowWidth() {
|
||||
const width = document.body.clientWidth;
|
||||
const xs = screenMap.get(sizeEnum.XS);
|
||||
const sm = screenMap.get(sizeEnum.SM);
|
||||
const md = screenMap.get(sizeEnum.MD);
|
||||
const lg = screenMap.get(sizeEnum.LG);
|
||||
const xl = screenMap.get(sizeEnum.XL);
|
||||
if (width < xs) {
|
||||
screenRef.value = sizeEnum.XS;
|
||||
}
|
||||
else if (width < sm) {
|
||||
screenRef.value = sizeEnum.SM;
|
||||
}
|
||||
else if (width < md) {
|
||||
screenRef.value = sizeEnum.MD;
|
||||
}
|
||||
else if (width < lg) {
|
||||
screenRef.value = sizeEnum.LG;
|
||||
}
|
||||
else if (width < xl) {
|
||||
screenRef.value = sizeEnum.XL;
|
||||
}
|
||||
else {
|
||||
screenRef.value = sizeEnum.XXL;
|
||||
}
|
||||
realWidthRef.value = width;
|
||||
}
|
||||
useEventListener({
|
||||
el: window,
|
||||
name: 'resize',
|
||||
listener: () => {
|
||||
getWindowWidth();
|
||||
resizeFn();
|
||||
},
|
||||
// wait: 100,
|
||||
});
|
||||
getWindowWidth();
|
||||
globalScreenRef = computed(() => unref(screenRef));
|
||||
globalWidthRef = computed(() => screenMap.get(unref(screenRef)));
|
||||
globalRealWidthRef = computed(() => unref(realWidthRef));
|
||||
function resizeFn() {
|
||||
fn === null || fn === void 0 ? void 0 : fn({
|
||||
screen: globalScreenRef,
|
||||
width: globalWidthRef,
|
||||
realWidth: globalRealWidthRef,
|
||||
screenEnum,
|
||||
screenMap,
|
||||
sizeEnum,
|
||||
});
|
||||
}
|
||||
resizeFn();
|
||||
return {
|
||||
screenRef: globalScreenRef,
|
||||
screenEnum,
|
||||
widthRef: globalWidthRef,
|
||||
realWidthRef: globalRealWidthRef,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
import { useTimeoutFn } from './useTimeout.js';
|
||||
import { tryOnUnmounted } from '@vueuse/core';
|
||||
import { unref, nextTick, watch, computed, ref } from 'vue';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { useEventListener } from './useEventListener.js';
|
||||
import echarts from './echarts.js';
|
||||
export function useECharts(elRef, theme = 'default') {
|
||||
const getDarkMode = computed(() => {
|
||||
return 'light'
|
||||
});
|
||||
let chartInstance = null;
|
||||
let resizeFn = resize;
|
||||
const cacheOptions = ref({});
|
||||
let removeResizeFn = () => { };
|
||||
resizeFn = useDebounceFn(resize, 200);
|
||||
const getOptions = computed(() => {
|
||||
if (getDarkMode.value !== 'dark') {
|
||||
return cacheOptions.value;
|
||||
}
|
||||
return Object.assign({ backgroundColor: 'transparent' }, cacheOptions.value);
|
||||
});
|
||||
function initCharts(t = theme) {
|
||||
const el = unref(elRef);
|
||||
if (!el || !unref(el)) {
|
||||
return;
|
||||
}
|
||||
chartInstance = echarts.init(el, t);
|
||||
const { removeEvent } = useEventListener({
|
||||
el: window,
|
||||
name: 'resize',
|
||||
listener: resizeFn,
|
||||
});
|
||||
removeResizeFn = removeEvent;
|
||||
// useTimeoutFn(() => {
|
||||
// resizeFn();
|
||||
// }, 30);
|
||||
}
|
||||
function setOptions(options, clear = true) {
|
||||
var _a;
|
||||
cacheOptions.value = options;
|
||||
if (((_a = unref(elRef)) === null || _a === void 0 ? void 0 : _a.offsetHeight) === 0) {
|
||||
useTimeoutFn(() => {
|
||||
setOptions(unref(getOptions));
|
||||
}, 30);
|
||||
return;
|
||||
}
|
||||
nextTick(() => {
|
||||
useTimeoutFn(() => {
|
||||
if (!chartInstance) {
|
||||
initCharts(getDarkMode.value);
|
||||
if (!chartInstance)
|
||||
return;
|
||||
}
|
||||
clear && (chartInstance === null || chartInstance === void 0 ? void 0 : chartInstance.clear());
|
||||
chartInstance === null || chartInstance === void 0 ? void 0 : chartInstance.setOption(unref(getOptions));
|
||||
}, 30);
|
||||
});
|
||||
}
|
||||
function resize() {
|
||||
chartInstance === null || chartInstance === void 0 ? void 0 : chartInstance.resize({
|
||||
animation: {
|
||||
duration: 300,
|
||||
easing: 'quadraticIn',
|
||||
},
|
||||
});
|
||||
}
|
||||
watch(() => getDarkMode.value, (theme) => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
initCharts(theme);
|
||||
setOptions(cacheOptions.value);
|
||||
}
|
||||
});
|
||||
tryOnUnmounted(() => {
|
||||
if (!chartInstance)
|
||||
return;
|
||||
removeResizeFn();
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
});
|
||||
function getInstance() {
|
||||
if (!chartInstance) {
|
||||
initCharts(getDarkMode.value);
|
||||
}
|
||||
return chartInstance;
|
||||
}
|
||||
return {
|
||||
setOptions,
|
||||
resize,
|
||||
echarts,
|
||||
getInstance,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { ref, watch, unref } from 'vue';
|
||||
import { useThrottleFn, useDebounceFn } from '@vueuse/core';
|
||||
export function useEventListener({ el = window, name, listener, options, autoRemove = true, isDebounce = true, wait = 80, }) {
|
||||
/* eslint-disable-next-line */
|
||||
let remove = () => { };
|
||||
const isAddRef = ref(false);
|
||||
if (el) {
|
||||
const element = ref(el);
|
||||
const handler = isDebounce ? useDebounceFn(listener, wait) : useThrottleFn(listener, wait);
|
||||
const realHandler = wait ? handler : listener;
|
||||
const removeEventListener = (e) => {
|
||||
isAddRef.value = true;
|
||||
e.removeEventListener(name, realHandler, options);
|
||||
};
|
||||
const addEventListener = (e) => e.addEventListener(name, realHandler, options);
|
||||
const removeWatch = watch(element, (v, _ov, cleanUp) => {
|
||||
if (v) {
|
||||
!unref(isAddRef) && addEventListener(v);
|
||||
cleanUp(() => {
|
||||
autoRemove && removeEventListener(v);
|
||||
});
|
||||
}
|
||||
}, { immediate: true });
|
||||
remove = () => {
|
||||
removeEventListener(element.value);
|
||||
removeWatch();
|
||||
};
|
||||
}
|
||||
return { removeEvent: remove };
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { ref, watch } from 'vue'
|
||||
import { tryOnUnmounted } from '@vueuse/core'
|
||||
import { isFunction } from './is.js'
|
||||
|
||||
export function useTimeoutFn(handle, wait, native = false) {
|
||||
if (!isFunction(handle)) {
|
||||
throw new Error('handle is not Function!')
|
||||
}
|
||||
|
||||
const { readyRef, stop, start } = useTimeoutRef(wait)
|
||||
if (native) {
|
||||
handle()
|
||||
} else {
|
||||
watch(
|
||||
readyRef,
|
||||
(maturity) => {
|
||||
maturity && handle()
|
||||
},
|
||||
{ immediate: false },
|
||||
)
|
||||
}
|
||||
return { readyRef, stop, start }
|
||||
}
|
||||
|
||||
export function useTimeoutRef(wait) {
|
||||
const readyRef = ref(false)
|
||||
|
||||
let timer
|
||||
function stop() {
|
||||
readyRef.value = false
|
||||
timer && window.clearTimeout(timer)
|
||||
}
|
||||
function start() {
|
||||
stop()
|
||||
timer = setTimeout(() => {
|
||||
readyRef.value = true
|
||||
}, wait)
|
||||
}
|
||||
|
||||
start()
|
||||
|
||||
tryOnUnmounted(stop)
|
||||
|
||||
return { readyRef, stop, start }
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { createApp } from 'vue'
|
||||
import Antd from 'ant-design-vue';
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import 'virtual:uno.css'
|
||||
import 'ant-design-vue/dist/reset.css';
|
||||
|
||||
|
||||
async function bootstrap() {
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
app.use(Antd)
|
||||
app.mount('#app')
|
||||
}
|
||||
bootstrap()
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
//路由初始指向
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('../views/home/index.vue'),
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
body {
|
||||
margin: 0;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.box-shadow {
|
||||
background: #ffffff;
|
||||
box-shadow: 0px 0px 16px 0px rgba(0, 0, 0, 0.04);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
|
||||
import axios from 'axios'
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: 200000,
|
||||
withCredentials: false
|
||||
})
|
||||
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
|
||||
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
service.interceptors.response.use(
|
||||
|
||||
response => {
|
||||
|
||||
const res = response
|
||||
const config = response.config
|
||||
if (res.status >=400) {
|
||||
|
||||
return Promise.reject(res.msg || 'Error')
|
||||
} else {
|
||||
|
||||
const { isTransformResponse } = config
|
||||
if (isTransformResponse) {
|
||||
return res
|
||||
}
|
||||
return res.data
|
||||
}
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default service
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
<template>
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="flex justify-between h-10 items-center px-4">
|
||||
<div class="text-[#999] text-sm">年度目标</div>
|
||||
<div class="w-30">
|
||||
<a-select
|
||||
ref="select"
|
||||
v-model:value="day"
|
||||
class="w-full"
|
||||
@change="onChange"
|
||||
>
|
||||
<a-select-option :value="item" v-for="item in years" :key="item">{{
|
||||
item
|
||||
}}</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1" ref="yRef"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useECharts } from '@/components/echarts/useECharts.js'
|
||||
import echarts from '@/components/echarts/echarts.js'
|
||||
import http from '@/utils/request'
|
||||
const day = ref(null)
|
||||
const yRef = ref(null)
|
||||
const { setOptions } = useECharts(yRef)
|
||||
const years = ref([])
|
||||
const datas = ref({})
|
||||
|
||||
const createFiveYears = () => {
|
||||
const now = new Date()
|
||||
for (let i = 0; i < 5; i++) {
|
||||
years.value.push(now.getFullYear() - i)
|
||||
}
|
||||
day.value = years.value[0]
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
createFiveYears()
|
||||
getData()
|
||||
})
|
||||
|
||||
const onChange = () => {
|
||||
getData()
|
||||
}
|
||||
|
||||
const getData = async () => {
|
||||
const resData = await http.get('/admin-api/api/cockpit/yearly-goals', {
|
||||
params: {
|
||||
year: day.value,
|
||||
},
|
||||
})
|
||||
datas.value = resData
|
||||
chatInit()
|
||||
}
|
||||
|
||||
const chatInit = () => {
|
||||
const { expected_performance, actual_performance } = datas.value
|
||||
let rate = (actual_performance / expected_performance).toFixed(2)
|
||||
console.log(rate);
|
||||
if (rate == 'NaN') {
|
||||
rate = 0
|
||||
}
|
||||
const rateValue = (rate * 100).toFixed(0)
|
||||
setOptions({
|
||||
title: {
|
||||
text: `${rateValue}%`,
|
||||
x: 'center',
|
||||
y: 'center',
|
||||
textStyle: {
|
||||
fontWeight: 'normal',
|
||||
color: '#000',
|
||||
},
|
||||
},
|
||||
color: ['#AAAFC8'],
|
||||
|
||||
series: [
|
||||
{
|
||||
name: 'Line1',
|
||||
type: 'pie',
|
||||
clockWise: true,
|
||||
radius: ['80%', '70%'],
|
||||
itemStyle: {
|
||||
normal: {
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
hoverAnimation: false,
|
||||
data: [
|
||||
{
|
||||
value: rateValue,
|
||||
name: '01',
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: '#0E7CE2',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '02',
|
||||
value: 100 - rateValue,
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: '#AAAFC8',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="flex justify-between p-4">
|
||||
<div class="text-[#999] text-sm">数据概览</div>
|
||||
</div>
|
||||
<div class="flex-1 grid grid-cols-2 py-5">
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-medium">门店总数</div>
|
||||
<div class="my-5">
|
||||
<CountTo class="text-2xl" :endVal="deta?.stores_count || 0" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-medium">员工总数</div>
|
||||
<div class="my-5">
|
||||
<CountTo class="text-2xl" :endVal="deta?.employees_count || 0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import CountTo from '@/components/CountTo/index.vue'
|
||||
import http from '@/utils/request'
|
||||
import { onMounted, ref } from 'vue'
|
||||
const deta = ref({})
|
||||
|
||||
onMounted(() => {
|
||||
getData()
|
||||
})
|
||||
|
||||
const getData = async () => {
|
||||
const resData = await http.get('/admin-api/api/cockpit/basic')
|
||||
deta.value = resData
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
<template>
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="flex justify-between p-4">
|
||||
<div class="text-[#999] text-sm">彩种销售走势</div>
|
||||
<div class="w-50">
|
||||
<a-select ref="select" v-model:value="day" class="w-full" @change="onChange">
|
||||
<a-select-option
|
||||
v-for="item in dayList"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>{{ item.label }}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1" ref="trendRef"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useECharts } from '@/components/echarts/useECharts.js'
|
||||
import echarts from '@/components/echarts/echarts.js'
|
||||
import http from '@/utils/request'
|
||||
|
||||
const datas = ref([])
|
||||
const day = ref('7days')
|
||||
const dayList = [
|
||||
{
|
||||
value: '7days',
|
||||
label: '近7天',
|
||||
},
|
||||
{
|
||||
value: '30days',
|
||||
label: '近30天',
|
||||
},
|
||||
{
|
||||
value: '180days',
|
||||
label: '进半年',
|
||||
},
|
||||
{
|
||||
value: '365days',
|
||||
label: '近一年',
|
||||
},
|
||||
]
|
||||
const trendRef = ref(null)
|
||||
const { setOptions } = useECharts(trendRef)
|
||||
|
||||
onMounted(() => {
|
||||
getData()
|
||||
})
|
||||
|
||||
const getData = async () => {
|
||||
const resData = await http.get('/admin-api/api/cockpit/lottery-sales-trend', {
|
||||
params: {
|
||||
last: day.value,
|
||||
},
|
||||
})
|
||||
datas.value = resData.data
|
||||
chatInit()
|
||||
}
|
||||
const chatInit = () => {
|
||||
const groupedData = {}
|
||||
|
||||
datas.value.forEach((item) => {
|
||||
item.data.forEach((entry) => {
|
||||
const { name, sales } = entry
|
||||
if (!groupedData[name]) {
|
||||
groupedData[name] = []
|
||||
}
|
||||
groupedData[name].push({ date: item.date, sales })
|
||||
})
|
||||
})
|
||||
|
||||
const xAxisData = []
|
||||
const seriesData = []
|
||||
Object.keys(groupedData).forEach((name) => {
|
||||
xAxisData.push(name)
|
||||
seriesData.push({
|
||||
name: name,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 5,
|
||||
showSymbol: false,
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
// normal: {
|
||||
// // color: new echarts.graphic.LinearGradient(
|
||||
// // 0,
|
||||
// // 0,
|
||||
// // 0,
|
||||
// // 1,
|
||||
// // [
|
||||
// // {
|
||||
// // offset: 0,
|
||||
// // color: 'rgba(25,163,223,.3)',
|
||||
// // },
|
||||
// // {
|
||||
// // offset: 1,
|
||||
// // color: 'rgba(25,163,223, 0)',
|
||||
// // },
|
||||
// // ],
|
||||
// // false
|
||||
// // ),
|
||||
// opacity: 0.1,
|
||||
// },
|
||||
},
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
},
|
||||
data: groupedData[name].map((item) => item.sales),
|
||||
})
|
||||
})
|
||||
|
||||
setOptions({
|
||||
title: {
|
||||
textStyle: {
|
||||
fontWeight: 'normal',
|
||||
fontSize: 16,
|
||||
color: '#F1F1F3',
|
||||
},
|
||||
left: '6%',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: '#57617B',
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
icon: 'rect',
|
||||
itemWidth: 14,
|
||||
itemHeight: 5,
|
||||
itemGap: 13,
|
||||
data: xAxisData,
|
||||
right: '4%',
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: '#292f39',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#999',
|
||||
},
|
||||
},
|
||||
data: datas.value.map((e) => e.date ?? e.month),
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#57617B',
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
margin: 10,
|
||||
textStyle: {
|
||||
fontSize: 14,
|
||||
},
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#eee',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
series: seriesData,
|
||||
})
|
||||
}
|
||||
|
||||
const onChange = (e) => {
|
||||
getData()
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
<template>
|
||||
<div class="h-full" ref="mapRef">12</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useECharts } from '@/components/echarts/useECharts.js'
|
||||
import chinaMap from './china.json'
|
||||
import { registerMap } from 'echarts'
|
||||
import http from '@/utils/request'
|
||||
|
||||
const mapRef = ref(null)
|
||||
const { setOptions } = useECharts(mapRef)
|
||||
const data = ref([])
|
||||
|
||||
registerMap('china', { geoJSON: chinaMap })
|
||||
|
||||
onMounted(() => {
|
||||
getData()
|
||||
})
|
||||
|
||||
const getData = async () => {
|
||||
const resData = await http.get(
|
||||
'/admin-api/api/cockpit/store-number-distribution'
|
||||
)
|
||||
data.value = resData
|
||||
chatInit()
|
||||
}
|
||||
|
||||
const convertData = (data) => {
|
||||
var geoCoordMap = []
|
||||
chinaMap.features.forEach((item) => {
|
||||
geoCoordMap[item.properties.name] = item.properties.center
|
||||
})
|
||||
var res = []
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var geoCoord = geoCoordMap[data[i].name]
|
||||
if (geoCoord && data[i].stores_count > 0) {
|
||||
res.push({
|
||||
name: data[i].name,
|
||||
value: geoCoord.concat(data[i].stores_count),
|
||||
})
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
const chatInit = () => {
|
||||
var max = 480,
|
||||
min = 14 // todo
|
||||
var maxSize4Pin = 100,
|
||||
minSize4Pin = 30
|
||||
|
||||
setOptions({
|
||||
baseOption: {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: function (params) {
|
||||
if (typeof params.value[2] == 'undefined') {
|
||||
return params.name + ' : ' + params.value
|
||||
} else {
|
||||
return params.name + ' : ' + params.value[2]
|
||||
}
|
||||
},
|
||||
},
|
||||
geo: {
|
||||
map: 'china',
|
||||
zoom: 1.5,
|
||||
roam: false,
|
||||
left: '20%',
|
||||
top: '23%',
|
||||
label: {
|
||||
show: true,
|
||||
color: 'rgb(249, 249, 249)', //省份标签字体颜色
|
||||
},
|
||||
itemStyle: {
|
||||
areaColor: '#24CFF4',
|
||||
borderColor: '#53D9FF',
|
||||
borderWidth: 1.3,
|
||||
shadowBlur: 15,
|
||||
shadowColor: 'rgb(58,115,192)',
|
||||
shadowOffsetX: 7,
|
||||
shadowOffsetY: 6,
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
areaColor: '#8dd7fc',
|
||||
borderWidth: 1.6,
|
||||
shadowBlur: 25,
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
color: '#f75a00',
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'map',
|
||||
map: 'china',
|
||||
geoIndex: 0,
|
||||
aspectScale: 0.75, //长宽比
|
||||
showLegendSymbol: false, // 存在legend时显示
|
||||
label: {
|
||||
normal: {
|
||||
show: false,
|
||||
},
|
||||
emphasis: {
|
||||
show: false,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
},
|
||||
roam: true,
|
||||
itemStyle: {
|
||||
normal: {
|
||||
areaColor: '#031525',
|
||||
borderColor: '#FFFFFF',
|
||||
},
|
||||
emphasis: {
|
||||
areaColor: '#2B91B7',
|
||||
},
|
||||
},
|
||||
animation: false,
|
||||
data: data.value.map((e) => {
|
||||
return {
|
||||
name: e.name,
|
||||
value: e.stores_count,
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: '点',
|
||||
type: 'scatter',
|
||||
coordinateSystem: 'geo',
|
||||
symbol: 'pin',
|
||||
symbolSize: function (val) {
|
||||
var a = (maxSize4Pin - minSize4Pin) / (max - min)
|
||||
var b = minSize4Pin - a * min
|
||||
b = maxSize4Pin - a * max
|
||||
return a * val[2] + b
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
},
|
||||
formatter(value) {
|
||||
return value.data.value[2]
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#F62157',
|
||||
},
|
||||
zlevel: 6,
|
||||
data: convertData(data.value),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
<template>
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="flex justify-between items-center p-4">
|
||||
<div class="text-[#999] text-sm">数据排行</div>
|
||||
<div class="w-30">
|
||||
<a-select
|
||||
ref="select"
|
||||
v-model:value="day"
|
||||
class="w-full"
|
||||
@change="onChange"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in dayList"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>{{ item.label }}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1" ref="rankRef"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useECharts } from '@/components/echarts/useECharts.js'
|
||||
import echarts from '@/components/echarts/echarts.js'
|
||||
import http from '@/utils/request'
|
||||
|
||||
const day = ref('365days')
|
||||
const dayList = [
|
||||
{
|
||||
value: '7days',
|
||||
label: '近7天',
|
||||
},
|
||||
{
|
||||
value: '30days',
|
||||
label: '近30天',
|
||||
},
|
||||
{
|
||||
value: '180days',
|
||||
label: '进半年',
|
||||
},
|
||||
{
|
||||
value: '365days',
|
||||
label: '近一年',
|
||||
},
|
||||
]
|
||||
const rankRef = ref(null)
|
||||
const { setOptions } = useECharts(rankRef)
|
||||
|
||||
let dataList = ref([])
|
||||
|
||||
onMounted(() => {
|
||||
getData()
|
||||
})
|
||||
|
||||
const onChange = () => {
|
||||
getData()
|
||||
}
|
||||
|
||||
const getData = async () => {
|
||||
const resData = await http.get('/admin-api/api/cockpit/store-sales-ranking', {
|
||||
params: {
|
||||
last: day.value,
|
||||
},
|
||||
})
|
||||
const arr = resData.map((e, i) => {
|
||||
return [i + 1, e.store.title, e.sales]
|
||||
})
|
||||
dataList.value = arr.filter((e) => e[2] > 0)
|
||||
chatInit()
|
||||
}
|
||||
|
||||
const chatInit = () => {
|
||||
__SetOption(dataList.value)
|
||||
}
|
||||
|
||||
let barWidth = 15
|
||||
let nameFontColor = '#000'
|
||||
let nameFontSize = 12
|
||||
let namePosition = [0, -15]
|
||||
let valueFontColor = '#FFFFFF'
|
||||
let valueFontSize = 12
|
||||
let indexNum = 0
|
||||
|
||||
function __getColorValue(name, val, index) {
|
||||
return {
|
||||
name: name,
|
||||
color: '#000',
|
||||
value: val,
|
||||
itemStyle: {
|
||||
normal: {
|
||||
show: true,
|
||||
color: '#0E7CE2',
|
||||
barBorderRadius: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function __SetOption(data) {
|
||||
let lists = []
|
||||
let values = []
|
||||
data.forEach((item, index) => {
|
||||
lists.push(item[1])
|
||||
values.push(__getColorValue(item[1], item[2], index))
|
||||
})
|
||||
|
||||
let option = {
|
||||
grid: {
|
||||
top: 0,
|
||||
left: '20px',
|
||||
right: '3%',
|
||||
bottom: 0,
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
inverse: true,
|
||||
axisTick: { show: false },
|
||||
axisLine: { show: false },
|
||||
axisLabel: { show: false, inside: false },
|
||||
data: lists,
|
||||
},
|
||||
],
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
axisTick: { show: false },
|
||||
axisLine: { show: false },
|
||||
splitLine: { show: false },
|
||||
axisLabel: { show: false },
|
||||
},
|
||||
series: [
|
||||
// {
|
||||
// show: true,
|
||||
// type: 'bar',
|
||||
// barGap: '-100%',
|
||||
// barWidth: barWidth,
|
||||
// itemStyle: {
|
||||
// normal: {
|
||||
// color: 'rgba(102, 102, 102,0.5)',
|
||||
// },
|
||||
// },
|
||||
// z: 1,
|
||||
// data: values.map(()=>1),
|
||||
// },
|
||||
{
|
||||
name: '排行',
|
||||
type: 'bar',
|
||||
barWidth: barWidth,
|
||||
data: values,
|
||||
animationDuration: 1500,
|
||||
zlevel: 2,
|
||||
|
||||
label: {
|
||||
normal: {
|
||||
show: true,
|
||||
color: nameFontColor,
|
||||
show: true,
|
||||
position: 'inside',
|
||||
position: namePosition,
|
||||
textStyle: {
|
||||
fontSize: nameFontSize,
|
||||
},
|
||||
formatter: function (data) {
|
||||
return `${data.data.name}`
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
setOptions(option)
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
<template>
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="flex justify-between items-center p-4">
|
||||
<div class="text-[#999] text-sm">销售走势</div>
|
||||
<div class="w-30">
|
||||
<a-select
|
||||
ref="select"
|
||||
v-model:value="day"
|
||||
class="w-full"
|
||||
@change="onChange"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in dayList"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>{{ item.label }}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1" ref="trendRef1"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useECharts } from '@/components/echarts/useECharts.js'
|
||||
import echarts from '@/components/echarts/echarts.js'
|
||||
import http from '@/utils/request'
|
||||
|
||||
const datas = ref([])
|
||||
const day = ref('7days')
|
||||
const dayList = [
|
||||
{
|
||||
value: '7days',
|
||||
label: '近7天',
|
||||
},
|
||||
{
|
||||
value: '30days',
|
||||
label: '近30天',
|
||||
},
|
||||
{
|
||||
value: '180days',
|
||||
label: '进半年',
|
||||
},
|
||||
{
|
||||
value: '365days',
|
||||
label: '近一年',
|
||||
},
|
||||
]
|
||||
const trendRef1 = ref(null)
|
||||
const { setOptions } = useECharts(trendRef1)
|
||||
|
||||
onMounted(() => {
|
||||
getData()
|
||||
})
|
||||
|
||||
const getData = async () => {
|
||||
const resData = await http.get('/admin-api/api/cockpit/sales-trend', {
|
||||
params: {
|
||||
last: day.value,
|
||||
},
|
||||
})
|
||||
datas.value = resData
|
||||
|
||||
chatInit()
|
||||
}
|
||||
const chatInit = () => {
|
||||
const xAxisData = []
|
||||
const sales = []
|
||||
datas.value.forEach((item) => {
|
||||
xAxisData.push(item.date ?? item.month)
|
||||
sales.push(item.sales)
|
||||
})
|
||||
|
||||
const seriesData = []
|
||||
seriesData.push({
|
||||
name: '销售',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 5,
|
||||
showSymbol: false,
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
},
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
},
|
||||
data: sales,
|
||||
})
|
||||
|
||||
setOptions({
|
||||
title: {
|
||||
textStyle: {
|
||||
fontWeight: 'normal',
|
||||
fontSize: 16,
|
||||
color: '#F1F1F3',
|
||||
},
|
||||
left: '6%',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: '#57617B',
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
icon: 'rect',
|
||||
itemWidth: 14,
|
||||
itemHeight: 5,
|
||||
itemGap: 13,
|
||||
// data: xAxisData,
|
||||
right: '4%',
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: '#292f39',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#999',
|
||||
},
|
||||
},
|
||||
data: xAxisData,
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#57617B',
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
margin: 10,
|
||||
textStyle: {
|
||||
fontSize: 14,
|
||||
},
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#eee',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
series: seriesData,
|
||||
})
|
||||
}
|
||||
|
||||
const onChange = (e) => {
|
||||
getData()
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="flex justify-between p-4">
|
||||
<div class="text-[#999] text-sm">门店分类</div>
|
||||
</div>
|
||||
<div class="flex-1" ref="sortRef"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useECharts } from '@/components/echarts/useECharts.js'
|
||||
import echarts from '@/components/echarts/echarts.js'
|
||||
import http from '@/utils/request'
|
||||
|
||||
const sortRef = ref(null)
|
||||
const datas = ref([])
|
||||
const { setOptions } = useECharts(sortRef)
|
||||
|
||||
onMounted(() => {
|
||||
getData()
|
||||
})
|
||||
|
||||
const getData = async () => {
|
||||
const resData = await http.get('/admin-api/api/cockpit/store-category')
|
||||
datas.value = resData
|
||||
chatInit()
|
||||
}
|
||||
|
||||
const chatInit = () => {
|
||||
setOptions({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b} : {c} ({d}%)',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '门店',
|
||||
type: 'pie',
|
||||
radius: '90%',
|
||||
center: ['50%', '50%'],
|
||||
clockwise: false,
|
||||
data: datas.value.map(e=>{
|
||||
return {
|
||||
name: e.name,
|
||||
value: e.stores_count
|
||||
}
|
||||
}),
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
itemStyle: {
|
||||
normal: {
|
||||
|
||||
borderColor: '#ffffff',
|
||||
},
|
||||
emphasis: {
|
||||
borderWidth: 0,
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<div class="p-5 flex flex-col">
|
||||
<div class="">驾驶舱</div>
|
||||
<div class="flex flex-1 space-x-4">
|
||||
<div class="w-6/10 space-y-4" v-if="true">
|
||||
<div class="h-150 box-shadow">
|
||||
<Map />
|
||||
</div>
|
||||
<div class="h-75 box-shadow">
|
||||
<LotteryTrends />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 space-y-4">
|
||||
<div class="box-shadow">
|
||||
<Count />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-x-4">
|
||||
<div class="space-y-4">
|
||||
<div class="box-shadow h-56">
|
||||
<Sort />
|
||||
</div>
|
||||
<div class="box-shadow h-56">
|
||||
<SalesTrend />
|
||||
</div>
|
||||
<div class="box-shadow h-56">
|
||||
<AnnualTarget />
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-shadow">
|
||||
<Rank />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import Map from './components/map.vue'
|
||||
import LotteryTrends from './components/lottery-trends.vue'
|
||||
import Count from './components/count.vue'
|
||||
import Sort from "./components/sort.vue"
|
||||
import AnnualTarget from './components/annual-target.vue'
|
||||
import Rank from './components/rank.vue'
|
||||
import SalesTrend from './components/sales-trend.vue'
|
||||
</script>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// uno.config.ts
|
||||
import {
|
||||
defineConfig,
|
||||
presetAttributify,
|
||||
presetIcons,
|
||||
presetTypography,
|
||||
presetUno,
|
||||
presetWebFonts,
|
||||
transformerDirectives,
|
||||
transformerVariantGroup
|
||||
} from 'unocss'
|
||||
|
||||
export default defineConfig({
|
||||
shortcuts: [
|
||||
// ...
|
||||
],
|
||||
theme: {
|
||||
colors: {
|
||||
// ...
|
||||
}
|
||||
},
|
||||
presets: [
|
||||
presetUno(),
|
||||
presetAttributify(),
|
||||
presetIcons(),
|
||||
presetTypography(),
|
||||
presetWebFonts({
|
||||
fonts: {
|
||||
// ...
|
||||
}
|
||||
})
|
||||
],
|
||||
transformers: [transformerDirectives(), transformerVariantGroup()]
|
||||
})
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import UnoCSS from 'unocss/vite'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), UnoCSS()],
|
||||
server:{
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://store-manage.hmily.club',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
})
|
||||
Loading…
Reference in New Issue