Commit 78d0eebbc60c2a0cd5f94309064bfae0052c0873

Authored by xp.Huang
2 parents f2aa60eb a5926ee9

Merge branch 'f-dev' into 'main'

feat:新增Cron组件 wip:报表功能开发中......

See merge request huang/yun-teng-iot-front!272
Showing 47 changed files with 2722 additions and 175 deletions

Too many changes to show.

To preserve performance only 47 of 48 files are displayed.

... ... @@ -2,21 +2,68 @@
2 2 * Introduces component library styles on demand.
3 3 * https://github.com/anncwb/vite-plugin-style-import
4 4 */
  5 + import styleImport from 'vite-plugin-style-import';
5 6
6   -import styleImport from 'vite-plugin-style-import';
7   -
8   -export function configStyleImportPlugin(isBuild: boolean) {
9   - if (!isBuild) return [];
10   - const styleImportPlugin = styleImport({
11   - libs: [
12   - {
13   - libraryName: 'ant-design-vue',
14   - esModule: true,
15   - resolveStyle: (name) => {
16   - return `ant-design-vue/es/${name}/style/index`;
17   - },
18   - },
19   - ],
20   - });
21   - return styleImportPlugin;
22   -}
  7 + export function configStyleImportPlugin(isBuild: boolean) {
  8 + if (!isBuild) {
  9 + return [];
  10 + }
  11 + const styleImportPlugin = styleImport({
  12 + libs: [
  13 + {
  14 + libraryName: 'ant-design-vue',
  15 + esModule: true,
  16 + resolveStyle: (name) => {
  17 + // 这里是“子组件”列表,无需额外引入样式文件
  18 + const ignoreList = [
  19 + 'typography-text',
  20 + 'typography-title',
  21 + 'typography-paragraph',
  22 + 'typography-link',
  23 + 'anchor-link',
  24 + 'sub-menu',
  25 + 'menu-item',
  26 + 'menu-item-group',
  27 + 'dropdown-button',
  28 + 'breadcrumb-item',
  29 + 'breadcrumb-separator',
  30 + 'input-password',
  31 + 'input-search',
  32 + 'input-group',
  33 + 'form-item',
  34 + 'radio-group',
  35 + 'checkbox-group',
  36 + 'layout-sider',
  37 + 'layout-content',
  38 + 'layout-footer',
  39 + 'layout-header',
  40 + 'step',
  41 + 'select-option',
  42 + 'select-opt-group',
  43 + 'card-grid',
  44 + 'card-meta',
  45 + 'collapse-panel',
  46 + 'descriptions-item',
  47 + 'list-item',
  48 + 'list-item-meta',
  49 + 'table-column',
  50 + 'table-column-group',
  51 + 'tab-pane',
  52 + 'tab-content',
  53 + 'timeline-item',
  54 + 'tree-node',
  55 + 'skeleton-input',
  56 + 'skeleton-avatar',
  57 + 'skeleton-title',
  58 + 'skeleton-paragraph',
  59 + 'skeleton-image',
  60 + 'skeleton-button',
  61 + ];
  62 + return ignoreList.includes(name) ? '' : `ant-design-vue/es/${name}/style/index`;
  63 + },
  64 + },
  65 + ],
  66 + });
  67 + return styleImportPlugin;
  68 + }
  69 +
\ No newline at end of file
... ...
... ... @@ -43,6 +43,7 @@
43 43 "ant-design-vue": "2.2.6",
44 44 "axios": "^0.21.1",
45 45 "codemirror": "^5.62.2",
  46 + "cron-parser": "3.5.0",
46 47 "cropperjs": "^1.5.12",
47 48 "crypto-js": "^4.1.1",
48 49 "echarts": "^5.1.2",
... ...
  1 +import { BasicPageParams } from '/@/api/model/baseModel';
  2 +export type ReportQueryParam = BasicPageParams & SchedueParam;
  3 +
  4 +export type SchedueParam = {
  5 + status: true;
  6 + jobName: string;
  7 + jobGroup: string;
  8 + orderFiled: string;
  9 + orderType: string;
  10 + data?: any;
  11 + code?: number;
  12 +};
  13 +
  14 +export interface ReportModel {
  15 + code?: number;
  16 + concurrent: number;
  17 + createTime: string;
  18 + creator: string;
  19 + cronExpression: string;
  20 + defaultConfig: string;
  21 + description: string;
  22 + enabled: true;
  23 + icon: string;
  24 + id: string;
  25 + invokeTarget: string;
  26 + jobGroup: string;
  27 + jobName: string;
  28 + misfirePolicy: number;
  29 + name: string;
  30 + roleIds: [string];
  31 + status: number;
  32 + tenantExpireTime: string;
  33 + tenantId: string;
  34 + tenantProfileId: string;
  35 + tenantStatus: string;
  36 + updateTime: string;
  37 + updater: string;
  38 +}
... ...
  1 +import { defHttp } from '/@/utils/http/axios';
  2 +import { ReportModel, ReportQueryParam } from './model/schedueModel';
  3 +
  4 +enum ReportManagerApi {
  5 + GET_REPORT_API = '/monitor/job/page',
  6 + POST_REPORT_API = '/monitor/job/add',
  7 + DELETE_REPORT_API = '/monitor/job',
  8 + PUT_REPORT_API = '/monitor/job/update',
  9 + PUTID_REPORT_API = '/monitor/job',
  10 + RUN_REPORT_API = '/monitor/job/run',
  11 + JOB_LOG_DETAIL_API = '/monitor/jobLog/get/',
  12 + JOB_LOG_PAGE_API = '/monitor/jobLog/page',
  13 + DELETE_LOG_API = '/monitor/jobLog',
  14 + POST_LOG_CLEAN_API = '/monitor/jobLog/clean',
  15 +}
  16 +
  17 +//分页
  18 +export const scheduePage = (params: ReportQueryParam) => {
  19 + return defHttp.get<ReportQueryParam>({
  20 + url: ReportManagerApi.GET_REPORT_API,
  21 + params,
  22 + });
  23 +};
  24 +
  25 +//删除
  26 +export const deleteSchedueManage = (ids: string[]) => {
  27 + return defHttp.delete({
  28 + url: ReportManagerApi.DELETE_REPORT_API,
  29 + data: {
  30 + ids: ids,
  31 + },
  32 + });
  33 +};
  34 +
  35 +// 创建
  36 +export const createOrEditSchedueManage = (data) => {
  37 + return defHttp.post<ReportModel>({
  38 + url: ReportManagerApi.POST_REPORT_API,
  39 + data,
  40 + });
  41 +};
  42 +
  43 +// 编辑
  44 +export const putSchedueConfigManage = (data) => {
  45 + return defHttp.post<ReportModel>({
  46 + url: ReportManagerApi.PUT_REPORT_API,
  47 + data,
  48 + });
  49 +};
  50 +
  51 +// 修改状态 id status
  52 +export const putSchedueByidAndStatusManage = (id, status) => {
  53 + return defHttp.put<ReportModel>({
  54 + url: ReportManagerApi.PUTID_REPORT_API + '/changeStatus/' + id + '/' + status,
  55 + });
  56 +};
  57 +//执行一次
  58 +export const postRunSchedueConfigManage = (id) => {
  59 + return defHttp.post<ReportModel>({
  60 + url: ReportManagerApi.RUN_REPORT_API+'/'+id,
  61 + });
  62 +};
  63 +
  64 +/**
  65 + * 调度日志
  66 + */
  67 +
  68 +//分页
  69 +export const schedueLogPage = (params: ReportQueryParam) => {
  70 + return defHttp.get<ReportQueryParam>({
  71 + url: ReportManagerApi.JOB_LOG_PAGE_API,
  72 + params,
  73 + });
  74 +};
  75 +
  76 +//删除
  77 +export const deleteSchedueLogManage = (ids: string[]) => {
  78 + return defHttp.delete({
  79 + url: ReportManagerApi.DELETE_LOG_API,
  80 + data: {
  81 + ids: ids,
  82 + },
  83 + });
  84 +};
  85 +
  86 +// 清空
  87 +export const schedueLogCleanPage = () => {
  88 + return defHttp.post<ReportModel>({
  89 + url: ReportManagerApi.POST_LOG_CLEAN_API,
  90 + });
  91 +};
  92 +
  93 +//详情
  94 +export const schedueLogDetailPage = (jobId, id) => {
  95 + return defHttp.get<ReportQueryParam>({
  96 + url: ReportManagerApi.JOB_LOG_DETAIL_API + '/' + jobId + '/' + id,
  97 + });
  98 +};
... ...
... ... @@ -11,5 +11,12 @@ export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.v
11 11 export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue';
12 12 export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue';
13 13
14   -
  14 +//注册自定义组件
  15 +export {
  16 + JEasyCron,
  17 + JEasyCronInner,
  18 + JEasyCronModal,
  19 +} from '/@/components/Form/src/jeecg/components/JEasyCron';
  20 +// Jeecg自定义校验
  21 +export { JCronValidator } from '/@/components/Form/src/jeecg/components/JEasyCron';
15 22 export { BasicForm };
... ...
... ... @@ -30,6 +30,7 @@ import { CountdownInput } from '/@/components/CountDown';
30 30 import ApiRadioGroup from './components/ApiRadioGroup.vue';
31 31 //自定义组件
32 32 import JAddInput from './jeecg/components/JAddInput.vue';
  33 +import { JEasyCron } from './jeecg/components/JEasyCron';
33 34
34 35 const componentMap = new Map<ComponentType, Component>();
35 36
... ... @@ -67,6 +68,7 @@ componentMap.set('InputCountDown', CountdownInput);
67 68 componentMap.set('Upload', BasicUpload);
68 69 //注册自定义组件
69 70 componentMap.set('JAddInput', JAddInput);
  71 +componentMap.set('JEasyCron', JEasyCron);
