main
ihzero 2024-05-08 23:54:04 +08:00
commit 24b28d428c
37 changed files with 8550 additions and 0 deletions

6
.env.development 100644
View File

@ -0,0 +1,6 @@
VITE_API_BASE_URL = '/api'
VITE_NFT_BASE_URL = 'http://store-manage.hmily.club'

6
.env.production 100644
View File

@ -0,0 +1,6 @@
VITE_API_BASE_URL = '/api'
VITE_NFT_BASE_URL = 'http://store-manage.hmily.club'

24
.gitignore vendored 100644
View File

@ -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?

3
.vscode/extensions.json vendored 100644
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

7
README.md 100644
View File

@ -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

13
index.html 100644
View File

@ -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>

4815
package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

24
package.json 100644
View File

@ -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"
}
}

1
public/vite.svg 100644
View File

@ -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

8
src/App.vue 100644
View File

@ -0,0 +1,8 @@
<template>
<div>
<router-view></router-view>
</div>
</template>
<script setup>
import { RouterView } from 'vue-router'
</script>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,3 @@
import ScaleScreen from './src/ScaleScreen.vue'
export default ScaleScreen

View File

@ -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>

View File

@ -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

View File

@ -0,0 +1,3 @@
export function isFunction(val) {
return typeof val === 'function'
}

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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 };
}

View File

@ -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 }
}

16
src/main.js 100644
View File

@ -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()

View File

@ -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

12
src/style.css 100644
View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

34
uno.config.js 100644
View File

@ -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()]
})

24
vite.config.js 100644
View File

@ -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))
},
},
})

1748
yarn.lock 100644

File diff suppressed because it is too large Load Diff