Commit 99bb0d33 authored by 郭铭瑶's avatar 郭铭瑶 🤘

init project

parents
current node
last 2 versions and > 2%
ie > 10
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'], // 禁止使用分号
},
}
.DS_Store
node_modules/
/dist/
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*
module.exports = {
tabWidth: 2,
useTabs: false,
singleQuote: true,
semi: false,
trailingComma: 'all',
}
# 动态组件库
## usage
1. 执行命令`npm run build:unpkg`进行打包;
2. 将打包后的文件`/dist/component-lib.min.js`放置在服务器上;
3. 在大屏项目中引入使用:
```javascript
import fetchComponents from 'http://127.0.0.1:8080/component-lib.min.js' // 假设打包文件放在http://127.0.0.1:8080上
import * as vue from 'vue'
fetchComponents(vue) // 需要和大屏共用一个vue,因此将vue作为参数传入
const {
Property,
PropertyFeatures,
PartyLead,
PublicEarning,
Bid,
IndustrySupervision,
} = window._component_lib // 组件默认会设置在window._component_lib上
export default {
Property,
PropertyFeatures,
PartyLead,
PublicEarning,
Bid,
IndustrySupervision,
}
```
const devPresets = ['@vue/babel-preset-app'];
const buildPresets = [
[
'@babel/preset-env',
// Config for @babel/preset-env
{
// Example: Always transpile optional chaining/nullish coalescing
// include: [
// /(optional-chaining|nullish-coalescing)/
// ],
},
],
'@babel/preset-typescript',
];
module.exports = {
presets: (process.env.NODE_ENV === 'development' ? devPresets : buildPresets),
};
// rollup.config.js
import fs from 'fs'
import path from 'path'
import vue from 'rollup-plugin-vue'
import alias from '@rollup/plugin-alias'
import commonjs from '@rollup/plugin-commonjs'
import resolve from '@rollup/plugin-node-resolve'
import replace from '@rollup/plugin-replace'
import babel from '@rollup/plugin-babel'
import PostCSS from 'rollup-plugin-postcss'
import { terser } from 'rollup-plugin-terser'
// import ttypescript from 'ttypescript'
// import typescript from 'rollup-plugin-typescript2'
import typescript from '@rollup/plugin-typescript'
import minimist from 'minimist'
// import postcssImport from 'postcss-import'
import image from '@rollup/plugin-image'
// Get browserslist config and remove ie from es build targets
const esbrowserslist = fs
.readFileSync('./.browserslistrc')
.toString()
.split('\n')
.filter((entry) => entry && entry.substring(0, 2) !== 'ie')
// Extract babel preset-env config, to combine with esbrowserslist
const babelPresetEnvConfig = require('../babel.config').presets.filter(
(entry) => entry[0] === '@babel/preset-env',
)[0][1]
const argv = minimist(process.argv.slice(2))
const projectRoot = path.resolve(__dirname, '..')
const baseConfig = {
input: 'src/entry.ts',
plugins: {
preVue: [
alias({
entries: [
{
find: '@',
replacement: `${path.resolve(projectRoot, 'src')}`,
},
],
}),
],
replace: {
'process.env.NODE_ENV': JSON.stringify('production'),
},
vue: { preprocessStyles: true },
postVue: [
resolve({
extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue'],
}),
PostCSS(),
// Process only `<style module>` blocks.
// PostCSS({
// modules: {
// generateScopedName: '[local]___[hash:base64:5]',
// },
// include: /&module=.*\.stylus$/,
// }),
// Process all `<style>` blocks except `<style module>`.
// PostCSS({
// include: /(?<!&module=.*)\.stylus$/,
// plugins: [
// postcssImport({
// resolve(id, basedir) {
// // // resolve alias @css, @import '@css/style.css'
// // // because @css/ has 5 chars
// // if (id.startsWith("@css")) {
// // return path.resolve("./src/assets/styles/css", id.slice(5));
// // }
// // // resolve node_modules, @import '~normalize.css/normalize.css'
// // // similar to how css-loader's handling of node_modules
// // if (id.startsWith("~")) {
// // return path.resolve("./node_modules", id.slice(1));
// // }
// // resolve relative path, @import './components/style.css'
// return path.resolve(basedir, id)
// },
// }),
// ],
// }),
commonjs(),
],
babel: {
exclude: 'node_modules/**',
extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue'],
babelHelpers: 'bundled',
},
},
}
// ESM/UMD/IIFE shared settings: externals
// Refer to https://rollupjs.org/guide/en/#warning-treating-module-as-external-dependency
const external = [
// list external dependencies, exactly the way it is written in the import statement.
// eg. 'jquery'
'vue',
]
// UMD/IIFE shared settings: output.globals
// Refer to https://rollupjs.org/guide/en#output-globals for details
const globals = {
// Provide global variable names to replace your external imports
// eg. jquery: '$'
vue: 'Vue',
}
// Customize configs for individual targets
const buildFormats = []
if (!argv.format || argv.format === 'es') {
const esConfig = {
...baseConfig,
input: 'src/entry.esm.ts',
external,
output: {
file: 'dist/component-lib.esm.js',
format: 'esm',
exports: 'named',
},
plugins: [
image(),
replace(baseConfig.plugins.replace),
...baseConfig.plugins.preVue,
vue(baseConfig.plugins.vue),
...baseConfig.plugins.postVue,
// Only use typescript for declarations - babel will
// do actual js transformations
// typescript({
// typescript: ttypescript,
// useTsconfigDeclarationDir: true,
// emitDeclarationOnly: true,
// }),
typescript(),
babel({
...baseConfig.plugins.babel,
presets: [
[
'@babel/preset-env',
{
...babelPresetEnvConfig,
targets: esbrowserslist,
},
],
],
}),
],
}
buildFormats.push(esConfig)
}
if (!argv.format || argv.format === 'cjs') {
const umdConfig = {
...baseConfig,
external,
output: {
compact: true,
file: 'dist/component-lib.ssr.js',
format: 'cjs',
name: 'ComponentLib',
exports: 'auto',
globals,
},
plugins: [
image(),
replace(baseConfig.plugins.replace),
...baseConfig.plugins.preVue,
vue(baseConfig.plugins.vue),
...baseConfig.plugins.postVue,
babel(baseConfig.plugins.babel),
],
}
buildFormats.push(umdConfig)
}
if (!argv.format || argv.format === 'iife') {
const unpkgConfig = {
...baseConfig,
external,
output: {
compact: true,
file: 'dist/component-lib.min.js',
format: 'iife',
name: 'ComponentLib',
exports: 'auto',
globals,
},
plugins: [
image(),
replace(baseConfig.plugins.replace),
...baseConfig.plugins.preVue,
vue(baseConfig.plugins.vue),
...baseConfig.plugins.postVue,
babel(baseConfig.plugins.babel),
terser({
output: {
ecma: 5,
},
}),
],
}
buildFormats.push(unpkgConfig)
}
// Export config
export default buildFormats
import { createApp } from 'vue';
import Dev from './serve.vue';
// To register individual components where they are used (serve.vue) instead of using the
// library as a whole, comment/remove this import and it's corresponding "app.use" call
import Com from '@/entry.esm';
const app = createApp(Dev);
app.use(Com);
app.mount('#app');
<script lang="ts">
import { defineComponent } from 'vue';
// Uncomment import and local "components" registration if library is not registered globally.
// import { ComSample } from '@/entry.esm';
export default defineComponent({
name: 'ServeDev',
// components: {
// ComSample,
// }
});
</script>
<template>
<div id="app">
<com-sample />
</div>
</template>
This diff is collapsed.
This diff is collapsed.
/** 打包后修改min.js其中内容用 */
const fs = require('fs')
const rimraf = require('rimraf')
const file = './dist/component-lib.min.js'
fs.readFile(file, 'utf-8', (err, data) => {
if (err) return console.error(err)
const newData = data.replace('var ComponentLib=', 'export default ').replace('(Vue);', '')
rimraf(file, err => {
if (err) return console.error(err)
fs.writeFile(file, newData, 'utf-8', err => {
if (err) return console.error(err)
})
})
})
\ No newline at end of file
declare module '*.vue' {
import { 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'
declare interface Window {
_component_lib: any
}
---
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">
<span class="dot" />
<p>
{{ title }}
</p>
</div>
<div class="card-content">
<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
background rgba(29,42,65,.8)
.card-title
display flex
align-items center
padding .04rem .1rem
background linear-gradient(to right, rgba(59,85,102,.8) 70%, transparent)
position relative
border-bottom .01rem solid #717F8C
>p
font-size .12rem
font-weight bold
.dot
display inline-block
width .02rem
height .2rem
background #fadb71
position absolute
top 0
bottom 0
left 0
margin auto
.card-content
position relative
box-sizing border-box
padding .05rem
display flex
flex-direction column
justify-content space-around
$blur(0.01rem)
>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()
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
</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 |
<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, nextTick, PropType, ref, watch } 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)
watch(
[() => props.template, () => props.columns, () => props.rows],
async ([val]) => {
if (val && val.length > 0) {
await nextTick()
setChildrenArea()
}
},
{ immediate: true },
)
function setChildrenArea() {
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
position relative
display grid
overflow hidden
flex 1
</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};height:${height};transform: translate(${offsetX},${offsetY})`"
>
<head>
<span class="dot" />
<p>
{{ title }}
</p>
<img
src="/src/assets/images/close-btn4.png"
draggable="false"
class="close-btn"
@click.prevent="closeModal"
/>
</head>
<div class="content" :style="`background: ${bgColor}`">
<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%',
},
/** 高度 */
height: {
type: String as PropType<string>,
default: 'auto',
},
/** 偏移 */
offsetX: {
type: String as PropType<string>,
default: '0',
},
/** 偏移 */
offsetY: {
type: String as PropType<string>,
default: '0',
},
/** 背景色 */
bgColor: {
type: String as PropType<string>,
default: 'initial',
},
/** 点击蒙层是否允许关闭 */
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
color #fff
$blur()
z-index 99999
head
height .3rem
display flex
align-items center
justify-content space-between
padding 0 .1rem
background linear-gradient(to right, rgba(59,85,102,.8) 70%, rgba(0,0,0,0.2))
position relative
border-bottom .01rem solid #717F8C
>p
font-size .12rem
font-weight bold
.dot
display inline-block
width .02rem
height .2rem
background #fadb71
position absolute
top 0
bottom 0
left 0
margin auto
>.close-btn
width .1rem
height @width
cursor pointer
margin-left .05rem
transition transform .2s ease-in-out
&:hover
transform rotate(90deg)
.content
height calc(100% - .3rem)
box-sizing border-box
padding .1rem
overflow-y auto
overflow-x hidden
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" :style="{ color: msg.color }">
<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
color?: 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"
:class="{ 'click-able': clickAble }"
@click="handleClick"
>
<span />
<p><slot /></p>
<div v-if="addition" class="addition">
<MyCount
v-if="addition.value"
class="count orange-count"
:value="addition.value"
/>
<span v-if="addition.unit">{{ addition.unit }}</span>
<!-- <img src="@/assets/images/title-dec.png" /> -->
</div>
<div v-if="searchAble" class="search-container">
<input
v-model="inputValue"
:class="{ open: isOpened }"
class="search-bar"
type="text"
@keypress.enter="handleSearch"
/>
<img
v-show="isOpened"
class="close-btn"
src="@/assets/images/close-btn2.png"
draggable="false"
@click.prevent="handleClose"
/>
<img
class="search-btn"
src="@/assets/images/search.png"
draggable="false"
@click.prevent="handleOpenSearchBar"
/>
</div>
</div>
<!-- <img src="@/assets/images/sub-dot.png" draggable="false" /> -->
<span class="dot" />
</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,
},
clickAble: {
type: Boolean as PropType<boolean>,
default: false,
},
},
emits: ['search', 'close', 'select'],
setup(props, ctx) {
const isOpened = ref(false)
const inputValue = ref('')
const handleSearch = () => {
ctx.emit('search', inputValue.value.trim())
}
const handleClose = () => {
inputValue.value = ''
ctx.emit('close')
}
const handleOpenSearchBar = () => {
if (!isOpened.value) {
isOpened.value = true
} else {
if (inputValue.value) {
handleSearch()
} else {
isOpened.value = false
}
}
}
const handleClick = () => {
if (props.clickAble) {
ctx.emit('select')
}
}
return {
isOpened,
inputValue,
handleSearch,
handleClose,
handleOpenSearchBar,
handleClick,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-sub
display flex
align-items center
border-bottom .01rem solid rgba(105,110,114,.5)
margin-bottom .03rem
>.title
display flex
align-items center
font-family $font-zcool
// color $secondary-color
flex 1
&.click-able
cursor pointer
&:hover
color $yellow
.addition
display flex
align-items center
margin-left .1rem
.count
// color $orange
font-size .15rem
font-family $font-barlow
padding-bottom .03rem
span
color #aaa
font-size .08rem
margin-left .05rem
img
width .3rem
margin-left .1rem
.search-container
display flex
align-items center
position relative
&:hover
.close-btn
display inline-block
.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 1.6rem
padding .02rem .16rem .02rem .05rem
border .01rem solid $blue
opacity 1
.search-btn
width .14rem
height @width
cursor pointer
transition transform .2s ease-in-out
&:hover
transform scale(1.3) rotate(360deg)
.close-btn
display none
width .08rem
height @width
cursor pointer
position absolute
right .24rem
transition transform .2s ease-in-out
&:hover
transform scale(1.3) rotate(360deg)
>.dot
display inline-block
width 0
height @width
border .05rem solid transparent
border-right-color #FFB560
</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 |
This diff is collapsed.
---
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>
<img
src="@/assets/images/filter.png"
class="filter"
draggable="false"
@click.prevent.stop="handleClick"
/>
<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,
},
},
emits: ['touch'],
setup(_, ctx) {
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
})
const handleClick = () => ctx.emit('touch')
return {
date,
time,
timer,
handleClick,
}
},
})
</script>
<style lang="stylus" scoped>
@import '../main.styl'
.my-title
$full()
$center()
position relative
background-size cover
background-position center
height .4rem
.date
z-index 2
position absolute
top .12rem
right 1%
color #45F7EF
.filter
z-index 2
position absolute
top .08rem
left 1%
width .24rem
height @width
cursor pointer
opacity .8
transition opacity .2s ease-in-out
&:hover
opacity 1
.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 40%, #0064d3)
font-size .2rem
letter-spacing .05rem
font-family $font-pang
font-weight normal
</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 |
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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