Compare commits
5 Commits
c870103d4c
...
ed99d3ce76
Author | SHA1 | Date |
---|---|---|
|
ed99d3ce76 | 11 months ago |
|
100855c936 | 11 months ago |
|
da27ad744a | 11 months ago |
|
a532e9c318 | 11 months ago |
|
d127e0aa42 | 11 months ago |
95 changed files with 3085 additions and 5116 deletions
@ -1,4 +0,0 @@
|
||||
export { default as CronTab } from './src/CronTabInput.vue' |
||||
export { default as CronTabInner } from './src/CronTabInner.vue' |
||||
export { default as CronTabModal } from './src/CronTabModal.vue' |
||||
export { default as CronValidator } from './src/validator' |
@ -1,360 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { computed, provide, reactive, ref, watch } from 'vue' |
||||
import { Col, Divider, Input, Row, TabPane, Tabs, Textarea, Tooltip } from 'ant-design-vue' |
||||
import CronParser from 'cron-parser' |
||||
import SecondUI from './tabs/SecondUI.vue' |
||||
import MinuteUI from './tabs/MinuteUI.vue' |
||||
import HourUI from './tabs/HourUI.vue' |
||||
import DayUI from './tabs/DayUI.vue' |
||||
import MonthUI from './tabs/MonthUI.vue' |
||||
import WeekUI from './tabs/WeekUI.vue' |
||||
import YearUI from './tabs/YearUI.vue' |
||||
import { cronEmits, cronProps } from './cron.data' |
||||
import { useDesign } from '@/hooks/web/useDesign' |
||||
import { dateFormat } from '@/utils/dateUtil' |
||||
import { simpleDebounce } from '@/utils' |
||||
|
||||
const props = defineProps({ ...cronProps }) |
||||
const emit = defineEmits([...cronEmits]) |
||||
const { prefixCls } = useDesign('cron-inner') |
||||
provide('prefixCls', prefixCls) |
||||
const activeKey = ref(props.hideSecond ? 'minute' : 'second') |
||||
const second = ref('*') |
||||
const minute = ref('*') |
||||
const hour = ref('*') |
||||
const day = ref('*') |
||||
const month = ref('*') |
||||
const week = ref('?') |
||||
const year = ref('*') |
||||
const inputValues = reactive({ |
||||
second: '', |
||||
minute: '', |
||||
hour: '', |
||||
day: '', |
||||
month: '', |
||||
week: '', |
||||
year: '', |
||||
cron: '', |
||||
}) |
||||
const preTimeList = ref('执行预览,会忽略年份参数。') |
||||
|
||||
// cron表达式 |
||||
const cronValueInner = computed(() => { |
||||
const result: string[] = [] |
||||
if (!props.hideSecond) |
||||
result.push(second.value ? second.value : '*') |
||||
|
||||
result.push(minute.value ? minute.value : '*') |
||||
result.push(hour.value ? hour.value : '*') |
||||
result.push(day.value ? day.value : '*') |
||||
result.push(month.value ? month.value : '*') |
||||
result.push(week.value ? week.value : '?') |
||||
if (!props.hideYear && !props.hideSecond) |
||||
result.push(year.value ? year.value : '*') |
||||
return result.join(' ') |
||||
}) |
||||
// 不含年 |
||||
const cronValueNoYear = computed(() => { |
||||
const v = cronValueInner.value |
||||
if (props.hideYear || props.hideSecond) |
||||
return v |
||||
const vs = v.split(' ') |
||||
if (vs.length >= 6) { |
||||
// 转成 Quartz 的规则 |
||||
vs[5] = convertWeekToQuartz(vs[5]) |
||||
} |
||||
return vs.slice(0, vs.length - 1).join(' ') |
||||
}) |
||||
const calTriggerList = simpleDebounce(calTriggerListInner, 500) |
||||
|
||||
watch( |
||||
() => props.value, |
||||
(newVal) => { |
||||
if (newVal === cronValueInner.value) |
||||
return |
||||
|
||||
formatValue() |
||||
}, |
||||
) |
||||
|
||||
watch(cronValueInner, (newValue) => { |
||||
calTriggerList() |
||||
emitValue(newValue) |
||||
assignInput() |
||||
}) |
||||
|
||||
assignInput() |
||||
formatValue() |
||||
calTriggerListInner() |
||||
|
||||
function assignInput() { |
||||
inputValues.second = second.value |
||||
inputValues.minute = minute.value |
||||
inputValues.hour = hour.value |
||||
inputValues.day = day.value |
||||
inputValues.month = month.value |
||||
inputValues.week = week.value |
||||
inputValues.year = year.value |
||||
inputValues.cron = cronValueInner.value |
||||
} |
||||
|
||||
function formatValue() { |
||||
if (!props.value) |
||||
return |
||||
const values = props.value.split(' ').filter(item => !!item) |
||||
if (!values || values.length <= 0) |
||||
return |
||||
let i = 0 |
||||
if (!props.hideSecond) |
||||
second.value = values[i++] |
||||
if (values.length > i) |
||||
minute.value = values[i++] |
||||
if (values.length > i) |
||||
hour.value = values[i++] |
||||
if (values.length > i) |
||||
day.value = values[i++] |
||||
if (values.length > i) |
||||
month.value = values[i++] |
||||
if (values.length > i) |
||||
week.value = values[i++] |
||||
if (values.length > i) |
||||
year.value = values[i] |
||||
assignInput() |
||||
} |
||||
|
||||
// Quartz 的规则: |
||||
// 1 = 周日,2 = 周一,3 = 周二,4 = 周三,5 = 周四,6 = 周五,7 = 周六 |
||||
function convertWeekToQuartz(week: string) { |
||||
const convert = (v: string) => { |
||||
if (v === '0') |
||||
return '1' |
||||
|
||||
if (v === '1') |
||||
return '0' |
||||
|
||||
return (Number.parseInt(v) - 1).toString() |
||||
} |
||||
// 匹配示例 1-7 or 1/7 |
||||
const patten1 = /^([0-7])([-/])([0-7])$/ |
||||
// 匹配示例 1,4,7 |
||||
const patten2 = /^([0-7])(,[0-7])+$/ |
||||
if (/^[0-7]$/.test(week)) { |
||||
return convert(week) |
||||
} |
||||
else if (patten1.test(week)) { |
||||
return week.replace(patten1, (_$0, before, separator, after) => { |
||||
if (separator === '/') |
||||
return convert(before) + separator + after |
||||
else |
||||
return convert(before) + separator + convert(after) |
||||
}) |
||||
} |
||||
else if (patten2.test(week)) { |
||||
return week |
||||
.split(',') |
||||
.map(v => convert(v)) |
||||
.join(',') |
||||
} |
||||
return week |
||||
} |
||||
|
||||
function calTriggerListInner() { |
||||
// 设置了回调函数 |
||||
if (props.remote) { |
||||
props.remote(cronValueInner.value, +new Date(), (v) => { |
||||
preTimeList.value = v |
||||
}) |
||||
return |
||||
} |
||||
const format = 'yyyy-MM-dd hh:mm:ss' |
||||
const options = { |
||||
currentDate: dateFormat(new Date(), format), |
||||
} |
||||
const iter = CronParser.parseExpression(cronValueNoYear.value, options) |
||||
const result: string[] = [] |
||||
for (let i = 1; i <= 10; i++) |
||||
result.push(dateFormat(new Date(iter.next() as any), format)) |
||||
|
||||
preTimeList.value = result.length > 0 ? result.join('\n') : '无执行时间' |
||||
} |
||||
|
||||
function onInputBlur() { |
||||
second.value = inputValues.second |
||||
minute.value = inputValues.minute |
||||
hour.value = inputValues.hour |
||||
day.value = inputValues.day |
||||
month.value = inputValues.month |
||||
week.value = inputValues.week |
||||
year.value = inputValues.year |
||||
} |
||||
|
||||
function onInputCronBlur(event) { |
||||
emitValue(event.target.value) |
||||
} |
||||
|
||||
function emitValue(value) { |
||||
emit('change', value) |
||||
emit('update:value', value) |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div :class="`${prefixCls}`"> |
||||
<div class="content"> |
||||
<Tabs v-model:activeKey="activeKey" size="small"> |
||||
<TabPane v-if="!hideSecond" key="second" tab="秒"> |
||||
<SecondUI v-model:value="second" :disabled="disabled" /> |
||||
</TabPane> |
||||
<TabPane key="minute" tab="分"> |
||||
<MinuteUI v-model:value="minute" :disabled="disabled" /> |
||||
</TabPane> |
||||
<TabPane key="hour" tab="时"> |
||||
<HourUI v-model:value="hour" :disabled="disabled" /> |
||||
</TabPane> |
||||
<TabPane key="day" tab="日"> |
||||
<DayUI v-model:value="day" :week="week" :disabled="disabled" /> |
||||
</TabPane> |
||||
<TabPane key="month" tab="月"> |
||||
<MonthUI v-model:value="month" :disabled="disabled" /> |
||||
</TabPane> |
||||
<TabPane key="week" tab="周"> |
||||
<WeekUI v-model:value="week" :day="day" :disabled="disabled" /> |
||||
</TabPane> |
||||
<TabPane v-if="!hideYear && !hideSecond" key="year" tab="年"> |
||||
<YearUI v-model:value="year" :disabled="disabled" /> |
||||
</TabPane> |
||||
</Tabs> |
||||
<Divider /> |
||||
<!-- 执行时间预览 --> |
||||
<Row :gutter="8"> |
||||
<Col :span="18" style="margin-top: 22px"> |
||||
<Row :gutter="8"> |
||||
<Col :span="8" style="margin-bottom: 12px"> |
||||
<Input v-model:value="inputValues.second" @blur="onInputBlur"> |
||||
<template #addonBefore> |
||||
<span class="allow-click" @click="activeKey = 'second'">秒</span> |
||||
</template> |
||||
</Input> |
||||
</Col> |
||||
<Col :span="8" style="margin-bottom: 12px"> |
||||
<Input v-model:value="inputValues.minute" @blur="onInputBlur"> |
||||
<template #addonBefore> |
||||
<span class="allow-click" @click="activeKey = 'minute'">分</span> |
||||
</template> |
||||
</Input> |
||||
</Col> |
||||
<Col :span="8" style="margin-bottom: 12px"> |
||||
<Input v-model:value="inputValues.hour" @blur="onInputBlur"> |
||||
<template #addonBefore> |
||||
<span class="allow-click" @click="activeKey = 'hour'">时</span> |
||||
</template> |
||||
</Input> |
||||
</Col> |
||||
<Col :span="8" style="margin-bottom: 12px"> |
||||
<Input v-model:value="inputValues.day" @blur="onInputBlur"> |
||||
<template #addonBefore> |
||||
<span class="allow-click" @click="activeKey = 'day'">日</span> |
||||
</template> |
||||
</Input> |
||||
</Col> |
||||
<Col :span="8" style="margin-bottom: 12px"> |
||||
<Input v-model:value="inputValues.month" @blur="onInputBlur"> |
||||
<template #addonBefore> |
||||
<span class="allow-click" @click="activeKey = 'month'">月</span> |
||||
</template> |
||||
</Input> |
||||
</Col> |
||||
<Col :span="8" style="margin-bottom: 12px"> |
||||
<Input v-model:value="inputValues.week" @blur="onInputBlur"> |
||||
<template #addonBefore> |
||||
<span class="allow-click" @click="activeKey = 'week'">周</span> |
||||
</template> |
||||
</Input> |
||||
</Col> |
||||
<Col :span="8"> |
||||
<Input v-model:value="inputValues.year" @blur="onInputBlur"> |
||||
<template #addonBefore> |
||||
<span class="allow-click" @click="activeKey = 'year'">年</span> |
||||
</template> |
||||
</Input> |
||||
</Col> |
||||
<Col :span="16"> |
||||
<Input v-model:value="inputValues.cron" @blur="onInputCronBlur"> |
||||
<template #addonBefore> |
||||
<Tooltip title="Cron表达式"> |
||||
式 |
||||
</Tooltip> |
||||
</template> |
||||
</Input> |
||||
</Col> |
||||
</Row> |
||||
</Col> |
||||
<Col :span="6"> |
||||
<div>近十次执行时间(不含年)</div> |
||||
<Textarea :value="preTimeList" :rows="5" /> |
||||
</Col> |
||||
</Row> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<style lang="less"> |
||||
@prefix-cls: ~'@{namespace}-cron-inner'; |
||||
|
||||
.@{prefix-cls} { |
||||
.content { |
||||
.ant-checkbox-wrapper + .ant-checkbox-wrapper { |
||||
margin-left: 0; |
||||
} |
||||
} |
||||
|
||||
&-config-list { |
||||
margin: 0 10px 10px; |
||||
text-align: left; |
||||
|
||||
.item { |
||||
margin-top: 5px; |
||||
font-size: 14px; |
||||
|
||||
span { |
||||
padding: 0 2px; |
||||
} |
||||
} |
||||
|
||||
.choice { |
||||
padding: 5px 8px; |
||||
} |
||||
|
||||
.w60 { |
||||
width: 60px; |
||||
min-width: 60px; |
||||
} |
||||
|
||||
.w80 { |
||||
width: 80px; |
||||
min-width: 80px; |
||||
} |
||||
|
||||
.list { |
||||
margin: 0 20px; |
||||
} |
||||
|
||||
.list-check-item { |
||||
width: 4em; |
||||
padding: 1px 3px; |
||||
} |
||||
|
||||
.list-cn .list-check-item { |
||||
width: 5em; |
||||
} |
||||
|
||||
.tip-info { |
||||
color: #999; |
||||
} |
||||
} |
||||
|
||||
.allow-click { |
||||
cursor: pointer; |
||||
} |
||||
} |
||||
</style> |
@ -1,55 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { ref, watch } from 'vue' |
||||
import { Input } from 'ant-design-vue' |
||||
import CronTabModal from './CronTabModal.vue' |
||||
import { cronEmits, cronProps } from './cron.data' |
||||
import { useModal } from '@/components/Modal' |
||||
import { propTypes } from '@/utils/propTypes' |
||||
|
||||
const props = defineProps({ |
||||
...cronProps, |
||||
placeholder: propTypes.string.def('请输入cron表达式'), |
||||
exeStartTime: propTypes.oneOfType([propTypes.number, propTypes.string, propTypes.object]).def(0), |
||||
}) |
||||
const emit = defineEmits([...cronEmits]) |
||||
const [registerModal, { openModal }] = useModal() |
||||
const editCronValue = ref(props.value) |
||||
|
||||
watch( |
||||
() => props.value, |
||||
(newVal) => { |
||||
if (newVal !== editCronValue.value) |
||||
editCronValue.value = newVal |
||||
}, |
||||
) |
||||
watch(editCronValue, (newVal) => { |
||||
emit('change', newVal) |
||||
emit('update:value', newVal) |
||||
}) |
||||
|
||||
function showConfigModal() { |
||||
if (!props.disabled) |
||||
openModal() |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<Input v-model:value="editCronValue" :placeholder="placeholder" :disabled="disabled"> |
||||
<template #addonAfter> |
||||
<a class="cursor-pointer" :disabled="disabled ? 'disabled' : null" @click="showConfigModal"> |
||||
<span class="i-ant-design:setting-outlined relative right-0.5 top-0.25" /> |
||||
<span>选择</span> |
||||
</a> |
||||
</template> |
||||
</Input> |
||||
<CronTabModal |
||||
v-model:value="editCronValue" |
||||
:exe-start-time="exeStartTime" |
||||
:hide-year="hideYear" |
||||
:remote="remote" |
||||
:hide-second="hideSecond" |
||||
@register="registerModal" |
||||
/> |
||||
</div> |
||||
</template> |
@ -1,18 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import CronTab from './CronTabInner.vue' |
||||
import { BasicModal, useModalInner } from '@/components/Modal' |
||||
|
||||
defineOptions({ name: 'CronTabModal', inheritAttrs: false }) |
||||
|
||||
const [registerModal, { closeModal }] = useModalInner() |
||||
|
||||
function onOk() { |
||||
closeModal() |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<BasicModal title="Cron表达式" width="800px" @register="registerModal" @ok="onOk"> |
||||
<CronTab v-bind="$attrs" /> |
||||
</BasicModal> |
||||
</template> |
@ -1,10 +0,0 @@
|
||||
import { propTypes } from '@/utils/propTypes' |
||||
|
||||
export const cronEmits = ['change', 'update:value'] |
||||
export const cronProps = { |
||||
value: propTypes.string.def(''), |
||||
disabled: propTypes.bool.def(false), |
||||
hideSecond: propTypes.bool.def(false), |
||||
hideYear: propTypes.bool.def(false), |
||||
remote: propTypes.func, |
||||
} |
@ -1,101 +0,0 @@
|
||||
<script lang="ts"> |
||||
import { computed, defineComponent, watch } from 'vue' |
||||
import { Checkbox, Input, Radio } from 'ant-design-vue' |
||||
import { TypeEnum, useTabEmits, useTabProps, useTabSetup } from './useTabMixin' |
||||
|
||||
export default defineComponent({ |
||||
name: 'DayUI', |
||||
components: { AInput: Input, Checkbox, CheckboxGroup: Checkbox.Group, Radio, RadioGroup: Radio.Group }, |
||||
props: useTabProps({ |
||||
defaultValue: '*', |
||||
props: { |
||||
week: { type: String, default: '?' }, |
||||
}, |
||||
}), |
||||
emits: useTabEmits(), |
||||
setup(props, context) { |
||||
const disabledChoice = computed(() => { |
||||
return (props.week && props.week !== '?') || props.disabled |
||||
}) |
||||
const setup = useTabSetup(props, context, { |
||||
defaultValue: '*', |
||||
valueWork: 1, |
||||
minValue: 1, |
||||
maxValue: 31, |
||||
valueRange: { start: 1, end: 31 }, |
||||
valueLoop: { start: 1, interval: 1 }, |
||||
disabled: disabledChoice, |
||||
}) |
||||
const typeWorkAttrs = computed(() => ({ |
||||
disabled: setup.type.value !== TypeEnum.work || props.disabled || disabledChoice.value, |
||||
...setup.inputNumberAttrs.value, |
||||
})) |
||||
|
||||
watch( |
||||
() => props.week, |
||||
() => { |
||||
setup.updateValue(disabledChoice.value ? '?' : setup.computeValue.value) |
||||
}, |
||||
) |
||||
|
||||
return { ...setup, typeWorkAttrs } |
||||
}, |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div :class="`${prefixCls}-config-list`"> |
||||
<RadioGroup v-model:value="type"> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.unset" v-bind="beforeRadioAttrs"> |
||||
不设置 |
||||
</Radio> |
||||
<span class="tip-info">日和周只能设置其中之一</span> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.every" v-bind="beforeRadioAttrs"> |
||||
每日 |
||||
</Radio> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.range" v-bind="beforeRadioAttrs"> |
||||
区间 |
||||
</Radio> |
||||
<span> 从 </span> |
||||
<AInput v-model:value="valueRange.start" type="number" v-bind="typeRangeAttrs" /> |
||||
<span> 日 至 </span> |
||||
<AInput v-model:value="valueRange.end" type="number" v-bind="typeRangeAttrs" /> |
||||
<span> 日 </span> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs"> |
||||
循环 |
||||
</Radio> |
||||
<span> 从 </span> |
||||
<AInput v-model:value="valueLoop.start" type="number" class="w-4" v-bind="typeLoopAttrs" /> |
||||
<span> 日开始,间隔 </span> |
||||
<AInput v-model:value="valueLoop.interval" type="number" v-bind="typeLoopAttrs" /> |
||||
<span> 日 </span> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.last" v-bind="beforeRadioAttrs"> |
||||
最后一日 |
||||
</Radio> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs"> |
||||
指定 |
||||
</Radio> |
||||
<div class="list"> |
||||
<CheckboxGroup v-model:value="valueList"> |
||||
<template v-for="i in specifyRange" :key="i"> |
||||
<Checkbox :value="i" v-bind="typeSpecifyAttrs"> |
||||
{{ i }} |
||||
</Checkbox> |
||||
</template> |
||||
</CheckboxGroup> |
||||
</div> |
||||
</div> |
||||
</RadioGroup> |
||||
</div> |
||||
</template> |
@ -1,69 +0,0 @@
|
||||
<script lang="ts"> |
||||
import { defineComponent } from 'vue' |
||||
import { Checkbox, Input, Radio } from 'ant-design-vue' |
||||
import { useTabEmits, useTabProps, useTabSetup } from './useTabMixin' |
||||
|
||||
export default defineComponent({ |
||||
name: 'HourUI', |
||||
components: { AInput: Input, Checkbox, CheckboxGroup: Checkbox.Group, Radio, RadioGroup: Radio.Group }, |
||||
props: useTabProps({ |
||||
defaultValue: '*', |
||||
}), |
||||
emits: useTabEmits(), |
||||
setup(props, context) { |
||||
return useTabSetup(props, context, { |
||||
defaultValue: '*', |
||||
minValue: 0, |
||||
maxValue: 23, |
||||
valueRange: { start: 0, end: 23 }, |
||||
valueLoop: { start: 0, interval: 1 }, |
||||
}) |
||||
}, |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div :class="`${prefixCls}-config-list`"> |
||||
<RadioGroup v-model:value="type"> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.every" v-bind="beforeRadioAttrs"> |
||||
每时 |
||||
</Radio> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.range" v-bind="beforeRadioAttrs"> |
||||
区间 |
||||
</Radio> |
||||
<span> 从 </span> |
||||
<AInput v-model:value="valueRange.start" type="number" v-bind="typeRangeAttrs" /> |
||||
<span> 时 至 </span> |
||||
<AInput v-model:value="valueRange.end" type="number" v-bind="typeRangeAttrs" /> |
||||
<span> 时 </span> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs"> |
||||
循环 |
||||
</Radio> |
||||
<span> 从 </span> |
||||
<AInput v-model:value="valueLoop.start" type="number" v-bind="typeLoopAttrs" /> |
||||
<span> 时开始,间隔 </span> |
||||
<AInput v-model:value="valueLoop.interval" type="number" v-bind="typeLoopAttrs" /> |
||||
<span> 时 </span> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs"> |
||||
指定 |
||||
</Radio> |
||||
<div class="list"> |
||||
<CheckboxGroup v-model:value="valueList"> |
||||
<template v-for="i in specifyRange" :key="i"> |
||||
<Checkbox :value="i" v-bind="typeSpecifyAttrs"> |
||||
{{ i }} |
||||
</Checkbox> |
||||
</template> |
||||
</CheckboxGroup> |
||||
</div> |
||||
</div> |
||||
</RadioGroup> |
||||
</div> |
||||
</template> |
@ -1,69 +0,0 @@
|
||||
<script lang="ts"> |
||||
import { defineComponent } from 'vue' |
||||
import { Checkbox, Input, Radio } from 'ant-design-vue' |
||||
import { useTabEmits, useTabProps, useTabSetup } from './useTabMixin' |
||||
|
||||
export default defineComponent({ |
||||
name: 'MinuteUI', |
||||
components: { AInput: Input, Checkbox, CheckboxGroup: Checkbox.Group, Radio, RadioGroup: Radio.Group }, |
||||
props: useTabProps({ |
||||
defaultValue: '*', |
||||
}), |
||||
emits: useTabEmits(), |
||||
setup(props, context) { |
||||
return useTabSetup(props, context, { |
||||
defaultValue: '*', |
||||
minValue: 0, |
||||
maxValue: 59, |
||||
valueRange: { start: 0, end: 59 }, |
||||
valueLoop: { start: 0, interval: 1 }, |
||||
}) |
||||
}, |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div :class="`${prefixCls}-config-list`"> |
||||
<RadioGroup v-model:value="type"> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.every" v-bind="beforeRadioAttrs"> |
||||
每分 |
||||
</Radio> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.range" v-bind="beforeRadioAttrs"> |
||||
区间 |
||||
</Radio> |
||||
<span> 从 </span> |
||||
<AInput v-model:value="valueRange.start" type="number" v-bind="typeRangeAttrs" /> |
||||
<span> 分 至 </span> |
||||
<AInput v-model:value="valueRange.end" type="number" v-bind="typeRangeAttrs" /> |
||||
<span> 分 </span> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs"> |
||||
循环 |
||||
</Radio> |
||||
<span> 从 </span> |
||||
<AInput v-model:value="valueLoop.start" type="number" v-bind="typeLoopAttrs" /> |
||||
<span> 分开始,间隔 </span> |
||||
<AInput v-model:value="valueLoop.interval" type="number" v-bind="typeLoopAttrs" /> |
||||
<span> 分 </span> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs"> |
||||
指定 |
||||
</Radio> |
||||
<div class="list"> |
||||
<CheckboxGroup v-model:value="valueList"> |
||||
<template v-for="i in specifyRange" :key="i"> |
||||
<Checkbox :value="i" v-bind="typeSpecifyAttrs"> |
||||
{{ i }} |
||||
</Checkbox> |
||||
</template> |
||||
</CheckboxGroup> |
||||
</div> |
||||
</div> |
||||
</RadioGroup> |
||||
</div> |
||||
</template> |
@ -1,69 +0,0 @@
|
||||
<script lang="ts"> |
||||
import { defineComponent } from 'vue' |
||||
import { Checkbox, Input, Radio } from 'ant-design-vue' |
||||
import { useTabEmits, useTabProps, useTabSetup } from './useTabMixin' |
||||
|
||||
export default defineComponent({ |
||||
name: 'MonthUI', |
||||
components: { AInput: Input, Checkbox, CheckboxGroup: Checkbox.Group, Radio, RadioGroup: Radio.Group }, |
||||
props: useTabProps({ |
||||
defaultValue: '*', |
||||
}), |
||||
emits: useTabEmits(), |
||||
setup(props, context) { |
||||
return useTabSetup(props, context, { |
||||
defaultValue: '*', |
||||
minValue: 1, |
||||
maxValue: 12, |
||||
valueRange: { start: 1, end: 12 }, |
||||
valueLoop: { start: 1, interval: 1 }, |
||||
}) |
||||
}, |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div :class="`${prefixCls}-config-list`"> |
||||
<RadioGroup v-model:value="type"> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.every" v-bind="beforeRadioAttrs"> |
||||
每月 |
||||
</Radio> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.range" v-bind="beforeRadioAttrs"> |
||||
区间 |
||||
</Radio> |
||||
<span> 从 </span> |
||||
<AInput v-model:value="valueRange.start" type="number" v-bind="typeRangeAttrs" /> |
||||
<span> 月 至 </span> |
||||
<AInput v-model:value="valueRange.end" type="number" v-bind="typeRangeAttrs" /> |
||||
<span> 月 </span> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs"> |
||||
循环 |
||||
</Radio> |
||||
<span> 从 </span> |
||||
<AInput v-model:value="valueLoop.start" type="number" v-bind="typeLoopAttrs" /> |
||||
<span> 月开始,间隔 </span> |
||||
<AInput v-model:value="valueLoop.interval" type="number" v-bind="typeLoopAttrs" /> |
||||
<span> 月 </span> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs"> |
||||
指定 |
||||
</Radio> |
||||
<div class="list"> |
||||
<CheckboxGroup v-model:value="valueList"> |
||||
<template v-for="i in specifyRange" :key="i"> |
||||
<Checkbox :value="i" v-bind="typeSpecifyAttrs"> |
||||
{{ i }} |
||||
</Checkbox> |
||||
</template> |
||||
</CheckboxGroup> |
||||
</div> |
||||
</div> |
||||
</RadioGroup> |
||||
</div> |
||||
</template> |
@ -1,69 +0,0 @@
|
||||
<script lang="ts"> |
||||
import { defineComponent } from 'vue' |
||||
import { Checkbox, Input, Radio } from 'ant-design-vue' |
||||
import { useTabEmits, useTabProps, useTabSetup } from './useTabMixin' |
||||
|
||||
export default defineComponent({ |
||||
name: 'SecondUI', |
||||
components: { AInput: Input, Checkbox, CheckboxGroup: Checkbox.Group, Radio, RadioGroup: Radio.Group }, |
||||
props: useTabProps({ |
||||
defaultValue: '*', |
||||
}), |
||||
emits: useTabEmits(), |
||||
setup(props, context) { |
||||
return useTabSetup(props, context, { |
||||
defaultValue: '*', |
||||
minValue: 0, |
||||
maxValue: 59, |
||||
valueRange: { start: 0, end: 59 }, |
||||
valueLoop: { start: 0, interval: 1 }, |
||||
}) |
||||
}, |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div :class="`${prefixCls}-config-list`"> |
||||
<RadioGroup v-model:value="type"> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.every" v-bind="beforeRadioAttrs"> |
||||
每秒 |
||||
</Radio> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.range" v-bind="beforeRadioAttrs"> |
||||
区间 |
||||
</Radio> |
||||
<span> 从 </span> |
||||
<AInput v-model:value="valueRange.start" type="number" v-bind="typeRangeAttrs" /> |
||||
<span> 秒 至 </span> |
||||
<AInput v-model:value="valueRange.end" type="number" v-bind="typeRangeAttrs" /> |
||||
<span> 秒 </span> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs"> |
||||
循环 |
||||
</Radio> |
||||
<span> 从 </span> |
||||
<AInput v-model:value="valueLoop.start" type="number" v-bind="typeLoopAttrs" /> |
||||
<span> 秒开始,间隔 </span> |
||||
<AInput v-model:value="valueLoop.interval" type="number" v-bind="typeLoopAttrs" /> |
||||
<span> 秒 </span> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs"> |
||||
指定 |
||||
</Radio> |
||||
<div class="list"> |
||||
<CheckboxGroup v-model:value="valueList"> |
||||
<template v-for="i in specifyRange" :key="i"> |
||||
<Checkbox :value="i" v-bind="typeSpecifyAttrs"> |
||||
{{ i }} |
||||
</Checkbox> |
||||
</template> |
||||
</CheckboxGroup> |
||||
</div> |
||||
</div> |
||||
</RadioGroup> |
||||
</div> |
||||
</template> |
@ -1,135 +0,0 @@
|
||||
<script lang="ts"> |
||||
import { computed, defineComponent, watch } from 'vue' |
||||
import { Checkbox, Input, Radio, Select } from 'ant-design-vue' |
||||
import { TypeEnum, useTabEmits, useTabProps, useTabSetup } from './useTabMixin' |
||||
|
||||
const WEEK_MAP_EN = { |
||||
1: 'SUN', |
||||
2: 'MON', |
||||
3: 'TUE', |
||||
4: 'WED', |
||||
5: 'THU', |
||||
6: 'FRI', |
||||
7: 'SAT', |
||||
} |
||||
|
||||
const WEEK_MAP_CN = { |
||||
1: '周日', |
||||
2: '周一', |
||||
3: '周二', |
||||
4: '周三', |
||||
5: '周四', |
||||
6: '周五', |
||||
7: '周六', |
||||
} |
||||
|
||||
export default defineComponent({ |
||||
name: 'WeekUI', |
||||
components: { AInput: Input, ASelect: Select, Checkbox, CheckboxGroup: Checkbox.Group, Radio, RadioGroup: Radio.Group }, |
||||
props: useTabProps({ |
||||
defaultValue: '?', |
||||
props: { |
||||
day: { type: String, default: '*' }, |
||||
}, |
||||
}), |
||||
emits: useTabEmits(), |
||||
setup(props, context) { |
||||
const disabledChoice = computed(() => { |
||||
return (props.day && props.day !== '?') || props.disabled |
||||
}) |
||||
const setup = useTabSetup(props, context, { |
||||
defaultType: TypeEnum.unset, |
||||
defaultValue: '?', |
||||
minValue: 1, |
||||
maxValue: 7, |
||||
// 0,7表示周日 1表示周一 |
||||
valueRange: { start: 1, end: 7 }, |
||||
valueLoop: { start: 2, interval: 1 }, |
||||
disabled: disabledChoice, |
||||
}) |
||||
const weekOptions = computed(() => { |
||||
const options: { label: string, value: number }[] = [] |
||||
for (const weekKey of Object.keys(WEEK_MAP_CN)) { |
||||
const weekName: string = WEEK_MAP_CN[weekKey] |
||||
options.push({ |
||||
value: Number.parseInt(weekKey), |
||||
label: weekName, |
||||
}) |
||||
} |
||||
return options |
||||
}) |
||||
|
||||
const typeRangeSelectAttrs = computed(() => ({ |
||||
class: ['w80'], |
||||
disabled: setup.typeRangeAttrs.value.disabled, |
||||
})) |
||||
|
||||
const typeLoopSelectAttrs = computed(() => ({ |
||||
class: ['w80'], |
||||
disabled: setup.typeLoopAttrs.value.disabled, |
||||
})) |
||||
|
||||
watch( |
||||
() => props.day, |
||||
() => { |
||||
setup.updateValue(disabledChoice.value ? '?' : setup.computeValue.value) |
||||
}, |
||||
) |
||||
|
||||
return { |
||||
...setup, |
||||
weekOptions, |
||||
typeLoopSelectAttrs, |
||||
typeRangeSelectAttrs, |
||||
WEEK_MAP_CN, |
||||
WEEK_MAP_EN, |
||||
} |
||||
}, |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div :class="`${prefixCls}-config-list`"> |
||||
<RadioGroup v-model:value="type"> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.unset" v-bind="beforeRadioAttrs"> |
||||
不设置 |
||||
</Radio> |
||||
<span class="tip-info">日和周只能设置其中之一</span> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.range" v-bind="beforeRadioAttrs"> |
||||
区间 |
||||
</Radio> |
||||
<span> 从 </span> |
||||
<ASelect v-model:value="valueRange.start" :options="weekOptions" v-bind="typeRangeSelectAttrs" /> |
||||
<span> 至 </span> |
||||
<ASelect v-model:value="valueRange.end" :options="weekOptions" v-bind="typeRangeSelectAttrs" /> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs"> |
||||
循环 |
||||
</Radio> |
||||
<span> 从 </span> |
||||
<ASelect v-model:value="valueLoop.start" :options="weekOptions" v-bind="typeLoopSelectAttrs" /> |
||||
<span> 开始,间隔 </span> |
||||
<AInput v-model:value="valueLoop.interval" type="number" v-bind="typeLoopAttrs" /> |
||||
<span> 天 </span> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs"> |
||||
指定 |
||||
</Radio> |
||||
<div class="list list-cn"> |
||||
<CheckboxGroup v-model:value="valueList"> |
||||
<template v-for="opt in weekOptions" :key="opt.value"> |
||||
<Checkbox :value="opt.value" v-bind="typeSpecifyAttrs"> |
||||
{{ opt.label }} |
||||
</Checkbox> |
||||
</template> |
||||
</CheckboxGroup> |
||||
</div> |
||||
</div> |
||||
</RadioGroup> |
||||
</div> |
||||
</template> |
@ -1,55 +0,0 @@
|
||||
<script lang="ts"> |
||||
import { defineComponent } from 'vue' |
||||
import { Input, Radio } from 'ant-design-vue' |
||||
import { useTabEmits, useTabProps, useTabSetup } from './useTabMixin' |
||||
|
||||
export default defineComponent({ |
||||
name: 'YearUI', |
||||
components: { AInput: Input, Radio, RadioGroup: Radio.Group }, |
||||
props: useTabProps({ |
||||
defaultValue: '*', |
||||
}), |
||||
emits: useTabEmits(), |
||||
setup(props, context) { |
||||
const nowYear = new Date().getFullYear() |
||||
return useTabSetup(props, context, { |
||||
defaultValue: '*', |
||||
minValue: 0, |
||||
valueRange: { start: nowYear, end: nowYear + 100 }, |
||||
valueLoop: { start: nowYear, interval: 1 }, |
||||
}) |
||||
}, |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div :class="`${prefixCls}-config-list`"> |
||||
<RadioGroup v-model:value="type"> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.every" v-bind="beforeRadioAttrs"> |
||||
每年 |
||||
</Radio> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.range" v-bind="beforeRadioAttrs"> |
||||
区间 |
||||
</Radio> |
||||
<span> 从 </span> |
||||
<AInput v-model:value="valueRange.start" type="number" class="w80" v-bind="typeRangeAttrs" /> |
||||
<span> 年 至 </span> |
||||
<AInput v-model:value="valueRange.end" type="number" class="w80" v-bind="typeRangeAttrs" /> |
||||
<span> 年 </span> |
||||
</div> |
||||
<div class="item"> |
||||
<Radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs"> |
||||
循环 |
||||
</Radio> |
||||
<span> 从 </span> |
||||
<AInput v-model:value="valueLoop.start" type="number" class="w80" v-bind="typeLoopAttrs" /> |
||||
<span> 年开始,间隔 </span> |
||||
<AInput v-model:value="valueLoop.interval" type="number" class="w80" v-bind="typeLoopAttrs" /> |
||||
<span> 年 </span> |
||||
</div> |
||||
</RadioGroup> |
||||
</div> |
||||
</template> |
@ -1,204 +0,0 @@
|
||||
// 主要用于日和星期的互斥使用
|
||||
import { computed, inject, reactive, ref, unref, watch } from 'vue' |
||||
import { propTypes } from '@/utils/propTypes' |
||||
|
||||
export enum TypeEnum { |
||||
unset = 'UNSET', |
||||
every = 'EVERY', |
||||
range = 'RANGE', |
||||
loop = 'LOOP', |
||||
work = 'WORK', |
||||
last = 'LAST', |
||||
specify = 'SPECIFY', |
||||
} |
||||
|
||||
// use 公共 props
|
||||
export function useTabProps(options) { |
||||
const defaultValue = options?.defaultValue ?? '?' |
||||
return { |
||||
value: propTypes.string.def(defaultValue), |
||||
disabled: propTypes.bool.def(false), |
||||
...options?.props, |
||||
} |
||||
} |
||||
|
||||
// use 公共 emits
|
||||
export function useTabEmits() { |
||||
return ['change', 'update:value'] |
||||
} |
||||
|
||||
// use 公共 setup
|
||||
export function useTabSetup(props, context, options) { |
||||
const { emit } = context |
||||
const prefixCls = inject('prefixCls') |
||||
const defaultValue = ref(options?.defaultValue ?? '?') |
||||
// 类型
|
||||
const type = ref(options.defaultType ?? TypeEnum.every) |
||||
const valueList = ref<any[]>([]) |
||||
// 对于不同的类型,所定义的值也有所不同
|
||||
const valueRange = reactive(options.valueRange) |
||||
const valueLoop = reactive(options.valueLoop) |
||||
const valueWeek = reactive(options.valueWeek) |
||||
const valueWork = ref(options.valueWork) |
||||
const maxValue = ref(options.maxValue) |
||||
const minValue = ref(options.minValue) |
||||
|
||||
// 根据不同的类型计算出的value
|
||||
const computeValue = computed(() => { |
||||
const valueArray: any[] = [] |
||||
switch (type.value) { |
||||
case TypeEnum.unset: |
||||
valueArray.push('?') |
||||
break |
||||
case TypeEnum.every: |
||||
valueArray.push('*') |
||||
break |
||||
case TypeEnum.range: |
||||
valueArray.push(`${valueRange.start}-${valueRange.end}`) |
||||
break |
||||
case TypeEnum.loop: |
||||
valueArray.push(`${valueLoop.start}/${valueLoop.interval}`) |
||||
break |
||||
case TypeEnum.work: |
||||
valueArray.push(`${valueWork.value}W`) |
||||
break |
||||
case TypeEnum.last: |
||||
valueArray.push('L') |
||||
break |
||||
case TypeEnum.specify: |
||||
if (valueList.value.length === 0) |
||||
valueList.value.push(minValue.value) |
||||
|
||||
valueArray.push(valueList.value.join(',')) |
||||
break |
||||
default: |
||||
valueArray.push(defaultValue.value) |
||||
break |
||||
} |
||||
return valueArray.length > 0 ? valueArray.join('') : defaultValue.value |
||||
}) |
||||
// 指定值范围区间,介于最小值和最大值之间
|
||||
const specifyRange = computed(() => { |
||||
const range: number[] = [] |
||||
if (maxValue.value != null) { |
||||
for (let i = minValue.value; i <= maxValue.value; i++) |
||||
range.push(i) |
||||
} |
||||
return range |
||||
}) |
||||
|
||||
watch( |
||||
() => props.value, |
||||
(val) => { |
||||
if (val !== computeValue.value) |
||||
parseValue(val) |
||||
}, |
||||
{ immediate: true }, |
||||
) |
||||
|
||||
watch(computeValue, v => updateValue(v)) |
||||
|
||||
function updateValue(value) { |
||||
emit('change', value) |
||||
emit('update:value', value) |
||||
} |
||||
|
||||
/** |
||||
* parseValue |
||||
* @param value |
||||
*/ |
||||
function parseValue(value) { |
||||
if (value === computeValue.value) |
||||
return |
||||
|
||||
try { |
||||
if (!value || value === defaultValue.value) { |
||||
type.value = TypeEnum.every |
||||
} |
||||
else if (value.includes('?')) { |
||||
type.value = TypeEnum.unset |
||||
} |
||||
else if (value.includes('-')) { |
||||
type.value = TypeEnum.range |
||||
const values = value.split('-') |
||||
if (values.length >= 2) { |
||||
valueRange.start = Number.parseInt(values[0]) |
||||
valueRange.end = Number.parseInt(values[1]) |
||||
} |
||||
} |
||||
else if (value.includes('/')) { |
||||
type.value = TypeEnum.loop |
||||
const values = value.split('/') |
||||
if (values.length >= 2) { |
||||
valueLoop.start = value[0] === '*' ? 0 : Number.parseInt(values[0]) |
||||
valueLoop.interval = Number.parseInt(values[1]) |
||||
} |
||||
} |
||||
else if (value.includes('W')) { |
||||
type.value = TypeEnum.work |
||||
const values = value.split('W') |
||||
if (!values[0] && !Number.isNaN(values[0])) |
||||
valueWork.value = Number.parseInt(values[0]) |
||||
} |
||||
else if (value.includes('L')) { |
||||
type.value = TypeEnum.last |
||||
} |
||||
else if (value.includes(',') || !Number.isNaN(value)) { |
||||
type.value = TypeEnum.specify |
||||
valueList.value = value.split(',').map(item => Number.parseInt(item)) |
||||
} |
||||
else { |
||||
type.value = TypeEnum.every |
||||
} |
||||
} |
||||
catch (e) { |
||||
type.value = TypeEnum.every |
||||
} |
||||
} |
||||
|
||||
const beforeRadioAttrs = computed(() => ({ |
||||
class: ['choice'], |
||||
disabled: props.disabled || unref(options.disabled), |
||||
})) |
||||
const inputNumberAttrs = computed(() => ({ |
||||
class: ['w60'], |
||||
max: maxValue.value, |
||||
min: minValue.value, |
||||
precision: 0, |
||||
})) |
||||
const typeRangeAttrs = computed(() => ({ |
||||
disabled: type.value !== TypeEnum.range || props.disabled || unref(options.disabled), |
||||
...inputNumberAttrs.value, |
||||
})) |
||||
const typeLoopAttrs = computed(() => ({ |
||||
disabled: type.value !== TypeEnum.loop || props.disabled || unref(options.disabled), |
||||
...inputNumberAttrs.value, |
||||
})) |
||||
const typeSpecifyAttrs = computed(() => ({ |
||||
disabled: type.value !== TypeEnum.specify || props.disabled || unref(options.disabled), |
||||
class: ['list-check-item'], |
||||
})) |
||||
|
||||
return { |
||||
type, |
||||
TypeEnum, |
||||
prefixCls, |
||||
defaultValue, |
||||
valueRange, |
||||
valueLoop, |
||||
valueWeek, |
||||
valueList, |
||||
valueWork, |
||||
maxValue, |
||||
minValue, |
||||
computeValue, |
||||
specifyRange, |
||||
updateValue, |
||||
parseValue, |
||||
beforeRadioAttrs, |
||||
inputNumberAttrs, |
||||
typeRangeAttrs, |
||||
typeLoopAttrs, |
||||
typeSpecifyAttrs, |
||||
} |
||||
} |
@ -1,50 +0,0 @@
|
||||
/* eslint-disable prefer-promise-reject-errors */ |
||||
import CronParser from 'cron-parser' |
||||
import type { ValidatorRule } from 'ant-design-vue/lib/form/interface' |
||||
|
||||
const cronRule: ValidatorRule = { |
||||
// eslint-disable-next-line no-empty-pattern
|
||||
validator({}, value) { |
||||
// 没填写就不校验
|
||||
if (!value) |
||||
return Promise.resolve() |
||||
|
||||
const values: string[] = value.split(' ').filter(item => !!item) |
||||
if (values.length > 7) |
||||
return Promise.reject('Cron表达式最多7项!') |
||||
|
||||
// 检查第7项
|
||||
let val: string = value |
||||
if (values.length === 7) { |
||||
const year = values[6] |
||||
if (year !== '*' && year !== '?') { |
||||
let yearValues: string[] = [] |
||||
if (year.includes('-')) |
||||
yearValues = year.split('-') |
||||
else if (year.indexOf('/')) |
||||
yearValues = year.split('/') |
||||
else |
||||
yearValues = [year] |
||||
|
||||
// 判断是否都是数字
|
||||
const checkYear = yearValues.some(item => Number.isNaN(Number(item))) |
||||
if (checkYear) |
||||
return Promise.reject(`Cron表达式参数[年]错误:${year}`) |
||||
} |
||||
// 取其中的前六项
|
||||
val = values.slice(0, 6).join(' ') |
||||
} |
||||
// 6位 没有年
|
||||
// 5位没有秒、年
|
||||
try { |
||||
const iter = CronParser.parseExpression(val) |
||||
iter.next() |
||||
return Promise.resolve() |
||||
} |
||||
catch (e: any) { |
||||
return Promise.reject(`Cron表达式错误:${e}`) |
||||
} |
||||
}, |
||||
} |
||||
|
||||
export default cronRule.validator |
@ -1,191 +0,0 @@
|
||||
export enum StationStatus { |
||||
Closed = '0', |
||||
Using = '1', |
||||
} |
||||
|
||||
/** |
||||
* 站点状态 |
||||
*/ |
||||
export const StationStatusOptions = [ |
||||
{ |
||||
value: StationStatus.Closed, |
||||
label: '关闭中', |
||||
}, |
||||
{ |
||||
value: StationStatus.Using, |
||||
label: '使用中', |
||||
}, |
||||
] |
||||
|
||||
export enum StationEnvironments { |
||||
OpenAir = '0', |
||||
Underground = '1', |
||||
Carport = '2', |
||||
} |
||||
|
||||
/** |
||||
* 站点环境 |
||||
*/ |
||||
export const StationEnvironmentOptions = [ |
||||
{ |
||||
value: StationEnvironments.OpenAir, |
||||
label: '露天', |
||||
}, |
||||
{ |
||||
value: StationEnvironments.Underground, |
||||
label: '地下', |
||||
}, |
||||
{ |
||||
value: StationEnvironments.Carport, |
||||
label: '车棚', |
||||
}, |
||||
] |
||||
|
||||
/** |
||||
* 建筑场所 |
||||
*/ |
||||
export const BuildingLotOptions = [ |
||||
{ |
||||
value: '1', |
||||
label: '居民区', |
||||
}, |
||||
{ |
||||
value: '2', |
||||
label: '公共机构', |
||||
}, |
||||
{ |
||||
value: '3', |
||||
label: '企事业单位', |
||||
}, |
||||
{ |
||||
value: '4', |
||||
label: '写字楼', |
||||
}, |
||||
{ |
||||
value: '5', |
||||
label: '工业园区', |
||||
}, |
||||
{ |
||||
value: '6', |
||||
label: '交通枢纽', |
||||
}, |
||||
{ |
||||
value: '7', |
||||
label: '大型文体设施', |
||||
}, |
||||
{ |
||||
value: '8', |
||||
label: '城市绿地', |
||||
}, |
||||
{ |
||||
value: '9', |
||||
label: '大型建筑配建停车场', |
||||
}, |
||||
{ |
||||
value: '10', |
||||
label: '路边停车位', |
||||
}, |
||||
{ |
||||
value: '11', |
||||
label: '城际高速服务区', |
||||
}, |
||||
{ |
||||
value: '255', |
||||
label: '其他', |
||||
}, |
||||
] |
||||
|
||||
/** |
||||
* 运营方式 |
||||
*/ |
||||
export const OperateWayOptions = [ |
||||
{ |
||||
value: 'self', |
||||
label: '自营', |
||||
}, |
||||
{ |
||||
value: 'other', |
||||
label: '代运营', |
||||
}, |
||||
] |
||||
|
||||
export enum ParkingChargeWay { |
||||
Free = '0', |
||||
LimitedFree = '1', |
||||
Changing = '2', |
||||
} |
||||
|
||||
export const ParkingChargeWayOptions = [ |
||||
{ |
||||
value: ParkingChargeWay.Free, |
||||
label: '免费停车', |
||||
}, |
||||
{ |
||||
value: ParkingChargeWay.LimitedFree, |
||||
label: '限时免费', |
||||
}, |
||||
{ |
||||
value: ParkingChargeWay.Changing, |
||||
label: '收费停车', |
||||
}, |
||||
] |
||||
|
||||
/** |
||||
* 支付状态 |
||||
*/ |
||||
export const PayStatusOptions = [ |
||||
{ |
||||
label: '未支付', |
||||
value: 'notPay', |
||||
}, |
||||
{ |
||||
label: '支付中', |
||||
value: 'paying', |
||||
}, |
||||
{ |
||||
label: '支付成功', |
||||
value: 'suc', |
||||
}, |
||||
{ |
||||
label: '支付失败', |
||||
value: 'fail', |
||||
}, |
||||
{ |
||||
label: '待退款', |
||||
value: 'waitRefund', |
||||
}, |
||||
{ |
||||
label: '退款中', |
||||
value: 'refunding', |
||||
}, |
||||
{ |
||||
label: '退款成功', |
||||
value: 'refundSuc', |
||||
}, |
||||
{ |
||||
label: '退款失败', |
||||
value: 'refundFailed', |
||||
}, |
||||
] |
||||
|
||||
/** |
||||
* 支付渠道 |
||||
*/ |
||||
export const PayModesEnum = [ |
||||
{ |
||||
label: '微信分支付', |
||||
value: 'wechatFen', |
||||
}, |
||||
{ |
||||
label: '微信扫码', |
||||
value: '1', |
||||
}, |
||||
{ |
||||
label: '支付宝扫码', |
||||
value: '2', |
||||
}, |
||||
{ |
||||
label: '银座钱包', |
||||
value: '3', |
||||
}, |
||||
] |
@ -1,41 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { Card, Tag } from 'ant-design-vue' |
||||
import { growCardList } from '../data' |
||||
import { CountTo } from '@/components/CountTo' |
||||
import { SvgIcon } from '@/components/Icon' |
||||
|
||||
defineProps({ |
||||
loading: { |
||||
type: Boolean, |
||||
}, |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="md:flex"> |
||||
<template v-for="(item, index) in growCardList" :key="item.title"> |
||||
<Card |
||||
:loading="loading" |
||||
:title="item.title" |
||||
class="w-full md:w-1/4 !md:mt-0" |
||||
:class="{ '!md:mr-4': index + 1 < 4, '!mt-4': index > 0 }" |
||||
> |
||||
<template #extra> |
||||
<Tag :color="item.color"> |
||||
{{ item.action }} |
||||
</Tag> |
||||
</template> |
||||
|
||||
<div class="flex items-center justify-between px-4 py-4"> |
||||
<CountTo prefix="$" :start-val="1" :end-val="item.value" class="text-2xl" /> |
||||
<SvgIcon :name="item.icon" :size="40" /> |
||||
</div> |
||||
|
||||
<div class="flex justify-between p-2 px-4"> |
||||
<span>总{{ item.title }}</span> |
||||
<CountTo prefix="$" :start-val="1" :end-val="item.total" /> |
||||
</div> |
||||
</Card> |
||||
</template> |
||||
</div> |
||||
</template> |
@ -1,61 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import type { Ref } from 'vue' |
||||
import { ref, watch } from 'vue' |
||||
import { Card } from 'ant-design-vue' |
||||
import { useECharts } from '@/hooks/web/useECharts' |
||||
import { propTypes } from '@/utils/propTypes' |
||||
|
||||
const props = defineProps({ |
||||
loading: Boolean, |
||||
width: propTypes.string.def('100%'), |
||||
height: propTypes.string.def('300px'), |
||||
}) |
||||
|
||||
const chartRef = ref<HTMLDivElement | null>(null) |
||||
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>) |
||||
|
||||
watch( |
||||
() => props.loading, |
||||
() => { |
||||
if (props.loading) |
||||
return |
||||
|
||||
setOptions({ |
||||
tooltip: { |
||||
trigger: 'item', |
||||
}, |
||||
|
||||
series: [ |
||||
{ |
||||
name: '访问来源', |
||||
type: 'pie', |
||||
radius: '80%', |
||||
center: ['50%', '50%'], |
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'], |
||||
data: [ |
||||
{ value: 500, name: '电子产品' }, |
||||
{ value: 310, name: '服装' }, |
||||
{ value: 274, name: '化妆品' }, |
||||
{ value: 400, name: '家居' }, |
||||
].sort((a, b) => { |
||||
return a.value - b.value |
||||
}), |
||||
roseType: 'radius', |
||||
animationType: 'scale', |
||||
animationEasing: 'exponentialInOut', |
||||
animationDelay() { |
||||
return Math.random() * 400 |
||||
}, |
||||
}, |
||||
], |
||||
}) |
||||
}, |
||||
{ immediate: true }, |
||||
) |
||||
</script> |
||||
|
||||
<template> |
||||
<Card title="成交占比" :loading="loading"> |
||||
<div ref="chartRef" :style="{ width, height }" /> |
||||
</Card> |
||||
</template> |
@ -1,34 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { ref } from 'vue' |
||||
import { Card } from 'ant-design-vue' |
||||
import VisitAnalysis from './VisitAnalysis.vue' |
||||
import VisitAnalysisBar from './VisitAnalysisBar.vue' |
||||
|
||||
const activeKey = ref('tab1') |
||||
|
||||
const tabListTitle = [ |
||||
{ |
||||
key: 'tab1', |
||||
tab: '流量趋势', |
||||
}, |
||||
{ |
||||
key: 'tab2', |
||||
tab: '访问量', |
||||
}, |
||||
] |
||||
|
||||
function onTabChange(key: string) { |
||||
activeKey.value = key |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<Card :tab-list="tabListTitle" v-bind="$attrs" :active-tab-key="activeKey" @tab-change="onTabChange"> |
||||
<p v-if="activeKey === 'tab1'"> |
||||
<VisitAnalysis /> |
||||
</p> |
||||
<p v-if="activeKey === 'tab2'"> |
||||
<VisitAnalysisBar /> |
||||
</p> |
||||
</Card> |
||||
</template> |
@ -1,83 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import type { Ref } from 'vue' |
||||
import { onMounted, ref } from 'vue' |
||||
import { basicProps } from './props' |
||||
import { useECharts } from '@/hooks/web/useECharts' |
||||
|
||||
defineProps({ |
||||
...basicProps, |
||||
}) |
||||
const chartRef = ref<HTMLDivElement | null>(null) |
||||
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>) |
||||
|
||||
onMounted(() => { |
||||
setOptions({ |
||||
tooltip: { |
||||
trigger: 'axis', |
||||
axisPointer: { |
||||
lineStyle: { |
||||
width: 1, |
||||
color: '#019680', |
||||
}, |
||||
}, |
||||
}, |
||||
xAxis: { |
||||
type: 'category', |
||||
boundaryGap: false, |
||||
data: [...Array.from({ length: 18 })].map((_item, index) => `${index + 6}:00`), |
||||
splitLine: { |
||||
show: true, |
||||
lineStyle: { |
||||
width: 1, |
||||
type: 'solid', |
||||
color: 'rgba(226,226,226,0.5)', |
||||
}, |
||||
}, |
||||
axisTick: { |
||||
show: false, |
||||
}, |
||||
}, |
||||
yAxis: [ |
||||
{ |
||||
type: 'value', |
||||
max: 80000, |
||||
splitNumber: 4, |
||||
axisTick: { |
||||
show: false, |
||||
}, |
||||
splitArea: { |
||||
show: true, |
||||
areaStyle: { |
||||
color: ['rgba(255,255,255,0.2)', 'rgba(226,226,226,0.2)'], |
||||
}, |
||||
}, |
||||
}, |
||||
], |
||||
grid: { left: '1%', right: '1%', top: '2 %', bottom: 0, containLabel: true }, |
||||
series: [ |
||||
{ |
||||
smooth: true, |
||||
data: [111, 222, 4000, 18000, 33333, 55555, 66666, 33333, 14000, 36000, 66666, 44444, 22222, 11111, 4000, 2000, 500, 333, 222, 111], |
||||
type: 'line', |
||||
areaStyle: {}, |
||||
itemStyle: { |
||||
color: '#5ab1ef', |
||||
}, |
||||
}, |
||||
{ |
||||
smooth: true, |
||||
data: [33, 66, 88, 333, 3333, 5000, 18000, 3000, 1200, 13000, 22000, 11000, 2221, 1201, 390, 198, 60, 30, 22, 11], |
||||
type: 'line', |
||||
areaStyle: {}, |
||||
itemStyle: { |
||||
color: '#019680', |
||||
}, |
||||
}, |
||||
], |
||||
}) |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div ref="chartRef" :style="{ height, width }" /> |
||||
</template> |
@ -1,47 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import type { Ref } from 'vue' |
||||
import { onMounted, ref } from 'vue' |
||||
import { basicProps } from './props' |
||||
import { useECharts } from '@/hooks/web/useECharts' |
||||
|
||||
defineProps({ |
||||
...basicProps, |
||||
}) |
||||
|
||||
const chartRef = ref<HTMLDivElement | null>(null) |
||||
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>) |
||||
onMounted(() => { |
||||
setOptions({ |
||||
tooltip: { |
||||
trigger: 'axis', |
||||
axisPointer: { |
||||
lineStyle: { |
||||
width: 1, |
||||
color: '#019680', |
||||
}, |
||||
}, |
||||
}, |
||||
grid: { left: '1%', right: '1%', top: '2 %', bottom: 0, containLabel: true }, |
||||
xAxis: { |
||||
type: 'category', |
||||
data: [...Array.from({ length: 12 })].map((_item, index) => `${index + 1}月`), |
||||
}, |
||||
yAxis: { |
||||
type: 'value', |
||||
max: 8000, |
||||
splitNumber: 4, |
||||
}, |
||||
series: [ |
||||
{ |
||||
data: [3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000, 3200, 4800], |
||||
type: 'bar', |
||||
barMaxWidth: 80, |
||||
}, |
||||
], |
||||
}) |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div ref="chartRef" :style="{ height, width }" /> |
||||
</template> |
@ -1,91 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import type { Ref } from 'vue' |
||||
import { ref, watch } from 'vue' |
||||
import { Card } from 'ant-design-vue' |
||||
import { useECharts } from '@/hooks/web/useECharts' |
||||
import { propTypes } from '@/utils/propTypes' |
||||
|
||||
const props = defineProps({ |
||||
loading: Boolean, |
||||
width: propTypes.string.def('100%'), |
||||
height: propTypes.string.def('300px'), |
||||
}) |
||||
const chartRef = ref<HTMLDivElement | null>(null) |
||||
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>) |
||||
|
||||
watch( |
||||
() => props.loading, |
||||
() => { |
||||
if (props.loading) |
||||
return |
||||
|
||||
setOptions({ |
||||
legend: { |
||||
bottom: 0, |
||||
data: ['访问', '购买'], |
||||
}, |
||||
tooltip: {}, |
||||
radar: { |
||||
radius: '60%', |
||||
splitNumber: 8, |
||||
indicator: [ |
||||
{ |
||||
name: '电脑', |
||||
}, |
||||
{ |
||||
name: '充电器', |
||||
}, |
||||
{ |
||||
name: '耳机', |
||||
}, |
||||
{ |
||||
name: '手机', |
||||
}, |
||||
{ |
||||
name: 'Ipad', |
||||
}, |
||||
{ |
||||
name: '耳机', |
||||
}, |
||||
], |
||||
}, |
||||
series: [ |
||||
{ |
||||
type: 'radar', |
||||
symbolSize: 0, |
||||
areaStyle: { |
||||
shadowBlur: 0, |
||||
shadowColor: 'rgba(0,0,0,.2)', |
||||
shadowOffsetX: 0, |
||||
shadowOffsetY: 10, |
||||
opacity: 1, |
||||
}, |
||||
data: [ |
||||
{ |
||||
value: [90, 50, 86, 40, 50, 20], |
||||
name: '访问', |
||||
itemStyle: { |
||||
color: '#b6a2de', |
||||
}, |
||||
}, |
||||
{ |
||||
value: [70, 75, 70, 76, 20, 85], |
||||
name: '购买', |
||||
itemStyle: { |
||||
color: '#5ab1ef', |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
], |
||||
}) |
||||
}, |
||||
{ immediate: true }, |
||||
) |
||||
</script> |
||||
|
||||
<template> |
||||
<Card title="转化率" :loading="loading"> |
||||
<div ref="chartRef" :style="{ width, height }" /> |
||||
</Card> |
||||
</template> |
@ -1,79 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import type { Ref } from 'vue' |
||||
import { ref, watch } from 'vue' |
||||
import { Card } from 'ant-design-vue' |
||||
import { useECharts } from '@/hooks/web/useECharts' |
||||
import { propTypes } from '@/utils/propTypes' |
||||
|
||||
const props = defineProps({ |
||||
loading: Boolean, |
||||
width: propTypes.string.def('100%'), |
||||
height: propTypes.string.def('300px'), |
||||
}) |
||||
const chartRef = ref<HTMLDivElement | null>(null) |
||||
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>) |
||||
|
||||
watch( |
||||
() => props.loading, |
||||
() => { |
||||
if (props.loading) |
||||
return |
||||
|
||||
setOptions({ |
||||
tooltip: { |
||||
trigger: 'item', |
||||
}, |
||||
legend: { |
||||
bottom: '1%', |
||||
left: 'center', |
||||
}, |
||||
series: [ |
||||
{ |
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'], |
||||
name: '访问来源', |
||||
type: 'pie', |
||||
radius: ['40%', '70%'], |
||||
avoidLabelOverlap: false, |
||||
itemStyle: { |
||||
borderRadius: 10, |
||||
borderColor: '#fff', |
||||
borderWidth: 2, |
||||
}, |
||||
label: { |
||||
show: false, |
||||
position: 'center', |
||||
}, |
||||
emphasis: { |
||||
label: { |
||||
show: true, |
||||
fontSize: '12', |
||||
fontWeight: 'bold', |
||||
}, |
||||
}, |
||||
labelLine: { |
||||
show: false, |
||||
}, |
||||
data: [ |
||||
{ value: 1048, name: '搜索引擎' }, |
||||
{ value: 735, name: '直接访问' }, |
||||
{ value: 580, name: '邮件营销' }, |
||||
{ value: 484, name: '联盟广告' }, |
||||
], |
||||
animationType: 'scale', |
||||
animationEasing: 'exponentialInOut', |
||||
animationDelay() { |
||||
return Math.random() * 100 |
||||
}, |
||||
}, |
||||
], |
||||
}) |
||||
}, |
||||
{ immediate: true }, |
||||
) |
||||
</script> |
||||
|
||||
<template> |
||||
<Card title="访问来源" :loading="loading"> |
||||
<div ref="chartRef" :style="{ width, height }" /> |
||||
</Card> |
||||
</template> |
@ -1,16 +0,0 @@
|
||||
import type { PropType } from 'vue' |
||||
|
||||
export interface BasicProps { |
||||
width: string |
||||
height: string |
||||
} |
||||
export const basicProps = { |
||||
width: { |
||||
type: String as PropType<string>, |
||||
default: '100%', |
||||
}, |
||||
height: { |
||||
type: String as PropType<string>, |
||||
default: '280px', |
||||
}, |
||||
} |
@ -1,43 +0,0 @@
|
||||
export interface GrowCardItem { |
||||
icon: string |
||||
title: string |
||||
value: number |
||||
total: number |
||||
color: string |
||||
action: string |
||||
} |
||||
|
||||
export const growCardList: GrowCardItem[] = [ |
||||
{ |
||||
title: '访问数', |
||||
icon: 'visit-count', |
||||
value: 2000, |
||||
total: 120000, |
||||
color: 'green', |
||||
action: '月', |
||||
}, |
||||
{ |
||||
title: '成交额', |
||||
icon: 'total-sales', |
||||
value: 20000, |
||||
total: 500000, |
||||
color: 'blue', |
||||
action: '月', |
||||
}, |
||||
{ |
||||
title: '下载数', |
||||
icon: 'download-count', |
||||
value: 8000, |
||||
total: 120000, |
||||
color: 'orange', |
||||
action: '周', |
||||
}, |
||||
{ |
||||
title: '成交数', |
||||
icon: 'transaction', |
||||
value: 5000, |
||||
total: 50000, |
||||
color: 'purple', |
||||
action: '年', |
||||
}, |
||||
] |
@ -1,26 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { ref } from 'vue' |
||||
import GrowCard from './components/GrowCard.vue' |
||||
import SiteAnalysis from './components/SiteAnalysis.vue' |
||||
import VisitSource from './components/VisitSource.vue' |
||||
import VisitRadar from './components/VisitRadar.vue' |
||||
import SalesProductPie from './components/SalesProductPie.vue' |
||||
|
||||
const loading = ref(true) |
||||
|
||||
setTimeout(() => { |
||||
loading.value = false |
||||
}, 500) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="p-4"> |
||||
<GrowCard :loading="loading" class="enter-y" /> |
||||
<SiteAnalysis class="enter-y !my-4" :loading="loading" /> |
||||
<div class="enter-y md:flex"> |
||||
<VisitRadar class="w-full md:w-1/3" :loading="loading" /> |
||||
<VisitSource class="w-full !my-4 md:w-1/3 !md:mx-4 !md:my-0" :loading="loading" /> |
||||
<SalesProductPie class="w-full md:w-1/3" :loading="loading" /> |
||||
</div> |
||||
</div> |
||||
</template> |
@ -1,34 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { Card, List } from 'ant-design-vue' |
||||
import { dynamicInfoItems } from './data' |
||||
import { SvgIcon } from '@/components/Icon' |
||||
|
||||
const ListItem = List.Item |
||||
const ListItemMeta = List.Item.Meta |
||||
</script> |
||||
|
||||
<template> |
||||
<Card title="最新动态" v-bind="$attrs"> |
||||
<template #extra> |
||||
<a-button type="link"> |
||||
更多 |
||||
</a-button> |
||||
</template> |
||||
<List item-layout="horizontal" :data-source="dynamicInfoItems"> |
||||
<template #renderItem="{ item }"> |
||||
<ListItem> |
||||
<ListItemMeta> |
||||
<template #description> |
||||
{{ item.date }} |
||||
</template> |
||||
<!-- eslint-disable-next-line --> |
||||
<template #title> {{ item.name }} <span v-html="item.desc"> </span> </template> |
||||
<template #avatar> |
||||
<SvgIcon :name="item.avatar" :size="30" /> |
||||
</template> |
||||
</ListItemMeta> |
||||
</ListItem> |
||||
</template> |
||||
</List> |
||||
</Card> |
||||
</template> |
@ -1,87 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { Card, CardGrid } from 'ant-design-vue' |
||||
|
||||
interface GroupItem { |
||||
title: string |
||||
icon: string |
||||
color: string |
||||
desc: string |
||||
date: string |
||||
group: string |
||||
} |
||||
|
||||
const groupItems: GroupItem[] = [ |
||||
{ |
||||
title: 'Github', |
||||
icon: 'i-carbon:logo-github', |
||||
color: '', |
||||
desc: '不要等待机会,而要创造机会。', |
||||
group: '开源组', |
||||
date: '2021-04-01', |
||||
}, |
||||
{ |
||||
title: 'Vue', |
||||
icon: 'i-ion:logo-vue', |
||||
color: '#3fb27f', |
||||
desc: '现在的你决定将来的你。', |
||||
group: '算法组', |
||||
date: '2021-04-01', |
||||
}, |
||||
{ |
||||
title: 'Html5', |
||||
icon: 'i-ion:logo-html5', |
||||
color: '#e18525', |
||||
desc: '没有什么才能比努力更重要。', |
||||
group: '上班摸鱼', |
||||
date: '2021-04-01', |
||||
}, |
||||
{ |
||||
title: 'Angular', |
||||
icon: 'i-ion:logo-angular', |
||||
color: '#bf0c2c', |
||||
desc: '热情和欲望可以突破一切难关。', |
||||
group: 'UI', |
||||
date: '2021-04-01', |
||||
}, |
||||
{ |
||||
title: 'React', |
||||
icon: 'i-bx:bxl-react', |
||||
color: '#00d8ff', |
||||
desc: '健康的身体是实现目标的基石。', |
||||
group: '技术牛', |
||||
date: '2021-04-01', |
||||
}, |
||||
{ |
||||
title: 'Js', |
||||
icon: 'i-ion:logo-javascript', |
||||
color: '#EBD94E', |
||||
desc: '路是走出来的,而不是空想出来的。', |
||||
group: '架构组', |
||||
date: '2021-04-01', |
||||
}, |
||||
] |
||||
</script> |
||||
|
||||
<template> |
||||
<Card title="项目" v-bind="$attrs"> |
||||
<template #extra> |
||||
<a-button type="link"> |
||||
更多 |
||||
</a-button> |
||||
</template> |
||||
|
||||
<CardGrid v-for="item in groupItems" :key="item.title" class="!w-full !md:w-1/3"> |
||||
<span class="flex"> |
||||
<span :class="item.icon" :style="{ color: item.color }" class="text-[30px]" /> |
||||
<span class="ml-4 text-lg">{{ item.title }}</span> |
||||
</span> |
||||
<div class="text-secondary mt-2 h-10 flex"> |
||||
{{ item.desc }} |
||||
</div> |
||||
<div class="text-secondary flex justify-between"> |
||||
<span>{{ item.group }}</span> |
||||
<span>{{ item.date }}</span> |
||||
</div> |
||||
</CardGrid> |
||||
</Card> |
||||
</template> |
@ -1,53 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { Card, CardGrid } from 'ant-design-vue' |
||||
|
||||
interface NavItem { |
||||
title: string |
||||
icon: string |
||||
color: string |
||||
} |
||||
|
||||
const navItems: NavItem[] = [ |
||||
{ |
||||
title: '首页', |
||||
icon: 'i-ion:home-outline', |
||||
color: '#1fdaca', |
||||
}, |
||||
{ |
||||
title: '仪表盘', |
||||
icon: 'i-ion:grid-outline', |
||||
color: '#bf0c2c', |
||||
}, |
||||
{ |
||||
title: '组件', |
||||
icon: 'i-ion:layers-outline', |
||||
color: '#e18525', |
||||
}, |
||||
{ |
||||
title: '系统管理', |
||||
icon: 'i-ion:settings-outline', |
||||
color: '#3fb27f', |
||||
}, |
||||
{ |
||||
title: '权限管理', |
||||
icon: 'i-ion:key-outline', |
||||
color: '#4daf1bc9', |
||||
}, |
||||
{ |
||||
title: '图表', |
||||
icon: 'i-ion:bar-chart-outline', |
||||
color: '#00d8ff', |
||||
}, |
||||
] |
||||
</script> |
||||
|
||||
<template> |
||||
<Card title="快捷导航"> |
||||
<CardGrid v-for="item in navItems" :key="item.title"> |
||||
<span class="flex flex-col items-center"> |
||||
<span :class="item.icon" :style="{ color: item.color }" class="text-[20px]" /> |
||||
<span class="text-md mt-2 truncate">{{ item.title }}</span> |
||||
</span> |
||||
</CardGrid> |
||||
</Card> |
||||
</template> |
@ -1,91 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import type { Ref } from 'vue' |
||||
import { ref, watch } from 'vue' |
||||
import { Card } from 'ant-design-vue' |
||||
import { useECharts } from '@/hooks/web/useECharts' |
||||
import { propTypes } from '@/utils/propTypes' |
||||
|
||||
const props = defineProps({ |
||||
loading: Boolean, |
||||
width: propTypes.string.def('100%'), |
||||
height: propTypes.string.def('400px'), |
||||
}) |
||||
|
||||
const chartRef = ref<HTMLDivElement | null>(null) |
||||
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>) |
||||
watch( |
||||
() => props.loading, |
||||
() => { |
||||
if (props.loading) |
||||
return |
||||
|
||||
setOptions({ |
||||
legend: { |
||||
bottom: 0, |
||||
data: ['Visits', 'Sales'], |
||||
}, |
||||
tooltip: {}, |
||||
radar: { |
||||
radius: '60%', |
||||
splitNumber: 8, |
||||
indicator: [ |
||||
{ |
||||
name: '2017', |
||||
}, |
||||
{ |
||||
name: '2017', |
||||
}, |
||||
{ |
||||
name: '2018', |
||||
}, |
||||
{ |
||||
name: '2019', |
||||
}, |
||||
{ |
||||
name: '2020', |
||||
}, |
||||
{ |
||||
name: '2021', |
||||
}, |
||||
], |
||||
}, |
||||
series: [ |
||||
{ |
||||
type: 'radar', |
||||
symbolSize: 0, |
||||
areaStyle: { |
||||
shadowBlur: 0, |
||||
shadowColor: 'rgba(0,0,0,.2)', |
||||
shadowOffsetX: 0, |
||||
shadowOffsetY: 10, |
||||
opacity: 1, |
||||
}, |
||||
data: [ |
||||
{ |
||||
value: [90, 50, 86, 40, 50, 20], |
||||
name: 'Visits', |
||||
itemStyle: { |
||||
color: '#b6a2de', |
||||
}, |
||||
}, |
||||
{ |
||||
value: [70, 75, 70, 76, 20, 85], |
||||
name: 'Sales', |
||||
itemStyle: { |
||||
color: '#67e0e3', |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
], |
||||
}) |
||||
}, |
||||
{ immediate: true }, |
||||
) |
||||
</script> |
||||
|
||||
<template> |
||||
<Card title="销售统计" :loading="loading"> |
||||
<div ref="chartRef" :style="{ width, height }" /> |
||||
</Card> |
||||
</template> |
@ -1,36 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { computed } from 'vue' |
||||
import { Avatar } from 'ant-design-vue' |
||||
import { useUserStore } from '@/store/modules/user' |
||||
import headerImg from '@/assets/images/header.jpg' |
||||
|
||||
const userStore = useUserStore() |
||||
const userinfo = computed(() => userStore.getUserInfo) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="lg:flex"> |
||||
<Avatar :src="userinfo.user.avatar || headerImg" :size="72" class="!mx-auto !block" /> |
||||
<div class="mt-2 flex flex-col justify-center md:ml-6 md:mt-0"> |
||||
<h1 class="text-md md:text-lg"> |
||||
早安, {{ userinfo.user.real_name }}, 开始您一天的工作吧! |
||||
</h1> |
||||
<span class="text-secondary"> 今日晴,20℃ - 32℃! </span> |
||||
</div> |
||||
<div class="mt-4 flex flex-1 justify-end md:mt-0"> |
||||
<div class="flex flex-col justify-center text-right"> |
||||
<span class="text-secondary"> 待办 </span> |
||||
<span class="text-2xl">2/10</span> |
||||
</div> |
||||
|
||||
<div class="mx-12 flex flex-col justify-center text-right md:mx-16"> |
||||
<span class="text-secondary"> 项目 </span> |
||||
<span class="text-2xl">8</span> |
||||
</div> |
||||
<div class="mr-4 flex flex-col justify-center text-right md:mr-10"> |
||||
<span class="text-secondary"> 团队 </span> |
||||
<span class="text-2xl">300</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
@ -1,57 +0,0 @@
|
||||
interface DynamicInfoItem { |
||||
avatar: string |
||||
name: string |
||||
date: string |
||||
desc: string |
||||
} |
||||
|
||||
export const dynamicInfoItems: DynamicInfoItem[] = [ |
||||
{ |
||||
avatar: 'dynamic-avatar-1', |
||||
name: '威廉', |
||||
date: '刚刚', |
||||
desc: '在 <a>开源组</a> 创建了项目 <a>Vue</a>', |
||||
}, |
||||
{ |
||||
avatar: 'dynamic-avatar-2', |
||||
name: '艾文', |
||||
date: '1个小时前', |
||||
desc: '关注了 <a>威廉</a> ', |
||||
}, |
||||
{ |
||||
avatar: 'dynamic-avatar-3', |
||||
name: '克里斯', |
||||
date: '1天前', |
||||
desc: '发布了 <a>个人动态</a> ', |
||||
}, |
||||
{ |
||||
avatar: 'dynamic-avatar-4', |
||||
name: 'Kay', |
||||
date: '2天前', |
||||
desc: '发表文章 <a>如何编写一个Vite插件</a> ', |
||||
}, |
||||
{ |
||||
avatar: 'dynamic-avatar-5', |
||||
name: '皮特', |
||||
date: '3天前', |
||||
desc: '回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>', |
||||
}, |
||||
{ |
||||
avatar: 'dynamic-avatar-6', |
||||
name: '杰克', |
||||
date: '1周前', |
||||
desc: '关闭了问题 <a>如何运行项目</a> ', |
||||
}, |
||||
{ |
||||
avatar: 'dynamic-avatar-1', |
||||
name: '威廉', |
||||
date: '1周前', |
||||
desc: '发布了 <a>个人动态</a> ', |
||||
}, |
||||
{ |
||||
avatar: 'dynamic-avatar-1', |
||||
name: '威廉', |
||||
date: '2021-04-01 20:00', |
||||
desc: '推送了代码到 <a>Github</a>', |
||||
}, |
||||
] |
@ -1,39 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { ref } from 'vue' |
||||
import { Card } from 'ant-design-vue' |
||||
import WorkbenchHeader from './components/WorkbenchHeader.vue' |
||||
import ProjectCard from './components/ProjectCard.vue' |
||||
import QuickNav from './components/QuickNav.vue' |
||||
import DynamicInfo from './components/DynamicInfo.vue' |
||||
import SaleRadar from './components/SaleRadar.vue' |
||||
import { PageWrapper } from '@/components/Page' |
||||
|
||||
const loading = ref(true) |
||||
|
||||
setTimeout(() => { |
||||
loading.value = false |
||||
}, 500) |
||||
</script> |
||||
|
||||
<template> |
||||
<PageWrapper> |
||||
<template #headerContent> |
||||
<WorkbenchHeader /> |
||||
</template> |
||||
<div class="lg:flex"> |
||||
<div class="enter-y w-full !mr-4 lg:w-7/10"> |
||||
<ProjectCard :loading="loading" class="enter-y" /> |
||||
<DynamicInfo :loading="loading" class="enter-y !my-4" /> |
||||
</div> |
||||
<div class="enter-y w-full lg:w-3/10"> |
||||
<QuickNav :loading="loading" class="enter-y" /> |
||||
|
||||
<Card class="enter-y !my-4" :loading="loading"> |
||||
<img class="mx-auto h-30 xl:h-50" src="@/assets/svg/illustration.svg"> |
||||
</Card> |
||||
|
||||
<SaleRadar :loading="loading" class="enter-y" /> |
||||
</div> |
||||
</div> |
||||
</PageWrapper> |
||||
</template> |
Loading…
Reference in new issue