70 72
71 73 export function add(compName: ComponentType, component: Component) {
72 74 componentMap.set(compName, component);
... ...
  1 +<template>
  2 + <div :class="`${prefixCls}`">
  3 + <div class="content">
  4 + <Tabs :size="`small`" v-model:activeKey="activeKey">
  5 + <TabPane tab="秒" key="second" v-if="!hideSecond">
  6 + <SecondUI v-model:value="second" :disabled="disabled" />
  7 + </TabPane>
  8 + <TabPane tab="分" key="minute">
  9 + <MinuteUI v-model:value="minute" :disabled="disabled" />
  10 + </TabPane>
  11 + <TabPane tab="时" key="hour">
  12 + <HourUI v-model:value="hour" :disabled="disabled" />
  13 + </TabPane>
  14 + <TabPane tab="日" key="day">
  15 + <DayUI v-model:value="day" :week="week" :disabled="disabled" />
  16 + </TabPane>
  17 + <TabPane tab="月" key="month">
  18 + <MonthUI v-model:value="month" :disabled="disabled" />
  19 + </TabPane>
  20 + <TabPane tab="周" key="week">
  21 + <WeekUI v-model:value="week" :day="day" :disabled="disabled" />
  22 + </TabPane>
  23 + <TabPane tab="年" key="year" v-if="!hideYear && !hideSecond">
  24 + <YearUI v-model:value="year" :disabled="disabled" />
  25 + </TabPane>
  26 + </Tabs>
  27 + <Divider />
  28 + <!-- 执行时间预览 -->
  29 + <Row :gutter="8">
  30 + <Col :span="18" style="margin-top: 22px">
  31 + <Row :gutter="8">
  32 + <Col :span="8" style="margin-bottom: 12px">
  33 + <Input v-model:value="inputValues.second" @blur="onInputBlur">
  34 + <template #addonBefore>
  35 + <span class="allow-click" @click="activeKey = 'second'">秒</span>
  36 + </template>
  37 + </Input>
  38 + </Col>
  39 + <Col :span="8" style="margin-bottom: 12px">
  40 + <Input v-model:value="inputValues.minute" @blur="onInputBlur">
  41 + <template #addonBefore>
  42 + <span class="allow-click" @click="activeKey = 'minute'">分</span>
  43 + </template>
  44 + </Input>
  45 + </Col>
  46 + <Col :span="8" style="margin-bottom: 12px">
  47 + <Input v-model:value="inputValues.hour" @blur="onInputBlur">
  48 + <template #addonBefore>
  49 + <span class="allow-click" @click="activeKey = 'hour'">时</span>
  50 + </template>
  51 + </Input>
  52 + </Col>
  53 + <Col :span="8" style="margin-bottom: 12px">
  54 + <Input v-model:value="inputValues.day" @blur="onInputBlur">
  55 + <template #addonBefore>
  56 + <span class="allow-click" @click="activeKey = 'day'">日</span>
  57 + </template>
  58 + </Input>
  59 + </Col>
  60 + <Col :span="8" style="margin-bottom: 12px">
  61 + <Input v-model:value="inputValues.month" @blur="onInputBlur">
  62 + <template #addonBefore>
  63 + <span class="allow-click" @click="activeKey = 'month'">月</span>
  64 + </template>
  65 + </Input>
  66 + </Col>
  67 + <Col :span="8" style="margin-bottom: 12px">
  68 + <Input v-model:value="inputValues.week" @blur="onInputBlur">
  69 + <template #addonBefore>
  70 + <span class="allow-click" @click="activeKey = 'week'">周</span>
  71 + </template>
  72 + </Input>
  73 + </Col>
  74 + <Col :span="8">
  75 + <Input v-model:value="inputValues.year" @blur="onInputBlur">
  76 + <template #addonBefore>
  77 + <span class="allow-click" @click="activeKey = 'year'">年</span>
  78 + </template>
  79 + </Input>
  80 + </Col>
  81 + <Col :span="16">
  82 + <Input v-model:value="inputValues.cron" @blur="onInputCronBlur">
  83 + <template #addonBefore>
  84 + <Tooltip title="Cron表达式">式</Tooltip>
  85 + </template>
  86 + </Input>
  87 + </Col>
  88 + </Row>
  89 + </Col>
  90 + <Col :span="6">
  91 + <div>近十次执行时间(不含年)</div>
  92 + <a-textarea type="textarea" :value="preTimeList" :rows="5" />
  93 + </Col>
  94 + </Row>
  95 + </div>
  96 + </div>
  97 +</template>
  98 +
  99 +<script lang="ts" setup>
  100 + import { computed, reactive, ref, watch, provide } from 'vue';
  101 + import { useDesign } from '/@/hooks/web/useDesign';
  102 + import CronParser from 'cron-parser';
  103 + import SecondUI from './tabs/SecondUI.vue';
  104 + import MinuteUI from './tabs/MinuteUI.vue';
  105 + import HourUI from './tabs/HourUI.vue';
  106 + import DayUI from './tabs/DayUI.vue';
  107 + import MonthUI from './tabs/MonthUI.vue';
  108 + import WeekUI from './tabs/WeekUI.vue';
  109 + import YearUI from './tabs/YearUI.vue';
  110 + import { cronEmits, cronProps } from './easy.cron.data';
  111 + import { dateFormat, simpleDebounce } from '/@/utils/common/compUtils';
  112 + import { Row, Col, Input, Tabs, Divider, Tooltip } from 'ant-design-vue';
  113 + const { TabPane } = Tabs;
  114 + const { prefixCls } = useDesign('easy-cron-inner');
  115 + provide('prefixCls', prefixCls);
  116 + const emit = defineEmits([...cronEmits]);
  117 + const props = defineProps({ ...cronProps });
  118 + const activeKey = ref(props.hideSecond ? 'minute' : 'second');
  119 + const second = ref('*');
  120 + const minute = ref('*');
  121 + const hour = ref('*');
  122 + const day = ref('*');
  123 + const month = ref('*');
  124 + const week = ref('?');
  125 + const year = ref('*');
  126 + const inputValues = reactive({
  127 + second: '',
  128 + minute: '',
  129 + hour: '',
  130 + day: '',
  131 + month: '',
  132 + week: '',
  133 + year: '',
  134 + cron: '',
  135 + });
  136 + const preTimeList = ref('执行预览,会忽略年份参数。');
  137 +
  138 + //fix 去除字符串里的'W' 当选择工作日 会自动在后面加上W 比如1W 2W corn解析会报错
  139 + function Letter(str) {
  140 + let result;
  141 + let reg = /[W]+/;
  142 + while ((result = str.match(reg))) {
  143 + //判断str.match(reg)是否没有字母了
  144 + str = str.replace(result[0], ''); //替换掉字母 result[0] 是 str.match(reg)匹配到的字母
  145 + }
  146 + return str;
  147 + }
  148 + // 最终生成的cron表达式
  149 + const cronValueInner = computed(() => {
  150 + let result: string[] = [];
  151 + if (!props.hideSecond) {
  152 + result.push(second.value ? second.value : '*');
  153 + }
  154 + result.push(minute.value ? minute.value : '*');
  155 + result.push(hour.value ? hour.value : '*');
  156 + result.push(day.value ? Letter(day.value) : '*');
  157 + result.push(month.value ? month.value : '*');
  158 + result.push(week.value ? week.value : '?');
  159 + if (!props.hideYear && !props.hideSecond) result.push(year.value ? year.value : '*');
  160 + return result.join(' ');
  161 + });
  162 + // 不含年
  163 + const cronValueNoYear = computed(() => {
  164 + const v = cronValueInner.value;
  165 + if (props.hideYear || props.hideSecond) return v;
  166 + const vs = v.split(' ');
  167 + if (vs.length >= 6) {
  168 + // 转成 Quartz 的规则
  169 + vs[5] = convertWeekToQuartz(vs[5]);
  170 + }
  171 + return vs.slice(0, vs.length - 1).join(' ');
  172 + });
  173 + const calTriggerList = simpleDebounce(calTriggerListInner, 500);
  174 +
  175 + watch(
  176 + () => props.value,
  177 + (newVal) => {
  178 + if (newVal === cronValueInner.value) {
  179 + return;
  180 + }
  181 + formatValue();
  182 + }
  183 + );
  184 +
  185 + watch(cronValueInner, (newValue) => {
  186 + calTriggerList();
  187 + emitValue(newValue);
  188 + assignInput();
  189 + });
  190 + assignInput();
  191 + formatValue();
  192 + calTriggerListInner();
  193 +
  194 + function assignInput() {
  195 + inputValues.second = second.value;
  196 + inputValues.minute = minute.value;
  197 + inputValues.hour = hour.value;
  198 + inputValues.day = Letter(day.value);
  199 + inputValues.month = month.value;
  200 + inputValues.week = week.value;
  201 + inputValues.year = year.value;
  202 + inputValues.cron = cronValueInner.value;
  203 + }
  204 +
  205 + function formatValue() {
  206 + if (!props.value) return;
  207 + const values = props.value.split(' ').filter((item) => !!item);
  208 + if (!values || values.length <= 0) return;
  209 + let i = 0;
  210 + if (!props.hideSecond) second.value = values[i++];
  211 + if (values.length > i) minute.value = values[i++];
  212 + if (values.length > i) hour.value = values[i++];
  213 + if (values.length > i) day.value = values[i++];
  214 + if (values.length > i) month.value = values[i++];
  215 + if (values.length > i) week.value = values[i++];
  216 + if (values.length > i) year.value = values[i];
  217 + assignInput();
  218 + }
  219 +
  220 + // Quartz 的规则:
  221 + // 1 = 周日,2 = 周一,3 = 周二,4 = 周三,5 = 周四,6 = 周五,7 = 周六
  222 + function convertWeekToQuartz(week: string) {
  223 + let convert = (v: string) => {
  224 + if (v === '0') {
  225 + return '1';
  226 + }
  227 + if (v === '1') {
  228 + return '0';
  229 + }
  230 + return (Number.parseInt(v) - 1).toString();
  231 + };
  232 + // 匹配示例 1-7 or 1/7
  233 + let patten1 = /^([0-7])([-/])([0-7])$/;
  234 + // 匹配示例 1,4,7
  235 + let patten2 = /^([0-7])(,[0-7])+$/;
  236 + if (/^[0-7]$/.test(week)) {
  237 + return convert(week);
  238 + } else if (patten1.test(week)) {
  239 + return week.replace(patten1, ($0, before, separator, after) => {
  240 + if (separator === '/') {
  241 + return convert(before) + separator + after;
  242 + } else {
  243 + return convert(before) + separator + convert(after);
  244 + }
  245 + });
  246 + } else if (patten2.test(week)) {
  247 + return week
  248 + .split(',')
  249 + .map((v) => convert(v))
  250 + .join(',');
  251 + }
  252 + return week;
  253 + }
  254 +
  255 + function calTriggerListInner() {
  256 + // 设置了回调函数
  257 + if (props.remote) {
  258 + props.remote(cronValueInner.value, +new Date(), (v) => {
  259 + preTimeList.value = v;
  260 + });
  261 + return;
  262 + }
  263 + const format = 'yyyy-MM-dd hh:mm:ss';
  264 + const options = {
  265 + currentDate: dateFormat(new Date(), format),
  266 + };
  267 + const iter = CronParser.parseExpression(cronValueNoYear.value, options);
  268 + const result: string[] = [];
  269 + for (let i = 1; i <= 10; i++) {
  270 + result.push(dateFormat(new Date(iter.next() as any), format));
  271 + }
  272 + preTimeList.value = result.length > 0 ? result.join('\n') : '无执行时间';
  273 + }
  274 +
  275 + function onInputBlur() {
  276 + second.value = inputValues.second;
  277 + minute.value = inputValues.minute;
  278 + hour.value = inputValues.hour;
  279 + day.value = inputValues.day;
  280 + month.value = inputValues.month;
  281 + week.value = inputValues.week;
  282 + year.value = inputValues.year;
  283 + }
  284 +
  285 + function onInputCronBlur(event) {
  286 + emitValue(event.target.value);
  287 + }
  288 +
  289 + function emitValue(value) {
  290 + emit('change', value);
  291 + emit('update:value', value);
  292 + }
  293 +</script>
  294 +<style lang="less">
  295 + @import './easy.cron.inner.less';
  296 +</style>
... ...
  1 +<template>
  2 + <div :class="`${prefixCls}`">
  3 + <a-input :placeholder="placeholder" v-model:value="editCronValue" :disabled="disabled">
  4 + <template #addonAfter>
  5 + <a class="open-btn" :disabled="disabled ? 'disabled' : null" @click="showConfigModal">
  6 + <Icon icon="ant-design:setting-outlined" />
  7 + <span>选择</span>
  8 + </a>
  9 + </template>
  10 + </a-input>
  11 + <EasyCronModal
  12 + @register="registerModal"
  13 + v-model:value="editCronValue"
  14 + :exeStartTime="exeStartTime"
  15 + :hideYear="hideYear"
  16 + :remote="remote"
  17 + :hideSecond="hideSecond"
  18 + />
  19 + </div>
  20 +</template>
  21 +
  22 +<script lang="ts" setup>
  23 + import { ref, watch } from 'vue';
  24 + import { useDesign } from '/@/hooks/web/useDesign';
  25 + import { useModal } from '/@/components/Modal';
  26 + import { propTypes } from '/@/utils/propTypes';
  27 + import Icon from '/@/components/Icon/src/Icon.vue';
  28 + import EasyCronModal from './EasyCronModal.vue';
  29 + import { cronEmits, cronProps } from './easy.cron.data';
  30 +
  31 + const { prefixCls } = useDesign('easy-cron-input');
  32 + const emit = defineEmits([...cronEmits]);
  33 + const props = defineProps({
  34 + ...cronProps,
  35 + placeholder: propTypes.string.def('请输入cron表达式'),
  36 + exeStartTime: propTypes
  37 + .oneOfType([propTypes.number, propTypes.string, propTypes.object])
  38 + .def(0),
  39 + });
  40 + const [registerModal, { openModal }] = useModal();
  41 + const editCronValue = ref(props.value);
  42 +
  43 + watch(
  44 + () => props.value,
  45 + (newVal) => {
  46 + if (newVal !== editCronValue.value) {
  47 + editCronValue.value = newVal;
  48 + }
  49 + }
  50 + );
  51 + watch(editCronValue, (newVal) => {
  52 + emit('change', newVal);
  53 + emit('update:value', newVal);
  54 + });
  55 +
  56 + function showConfigModal() {
  57 + if (!props.disabled) {
  58 + openModal();
  59 + }
  60 + }
  61 +</script>
  62 +
  63 +<style lang="less">
  64 + @import './easy.cron.input.less';
  65 +</style>
... ...
  1 +<template>
  2 + <BasicModal
  3 + @register="registerModal"
  4 + title="Cron表达式"
  5 + width="800px"
  6 + @cancel="onCancel"
  7 + :footer="null"
  8 + >
  9 + <EasyCron v-bind="attrs" />
  10 + <div style="float: right; margin-top: 2vh">
  11 + <Button type="default" @click="onCancel" class="mr-2">取消</Button>
  12 + <Button type="primary" @click="onOk" class="mr-2">确认</Button>
  13 + </div>
  14 + </BasicModal>
  15 +</template>
  16 +
  17 +<script lang="ts">
  18 + import { defineComponent } from 'vue';
  19 + import { useAttrs } from '/@/hooks/core/useAttrs';
  20 + import { BasicModal, useModalInner } from '/@/components/Modal';
  21 + import EasyCron from './EasyCronInner.vue';
  22 + import { Button } from '/@/components/Button';
  23 +
  24 + export default defineComponent({
  25 + name: 'EasyCronModal',
  26 + inheritAttrs: false,
  27 + components: { BasicModal, EasyCron, Button },
  28 + setup() {
  29 + const attrs = useAttrs();
  30 + const [registerModal, { closeModal }] = useModalInner();
  31 +
  32 + function onOk() {
  33 + setTimeout(() => {
  34 + closeModal();
  35 + }, 500);
  36 + }
  37 + function onCancel() {
  38 + setTimeout(() => {
  39 + closeModal();
  40 + }, 500);
  41 + }
  42 +
  43 + return { attrs, registerModal, onOk, onCancel };
  44 + },
  45 + });
  46 +</script>
... ...
  1 +MIT License
  2 +
  3 +Copyright (c) 2019 知行合一
  4 +
  5 +Permission is hereby granted, free of charge, to any person obtaining a copy
  6 +of this software and associated documentation files (the "Software"), to deal
  7 +in the Software without restriction, including without limitation the rights
  8 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9 +copies of the Software, and to permit persons to whom the Software is
  10 +furnished to do so, subject to the following conditions:
  11 +
  12 +The above copyright notice and this permission notice shall be included in all
  13 +copies or substantial portions of the Software.
  14 +
  15 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16 +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18 +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19 +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20 +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21 +SOFTWARE.
... ...
  1 +import { propTypes } from '/@/utils/propTypes';
  2 +
  3 +export const cronEmits = ['change', 'update:value'];
  4 +export const cronProps = {
  5 + value: propTypes.string.def(''),
  6 + disabled: propTypes.bool.def(false),
  7 + hideSecond: propTypes.bool.def(false),
  8 + hideYear: propTypes.bool.def(false),
  9 + remote: propTypes.func,
  10 +};
... ...
  1 +//noinspection LessUnresolvedVariable
  2 +@prefix-cls: ~'@{namespace}-easy-cron-inner';
  3 +
  4 +.@{prefix-cls} {
  5 + .content {
  6 + .ant-checkbox-wrapper + .ant-checkbox-wrapper {
  7 + margin-left: 0;
  8 + }
  9 + }
  10 +
  11 + &-config-list {
  12 + text-align: left;
  13 + margin: 0 10px 10px 10px;
  14 +
  15 + .item {
  16 + margin-top: 5px;
  17 + }
  18 +
  19 + .choice {
  20 + padding: 5px 8px;
  21 + }
  22 +
  23 + .w60 {
  24 + width: 60px;
  25 + min-width: 60px;
  26 + }
  27 +
  28 + .w80 {
  29 + width: 80px;
  30 + min-width: 80px;
  31 + }
  32 +
  33 + .list {
  34 + margin: 0 20px;
  35 + }
  36 +
  37 + .list-check-item {
  38 + padding: 1px 3px;
  39 + width: 4em;
  40 + }
  41 +
  42 + .list-cn .list-check-item {
  43 + width: 5em;
  44 + }
  45 +
  46 + .tip-info {
  47 + color: #999;
  48 + }
  49 + }
  50 +
  51 + .allow-click {
  52 + cursor: pointer;
  53 + }
  54 +}
... ...
  1 +//noinspection LessUnresolvedVariable
  2 +@prefix-cls: ~'@{namespace}-easy-cron-input';
  3 +
  4 +.@{prefix-cls} {
  5 + a.open-btn {
  6 + cursor: pointer;
  7 +
  8 + .app-iconify {
  9 + position: relative;
  10 + top: 1px;
  11 + right: 2px;
  12 + }
  13 + }
  14 +}
... ...
  1 +// 原开源项目地址:https://gitee.com/toktok/easy-cron
  2 +
  3 +export { default as JEasyCron } from './EasyCronInput.vue';
  4 +export { default as JEasyCronInner } from './EasyCronInner.vue';
  5 +export { default as JEasyCronModal } from './EasyCronModal.vue';
  6 +export { default as JCronValidator } from './validator';
... ...
  1 +<template>
  2 + <div :class="`${prefixCls}-config-list`">
  3 + <a-radio-group v-model:value="type">
  4 + <div class="item">
  5 + <a-radio :value="TypeEnum.unset" v-bind="beforeRadioAttrs">不设置</a-radio>
  6 + <span class="tip-info">日和周只能设置其中之一</span>
  7 + </div>
  8 + <div class="item">
  9 + <a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每日</a-radio>
  10 + </div>
  11 + <div class="item">
  12 + <a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
  13 + <span> 从 </span>
  14 + <InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
  15 + <span> 日 至 </span>
  16 + <InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
  17 + <span> 日 </span>
  18 + </div>
  19 + <div class="item">
  20 + <a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
  21 + <span> 从 </span>
  22 + <InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
  23 + <span> 日开始,间隔 </span>
  24 + <InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
  25 + <span> 日 </span>
  26 + </div>
  27 + <div class="item">
  28 + <a-radio :value="TypeEnum.work" v-bind="beforeRadioAttrs">工作日</a-radio>
  29 + <span> 本月 </span>
  30 + <InputNumber v-model:value="valueWork" v-bind="typeWorkAttrs" />
  31 + <span> 日,最近的工作日 </span>
  32 + </div>
  33 + <div class="item">
  34 + <a-radio :value="TypeEnum.last" v-bind="beforeRadioAttrs">最后一日</a-radio>
  35 + </div>
  36 + <div class="item">
  37 + <a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
  38 + <div class="list">
  39 + <a-checkbox-group v-model:value="valueList">
  40 + <template v-for="i in specifyRange" :key="i">
  41 + <a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
  42 + </template>
  43 + </a-checkbox-group>
  44 + </div>
  45 + </div>
  46 + </a-radio-group>
  47 + </div>
  48 +</template>
  49 +
  50 +<script lang="ts">
  51 + import { computed, defineComponent, watch } from 'vue';
  52 + import { InputNumber } from 'ant-design-vue';
  53 + import { TypeEnum, useTabEmits, useTabProps, useTabSetup } from './useTabMixin';
  54 +
  55 + export default defineComponent({
  56 + name: 'DayUI',
  57 + components: { InputNumber },
  58 + props: useTabProps({
  59 + defaultValue: '*',
  60 + props: {
  61 + week: { type: String, default: '?' },
  62 + },
  63 + }),
  64 + emits: useTabEmits(),
  65 + setup(props, context) {
  66 + const disabledChoice = computed(() => {
  67 + return (props.week && props.week !== '?') || props.disabled;
  68 + });
  69 + const setup = useTabSetup(props, context, {
  70 + defaultValue: '*',
  71 + valueWork: 1,
  72 + minValue: 1,
  73 + maxValue: 31,
  74 + valueRange: { start: 1, end: 31 },
  75 + valueLoop: { start: 1, interval: 1 },
  76 + disabled: disabledChoice,
  77 + });
  78 + const typeWorkAttrs = computed(() => ({
  79 + disabled: setup.type.value !== TypeEnum.work || props.disabled || disabledChoice.value,
  80 + ...setup.inputNumberAttrs.value,
  81 + }));
  82 +
  83 + watch(
  84 + () => props.week,
  85 + () => {
  86 + setup.updateValue(disabledChoice.value ? '?' : setup.computeValue.value);
  87 + }
  88 + );
  89 +
  90 + return { ...setup, typeWorkAttrs };
  91 + },
  92 + });
  93 +</script>
... ...
  1 +<template>
  2 + <div :class="`${prefixCls}-config-list`">
  3 + <a-radio-group v-model:value="type">
  4 + <div class="item">
  5 + <a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每时</a-radio>
  6 + </div>
  7 + <div class="item">
  8 + <a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
  9 + <span> 从 </span>
  10 + <InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
  11 + <span> 时 至 </span>
  12 + <InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
  13 + <span> 时 </span>
  14 + </div>
  15 + <div class="item">
  16 + <a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
  17 + <span> 从 </span>
  18 + <InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
  19 + <span> 时开始,间隔 </span>
  20 + <InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
  21 + <span> 时 </span>
  22 + </div>
  23 + <div class="item">
  24 + <a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
  25 + <div class="list">
  26 + <a-checkbox-group v-model:value="valueList">
  27 + <template v-for="i in specifyRange" :key="i">
  28 + <a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
  29 + </template>
  30 + </a-checkbox-group>
  31 + </div>
  32 + </div>
  33 + </a-radio-group>
  34 + </div>
  35 +</template>
  36 +
  37 +<script lang="ts">
  38 + import { defineComponent } from 'vue';
  39 + import { InputNumber } from 'ant-design-vue';
  40 + import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin';
  41 +
  42 + export default defineComponent({
  43 + name: 'HourUI',
  44 + components: { InputNumber },
  45 + props: useTabProps({
  46 + defaultValue: '*',
  47 + }),
  48 + emits: useTabEmits(),
  49 + setup(props, context) {
  50 + return useTabSetup(props, context, {
  51 + defaultValue: '*',
  52 + minValue: 0,
  53 + maxValue: 23,
  54 + valueRange: { start: 0, end: 23 },
  55 + valueLoop: { start: 0, interval: 1 },
  56 + });
  57 + },
  58 + });
  59 +</script>
... ...
  1 +<template>
  2 + <div :class="`${prefixCls}-config-list`">
  3 + <a-radio-group v-model:value="type">
  4 + <div class="item">
  5 + <a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每分</a-radio>
  6 + </div>
  7 + <div class="item">
  8 + <a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
  9 + <span> 从 </span>
  10 + <InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
  11 + <span> 分 至 </span>
  12 + <InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
  13 + <span> 分 </span>
  14 + </div>
  15 + <div class="item">
  16 + <a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
  17 + <span> 从 </span>
  18 + <InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
  19 + <span> 分开始,间隔 </span>
  20 + <InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
  21 + <span> 分 </span>
  22 + </div>
  23 + <div class="item">
  24 + <a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
  25 + <div class="list">
  26 + <a-checkbox-group v-model:value="valueList">
  27 + <template v-for="i in specifyRange" :key="i">
  28 + <a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
  29 + </template>
  30 + </a-checkbox-group>
  31 + </div>
  32 + </div>
  33 + </a-radio-group>
  34 + </div>
  35 +</template>
  36 +
  37 +<script lang="ts">
  38 + import { defineComponent } from 'vue';
  39 + import { InputNumber } from 'ant-design-vue';
  40 + import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin';
  41 +
  42 + export default defineComponent({
  43 + name: 'MinuteUI',
  44 + components: { InputNumber },
  45 + props: useTabProps({
  46 + defaultValue: '*',
  47 + }),
  48 + emits: useTabEmits(),
  49 + setup(props, context) {
  50 + return useTabSetup(props, context, {
  51 + defaultValue: '*',
  52 + minValue: 0,
  53 + maxValue: 59,
  54 + valueRange: { start: 0, end: 59 },
  55 + valueLoop: { start: 0, interval: 1 },
  56 + });
  57 + },
  58 + });
  59 +</script>
... ...
  1 +<template>
  2 + <div :class="`${prefixCls}-config-list`">
  3 + <a-radio-group v-model:value="type">
  4 + <div class="item">
  5 + <a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每月</a-radio>
  6 + </div>
  7 + <div class="item">
  8 + <a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
  9 + <span> 从 </span>
  10 + <InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
  11 + <span> 月 至 </span>
  12 + <InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
  13 + <span> 月 </span>
  14 + </div>
  15 + <div class="item">
  16 + <a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
  17 + <span> 从 </span>
  18 + <InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
  19 + <span> 月开始,间隔 </span>
  20 + <InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
  21 + <span> 月 </span>
  22 + </div>
  23 + <div class="item">
  24 + <a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
  25 + <div class="list">
  26 + <a-checkbox-group v-model:value="valueList">
  27 + <template v-for="i in specifyRange" :key="i">
  28 + <a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
  29 + </template>
  30 + </a-checkbox-group>
  31 + </div>
  32 + </div>
  33 + </a-radio-group>
  34 + </div>
  35 +</template>
  36 +
  37 +<script lang="ts">
  38 + import { defineComponent } from 'vue';
  39 + import { InputNumber } from 'ant-design-vue';
  40 + import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin';
  41 +
  42 + export default defineComponent({
  43 + name: 'MonthUI',
  44 + components: { InputNumber },
  45 + props: useTabProps({
  46 + defaultValue: '*',
  47 + }),
  48 + emits: useTabEmits(),
  49 + setup(props, context) {
  50 + return useTabSetup(props, context, {
  51 + defaultValue: '*',
  52 + minValue: 1,
  53 + maxValue: 12,
  54 + valueRange: { start: 1, end: 12 },
  55 + valueLoop: { start: 1, interval: 1 },
  56 + });
  57 + },
  58 + });
  59 +</script>
... ...
  1 +<template>
  2 + <div :class="`${prefixCls}-config-list`">
  3 + <a-radio-group v-model:value="type">
  4 + <div class="item">
  5 + <a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每秒</a-radio>
  6 + </div>
  7 + <div class="item">
  8 + <a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
  9 + <span> 从 </span>
  10 + <InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
  11 + <span> 秒 至 </span>
  12 + <InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
  13 + <span> 秒 </span>
  14 + </div>
  15 + <div class="item">
  16 + <a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
  17 + <span> 从 </span>
  18 + <InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
  19 + <span> 秒开始,间隔 </span>
  20 + <InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
  21 + <span> 秒 </span>
  22 + </div>
  23 + <div class="item">
  24 + <a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
  25 + <div class="list">
  26 + <a-checkbox-group v-model:value="valueList">
  27 + <template v-for="i in specifyRange" :key="i">
  28 + <a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
  29 + </template>
  30 + </a-checkbox-group>
  31 + </div>
  32 + </div>
  33 + </a-radio-group>
  34 + </div>
  35 +</template>
  36 +
  37 +<script lang="ts">
  38 + import { defineComponent } from 'vue';
  39 + import { InputNumber } from 'ant-design-vue';
  40 + import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin';
  41 +
  42 + export default defineComponent({
  43 + name: 'SecondUI',
  44 + components: { InputNumber },
  45 + props: useTabProps({
  46 + defaultValue: '*',
  47 + }),
  48 + emits: useTabEmits(),
  49 + setup(props, context) {
  50 + return useTabSetup(props, context, {
  51 + defaultValue: '*',
  52 + minValue: 0,
  53 + maxValue: 59,
  54 + valueRange: { start: 0, end: 59 },
  55 + valueLoop: { start: 0, interval: 1 },
  56 + });
  57 + },
  58 + });
  59 +</script>
... ...
  1 +<template>
  2 + <div :class="`${prefixCls}-config-list`">
  3 + <a-radio-group v-model:value="type">
  4 + <div class="item">
  5 + <a-radio :value="TypeEnum.unset" v-bind="beforeRadioAttrs">不设置</a-radio>
  6 + <span class="tip-info">日和周只能设置其中之一</span>
  7 + </div>
  8 + <div class="item">
  9 + <a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
  10 + <span> 从 </span>
  11 + <a-select
  12 + v-model:value="valueRange.start"
  13 + :options="weekOptions"
  14 + v-bind="typeRangeSelectAttrs"
  15 + />
  16 + <span> 至 </span>
  17 + <a-select
  18 + v-model:value="valueRange.end"
  19 + :options="weekOptions"
  20 + v-bind="typeRangeSelectAttrs"
  21 + />
  22 + </div>
  23 + <div class="item">
  24 + <a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
  25 + <span> 从 </span>
  26 + <a-select
  27 + v-model:value="valueLoop.start"
  28 + :options="weekOptions"
  29 + v-bind="typeLoopSelectAttrs"
  30 + />
  31 + <span> 开始,间隔 </span>
  32 + <InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
  33 + <span> 天 </span>
  34 + </div>
  35 + <div class="item">
  36 + <a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
  37 + <div class="list list-cn">
  38 + <a-checkbox-group v-model:value="valueList">
  39 + <template v-for="opt in weekOptions" :key="opt.label">
  40 + <a-checkbox :value="opt.value" v-bind="typeSpecifyAttrs">{{ opt.label }}</a-checkbox>
  41 + </template>
  42 + </a-checkbox-group>
  43 + </div>
  44 + </div>
  45 + </a-radio-group>
  46 + </div>
  47 +</template>
  48 +
  49 +<script lang="ts">
  50 + import { computed, watch, defineComponent } from 'vue';
  51 + import { InputNumber } from 'ant-design-vue';
  52 + import { useTabProps, useTabEmits, useTabSetup, TypeEnum } from './useTabMixin';
  53 +
  54 + const WEEK_MAP_EN = {
  55 + '1': 'SUN',
  56 + '2': 'MON',
  57 + '3': 'TUE',
  58 + '4': 'WED',
  59 + '5': 'THU',
  60 + '6': 'FRI',
  61 + '7': 'SAT',
  62 + };
  63 +
  64 + const WEEK_MAP_CN = {
  65 + '1': '周日',
  66 + '2': '周一',
  67 + '3': '周二',
  68 + '4': '周三',
  69 + '5': '周四',
  70 + '6': '周五',
  71 + '7': '周六',
  72 + };
  73 +
  74 + export default defineComponent({
  75 + name: 'WeekUI',
  76 + components: { InputNumber },
  77 + props: useTabProps({
  78 + defaultValue: '?',
  79 + props: {
  80 + day: { type: String, default: '*' },
  81 + },
  82 + }),
  83 + emits: useTabEmits(),
  84 + setup(props, context) {
  85 + const disabledChoice = computed(() => {
  86 + return (props.day && props.day !== '?') || props.disabled;
  87 + });
  88 + const setup = useTabSetup(props, context, {
  89 + defaultType: TypeEnum.unset,
  90 + defaultValue: '?',
  91 + minValue: 1,
  92 + maxValue: 7,
  93 + // 0,7表示周日 1表示周一
  94 + valueRange: { start: 1, end: 7 },
  95 + valueLoop: { start: 2, interval: 1 },
  96 + disabled: disabledChoice,
  97 + });
  98 + const weekOptions = computed(() => {
  99 + let options: { label: string; value: number }[] = [];
  100 + for (let weekKey of Object.keys(WEEK_MAP_CN)) {
  101 + let weekName: string = WEEK_MAP_CN[weekKey];
  102 + options.push({
  103 + value: Number.parseInt(weekKey),
  104 + label: weekName,
  105 + });
  106 + }
  107 + return options;
  108 + });
  109 +
  110 + const typeRangeSelectAttrs = computed(() => ({
  111 + class: ['w80'],
  112 + disabled: setup.typeRangeAttrs.value.disabled,
  113 + }));
  114 +
  115 + const typeLoopSelectAttrs = computed(() => ({
  116 + class: ['w80'],
  117 + disabled: setup.typeLoopAttrs.value.disabled,
  118 + }));
  119 +
  120 + watch(
  121 + () => props.day,
  122 + () => {
  123 + setup.updateValue(disabledChoice.value ? '?' : setup.computeValue.value);
  124 + }
  125 + );
  126 +
  127 + return {
  128 + ...setup,
  129 + weekOptions,
  130 + typeLoopSelectAttrs,
  131 + typeRangeSelectAttrs,
  132 + WEEK_MAP_CN,
  133 + WEEK_MAP_EN,
  134 + };
  135 + },
  136 + });
  137 +</script>
... ...
  1 +<template>
  2 + <div :class="`${prefixCls}-config-list`">
  3 + <a-radio-group v-model:value="type">
  4 + <div class="item">
  5 + <a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每年</a-radio>
  6 + </div>
  7 + <div class="item">
  8 + <a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
  9 + <span> 从 </span>
  10 + <InputNumber class="w80" v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
  11 + <span> 年 至 </span>
  12 + <InputNumber class="w80" v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
  13 + <span> 年 </span>
  14 + </div>
  15 + <div class="item">
  16 + <a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
  17 + <span> 从 </span>
  18 + <InputNumber class="w80" v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
  19 + <span> 年开始,间隔 </span>
  20 + <InputNumber class="w80" v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
  21 + <span> 年 </span>
  22 + </div>
  23 + </a-radio-group>
  24 + </div>
  25 +</template>
  26 +
  27 +<script lang="ts">
  28 + import { defineComponent } from 'vue';
  29 + import { InputNumber } from 'ant-design-vue';
  30 + import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin';
  31 +
  32 + export default defineComponent({
  33 + name: 'YearUI',
  34 + components: { InputNumber },
  35 + props: useTabProps({
  36 + defaultValue: '*',
  37 + }),
  38 + emits: useTabEmits(),
  39 + setup(props, context) {
  40 + const nowYear = new Date().getFullYear();
  41 + return useTabSetup(props, context, {
  42 + defaultValue: '*',
  43 + minValue: 0,
  44 + valueRange: { start: nowYear, end: nowYear + 100 },
  45 + valueLoop: { start: nowYear, interval: 1 },
  46 + });
  47 + },
  48 + });
  49 +</script>
... ...
  1 +// 主要用于日和星期的互斥使用
  2 +import { computed, inject, reactive, ref, unref, watch } from 'vue';
  3 +import { propTypes } from '/@/utils/propTypes';
  4 +
  5 +export enum TypeEnum {
  6 + unset = 'UNSET',
  7 + every = 'EVERY',
  8 + range = 'RANGE',
  9 + loop = 'LOOP',
  10 + work = 'WORK',
  11 + last = 'LAST',
  12 + specify = 'SPECIFY',
  13 +}
  14 +
  15 +// use 公共 props
  16 +export function useTabProps(options) {
  17 + const defaultValue = options?.defaultValue ?? '?';
  18 + return {
  19 + value: propTypes.string.def(defaultValue),
  20 + disabled: propTypes.bool.def(false),
  21 + ...options?.props,
  22 + };
  23 +}
  24 +
  25 +// use 公共 emits
  26 +export function useTabEmits() {
  27 + return ['change', 'update:value'];
  28 +}
  29 +
  30 +// use 公共 setup
  31 +export function useTabSetup(props, context, options) {
  32 + const { emit } = context;
  33 + const prefixCls = inject('prefixCls');
  34 + const defaultValue = ref(options?.defaultValue ?? '?');
  35 + // 类型
  36 + const type = ref(options.defaultType ?? TypeEnum.every);
  37 + const valueList = ref<any[]>([]);
  38 + // 对于不同的类型,所定义的值也有所不同
  39 + const valueRange = reactive(options.valueRange);
  40 + const valueLoop = reactive(options.valueLoop);
  41 + const valueWeek = reactive(options.valueWeek);
  42 + const valueWork = ref(options.valueWork);
  43 + const maxValue = ref(options.maxValue);
  44 + const minValue = ref(options.minValue);
  45 +
  46 + // 根据不同的类型计算出的value
  47 + const computeValue = computed(() => {
  48 + let valueArray: any[] = [];
  49 + switch (type.value) {
  50 + case TypeEnum.unset:
  51 + valueArray.push('?');
  52 + break;
  53 + case TypeEnum.every:
  54 + valueArray.push('*');
  55 + break;
  56 + case TypeEnum.range:
  57 + valueArray.push(`${valueRange.start}-${valueRange.end}`);
  58 + break;
  59 + case TypeEnum.loop:
  60 + valueArray.push(`${valueLoop.start}/${valueLoop.interval}`);
  61 + break;
  62 + case TypeEnum.work:
  63 + valueArray.push(`${valueWork.value}W`);
  64 + break;
  65 + case TypeEnum.last:
  66 + valueArray.push('L');
  67 + break;
  68 + case TypeEnum.specify:
  69 + if (valueList.value.length === 0) {
  70 + valueList.value.push(minValue.value);
  71 + }
  72 + valueArray.push(valueList.value.join(','));
  73 + break;
  74 + default:
  75 + valueArray.push(defaultValue.value);
  76 + break;
  77 + }
  78 + return valueArray.length > 0 ? valueArray.join('') : defaultValue.value;
  79 + });
  80 + // 指定值范围区间,介于最小值和最大值之间
  81 + const specifyRange = computed(() => {
  82 + let range: number[] = [];
  83 + if (maxValue.value != null) {
  84 + for (let i = minValue.value; i <= maxValue.value; i++) {
  85 + range.push(i);
  86 + }
  87 + }
  88 + return range;
  89 + });
  90 +
  91 + watch(
  92 + () => props.value,
  93 + (val) => {
  94 + if (val !== computeValue.value) {
  95 + parseValue(val);
  96 + }
  97 + },
  98 + { immediate: true }
  99 + );
  100 +
  101 + watch(computeValue, (v) => updateValue(v));
  102 +
  103 + function updateValue(value) {
  104 + emit('change', value);
  105 + emit('update:value', value);
  106 + }
  107 +
  108 + /**
  109 + * parseValue
  110 + * @param value
  111 + */
  112 + function parseValue(value) {
  113 + if (value === computeValue.value) {
  114 + return;
  115 + }
  116 + try {
  117 + if (!value || value === defaultValue.value) {
  118 + type.value = TypeEnum.every;
  119 + } else if (value.indexOf('?') >= 0) {
  120 + type.value = TypeEnum.unset;
  121 + } else if (value.indexOf('-') >= 0) {
  122 + type.value = TypeEnum.range;
  123 + const values = value.split('-');
  124 + if (values.length >= 2) {
  125 + valueRange.start = parseInt(values[0]);
  126 + valueRange.end = parseInt(values[1]);
  127 + }
  128 + } else if (value.indexOf('/') >= 0) {
  129 + type.value = TypeEnum.loop;
  130 + const values = value.split('/');
  131 + if (values.length >= 2) {
  132 + valueLoop.start = value[0] === '*' ? 0 : parseInt(values[0]);
  133 + valueLoop.interval = parseInt(values[1]);
  134 + }
  135 + } else if (value.indexOf('W') >= 0) {
  136 + type.value = TypeEnum.work;
  137 + const values = value.split('W');
  138 + if (!values[0] && !isNaN(values[0])) {
  139 + valueWork.value = parseInt(values[0]);
  140 + }
  141 + } else if (value.indexOf('L') >= 0) {
  142 + type.value = TypeEnum.last;
  143 + } else if (value.indexOf(',') >= 0 || !isNaN(value)) {
  144 + type.value = TypeEnum.specify;
  145 + valueList.value = value.split(',').map((item) => parseInt(item));
  146 + } else {
  147 + type.value = TypeEnum.every;
  148 + }
  149 + } catch (e) {
  150 + type.value = TypeEnum.every;
  151 + }
  152 + }
  153 +
  154 + const beforeRadioAttrs = computed(() => ({
  155 + class: ['choice'],
  156 + disabled: props.disabled || unref(options.disabled),
  157 + }));
  158 + const inputNumberAttrs = computed(() => ({
  159 + class: ['w60'],
  160 + max: maxValue.value,
  161 + min: minValue.value,
  162 + precision: 0,
  163 + }));
  164 + const typeRangeAttrs = computed(() => ({
  165 + disabled: type.value !== TypeEnum.range || props.disabled || unref(options.disabled),
  166 + ...inputNumberAttrs.value,
  167 + }));
  168 + const typeLoopAttrs = computed(() => ({
  169 + disabled: type.value !== TypeEnum.loop || props.disabled || unref(options.disabled),
  170 + ...inputNumberAttrs.value,
  171 + }));
  172 + const typeSpecifyAttrs = computed(() => ({
  173 + disabled: type.value !== TypeEnum.specify || props.disabled || unref(options.disabled),
  174 + class: ['list-check-item'],
  175 + }));
  176 +
  177 + return {
  178 + type,
  179 + TypeEnum,
  180 + prefixCls,
  181 + defaultValue,
  182 + valueRange,
  183 + valueLoop,
  184 + valueWeek,
  185 + valueList,
  186 + valueWork,
  187 + maxValue,
  188 + minValue,
  189 + computeValue,
  190 + specifyRange,
  191 + updateValue,
  192 + parseValue,
  193 + beforeRadioAttrs,
  194 + inputNumberAttrs,
  195 + typeRangeAttrs,
  196 + typeLoopAttrs,
  197 + typeSpecifyAttrs,
  198 + };
  199 +}
... ...
  1 +import CronParser from 'cron-parser';
  2 +import type { ValidatorRule } from 'ant-design-vue/lib/form/interface';
  3 +
  4 +const cronRule: ValidatorRule = {
  5 + validator({}, value) {
  6 + // 没填写就不校验
  7 + if (!value) {
  8 + return Promise.resolve();
  9 + }
  10 + const values: string[] = value.split(' ').filter((item) => !!item);
  11 + if (values.length > 7) {
  12 + return Promise.reject('Cron表达式最多7项!');
  13 + }
  14 + // 检查第7项
  15 + let val: string = value;
  16 + if (values.length === 7) {
  17 + const year = values[6];
  18 + if (year !== '*' && year !== '?') {
  19 + let yearValues: string[] = [];
  20 + if (year.indexOf('-') >= 0) {
  21 + yearValues = year.split('-');
  22 + } else if (year.indexOf('/')) {
  23 + yearValues = year.split('/');
  24 + } else {
  25 + yearValues = [year];
  26 + }
  27 + // 判断是否都是数字
  28 + const checkYear = yearValues.some((item) => isNaN(Number(item)));
  29 + if (checkYear) {
  30 + return Promise.reject('Cron表达式参数[年]错误:' + year);
  31 + }
  32 + }
  33 + // 取其中的前六项
  34 + val = values.slice(0, 6).join(' ');
  35 + }
  36 + // 6位 没有年
  37 + // 5位没有秒、年
  38 + try {
  39 + const iter = CronParser.parseExpression(val);
  40 + iter.next();
  41 + return Promise.resolve();
  42 + } catch (e) {
  43 + return Promise.reject('Cron表达式错误:' + e);
  44 + }
  45 + },
  46 +};
  47 +
  48 +export default cronRule.validator;
... ...
... ... @@ -82,6 +82,7 @@ export interface ColEx {
82 82
83 83 export type ComponentType =
84 84 | 'Input'
  85 + | 'JEasyCron'
85 86 | 'ApiRadioGroup'
86 87 | 'InputGroup'
87 88 | 'InputPassword'
... ...
... ... @@ -472,19 +472,49 @@
472 472 });
473 473 </script>
474 474 <style lang="less">
475   - .fold-left {
476   - z-index: 1;
477   - cursor: pointer;
478   - position: absolute;
479   - top: 0.85rem;
480   - left: 1.1vw;
  475 + // 使用媒体查询兼容 1920 1280 1024 800
  476 + @media screen and (max-width: 1980px) {
  477 + .fold-left {
  478 + z-index: 1;
  479 + cursor: pointer;
  480 + position: absolute;
  481 + top: 0.85rem;
  482 + left: 1.1vw;
  483 + }
  484 + .fold-right {
  485 + z-index: 1;
  486 + cursor: pointer;
  487 + position: absolute;
  488 + top: 0.85rem;
  489 + left: 18.2vw;
  490 + }
  491 + }
  492 + @media screen and (max-width: 1280px) {
  493 + .fold-right {
  494 + z-index: 1;
  495 + cursor: pointer;
  496 + position: absolute;
  497 + top: 0.85rem;
  498 + left: 17.5vw;
  499 + }
  500 + }
  501 + @media screen and (max-width: 1024px) {
  502 + .fold-right {
  503 + z-index: 1;
  504 + cursor: pointer;
  505 + position: absolute;
  506 + top: 0.85rem;
  507 + left: 14.2vw;
  508 + }
481 509 }
482   - .fold-right {
483   - z-index: 1;
484   - cursor: pointer;
485   - position: absolute;
486   - top: 0.85rem;
487   - left: 18.2vw;
  510 + @media screen and (max-width: 800px) {
  511 + .fold-right {
  512 + z-index: 1;
  513 + cursor: pointer;
  514 + position: absolute;
  515 + top: 0.85rem;
  516 + left: 18vw;
  517 + }
488 518 }
489 519 @prefix-cls: ~'@{namespace}-basic-tree';
490 520
... ...
1   -// 全局按需注册组件
2 1 import type { App } from 'vue';
  2 +import { Icon } from './Icon';
3 3 import { Button } from './Button';
4 4 import {
5 5 // Need
6 6 Button as AntButton,
  7 + Select,
  8 + Alert,
  9 + Checkbox,
  10 + DatePicker,
  11 + Radio,
  12 + Switch,
  13 + Card,
  14 + List,
  15 + Tabs,
  16 + Descriptions,
  17 + Tree,
  18 + Table,
  19 + Divider,
  20 + Modal,
  21 + Drawer,
  22 + TreeSelect,
  23 + Dropdown,
  24 + Tag,
  25 + Tooltip,
  26 + Badge,
  27 + Popover,
  28 + Upload,
  29 + Transfer,
  30 + Steps,
  31 + PageHeader,
  32 + Result,
  33 + Empty,
  34 + Avatar,
  35 + Menu,
  36 + Breadcrumb,
  37 + Form,
7 38 Input,
  39 + Row,
  40 + Col,
  41 + Spin,
  42 + Space,
8 43 Layout,
  44 + Collapse,
  45 + Slider,
  46 + InputNumber,
  47 + Carousel,
  48 + Popconfirm,
  49 + Skeleton,
  50 + Cascader,
  51 + Rate,
9 52 } from 'ant-design-vue';
10 53
11   -const compList = [AntButton.Group];
  54 +const compList = [AntButton.Group, Icon];
12 55
13 56 export function registerGlobComp(app: App) {
14 57 compList.forEach((comp) => {
15 58 app.component(comp.name || comp.displayName, comp);
16 59 });
17 60
18   - app.use(Input).use(Button).use(Layout);
  61 + app
  62 + .use(Select)
  63 + .use(Alert)
  64 + .use(Button)
  65 + .use(Breadcrumb)
  66 + .use(Checkbox)
  67 + .use(DatePicker)
  68 + .use(Radio)
  69 + .use(Switch)
  70 + .use(Card)
  71 + .use(List)
  72 + .use(Descriptions)
  73 + .use(Tree)
  74 + .use(TreeSelect)
  75 + .use(Table)
  76 + .use(Divider)
  77 + .use(Modal)
  78 + .use(Drawer)
  79 + .use(Dropdown)
  80 + .use(Tag)
  81 + .use(Tooltip)
  82 + .use(Badge)
  83 + .use(Popover)
  84 + .use(Upload)
  85 + .use(Transfer)
  86 + .use(Steps)
  87 + .use(PageHeader)
  88 + .use(Result)
  89 + .use(Empty)
  90 + .use(Avatar)
  91 + .use(Menu)
  92 + .use(Tabs)
  93 + .use(Form)
  94 + .use(Input)
  95 + .use(Row)
  96 + .use(Col)
  97 + .use(Spin)
  98 + .use(Space)
  99 + .use(Layout)
  100 + .use(Collapse)
  101 + .use(Slider)
  102 + .use(InputNumber)
  103 + .use(Carousel)
  104 + .use(Popconfirm)
  105 + .use(Skeleton)
  106 + .use(Cascader)
  107 + .use(Rate);
19 108 }
... ...
... ... @@ -174,6 +174,9 @@ export default {
174 174 menu: 'Menu management',
175 175 role: 'Role management',
176 176 },
  177 + report: {
  178 + schedue_log: 'Schedue Log',
  179 + },
177 180 table: {
178 181 table: 'Table',
179 182
... ...
... ... @@ -166,6 +166,9 @@ export default {
166 166 menu: '菜单管理',
167 167 role: '角色管理',
168 168 },
  169 + report: {
  170 + schedue_log: '调度日志',
  171 + },
169 172 table: {
170 173 table: 'Table',
171 174 basic: '基础表格',
... ...
... ... @@ -53,12 +53,34 @@ export const AccountDetail: AppRouteRecordRaw = {
53 53 },
54 54 ],
55 55 };
  56 +// 调度日志静态路由
  57 +export const SchedueLog: AppRouteRecordRaw = {
  58 + path: '/report',
  59 + name: '报表管理',
  60 + component: LAYOUT,
  61 + meta: {
  62 + title: '报表管理',
  63 + hideBreadcrumb: true,
  64 + hideChildrenInMenu: true,
  65 + },
  66 + children: [
  67 + {
  68 + path: 'schedue_log/:id',
  69 + name: 'Schedue_Log',
  70 + component: () => import('../../views/system/scheduled/SchedueLog.vue'),
  71 + meta: {
  72 + title: '调度日志',
  73 + },
  74 + },
  75 + ],
  76 +};
56 77
57 78 // Basic routing without permission
58 79 export const basicRoutes = [
59 80 LoginRoute,
60 81 RootRoute,
61 82 AccountDetail,
  83 + SchedueLog,
62 84 ...mainOutRoutes,
63 85 REDIRECT_ROUTE,
64 86 PAGE_NOT_FOUND_ROUTE,
... ...
... ... @@ -107,7 +107,7 @@ const setting: ProjectConfig = {
107 107 // Fold trigger position
108 108 trigger: TriggerEnum.HEADER,
109 109 // Turn on accordion mode, only show a menu
110   - accordion: false,
  110 + accordion: true,
111 111 // Switch page to close menu
112 112 closeMixSidebarOnChange: false,
113 113 // Module opening method ‘click’ |'hover'
... ...
  1 +/**
  2 + * 简单实现防抖方法
  3 + *
  4 + * 防抖(debounce)函数在第一次触发给定的函数时,不立即执行函数,而是给出一个期限值(delay),比如100ms。
  5 + * 如果100ms内再次执行函数,就重新开始计时,直到计时结束后再真正执行函数。
  6 + * 这样做的好处是如果短时间内大量触发同一事件,只会执行一次函数。
  7 + *
  8 + * @param fn 要防抖的函数
  9 + * @param delay 防抖的毫秒数
  10 + * @returns {Function}
  11 + */
  12 +export function simpleDebounce(fn, delay = 100) {
  13 + let timer: any | null = null;
  14 + return function () {
  15 + let args = arguments;
  16 + if (timer) {
  17 + clearTimeout(timer);
  18 + }
  19 + timer = setTimeout(() => {
  20 + // @ts-ignore
  21 + fn.apply(this, args);
  22 + }, delay);
  23 + };
  24 +}
  25 +
  26 +// /**
  27 +// * 日期格式化
  28 +// * @param date 日期
  29 +// * @param block 格式化字符串
  30 +// */
  31 +export function dateFormat(date, block) {
  32 + if (!date) {
  33 + return '';
  34 + }
  35 + let format = block || 'yyyy-MM-dd';
  36 + date = new Date(date);
  37 + const map = {
  38 + M: date.getMonth() + 1, // 月份
  39 + d: date.getDate(), // 日
  40 + h: date.getHours(), // 小时
  41 + m: date.getMinutes(), // 分
  42 + s: date.getSeconds(), // 秒
  43 + q: Math.floor((date.getMonth() + 3) / 3), // 季度
  44 + S: date.getMilliseconds(), // 毫秒
  45 + };
  46 + format = format.replace(/([yMdhmsqS])+/g, (all, t) => {
  47 + let v = map[t];
  48 + if (v !== undefined) {
  49 + if (all.length > 1) {
  50 + v = `0${v}`;
  51 + v = v.substr(v.length - 2);
  52 + }
  53 + return v;
  54 + } else if (t === 'y') {
  55 + return date
  56 + .getFullYear()
  57 + .toString()
  58 + .substr(4 - all.length);
  59 + }
  60 + return all;
  61 + });
  62 + return format;
  63 +}
  64 +
... ...
... ... @@ -40,7 +40,7 @@ export const CoapSchemas: FormSchema[] = [
40 40 { label: 'Efento NB-IoT', value: 'EFENTO' },
41 41 ],
42 42 },
43   - colProps: { span: 11 },
  43 + colProps: { span: 22},
44 44 },
45 45 {
46 46 field: 'transportPayloadType',
... ... @@ -53,7 +53,7 @@ export const CoapSchemas: FormSchema[] = [
53 53 { label: 'PROTOBUF', value: 'PROTOBUF' },
54 54 ],
55 55 },
56   - colProps: { span: 11 },
  56 + colProps: { span: 22 },
57 57 ifShow: ({ values }) => !isEfentoNb(values.coapDeviceType),
58 58 },
59 59 {
... ...
1 1 <template>
2   - <div
3   - style="
4   - margin-top: -5vh;
5   - padding-left: 1.5vw;
6   - border: 1px solid gray;
7   - margin-left: 0.1vw;
8   - border-radius: 5px;
9   - "
10   - >
  2 + <div style="margin-top: -5vh; border: 1px solid gray; border-radius: 5px">
11 3 <div style="margin-top: 1.2vh">
12 4 <BasicForm :showResetButton="false" :showSubmitButton="false" @register="register" />
13 5 </div>
... ...
... ... @@ -16,13 +16,18 @@
16 16 :options="selectOptions"
17 17 @change="handleDeviceChange"
18 18 mode="multiple"
  19 + labelInValue
  20 + allowClear
  21 + @deselect="handleDeSelect"
  22 + notFoundContent="请选择设备"
19 23 />
20 24 <div style="margin-top: 1.5vh"></div>
21 25 <DeviceAttrCpns
22 26 ref="deviceAttrRef"
23 27 @change="handleChange"
24 28 :value="deviceList"
25   - :orgId="organizationId"
  29 + :orgId="organizationId || orgId"
  30 + :deSelectValue="deSelectValue"
26 31 />
27 32 </template>
28 33 </BasicForm>
... ... @@ -64,9 +69,13 @@
64 69 });
65 70 });
66 71 const handleDeviceChange = (e) => {
67   - console.log('e', e);
68 72 deviceList.value = e;
69 73 };
  74 + const deSelectValue = ref('');
  75 + const handleDeSelect = ({ key }) => {
  76 + //取消选中的key
  77 + deSelectValue.value = key;
  78 + };
70 79 const [registerForm, { validate, setFieldsValue, resetFields }] = useForm({
71 80 labelWidth: 120,
72 81 schemas: formSchema,
... ... @@ -101,10 +110,22 @@
101 110 });
102 111 //TODO 模拟的数据 待服务端返回
103 112 const deviceIds: any = [
104   - '8943f0b0-f1f7-11ec-98ad-a9680487d1e0',
105   - '8f5b4280-f29e-11ec-98ad-a9680487d1e0',
106   - '54e199d0-f1f7-11ec-98ad-a9680487d1e0',
107   - '6d9043f0-f1f7-11ec-98ad-a9680487d1e0',
  113 + {
  114 + label: '奥迪网关子设备',
  115 + key: '8a4cc9a0-f201-11ec-98ad-a9680487d1e0',
  116 + },
  117 + {
  118 + label: '宝马默认设备',
  119 + key: '8943f0b0-f1f7-11ec-98ad-a9680487d1e0',
  120 + },
  121 + {
  122 + label: '奔驰默认设备',
  123 + key: '6d9043f0-f1f7-11ec-98ad-a9680487d1e0',
  124 + },
  125 + {
  126 + label: '新增奥迪测试设备',
  127 + key: '8f5b4280-f29e-11ec-98ad-a9680487d1e0',
  128 + },
108 129 ];
109 130 selectDevice.value = deviceIds;
110 131 //回显设备属性 TODO 模拟的数据 待服务端返回
... ... @@ -133,6 +154,7 @@
133 154 deviceAttrRef.value?.echoDynamicInputFunc(deviceAttrData.value);
134 155 } else {
135 156 editId.value = '';
  157 + orgId.value = '';
136 158 selectDevice.value = [];
137 159 selectOptions.value = [];
138 160 deviceList.value = [];
... ...
... ... @@ -12,14 +12,9 @@ import {
12 12 isMonth,
13 13 isEmpty,
14 14 isDefultWeek,
15   - isMin,
16   - isMax,
17   - isAvg,
18   - isSum,
19   - isCountAll,
  15 + isFixedWeek,
20 16 } from './timeConfig';
21 17 import { AggregateDataEnum } from '../../device/localtion/cpns/TimePeriodForm/config';
22   -// import { JCronValidator } from '/@/components/Form';
23 18
24 19 export enum SchemaFiled {
25 20 WAY = 'way',
... ... @@ -208,13 +203,6 @@ export const formSchema: QFormSchema[] = [
208 203 ],
209 204 },
210 205 },
211   - // {
212   - // field: 'cronExpression',
213   - // label: 'Cron表达式',
214   - // component: 'JEasyCron',
215   - // defaultValue: '* * * * * ? *',
216   - // rules: [{ required: true, message: '请输入Cron表达式' }, { validator: JCronValidator }],
217   - // },
218 206 {
219 207 field: 'timeWeek',
220 208 component: 'Select',
... ... @@ -354,16 +342,6 @@ export const formSchema: QFormSchema[] = [
354 342 };
355 343 },
356 344 },
357   - // {
358   - // field: 'deviceAttr',
359   - // label: '设备属性',
360   - // component: 'Select',
361   - // componentProps: {
362   - // placeholder: '请选择设备属性',
363   - // },
364   - // colProps: { span: 24 },
365   - // slot: 'deviceAttr',
366   - // },
367 345 {
368 346 field: 'dataCompare',
369 347 label: '数据类型',
... ... @@ -576,13 +554,7 @@ export const formSchema: QFormSchema[] = [
576 554 };
577 555 },
578 556 colProps: { span: 24 },
579   - ifShow: ({ values }) =>
580   - (!isEmpty(values.agg) && !isDefultWeek(values.defaultWeek)) ||
581   - isMin(values.agg) ||
582   - isMax(values.agg) ||
583   - isAvg(values.agg) ||
584   - isSum(values.agg) ||
585   - isCountAll(values.agg),
  557 + ifShow: ({ values }) => isFixedWeek(values.defaultWeek) && !isEmpty(values.agg),
586 558 },
587 559 {
588 560 field: 'interval',
... ... @@ -600,12 +572,6 @@ export const formSchema: QFormSchema[] = [
600 572 },
601 573 ],
602 574 },
603   - ifShow: ({ values }) =>
604   - (!isEmpty(values.agg) && !isDefultWeek(values.defaultWeek)) ||
605   - isMin(values.agg) ||
606   - isMax(values.agg) ||
607   - isAvg(values.agg) ||
608   - isSum(values.agg) ||
609   - isCountAll(values.agg),
  575 + ifShow: ({ values }) => isFixedWeek(values.defaultWeek) && !isEmpty(values.agg),
610 576 },
611 577 ];
... ...
1 1 <template>
2 2 <div
3   - v-for="(param, index) in dynamicInput.params"
4   - :key="index"
  3 + v-for="param in dynamicInput.params"
  4 + :key="param.key"
5 5 style="display: flex; margin-top: 0.25vh"
6 6 >
7 7 <a-input
... ... @@ -16,6 +16,7 @@
16 16 style="width: 160px; margin-left: 1.8vw"
17 17 :options="selectOptions"
18 18 @change="emitChange"
  19 + allowClear
19 20 />
20 21 </div>
21 22 </template>
... ... @@ -29,9 +30,7 @@
29 30 import { propTypes } from '/@/utils/propTypes';
30 31 import { SelectTypes } from 'ant-design-vue/es/select';
31 32 import { Select } from 'ant-design-vue';
32   - import { screenLinkPageByDeptIdGetDevice } from '/@/api/ruleengine/ruleengineApi';
33 33 import { getAttribute } from '/@/api/ruleengine/ruleengineApi';
34   - // import { getDeviceAttribute } from '/@/api/alarm/position';
35 34
36 35 interface Params {
37 36 [x: string]: string;
... ... @@ -42,17 +41,11 @@
42 41 value: propTypes.array.def([]),
43 42 orgId: propTypes.string.def(''),
44 43 });
45   - const emits = defineEmits(['change', 'update:value', 'dynamicReduceHeight', 'dynamicAddHeight']);
  44 + const emits = defineEmits(['change', 'update:value']);
