Commit 47f088fd authored by 郭铭瑶's avatar 郭铭瑶 🤘

init project

parents
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: ['plugin:vue/vue3-recommended', '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/script-setup-uses-vars': '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,
trailingComma: 'all',
}
\ No newline at end of file
{
"editor.tabSize": 2,
"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
MIT License
Copyright (c) 2021 Max Kwok
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# my-view
My Dataview Project
## Use
```bash
# install dependencies
npm install
# serve with hot reload at localhost:3000
npm run dev
# or
npm start
# build for production
npm run build
```
## Document
[查看组件文档](https://guomingyao.gitee.io/my-view-components)
<!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
This diff is collapsed.
{
"name": "jingan-wisdom",
"version": "1.0.0",
"author": "Guo",
"private": true,
"scripts": {
"dev": "vite",
"start": "npm run dev",
"build": "vue-tsc --noEmit && vite build",
"serve": "vite preview"
},
"dependencies": {
"animate.css": "^4.1.1",
"axios": "^0.21.1",
"countup.js": "^2.0.8",
"dayjs": "^1.10.6",
"echarts": "^5.1.2",
"normalize.css": "^8.0.1",
"qs": "^6.10.1",
"vue": "^3.2.6",
"vuex": "^4.0.0"
},
"devDependencies": {
"@types/node": "^16.3.2",
"@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "^4.28.1",
"@typescript-eslint/parser": "^4.28.5",
"@vitejs/plugin-vue": "^1.6.0",
"@vitejs/plugin-vue-jsx": "^1.1.7",
"@vue/compiler-sfc": "^3.2.6",
"eslint": "^7.30.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-vue": "^7.14.0",
"prettier": "^2.3.2",
"stylus": "^0.54.8",
"typescript": "^4.3.5",
"vite": "^2.5.2",
"vue-eslint-parser": "^7.10.0",
"vue-tsc": "^0.2.2"
},
"description": "静安智慧房管"
}
<template>
<Main />
<m-loader v-if="showLoading" />
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import Main from './view/main.vue'
import store from '@/store'
// import TsxComponentExample from './tsx-component-example'
export default defineComponent({
name: 'App',
components: { Main },
setup() {
const showLoading = computed(() => store.state.showLoading)
return {
showLoading,
}
},
})
</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 #000
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
p
margin 0
#app
width 100%
height 100%
overflow hidden
font-size .1rem
background url('/src/assets/images/background.jpg') center/cover no-repeat
color #fff
/* 设置滚动条的样式 */
::-webkit-scrollbar {
width: .05rem;
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
background:rgba(91, 213, 255, 0.3)
-webkit-box-shadow:inset006pxrgba(0,0,0,0.5);
}
::-webkit-scrollbar-thumb:window-inactive {
background:rgba(91, 213, 255, 0.3)
}
</style>
let BASE_URL: string = ''
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, 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) => {
if (response.config.headers.__show_loading) {
store.commit('SET_LOADING', false)
}
// TODO 返回的数据status判断错误操作等……
return response.data
},
(error) => {
store.commit('SET_LOADING', false)
return Promise.reject(error)
}
)
interface ParamsProp {
[propName: string]: unknown
}
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
headers?: { [key: string]: string }
}
/**
* 请求
* @param {String} method [请求方法]
* @param {String} url [请求地址]
* @param {Object} params [请求参数]
* @param {String} contentType [请求头,默认为'application/json;charset=UTF-8']
* @param {Boolean} showLoading [是否显示请求时的loading图,默认为true]
* @param {Object} headers [自定义请求头]
*/
const ajax = ({
method = 'GET',
url,
params = {},
contentType = 'application/json;charset=UTF-8',
showLoading = true,
headers = {},
}: 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': '*',
__show_loading: showLoading,
...headers,
},
}
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<any> {
return ajax({ method: 'GET', ...args })
},
post(args: RequestOptions): AxiosPromise<any> {
// args.contentType = 'application/x-www-form-urlencoded;charset=UTF-8'
return ajax({ method: 'POST', ...args })
},
put(args: RequestOptions): AxiosPromise<any> {
return ajax({ method: 'PUT', ...args })
},
delete(args: RequestOptions): AxiosPromise<any> {
return ajax({ method: 'DELETE', ...args })
},
}
import ajax from './axios'
import api from './api'
export { ajax, api }
---
title: MyAnimate
wrapperClass: my-animate
---
# MyAnimate
基于[animate.css](https://animate.style/)封装的动画组件,对包裹其中的元素实现进入、离开时的动画。
Tag: `m-animate`
```vue demo
<template>
<m-animate enter="fadeInLeft" leave="fadeOutLeft">
<h1 v-show="show">Animate 1</h1>
</m-animate>
<m-animate enter="zoomInDown" leave="zoomOutUp">
<h1 v-show="show">Animate 2</h1>
</m-animate>
<m-animate enter="slideInRight" leave="slideOutRight">
<h1 v-show="show">Animate 3</h1>
</m-animate>
<button @click="show = !show">Click Me!</button>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const show = ref(true)
return {
show,
}
},
}
</script>
```
## Props
| props | description | type | default |
| -------- | ------------------ | ------ | ------- |
| enter | 进入动画名 | string | - |
| leave | 离开动画名 | string | - |
| duration | 动画执行时间(毫秒) | number | 500 |
<template>
<transition
name="animate__animated"
class="animate__animated"
:enter-active-class="`animate__${enter}`"
:leave-active-class="`animate__${leave}`"
:style="`animation-duration: ${duration}ms`"
>
<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,
},
/** 动画执行时间 */
duration: {
type: Number as PropType<number>,
default: 500,
},
},
})
</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('@/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('@/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>
<div class="card-wrapper">
<div class="card-title">
{{ title }}
<div v-if="addition" class="addition">
<MyCount v-if="addition.value" class="count" :value="addition.value" />
<span v-if="addition.unit">{{ addition.unit }}</span>
</div>
<img class="tri" src="@/assets/images/modal-head-tri.png" />
<img class="flag" :src="flag" />
</div>
<div class="card-content">
<slot />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import flag from '@/assets/images/card-model2-flag.png'
import MyCount from '../MyCount/my-count.vue'
export interface AdditionProp {
name?: string
value?: number
unit?: string
}
export default defineComponent({
name: 'Mode2',
components: { MyCount },
props: {
title: {
type: String as PropType<string>,
required: true,
},
addition: {
type: Object as PropType<AdditionProp>,
default: null,
},
},
setup() {
return {
flag,
}
},
})
</script>
<style scoped lang="stylus">
@import '../main.styl'
.card-wrapper
$full()
.card-title
display flex
background linear-gradient(to bottom, transparent, rgba(0,148,255,.3))
position relative
color $card-title-color
font-size $card-title-size
height .2rem
font-weight bold
padding-left .2rem
align-items center
font-size .13rem
box-sizing border-box
border-bottom .01rem solid $primary-border
.tri
position absolute
left 0
width .2rem
.flag
position absolute
height 100%
right .05rem
.addition
display flex
align-items center
margin-left .1rem
.count
color $yellow
font-size .15rem
font-family $font-pang
padding-bottom .03rem
span
color #aaa
font-size .08rem
margin-left .05rem
.card-content
display flex
background $primary-bg
flex-direction column
justify-content space-around
position relative
box-sizing border-box
padding .02rem .05rem
overflow hidden !important
>div
$full()
</style>
---
title: MyCard
wrapperClass: my-card
---
# MyCard
模块包裹组件
Tag: `m-card`
```vue demo
<template>
<m-card title="这是标题"><h1>内容或组件</h1></m-card>
<m-card title="这是标题" mode="2"><h1>内容或组件</h1></m-card>
</template>
```
## Props
| props | description | type | default |
| ----- | ----------- | --------------- | ----------- |
| title | 标题名 | string | - |
| mode | 模式选择 | string / number | 1 |
| enter | 进入动画名 | string | fadeInLeft |
| leave | 离开动画名 | string | fadeOutLeft |
<template>
<MyAnimate :enter="enter" :leave="leave">
<div class="my-card">
<component :is="card" :title="title" v-bind="$attrs">
<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 mode2 from './mode2.vue'
export default defineComponent({
name: 'MyCard',
displayName: 'm-card',
components: {
MyAnimate,
mode1,
mode2,
},
props: {
/** 标题名 */
title: {
type: String as PropType<string>,
required: true,
},
/** 模式选择 默认为1 */
mode: {
type: [String, Number] as PropType<string | number>,
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
case '2':
return mode2
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, PropType, onMounted, watchEffect } from 'vue'
import { use } from 'echarts/core'
import { LineChart, BarChart, PictorialBarChart } from 'echarts/charts'
use([LineChart, BarChart, PictorialBarChart])
import useChartGenerate from './useChartGenerate'
import { BarOption, DatasetComponentOption } from './types'
export default defineComponent({
name: 'MyBar',
displayName: 'm-bar',
props: {
dataset: {
type: Object as PropType<DatasetComponentOption>,
default: null,
},
option: {
type: Object as PropType<BarOption>,
default: null,
},
},
emits: ['init'],
setup(props, ctx) {
const defaultOption: BarOption = {
backgroundColor: 'transparent',
tooltip: {
confine: true,
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
legend: {},
grid: {
left: '2%',
right: '4%',
bottom: '1%',
top: '20%',
containLabel: true,
},
xAxis: [
{
type: 'category',
},
],
yAxis: [
{
type: 'value',
},
],
}
const defaultSeriesItem = {
type: 'bar',
barGap: 0,
emphasis: {
focus: 'series',
},
}
const { chartRef, initChart } = useChartGenerate(
defaultOption,
defaultSeriesItem
)
onMounted(() => {
const instance = initChart(props.dataset, props.option)
instance && ctx.emit('init', instance)
})
watchEffect(() => {
const instance = initChart(props.dataset, props.option)
instance && ctx.emit('init', instance)
})
return {
chartRef,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-chart
$full()
</style>
This diff is collapsed.
<template>
<div ref="chartRef" class="my-chart" />
</template>
<script lang="ts">
import { defineComponent, PropType, onMounted, watchEffect } from 'vue'
import { use } from 'echarts/core'
import { LineChart, BarChart } from 'echarts/charts'
use([LineChart, BarChart])
import useChartGenerate from './useChartGenerate'
import { LineOption, DatasetComponentOption } from './types'
export default defineComponent({
name: 'MyLine',
displayName: 'm-line',
props: {
dataset: {
type: Object as PropType<DatasetComponentOption>,
default: null,
},
option: {
type: Object as PropType<LineOption>,
default: null,
},
},
emits: ['init'],
setup(props, ctx) {
const defaultOption: LineOption = {
backgroundColor: 'transparent',
tooltip: {
confine: true,
trigger: 'axis',
},
legend: {},
grid: {
left: '2%',
right: '5%',
bottom: '1%',
top: '20%',
containLabel: true,
},
xAxis: [
{
type: 'category',
boundaryGap: false,
},
],
yAxis: [
{
type: 'value',
},
],
}
const defaultSeriesItem = {
type: 'line',
smooth: true,
lineStyle: {
width: 2,
},
emphasis: {
focus: 'series',
},
}
const { chartRef, initChart } = useChartGenerate(
defaultOption,
defaultSeriesItem
)
onMounted(() => {
const instance = initChart(props.dataset, props.option)
instance && ctx.emit('init', instance)
})
watchEffect(() => {
const instance = initChart(props.dataset, props.option)
instance && ctx.emit('init', instance)
})
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, PropType, onMounted, watchEffect } from 'vue'
import { use } from 'echarts/core'
import { PieChart } from 'echarts/charts'
use([PieChart])
import useChartGenerate from './useChartGenerate'
import { PieOption, DatasetComponentOption } from './types'
export default defineComponent({
name: 'MyPie',
displayName: 'm-pie',
props: {
dataset: {
type: Object as PropType<DatasetComponentOption>,
default: null,
},
option: {
type: Object as PropType<PieOption>,
default: null,
},
},
emits: ['init'],
setup(props, ctx) {
const defaultOption: PieOption = {
backgroundColor: 'transparent',
tooltip: {
confine: true,
trigger: 'item',
},
legend: {},
grid: {
left: '2%',
right: '4%',
bottom: '1%',
containLabel: true,
},
}
const defaultSeriesItem = {
type: 'pie',
radius: ['30%', '50%'],
center: ['50%', '55%'],
label: {
show: false,
position: 'center',
},
// itemStyle: {
// borderRadius: 2,
// },
}
const { chartRef, initChart } = useChartGenerate(
defaultOption,
defaultSeriesItem
)
onMounted(() => {
const instance = initChart(props.dataset, props.option)
instance && ctx.emit('init', instance)
})
watchEffect(() => {
const instance = initChart(props.dataset, props.option)
instance && ctx.emit('init', instance)
})
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, PropType, onMounted, watchEffect } from 'vue'
import { use } from 'echarts/core'
import { RadarChart } from 'echarts/charts'
use([RadarChart])
import useChartGenerate from './useChartGenerate'
import { RadarOption, DatasetComponentOption } from './types'
export default defineComponent({
name: 'MyRadar',
displayName: 'm-radar',
props: {
dataset: {
type: Object as PropType<DatasetComponentOption>,
default: null,
},
option: {
type: Object as PropType<RadarOption>,
default: null,
},
},
emits: ['init'],
setup(props, ctx) {
const defaultOption: RadarOption = {
backgroundColor: 'transparent',
tooltip: {
confine: true,
trigger: 'item',
},
legend: {},
grid: {
left: '2%',
right: '4%',
bottom: '1%',
containLabel: true,
},
radar: {
axisName: {
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 = {
type: 'radar',
symbol: 'none',
areaStyle: {
opacity: 0.5,
},
emphasis: {
focus: 'item',
},
}
const { chartRef, initChart } = useChartGenerate(
defaultOption,
defaultSeriesItem
)
onMounted(() => {
// eslint-disable-next-line
;(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,
}))
const instance = initChart(props.dataset, props.option)
instance && ctx.emit('init', instance)
})
watchEffect(() => {
// eslint-disable-next-line
;(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,
}))
const instance = initChart(props.dataset, props.option)
instance && ctx.emit('init', instance)
})
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, PropType, onMounted, watchEffect } from 'vue'
import { use } from 'echarts/core'
import { ScatterChart, EffectScatterChart } from 'echarts/charts'
use([ScatterChart, EffectScatterChart])
import useChartGenerate from './useChartGenerate'
import { ScatterOption, DatasetComponentOption } from './types'
export default defineComponent({
name: 'MyScatter',
displayName: 'm-scatter',
props: {
dataset: {
type: Object as PropType<DatasetComponentOption>,
default: null,
},
option: {
type: Object as PropType<ScatterOption>,
default: null,
},
},
emits: ['init'],
setup(props, ctx) {
const defaultOption: ScatterOption = {
backgroundColor: 'transparent',
tooltip: {
confine: true,
trigger: 'item',
},
legend: {},
grid: {
left: '2%',
right: '4%',
bottom: '1%',
containLabel: true,
},
xAxis: {},
yAxis: {},
}
const defaultSeriesItem = {
type: 'scatter',
}
const { chartRef, initChart } = useChartGenerate(
defaultOption,
defaultSeriesItem
)
onMounted(() => {
const instance = initChart(props.dataset, props.option)
instance && ctx.emit('init', instance)
})
watchEffect(() => {
const instance = initChart(props.dataset, props.option)
instance && ctx.emit('init', instance)
})
return {
chartRef,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-chart
$full()
</style>
import { ComposeOption } from 'echarts/core'
import {
DatasetComponentOption as DatasetOption,
TitleComponentOption,
TooltipComponentOption,
GridComponentOption,
LegendComponentOption,
DataZoomComponentOption,
} from 'echarts/components'
import {
LineSeriesOption,
BarSeriesOption,
PictorialBarSeriesOption,
PieSeriesOption,
RadarSeriesOption,
ScatterSeriesOption,
} from 'echarts/charts'
/** Dataset选项参数 */
export type DatasetComponentOption = DatasetOption
/** 附加组件的选项参数(如title、tooltip、legend等) */
export type ComponentsOption = ComposeOption<
| DatasetComponentOption
| TitleComponentOption
| TooltipComponentOption
| GridComponentOption
| LegendComponentOption
| DataZoomComponentOption
>
/** 所有选项参数 */
export type ECOption = ComposeOption<
| LineSeriesOption
| BarSeriesOption
| PictorialBarSeriesOption
| PieSeriesOption
| RadarSeriesOption
| ScatterSeriesOption
> &
ComponentsOption
/** 柱状图选项参数 */
export type BarOption = ComponentsOption &
ComposeOption<LineSeriesOption | BarSeriesOption | PictorialBarSeriesOption>
/** 折线图选项参数 */
export type LineOption = ComponentsOption &
ComposeOption<LineSeriesOption | BarSeriesOption>
/** 饼图选项参数 */
export type PieOption = ComponentsOption & ComposeOption<PieSeriesOption>
/** 雷达图选项参数 */
export type RadarOption = ComponentsOption & ComposeOption<RadarSeriesOption>
/** 散点图选项参数 */
export type ScatterOption = ComponentsOption &
ComposeOption<ScatterSeriesOption>
import { onMounted, onBeforeUnmount, nextTick, shallowRef, Ref } from 'vue'
import { use, init, graphic, ECharts, EChartsType } from 'echarts/core'
import { SVGRenderer } from 'echarts/renderers'
import {
DatasetComponent,
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
DataZoomComponent,
} from 'echarts/components'
use([
SVGRenderer,
DatasetComponent,
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
DataZoomComponent,
])
import { RadarSeriesOption } from 'echarts/charts'
import { ECOption, RadarOption, DatasetComponentOption } from './types'
interface ReturnProp {
chartRef: Ref<null | HTMLElement>
initChart: (
dataset: DatasetComponentOption,
option?: ECOption,
) => EChartsType | undefined
}
/**
* 根据屏幕调整尺寸
* @param myChart Echarts实例引用
* @param chartRef Echarts容器引用
*/
const setResizeAble = (myChart: Ref, chartRef: Ref) => {
const handleResize = () => {
if (!myChart.value) return
myChart.value.resize()
}
onMounted(async () => {
window.addEventListener('resize', handleResize)
await nextTick()
myChart.value = init(chartRef.value as HTMLElement, 'dark')
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
if (!myChart.value) return
myChart.value.dispose()
myChart.value = null
})
}
/**
* 由于radar图的dataset支持不好
* 故此根据数据集里source数组里的seriesName来重置tooltip显示的名字
* @param defaultOption 图表默认配置
* @param defaultSeriesItem 默认series项
* @param dataset 数据集
* @returns 转换后的图表配置
*/
const transRadarOption = (
defaultOption: RadarOption,
defaultSeriesItem: RadarSeriesOption,
dataset: DatasetComponentOption,
): RadarOption => {
const source = dataset && (dataset.source as any[])
const length = (source && source.length) || 0
const result = Object.assign({}, defaultOption)
const seriesData: RadarSeriesOption[] = []
for (let index = 0; index < length; index++) {
const data: number[] = []
Object.keys(source[index]).forEach((key) => {
if (key !== 'seriesName') {
data.push(source[index][key])
}
})
seriesData.push(
Object.assign({}, defaultSeriesItem, {
value: data,
name: source[index].seriesName,
}),
)
}
result.series = [
{
type: 'radar',
data: seriesData as any[],
},
]
if (dataset) result.dataset = dataset
return result
}
/**
* 设置color选项中
* 如果有数组项则封装为渐变
* @param options 图表配置
*/
const transLinearColorOption = (options: ECOption): ECOption => {
if (!options.color || (options.color as string[]).length === 0) {
return options
}
const result = Object.assign({}, options)
result.color = (result.color as string[]).map((color) => {
if (Array.isArray(color)) {
if (color.length < 2) {
return color[0]
}
return new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: color[0],
},
{
offset: 1,
color: color[1],
},
])
}
return color
})
return result
}
/**
* 将defaultSeriesItem和dataset填充进defaultOption形成初始配置项
* @param defaultOption 默认图表配置
* @param defaultSeriesItem 默认series项
* @param dataset 数据集
* @returns 初始配置项
*/
const defaultConfig = <T>(
defaultOption: ECOption,
defaultSeriesItem: T,
dataset: DatasetComponentOption,
): ECOption => {
const result = Object.assign({}, defaultOption)
let length = 0
result.series = []
if (dataset) {
result.dataset = dataset
if (dataset.dimensions) {
length = dataset.dimensions.length - 1
} else if (dataset.source && Array.isArray(dataset.source)) {
length = Object.keys(dataset.source[0] || []).length - 1
}
}
for (let index = 0; index < length; index++) {
result.series.push(defaultSeriesItem)
}
return result
}
/**
* 合并默认配置项和自定义配置项
* @param defaultOption 默认图表配置
* @param customOption 自定义图表配置
*/
const mergeOptions = <T>(defaultOption: T, customOption: T) => {
if (!customOption) return
for (const key in customOption) {
if (Array.isArray(customOption[key])) {
defaultOption[key] = customOption[key]
continue
}
if (customOption[key] !== null && typeof customOption[key] === 'object') {
if (!defaultOption[key]) {
defaultOption[key] = customOption[key]
} else {
mergeOptions(defaultOption[key], customOption[key])
}
} else {
defaultOption[key] = customOption[key]
}
}
}
/**
* 构建图表组件公共方法
* @param defaultOption 默认配置
* @param defaultSeriesItem 默认的series项
* @returns chartRef:Echarts容器引用, initChart:初始化方法
*/
const useChartGenerate = <T>(
defaultOption: ECOption,
defaultSeriesItem: T,
): ReturnProp => {
const chartRef = shallowRef<null | HTMLElement>(null)
const myChart = shallowRef<null | ECharts>(null)
setResizeAble(myChart, chartRef)
/**
* 根据数据集和配置初始化图表
* @param dataset 数据集
* @param option 自定义图表配置
*/
const initChart = (
dataset: DatasetComponentOption,
option?: ECOption,
): EChartsType | undefined => {
if (!myChart.value) return
const config =
(defaultSeriesItem as any).type === 'radar'
? transRadarOption(
defaultOption as RadarOption,
defaultSeriesItem as RadarSeriesOption,
dataset,
)
: defaultConfig(defaultOption, defaultSeriesItem, dataset)
mergeOptions(config, option)
myChart.value.setOption(transLinearColorOption(config), true)
return myChart.value
}
return { chartRef, initChart }
}
export default useChartGenerate
---
title: MyCount
wrapperClass: my-count
---
# MyCount
数字跳动展示组件
Tag: `m-count`
```vue demo
<template>
<div><m-count :value="99999" /></div>
<div><m-count :value="99999" :speed="1" /></div>
<div><m-count :value="99999" :decimal="2" /></div>
<div><m-count :value="99999" auto-play :duration="5" /></div>
</template>
```
## Props
| props | description | type | default |
| --------- | ---------------------------------------- | --------------- | ------- |
| value | 展示数字 | number / string | 0 |
| decimal | 保留几位小数 | number / string | 0 |
| speed | 数字跳动速度(秒) | number / string | 2 |
| auto-play | 是否定时自动跳动 | boolean | false |
| duration | auto-play 开启后,每次跳动间隔时间(秒) | number / string | 10 |
// 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>
<b 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 || 0)
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}`)
return
}
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 || 0
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>
---
title: MyDrawer
wrapperClass: my-drawer
---
# MyDrawer
侧边抽屉组件
Tag: `m-drawer`
```vue demo
<template>
<m-drawer v-model="show" @close="handleClose">
<h1>内容……</h1>
</m-drawer>
<button @click="show = true">Click Me!</button>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const show = ref(false)
const handleClose = () => {
console.log(
'可以使用close回调,或者直接watch v-model的值进行关闭后的操作'
)
}
return {
show,
handleClose,
}
},
}
</script>
```
## Props
| props | description | type | default |
| -------------------- | ----------- | ------- | ------- |
| v-model / modelValue | 值 | boolean | false |
| width | 抽屉宽度 | string | 49vw |
## Events
| events | description | arguments |
| ------ | ---------------- | --------- |
| close | 关闭抽屉触发事件 | - |
<template>
<teleport to="body">
<MyAnimate enter="fadeInRight" leave="fadeOutRight">
<div v-if="modelValue" class="my-drawer" :style="{ width: width }">
<img
src="@/assets/images/close-btn3.png"
class="close-btn"
draggable="false"
@click="closeDrawer"
/>
<div class="content"><slot /></div>
</div>
</MyAnimate>
</teleport>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import MyAnimate from '../MyAnimate/my-animate.vue'
export default defineComponent({
name: 'MyDrawer',
displayName: 'm-drawer',
components: { MyAnimate },
props: {
/** 值 */
modelValue: {
type: Boolean as PropType<boolean>,
default: false,
},
/** 宽度 */
width: {
type: String as PropType<string>,
default: '49vw',
},
// /** 点击蒙层是否允许关闭 */
// maskClosable: {
// type: Boolean as PropType<boolean>,
// default: true,
// },
},
emits: ['update:modelValue', 'close'],
setup(_, ctx) {
const closeDrawer = () => {
ctx.emit('update:modelValue', false)
ctx.emit('close')
}
return {
closeDrawer,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-drawer
// background url('@/assets/images/drawer-bg.png') 100% / 100% 100% no-repeat
background-color $table-title-bg
position fixed
top .05rem
right @top
bottom @top
z-index 9999
padding .15rem
$blur()
color #fff
font-size .1rem
.close-btn
position absolute
top inherit
bottom inherit
margin auto
width .24rem
left -@width
cursor pointer
transition transform .3s ease
&:hover
transform scale(1.2)
</style>
---
title: MyEmpty
wrapperClass: my-empty
---
# MyEmpty
无数据时展示
Tag: `m-empty`
```vue demo
<template>
<m-empty text="建设中" />
</template>
```
## Props
| props | description | type | default |
| ----- | ----------- | ------ | -------- |
| text | 展示信息 | string | 暂无数据 |
<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>
---
title: MyForm
wrapperClass: my-form
---
# MyForm
Form 表单组件
Tag: `m-form`
### Basic Use
```vue demo
<template>
<m-form
:template="[
'key1:标题1|key2:标题2|key3:标题3',
'key4:标题4|key5:标题5|key6:标题6',
]"
:data="formData"
/>
</template>
<script>
export default {
setup() {
return {
formData: {
key1: '测试1',
key2: '测试2',
key3: '测试3',
key4: '测试4',
key5: '测试5',
key6: '测试6',
},
}
},
}
</script>
```
### Advanced Use
```vue demo
<template>
<m-form
:template="[
'name:小区名称|address:小区地址*3',
'belong:所属居委会|buildingNum:总门牌幢数|roomNum:总户数>formatRoomNum|',
'||buildingArea:总建筑面积>formatArea|area:占地面积>formatArea',
'rang:小区四至范围|excludeRang:四至范围不包括',
'photo:照片#image',
]"
:data="formData"
:formatter="formatter"
/>
</template>
<script>
export default {
setup() {
return {
formData: {
name: '测试文字',
photo: 'https://avatars2.githubusercontent.com/u/43328103?v=4',
address: '测试文字',
belong: '测试文字',
buildingNum: '测试文字',
roomNum: '测试文字',
buildingArea: '测试文字',
area: '测试文字',
rang: '测试文字',
excludeRang: '测试文字',
},
formatter: {
formatArea: (area) => area + '㎡',
formatRoomNum: (val) => val + '万户',
},
}
},
}
</script>
```
## Props
| props | description | type | default |
| --------------------- | ---------------------- | -------- | ------- |
| [template](#template) | 布局模板 | string[] | - |
| data | 表单数据 | object | - |
| formatter | 数据格式化方法对象集合 | object | - |
| label-width | label 宽度 | string | 1rem |
### <i id="template">template introduction</i>
| tag | description |
| --- | ------------------------------------------------------------------------ |
| \| | 将每行划分为 col 块 |
| : | 冒号前为数据 key,冒号后为 label 名称 |
| \* | 后跟该 col 块所占行的比例 |
| > | 后跟该 col 块所需使用的 format 数据方法(方法需在 formatter 参数中定义) |
| # | 后跟该 col 块所属的特殊类型(目前仅实现 image) |
| = | 放在 col 块头则 label 左对齐,尾则右对齐(默认为右对齐) |
<template>
<div class="my-form">
<div v-for="(row, rowIndex) in template" :key="rowIndex" class="row">
<div
v-for="(col, colIndex) in row.split('|')"
:key="colIndex"
class="col"
:style="{ flex: calcWidth(col) }"
>
<p :style="`width: ${labelWidth}; text-align: ${calcAlign(col)}`">
{{ getLabel(col) }}
</p>
<img
v-if="getImage(col).isImg"
:src="getImage(col).src"
:draggable="false"
@click.stop="handleViewImage(getImage(col).src)"
/>
<p v-else class="content">
{{ formatData(col) }}
</p>
</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, data?: DataType) => T
}
interface DataType {
[propName: string]: string | number | string[] | number[]
}
export default defineComponent({
name: 'MyForm',
displayName: 'm-form',
components: { MyModal },
props: {
/** 布局模板 */
template: {
type: Array as PropType<string[]>,
required: true,
},
/** 数据 */
data: {
type: Object as PropType<DataType>,
default: null,
},
/** 数据的格式化方法对象集合 */
formatter: {
type: Object as PropType<FormatterType>,
default: null,
},
/** label宽度 */
labelWidth: {
type: String as PropType<string>,
default: '1rem',
},
},
setup(props) {
const datasource = computed(() => props.data)
const calcWidth = (key: string): number => {
if (!key) return 1
if (key.match(/\*(\d*)[#>:]?/)) {
return +RegExp.$1
}
return 1
}
const getLabel = (key: string): string => {
if (!key) return ''
if (key.match(/:([\u4e00-\u9fa5]*[^#=>\*]*)[#>\*]?/)) {
return RegExp.$1 + ':'
}
return ''
}
const imgSrc = ref<string | null>(null)
const showImgModal = ref(false)
const handleViewImage = (src: string): void => {
imgSrc.value = src
showImgModal.value = true
}
/** 判断并返回图片src */
const getImage = (key: string): { isImg: boolean; src: string } => {
const result = {
isImg: false,
src: '',
}
if (key && key.match(/#(\w*)[>:=]?/)) {
if (RegExp.$1 === 'image') {
key.match(/=?(\w*)[#>:]?/)
result.src = datasource.value[RegExp.$1] as string
result.isImg = true
}
}
return result
}
const formatData = (key: string): unknown => {
if (!key) return ''
const { formatter, data } = props
key.match(/=?(\w*)[#>\*:]?/)
const dataKey = RegExp.$1
if (formatter && key.match(/>(\w*)[#\*:]?/)) {
return formatter[RegExp.$1](data[dataKey], data)
}
return data[dataKey] || ''
}
const calcAlign = (key: string): string => {
if (!key) return 'right'
const index = key.indexOf('=')
if (index === 0) return 'left'
if (index === key.length - 1) return 'right'
return 'right'
}
return {
calcWidth,
getLabel,
handleViewImage,
getImage,
imgSrc,
showImgModal,
formatData,
calcAlign,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-form
$full()
.row
display flex
background $table-content-bg
padding .08rem 0
&:nth-child(odd)
background transparent
.col
display flex
align-items center
p
padding 0 .05rem
box-sizing border-box
>.content
flex 1
padding-left 0
>img
max-height .6rem
cursor pointer
</style>
---
title: MyGrid
wrapperClass: my-grid
---
# MyGrid
大屏布局组件
Tag: `m-grid`
```vue demo
<template>
<!-- 真实使用中不需要这个min-height, m-grid默认高度为父容器的100% -->
<m-grid
style="min-height: 50vh;"
:template="[
'title title title',
'box1 . box3',
'box2 . box3',
'box2 box4 box4',
]"
columns="1fr 1.5fr 1fr"
rows="0.4rem 1fr 1fr 1fr"
>
<m-title area="title">大标题</m-title>
<div area="box1" style="background:skyblue;padding:.1rem">
<h3>有area名的元素会被摆放至template中的同名位置</h3>
</div>
<m-card title="box2" area="box2">
<m-grid
:template="['inner1 inner2 inner3', 'inner4 inner4 inner3']"
columns="1rem 20% auto"
rows="30% 1fr"
>
<div area="inner1" style="background:gold"></div>
<div area="inner2" style="background:gold"></div>
<div area="inner3" style="background:gold"></div>
<div area="inner4" style="background:gold"></div>
</m-grid>
</m-card>
<m-card title="box3" area="box3"></m-card>
<div area="box4" style="background:brown;padding:.1rem"></div>
</m-grid>
</template>
```
## Props
| props | description | type | default |
| -------- | ------------ | -------- | ------- |
| template | 区块摆放模板 | string[] | - |
| columns | 列宽比例 | string | - |
| rows | 行高比例 | string | - |
| gap | 区块间隔 | string | 0.05rem |
<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(() => {
if (!gridRef.value) return
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()
position relative
display grid
overflow hidden
</style>
---
title: MyLoader
wrapperClass: my-loader
---
# MyLoader
Loading 组件
Tag: `m-loader`
```vue demo
<template>
<m-loader v-if="show" />
<button @click="handleClick">Click Me !</button>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const show = ref(false)
const handleClick = () => {
show.value = true
setTimeout(() => {
show.value = false
}, 3000)
}
return {
show,
handleClick,
}
},
}
</script>
```
## Props
| props | description | type | default |
| ----- | ----------- | ---- | ------- |
| - | - | - | - |
<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%
transform translate(-50%, -50%) rotate(0deg)
.outer
width .35rem
height @width
animation spin 2.5s linear infinite
.middle
width .21rem
height @width
animation spin 2s linear reverse infinite
.inner
width .08rem
height @width
animation spin 1.5s linear infinite
@keyframes spin {
to {
transform translate(-50%, -50%) 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'
export default defineComponent({
name: 'MyMap',
displayName: 'm-map',
})
</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>
---
title: MyModal
wrapperClass: my-modal
---
# MyModal
弹窗组件
Tag: `m-modal`
```vue demo
<template>
<m-modal v-model="show" title="这是个弹窗" @close="handleClose">
<h1>内容……</h1>
</m-modal>
<button @click="show = true">Click Me!</button>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const show = ref(false)
const handleClose = () => {
console.log('可以使用close回调,或者直接watch v-model的值进行操作')
}
return {
show,
handleClose,
}
},
}
</script>
```
## Props
| props | description | type | default |
| -------------------- | -------------------- | ------- | ---------- |
| v-model / modelValue | 值 | boolean | false |
| enter | 进入动画 | string | fadeInDown |
| leave | 离开动画 | string | fadeOutUp |
| title | 标题 | string | - |
| width | 弹窗宽度 | string | 32% |
| offset | 弹窗水平偏移 | string | 0 |
| mask-closable | 点击蒙层是否允许关闭 | boolean | true |
## Events
| events | description | arguments |
| ------ | ---------------- | --------- |
| close | 关闭弹窗触发事件 | - |
<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(_, 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>
---
title: MyProgress
wrapperClass: my-progress
---
# MyProgress
进度条组件
Tag: `m-progress`
```vue demo
<template>
<m-progress :value="80" />
<m-progress
:value="67"
:msg="{ name: '测试', value: 67, unit: '%' }"
:color="['red', 'gold']"
/>
</template>
```
## Props
| props | description | type | default |
| ----------- | ------------------------- | ----------------- | ---------------------- |
| color | 进度条颜色 如为数组则渐变 | string / string[] | ['#0094FF', '#1EFBFF'] |
| value | 进度百分值 | number | 0 |
| [msg](#msg) | 附加信息 | object | - |
| height | 进度条高度(rem) | number | 0.07 |
### <i id="msg">msg props</i>
| props | description | type | default |
| ----- | ----------- | ------ | ------- |
| name | 名字 | string | - |
| value | 展示值 | number | - |
| unit | 值后边单位 | string | - |
<template>
<div class="my-progress">
<div v-if="msg" class="msg">
<p v-if="msg.name">{{ msg.name }}</p>
<p v-if="msg.value">
<MonitorCount :value="msg.value" />
<span v-if="msg.unit">{{ msg.unit }}</span>
</p>
</div>
<div class="bar" :style="{ height: `${height}rem` }">
<div class="inner" :style="{ width: `${percent}%`, ...barColor }"></div>
<div class="bg" :style="bgColor"></div>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
import MonitorCount from '../MyCount/my-count.vue'
export interface ProgressProps {
name?: string
value?: number
unit?: string
}
export default defineComponent({
name: 'MyProgress',
displayName: 'm-progress',
components: { MonitorCount },
props: {
/** 进度条颜色 如为数组则渐变 */
color: {
type: [String, Array] as PropType<string | string[]>,
default: () => ['#0094FF', '#1EFBFF'],
},
/** 进度百分值 */
value: {
type: Number as PropType<number>,
default: 0,
},
/** 附加信息 */
msg: {
type: Object as PropType<ProgressProps>,
default: null,
},
/** 进度条高度 */
height: {
type: Number as PropType<number>,
default: 0.07,
},
},
setup(props) {
const barColor = computed(() => {
if (Array.isArray(props.color)) {
return {
background: `linear-gradient(to right, ${props.color[0]}, ${props.color[1]})`,
}
}
return { background: props.color }
})
const bgColor = computed(() => {
if (Array.isArray(props.color)) {
return {
background: props.color[0],
}
}
return { background: props.color }
})
const percent = computed(() => (props.value > 100 ? 100 : props.value))
return {
barColor,
bgColor,
percent,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-progress
width 100%
.msg
display flex
align-items center
justify-content space-between
padding 0 .05rem
>p
color #ccc
&:last-child
color $secondary-color
font-size .14rem
font-weight bold
span
font-size .1rem
.bar
width 100%
position relative
border-radius .1rem
>div
position absolute
border-radius inherit
left 0
right 0
bottom 0
top 0
&.inner
transition width .5s ease-in-out
&.bg
opacity .3
</style>
---
title: MyScroll
wrapperClass: my-scroll
---
# MyScroll
轮播组件
Tag: `m-scroll`
```vue demo
<template>
<div style="display:inline-block;width:50%;height:2rem;overflow:hidden;">
<m-scroll :length="10" :limit="3">
<div
v-for="i in 10"
:key="i"
style="height: 0.8rem;text-align:center;background:skyblue;margin-bottom:.05rem;line-height:0.8rem;"
>
{{ i }}
</div>
</m-scroll>
</div>
<div style="display:inline-block;width:50%;height:2rem;overflow:hidden;">
<m-scroll :length="colors.length" :limit="2" mode="2" :step="2">
<div
v-for="color in colors"
:key="color"
:style="`height: 2rem;text-align:center;background:${color};line-height:2rem;`"
>
{{ color }}
</div>
</m-scroll>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const colors = ref(['red', 'blue', 'green', 'brown', 'pink'])
return {
colors,
}
},
}
</script>
```
## Props
| props | description | type | default |
| -------- | --------------------------------------------------------------- | --------------- | ------- |
| mode | 模式 | number / string | 1 |
| length | 数据长度 | number | - |
| limit | 数据长度小于次值则不轮播 | number | - |
| speed | mode1 的轮播速度(毫秒) | number | 50 |
| step | mode2 的每个元素高度, 有 margin 记得也要把 margin 算进去(rem) | number | 0 |
| duration | mode2 的轮播间隔(毫秒) | number | 4000 |
<template>
<div class="my-scroll" @mouseenter.prevent="stop" @mouseleave.prevent="start">
<div ref="contentRef">
<slot />
<template v-if="isNeedMockContent">
<slot />
</template>
</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: {
/** 模式 */
mode: {
type: [Number, String] as PropType<number | string>,
default: 1,
},
/** 数据长度 */
length: {
type: Number as PropType<number>,
required: true,
},
/** 小于此长度则不轮播 */
limit: {
type: Number as PropType<number>,
required: true,
},
/** mode1的轮播速度 */
speed: {
type: Number as PropType<number>,
default: 50,
},
/** mode2的每个元素高度 */
step: {
type: Number as PropType<number>,
default: 0,
},
/** mode2的轮播间隔 */
duration: {
type: Number as PropType<number>,
default: 4000,
},
},
setup(props) {
const contentRef = 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.length !== 0) {
console.error('MyScroll 需要length参数!')
return
}
if (!props.limit) {
console.error('MyScroll 需要limit参数!')
return
}
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
if (!content) return
let height = content.offsetHeight / 2
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)`
}, props.speed)
}
const startMode2 = () => {
if (!props.step) {
console.error('MyScroll mode2模式需要step参数!')
return
}
const content = contentRef.value
if (!content) return
const len = ((content.children && content.children.length) || 0) / 2
timer.value = +setInterval(() => {
if (index.value < len) {
index.value += 1
content.style.transition = 'transform 0.5s ease-in-out'
} else {
index.value = 0
content.style.transition = 'none'
}
content.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
content && (content.style.transform = 'translateY(0)')
await nextTick()
start()
}
)
return {
isNeedMockContent,
start,
stop,
contentRef,
timer,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-scroll
$full()
overflow hidden
z-index 99
</style>
---
title: MyStep
wrapperClass: my-step
---
# MyStep
步骤条组件
Tag: `m-step`
```vue demo
<template>
<m-step :steps="['第一步', '第二步', '第三步', '第四步']" current="3" />
<m-step
:msg="['哈哈哈', `<b style='color:red;'>哈哈哈哈哈</b>`]"
:steps="['第一步', '第二步', '第三步', '第四步']"
current="2"
/>
<m-step
:msg="['msg1', 'msg2', 'msg3', 'msg4']"
:steps="['第一步', '第二步', '第三步', '第四步']"
current="1"
/>
</template>
```
## Props
| props | description | type | default |
| ------- | ------------------ | --------------- | ------- |
| steps | 节点名称 | string[] | - |
| current | 当前节点索引 | number / string | 0 |
| msg | 每个节点的附加信息 | string[] | - |
<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
.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
z-index 1
.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-sub">
<div class="title">
<span />
<p><slot /></p>
<div v-if="addition" class="addition">
<MyCount v-if="addition.value" class="count" :value="addition.value" />
<span v-if="addition.unit">{{ addition.unit }}</span>
</div>
<input
v-if="searchAble"
v-model="inputValue"
:class="{ open: isOpened }"
class="search-bar"
type="text"
@keypress.enter="handleSearch"
/>
<img
v-if="searchAble"
class="search-btn"
src="@/assets/images/search.png"
draggable="false"
@click.prevent="handleOpenSearchBar"
/>
</div>
<img src="@/assets/images/sub-dot.png" draggable="false" />
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, ref } from 'vue'
import MyCount from '../MyCount/my-count.vue'
export interface AdditionProp {
name?: string
value?: number
unit?: string
}
export default defineComponent({
name: 'MySub',
displayName: 'm-sub',
components: { MyCount },
props: {
addition: {
type: Object as PropType<AdditionProp>,
default: null,
},
searchAble: {
type: Boolean as PropType<boolean>,
default: false,
},
},
emits: ['search'],
setup(props, ctx) {
const isOpened = ref(false)
const inputValue = ref('')
const handleSearch = () => {
ctx.emit('search', inputValue.value.trim())
}
const handleOpenSearchBar = () => {
if (!isOpened.value) {
isOpened.value = true
} else {
if (inputValue.value) {
handleSearch()
} else {
isOpened.value = false
}
}
}
return {
isOpened,
inputValue,
handleSearch,
handleOpenSearchBar,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-sub
display flex
align-items center
>.title
display flex
align-items center
font-family $font-zcool
color $secondary-color
flex 1
font-size .13rem
>span
display inline-block
width .02rem
height @width
background $secondary-color
margin-right .05rem
.addition
display flex
align-items center
margin-left .1rem
.count
color $yellow
font-size .15rem
font-family $font-pang
padding-bottom .03rem
span
color #aaa
font-size .08rem
margin-left .05rem
.search-bar
display block
width 0
height .16rem
background rgba(0,0,0,0.1)
border-radius .02rem
outline none
margin 0 .05rem
padding 0
box-sizing border-box
font-family $font-din
font-size .1rem
transition all .3s ease-in-out
opacity 0
&.open
width 1rem
padding .02rem .05rem
border .01rem solid $blue
opacity 1
.search-btn
width .14rem
height @width
cursor pointer
transition transform .3s ease-in-out
&:hover
transform scale(1.3) rotate(360deg)
>img
width .6rem
</style>
---
title: MyTable
wrapperClass: my-table
---
# MyTable
Table 组件
Tag: `m-table`
```vue demo
<template>
<m-table
:template="[
'标题1|标题2*2|标题3*2',
'key1|key2>customFormatter|key3#image',
]"
:data="tableData"
:formatter="{ customFormatter }"
/>
</template>
<script>
export default {
setup() {
return {
tableData: [
{
key1: '文字1',
key2: '文字2',
key3: 'https://avatars2.githubusercontent.com/u/43328103?v=4',
},
{
key1: '文字1',
key2: '文字2',
key3: 'https://avatars2.githubusercontent.com/u/43328103?v=4',
},
{
key1: '文字1',
key2: '文字2',
key3: 'https://avatars2.githubusercontent.com/u/43328103?v=4',
},
{
key1: '文字1',
key2: '文字2',
key3: 'https://avatars2.githubusercontent.com/u/43328103?v=4',
},
],
customFormatter: (val) => {
return val + '哈哈哈'
},
}
},
}
</script>
```
## Props
| props | description | type | default |
| --------------------- | ---------------------- | -------- | ------- |
| [template](#template) | 布局模板 | string[] | - |
| data | 表单数据 | [] | - |
| formatter | 数据格式化方法对象集合 | object | - |
| selectable | 是否可触发点击事件 | boolean | false |
### <i id="template">template introduction</i>
| tag | description |
| --- | ----------------------------------------------------------------------- |
| \| | 将每行划分为 col 块 |
| \* | 后跟该列所占行的比例 |
| > | 后跟该项所需使用的 format 数据方法(方法需在 formatter 参数中定义) |
| # | 后跟该项所属的特殊类型(目前仅实现 image) |
| = | 放在 template 第一项 col 块头则标题左对齐,尾则右对齐(默认为居中对齐) |
## Events
| events | description | arguments |
| ------ | -------------------------------- | --------- |
| select | 开启 selectable 后点击行触发事件 | rowData |
<template>
<div class="my-table">
<div class="table-title">
<div
v-for="(title, index) in layout.header"
:key="title"
:style="`flex:${
Array.isArray(calcWidth) ? calcWidth[index] : calcWidth
}`"
>
<p :style="`text-align:${calcAlign[index]}`">
{{ title.split('*')[0].replace('=', '') }}
</p>
</div>
</div>
<div class="table-content">
<div
v-for="(item, index) in data"
:key="index"
:class="{ selectable: selectable }"
@click.prevent="handleClick(item)"
>
<div
v-for="(key, i) in layout.keys"
:key="key"
:style="`flex:${
Array.isArray(calcWidth) ? calcWidth[i] : calcWidth
};text-align:${calcAlign[i]}`"
>
<img
v-if="getImage(key, item).isImg"
:src="getImage(key, item).src"
:draggable="false"
@click.stop="handleViewImage(getImage(key, item).src)"
/>
<p v-else :style="`text-align:${calcAlign[i]}`">
{{ formatData(key, item) }}
</p>
</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, data?: DataType) => T
}
interface DataType {
[propName: string]: string | number | string[] | number[]
}
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 {
header: [],
keys: [],
}
}
const header = template[0].split('|')
const keys = template[1].split('|')
return { header, keys }
})
const handleClick = (data: DataType): void => {
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<('center' | 'left' | 'right')[]>(() => {
if (!layout.value) return ['center', 'center', '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): void => {
imgSrc.value = src
showImgModal.value = true
}
/** 判断并返回图片src */
const getImage = (
key: string,
item: DataType
): { isImg: boolean; src: string } => {
const result = {
isImg: false,
src: '',
}
if (key.match(/#(\w*)[>:]?/)) {
if (RegExp.$1 === 'image') {
key.match(/(\w*)[#>:]?/)
result.src = item[RegExp.$1] as string
result.isImg = true
}
}
return result
}
const formatData = (key: string, data: DataType): unknown => {
const { formatter } = props
key.match(/(\w*)[#>:]?/)
const dataKey = RegExp.$1
if (formatter && key.match(/>(\w*)[#:]?/)) {
return formatter[RegExp.$1](data[dataKey], data)
}
return data[dataKey] || ''
}
return {
layout,
handleClick,
calcWidth,
calcAlign,
imgSrc,
handleViewImage,
showImgModal,
getImage,
formatData,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-table
$full()
p
padding .05rem
margin 0
flex 1
box-sizing border-box
.table-title
display flex
background $table-title-bg
color $blue
font-weight bold
box-sizing inherit
>div
box-sizing inherit
.table-content
>div
display flex
align-items center
background $table-content-bg
box-sizing inherit
&:nth-child(odd)
background transparent
&.selectable
cursor pointer
&:hover
color $table-content-hover-color
background $table-content-hover-bg
img
max-height 1rem
max-width 100%
cursor pointer
margin-top .05rem
</style>
---
title: MyTitle
wrapperClass: my-title
---
# MyTitle
大屏标题组件
Tag: `m-title`
```vue demo
<template>
<m-title>XXXXXXX数据平台1</m-title>
<m-title :bg-img="bgImg">XXXXXXX数据平台2</m-title>
</template>
<script>
import bgImg from '@/assets/images/title-bg2.png'
export default {
setup() {
return {
bgImg,
}
},
}
</script>
```
## Props
| props | description | type | default |
| ------ | ----------- | ----- | ------------ |
| bg-img | 背景图片 | Image | title-bg.png |
<template>
<div class="my-title">
<img :src="bgImg" class="bg" />
<div class="date">{{ date }} {{ time }}</div>
<h1><slot /></h1>
</div>
</template>
<script lang="ts">
import { defineComponent, onBeforeUnmount, PropType, ref } from 'vue'
import { getDate, getTime } from '../util'
import bgImg from '@/assets/images/title-bg.png'
export default defineComponent({
name: 'MyTitle',
displayName: 'm-title',
props: {
/** 背景图片 默认为title-bg.png */
bgImg: {
type: String as PropType<string>,
default: bgImg,
},
},
setup() {
const date = ref(getDate())
const time = ref('')
const timer = ref<NodeJS.Timer | 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>
---
title: MyWave
wrapperClass: my-wave
---
# MyWave
百分比波浪球组件
Tag: `m-wave`
```vue demo
<template>
<m-wave :value="67" size=".5rem">67</m-wave>
<m-wave :value="80" size=".5rem" color="gold">
<m-count :value="80" />
</m-wave>
</template>
```
## Props
| props | description | type | default |
| ----- | ----------- | --------------- | ------- |
| value | 值 | number / string | 0 |
| size | 球体宽高 | string | 0.4rem |
| color | 球体颜色 | string | #4F953B |
<template>
<div
class="my-wave-ball"
:style="{
width: size,
height: size,
borderColor: color,
boxShadow: `0 0 .08rem 0 ${color} inset`,
}"
>
<div
v-if="value"
class="before"
:style="{ top: `${percent}%`, background: color }"
/>
<div
v-if="value"
class="after"
:style="{ top: `${percent}%`, background: color }"
/>
<p><slot /></p>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
export default defineComponent({
name: 'MyWave',
displayName: 'm-wave',
props: {
/** 值 */
value: {
type: [Number, String] as PropType<number | string>,
default: 0,
},
/** 球体宽高 默认0.4rem */
size: {
type: String as PropType<string>,
default: '.4rem',
},
/** 球体颜色 默认#4F953B */
color: {
type: String as PropType<string>,
default: '#4F953B',
},
},
setup(props) {
const percent = computed(() => 250 - +props.value)
return {
percent,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-wave-ball
position relative
background transparent
border-radius 50%
overflow hidden
border .02rem solid
box-sizing content-box
transform translateZ(0)
$center()
p
z-index 30
.before
.after
content ''
position absolute
width 200%
height @width
left 50%
opacity .5
border-radius 40%
animation rotate 10s ease infinite alternate
z-index 20
.after
radius 30%
opacity .8
width 195%
height @width
animation rotate 20s linear infinite alternate
z-index 10
@keyframes rotate {
from {
transform translate(-46%, -70%) rotate(0)
}
to {
transform translate(-50%, -72%) rotate(360deg)
}
}
</style>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
@keyframes move1 {
0% {
transform: translateX(-500px) scaleX(2.5);
}
100% {
transform: translateX(0) scaleX(2.5);
}
}
@keyframes move2 {
0% {
transform: translateX(-600px) scaleX(3);
}
100% {
transform: translateX(0) scaleX(3);
}
}
@keyframes move3 {
0% {
transform: translateX(-800px) scaleX(4);
}
100% {
transform: translateX(0) scaleX(4);
}
}
#wave1 {
animation: move1 2s linear infinite;
}
#wave2 {
animation: move2 1.7s linear infinite;
}
#wave3 {
animation: move3 2s linear infinite;
}
</style>
</head>
<body>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="600px" height="300px">
<!-- <defs>
<g id="whole" fill-opacity="0.3" fill="cornflowerblue">
<path id="wave1" d="M0,200c50-50,99,0,99,0c51,50,101,0,101,0c50-50,100,0,100,0c50,50,100,0,100,0c50-50,100,0,100,0
c50,50,100,0,100,0v200H0V200z"></path>
<path id="wave2" d="M0,200c50-50,99,0,99,0c51,50,101,0,101,0c50-50,100,0,100,0c50,50,100,0,100,0c50-50,100,0,100,0
c50,50,100,0,100,0v200H0V200z"></path>
<path id="wave3" d="M0,200c50-50,99,0,99,0c51,50,101,0,101,0c50-50,100,0,100,0c50,50,100,0,100,0c50-50,100,0,100,0
c50,50,100,0,100,0v200H0V200z"></path>
</g>
</defs> -->
<!-- <defs>
<g id="whole" fill-opacity="0.3" fill="black" mask="url(#mask)">
<path id="wave1" d="M0,200c50-50,99,0,99,0c51,50,101,0,101,0c50-50,100,0,100,0c50,50,100,0,100,0c50-50,100,0,100,0
c50,50,100,0,100,0v200H0V200z"></path>
<path id="wave2" d="M0,200c50-50,99,0,99,0c51,50,101,0,101,0c50-50,100,0,100,0c50,50,100,0,100,0c50-50,100,0,100,0
c50,50,100,0,100,0v200H0V200z"></path>
<path id="wave3" d="M0,200c50-50,99,0,99,0c51,50,101,0,101,0c50-50,100,0,100,0c50,50,100,0,100,0c50-50,100,0,100,0
c50,50,100,0,100,0v200H0V200z"></path>
</g>
<text id="text" transform="translate(100,290)" font-size="150" font-weight="600">TEXT</text>
<mask id="mask">
<use x="0" y="0" xlink:href="#text" opacity="1" fill="#FFF"></use>
</mask>
</defs> -->
<defs>
<g id="whole" fill-opacity="0.3" fill="gold" mask="url(#mask)">
<path id="wave1" d="M0,200c50-50,99,0,99,0c51,50,101,0,101,0c50-50,100,0,100,0c50,50,100,0,100,0c50-50,100,0,100,0
c50,50,100,0,100,0v200H0V200z"></path>
<path id="wave2" d="M0,200c50-50,99,0,99,0c51,50,101,0,101,0c50-50,100,0,100,0c50,50,100,0,100,0c50-50,100,0,100,0
c50,50,100,0,100,0v200H0V200z"></path>
<path id="wave3" d="M0,200c50-50,99,0,99,0c51,50,101,0,101,0c50-50,100,0,100,0c50,50,100,0,100,0c50-50,100,0,100,0
c50,50,100,0,100,0v200H0V200z"></path>
</g>
<circle id="circle" cx="200" cy="-100" r="100" stroke="red" stroke-width="8" transform="translate(100,290)" />
<mask id="mask">
<use x="0" y="0" xlink:href="#circle" opacity="1" fill="#FFF"></use>
</mask>
</defs>
<use xlink:href="#whole" opacity="1"></use>
</svg>
</body>
</html>
\ No newline at end of file
import { App } from 'vue'
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 MySub from './MySub/my-sub.vue'
import MyWave from './MyWave/my-wave.vue'
import MyProgress from './MyProgress/my-progress.vue'
import MyDrawer from './MyDrawer/my-drawer.vue'
import * as ChartTypes from './MyChart/types'
// import { withInstall } from './util'
import 'animate.css'
import 'normalize.css'
const components = [
MyMap,
MyTitle,
MyAnimate,
MyGrid,
MyCard,
MyCount,
MyLoader,
MyModal,
MyScroll,
MyForm,
MyTable,
MyStep,
MyEmpty,
MyLine,
MyBar,
MyPie,
MyRadar,
MyScatter,
MySub,
MyWave,
MyProgress,
MyDrawer,
]
const install = (app: App): App => {
// 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,
MySub,
MyWave,
MyProgress,
MyDrawer,
}
// 图表组件的类型参数
export { ChartTypes }
// 默认导出 —— 使用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
$secondary-color = #5BD5FF
$primary-bg = rgba(49,94,139,.3)
$primary-border = rgba(91,213,255,.5)
$blue = #2F86EE
$edge = #00f2ff
$yellow = #ffd400
$green = #4F953B
$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
$font-color = #70A7E0
$table-bg(opacity = 0.3)
rgba(0, 118, 255, opacity )
\ No newline at end of file
import { App, Plugin } from 'vue'
import dayjs from 'dayjs'
import ch from 'dayjs/locale/zh-cn'
import LocalizedFormat from 'dayjs/plugin/LocalizedFormat'
dayjs.extend(LocalizedFormat)
dayjs.locale(ch)
export const withInstall = <T>(comp: T): T & Plugin => {
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 => dayjs().format('YYYY-MM-DD dddd')
export const getTime = (): string => dayjs().format('LTS')
export const toRGB = (c: string, opacity = 1): string => {
let color = c.toLowerCase()
const reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/
if (!color || !reg.test(color)) {
console.error('toRGB转换的颜色需要是十六进制颜色值')
return color
}
if (color.length === 4) {
let newColor = '#'
for (let i = 1; i < 4; i++) {
newColor += color.slice(i, i + 1).concat(color.slice(i, i + 1))
}
color = newColor
}
const result: number[] = []
for (let i = 1; i < 7; i += 2) {
result.push(parseInt(`0x${color.slice(i, i + 2)}`))
}
return `rgba(${result.join(',')}, ${opacity})`
}
import { ref, Ref, onMounted, onUnmounted } from 'vue'
export default function useClickOutside(
elementRef: Ref<HTMLElement | null>,
cb?: (val: boolean) => any,
) {
const isClickOutSide = ref(false)
const handler = (e: MouseEvent) => {
if (!elementRef || !elementRef.value) return
isClickOutSide.value = !elementRef.value.contains(e.target as HTMLElement)
cb && cb(isClickOutSide.value)
}
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
import { Commit, Dispatch } from './index'
interface Method {
commit: Commit
dispatch: Dispatch
}
export default {
example({ commit }: Method): void {
// DO SOMETHING
commit('SET_LOADING', true)
},
}
import { createStore, DispatchOptions, CommitOptions } from 'vuex'
import state from './state'
import mutations from './mutations'
import actions from './actions'
export type GlobalStateProps = typeof state
/** ------------------------------------- 分隔线 --------------------------------------- */
/** 去除tuple第一项 */
type CutHead<Tuple extends unknown[]> = ((...args: Tuple) => unknown) extends (
first: any,
...rest: infer Result
) => unknown
? Result
: never
type MutationsType = typeof mutations
type CommitPayload = {
[key in keyof MutationsType]: CutHead<Parameters<MutationsType[key]>>[0]
}
/** 取代Vuex中的Commit类型,以此加强约束和代码提示 */
export interface Commit {
<T extends keyof CommitPayload>(
type: T,
payload?: CommitPayload[T],
options?: CommitOptions,
): void
}
type ActionsType = typeof actions
type DispatchPayload = {
[key in keyof ActionsType]: CutHead<Parameters<ActionsType[key]>>[0]
}
/** 取代Vuex中的Dispatch类型,以此加强约束和代码提示 */
export interface Dispatch {
<T extends keyof DispatchPayload>(
type: T,
payload?: DispatchPayload[T],
options?: DispatchOptions,
): Promise<any>
}
/** 修改Store类型上的commit和dispatch方法类型, 使其在页面中使用store.commit等方法有约束和提示 */
const store = createStore<GlobalStateProps>({
state,
mutations,
actions,
})
type MyStore = Omit<typeof store, 'commit' | 'dispatch'> & {
commit: Commit
dispatch: Dispatch
}
export default store as MyStore
import { GlobalStateProps } from './index'
export default {
SET_LOADING(state: GlobalStateProps, val: boolean): void {
state.showLoading = val
},
SET_COM(state: GlobalStateProps, e: any) {
state.com = e
},
}
export default {
showLoading: false,
com: null,
}
import { computed, defineComponent, PropType, ref } from 'vue'
export default defineComponent({
name: 'TsxComponent',
props: {
msg: {
type: String as PropType<string>,
default: 'Tsx',
},
},
setup(props) {
const count = ref(1)
const list = computed(() => {
const result: number[] = []
for (let i = 0; i < count.value; i++) {
result.push(i + 1)
}
return result
})
return () => (
<>
<h1>Hello, {props.msg}</h1>
<button onClick={() => (count.value += 1)}>
Add count: <b>{count.value}</b>
</button>
<ul>
{list.value.map((_, i) => (
<li>{i + 1}</li>
))}
</ul>
</>
)
},
})
<template>
<m-grid
:template="[
'title title title',
'box1 . box3',
'box1 . box3',
'box2 . box3',
'box2 box4 box4',
]"
columns="1fr 1.5fr 1fr"
rows="0.4rem 1fr 0.5fr 0.5fr 1fr"
>
<m-title area="title" @click="handleClick">XXX数据平台</m-title>
<m-card area="box1" title="Box1">
<!-- <TestComponent2 /> -->
<!-- <div :component="com"></div> -->
<component :is="com"></component>
</m-card>
<m-card area="box2" title="Box2">
<m-empty v-if="!show"></m-empty>
<m-bar :dataset="chartData" :option="chartOption" />
</m-card>
<m-card
v-show="show"
area="box3"
title="Box3"
enter="fadeInRight"
leave="fadeOutRight"
>
<m-table
:template="[
'标题1|标题2*2|标题3*2',
'key1|key2>customFormatter|key3#image',
]"
:data="tableData"
:formatter="{ customFormatter }"
/>
</m-card>
<m-card area="box4" :title="`Box${boxNumber}`">
<TestComponent @select="(val) => (boxNumber += val * 2)" />
</m-card>
</m-grid>
</template>
<script lang="ts">
import { computed, defineComponent, ref, shallowRef } from 'vue'
import TestComponent from './test-component.vue'
import TestComponent2 from './test-component2.vue'
import { ChartTypes } from '@/components/MyComponent'
import store from '@/store'
interface TableDataProps {
[propName: string]: string
}
export default defineComponent({
name: 'Main',
components: { TestComponent, TestComponent2 },
setup() {
const show = ref(false)
const chartData = ref<ChartTypes.DatasetComponentOption | null>(null)
const chartOption = ref<ChartTypes.BarOption>({
color: [['#5BD5FF', '#826AFA'], ['#FFCE34', 'red'], '#7BFFB3'],
series: [
{
type: 'bar',
barWidth: '30%',
itemStyle: { borderRadius: 8 },
barGap: 0,
stack: '总量',
},
{
type: 'bar',
barWidth: '30%',
itemStyle: { borderRadius: 8 },
barGap: 0,
stack: '总量',
},
{
type: 'line',
smooth: true,
lineStyle: {
width: 2,
},
},
],
})
const boxNumber = ref(4)
const tableData = ref<TableDataProps[]>([])
setTimeout(() => {
show.value = true
tableData.value = [
{
key1: 'key1',
key2: 'key2',
key3: 'https://avatars2.githubusercontent.com/u/43328103?v=4',
},
{
key1: 'key1',
key2: 'key2',
key3: 'https://avatars2.githubusercontent.com/u/43328103?v=4',
},
{
key1: 'key1',
key2: 'key2',
key3: 'https://avatars2.githubusercontent.com/u/43328103?v=4',
},
{
key1: 'key1',
key2: 'key2',
key3: 'https://avatars2.githubusercontent.com/u/43328103?v=4',
},
]
chartData.value = {
dimensions: [
{ name: 'date', displayName: '日期' },
{ name: 'data1', displayName: '发现数' },
{ name: 'data2', displayName: '处置数' },
{ name: 'data3', displayName: '结案数' },
],
source: [
{ date: '11日', data1: 100, data2: 100, data3: 100 },
{ date: '12日', data1: 110, data2: 110, data3: 120 },
{ date: '13日', data1: 120, data2: 130, data3: 140 },
{ date: '14日', data1: 130, data2: 140, data3: 160 },
{ date: '15日', data1: 140, data2: 150, data3: 180 },
{ date: '16日', data1: 150, data2: 160, data3: 190 },
{ date: '17日', data1: 150, data2: 160, data3: 190 },
{ date: '18日', data1: 150, data2: 160, data3: 190 },
],
}
}, 3000)
const customFormatter = (val: string) => {
return val + '哈哈哈'
}
// const com = computed(() => store.state.com)
const com = shallowRef<any>(null)
function handleClick() {
console.log('123')
// store.commit('SET_COM', TestComponent2)
com.value = TestComponent2
}
return {
show,
tableData,
customFormatter,
chartData,
chartOption,
boxNumber,
com,
handleClick,
}
},
})
</script>
<style lang="stylus"></style>
This diff is collapsed.
This diff is collapsed.
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"skipLibCheck": true,
"esModuleInterop": true,
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"baseUrl": ".",
"lib": ["esnext", "dom"],
"noImplicitAny": false,
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": ["node_modules", "dist", "public"]
}
This diff is collapsed.
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { resolve } from 'path'
export default defineConfig({
base: './',
plugins: [vue(), vueJsx()],
resolve: {
alias: {
'@': resolve(__dirname, './src'),
},
},
})
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