PlanForm.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. <template>
  2. <el-dialog :title="title" :visible.sync="visible" width="950px" append-to-body
  3. :close-on-click-modal="false" @close="handleClose"
  4. >
  5. <el-form ref="form" :model="form" :rules="rules" label-width="100px" v-loading="formLoading">
  6. <el-row :gutter="20">
  7. <el-col :span="12">
  8. <el-form-item label="计划名称" prop="planName">
  9. <el-input v-model="form.planName" placeholder="请输入计划名称" maxlength="64" show-word-limit/>
  10. </el-form-item>
  11. </el-col>
  12. <el-col :span="12">
  13. <el-form-item label="归属区域" prop="areaCode">
  14. <el-select v-model="form.areaCode" placeholder="请选择归属区域" style="width: 100%" filterable>
  15. <el-option v-for="item in flatAreaOptions" :key="item.id" :label="item.label" :value="item.id"/>
  16. </el-select>
  17. </el-form-item>
  18. </el-col>
  19. </el-row>
  20. <el-row :gutter="20">
  21. <el-col :span="12">
  22. <el-form-item label="计划类型">
  23. <el-tag :type="form.planType === 2 ? '' : 'warning'">
  24. {{ form.planType === 2 ? '自动巡检' : '手动巡检' }}
  25. </el-tag>
  26. </el-form-item>
  27. </el-col>
  28. <el-col :span="12">
  29. <el-form-item label="目标类型" prop="targetType">
  30. <el-radio-group v-model="form.targetType" @change="handleTargetTypeChange">
  31. <el-radio-button v-for="item in TARGET_TYPE_OPTIONS" :key="item.value" :label="item.value">
  32. {{ item.label }}
  33. </el-radio-button>
  34. </el-radio-group>
  35. </el-form-item>
  36. </el-col>
  37. </el-row>
  38. <el-form-item label="巡检目标" prop="targetCodeList">
  39. <target-selector ref="targetSelector" v-model="form.targetCodeList"
  40. :target-type="form.targetType" :area-code="form.areaCode" @change="handleTargetChange"
  41. />
  42. </el-form-item>
  43. <el-row :gutter="20">
  44. <el-col :span="12" v-if="form.planType === 1">
  45. <el-form-item label="默认执行人" prop="executor">
  46. <el-input v-model="form.executor" placeholder="请输入执行人姓名" maxlength="32"/>
  47. </el-form-item>
  48. </el-col>
  49. <el-col :span="12">
  50. <el-form-item label="计划时间" prop="planTime">
  51. <el-date-picker v-model="form.planTime" type="datetime"
  52. format="yyyy-MM-dd HH:mm" value-format="yyyy-MM-dd HH:mm:00"
  53. placeholder="选择计划执行时间" style="width: 100%"
  54. />
  55. </el-form-item>
  56. </el-col>
  57. </el-row>
  58. <!-- 自动巡检:调度配置 -->
  59. <template v-if="form.planType === 2">
  60. <el-divider content-position="left"><span class="divider-title">定时调度配置</span></el-divider>
  61. <el-row :gutter="20">
  62. <el-col :span="12">
  63. <el-form-item label="Cron表达式" prop="cronExpression">
  64. <el-input v-model="form.cronExpression" placeholder="如:0 0 8 * * ?">
  65. <template slot="append">
  66. <el-button icon="el-icon-question" @click="showCronHelp">帮助</el-button>
  67. </template>
  68. </el-input>
  69. <div class="cron-preview" v-if="form.cronExpression">
  70. <i class="el-icon-time"></i>
  71. {{ cronDescription }}
  72. </div>
  73. </el-form-item>
  74. </el-col>
  75. <el-col :span="12">
  76. <el-form-item label="启用调度">
  77. <el-switch v-model="form.scheduleEnabled" :active-value="1" :inactive-value="0"
  78. active-text="启用" inactive-text="禁用" active-color="#13ce66">
  79. </el-switch>
  80. <span class="schedule-tip" v-if="form.scheduleEnabled === 1 && !form.cronExpression">
  81. <i class="el-icon-warning" style="color: #E6A23C"></i>
  82. 请配置Cron表达式
  83. </span>
  84. </el-form-item>
  85. </el-col>
  86. </el-row>
  87. <!-- Cron快捷选择 -->
  88. <el-row :gutter="20">
  89. <el-col :span="24">
  90. <el-form-item label="快捷选择">
  91. <el-button-group>
  92. <el-button size="small" @click="setCron('0 0 8 * * ?')">每天8:00</el-button>
  93. <el-button size="small" @click="setCron('0 0 8,14 * * ?')">每天8:00/14:00</el-button>
  94. <el-button size="small" @click="setCron('0 0 9 * * 2')">每周一9:00</el-button>
  95. <el-button size="small" @click="setCron('0 0 9 1 * ?')">每月1号9:00</el-button>
  96. <el-button size="small" @click="setCron('0 */30 * * * ?')">每30分钟</el-button>
  97. <el-button size="small" @click="setCron('0 0 */2 * * ?')">每2小时</el-button>
  98. </el-button-group>
  99. </el-form-item>
  100. </el-col>
  101. </el-row>
  102. </template>
  103. <el-form-item label="计划描述" prop="description">
  104. <el-input v-model="form.description" type="textarea" :rows="2" maxlength="256" show-word-limit/>
  105. </el-form-item>
  106. <template v-if="form.planType === 2">
  107. <el-divider content-position="left"><span class="divider-title">巡检规则配置</span></el-divider>
  108. <rule-config ref="ruleConfig" v-model="form.rules"/>
  109. </template>
  110. </el-form>
  111. <div slot="footer" class="dialog-footer">
  112. <el-button @click="handleClose">取 消</el-button>
  113. <el-button type="primary" @click="submitForm" :loading="submitting">确 定</el-button>
  114. </div>
  115. <!-- Cron帮助对话框 -->
  116. <el-dialog title="Cron表达式帮助" :visible.sync="cronHelpVisible" width="600px" append-to-body>
  117. <div class="cron-help">
  118. <h4>Cron表达式格式</h4>
  119. <p>格式: <code>秒 分 时 日 月 周 [年]</code></p>
  120. <el-table :data="cronFields" border size="small">
  121. <el-table-column label="字段" prop="field" width="80" />
  122. <el-table-column label="允许值" prop="allowed" width="200" />
  123. <el-table-column label="特殊字符" prop="special" />
  124. </el-table>
  125. <h4 style="margin-top: 20px">常用示例</h4>
  126. <el-table :data="cronExamples" border size="small">
  127. <el-table-column label="表达式" prop="cron" width="200" />
  128. <el-table-column label="说明" prop="desc" />
  129. </el-table>
  130. </div>
  131. <div slot="footer">
  132. <el-button type="primary" @click="cronHelpVisible = false">知道了</el-button>
  133. </div>
  134. </el-dialog>
  135. </el-dialog>
  136. </template>
  137. <script>
  138. import { getPlan, addPlan, updatePlan } from '@/api/inspection/plan'
  139. import { registerPlan, unregisterPlan } from '@/api/inspection/scheduler'
  140. import { PLAN_TYPE_OPTIONS, TARGET_TYPE_OPTIONS } from '@/enums/InspectionEnums'
  141. import TargetSelector from './TargetSelector.vue'
  142. import RuleConfig from './RuleConfig.vue'
  143. export default {
  144. name: 'PlanForm',
  145. components: { TargetSelector, RuleConfig },
  146. props: {
  147. areaOptions: { type: Array, default: () => [] }
  148. },
  149. data() {
  150. const validateTargets = (rule, value, callback) => {
  151. if (!value || value.length === 0) {
  152. callback(new Error('请选择至少一个巡检目标'))
  153. } else {
  154. callback()
  155. }
  156. }
  157. const validateCron = (rule, value, callback) => {
  158. if (this.form.scheduleEnabled === 1 && !value) {
  159. callback(new Error('启用调度时必须配置Cron表达式'))
  160. } else {
  161. callback()
  162. }
  163. }
  164. return {
  165. visible: false,
  166. formLoading: false,
  167. submitting: false,
  168. title: '',
  169. form: this.initForm(),
  170. rules: {
  171. planName: [{ required: true, message: '请输入计划名称', trigger: 'blur' }],
  172. areaCode: [{ required: true, message: '请选择归属区域', trigger: 'change' }],
  173. targetType: [{ required: true, message: '请选择目标类型', trigger: 'change' }],
  174. targetCodeList: [{ required: true, validator: validateTargets, trigger: 'change' }],
  175. executor: [{ required: true, message: '请输入执行人', trigger: 'blur' }],
  176. cronExpression: [{ validator: validateCron, trigger: 'blur' }]
  177. },
  178. PLAN_TYPE_OPTIONS,
  179. TARGET_TYPE_OPTIONS,
  180. cronHelpVisible: false,
  181. cronFields: [
  182. { field: '秒', allowed: '0-59', special: ', - * /' },
  183. { field: '分', allowed: '0-59', special: ', - * /' },
  184. { field: '时', allowed: '0-23', special: ', - * /' },
  185. { field: '日', allowed: '1-31', special: ', - * ? / L W' },
  186. { field: '月', allowed: '1-12 或 JAN-DEC', special: ', - * /' },
  187. { field: '周', allowed: '1-7 或 SUN-SAT', special: ', - * ? / L #' }
  188. ],
  189. cronExamples: [
  190. { cron: '0 0 8 * * ?', desc: '每天早上8点执行' },
  191. { cron: '0 0 8,14 * * ?', desc: '每天8点和14点执行' },
  192. { cron: '0 0 9 * * 2', desc: '每周一早上9点执行' },
  193. { cron: '0 0 9 1 * ?', desc: '每月1号早上9点执行' },
  194. { cron: '0 */30 * * * ?', desc: '每30分钟执行一次' },
  195. { cron: '0 0 */2 * * ?', desc: '每2小时执行一次' }
  196. ]
  197. }
  198. },
  199. computed: {
  200. flatAreaOptions() {
  201. const result = []
  202. const flatten = (list, prefix = '') => {
  203. list.forEach(item => {
  204. result.push({ id: item.id, label: prefix + item.label })
  205. if (item.children && item.children.length > 0) flatten(item.children, prefix + item.label + ' / ')
  206. })
  207. }
  208. flatten(this.areaOptions)
  209. return result
  210. },
  211. cronDescription() {
  212. const cron = this.form.cronExpression
  213. if (!cron) return ''
  214. const parts = cron.split(' ')
  215. if (parts.length < 6) return '表达式格式不正确'
  216. const [second, minute, hour, day, month, week] = parts
  217. // 每天执行
  218. if (day === '*' && month === '*' && week === '?') {
  219. if (hour.includes(',')) {
  220. return `每天 ${hour.split(',').map(h => h + ':' + minute.padStart(2, '0')).join('、')} 执行`
  221. }
  222. if (hour.includes('/')) {
  223. return `每${hour.split('/')[1]}小时执行一次`
  224. }
  225. return `每天 ${hour}:${minute.padStart(2, '0')} 执行`
  226. }
  227. // 每周执行
  228. if (day === '?' && week !== '*') {
  229. const weekMap = { '1': '周日', '2': '周一', '3': '周二', '4': '周三', '5': '周四', '6': '周五', '7': '周六' }
  230. return `每${weekMap[week] || '周' + week} ${hour}:${minute.padStart(2, '0')} 执行`
  231. }
  232. // 每月执行
  233. if (day !== '*' && day !== '?') {
  234. return `每月${day}日 ${hour}:${minute.padStart(2, '0')} 执行`
  235. }
  236. // 分钟级
  237. if (minute.includes('/')) {
  238. return `每${minute.split('/')[1]}分钟执行一次`
  239. }
  240. return '自定义调度规则'
  241. }
  242. },
  243. methods: {
  244. initForm() {
  245. return {
  246. id: null,
  247. planCode: null,
  248. planName: '',
  249. areaCode: null,
  250. planType: 2,
  251. targetType: 2,
  252. targetCodeList: [],
  253. targetNames: '',
  254. planTime: null,
  255. cronExpression: '',
  256. scheduleEnabled: 0, // 新增字段:是否启用调度
  257. executor: '',
  258. description: '',
  259. rules: []
  260. }
  261. },
  262. async open(id, planType) {
  263. this.form = this.initForm()
  264. if (planType) {
  265. this.form.planType = planType
  266. }
  267. this.title = id ? '编辑巡检计划' : '新增巡检计划'
  268. this.visible = true
  269. if (id) {
  270. this.formLoading = true
  271. try {
  272. const res = await getPlan(id)
  273. const data = res.data || {}
  274. this.form = {
  275. ...data,
  276. targetCodeList: data.targetCodeList || [],
  277. scheduleEnabled: data.scheduleEnabled || 0
  278. }
  279. } catch (e) {
  280. this.$modal.msgError('获取计划详情失败')
  281. this.visible = false
  282. } finally {
  283. this.formLoading = false
  284. }
  285. }
  286. },
  287. handleClose() {
  288. this.visible = false
  289. this.$refs.form.resetFields()
  290. this.form = this.initForm()
  291. },
  292. handleTargetTypeChange() {
  293. this.form.targetCodeList = []
  294. this.form.targetNames = ''
  295. },
  296. handleTargetChange(names) {
  297. this.form.targetNames = names
  298. },
  299. setCron(cron) {
  300. this.form.cronExpression = cron
  301. },
  302. showCronHelp() {
  303. this.cronHelpVisible = true
  304. },
  305. submitForm() {
  306. this.$refs.form.validate(async (valid) => {
  307. if (!valid) return
  308. // 自动巡检校验
  309. if (this.form.planType === 2) {
  310. if (!this.form.rules || this.form.rules.length === 0) {
  311. this.$modal.msgWarning('自动巡检请至少配置一条巡检规则')
  312. return
  313. }
  314. if (this.form.scheduleEnabled === 1 && !this.form.cronExpression) {
  315. this.$modal.msgWarning('启用调度时请配置Cron表达式')
  316. return
  317. }
  318. }
  319. const data = {
  320. ...this.form,
  321. targetCodes: JSON.stringify(this.form.targetCodeList)
  322. }
  323. this.submitting = true
  324. try {
  325. const isEdit = !!this.form.id
  326. if (isEdit) {
  327. await updatePlan(data)
  328. } else {
  329. await addPlan(data)
  330. }
  331. // 如果是自动巡检且启用了调度,同步注册到调度器
  332. if (this.form.planType === 2 && this.form.scheduleEnabled === 1 && this.form.cronExpression) {
  333. try {
  334. await registerPlan(this.form.planCode || data.planCode)
  335. } catch (e) {
  336. console.warn('注册调度器失败,但计划已保存', e)
  337. }
  338. }
  339. this.$modal.msgSuccess(isEdit ? '修改成功' : '新增成功')
  340. this.handleClose()
  341. this.$emit('success')
  342. } catch (err) {
  343. this.$modal.msgError(err.message || '操作失败')
  344. } finally {
  345. this.submitting = false
  346. }
  347. })
  348. }
  349. }
  350. }
  351. </script>
  352. <style scoped>
  353. .divider-title {
  354. font-weight: bold;
  355. color: #409EFF;
  356. font-size: 14px;
  357. }
  358. .cron-preview {
  359. margin-top: 5px;
  360. font-size: 12px;
  361. color: #67C23A;
  362. }
  363. .schedule-tip {
  364. margin-left: 10px;
  365. font-size: 12px;
  366. color: #E6A23C;
  367. }
  368. .cron-help h4 {
  369. margin-bottom: 10px;
  370. color: #303133;
  371. }
  372. .cron-help code {
  373. background: #f5f7fa;
  374. padding: 2px 6px;
  375. border-radius: 3px;
  376. color: #409EFF;
  377. }
  378. </style>