46 45 const selectOptions = ref<SelectTypes['options']>([]);
47   - //动态数据
48   - const dynamicInput: UnwrapRef<{ params: Params[] }> = reactive({ params: [] });
49   - //监听传入数据value
50   - watchEffect(() => {
51   - initVal();
52   - });
53 46 //获取属性
54   - const getAttr = async (orgId, value) => {
55   - const res = await getAttribute(orgId, value.join(','));
  47 + const getAttr = async (orgId, deviceId) => {
  48 + const res = await getAttribute(orgId, deviceId.join(','));
56 49 selectOptions.value = res.map((o) => {
57 50 return {
58 51 label: o,
... ... @@ -60,43 +53,24 @@
60 53 };
61 54 });
62 55 };
63   - //获取设备
64   - const deviceOptions: any = ref([]);
65   - const getDevice = async (orgId) => {
66   - const { items } = await screenLinkPageByDeptIdGetDevice({
67   - organizationId: orgId,
68   - });
69   - deviceOptions.value = items.map((item) => {
70   - return {
71   - label: item.name,
72   - value: item.tbDeviceId,
73   - };
74   - });
75   - };
  56 + //动态数据
  57 + const dynamicInput: UnwrapRef<{ params: Params[] }> = reactive({ params: [] });
  58 + //监听传入数据value
  59 + watchEffect(() => {
  60 + initVal();
  61 + });
76 62 /**
77 63 * 初始化数值
78 64 */
79 65 async function initVal() {
80 66 dynamicInput.params = [];
81 67 if (props.value && props.orgId) {
82   - const getPropsDevice = props.value;
83   - const getPropsOrgId = props.orgId;
84   - await getAttr(getPropsOrgId, getPropsDevice);
85   - await getDevice(getPropsOrgId);
86   - const temp: any = [];
87   - deviceOptions.value.forEach((f) => {
88   - getPropsDevice.forEach((f1) => {
89   - if (f1 == f.value) {
90   - temp.push({
91   - label: f.label,
92   - value: f.value,
93   - });
94   - }
95   - });
96   - });
97   - temp.forEach((item: Params) => {
  68 + let jsonObj = props.value;
  69 + const deviceId = jsonObj.map((m: any) => m.value);
  70 + await getAttr(props.orgId, deviceId);
  71 + jsonObj.forEach((item: any) => {
98 72 dynamicInput.params.push({
99   - attribute: item.attribute,
  73 + attribute: '',
100 74 device: item.label,
101 75 value: item.value,
102 76 });
... ... @@ -116,18 +90,19 @@
116 90 });
117 91 });
118 92 }
  93 + console.log('emitChange', obj);
119 94 emits('change', obj);
120 95 emits('update:value', obj);
121 96 }
122 97 //回显
123 98 const echoDynamicInputFunc = (o) => {
124 99 dynamicInput.params = [];
125   - dynamicInput.params = o.map((m) => {
126   - return {
  100 + o.forEach((m: any) => {
  101 + dynamicInput.params.push({
127 102 device: m.name,
128 103 attribute: m.attribute,
129 104 value: m.device,
130   - };
  105 + });
131 106 });
132 107 };
133 108 defineExpose({
... ... @@ -135,21 +110,5 @@
135 110 });
136 111 </script>
137 112 <style scoped lang="css">
138   - .dynamic-delete-button {
139   - cursor: pointer;
140   - position: relative;
141   - top: 4px;
142   - font-size: 24px;
143   - color: #999;
144   - transition: all 0.3s;
145   - }
146   -
147   - .dynamic-delete-button:hover {
148   - color: #777;
149   - }
150   -
151   - .dynamic-delete-button[disabled] {
152   - cursor: not-allowed;
153   - opacity: 0.5;
154   - }
  113 + @import './deviceAttrCpns.css';
155 114 </style>
... ...
  1 +.dynamic-delete-button {
  2 + cursor: pointer;
  3 + position: relative;
  4 + top: 4px;
  5 + font-size: 24px;
  6 + color: #999;
  7 + transition: all 0.3s;
  8 +}
  9 +
  10 +.dynamic-delete-button:hover {
  11 + color: #777;
  12 +}
  13 +
  14 +.dynamic-delete-button[disabled] {
  15 + cursor: not-allowed;
  16 + opacity: 0.5;
  17 +}
... ...
... ... @@ -44,26 +44,6 @@
44 44 },
45 45 },
46 46 ]"
47   - :dropDownActions="[
48   - {
49   - label: '执行一次',
50   - popConfirm: {
51   - title: '是否执行一次?',
52   - },
53   - },
54   - {
55   - label: '任务详细',
56   - popConfirm: {
57   - title: '任务详细',
58   - },
59   - },
60   - {
61   - label: '调度日志',
62   - popConfirm: {
63   - title: '调度日志',
64   - },
65   - },
66   - ]"
67 47 />
68 48 </template>
69 49 <template #status="{ record }">
... ...
... ... @@ -80,6 +80,7 @@ export enum TypeEnum {
80 80 IS_MONTH = 'month',
81 81 IS_EMPTY = 'NONE',
82 82 IS_DEFAULT_WEEK = 'defaultIsWeek',
  83 + IS_FIXED_WEEK = '2',
83 84 IS_MIN = 'MIN',
84 85 IS_MAX = 'MAX',
85 86 IS_AVG = 'AVG',
... ... @@ -115,6 +116,9 @@ export const isEmpty = (type: string) => {
115 116 export const isDefultWeek = (type: string) => {
116 117 return type === TypeEnum.IS_DEFAULT_WEEK;
117 118 };
  119 +export const isFixedWeek = (type: string) => {
  120 + return type === TypeEnum.IS_FIXED_WEEK;
  121 +};
118 122
119 123 export const isMin = (type: string) => {
120 124 return type === TypeEnum.IS_MIN;
... ...
... ... @@ -137,9 +137,6 @@
137 137 delete newFieldValue.nameCity;
138 138 delete newFieldValue.nameCoun;
139 139 delete newFieldValue.nameTown;
140   -
141   - console.log(newFieldValue.qrCode);
142   - console.log(newFieldValue.codeTown);
143 140 // 表单校验
144 141 let validateArray = [
145 142 'name',
... ... @@ -153,7 +150,7 @@
153 150 'tel',
154 151 'id',
155 152 ];
156   - if (newFieldValue.qrCode == undefined) {
  153 + if (newFieldValue.qrCode == undefined || newFieldValue.qrCode == '') {
157 154 validateArray.push('qrcode');
158 155 } else {
159 156 const findExistIndex = validateArray.findIndex((o) => o == 'qrcode');
... ...
  1 +<template>
  2 + <div>
  3 + <BasicModal
  4 + v-bind="$attrs"
  5 + width="110rem"
  6 + :height="heightNum"
  7 + @register="register"
  8 + title="调度日志"
  9 + @cancel="handleCancel"
  10 + :showOkBtn="false"
  11 + destroyOnClose
  12 + >
  13 + <div>
  14 + <BasicTable @register="registerTable">
  15 + <template #toolbar>
  16 + <Popconfirm
  17 + title="您确定要清空全部数据"
  18 + ok-text="确定"
  19 + cancel-text="取消"
  20 + @confirm="handleClear"
  21 + >
  22 + <a-button type="primary"> 清空 </a-button>
  23 + </Popconfirm>
  24 + <Popconfirm title="您确定要批量删除数据" ok-text="确定" cancel-text="取消">
  25 + <a-button type="primary" color="error" :disabled="hasBatchDelete">
  26 + 批量删除
  27 + </a-button>
  28 + </Popconfirm>
  29 + </template>
  30 + <template #action="{ record }">
  31 + <TableAction
  32 + :actions="[
  33 + {
  34 + label: '查看',
  35 + icon: 'clarity:note-edit-line',
  36 + onClick: handleView.bind(null, record),
  37 + },
  38 + {
  39 + label: '删除',
  40 + icon: 'ant-design:delete-outlined',
  41 + color: 'error',
  42 + popConfirm: {
  43 + title: '是否确认删除',
  44 + confirm: handleDeleteOrBatchDelete.bind(null, record),
  45 + },
  46 + },
  47 + ]"
  48 + />
  49 + </template>
  50 + </BasicTable>
  51 + <ScheduleLogViewModal @register="registerModalScheduleLogView" />
  52 + </div>
  53 + </BasicModal>
  54 + </div>
  55 +</template>
  56 +<script setup lang="ts">
  57 + import { ref, nextTick } from 'vue';
  58 + import { BasicModal, useModalInner } from '/@/components/Modal';
  59 + import { BasicTable, useTable, TableAction } from '/@/components/Table';
  60 + import { columnSchedue, searchSchedueFormSchema } from './config.data';
  61 + import { useBatchDelete } from '/@/hooks/web/useBatchDelete';
  62 + import {
  63 + deleteSchedueLogManage,
  64 + schedueLogPage,
  65 + schedueLogCleanPage,
  66 + } from '/@/api/schedue/schedueManager';
  67 + import { Popconfirm } from 'ant-design-vue';
  68 + import { useMessage } from '/@/hooks/web/useMessage';
  69 + import ScheduleLogViewModal from './ScheduleLogViewModal.vue';
  70 + import { useModal } from '/@/components/Modal';
  71 +
  72 + const { createMessage } = useMessage();
  73 + const heightNum = ref(800);
  74 + const [registerTable, { setProps, reload, getForm }] = useTable({
  75 + title: '调度日志列表',
  76 + api: schedueLogPage,
  77 + columns: columnSchedue,
  78 + showIndexColumn: false,
  79 + clickToRowSelect: false,
  80 + useSearchForm: true,
  81 + ellipsis: true,
  82 + showTableSetting: true,
  83 + bordered: true,
  84 + formConfig: {
  85 + labelWidth: 120,
  86 + schemas: searchSchedueFormSchema,
  87 + fieldMapToTime: [['sendTime', ['startTime', 'endTime'], 'YYYY-MM-DD HH:mm:ss']],
  88 + },
  89 + rowKey: 'id',
  90 + actionColumn: {
  91 + width: 200,
  92 + title: '操作',
  93 + dataIndex: 'action',
  94 + slots: { customRender: 'action' },
  95 + fixed: 'right',
  96 + },
  97 + });
  98 + // 刷新
  99 + const handleSuccess = () => {
  100 + reload();
  101 + };
  102 + const { hasBatchDelete, handleDeleteOrBatchDelete, selectionOptions } = useBatchDelete(
  103 + deleteSchedueLogManage,
  104 + handleSuccess,
  105 + setProps
  106 + );
  107 +
  108 + const [register] = useModalInner(() => {
  109 + nextTick(() => {
  110 + setProps(selectionOptions);
  111 + setProps({
  112 + rowKey: 'id',
  113 + });
  114 + //重置清空搜索表单
  115 + const { resetFields } = getForm();
  116 + resetFields();
  117 + });
  118 + });
  119 + const handleCancel = () => {};
  120 + const handleClear = async () => {
  121 + await schedueLogCleanPage();
  122 + createMessage.success(`清空成功`);
  123 + handleSuccess();
  124 + };
  125 + const [registerModalScheduleLogView, { openModal: openModalLogView }] = useModal();
  126 + const handleView = (record: Recordable) => {
  127 + openModalLogView(true, {
  128 + isUpdate: true,
  129 + record,
  130 + });
  131 + };
  132 +</script>
  133 +<style lang="less" scoped></style>
... ...
  1 +<template>
  2 + <div>
  3 + <BasicModal
  4 + v-bind="$attrs"
  5 + width="62rem"
  6 + :height="heightNum"
  7 + @register="register"
  8 + title="调度日志详细信息"
  9 + @cancel="handleCancel"
  10 + :showOkBtn="false"
  11 + style="font-size: 12px"
  12 + >
  13 + <div>
  14 + <Description :column="3" size="middle" @register="registeDesc" />
  15 + </div>
  16 + </BasicModal>
  17 + </div>
  18 +</template>
  19 +<script setup lang="ts">
  20 + import { ref, nextTick, reactive } from 'vue';
  21 + import { BasicModal, useModalInner } from '/@/components/Modal';
  22 + import { scheduleLogDetailSchema } from './config.data';
  23 + import { Description } from '/@/components/Description/index';
  24 + import { useDescription } from '/@/components/Description';
  25 + import { schedueLogDetailPage } from '/@/api/schedue/schedueManager';
  26 +
  27 + const heightNum = ref(800);
  28 + let personData = reactive({});
  29 + const [registeDesc, { setDescProps }] = useDescription({
  30 + title: '调度日志详细信息',
  31 + data: personData,
  32 + schema: scheduleLogDetailSchema,
  33 + column: 3,
  34 + });
  35 + const [register] = useModalInner(async (data) => {
  36 + const res = await schedueLogDetailPage(data.record.jobId, data.record.id);
  37 + if (res.code === 200) {
  38 + nextTick(() => {
  39 + for (let i in res.data) {
  40 + Reflect.set(personData, i, res.data[i]);
  41 + }
  42 + setDescProps(personData);
  43 + });
  44 + }
  45 + });
  46 + const handleCancel = () => {};
  47 +</script>
  48 +<style lang="less" scoped>
  49 + :deep(.vben-basic-title-normal) {
  50 + font-size: 16px;
  51 + font-weight: 500;
  52 + }
  53 + :deep(.vben-collapse-container__header) {
  54 + border-bottom: none;
  55 + }
  56 +</style>
... ...
  1 +<template>
  2 + <BasicDrawer
  3 + v-bind="$attrs"
  4 + @register="registerDrawer"
  5 + showFooter
  6 + :title="getTitle"
  7 + width="30%"
  8 + @ok="handleSubmit"
  9 + >
  10 + <BasicForm @register="registerForm" />
  11 + </BasicDrawer>
  12 +</template>
  13 +<script lang="ts" setup>
  14 + import { ref, computed, unref, reactive } from 'vue';
  15 + import { BasicForm, useForm } from '/@/components/Form';
  16 + import { formSchema } from './config.form.data';
  17 + import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
  18 + import { createOrEditSchedueManage, putSchedueConfigManage } from '/@/api/schedue/schedueManager';
  19 + import { useMessage } from '/@/hooks/web/useMessage';
  20 +
  21 + const emit = defineEmits(['success', 'register']);
  22 + const isUpdate = ref(true);
  23 + const editId = ref('');
  24 + const [registerForm, { validate, setFieldsValue, resetFields }] = useForm({
  25 + labelWidth: 120,
  26 + schemas: formSchema,
  27 + showActionButtonGroup: false,
  28 + fieldMapToTime: [['timeZone', ['startTime', 'endTime'], 'YYYY-MM-DD HH:mm:ss']],
  29 + });
  30 + const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
  31 + await resetFields();
  32 + setDrawerProps({ confirmLoading: false });
  33 + isUpdate.value = !!data?.isUpdate;
  34 + if (unref(isUpdate)) {
  35 + //回显基础数据
  36 + editId.value = data.record.id;
  37 + await setFieldsValue(data.record);
  38 + } else {
  39 + editId.value = '';
  40 + }
  41 + });
  42 + const getTitle = computed(() => (!unref(isUpdate) ? '新增定时任务' : '编辑定时任务'));
  43 + let postObj: any = reactive({});
  44 + async function handleSubmit() {
  45 + setDrawerProps({ confirmLoading: true });
  46 + try {
  47 + const { createMessage } = useMessage();
  48 + const values = await validate();
  49 + if (!values) return;
  50 + postObj = {
  51 + ...values,
  52 + ...{ id: editId.value !== '' ? editId.value : '' },
  53 + };
  54 + let saveMessage = '添加成功';
  55 + let updateMessage = '修改成功';
  56 + editId.value !== ''
  57 + ? await putSchedueConfigManage(postObj)
  58 + : await createOrEditSchedueManage(postObj);
  59 +
  60 + closeDrawer();
  61 + emit('success');
  62 + createMessage.success(unref(isUpdate) ? updateMessage : saveMessage);
  63 + } finally {
  64 + setTimeout(() => {
  65 + setDrawerProps({ confirmLoading: false });
  66 + }, 300);
  67 + }
  68 + }
  69 +</script>
... ...
  1 +<template>
  2 + <div>
  3 + <BasicModal
  4 + v-bind="$attrs"
  5 + width="62rem"
  6 + :height="heightNum"
  7 + @register="register"
  8 + title="任务详细"
  9 + @cancel="handleCancel"
  10 + :showOkBtn="false"
  11 + style="font-size: 12px"
  12 + >
  13 + <div>
  14 + <Description :column="3" size="middle" @register="registeDesc" />
  15 + </div>
  16 + </BasicModal>
  17 + </div>
  18 +</template>
  19 +<script setup lang="ts">
  20 + import { ref, nextTick, reactive } from 'vue';
  21 + import { BasicModal, useModalInner } from '/@/components/Modal';
  22 + import { personSchema } from './config.data';
  23 + import { Description } from '/@/components/Description/index';
  24 + import { useDescription } from '/@/components/Description';
  25 +
  26 + const heightNum = ref(800);
  27 + let personData = reactive({});
  28 + const [registeDesc, { setDescProps }] = useDescription({
  29 + title: '任务详细信息',
  30 + data: personData,
  31 + schema: personSchema,
  32 + column: 3,
  33 + });
  34 + const [register] = useModalInner((data) => {
  35 + nextTick(() => {
  36 + //回显
  37 + for (let i in data.record) {
  38 + Reflect.set(personData, i, data.record[i]);
  39 + }
  40 + setDescProps(personData);
  41 + });
  42 + });
  43 + const handleCancel = () => {};
  44 +</script>
  45 +<style lang="less" scoped>
  46 + :deep(.vben-basic-title-normal) {
  47 + font-size: 16px;
  48 + font-weight: 500;
  49 + }
  50 + :deep(.vben-collapse-container__header) {
  51 + border-bottom: none;
  52 + }
  53 +</style>
... ...
  1 +import { DescItem } from '/@/components/Description/index';
  2 +import { BasicColumn, FormSchema } from '/@/components/Table';
  3 +import moment from 'moment';
  4 +import { h } from 'vue';
  5 +import { Tag } from 'ant-design-vue';
  6 +
  7 +//任务详细配置
  8 +export const personSchema: DescItem[] = [
  9 + {
  10 + field: 'jobGroup',
  11 + label: '任务分组:',
  12 + render: (_, data) => {
  13 + return data.jobGroup == 'Default' ? '默认' : data.jobGroup == 'System' ? '系统' : '报表';
  14 + },
  15 + },
  16 + {
  17 + field: 'jobName',
  18 + label: '任务名称:',
  19 + },
  20 + {
  21 + field: 'createTime',
  22 + label: '创建时间:',
  23 + },
  24 + {
  25 + field: 'cronExpression',
  26 + label: 'cron表达式:',
  27 + },
  28 + {
  29 + field: 'b6',
  30 + label: '下次执行时间:',
  31 + },
  32 + {
  33 + field: 'invokeTarget',
  34 + label: '调用目标方法:',
  35 + },
  36 + {
  37 + field: 'status',
  38 + label: '任务状态:',
  39 + render: (_, data) => {
  40 + return data.status == 1 ? '启用' : '禁用';
  41 + },
  42 + },
  43 + {
  44 + field: 'b9',
  45 + label: '是否并发:',
  46 + },
  47 + {
  48 + field: 'b10',
  49 + label: '执行策略:',
  50 + },
  51 +];
  52 +
  53 +// 调度日志表格配置
  54 +export const columnSchedue: BasicColumn[] = [
  55 + {
  56 + title: '任务名称',
  57 + dataIndex: 'jobName',
  58 + width: 120,
  59 + },
  60 + {
  61 + title: '任务组名',
  62 + dataIndex: 'jobGroup',
  63 + width: 120,
  64 + format: (_text: string, record: Recordable) => {
  65 + return record.jobGroup === 'Default'
  66 + ? '默认'
  67 + : record.jobGroup === 'System'
  68 + ? '系统'
  69 + : '报表';
  70 + },
  71 + },
  72 + {
  73 + title: '调用目标字符串',
  74 + dataIndex: 'invokeTarget',
  75 + width: 120,
  76 + },
  77 + {
  78 + title: '日志信息',
  79 + dataIndex: 'jobMessage',
  80 + width: 160,
  81 + },
  82 + {
  83 + title: '执行状态',
  84 + dataIndex: 'status',
  85 + width: 160,
  86 + customRender: ({ record }) => {
  87 + const status = record.status;
  88 + const success = status === 1;
  89 + const color = success ? 'green' : 'red';
  90 + const successText: string = '成功';
  91 + const failedText: string = '失败';
  92 + const text = success ? successText : failedText;
  93 + return h(Tag, { color: color }, () => text);
  94 + },
  95 + },
  96 + {
  97 + title: '执行时间',
  98 + dataIndex: 'startTime',
  99 + width: 180,
  100 + },
  101 +];
  102 +
  103 +// 调度日志表格查询配置
  104 +export const searchSchedueFormSchema: FormSchema[] = [
  105 + {
  106 + field: 'jobName',
  107 + label: '任务名称',
  108 + component: 'Input',
  109 + colProps: { span: 4 },
  110 + componentProps: {
  111 + maxLength: 36,
  112 + placeholder: '请输入任务名称',
  113 + },
  114 + },
  115 + {
  116 + field: 'jobGroup',
  117 + label: '任务组名',
  118 + component: 'Select',
  119 + colProps: { span: 4 },
  120 + componentProps: {
  121 + options: [
  122 + {
  123 + label: '默认',
  124 + value: 'Default',
  125 + },
  126 + {
  127 + label: '系统',
  128 + value: 'System',
  129 + },
  130 + {
  131 + label: '报表',
  132 + value: 'Report',
  133 + },
  134 + ],
  135 + placeholder: '请选择任务组名',
  136 + },
  137 + },
  138 + {
  139 + field: 'status',
  140 + label: '执行状态',
  141 + component: 'Select',
  142 + colProps: { span: 4 },
  143 + componentProps: {
  144 + options: [
  145 + {
  146 + label: '成功',
  147 + value: 1,
  148 + },
  149 + {
  150 + label: '失败',
  151 + value: 0,
  152 + },
  153 + ],
  154 + placeholder: '请选择执行状态',
  155 + },
  156 + },
  157 + {
  158 + field: 'sendTime',
  159 + label: '执行时间',
  160 + component: 'RangePicker',
  161 + componentProps: {
  162 + showTime: {
  163 + defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
  164 + },
  165 + },
  166 + colProps: { span: 4 },
  167 + },
  168 +];
  169 +// 调度日志详情
  170 +export const scheduleLogDetailSchema: DescItem[] = [
  171 + {
  172 + field: 'jobName',
  173 + label: '任务名称:',
  174 + },
  175 + {
  176 + field: 'jobGroup',
  177 + label: '任务组名:',
  178 + render: (_, data) => {
  179 + return data.jobGroup == 'Default' ? '默认' : data.jobGroup == 'System' ? '系统' : '报表';
  180 + },
  181 + },
  182 + {
  183 + field: 'invokeTarget',
  184 + label: '调用目标字符串:',
  185 + },
  186 + {
  187 + field: 'jobMessage',
  188 + label: '日志信息:',
  189 + },
  190 + {
  191 + field: 'status',
  192 + label: '执行状态:',
  193 + render: (_, data) => {
  194 + return data.status == 1 ? '成功' : '失败';
  195 + },
  196 + },
  197 + {
  198 + field: 'startTime',
  199 + label: '执行时间:',
  200 + },
  201 +];
... ...
  1 +import { BasicColumn, FormSchema } from '/@/components/Table';
  2 +import type { FormSchema as QFormSchema } from '/@/components/Form/index';
  3 +import { JCronValidator } from '/@/components/Form';
  4 +
  5 +// 定时任务表格配置
  6 +export const columnSchedue: BasicColumn[] = [
  7 + {
  8 + title: '任务名称',
  9 + dataIndex: 'jobName',
  10 + width: 120,
  11 + },
  12 + {
  13 + title: '任务组名',
  14 + dataIndex: 'jobGroup',
  15 + width: 120,
  16 + format: (_text: string, record: Recordable) => {
  17 + return record.jobGroup === 'Default'
  18 + ? '默认'
  19 + : record.jobGroup === 'System'
  20 + ? '系统'
  21 + : '报表';
  22 + },
  23 + },
  24 + {
  25 + title: '调用目标字符串',
  26 + dataIndex: 'invokeTarget',
  27 + width: 120,
  28 + },
  29 + {
  30 + title: 'cron执行表达式',
  31 + dataIndex: 'cronExpression',
  32 + width: 160,
  33 + },
  34 + {
  35 + title: '状态',
  36 + dataIndex: 'status',
  37 + width: 160,
  38 + slots: { customRender: 'status' },
  39 + },
  40 +];
  41 +
  42 +// 定时任务格查询配置
  43 +export const searchSchedueFormSchema: FormSchema[] = [
  44 + {
  45 + field: 'jobName',
  46 + label: '任务名称',
  47 + component: 'Input',
  48 + colProps: { span: 6 },
  49 + componentProps: {
  50 + maxLength: 36,
  51 + placeholder: '请输入任务名称',
  52 + },
  53 + },
  54 + {
  55 + field: 'jobGroup',
  56 + label: '任务组名',
  57 + component: 'Select',
  58 + colProps: { span: 6 },
  59 + componentProps: {
  60 + options: [
  61 + {
  62 + label: '默认',
  63 + value: 'Default',
  64 + },
  65 + {
  66 + label: '系统',
  67 + value: 'System',
  68 + },
  69 + {
  70 + label: '报表',
  71 + value: 'Report',
  72 + },
  73 + ],
  74 + placeholder: '请选择任务组名',
  75 + },
  76 + },
  77 + {
  78 + field: 'status',
  79 + label: '任务状态',
  80 + component: 'Select',
  81 + colProps: { span: 6 },
  82 + componentProps: {
  83 + options: [
  84 + {
  85 + label: '正常',
  86 + value: 1,
  87 + },
  88 + {
  89 + label: '暂停',
  90 + value: 0,
  91 + },
  92 + ],
  93 + placeholder: '请选择任务状态',
  94 + },
  95 + },
  96 +];
  97 +
  98 +// 新增编辑配置
  99 +export const formSchema: QFormSchema[] = [
  100 + {
  101 + field: 'jobName',
  102 + label: '任务名称',
  103 + colProps: { span: 24 },
  104 + required: true,
  105 + component: 'Input',
  106 + componentProps: {
  107 + maxLength: 255,
  108 + placeholder: '请输入任务名称',
  109 + },
  110 + },
  111 + {
  112 + field: 'jobGroup',
  113 + component: 'Select',
  114 + label: '任务分组',
  115 + colProps: {
  116 + span: 24,
  117 + },
  118 + componentProps: {
  119 + placeholder: '请选择任务分组',
  120 + options: [
  121 + {
  122 + label: '默认',
  123 + value: 'Default',
  124 + },
  125 + {
  126 + label: '系统',
  127 + value: 'System',
  128 + },
  129 + {
  130 + label: '报表',
  131 + value: 'Report',
  132 + },
  133 + ],
  134 + },
  135 + },
  136 + {
  137 + field: 'invokeTarget',
  138 + label: ' 调用方法',
  139 + helpMessage: [
  140 + 'Bean调用示例:reportTask.noParams()Class类调用示例:reportTask.noParams()参数说明:支持字符串,布尔类型,长整型,浮点型,整型',
  141 + ],
  142 + colProps: { span: 24 },
  143 + required: true,
  144 + component: 'Input',
  145 + componentProps: {
  146 + maxLength: 255,
  147 + placeholder: '请输入调用方法',
  148 + },
  149 + },
  150 + {
  151 + field: 'cronExpression',
  152 + label: 'Cron表达式',
  153 + component: 'JEasyCron',
  154 + defaultValue: '* * * * * ? *',
  155 + colProps: {
  156 + span: 24,
  157 + },
  158 + rules: [{ required: true, message: '请输入Cron表达式' }, { validator: JCronValidator }],
  159 + },
  160 + {
  161 + field: 'field10',
  162 + component: 'RadioButtonGroup',
  163 + label: '执行策略',
  164 + colProps: {
  165 + span: 24,
  166 + },
  167 + defaultValue: '1',
  168 + componentProps: {
  169 + options: [
  170 + {
  171 + label: '立即执行',
  172 + value: '1',
  173 + },
  174 + {
  175 + label: '执行一次',
  176 + value: '2',
  177 + },
  178 + {
  179 + label: '放弃执行',
  180 + value: '3',
  181 + },
  182 + ],
  183 + },
  184 + },
  185 + {
  186 + field: 'field11',
  187 + component: 'RadioButtonGroup',
  188 + label: '是否并发',
  189 + colProps: {
  190 + span: 24,
  191 + },
  192 + defaultValue: '2',
  193 + componentProps: {
  194 + options: [
  195 + {
  196 + label: '允许',
  197 + value: '1',
  198 + },
  199 + {
  200 + label: '禁止',
  201 + value: '2',
  202 + },
  203 + ],
  204 + },
  205 + },
  206 + {
  207 + field: 'status',
  208 + component: 'RadioGroup',
  209 + label: '状态',
  210 + colProps: {
  211 + span: 24,
  212 + },
  213 + defaultValue: 1,
  214 + componentProps: {
  215 + options: [
  216 + {
  217 + label: '正常',
  218 + value: 1,
  219 + },
  220 + {
  221 + label: '暂停',
  222 + value: 0,
  223 + },
  224 + ],
  225 + },
  226 + },
  227 +];
... ...
  1 +<template>
  2 + <div>
  3 + <BasicTable @register="registerTable">
  4 + <template #toolbar>
  5 + <Authority value="api:yt:schedule:post">
  6 + <a-button type="primary" @click="handleCreateOrEdit(null)"> 新增 </a-button>
  7 + </Authority>
  8 + <Authority value="api:yt:schedule:get">
  9 + <a-button type="primary"> 导出 </a-button>
  10 + </Authority>
  11 + <Authority value="api:yt:schedule:delete">
  12 + <Popconfirm title="您确定要批量删除数据" ok-text="确定" cancel-text="取消">
  13 + <a-button type="primary" color="error" :disabled="hasBatchDelete"> 批量删除 </a-button>
  14 + </Popconfirm>
  15 + </Authority>
  16 + </template>
  17 + <template #action="{ record }">
  18 + <TableAction
  19 + :actions="[
  20 + {
  21 + label: '编辑',
  22 + icon: 'clarity:note-edit-line',
  23 + auth: 'api:yt:schedule:update',
  24 + onClick: handleCreateOrEdit.bind(null, record),
  25 + },
  26 + {
  27 + label: '删除',
  28 + icon: 'ant-design:delete-outlined',
  29 + color: 'error',
  30 + auth: 'api:yt:schedule:delete',
  31 + popConfirm: {
  32 + title: '是否确认删除',
  33 + confirm: handleDeleteOrBatchDelete.bind(null, record),
  34 + },
  35 + },
  36 + ]"
  37 + :dropDownActions="[
  38 + {
  39 + label: '执行一次',
  40 + icon: 'ant-design:caret-right-filled',
  41 + popConfirm: {
  42 + title: '确认要立即执行一次' + '“' + record.jobName + '”' + '任务吗?',
  43 + confirm: handleRunOne.bind(null, record),
  44 + },
  45 + },
  46 + {
  47 + label: '任务详细',
  48 + icon: 'ant-design:eye-outlined',
  49 + onClick: handleTaskDetailModal.bind(null, record),
  50 + },
  51 + {
  52 + label: '调度日志',
  53 + icon: 'ant-design:insert-row-below-outlined',
  54 + onClick: handleSchedulingLogFunc.bind(null, record),
  55 + },
  56 + ]"
  57 + />
  58 + </template>
  59 + <template #status="{ record }">
  60 + <Switch
  61 + :disabled="disabledSwitch"
  62 + :checked="record.status === 1"
  63 + :loading="record.pendingStatus"
  64 + checkedChildren="启用"
  65 + unCheckedChildren="禁用"
  66 + @change="(checked:boolean)=>statusChange(checked,record)"
  67 + />
  68 + </template>
  69 + </BasicTable>
  70 + <ScheduledDrawer @register="registerDrawer" @success="handleSuccess" />
  71 + <TaskDetailPreviewModal @register="registerModalTaskDetail" />
  72 + <SchedueLog @register="registerModalSchedueLog" />
  73 + </div>
  74 +</template>
  75 +<script setup lang="ts">
  76 + import { nextTick, ref } from 'vue';
  77 + import { BasicTable, useTable, TableAction } from '/@/components/Table';
  78 + import { columnSchedue, searchSchedueFormSchema } from './config.form.data';
  79 + import { useBatchDelete } from '/@/hooks/web/useBatchDelete';
  80 + import {
  81 + deleteSchedueManage,
  82 + scheduePage,
  83 + putSchedueByidAndStatusManage,
  84 + postRunSchedueConfigManage,
  85 + } from '/@/api/schedue/schedueManager';
  86 + import { Popconfirm, Switch } from 'ant-design-vue';
  87 + import { useModal } from '/@/components/Modal';
  88 + import TaskDetailPreviewModal from './TaskDetailPreviewModal.vue';
  89 + import SchedueLog from './SchedueLog.vue';
  90 + import ScheduledDrawer from './ScheduledDrawer.vue';
  91 + import { useDrawer } from '/@/components/Drawer';
  92 + import { Authority } from '/@/components/Authority';
  93 + import { useMessage } from '/@/hooks/web/useMessage';
  94 +
  95 + const disabledSwitch = ref(false);
  96 + const { createMessage } = useMessage();
  97 + const [registerTable, { setProps, reload }] = useTable({
  98 + title: '定时任务列表',
  99 + api: scheduePage,
  100 + columns: columnSchedue,
  101 + showIndexColumn: false,
  102 + clickToRowSelect: false,
  103 + useSearchForm: true,
  104 + ellipsis: true,
  105 + showTableSetting: true,
  106 + bordered: true,
  107 + formConfig: {
  108 + labelWidth: 120,
  109 + schemas: searchSchedueFormSchema,
  110 + fieldMapToTime: [['sendTime', ['startTime', 'endTime'], 'YYYY-MM-DD HH:mm:ss']],
  111 + },
  112 + actionColumn: {
  113 + width: 200,
  114 + title: '操作',
  115 + dataIndex: 'action',
  116 + slots: { customRender: 'action' },
  117 + fixed: 'right',
  118 + },
  119 + });
  120 + // 刷新
  121 + const handleSuccess = () => {
  122 + reload();
  123 + };
  124 + const { hasBatchDelete, handleDeleteOrBatchDelete, selectionOptions } = useBatchDelete(
  125 + deleteSchedueManage,
  126 + handleSuccess,
  127 + setProps
  128 + );
  129 +
  130 + nextTick(() => {
  131 + setProps(selectionOptions);
  132 + });
  133 + const [registerDrawer, { openDrawer }] = useDrawer();
  134 + const [registerModalTaskDetail, { openModal: openModalTaskDetail }] = useModal();
  135 + const [registerModalSchedueLog, { openModal: openModalSchedueLog }] = useModal();
  136 + const handleSchedulingLogFunc = (record: Recordable) => {
  137 + openModalSchedueLog(true, {
  138 + isUpdate: 2,
  139 + record,
  140 + });
  141 + };
  142 + const handleTaskDetailModal = (record: Recordable) => {
  143 + openModalTaskDetail(true, {
  144 + isUpdate: true,
  145 + record,
  146 + });
  147 + };
  148 + // 新增或编辑
  149 + const handleCreateOrEdit = (record: Recordable | null) => {
  150 + if (record) {
  151 + openDrawer(true, {
  152 + isUpdate: true,
  153 + record,
  154 + });
  155 + } else {
  156 + openDrawer(true, {
  157 + isUpdate: false,
  158 + });
  159 + }
  160 + };
  161 +
  162 + const statusChange = async (checked, record) => {
  163 + try {
  164 + setProps({
  165 + loading: true,
  166 + });
  167 + disabledSwitch.value = true;
  168 + const newStatus = checked ? 1 : 0;
  169 + const res = await putSchedueByidAndStatusManage(record.id, newStatus);
  170 + if (res && newStatus) {
  171 + createMessage.success(`启用成功`);
  172 + } else {
  173 + createMessage.success('禁用成功');
  174 + }
  175 + } finally {
  176 + setTimeout(() => {
  177 + setProps({
  178 + loading: false,
  179 + });
  180 + disabledSwitch.value = false;
  181 + }, 500);
  182 + reload();
  183 + }
  184 + };
  185 + const handleRunOne = async (record: Recordable) => {
  186 + const res = await postRunSchedueConfigManage(record.id);
  187 + if (res?.code === 200) {
  188 + createMessage.success(`执行一次任务"${record.jobName}"成功`);
  189 + }
  190 + };
  191 +</script>
  192 +<style lang="less" scoped></style>
... ...