Commit b5c57ac7 authored by 郭铭瑶's avatar 郭铭瑶 🤘

Init Project

parents
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'prettier/@typescript-eslint',
'plugin:prettier/recommended',
],
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 2020,
parser: '@typescript-eslint/parser',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
plugins: ['vue', '@typescript-eslint'],
rules: {
'vue/no-multiple-template-root': 'off',
'vue/max-attributes-per-line': 'off',
'vue/html-self-closing': 'off',
'vue/singleline-html-element-content-newline': 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, // 生产环境禁止debugger
'no-console': process.env.NODE_ENV === 'production' ? 2 : 0, // 生产环境禁止console
'no-alert': process.env.NODE_ENV === 'production' ? 2 : 0, // 生产环境禁止alert
'no-shadow-restricted-names': 2, // 禁用关键字及保留字等
'dot-notation': 1, // 尽可能使用 . 来访问对象属性
'no-multi-spaces': 1, // 禁止使用多个空格
'brace-style': 1, // 大括号风格 - one true brace style
'no-var': 1, // 禁用var声明
'no-new-object': 1, // 禁止new Object
'no-array-constructor': 1, // 禁止new Array
'prefer-const': 1, // 要求使用 const 声明那些声明后不再被修改的变量
'prefer-destructuring': 1, // 优先使用数组和对象解构
'no-param-reassign': 1, // 禁止在函数中对函数参数重新赋值
'no-extra-semi': 1, // 禁用不必要的分号
// "no-unused-vars": 1, // 禁止已声明但未使用的变量
// "indent": [1, 2], // 使用2个空格缩进
'no-multiple-empty-lines': [1, { max: 1 }], // 禁止连续出现2个及以上空行
'default-case': 1, // 要求switch语句必须有default分支
'key-spacing': [1, { beforeColon: false, afterColon: true }], // 冒号前不要空格,后需要空格
'comma-spacing': [1, { before: false, after: true }], // 逗号前不要空格,后需要空格
'arrow-spacing': [1, { before: true, after: true }], // 箭头函数中的箭头前后需要留空格
quotes: [1, 'single'], // 字符串使用单引号
semi: [1, 'never'], // 禁止使用分号
},
}
node_modules
.DS_Store
dist
dist-ssr
*.local
module.exports = {
tabWidth: 2,
useTabs: false,
singleQuote: true,
semi: false,
}
\ No newline at end of file
{
"prettier.printWidth": 200,
"prettier.singleQuote": true,
"prettier.semi": false,
// vetur设置
"vetur.format.defaultFormatter.ts": "prettier",
"vetur.format.defaultFormatter.js": "prettier",
"vetur.format.defaultFormatter.less": "prettier",
"vetur.experimental.templateInterpolationService": true,
"vetur.validation.interpolation": false,
"vetur.validation.templateProps": true,
"vetur.format.defaultFormatterOptions": {
"prettyhtml": {
"printWidth": 100,
"singleQuote": false,
"wrapAttributes": false,
"sortAttributes": false
},
},
"eslint.options": {
"extensions": [
".js",
".vue",
".ts",
".tsx"
]
},
"editor.formatOnSave": true,
}
\ No newline at end of file
# 新版南京东路街道大屏
使用新大屏框架试验
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>南京东路街道</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
\ No newline at end of file
{
"name": "east-nanjing-new",
"version": "1.0.0",
"author": "yaominguo <missgmy@yahoo.com>",
"private": true,
"scripts": {
"dev": "vite",
"start": "npm run dev",
"clean": "rimraf ./dist",
"build": "npm run clean && vuedx-typecheck . && vite build"
},
"dependencies": {
"animate.css": "^4.1.1",
"axios": "^0.21.1",
"countup.js": "^2.0.7",
"echarts": "^5.0.1",
"moment": "^2.24.0",
"normalize.css": "^8.0.1",
"qs": "^6.9.6",
"vue": "^3.0.5",
"vuex": "^4.0.0-rc.2"
},
"devDependencies": {
"@types/node": "^14.14.22",
"@types/qs": "^6.9.5",
"@typescript-eslint/eslint-plugin": "^4.12.0",
"@typescript-eslint/parser": "^4.12.0",
"@vitejs/plugin-vue": "^1.1.4",
"@vitejs/plugin-vue-jsx": "^1.0.2",
"@vue/compiler-sfc": "^3.0.5",
"@vuedx/typecheck": "^0.4.1",
"@vuedx/typescript-plugin-vue": "^0.4.1",
"eslint": "^7.17.0",
"eslint-config-prettier": "^7.1.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-vue": "^7.4.1",
"prettier": "^2.2.1",
"stylus": "^0.54.8",
"typescript": "^4.1.3",
"vite": "^2.0.1",
"vue-eslint-parser": "^7.4.1"
}
}
<template>
<div></div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'App',
})
</script>
<style lang="stylus">
@font-face
font-family DIN
src url(./assets/font/DIN-Medium.otf)
@font-face
font-family Pangmenzhengdao
src url(./assets/font/pangmenzhengdao.ttf)
html, body
background rgba(0,0,0,0.5)
font-family DIN, Avenir, Helvetica, Arial, sans-serif
-webkit-font-smoothing antialiased
-moz-osx-font-smoothing grayscale
width 100%
height 100%
user-select none
font-size 16vh
line-height 1.5
color #fff
#app
width 100%
height 100%
overflow hidden
font-size .1rem
background url('/src/assets/images/background.jpg') center/cover no-repeat
/* 设置滚动条的样式 */
::-webkit-scrollbar {
width: .05rem;
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
background:rgba(91, 213, 255, 0.5)
-webkit-box-shadow:inset006pxrgba(0,0,0,0.5);
}
::-webkit-scrollbar-thumb:window-inactive {
background:rgba(91, 213, 255, 0.5)
}
</style>
let BASE_URL = ''
switch (process.env.NODE_ENV) {
case 'production':
BASE_URL = ''
break
default:
BASE_URL = ''
}
export default {
BASE_URL,
TEST_URL: 'https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js',
}
import axios, { AxiosRequestConfig, AxiosInstance, AxiosPromise } from 'axios'
import qs from 'qs'
import api from './api'
import store from '@/store'
const Axios = axios.create({
baseURL: api.BASE_URL,
timeout: 15000,
})
Axios.interceptors.request.use(
(config) => {
// 添加token
// config.headers.Authorization = 'token'
return config
},
(error) => {
return Promise.reject(error)
}
)
Axios.interceptors.response.use(
(response) => {
// TODO 返回的数据status判断错误操作等……
store.commit('SET_LOADING', false)
console.log('res', response.data)
return response.data
},
(error) => {
store.commit('SET_LOADING', false)
return Promise.reject(error)
}
)
interface ParamsProp {
[propName: string]: any
}
interface RequestOptions {
method?:
| 'GET'
| 'get'
| 'delete'
| 'DELETE'
| 'head'
| 'HEAD'
| 'options'
| 'OPTIONS'
| 'post'
| 'POST'
| 'put'
| 'PUT'
| 'patch'
| 'PATCH'
| 'purge'
| 'PURGE'
| 'link'
| 'LINK'
| 'unlink'
| 'UNLINK'
| undefined
url: string
params?: ParamsProp
contentType?:
| 'application/json;charset=UTF-8'
| 'application/x-www-form-urlencoded;charset=UTF-8'
showLoading?: boolean
}
/**
* 请求
* @param {String} method [请求方法]
* @param {String} url [请求地址]
* @param {Object} params [请求参数]
* @param {String} contentType [请求头,默认为'application/json;charset=UTF-8']
* @param {Boolean} showLoading [是否显示请求时的loading图,默认为true]
*/
const ajax = ({
method = 'GET',
url,
params = {},
contentType = 'application/json;charset=UTF-8',
showLoading = true,
}: RequestOptions) => {
if (!url || typeof url != 'string') {
throw new Error('接口URL不正确')
}
let config: AxiosRequestConfig = {
method,
url,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': contentType,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers':
'Authorization,Origin, X-Requested-With, Content-Type, Accept',
'Access-Control-Allow-Methods': '*',
},
}
if (method === 'GET') {
config = Object.assign(config, { params })
} else {
if (contentType.toLowerCase().indexOf('x-www-form-urlencoded') >= 0) {
config = Object.assign(config, { data: qs.stringify(params) })
} else {
config = Object.assign(config, { data: params })
}
}
if (showLoading) {
store.commit('SET_LOADING', true)
}
return Axios(config)
}
export default {
get(args: RequestOptions): AxiosPromise<AxiosInstance> {
return ajax({ method: 'GET', ...args })
},
post(args: RequestOptions): AxiosPromise<AxiosInstance> {
// args.contentType = 'application/x-www-form-urlencoded;charset=UTF-8'
return ajax({ method: 'POST', ...args })
},
put(args: RequestOptions): AxiosPromise<AxiosInstance> {
return ajax({ method: 'PUT', ...args })
},
delete(args: RequestOptions): AxiosPromise<AxiosInstance> {
return ajax({ method: 'DELETE', ...args })
},
}
import ajax from './axios'
import api from './api'
export { ajax, api }
<template>
<transition
name="animate__animated"
class="animate__animated"
:enter-active-class="`animate__${enter}`"
:leave-active-class="`animate__${leave}`"
>
<slot />
</transition>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
export default defineComponent({
name: 'MyAnimate',
displayName: 'm-animate',
props: {
enter: {
type: String as PropType<string>,
required: true,
},
leave: {
type: String as PropType<string>,
required: true,
},
},
})
</script>
<template>
<div class="card-wrapper">
<div class="card-title">
<p>
{{ title }}
</p>
</div>
<div class="card-content">
<span class="border" />
<span class="border" />
<slot />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
export default defineComponent({
name: 'Mode1',
props: {
title: {
type: String as PropType<string>,
required: true,
},
},
})
</script>
<style scoped lang="stylus">
@import '../main.styl'
.card-wrapper
.card-title
display flex
background url('/src/assets/images/card-mode1-header.png') left bottom / 100% 60% no-repeat
>p
color $card-title-color
font-size $card-title-size
font-weight bold
background url('/src/assets/images/card-mode1-title.png') left bottom / 100% 60% no-repeat
padding 0 .2rem
margin 0
.card-content
position relative
box-sizing border-box
background $card-bg
padding .05rem
border-bottom $card-border
>.border
display block
position absolute
top 0
left 0
bottom 0
width .01rem
background linear-gradient(to bottom, rgba(148,236,255, 0), rgba(91,214,255,.5))
&:nth-of-type(2)
left auto
right 0
>div
$full()
</style>
<template>
<MyAnimate :enter="enter" :leave="leave">
<div class="my-card">
<component :is="card" :title="title">
<slot />
</component>
</div>
</MyAnimate>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
import MyAnimate from '../MyAnimate/my-animate.vue'
import mode1 from './mode1.vue'
import '../main.styl'
export default defineComponent({
name: 'MyCard',
displayName: 'm-card',
components: {
MyAnimate,
mode1,
},
props: {
title: {
type: String as PropType<string>,
required: true,
},
mode: {
type: String as PropType<string>,
default: '1',
},
enter: {
type: String as PropType<string>,
default: 'fadeInLeft',
},
leave: {
type: String as PropType<string>,
default: 'fadeOutLeft',
},
},
setup(props) {
const card = computed(() => {
switch (props.mode) {
case '1':
return mode1
default:
return mode1
}
})
return {
card,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-card
$full()
width initial
height initial
overflow hidden
z-index 10
:deep(.card-wrapper)
$full()
// padding .05rem .1rem
position relative
:deep(.card-title)
max-height .2rem
:deep(.card-content)
height calc(100% - .2rem)
overflow-y auto
overflow-x hidden
$blur()
</style>
<template>
<div ref="chartRef" class="my-chart" />
</template>
<script lang="ts">
import { defineComponent, onMounted, PropType, watchEffect } from 'vue'
import * as echarts from 'echarts/core'
import {
DatasetComponent,
DatasetComponentOption,
TitleComponent,
TitleComponentOption,
TooltipComponent,
TooltipComponentOption,
GridComponent,
GridComponentOption,
LegendComponent,
LegendComponentOption,
} from 'echarts/components'
import {
LineChart,
LineSeriesOption,
BarChart,
BarSeriesOption,
} from 'echarts/charts'
import { SVGRenderer } from 'echarts/renderers'
echarts.use([
DatasetComponent,
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
LineChart,
BarChart,
SVGRenderer,
])
import useChartGenerate from '@/hooks/useChartGenerate.ts'
type ECOption = echarts.ComposeOption<
| DatasetComponentOption
| TitleComponentOption
| TooltipComponentOption
| GridComponentOption
| LegendComponentOption
| LineSeriesOption
| BarSeriesOption
>
export default defineComponent({
name: 'MyBar',
displayName: 'm-bar',
props: {
dataset: {
type: Object as PropType<DatasetComponentOption>,
default: null,
},
option: {
type: Object as PropType<ECOption>,
default: null,
},
format: {
type: Function as PropType<
(dataset: DatasetComponentOption, option: ECOption) => ECOption
>,
default: null,
},
},
setup(props) {
const defaultOption: ECOption = {
backgroundColor: 'transparent',
tooltip: {
confine: true,
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
legend: {},
grid: {
left: '2%',
right: '4%',
bottom: '1%',
containLabel: true,
},
xAxis: [
{
type: 'category',
},
],
yAxis: [
{
type: 'value',
},
],
}
const defaultSeriesItem: BarSeriesOption = {
type: 'bar',
barGap: 0,
emphasis: {
focus: 'series',
},
}
const { chartRef, initChart } = useChartGenerate(
defaultOption,
defaultSeriesItem,
props.format
)
onMounted(() => {
initChart(props.dataset, props.option)
})
watchEffect(() => {
initChart(props.dataset, props.option)
})
return {
chartRef,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-chart
$full()
</style>
<template>
<div ref="chartRef" class="my-chart" />
</template>
<script lang="ts">
import { defineComponent, onMounted, PropType, watchEffect } from 'vue'
import * as echarts from 'echarts/core'
import {
DatasetComponent,
DatasetComponentOption,
TitleComponent,
TitleComponentOption,
TooltipComponent,
TooltipComponentOption,
GridComponent,
GridComponentOption,
LegendComponent,
LegendComponentOption,
} from 'echarts/components'
import {
LineChart,
LineSeriesOption,
BarChart,
BarSeriesOption,
} from 'echarts/charts'
import { SVGRenderer } from 'echarts/renderers'
echarts.use([
DatasetComponent,
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
LineChart,
BarChart,
SVGRenderer,
])
import useChartGenerate from '@/hooks/useChartGenerate'
type ECOption = echarts.ComposeOption<
| DatasetComponentOption
| TitleComponentOption
| TooltipComponentOption
| GridComponentOption
| LegendComponentOption
| LineSeriesOption
| BarSeriesOption
>
export default defineComponent({
name: 'MyLine',
displayName: 'm-line',
props: {
dataset: {
type: Object as PropType<DatasetComponentOption>,
default: null,
},
option: {
type: Object as PropType<ECOption>,
default: null,
},
format: {
type: Function as PropType<
(dataset: DatasetComponentOption, option: ECOption) => ECOption
>,
default: null,
},
},
setup(props) {
const defaultOption: ECOption = {
backgroundColor: 'transparent',
tooltip: {
confine: true,
trigger: 'axis',
},
legend: {},
grid: {
left: '2%',
right: '4%',
bottom: '1%',
containLabel: true,
},
xAxis: [
{
type: 'category',
boundaryGap: false,
},
],
yAxis: [
{
type: 'value',
},
],
}
const defaultSeriesItem: LineSeriesOption = {
type: 'line',
smooth: true,
lineStyle: {
width: 2,
},
emphasis: {
focus: 'series',
},
}
const { chartRef, initChart } = useChartGenerate(
defaultOption,
defaultSeriesItem,
props.format
)
onMounted(async () => {
initChart(props.dataset, props.option)
})
watchEffect(() => {
initChart(props.dataset, props.option)
})
return {
chartRef,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-chart
$full()
</style>
<template>
<div ref="chartRef" class="my-chart" />
</template>
<script lang="ts">
import { defineComponent, onMounted, PropType, watchEffect } from 'vue'
import * as echarts from 'echarts/core'
import {
DatasetComponent,
DatasetComponentOption,
TitleComponent,
TitleComponentOption,
TooltipComponent,
TooltipComponentOption,
GridComponent,
GridComponentOption,
LegendComponent,
LegendComponentOption,
} from 'echarts/components'
import { PieChart, PieSeriesOption } from 'echarts/charts'
import { SVGRenderer } from 'echarts/renderers'
echarts.use([
DatasetComponent,
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
PieChart,
SVGRenderer,
])
import useChartGenerate from '@/hooks/useChartGenerate'
type ECOption = echarts.ComposeOption<
| DatasetComponentOption
| TitleComponentOption
| TooltipComponentOption
| GridComponentOption
| LegendComponentOption
| PieSeriesOption
>
export default defineComponent({
name: 'MyPie',
displayName: 'm-pie',
props: {
dataset: {
type: Object as PropType<DatasetComponentOption>,
default: null,
},
option: {
type: Object as PropType<ECOption>,
default: null,
},
format: {
type: Function as PropType<
(dataset: DatasetComponentOption, option: ECOption) => ECOption
>,
default: null,
},
},
setup(props) {
const defaultOption: ECOption = {
backgroundColor: 'transparent',
tooltip: {
confine: true,
trigger: 'item',
},
legend: {},
grid: {
left: '2%',
right: '4%',
bottom: '1%',
containLabel: true,
},
}
const defaultSeriesItem: PieSeriesOption = {
type: 'pie',
radius: ['30%', '50%'],
center: ['50%', '55%'],
label: {
show: false,
position: 'center',
},
itemStyle: {
borderRadius: 2,
},
}
const { chartRef, initChart } = useChartGenerate(
defaultOption,
defaultSeriesItem,
props.format
)
onMounted(async () => {
initChart(props.dataset, props.option)
})
watchEffect(() => {
initChart(props.dataset, props.option)
})
return {
chartRef,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-chart
$full()
</style>
<template>
<div ref="chartRef" class="my-chart" />
</template>
<script lang="ts">
import { defineComponent, onMounted, PropType, watchEffect } from 'vue'
import * as echarts from 'echarts/core'
import {
DatasetComponent,
DatasetComponentOption,
TitleComponent,
TitleComponentOption,
TooltipComponent,
TooltipComponentOption,
GridComponent,
GridComponentOption,
LegendComponent,
LegendComponentOption,
} from 'echarts/components'
import { RadarChart, RadarSeriesOption } from 'echarts/charts'
import { SVGRenderer } from 'echarts/renderers'
echarts.use([
DatasetComponent,
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
RadarChart,
SVGRenderer,
])
import useChartGenerate from '@/hooks/useChartGenerate'
type ECOption = echarts.ComposeOption<
| DatasetComponentOption
| TitleComponentOption
| TooltipComponentOption
| GridComponentOption
| LegendComponentOption
| RadarSeriesOption
>
export default defineComponent({
name: 'MyRadar',
displayName: 'm-radar',
props: {
dataset: {
type: Object as PropType<DatasetComponentOption>,
default: null,
},
option: {
type: Object as PropType<ECOption>,
default: null,
},
format: {
type: Function as PropType<
(dataset: DatasetComponentOption, option: ECOption) => ECOption
>,
default: null,
},
},
setup(props) {
const defaultOption: ECOption = {
backgroundColor: 'transparent',
tooltip: {
confine: true,
trigger: 'item',
},
legend: {},
grid: {
left: '2%',
right: '4%',
bottom: '1%',
containLabel: true,
},
radar: {
name: {
textStyle: {
color: '#fff',
backgroundColor: 'rgba(255,255,255,.3)',
borderRadius: 3,
padding: [3, 5],
},
},
indicator: {},
splitArea: {
show: true,
areaStyle: {
color: [
'rgba(1,124,143,.9)',
'rgba(1,124,143,.7)',
'rgba(1,124,143,.5)',
'rgba(1,124,143,.3)',
'rgba(1,124,143,.1)',
],
},
},
splitLine: {
show: true,
lineStyle: {
color: 'rgba(255,255,255,.3)',
},
},
axisLine: {
show: false,
},
center: ['50%', '55%'],
radius: '55%',
},
}
const defaultSeriesItem: RadarSeriesOption = {
type: 'radar',
symbol: 'none',
areaStyle: {
opacity: 0.5,
},
emphasis: {
focus: 'item',
},
}
const { chartRef, initChart } = useChartGenerate(
defaultOption,
defaultSeriesItem,
props.format
)
onMounted(async () => {
initChart(props.dataset, props.option)
})
watchEffect(() => {
(defaultOption as any).radar.indicator =
props.dataset &&
props.dataset.dimensions &&
props.dataset.dimensions.map((d) => ({
name: (d as any).displayName,
max: (d as any).max,
}))
initChart(props.dataset, props.option)
})
return {
chartRef,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-chart
$full()
</style>
<template>
<div ref="chartRef" class="my-chart" />
</template>
<script lang="ts">
import { defineComponent, onMounted, PropType, watchEffect } from 'vue'
import * as echarts from 'echarts/core'
import {
DatasetComponent,
DatasetComponentOption,
TitleComponent,
TitleComponentOption,
TooltipComponent,
TooltipComponentOption,
GridComponent,
GridComponentOption,
LegendComponent,
LegendComponentOption,
} from 'echarts/components'
import { ScatterChart, ScatterSeriesOption } from 'echarts/charts'
import { SVGRenderer } from 'echarts/renderers'
echarts.use([
DatasetComponent,
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
ScatterChart,
SVGRenderer,
])
import useChartGenerate from '@/hooks/useChartGenerate'
type ECOption = echarts.ComposeOption<
| DatasetComponentOption
| TitleComponentOption
| TooltipComponentOption
| GridComponentOption
| LegendComponentOption
| ScatterSeriesOption
>
export default defineComponent({
name: 'MyScatter',
displayName: 'm-scatter',
props: {
dataset: {
type: Object as PropType<DatasetComponentOption>,
default: null,
},
option: {
type: Object as PropType<ECOption>,
default: null,
},
format: {
type: Function as PropType<
(dataset: DatasetComponentOption, option: ECOption) => ECOption
>,
default: null,
},
},
setup(props) {
const defaultOption: ECOption = {
backgroundColor: 'transparent',
tooltip: {
confine: true,
trigger: 'item',
},
legend: {},
grid: {
left: '2%',
right: '4%',
bottom: '1%',
containLabel: true,
},
xAxis: {},
yAxis: {},
}
const defaultSeriesItem: ScatterSeriesOption = {
type: 'scatter',
}
const { chartRef, initChart } = useChartGenerate(
defaultOption,
defaultSeriesItem,
props.format
)
onMounted(async () => {
initChart(props.dataset, props.option)
})
watchEffect(() => {
initChart(props.dataset, props.option)
})
return {
chartRef,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-chart
$full()
</style>
// import {
// defineComponent,
// PropType,
// onBeforeUnmount,
// onMounted,
// ref,
// watch,
// } from 'vue'
// import { CountUp, CountUpOptions } from 'countup.js'
// export default defineComponent({
// name: 'MyCount',
// displayName: 'm-count',
// props: {
// value: {
// type: [Number, String] as PropType<number | string>,
// default: 0,
// },
// /** 保留几位小数,默认为0 */
// decimal: {
// type: [Number, String] as PropType<number | string>,
// default: 0,
// },
// /** 数字跳动速度 默认为2 */
// speed: {
// type: [Number, String] as PropType<number | string>,
// default: 2,
// },
// /** 是否隔一段时间自动跳动 默认为false */
// autoPlay: {
// type: Boolean as PropType<boolean>,
// default: false,
// },
// /** autoPlay开启后的间隔时间设置 默认为10 */
// duration: {
// type: [Number, String] as PropType<number | string>,
// default: 10,
// },
// },
// setup(props) {
// const endValue = ref(+props.value)
// const countRef = ref<null | HTMLElement>(null)
// const countUpInstance = ref<CountUp | null>(null)
// const timer = ref<null | number>(null)
// const options: CountUpOptions = {
// decimalPlaces: +props.decimal,
// duration: +props.speed,
// }
// onMounted(() => {
// const countUp = new CountUp(
// countRef.value as HTMLElement,
// endValue.value,
// options
// )
// if (!countUp.error) {
// countUp.start()
// } else {
// console.error(`m-count error: ${countUp.error}`)
// }
// countUpInstance.value = countUp
// if (props.autoPlay) {
// timer.value = +setInterval(() => {
// countUp.reset()
// countUp.update(endValue.value)
// }, +props.duration * 1000)
// }
// })
// watch(
// () => props.value,
// (cur) => {
// endValue.value = +cur
// if (
// countUpInstance.value &&
// typeof countUpInstance.value.update === 'function'
// )
// countUpInstance.value.update(+cur)
// }
// )
// onBeforeUnmount(() => {
// clearInterval(Number(timer.value))
// timer.value = null
// countUpInstance.value = null
// })
// // return {
// // countRef,
// // countUpInstance,
// // endValue,
// // }
// return () => <span ref={countRef}></span>
// },
// })
<template>
<span ref="countRef" />
</template>
<script lang="ts">
import {
defineComponent,
PropType,
onBeforeUnmount,
onMounted,
ref,
watch,
} from 'vue'
import { CountUp, CountUpOptions } from 'countup.js'
export default defineComponent({
name: 'MyCount',
displayName: 'm-count',
props: {
value: {
type: [Number, String] as PropType<number | string>,
default: 0,
},
/** 保留几位小数,默认为0 */
decimal: {
type: [Number, String] as PropType<number | string>,
default: 0,
},
/** 数字跳动速度 默认为2 */
speed: {
type: [Number, String] as PropType<number | string>,
default: 2,
},
/** 是否隔一段时间自动跳动 默认为false */
autoPlay: {
type: Boolean as PropType<boolean>,
default: false,
},
/** autoPlay开启后的间隔时间设置 默认为10 */
duration: {
type: [Number, String] as PropType<number | string>,
default: 10,
},
},
setup(props) {
const endValue = ref(+props.value)
const countRef = ref<null | HTMLElement>(null)
const countUpInstance = ref<CountUp | null>(null)
const timer = ref<null | number>(null)
const options: CountUpOptions = {
decimalPlaces: +props.decimal,
duration: +props.speed,
}
onMounted(() => {
const countUp = new CountUp(
countRef.value as HTMLElement,
endValue.value,
options
)
if (!countUp.error) {
countUp.start()
} else {
console.error(`m-count error: ${countUp.error}`)
}
countUpInstance.value = countUp
if (props.autoPlay) {
timer.value = +setInterval(() => {
countUp.reset()
countUp.update(endValue.value)
}, +props.duration * 1000)
}
})
watch(
() => props.value,
(cur) => {
endValue.value = +cur
if (
countUpInstance.value &&
typeof countUpInstance.value.update === 'function'
)
countUpInstance.value.update(+cur)
}
)
onBeforeUnmount(() => {
clearInterval(Number(timer.value))
timer.value = null
countUpInstance.value = null
})
return {
countRef,
countUpInstance,
endValue,
}
},
})
</script>
<template>
<div class="my-empty">
<img src="/src/assets/images/no-data.png" />
<p>{{ text }}</p>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
export default defineComponent({
name: 'MyEmpty',
displayName: 'm-empty',
props: {
text: {
type: String as PropType<string>,
default: '暂无数据',
},
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-empty
$center()
$full()
flex-direction column
color #ccc
font-weight bold
>img
width .4rem
height @width
</style>
<template>
<div />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'MyForm',
displayName: 'm-form',
})
</script>
<style lang="stylus" scoped></style>
<template>
<div ref="gridRef" class="my-grid" :style="style">
<slot />
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, PropType, ref } from 'vue'
export default defineComponent({
name: 'MyGrid',
displayName: 'm-grid',
props: {
template: {
type: Array as PropType<string[]>,
required: true,
},
columns: {
type: String as PropType<string>,
required: true,
},
rows: {
type: String as PropType<string>,
required: true,
},
gap: {
type: String as PropType<string>,
default: '.05rem',
},
},
setup(props) {
const gridRef = ref<HTMLElement | null>(null)
onMounted(() => {
const { children } = gridRef.value as HTMLElement
for (let i = 0; i < children.length; i++) {
const child = children[i] as HTMLElement
const area = child.getAttribute('area')
if (area) {
child.style.gridArea = area
}
}
})
const style = computed(() => {
const { template, columns, rows, gap } = props
if (!template || template.length === 0) return
let areas = ''
template.forEach((area) => {
areas += `'${area}'`
})
return {
'grid-template-areas': areas,
'grid-template-columns': columns,
'grid-template-rows': rows,
'grid-gap': gap,
padding: gap,
'padding-top': 0,
}
})
return {
gridRef,
style,
}
},
})
</script>
<style scoped lang="stylus">
@import '../main.styl'
.my-grid
$full()
background-size cover
background-position center
position relative
display grid
overflow hidden
</style>
<template>
<teleport to="#MyLoader">
<div class="my-loader-mask">
<div class="my-loader">
<div class="outer" />
<div class="middle" />
<div class="inner" />
</div>
</div>
</teleport>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import useDOMCreate from '@/hooks/useDOMCreate'
export default defineComponent({
name: 'MyLoader',
displayName: 'm-loader',
setup() {
useDOMCreate('MyLoader')
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-loader-mask
position fixed
top 0
right 0
bottom 0
left 0
background rgba(0,0,0,0.3)
z-index 99999
$center()
.my-loader
position relative
.outer,
.middle,
.inner
border .03rem solid transparent
border-top-color #47B3FF
border-right-color @border-top-color
border-radius 50%
position absolute
top 50%
left 50%
.outer
width .35rem
height @width
margin-left -0.175rem
margin-top @margin-left
animation spin 2.5s linear infinite
.middle
width .21rem
height @width
margin-left -0.105rem
margin-top @margin-left
animation spin 2s linear reverse infinite
.inner
width .08rem
height @width
margin-left -0.04rem
margin-top @margin-left
animation spin 1.5s linear infinite
@keyframes spin {
to {
transform rotate(360deg)
border-top-color #00f2ff
border-right-color @border-top-color
}
}
</style>
<template>
<div id="MapContainer" />
</template>
<script lang="ts">
import { computed, defineComponent, nextTick, onMounted, ref } from 'vue'
declare const AMap: any
interface MarkersProp {
x: number | string
y: number | string
}
export default defineComponent({
name: 'MyMap',
displayName: 'm-map',
emits: ['complete', 'marker-click'],
setup(props, ctx) {
const map = ref<any>(null)
const markers = ref<any[]>([])
const sizeRate = computed(() => Number((screen.height / 800).toFixed(1)))
const injectMapJs = () => {
if (!document.querySelector('#_MapJs')) {
const mapJs = document.createElement('script')
mapJs.src =
'http://webapi.amap.com/maps?v=1.4.15&key=ee2b5d5c0c44c768f1d2593eb4a7dfa6&plugin=Map3D,AMap.DistrictSearch'
mapJs.setAttribute('id', '_MapJs')
mapJs.onload = async () => {
await nextTick()
initMap()
}
document.head.appendChild(mapJs)
}
}
const initWallLayer = (
name = '黄浦区',
color = 'rgba(0,242,255,0.3)',
height = 1500
) => {
const object3DLayer = new AMap.Object3DLayer({ zIndex: 100 })
map.value.add(object3DLayer)
const district = new AMap.DistrictSearch({
subdistrict: 0,
extensions: 'all',
level: 'city',
})
district.search(name, function (status, result) {
const wall = new AMap.Object3D.Wall({
path: result.districtList[0].boundaries,
height,
color,
})
wall.backOrFront = 'both'
wall.transparent = true
object3DLayer.add(wall)
})
}
const initMap = () => {
const mapConfig = {
resizeEnable: true,
rotateEnable: true,
pitchEnable: true,
showLabel: true,
zoom: 14,
pitch: 50,
rotation: 45,
viewMode: '3D', // 开启3D视图,默认为关闭
buildingAnimation: true, // 楼块出现是否带动画
expandZoomRange: true,
zooms: [10, 20],
center: [121.5112, 31.195],
showIndoorMap: false,
// mapStyle: 'amap://styles/grey',
mapStyle: 'amap://styles/fa9ac17bb73a5cc74c551d8e8162086d',
features: ['bg', 'road', 'building'],
layers: [
new AMap.TileLayer(),
new AMap.Buildings({
zooms: [10, 20],
zIndex: 10,
}),
],
}
map.value = new AMap.Map('MapContainer', mapConfig).on(
'click',
({ lnglat }) => {
console.log('click-lnglat: ', [lnglat.lng, lnglat.lat])
}
)
initWallLayer()
ctx.emit('complete', true)
}
const addMarkers = (data: MarkersProp[]) => {
map.value.remove(markers.value)
markers.value = data.map((item) => {
const { x, y } = item // TODO
const marker = new AMap.Marker({
position: [+x, +y],
zIndex: 99,
icon: new AMap.Icon({
size: [16 * sizeRate.value, 20 * sizeRate.value],
image: '/src/assets/images/dimond1.png',
}),
extData: { ...item },
}).on('click', () => ctx.emit('marker-click', marker.getExtData()))
return marker
})
map.value.add(markers.value)
}
const focus = (x: number | string, y: number | string, zoom = 18.5) => {
map.value.setZoomAndCenter(zoom, [x, y])
}
onMounted(injectMapJs)
return {
map,
markers,
initWallLayer,
addMarkers,
focus,
}
},
})
</script>
<style lang="stylus" scoped>
#MapContainer
position fixed
top 0
right 0
bottom 0
left 0
</style>
<style lang="stylus">
.amap-logo
.amap-copyright
display none !important
.amap-icon
img
width 100%
height 100%
</style>
<template>
<teleport to="body">
<MyAnimate :enter="enter" :leave="leave">
<div
v-if="modelValue"
class="my-modal-mask"
style="animation-duration: 300ms"
@click.prevent.self="
() => {
maskClosable ? closeModal() : null
}
"
>
<div
class="my-modal"
:style="`width:${width};transform: translateX(${offset})`"
>
<head>
<p>
{{ title }}
<span class="left" />
<span class="right" />
</p>
<div>
<img
src="/src/assets/images/modal-flag.png"
draggable="false"
class="flag"
/>
<img
src="/src/assets/images/close-btn.png"
draggable="false"
class="close-btn"
@click.prevent="closeModal"
/>
</div>
<img
src="/src/assets/images/modal-title-left.png"
draggable="false"
class="left"
/>
</head>
<div class="content">
<slot />
</div>
</div>
</div>
</MyAnimate>
</teleport>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import MyAnimate from '../MyAnimate/my-animate.vue'
export default defineComponent({
name: 'MyModal',
displayName: 'm-modal',
components: { MyAnimate },
props: {
modelValue: {
type: Boolean as PropType<boolean>,
default: false,
},
enter: {
type: String as PropType<string>,
default: 'fadeInDown',
},
leave: {
type: String as PropType<string>,
default: 'fadeOutUp',
},
title: {
type: String as PropType<string>,
default: '',
},
width: {
type: String as PropType<string>,
default: '32%',
},
offset: {
type: String as PropType<string>,
default: '0',
},
maskClosable: {
type: Boolean as PropType<boolean>,
default: true,
},
},
emits: ['update:modelValue', 'close'],
setup(props, context) {
const closeModal = () => {
context.emit('update:modelValue', false)
context.emit('close')
}
return {
closeModal,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-modal-mask
position fixed
top 0
right 0
bottom 0
left 0
background rgba(0,0,0,0.1)
z-index 9999
$center()
.my-modal
background rgba(0,0,0,0.1)
color #fff
$blur()
z-index 99999
head
position relative
height .4rem
display flex
align-items center
justify-content space-between
padding 0 .1rem
margin-bottom .05rem
background-image repeating-linear-gradient(45deg, $blue, $blue, .01rem, transparent .01rem, transparent .08rem)
$border($blue)
>p
font-size .12rem
font-weight bold
padding .02rem .05rem
position relative
border-bottom .01rem solid $blue
span
position absolute
width .04rem
height @width
background $edge
bottom -(@height / 2)
&.left
left -(@width / 2)
&.right
right -(@width / 2)
>div
display flex
align-items center
img
z-index 9
&.close-btn
width .2rem
height @width
cursor pointer
margin-left .05rem
transition transform .3s ease-in-out
&:hover
transform rotate(90deg)
&.flag
height .2rem
&.left
position absolute
left -0.01rem
height 80%
.content
min-height 30vh
max-height 80vh
padding .1rem
overflow-y auto
overflow-x hidden
$border($blue)
border-top none
font-size .1rem
</style>
<template>
<div class="my-scroll" @mouseenter.prevent="stop" @mouseleave.prevent="start">
<div ref="contentRef">
<slot />
</div>
<div v-if="isNeedMockContent" ref="mockContentRef">
<slot />
</div>
</div>
</template>
<script lang="ts">
import {
computed,
defineComponent,
PropType,
onBeforeUnmount,
onMounted,
ref,
watch,
nextTick,
} from 'vue'
export default defineComponent({
name: 'MyScroll',
displayName: 'm-scroll',
props: {
length: {
type: Number as PropType<number>,
required: true,
},
limit: {
type: Number as PropType<number>,
required: true,
},
duration: {
type: Number as PropType<number>,
default: 4000,
},
speed: {
type: Number as PropType<number>,
default: 50,
},
mode: {
type: [Number, String] as PropType<number | string>,
default: 1,
},
step: {
type: Number as PropType<number>,
default: 0,
},
},
setup(props) {
const contentRef = ref<null | HTMLElement>(null)
const mockContentRef = ref<null | HTMLElement>(null)
const timer = ref<null | number>(null)
const index = ref(0)
const isNeedMockContent = computed(
() => props.length <= 100 && props.length >= props.limit
)
const start = () => {
if (props.length < props.limit) return
if (+props.mode === 2) {
startMode2()
} else {
startMode1()
}
}
const stop = () => {
clearInterval(Number(timer.value))
timer.value = null
}
const startMode1 = () => {
const content = contentRef.value
const mockContent = mockContentRef.value
if (!content) return
let height = content.offsetHeight
timer.value = +setInterval(() => {
if (height <= 0) {
height = content.offsetHeight
return
}
if (index.value < height) {
index.value += 1
} else {
index.value = 0
}
content.style.transform = `translateY(${-index.value}px)`
mockContent &&
(mockContent.style.transform = `translateY(${-index.value}px)`)
}, props.speed)
}
const startMode2 = () => {
if (!props.step) {
console.error('MyScroll mode2模式需要step参数!')
return
}
const content = contentRef.value
const mockContent = mockContentRef.value
if (!content) return
const len = (content.children && content.children.length) || 0
timer.value = +setInterval(() => {
if (index.value < len) {
index.value += 1
content.style.transition = 'transform 0.5s ease-in-out'
mockContent &&
(mockContent.style.transition = 'transform 0.5s ease-in-out')
} else {
index.value = 0
content.style.transition = 'none'
mockContent && (mockContent.style.transition = 'none')
}
content.style.transform = `translateY(${-props.step * index.value}rem)`
mockContent &&
(mockContent.style.transform = `translateY(${
-props.step * index.value
}rem)`)
}, props.duration)
}
onMounted(() => {
start()
})
onBeforeUnmount(() => {
stop()
})
watch(
() => props.length,
async () => {
stop()
index.value = 0
const content = contentRef.value
const mockContent = mockContentRef.value
content && (content.style.transform = 'translateY(0)')
mockContent && (mockContent.style.transform = 'translateY(0)')
await nextTick()
start()
}
)
return {
isNeedMockContent,
start,
stop,
contentRef,
mockContentRef,
timer,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-scroll
$full()
overflow hidden
z-index 99
</style>
<template>
<div class="my-step">
<div
class="lines"
:style="{ transform: `translateY(calc(${iconTop}px + 0.035rem))` }"
>
<div class="line done" :style="`width:${doneWidth}%`" />
<div class="line" :style="`width:${undoneWidth}%`" />
</div>
<div
v-for="(step, i) in steps"
:key="i"
class="item"
:class="{ on: i == current }"
>
<p :title="step">
{{ step }}
</p>
<div v-if="i <= current" ref="iconRef" class="icon" />
<div v-else class="none"></div>
<span class="msg" :title="msg[i]" v-html="msg[i]" />
</div>
</div>
</template>
<script lang="ts">
import {
computed,
defineComponent,
nextTick,
onMounted,
PropType,
ref,
} from 'vue'
export default defineComponent({
name: 'MyStep',
displayName: 'm-step',
props: {
steps: {
type: Array as PropType<string[]>,
required: true,
},
current: {
type: [Number, String] as PropType<number | string>,
default: 0,
},
msg: {
type: Array as PropType<string[]>,
default: () => [],
},
},
setup(props) {
const iconRef = ref<null | HTMLElement>(null)
const linePercent = computed(() => {
const { length } = props.steps
if (length > 2) return 100 / (length - 1)
return 100
})
const doneWidth = computed(() => {
if (Number.isNaN(+props.current)) {
console.error('MyStep组件传入的current参数是NaN!')
return 0
}
return Math.round(linePercent.value * +props.current)
})
const undoneWidth = computed(() => 100 - doneWidth.value)
const iconTop = ref(0)
onMounted(async () => {
await nextTick()
iconTop.value = (iconRef.value as HTMLElement).offsetTop
})
return {
iconRef,
linePercent,
doneWidth,
undoneWidth,
iconTop,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-step
width 90%
margin .05rem auto 0
display flex
justify-content space-between
position relative
font-size .09rem
.lines
width 100%
display flex
position absolute
left -0.05rem
z-index -1
.line
height .03rem
background rgba(0,0,0,0.8)
border-radius .1rem
margin-left .05rem
&.done
background linear-gradient(to right, $blue, $edge)
.item
position relative
text-align center
width .1rem
.icon
width .1rem
height @width
border-radius 50%
background url('../../../assets/images/true.png') center center / 60% 60% no-repeat
background-color $edge
.none
width .06rem
height @width
border .02rem solid $edge
background rgba(0,0,0,0.8)
border-radius 50%
.msg
display inline-block
font-size .06rem
width 1rem
transform translateX(-50%)
margin .02rem 50%
font-weight normal
color #aaa
p
width .8rem
margin .05rem 50%
transform translateX(-50%)
overflow hidden
white-space nowrap
text-overflow ellipsis
color #ccc
font-size .08rem
&.on
.icon
transform scale(1.2)
box-shadow 0 0 .05rem .01rem $edge
p
color $edge
font-weight bold
</style>
<template>
<div class="my-table">
<div class="table-title">
<div
v-for="(title, index) in layout.header"
:key="title"
:style="`flex:${calcWidth[index]}`"
>
<p :style="`text-align:${calcAlign[index]}`">
{{ title.split('*')[0].replace('::', '') }}
</p>
</div>
</div>
<div class="table-content">
<div
v-for="(item, index) in dataSource"
:key="index"
:class="{ selectable: selectable }"
@click.prevent="handleClick(item)"
>
<div
v-for="(key, i) in layout.keys"
:key="key"
:style="`flex:${calcWidth[i]}`"
>
<p
v-if="key.indexOf('>') >= 0"
:style="`text-align:${calcAlign[i]}`"
v-html="transValue(item, key)"
/>
<img
v-else-if="key.indexOf('#') >= 0 && key.split('#')[1] === 'image'"
:src="item[key.split('#')[0]]"
:draggable="false"
@click.stop="handleViewImage(item[key.split('#')[0]])"
/>
<p v-else :style="`text-align:${calcAlign[i]}`" v-html="item[key]" />
</div>
</div>
</div>
<MyModal v-model="showImgModal" title="照片预览">
<img v-if="showImgModal && imgSrc" style="width: 100%" :src="imgSrc" />
</MyModal>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, ref } from 'vue'
import MyModal from '../MyModal/my-modal.vue'
interface FormatterType {
[propName: string]: <T>(val: T) => T
}
interface dataType {
[propName: string]: string | number | string[]
}
export default defineComponent({
name: 'MyTable',
displayName: 'm-table',
components: { MyModal },
props: {
template: {
type: Array as PropType<string[]>,
required: true,
},
data: {
type: Array as PropType<dataType[]>,
default: () => [],
},
formatter: {
type: Object as PropType<FormatterType>,
default: null,
},
selectable: {
type: Boolean as PropType<boolean>,
default: false,
},
},
emits: ['select'],
setup(props, ctx) {
const layout = computed(() => {
const { template } = props
if (!template || template.length < 2) return
const header = template[0].split('|')
const keys = template[1].split('|')
return { header, keys }
})
const dataSource = computed(() => props.data)
const transValue = (item: dataType, key: string) => {
return (
props.formatter &&
props.formatter[key.split('>')[1]](item[key.split('>')[0]])
)
}
const handleClick = (data: dataType) => {
if (props.selectable) ctx.emit('select', data)
}
const calcWidth = computed(() => {
if (!layout.value) return 1
return layout.value.header.map((item) => {
if (item.indexOf('*') >= 0) {
return item.split('*')[1]
}
return 1
})
})
const calcAlign = computed(() => {
if (!layout.value) return 'center'
const { length } = layout.value.header
return layout.value.header.map((item) => {
const index = item.indexOf('::')
if (index === 0) return 'left'
if (index === length) return 'right'
return 'center'
})
})
const imgSrc = ref<string | null>(null)
const showImgModal = ref(false)
const handleViewImage = (src: string) => {
imgSrc.value = src
showImgModal.value = true
}
return {
layout,
dataSource,
transValue,
handleClick,
calcWidth,
calcAlign,
imgSrc,
handleViewImage,
showImgModal,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-table
$full()
p
padding .05rem .1rem
margin 0
.table-title
display flex
background $table-title-bg
color $blue
font-weight bold
.table-content
>div
display flex
background $table-content-bg
&:nth-child(odd)
background transparent
&.selectable
cursor pointer
&:hover
color $table-content-hover-color
background $table-content-hover-bg
img
width 100%
cursor pointer
</style>
<template>
<div class="my-title">
<img src="/src/assets/images/title-bg.png" class="bg" />
<div class="date">{{ date }} {{ time }}</div>
<h1><slot /></h1>
</div>
</template>
<script lang="ts">
import { defineComponent, onBeforeUnmount, ref } from 'vue'
import { getDate, getTime } from '../util'
export default defineComponent({
name: 'MyTitle',
displayName: 'm-title',
setup() {
const date = ref(getDate())
const time = ref('')
const timer = ref<number | null>(null)
timer.value = setInterval(() => {
time.value = getTime()
})
onBeforeUnmount(() => {
clearInterval(Number(timer))
timer.value = null
})
return {
date,
time,
timer,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-title
$full()
$center()
position relative
background-size cover
background-position center
font-weight bold
.date
z-index 2
position absolute
top .07rem
right 3%
.bg
position absolute
$full()
top 0
left 0
h1
z-index 2
background-clip text
-webkit-background-clip text
-webkit-text-fill-color transparent
background-image linear-gradient(to bottom, #fff, #B3EBFF)
font-size .2rem
letter-spacing .05rem
</style>
import { App } from 'vue'
import animate from 'animate.css'
import MyMap from './MyMap/my-map.vue'
import MyTitle from './MyTitle/my-title.vue'
import MyLoader from './MyLoader/my-loader.vue'
import MyAnimate from './MyAnimate/my-animate.vue'
import MyGrid from './MyGrid/my-grid.vue'
import MyCard from './MyCard/my-card.vue'
import MyCount from './MyCount/my-count.vue'
import MyModal from './MyModal/my-modal.vue'
import MyScroll from './MyScroll/my-scroll.vue'
import MyForm from './MyForm/my-form.vue'
import MyTable from './MyTable/my-table.vue'
import MyStep from './MyStep/my-step.vue'
import MyEmpty from './MyEmpty/my-empty.vue'
import MyLine from './MyChart/my-line.vue'
import MyBar from './MyChart/my-bar.vue'
import MyPie from './MyChart/my-pie.vue'
import MyRadar from './MyChart/my-radar.vue'
import MyScatter from './MyChart/my-scatter.vue'
// import { withInstall } from './util'
import 'normalize.css'
const components = [
MyMap,
MyTitle,
MyAnimate,
MyGrid,
MyCard,
MyCount,
MyLoader,
MyModal,
MyScroll,
MyForm,
MyTable,
MyStep,
MyEmpty,
MyLine,
MyBar,
MyPie,
MyRadar,
MyScatter,
]
const install = (app: App): App => {
app.use(animate)
// components.forEach((component) => app.use(withInstall(component)))
components.forEach((component) =>
app.component(component.displayName, component)
)
return app
}
// 使用import {MyGrid} from './components/MyComponent'来按需引入个别组件
export {
MyMap,
MyTitle,
MyAnimate,
MyGrid,
MyCard,
MyCount,
MyLoader,
MyModal,
MyScroll,
MyForm,
MyTable,
MyStep,
MyEmpty,
MyLine,
MyBar,
MyPie,
MyRadar,
MyScatter,
}
// 默认导出 —— 使用import MyComponent from './components/MyComponent'来引入所有组件
export default { install }
$font-pang = Pangmenzhengdao, 'Avenir', Helvetica, Arial, sans-serif
$font-din = DIN, 'Avenir', Helvetica, Arial, sans-serif
$full(w = 100%, h = 100%)
width w
height h
box-sizing border-box
$center()
display flex
align-items center
justify-content center
$blur(val = 0.05rem)
backdrop-filter blur(val)
-webkit-backdrop-filter blur(val)
$border(color = #000, width = 0.01rem)
border width solid color
$primary-color = #47B3FF
$primary-bg = rgba(49,94,139,.3)
$primary-border = rgba(91,213,255,.5)
$blue = #2F86EE
$edge = #00f2ff
$card-bg = linear-gradient(to bottom, rgba(5,71,138,.2), rgba(5,71,138,.6))
$card-title-color = #fff
$card-title-size = .14rem
$card-border = .01rem solid $primary-border
$table-title-bg =rgba(2,27,53,.5)
$table-content-bg = rgba(2,27,53,.2)
$table-content-hover-color = $primary-color
$table-content-hover-bg = $primary-bg
\ No newline at end of file
import { App, Plugin } from 'vue'
import moment from 'moment'
import 'moment/locale/zh-cn'
moment.locale('zh-ch')
export const withInstall = <T>(comp: T) => {
const c = comp as any
c.install = function (app: App) {
app.component(c.displayName || c.name, comp)
}
return comp as T & Plugin
}
export const getDate = (): string => moment().format('YYYY-MM-DD dddd')
export const getTime = (): string => moment().format('LTS')
import { onMounted, onBeforeUnmount, nextTick, shallowRef, Ref } from 'vue'
import * as echarts from 'echarts/core'
import {
DatasetComponentOption,
TitleComponentOption,
TooltipComponentOption,
GridComponentOption,
LegendComponentOption,
} from 'echarts/components'
import {
LineSeriesOption,
BarSeriesOption,
PieSeriesOption,
RadarSeriesOption,
ScatterSeriesOption,
} from 'echarts/charts'
type ECOption = echarts.ComposeOption<
| DatasetComponentOption
| TitleComponentOption
| TooltipComponentOption
| GridComponentOption
| LegendComponentOption
| LineSeriesOption
| BarSeriesOption
| PieSeriesOption
| RadarSeriesOption
| ScatterSeriesOption
>
interface ReturnProp {
chartRef: Ref<null | HTMLElement>
initChart: (dataset: DatasetComponentOption, option: ECOption) => void
}
/**
* 构建图表组件公共方法
* @param defaultOption 默认配置
* @param defaultSeriesItem 默认的series项
* @param fn 自定义的合并配置的方法
*/
export default function useChartGenerate<T extends ECOption, U>(
defaultOption: T,
defaultSeriesItem?: U,
fn?: (dataset: DatasetComponentOption, option: any) => ECOption
): ReturnProp {
const chartRef = shallowRef<null | HTMLElement>(null)
const myChart = shallowRef<null | echarts.ECharts>(null)
/**
* 由于radar图的dataset支持不好,故这里根据数据集中source数组里的seriesName来重置tooltip显示的名字
* @param originOptions 图标配置
* @param dataset 数据集
*/
const formatRadarSeries = (originOptions: ECOption, dataset: any) => {
const source = dataset && dataset.source
const length = (source && source.length) || 0
const options = JSON.parse(JSON.stringify(originOptions))
options.series = [
{
type: 'radar',
data: [],
},
]
for (let index = 0; index < length; index++) {
const data: number[] = []
Object.keys(source[index]).forEach((key) => {
if (key !== 'seriesName') {
data.push(source[index][key])
}
})
options.series[0].data.push(
Object.assign({}, defaultSeriesItem, {
value: data,
name: source[index].seriesName,
})
)
}
return options
}
/**
* 根据默认配置设置series,有自定义配置则覆盖默认配置
* @param dataset 数据集
* @param option 自定义图表配置
*/
const mergeOptions = (
dataset: DatasetComponentOption,
option?: ECOption
): ECOption => {
let length = 0
if (dataset) {
defaultOption.dataset = dataset
if (dataset.dimensions) {
length = dataset.dimensions.length - 1
} else if (dataset.source) {
length =
Object.keys(
(Array.isArray(dataset.source) && dataset.source[0]) || []
).length - 1
}
}
if (!defaultOption.series || (defaultOption.series as any[]).length === 0) {
defaultOption.series = []
for (let index = 0; index < length; index++) {
defaultSeriesItem && defaultOption.series.push(defaultSeriesItem)
}
}
if ((defaultSeriesItem as any).type === 'radar') {
return Object.assign(
{},
formatRadarSeries(defaultOption, dataset),
option || {}
)
}
return Object.assign({}, defaultOption, option || {})
}
/**
* 将图表配置中的color选项如果有数组项则转换为渐变
* @param options 图表配置
*/
const formatColorOption = (options: ECOption): ECOption => {
if (!options.color || (options.color as string[]).length === 0) {
return options
}
options.color = (options.color as string[]).map((color) => {
if (Array.isArray(color)) {
if (color.length < 2) {
console.error('Echarts color选项里的数组参数至少需要2个字符串元素!')
return 'black'
}
return new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: color[0],
},
{
offset: 1,
color: color[1],
},
])
}
return color
})
return options
}
/**
* 根据数据集和配置初始化图表
* @param dataset 数据集
* @param option 自定义图表配置
*/
const initChart = (dataset: DatasetComponentOption, option?: ECOption) => {
if (!myChart.value) return
const options = fn ? fn(dataset, option) : mergeOptions(dataset, option)
if (
!options.series ||
(Array.isArray(options.series) && options.series.length === 0)
) {
return
}
const resultOptions = formatColorOption(options)
myChart.value.setOption(resultOptions, true)
}
/**
* 根据屏幕大小自动调整图表尺寸
*/
const handleResize = () => {
if (!myChart.value) return
myChart.value.resize()
}
onMounted(async () => {
window.addEventListener('resize', handleResize)
await nextTick()
myChart.value = echarts.init(chartRef.value as HTMLElement, 'dark')
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
if (!myChart.value) return
myChart.value.dispose()
myChart.value = null
})
return { chartRef, initChart }
}
import { ref, Ref, onMounted, onUnmounted } from 'vue'
export default function useClickOutside(elementRef: Ref<HTMLElement | null>) {
const isClickOutSide = ref(false)
const handler = (e: MouseEvent) => {
if (!elementRef || !elementRef.value) return
isClickOutSide.value = !elementRef.value.contains(e.target as HTMLElement)
}
onMounted(() => {
document.addEventListener('click', handler)
})
onUnmounted(() => {
document.removeEventListener('click', handler)
})
return { isClickOutSide }
}
export default function useDOMCreate(nodeId: string): void {
const existNode = document.querySelector(`#${nodeId}`)
if (!existNode) {
const node = document.createElement('div')
node.id = nodeId
document.body.appendChild(node)
}
}
import { createApp } from 'vue'
import App from './App.vue'
import MyComponent from '@/components/MyComponent'
createApp(App).use(MyComponent).mount('#app')
/* eslint-disable */
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
declare module 'animate.css'
declare module '*.css'
declare module '*.styl'
declare module '*.png'
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.gif'
\ No newline at end of file
export default {}
import { createStore } from 'vuex'
import state from './state'
import mutations from './mutations'
import actions from './actions'
export interface GlobalStateProps {
showLoading: boolean
}
export default createStore<GlobalStateProps>({
state,
mutations,
actions,
})
import { GlobalStateProps } from './index'
export default {
SET_LOADING(state: GlobalStateProps, val: boolean): void {
state.showLoading = val
},
}
export default {
showLoading: false,
}
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"skipLibCheck": true,
"esModuleInterop": true,
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"baseUrl": ".",
"lib": ["esnext", "dom"],
"types": ["vite/client", "node"],
"plugins": [{ "name": "@vuedx/typescript-plugin-vue" }],
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": ["node_modules", "dist"]
}
module.exports = {
settings: {
'vetur.useWorkspaceDependencies': true,
'vetur.experimental.templateInterpolationService': true,
},
projects: [
{
root: './',
// **optional** default: `[]`
// Register globally Vue component glob.
// If you set it, you can get completion by that components.
// It is relative to root property.
// Notice: It won't actually do it. You need to use `require.context` or `Vue.component`
globalComponents: [
{
name: 'm-animate',
path: './src/components/MyComponent/MyAnimate/my-animate.vue',
},
{
name: 'm-card',
path: './src/components/MyComponent/MyCard/my-card.vue',
},
{
name: 'm-bar',
path: './src/components/MyComponent/MyChart/my-bar.vue',
},
{
name: 'm-line',
path: './src/components/MyComponent/MyChart/my-line.vue',
},
{
name: 'm-pie',
path: './src/components/MyComponent/MyChart/my-pie.vue',
},
{
name: 'm-radar',
path: './src/components/MyComponent/MyChart/my-scatter.vue',
},
{
name: 'm-scatter',
path: './src/components/MyComponent/MyChart/my-scatter.vue',
},
{
name: 'm-count',
path: './src/components/MyComponent/MyCount/my-count.vue',
},
{
name: 'm-empty',
path: './src/components/MyComponent/MyEmpty/my-empty.vue',
},
{
name: 'm-form',
path: './src/components/MyComponent/MyForm/my-form.vue',
},
{
name: 'm-grid',
path: './src/components/MyComponent/MyGrid/my-grid.vue',
},
{
name: 'm-loader',
path: './src/components/MyComponent/MyLoader/my-loader.vue',
},
{
name: 'm-map',
path: './src/components/MyComponent/MyMap/my-map.vue',
},
{
name: 'm-modal',
path: './src/components/MyComponent/MyModal/my-modal.vue',
},
{
name: 'm-scroll',
path: './src/components/MyComponent/MyScroll/my-scroll.vue',
},
{
name: 'm-step',
path: './src/components/MyComponent/MyStep/my-step.vue',
},
{
name: 'm-table',
path: './src/components/MyComponent/MyTable/my-table.vue',
},
{
name: 'm-title',
path: './src/components/MyComponent/MyTitle/my-title.vue',
},
],
},
],
}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue(), vueJsx()],
resolve: {
alias: {
'@': resolve(__dirname, './src'),
},
},
// build: {
// base: './',
// },
// assetsDir: 'assets',
optimizeDeps: {
include: [
'moment/locale/zh-cn',
'echarts/core',
'echarts/components',
'echarts/charts',
'echarts/renderers',
],
},
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment