浏览代码

Merge branch 'dev-ly' into dev-cmn

cmy 1 天之前
父节点
当前提交
720b3f53b5
共有 37 个文件被更改,包括 1504 次插入16 次删除
  1. 7 0
      src/api/marketing/data.ts
  2. 3 1
      src/layout/routerView/parent.vue
  3. 24 4
      src/views/home/user-info.vue
  4. 14 3
      src/views/marketing/data/dataListModal.vue
  5. 10 2
      src/views/marketing/data/index.vue
  6. 1 1
      src/views/marketing/rules/components/Edit.vue
  7. 101 5
      src/views/marketing/rules/index.vue
  8. 182 0
      src/views/system/client/form.vue
  9. 45 0
      src/views/system/client/i18n/en.ts
  10. 43 0
      src/views/system/client/i18n/zh-cn.ts
  11. 154 0
      src/views/system/client/index.vue
  12. 184 0
      src/views/system/daemon/job-manage/form.vue
  13. 94 0
      src/views/system/daemon/job-manage/i18n/en.ts
  14. 83 0
      src/views/system/daemon/job-manage/i18n/zh-cn.ts
  15. 284 0
      src/views/system/daemon/job-manage/index.vue
  16. 125 0
      src/views/system/daemon/job-manage/job-log.vue
  17. 0 0
      src/views/system/dict/dictItem/form.vue
  18. 0 0
      src/views/system/dict/dictItem/index.vue
  19. 0 0
      src/views/system/dict/form.vue
  20. 0 0
      src/views/system/dict/i18n/en.ts
  21. 0 0
      src/views/system/dict/i18n/zh-cn.ts
  22. 0 0
      src/views/system/dict/index.vue
  23. 0 0
      src/views/system/file/form.vue
  24. 0 0
      src/views/system/file/i18n/en.ts
  25. 0 0
      src/views/system/file/i18n/zh-cn.ts
  26. 0 0
      src/views/system/file/index.vue
  27. 0 0
      src/views/system/logs/detail.vue
  28. 0 0
      src/views/system/logs/i18n/en.ts
  29. 0 0
      src/views/system/logs/i18n/zh-cn.ts
  30. 0 0
      src/views/system/logs/index.vue
  31. 0 0
      src/views/system/param/form.vue
  32. 0 0
      src/views/system/param/i18n/en.ts
  33. 0 0
      src/views/system/param/i18n/zh-cn.ts
  34. 0 0
      src/views/system/param/index.vue
  35. 14 0
      src/views/system/token/i18n/en.ts
  36. 14 0
      src/views/system/token/i18n/zh-cn.ts
  37. 122 0
      src/views/system/token/index.vue

+ 7 - 0
src/api/marketing/data.ts

@@ -16,4 +16,11 @@ export const getDataPage = (data) => {
     data: data,
 	});
 };
+//统计累计用户数和活跃用户数
+export const getUserNum = () => {
+	return request({
+		url: 'marketing/tcp/statistics/users',
+		method: 'get',
+	});
+};
 

+ 3 - 1
src/layout/routerView/parent.vue

@@ -46,7 +46,9 @@ const state = reactive<ParentViewState>({
 });
 const menuLeftList = computed(() => { 
 	const parentName = route.path.split('/')[1];
-	const parentRoute = routeStores.routesList.filter((item: RouteItem) => item.path.includes(parentName));
+	const parentRoute = routeStores.routesList.filter((item: RouteItem) => {
+		return item.path.includes(parentName)
+	});
 	return parentRoute[0].children || [];
 });
 console.log(menuLeftList.value, '移动');

+ 24 - 4
src/views/home/user-info.vue

@@ -34,8 +34,8 @@
       </div>
       <div class="task-list">
         <div class="task-item">
-          <div class="task-title">{{t('home.pendingTasks')}}</div>
-          <div class="task-num">0</div>
+          <div class="task-title">全部用户</div>
+          <div class="task-num">{{ totalUsers }}</div>
           <svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
             <path d="M3.125 11.875H26.875V25.625C26.875 26.3154 26.3154 26.875 25.625 26.875H4.375C3.68464 26.875 3.125 26.3154 3.125 25.625V11.875Z" stroke="#167AF0" stroke-width="2" stroke-linejoin="round"/>
             <path d="M3.125 6.25C3.125 5.55964 3.68464 5 4.375 5H25.625C26.3154 5 26.875 5.55964 26.875 6.25V11.875H3.125V6.25Z" stroke="#167AF0" stroke-width="2" stroke-linejoin="round"/>
@@ -45,8 +45,8 @@
           </svg>
         </div>
         <div class="task-item">
-          <div class="task-title">{{t('home.ccTasks')}}</div>
-          <div class="task-num">0</div>
+          <div class="task-title">在线用户</div>
+          <div class="task-num">{{ activeUsers }}</div>
           <svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
             <path d="M26.875 3.125L18.5625 26.875L13.8125 16.1875L3.125 11.4375L26.875 3.125Z" stroke="#167AF0" stroke-width="2" stroke-linejoin="round"/>
             <path d="M26.8751 3.125L13.8126 16.1875" stroke="#167AF0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
@@ -60,12 +60,32 @@
 <script setup lang="ts">
 import {useUserInfo} from '/@/stores/userInfo';
 import { useI18n } from 'vue-i18n';
+import { getUserNum } from '/@/api/marketing/data';
 
 const { t } = useI18n();
 
 const { user } = useUserInfo().userInfos;
 console.log('user', user);
 
+const getUserTime = ref(0);
+const totalUsers = ref('0')
+const activeUsers = ref('0')
+onMounted(() => {
+  getUserNum().then((res) => {
+    totalUsers.value = res.data.totalUsers;
+    activeUsers.value = res.data.activeUsers;
+  });
+  getUserTime.value = setInterval(() => {
+    getUserNum().then((res) => {
+      totalUsers.value = res.data.totalUsers;
+      activeUsers.value = res.data.activeUsers;
+    });
+  },  10* 60* 1000)
+});
+
+onUnmounted(() => {
+  clearInterval(getUserTime.value);
+})
 
 const errorHandler = () => {
   return './src/assets/avatar.png'

+ 14 - 3
src/views/marketing/data/dataListModal.vue

@@ -1,6 +1,6 @@
 <template>
   <!-- 弹窗组件 -->
-  <el-dialog v-model="dialogVisible" title="数据信息(23)" width="860px" @close="onClose">
+  <el-dialog v-model="dialogVisible" :title="`数据信息(${pagination.total})`" width="860px" @close="onClose">
     <!-- 弹窗头部:搜索区域 -->
     <div class="dialog-header">
       <el-form :inline="true" :model="searchForm" @keyup.enter="handleSearch">
@@ -40,7 +40,7 @@
           title="点击查看数据详情"
           @click="handleOpenDialog(item)"
         >
-          <div class="rich-content" v-html="item.msgData"></div>
+          <div class="rich-content" v-html="highlightKeyword(item.msgData, searchForm.keyWord)"></div>
           <div class="item-time">{{ formatTime(item.reportTime) }}</div>
         </div>
         
@@ -84,7 +84,12 @@ const handleOpenDialog = (item: any) => {
   }
 )
 }
-
+const highlightKeyword = (content: string, keyword: string) => {
+  if (!keyword) return content;
+  
+  const regex = new RegExp(`(${keyword})`, 'gi');
+  return content.replace(regex, '<span style="color: red">$1</span>');
+}
 // 弹窗可见性
 const dialogVisible = ref(false)
 
@@ -260,4 +265,10 @@ defineExpose({
 :deep(.el-pagination) {
   margin: 0;
 }
+.highlight-keyword {
+  background-color: #ffff00;
+  color: #000;
+  font-weight: bold;
+  padding: 1px 2px;
+}
 </style>

+ 10 - 2
src/views/marketing/data/index.vue

@@ -27,7 +27,7 @@
         <el-table-column label="IP" prop="updateTime" show-overflow-tooltip>
           <template #default="{ row }">{{ getIp(row.deviceInfo || '{}') }}</template>
         </el-table-column>
-        <el-table-column label="设备信息" prop="deviceInfo" show-overflow-tooltip>
+        <el-table-column label="设备信息" min-width="150" prop="deviceInfo" show-overflow-tooltip>
           <template #header>
             <div class="header-wrapper">
               <span>设备信息</span>
@@ -42,7 +42,7 @@
           </template>
           <template #default="{ row }">
             <div @click="showJsonData(row.deviceInfo)">
-              {{ row.deviceInfo }}
+              {{ getDeviceName(row.deviceInfo) }}
             </div>
           </template>
         </el-table-column>
@@ -117,6 +117,14 @@ const getIp = (jsonString: string) => {
   return ipv4Match ? ipv4Match[1] : null;
 } 
 
+const getDeviceName = (jsonString: string) => {
+  const nameMatch = jsonString.match(/"name"\s*:\s*"([^"]+)"/);
+  const systemNameMatch = jsonString.match(/"systemName"\s*:\s*"([^"]+)"/);
+  const name = nameMatch ? nameMatch[1] : 'unKnown';
+  const systemName = systemNameMatch ? systemNameMatch[1] : 'unKnown';
+  return `设备名称:${name}; 系统名称:${systemName}`
+}
+
 const timeRange = computed({
   get() {
     return [state.queryForm.startTime, state.queryForm.endTime]

+ 1 - 1
src/views/marketing/rules/components/Edit.vue

@@ -3,7 +3,7 @@
 		<div class="p-4 rounded overflow-y-auto" style="max-height: calc(100vh - 350px)">
 			<div class="flex items-start">
                 <label class="w-[50px] leading-8">关键字</label>
-				<div class="flex gap-2 ml-2 flex-wrap w-[59vw]">
+				<div class="flex gap-2 ml-2 flex-wrap w-57vw]">
 					<el-tag v-for="tag in dynamicTags" :key="tag" size="large" closable :disable-transitions="false" @close="handleClose(tag)">
 						{{ tag }}
 					</el-tag>

+ 101 - 5
src/views/marketing/rules/index.vue

@@ -20,20 +20,23 @@
 			</el-row>
 		
 			<el-table
-				:data="state.dataList"
+				:data="tableData"
 				v-loading="state.loading"
 				border
 				:cell-style="tableStyle.cellStyle"
 				:header-cell-style="tableStyle.headerCellStyle"
 			>
 				<el-table-column label="规则名称" prop="title" show-overflow-tooltip></el-table-column>
-				<el-table-column label="关键字" prop="remoteAddr" show-overflow-tooltip></el-table-column>
-				<el-table-column label="IP" prop="method" show-overflow-tooltip></el-table-column>
-				<el-table-column label="域名" prop="createTime" show-overflow-tooltip width="200"></el-table-column>
-				<el-table-column label="触发规则" prop="createBy" show-overflow-tooltip width="200"></el-table-column>
+				<el-table-column label="关键字" prop="keywords" show-overflow-tooltip></el-table-column>
+				<el-table-column label="IP" prop="ip" show-overflow-tooltip></el-table-column>
+				<el-table-column label="域名" prop="domain" show-overflow-tooltip width="200"></el-table-column>
+				<el-table-column label="触发规则" prop="rules" show-overflow-tooltip width="200"></el-table-column>
 				<el-table-column label="备注" prop="createBy" show-overflow-tooltip width="200"></el-table-column>
 				<el-table-column label="操作" width="150">
 					<template #default="scope">
+						<el-button @click="openEdit({})" size="small" text type="primary">
+							新增
+						</el-button>
 						<el-button @click="openEdit(scope.row)" size="small" text type="primary">
 							编辑
 						</el-button>
@@ -73,6 +76,99 @@ const state: BasicTableProps = reactive<BasicTableProps>({
 	descs: ['create_time'],
 });
 
+const tableData = ref([
+  {
+    title: '系统安全检测',
+    keywords: '安全,检测,漏洞',
+    ip: '192.168.1.2',
+    domain: 'www.google.com',
+    rules: '基础安全扫描规则',
+    createBy: '张三',
+    createTime: '2021-02-15 09:30:00'
+  },
+  {
+    title: '性能压力测试',
+    keywords: '性能,压力,并发',
+    ip: '192.168.1.3',
+    domain: 'www.github.com',
+    rules: '高并发压力测试规则',
+    createBy: '李四',
+    createTime: '2021-03-20 14:15:00'
+  },
+  {
+    title: '接口兼容性测试',
+    keywords: '接口,兼容,API',
+    ip: '192.168.1.4',
+    domain: 'www.api.com',
+    rules: '接口版本兼容规则',
+    createBy: '王五',
+    createTime: '2021-04-05 10:45:00'
+  },
+  {
+    title: '数据备份验证',
+    keywords: '数据,备份,验证',
+    ip: '192.168.1.5',
+    domain: 'www.backup.com',
+    rules: '数据完整性校验规则',
+    createBy: '赵六',
+    createTime: '2021-05-18 16:20:00'
+  },
+  {
+    title: '用户体验测试',
+    keywords: '用户,体验,UI',
+    ip: '192.168.1.6',
+    domain: 'www.ux.com',
+    rules: '界面交互测试规则',
+    createBy: '孙七',
+    createTime: '2021-06-30 11:10:00'
+  },
+  {
+    title: '权限控制测试',
+    keywords: '权限,控制,安全',
+    ip: '192.168.1.7',
+    domain: 'www.auth.com',
+    rules: 'RBAC权限验证规则',
+    createBy: '周八',
+    createTime: '2021-07-12 15:50:00'
+  },
+  {
+    title: '数据库性能测试',
+    keywords: '数据库,性能,查询',
+    ip: '192.168.1.8',
+    domain: 'www.db.com',
+    rules: 'SQL执行效率测试规则',
+    createBy: '吴九',
+    createTime: '2021-08-25 08:35:00'
+  },
+  {
+    title: '移动端适配测试',
+    keywords: '移动,适配,响应式',
+    ip: '192.168.1.9',
+    domain: 'www.mobile.com',
+    rules: '多终端适配测试规则',
+    createBy: '郑十',
+    createTime: '2021-09-10 13:25:00'
+  },
+  {
+    title: '日志分析测试',
+    keywords: '日志,分析,监控',
+    ip: '192.168.1.10',
+    domain: 'www.log.com',
+    rules: '日志完整性检查规则',
+    createBy: '钱十一',
+    createTime: '2021-10-01 17:40:00'
+  },
+  {
+    title: '缓存机制测试',
+    keywords: '缓存,机制,性能',
+    ip: '192.168.1.11',
+    domain: 'www.cache.com',
+    rules: '缓存有效性验证规则',
+    createBy: '孙十二',
+    createTime: '2021-11-15 10:15:00'
+  }
+]);
+
 //  table hook
 const { downBlobFile, getDataList, currentChangeHandle, sortChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
 

+ 182 - 0
src/views/system/client/form.vue

@@ -0,0 +1,182 @@
+<template>
+  <el-dialog :close-on-click-modal="false" :title="form.id ? $t('common.editBtn') : $t('common.addBtn')" width="600"
+             draggable v-model="visible">
+    <el-form :model="form" :rules="dataRules" formDialogRef label-width="120px" ref="dataFormRef" v-loading="loading">
+      <el-form-item :label="t('client.clientId')" prop="clientId">
+        <el-input :placeholder="t('client.inputClientIdTip')" v-model="form.clientId"/>
+      </el-form-item>
+      <el-form-item :label="t('client.clientSecret')" prop="clientSecret">
+        <el-input :placeholder="t('client.inputClientSecretTip')" v-model="form.clientSecret"/>
+      </el-form-item>
+      <el-form-item :label="t('client.scope')" prop="scope">
+        <el-input :placeholder="t('client.inputScopeTip')" v-model="form.scope"/>
+      </el-form-item>
+      <el-form-item :label="t('client.authorizedGrantTypes')" prop="authorizedGrantTypes">
+        <el-select collapse-tags collapse-tags-tooltip multiple v-model="form.authorizedGrantTypes">
+          <el-option :key="index" :label="item.label" :value="item.value"
+                     v-for="(item, index) in grant_types"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item :label="t('client.accessTokenValidity')" prop="accessTokenValidity">
+        <el-input-number :placeholder="t('client.inputAccessTokenValidityTip')" v-model="form.accessTokenValidity"/>
+      </el-form-item>
+      <el-form-item :label="t('client.refreshTokenValidity')" prop="refreshTokenValidity">
+        <el-input-number :placeholder="t('client.inputRefreshTokenValidityTip')" v-model="form.refreshTokenValidity"/>
+      </el-form-item>
+      <el-form-item :label="t('client.autoapprove')" prop="autoapprove"
+                    v-if="form.authorizedGrantTypes.includes('authorization_code')">
+        <el-radio-group v-model="form.autoapprove">
+          <el-radio :key="index" :value="item.value" border v-for="(item, index) in common_status">{{
+              item.label
+            }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item :label="t('client.authorities')" prop="authorities"
+                    v-if="form.authorizedGrantTypes.includes('authorization_code')">
+        <el-input :placeholder="t('client.inputAuthoritiesTip')" v-model="form.authorities"/>
+      </el-form-item>
+
+      <el-form-item :label="t('client.webServerRedirectUri')" prop="webServerRedirectUri"
+                    v-if="form.authorizedGrantTypes.includes('authorization_code')">
+        <el-input :placeholder="t('client.inputWebServerRedirectUriTip')" v-model="form.webServerRedirectUri"/>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+			<span class="dialog-footer">
+				<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
+				<el-button @click="onSubmit" type="primary" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
+			</span>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts" name="SysOauthClientDetailsDialog" setup>
+import {useDict} from '/@/hooks/dict';
+import {useMessage} from '/@/hooks/message';
+import {addObj, getObj, putObj, validateclientId} from '/@/api/admin/client';
+import {useI18n} from 'vue-i18n';
+import {rule} from '/@/utils/validate';
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['refresh']);
+
+const {t} = useI18n();
+
+// 定义变量内容
+const dataFormRef = ref();
+const visible = ref(false);
+const loading = ref(false);
+
+// 定义字典
+const {grant_types, common_status} = useDict(
+    'grant_types',
+    'common_status',
+);
+
+// 提交表单数据
+const form = reactive({
+  id: '',
+  clientId: '',
+  clientSecret: '',
+  scope: 'server',
+  authorizedGrantTypes: [] as string[],
+  webServerRedirectUri: '',
+  authorities: '',
+  accessTokenValidity: 43200,
+  refreshTokenValidity: 2592001,
+  autoapprove: 'true',
+  delFlag: '',
+  createBy: '',
+  updateBy: '',
+  createTime: '',
+  updateTime: '',
+  tenantId: '',
+  onlineQuantity: '1',
+  captchaFlag: '1',
+  encFlag: '1',
+});
+
+// 定义校验规则
+const dataRules = ref({
+  clientId: [
+    {validator: rule.overLength, trigger: 'blur'},
+    {required: true, message: '编号不能为空', trigger: 'blur'},
+    {validator: rule.validatorLowercase, trigger: 'blur'},
+    {
+      validator: (rule: any, value: any, callback: any) => {
+        validateclientId(rule, value, callback, form.id !== '');
+      },
+      trigger: 'blur',
+    },
+  ],
+  clientSecret: [
+    {validator: rule.overLength, trigger: 'blur'},
+    {required: true, message: '密钥不能为空', trigger: 'blur'},
+    {validator: rule.validatorLower, trigger: 'blur'},
+  ],
+  scope: [{validator: rule.overLength, trigger: 'blur'},{required: true, message: '域不能为空', trigger: 'blur'}],
+  authorizedGrantTypes: [{required: true, message: '授权模式不能为空', trigger: 'blur'}],
+  accessTokenValidity: [
+    {validator: rule.overLength, trigger: 'blur'},
+    {required: true, message: '令牌时效不能为空', trigger: 'blur'},
+    {type: 'number', min: 1, message: '令牌时效不能小于一小时', trigger: 'blur'},
+  ],
+  refreshTokenValidity: [
+    {validator: rule.overLength, trigger: 'blur'},
+    {required: true, message: '刷新时效不能为空', trigger: 'blur'},
+    {type: 'number', min: 1, message: '刷新时效不能小于两小时', trigger: 'blur'},
+  ],
+  autoapprove: [{required: true, message: '自动放行不能为空', trigger: 'blur'}],
+  webServerRedirectUri: [{validator: rule.overLength, trigger: 'blur'},{required: true, message: '回调地址不能为空', trigger: 'blur'}],
+  authorities: [{validator: rule.overLength, trigger: 'blur'}],
+});
+
+// 打开弹窗
+const openDialog = (id: string) => {
+  visible.value = true;
+  form.id = '';
+  // 重置表单数据
+  nextTick(() => {
+    dataFormRef.value?.resetFields();
+  });
+
+  // 获取sysOauthClientDetails信息
+  if (id) {
+    form.id = id;
+    getsysOauthClientDetailsData(id);
+  }
+};
+
+// 提交
+const onSubmit = async () => {
+  const valid = await dataFormRef.value.validate().catch(() => {
+  });
+  if (!valid) return false;
+
+  try {
+    loading.value = true;
+    form.id ? await putObj(form) : await addObj(form);
+    useMessage().success(t(form.id ? 'common.editSuccessText' : 'common.addSuccessText'));
+    visible.value = false;
+    emit('refresh');
+  } catch (err: any) {
+    useMessage().error(err.msg);
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 初始化表单数据
+const getsysOauthClientDetailsData = (id: string) => {
+  // 获取数据
+  getObj(id).then((res: any) => {
+    Object.assign(form, res.data);
+  });
+};
+
+// 暴露变量
+defineExpose({
+  openDialog,
+});
+</script>

+ 45 - 0
src/views/system/client/i18n/en.ts

@@ -0,0 +1,45 @@
+export default {
+	client: {
+		index: '#',
+		importsysOauthClientDetailsTip: 'import SysOauthClientDetails',
+		id: 'id',
+		clientId: 'client id',
+		resourceIds: 'resourceIds',
+		clientSecret: 'client secret',
+		scope: 'scope',
+		authorizedGrantTypes: 'authorizedGrantTypes',
+		webServerRedirectUri: 'webServerRedirectUri',
+		authorities: 'authorities',
+		accessTokenValidity: 'accessTokenValidity',
+		refreshTokenValidity: 'refreshTokenValidity',
+		additionalInformation: 'additionalInformation',
+		autoapprove: 'autoapprove',
+		delFlag: 'delFlag',
+		createBy: 'createBy',
+		updateBy: 'updateBy',
+		createTime: 'createTime',
+		updateTime: 'updateTime',
+		tenantId: 'tenantId',
+		captchaFlag: 'captchaFlag',
+		encFlag: 'encFlag',
+		onlineQuantity: 'onlineQuantity',
+		inputIdTip: 'input id',
+		inputClientIdTip: 'input clientId',
+		inputResourceIdsTip: 'input resourceIds',
+		inputClientSecretTip: 'input clientSecret',
+		inputScopeTip: 'input scope',
+		inputAuthorizedGrantTypesTip: 'input authorizedGrantTypes',
+		inputWebServerRedirectUriTip: 'input webServerRedirectUri',
+		inputAuthoritiesTip: 'input authorities',
+		inputAccessTokenValidityTip: 'input accessTokenValidity',
+		inputRefreshTokenValidityTip: 'input refreshTokenValidity',
+		inputAdditionalInformationTip: 'input additionalInformation',
+		inputAutoapproveTip: 'input autoapprove',
+		inputDelFlagTip: 'input delFlag',
+		inputCreateByTip: 'input createBy',
+		inputUpdateByTip: 'input updateBy',
+		inputCreateTimeTip: 'input createTime',
+		inputUpdateTimeTip: 'input updateTime',
+		inputTenantIdTip: 'input tenantId',
+	},
+};

+ 43 - 0
src/views/system/client/i18n/zh-cn.ts

@@ -0,0 +1,43 @@
+export default {
+	client: {
+		index: '#',
+		importsysOauthClientDetailsTip: '导入终端信息表',
+		id: 'ID',
+		clientId: '客户端ID',
+		resourceIds: '',
+		clientSecret: '客户端密钥',
+		scope: '域',
+		authorizedGrantTypes: '授权模式',
+		webServerRedirectUri: '回调地址',
+		authorities: '权限',
+		accessTokenValidity: '令牌时效(秒)',
+		refreshTokenValidity: '刷新时效(秒)',
+		additionalInformation: '扩展信息',
+		autoapprove: '自动放行',
+		createBy: '创建人',
+		updateBy: '修改人',
+		createTime: '上传时间',
+		updateTime: '更新时间',
+		tenantId: '所属租户',
+		captchaFlag: '验证码开关',
+		encFlag: '前端密码加密',
+		onlineQuantity: '允许同时在线',
+		inputIdTip: '请输入ID',
+		inputClientIdTip: '请输入客户端id',
+		inputResourceIdsTip: '请输入',
+		inputClientSecretTip: '请输入密钥',
+		inputScopeTip: '请输入域',
+		inputAuthorizedGrantTypesTip: '请输入授权模式',
+		inputWebServerRedirectUriTip: '请输入回调地址',
+		inputAuthoritiesTip: '请输入权限',
+		inputAccessTokenValidityTip: '请输入令牌时效',
+		inputRefreshTokenValidityTip: '请输入刷新时效',
+		inputAdditionalInformationTip: '请输入扩展信息',
+		inputAutoapproveTip: '请输入自动放行',
+		inputCreateByTip: '请输入创建人',
+		inputUpdateByTip: '请输入修改人',
+		inputCreateTimeTip: '请输入上传时间',
+		inputUpdateTimeTip: '请输入更新时间',
+		inputTenantIdTip: '请输入所属租户',
+	},
+};

+ 154 - 0
src/views/system/client/index.vue

@@ -0,0 +1,154 @@
+<template>
+	<div class="layout-padding">
+		<div class="layout-padding-auto layout-padding-view">
+			<el-row class="ml10" v-show="showSearch">
+				<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
+					<el-form-item :label="$t('client.clientId')" prop="clientId">
+						<el-input :placeholder="$t('client.clientId')" style="max-width: 180px" v-model="state.queryForm.clientId" />
+					</el-form-item>
+					<el-form-item :label="$t('client.clientSecret')" prop="clientSecret">
+						<el-input :placeholder="$t('client.clientSecret')" style="max-width: 180px" v-model="state.queryForm.clientSecret" />
+					</el-form-item>
+					<el-form-item>
+						<el-button @click="getDataList" icon="search" type="primary">
+							{{ $t('common.queryBtn') }}
+						</el-button>
+						<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
+					</el-form-item>
+				</el-form>
+			</el-row>
+			<el-row>
+				<div class="mb8" style="width: 100%">
+					<el-button v-auth="'sys_client_add'" @click="formDialogRef.openDialog()" class="ml10" icon="folder-add" type="primary">
+						{{ $t('common.addBtn') }}
+					</el-button>
+					<el-button v-auth="'sys_client_del'" plain @click="handleRefreshCache()" class="ml10" icon="refresh-left" type="primary">
+						{{ $t('common.refreshCacheBtn') }}
+					</el-button>
+
+					<el-button plain :disabled="multiple" @click="handleDelete(selectObjs)" class="ml10" icon="Delete" type="primary" v-auth="'sys_client_del'">
+						{{ $t('common.delBtn') }}
+					</el-button>
+
+					<right-toolbar
+						:export="'sys_client_del'"
+						@exportExcel="exportExcel"
+						@queryTable="getDataList"
+						class="ml10"
+						style="float: right; margin-right: 20px"
+						v-model:showSearch="showSearch"
+					></right-toolbar>
+				</div>
+			</el-row>
+			<el-table
+				:data="state.dataList"
+				@selection-change="handleSelectionChange"
+				style="width: 100%"
+				v-loading="state.loading"
+				border
+				:cell-style="tableStyle.cellStyle"
+				:header-cell-style="tableStyle.headerCellStyle"
+			>
+				<el-table-column align="center" type="selection" width="40" />
+				<el-table-column :label="t('client.index')" type="index" width="60" />
+				<el-table-column :label="t('client.clientId')" prop="clientId" show-overflow-tooltip />
+				<el-table-column :label="t('client.clientSecret')" prop="clientSecret" show-overflow-tooltip />
+				<el-table-column :label="t('client.scope')" prop="scope" show-overflow-tooltip />
+				<el-table-column :label="t('client.authorizedGrantTypes')" prop="authorizedGrantTypes" show-overflow-tooltip width="400px">
+					<template #default="scope">
+						<dict-tag :options="grant_types" :value="scope.row.authorizedGrantTypes" />
+					</template>
+				</el-table-column>
+				<el-table-column :label="t('client.accessTokenValidity')" prop="accessTokenValidity" show-overflow-tooltip />
+				<el-table-column :label="t('client.refreshTokenValidity')" prop="refreshTokenValidity" show-overflow-tooltip />
+				<el-table-column :label="$t('common.action')" width="150">
+					<template #default="scope">
+						<el-button icon="edit-pen" @click="formDialogRef.openDialog(scope.row.clientId)" text type="primary" v-auth="'sys_client_add'"
+							>{{ $t('common.editBtn') }}
+						</el-button>
+						<el-button icon="delete" @click="handleDelete([scope.row.id])" text type="primary" v-auth="'sys_client_del'">
+							{{ $t('common.delBtn') }}
+						</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+			<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination" />
+		</div>
+		<!-- 编辑、新增  -->
+		<form-dialog @refresh="getDataList()" ref="formDialogRef" />
+	</div>
+</template>
+
+<script lang="ts" name="systemSysOauthClientDetails" setup>
+import { BasicTableProps, useTable } from '/@/hooks/table';
+import { delObj, fetchList, refreshCache } from '/@/api/admin/client';
+import { useMessage, useMessageBox } from '/@/hooks/message';
+import { useDict } from '/@/hooks/dict';
+import { useI18n } from 'vue-i18n';
+
+// 引入组件
+const FormDialog = defineAsyncComponent(() => import('./form.vue'));
+const { t } = useI18n();
+// 定义查询字典
+
+const { grant_types } = useDict('grant_types');
+// 定义变量内容
+const formDialogRef = ref();
+const queryRef = ref();
+// 搜索变量
+const showSearch = ref(true);
+// 多选变量
+const selectObjs = ref([]) as any;
+const multiple = ref(true);
+
+const state: BasicTableProps = reactive<BasicTableProps>({
+	queryForm: {},
+	pageList: fetchList,
+	descs: ['id'],
+});
+
+//  table hook
+const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
+
+// 删除缓存
+const handleRefreshCache = () => {
+	refreshCache().then(() => {
+		useMessage().success('同步成功');
+	});
+};
+
+const resetQuery = () => {
+	queryRef.value.resetFields();
+	// state.queryForm = {};
+	selectObjs.value = [];
+	getDataList();
+};
+
+// 导出excel
+const exportExcel = () => {
+	downBlobFile('/admin/client/export', state.queryForm, 'client.xlsx');
+};
+
+// 多选事件
+const handleSelectionChange = (objs: { id: string }[]) => {
+	selectObjs.value = objs.map(({ id }) => id);
+	multiple.value = !objs.length;
+};
+
+// 删除操作
+const handleDelete = async (ids: string[]) => {
+	try {
+		await useMessageBox().confirm(t('common.delConfirmText'));
+	} catch {
+		return;
+	}
+
+	try {
+		await delObj(ids);
+		getDataList();
+		useMessage().success(t('common.delSuccessText'));
+	} catch (err: any) {
+		useMessage().error(err.msg);
+	}
+};
+</script>

+ 184 - 0
src/views/system/daemon/job-manage/form.vue

@@ -0,0 +1,184 @@
+<template>
+	<el-dialog v-model="visible" :close-on-click-modal="false" :title="form.jobId ? $t('common.editBtn') : $t('common.addBtn')" draggable>
+		<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="120px" v-loading="loading">
+      <el-row :gutter="20">
+        <el-col :span="12" class="mb20">
+          <el-form-item :label="t('job.jobName')" prop="jobName">
+            <el-input v-model="form.jobName" :placeholder="t('job.inputjobNameTip')"/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12" class="mb20">
+          <el-form-item :label="t('job.jobGroup')" prop="jobGroup">
+            <el-input v-model="form.jobGroup" :placeholder="t('job.inputjobGroupTip')"/>
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12" class="mb20">
+          <el-form-item :label="t('job.jobType')" prop="jobType">
+            <el-select v-model="form.jobType" :placeholder="t('job.jobType')">
+              <el-option v-for="(item, index) in job_type" :key="index" :label="item.label"
+                         :value="item.value"></el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12" class="mb20" v-if="['3', '4'].includes(form.jobType)">
+          <el-form-item :label="t('job.executePath')" prop="executePath">
+            <el-input v-model="form.executePath" :placeholder="t('job.inputexecutePathTip')"/>
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12" class="mb20" v-if="['1', '2'].includes(form.jobType)">
+          <el-form-item :label="t('job.className')" prop="className">
+            <el-input v-model="form.className" :placeholder="t('job.inputclassNameTip')"/>
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12" class="mb20" v-if="['1', '2'].includes(form.jobType)">
+          <el-form-item :label="t('job.methodName')" prop="methodName">
+            <el-input v-model="form.methodName" :placeholder="t('job.inputmethodNameTip')"/>
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12" class="mb20">
+          <el-form-item :label="t('job.methodParamsValue')" prop="methodParamsValue">
+            <el-input v-model="form.methodParamsValue" :placeholder="t('job.inputmethodParamsValueTip')"/>
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12" class="mb20">
+          <el-form-item :label="t('job.cronExpression')" prop="cronExpression">
+            <crontab clearable @hide="popoverVis(false)" v-model="form.cronExpression"></crontab>
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12" class="mb20">
+          <el-form-item :label="t('job.misfirePolicy')" prop="misfirePolicy">
+            <el-select v-model="form.misfirePolicy" :placeholder="t('job.inputmisfirePolicyTip')">
+              <el-option v-for="(item, index) in misfire_policy" :key="index" :label="item.label"
+                         :value="item.value"></el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="24" class="mb20">
+          <el-form-item :label="t('job.remark')" prop="remark">
+            <el-input v-model="form.remark" :placeholder="t('job.inputremarkTip')" type="textarea"/>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+			<span class="dialog-footer">
+				<el-button formDialogRef @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
+				<el-button formDialogRef type="primary" @click="onSubmit" :disabled="loading">{{
+            $t('common.confirmButtonText')
+          }}</el-button>
+			</span>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts" name="SysJobDialog" setup>
+// 定义子组件向父组件传值/事件
+import {useDict} from '/@/hooks/dict';
+import {useMessage} from '/@/hooks/message';
+import {addObj, getObj, putObj} from '/@/api/daemon/job';
+import {useI18n} from 'vue-i18n';
+import {rule} from '/@/utils/validate';
+const emit = defineEmits(['refresh']);
+const Crontab = defineAsyncComponent(() => import('/@/components/Crontab/index.vue'));
+
+const {t} = useI18n();
+
+// 定义变量内容
+const dataFormRef = ref();
+const visible = ref(false);
+const loading = ref(false);
+
+// 定义字典
+const {misfire_policy, job_type} = useDict('job_status', 'job_execute_status', 'misfire_policy', 'job_type');
+
+// 提交表单数据
+const form = reactive({
+  jobId: '',
+  jobName: '',
+  jobGroup: '',
+  jobType: '',
+  executePath: '',
+  className: '',
+  methodName: '',
+  methodParamsValue: '',
+  cronExpression: '',
+  misfirePolicy: '',
+  jobStatus: '',
+  jobExecuteStatus: '',
+  remark: '',
+});
+
+const popoverVis = (bol: boolean) => {
+  popoverVisible.value = bol;
+};
+
+const popoverVisible = ref(false);
+// 定义校验规则
+const dataRules = reactive({
+  jobName: [{validator: rule.overLength, trigger: 'blur'},{required: true, message: '任务名称不能为空', trigger: 'blur'}],
+  jobGroup: [{validator: rule.overLength, trigger: 'blur'},{required: true, message: '任务组名不能为空', trigger: 'blur'}],
+  jobType: [{required: true, message: '任务类型不能为空', trigger: 'blur'}],
+  cronExpression: [{validator: rule.overLength, trigger: 'blur'},{required: true, message: 'cron不能为空', trigger: 'blur'}],
+  misfirePolicy: [{required: true, message: '策略不能为空', trigger: 'blur'}],
+  executePath: [{validator: rule.overLength, trigger: 'blur'},{required: true, message: '执行路径不能为空', trigger: 'blur'}],
+  className: [{validator: rule.overLength, trigger: 'blur'},{required: true, message: '执行文件不能为空', trigger: 'blur'}],
+  methodName: [{validator: rule.overLength, trigger: 'blur'},{required: true, message: '执行方法不能为空', trigger: 'blur'}],
+  methodParamsValue: [{validator: rule.overLength, trigger: 'blur'}],
+});
+
+// 打开弹窗
+const openDialog = (id: string) => {
+  visible.value = true;
+  form.jobId = '';
+
+  // 重置表单数据
+  nextTick(() => {
+    dataFormRef.value?.resetFields();
+  });
+
+  // 获取sysJob信息
+  if (id) {
+    form.jobId = id;
+    getsysJobData(id);
+  }
+};
+
+// 提交
+const onSubmit = async () => {
+  const valid = await dataFormRef.value.validate().catch(() => {
+  });
+  if (!valid) return false;
+
+  try {
+    loading.value = true;
+    form.jobId ? await putObj(form) : await addObj(form);
+    useMessage().success(t(form.jobId ? 'common.editSuccessText' : 'common.addSuccessText'));
+    visible.value = false;
+    emit('refresh');
+  } catch (err: any) {
+    useMessage().error('任务初始化异常');
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 初始化表单数据
+const getsysJobData = (id: string) => {
+  // 获取数据
+  getObj(id).then((res: any) => {
+    Object.assign(form, res.data);
+  });
+};
+
+// 暴露变量
+defineExpose({
+  openDialog,
+});
+</script>

+ 94 - 0
src/views/system/daemon/job-manage/i18n/en.ts

@@ -0,0 +1,94 @@
+export default {
+	job: {
+		index: '#',
+		importsysJobTip: 'import SysJob',
+		jobId: 'jobId',
+		jobName: 'jobName',
+		jobGroup: 'jobGroup',
+		jobOrder: 'jobOrder',
+		jobType: 'jobType',
+		executePath: 'executePath',
+		className: 'className',
+		methodName: 'methodName',
+		methodParamsValue: 'methodParamsValue',
+		cronExpression: 'cronExpression',
+		misfirePolicy: 'misfirePolicy',
+		jobTenantType: 'jobTenantType',
+		jobStatus: 'jobStatus',
+		jobExecuteStatus: 'jobExecuteStatus',
+		createBy: 'createBy',
+		createTime: 'createTime',
+		updateBy: 'updateBy',
+		updateTime: 'updateTime',
+		startTime: 'startTime',
+		previousTime: 'previousTime',
+		nextTime: 'nextTime',
+		tenantId: 'tenantId',
+		remark: 'remark',
+		jobMessage: 'jobMessage',
+		jobLogStatus: 'jobLogStatus',
+		executeTime: 'executeTime',
+		exceptionInfo: 'exceptionInfo',
+		inputjobIdTip: 'input jobId',
+		inputjobNameTip: 'input jobName',
+		inputjobGroupTip: 'input jobGroup',
+		inputjobOrderTip: 'input jobOrder',
+		inputjobTypeTip: 'input jobType',
+		inputexecutePathTip: 'input executePath',
+		inputclassNameTip: 'input className',
+		inputmethodNameTip: 'input methodName',
+		inputmethodParamsValueTip: 'input methodParamsValue',
+		inputcronExpressionTip: 'input cronExpression',
+		inputmisfirePolicyTip: 'input misfirePolicy',
+		inputjobTenantTypeTip: 'input jobTenantType',
+		inputjobStatusTip: 'input jobStatus',
+		inputjobExecuteStatusTip: 'input jobExecuteStatus',
+		inputcreateByTip: 'input createBy',
+		inputcreateTimeTip: 'input createTime',
+		inputupdateByTip: 'input updateBy',
+		inputupdateTimeTip: 'input updateTime',
+		inputstartTimeTip: 'input startTime',
+		inputpreviousTimeTip: 'input previousTime',
+		inputnextTimeTip: 'input nextTime',
+		inputtenantIdTip: 'input tenantId',
+		inputremarkTip: 'input remark',
+	},
+	log: {
+		index: '#',
+		importsysJobLogTip: 'import SysJobLog',
+		jobLogId: 'jobLogId',
+		jobId: 'jobId',
+		jobName: 'jobName',
+		jobGroup: 'jobGroup',
+		jobOrder: 'jobOrder',
+		jobType: 'jobType',
+		executePath: 'executePath',
+		className: 'className',
+		methodName: 'methodName',
+		methodParamsValue: 'methodParamsValue',
+		cronExpression: 'cronExpression',
+		jobMessage: 'jobMessage',
+		jobLogStatus: 'jobLogStatus',
+		executeTime: 'executeTime',
+		exceptionInfo: 'exceptionInfo',
+		createTime: 'createTime',
+		tenantId: 'tenantId',
+		inputJobLogIdTip: 'input jobLogId',
+		inputJobIdTip: 'input jobId',
+		inputJobNameTip: 'input jobName',
+		inputJobGroupTip: 'input jobGroup',
+		inputJobOrderTip: 'input jobOrder',
+		inputJobTypeTip: 'input jobType',
+		inputExecutePathTip: 'input executePath',
+		inputClassNameTip: 'input className',
+		inputMethodNameTip: 'input methodName',
+		inputMethodParamsValueTip: 'input methodParamsValue',
+		inputCronExpressionTip: 'input cronExpression',
+		inputJobMessageTip: 'input jobMessage',
+		inputJobLogStatusTip: 'input jobLogStatus',
+		inputExecuteTimeTip: 'input executeTime',
+		inputExceptionInfoTip: 'input exceptionInfo',
+		inputCreateTimeTip: 'input createTime',
+		inputTenantIdTip: 'input tenantId',
+	},
+};

+ 83 - 0
src/views/system/daemon/job-manage/i18n/zh-cn.ts

@@ -0,0 +1,83 @@
+export default {
+	job: {
+		index: '#',
+		importsysJobTip: '导入定时任务调度表',
+		jobId: '任务id',
+		jobName: '任务名称',
+		jobGroup: '任务组名',
+		jobOrder: '组内执行顺利',
+		jobType: '类型',
+		executePath: '执行路径',
+		className: '执行文件',
+		methodName: '执行方法',
+		methodParamsValue: '参数值',
+		cronExpression: 'cron表达式',
+		misfirePolicy: '错失执行策略',
+		jobTenantType: '1、多租户任务;2、非多租户任务',
+		jobStatus: '任务状态',
+		jobExecuteStatus: '执行状态',
+		createBy: '创建者',
+		createTime: '创建时间',
+		updateBy: '更新者',
+		updateTime: '更新时间',
+		startTime: '初次执行时间',
+		previousTime: '上次执行时间',
+		nextTime: '下次执行时间',
+		tenantId: '租户',
+		remark: '备注信息',
+		jobMessage: '日志信息',
+		jobLogStatus: '执行状态',
+		executeTime: '执行时间',
+		exceptionInfo: '异常信息',
+		inputjobIdTip: '请输入任务id',
+		inputjobNameTip: '请输入任务名称',
+		inputjobGroupTip: '请输入任务组名',
+		inputjobOrderTip: '请输入组内执行顺利',
+		inputjobTypeTip: '请输入类型',
+		inputexecutePathTip: '请输入执行路径',
+		inputclassNameTip: '请输入执行文件',
+		inputmethodNameTip: '请输入执行方法',
+		inputmethodParamsValueTip: '请输入参数值',
+		inputcronExpressionTip: '请输入cron表达式',
+		inputmisfirePolicyTip: '请输入错失执行策略',
+		inputjobTenantTypeTip: '请输入1、多租户任务;2、非多租户任务',
+		inputjobStatusTip: '请输入任务状态',
+		inputjobExecuteStatusTip: '请输入执行状态',
+		inputcreateByTip: '请输入创建者',
+		inputcreateTimeTip: '请输入创建时间',
+		inputupdateByTip: '请输入更新者',
+		inputupdateTimeTip: '请输入更新时间',
+		inputstartTimeTip: '请输入初次执行时间',
+		inputpreviousTimeTip: '请输入上次执行时间',
+		inputnextTimeTip: '请输入下次执行时间',
+		inputtenantIdTip: '请输入租户',
+		inputremarkTip: '请输入备注信息',
+	},
+	log: {
+		index: '#',
+		importsysJobLogTip: '导入定时任务执行日志表',
+		jobLogId: '任务日志ID',
+		jobId: '任务id',
+		jobName: '任务名称',
+		jobGroup: '任务组名',
+		jobMessage: '日志信息',
+		jobLogStatus: '执行状态',
+		executeTime: '执行时间',
+		exceptionInfo: '异常信息',
+		createTime: '创建时间',
+		tenantId: '租户id',
+		inputJobLogIdTip: '请输入任务日志ID',
+		inputJobIdTip: '请输入任务id',
+		inputJobNameTip: '请输入任务名称',
+		inputJobGroupTip: '请输入任务组名',
+		inputMethodNameTip: '请输入任务方法',
+		inputMethodParamsValueTip: '请输入参数值',
+		inputCronExpressionTip: '请输入cron执行表达式',
+		inputJobMessageTip: '请输入日志信息',
+		inputJobLogStatusTip: '请输入执行状态',
+		inputExecuteTimeTip: '请输入执行时间',
+		inputExceptionInfoTip: '请输入异常信息',
+		inputCreateTimeTip: '请输入创建时间',
+		inputTenantIdTip: '请输入租户id',
+	},
+};

+ 284 - 0
src/views/system/daemon/job-manage/index.vue

@@ -0,0 +1,284 @@
+<template>
+	<div class="layout-padding">
+		<div class="layout-padding-auto layout-padding-view">
+			<el-row class="ml10" v-show="showSearch">
+				<el-form :inline="true" :model="state.queryForm" ref="queryRef">
+					<el-form-item :label="$t('job.jobName')" prop="jobName">
+						<el-input :placeholder="$t('job.inputjobNameTip')" @keyup.enter="getDataList" clearable v-model="state.queryForm.jobName" />
+					</el-form-item>
+					<el-form-item :label="t('job.jobStatus')" prop="jobStatus">
+						<el-select :placeholder="t('job.inputjobStatusTip')" v-model="state.queryForm.jobStatus">
+							<el-option :key="index" :label="item.label" :value="item.value" v-for="(item, index) in job_status"></el-option>
+						</el-select>
+					</el-form-item>
+					<el-form-item :label="t('job.jobExecuteStatus')" prop="jobExecuteStatus">
+						<el-select :placeholder="t('job.inputjobExecuteStatusTip')" v-model="state.queryForm.jobExecuteStatus">
+							<el-option :key="index" :label="item.label" :value="item.value" v-for="(item, index) in job_execute_status"></el-option>
+						</el-select>
+					</el-form-item>
+
+					<el-form-item>
+						<el-button @click="getDataList" icon="Search" type="primary">{{ $t('common.queryBtn') }} </el-button>
+						<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
+					</el-form-item>
+				</el-form>
+			</el-row>
+			<el-row>
+				<div class="mb8" style="width: 100%">
+					<el-button v-auth="'job_sys_job_add'" @click="formDialogRef.openDialog()" class="ml10" icon="folder-add" type="primary">
+						{{ $t('common.addBtn') }}
+					</el-button>
+					<el-button plain v-auth="'job_sys_job_del'" :disabled="multiple" @click="handleDelete(undefined)" class="ml10" icon="Delete" type="primary">
+						{{ $t('common.delBtn') }}
+					</el-button>
+					<right-toolbar
+						:export="'job_sys_job_add'"
+						@exportExcel="exportExcel"
+						@queryTable="getDataList"
+						class="ml10"
+						style="float: right; margin-right: 20px"
+						v-model:showSearch="showSearch"
+					/>
+				</div>
+			</el-row>
+			<el-table
+				:data="state.dataList"
+				@selection-change="handleSelectionChange"
+				style="width: 100%"
+				v-loading="state.loading"
+				border
+				:cell-style="tableStyle.cellStyle"
+				:header-cell-style="tableStyle.headerCellStyle"
+			>
+				<el-table-column align="center" type="selection" width="40" />
+				<el-table-column :label="t('job.index')" fixed="left" type="index" width="60" />
+				<el-table-column :label="t('job.jobName')" fixed="left" prop="jobName" show-overflow-tooltip width="120" />
+				<el-table-column :label="t('job.jobGroup')" prop="jobGroup" show-overflow-tooltip width="120" />
+				<el-table-column :label="t('job.jobStatus')" prop="jobStatus" show-overflow-tooltip width="120">
+					<template #default="scope">
+						<dict-tag :options="job_status" :value="scope.row.jobStatus"></dict-tag>
+					</template>
+				</el-table-column>
+				<el-table-column :label="t('job.jobExecuteStatus')" prop="jobExecuteStatus" show-overflow-tooltip width="120">
+					<template #default="scope">
+						<dict-tag :options="job_execute_status" :value="scope.row.jobExecuteStatus"></dict-tag>
+					</template>
+				</el-table-column>
+
+				<el-table-column :label="t('job.startTime')" prop="startTime" show-overflow-tooltip width="120" />
+
+				<el-table-column :label="t('job.previousTime')" prop="previousTime" show-overflow-tooltip width="120" />
+				<el-table-column :label="t('job.nextTime')" prop="nextTime" show-overflow-tooltip width="120" />
+				<el-table-column :label="t('job.jobType')" prop="jobType" show-overflow-tooltip width="120">
+					<template #default="scope">
+						<dict-tag :options="job_type" :value="scope.row.jobType"></dict-tag>
+					</template>
+				</el-table-column>
+				<el-table-column :label="t('job.executePath')" prop="executePath" show-overflow-tooltip width="120" />
+				<el-table-column :label="t('job.className')" prop="className" show-overflow-tooltip width="120" />
+				<el-table-column :label="t('job.methodName')" prop="methodName" show-overflow-tooltip width="120" />
+				<el-table-column :label="t('job.methodParamsValue')" prop="methodParamsValue" show-overflow-tooltip width="120" />
+				<el-table-column :label="t('job.cronExpression')" prop="cronExpression" show-overflow-tooltip width="120" />
+				<el-table-column :label="t('job.misfirePolicy')" prop="misfirePolicy" show-overflow-tooltip width="200">
+					<template #default="scope">
+						<dict-tag :options="misfire_policy" :value="scope.row.misfirePolicy"></dict-tag>
+					</template>
+				</el-table-column>
+
+				<el-table-column :label="$t('common.action')" fixed="right" width="300">
+					<template #default="scope">
+						<el-button @click="handleJobLog(scope.row)" text type="primary">日志</el-button>
+
+						<el-button v-auth="'job_sys_job_start_job'" @click="handleStartJob(scope.row)" text type="primary" v-if="scope.row.jobStatus !== '2'"
+							>启动
+						</el-button>
+
+						<el-button
+							v-auth="'job_sys_job_shutdown_job'"
+							@click="handleShutDownJob(scope.row)"
+							text
+							type="primary"
+							v-if="scope.row.jobStatus === '2'"
+							>暂停
+						</el-button>
+
+						<el-button v-auth="'job_sys_job_edit'" @click="handleEditJob(scope.row)" text type="primary">{{ $t('common.editBtn') }} </el-button>
+
+						<el-button v-auth="'job_sys_job_start_job'" @click="handleRunJob(scope.row)" text type="primary">执行</el-button>
+
+						<el-button v-auth="'job_sys_job_del'" @click="handleDelete(scope.row)" text type="primary">{{ $t('common.delBtn') }} </el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+			<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination" />
+		</div>
+
+		<!-- 编辑、新增  -->
+		<form-dialog @refresh="getDataList()" ref="formDialogRef" />
+		<job-log ref="jobLogRef"></job-log>
+	</div>
+</template>
+
+<script lang="ts" name="systemSysJob" setup>
+import { BasicTableProps, useTable } from '/@/hooks/table';
+import { delObj, fetchList, runJobRa, shutDownJobRa, startJobRa } from '/@/api/daemon/job';
+import { useMessage, useMessageBox } from '/@/hooks/message';
+import { useDict } from '/@/hooks/dict';
+import { useI18n } from 'vue-i18n';
+
+// 引入组件
+const FormDialog = defineAsyncComponent(() => import('./form.vue'));
+const JobLog = defineAsyncComponent(() => import('./job-log.vue'));
+
+// 获取国际化方法
+const { t } = useI18n();
+
+/** 表单弹窗引用 */
+const formDialogRef = ref();
+/** 作业日志引用 */
+const jobLogRef = ref();
+
+/** 搜索表单信息 */
+const queryForm = reactive({
+	jobName: '',
+	jobGroup: '',
+	jobStatus: '',
+	jobExecuteStatus: '',
+});
+/** 是否展示搜索表单 */
+const showSearch = ref(true);
+
+// 多选变量
+/** 选中的行 */
+const selectedRows = ref([]);
+/** 是否可以多选 */
+const multiple = ref(true);
+
+/** 查询字典 */
+const { job_status, job_execute_status, misfire_policy, job_type } = useDict('job_status', 'job_execute_status', 'misfire_policy', 'job_type');
+
+/** 表格状态变量 */
+const state = reactive<BasicTableProps>({
+	queryForm,
+	pageList: fetchList,
+});
+
+/** 获取表格数据方法 */
+const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
+
+/** 重置查询表单 */
+const resetQuery = () => {
+	Object.keys(queryForm).forEach((key) => (queryForm[key] = ''));
+	getDataList();
+};
+
+/** 行选中事件 */
+const handleSelectionChange = (rows: any) => {
+	selectedRows.value = rows;
+	multiple.value = !rows.length;
+};
+
+/** 导出Excel */
+const exportExcel = () => {
+	downBlobFile('/job/sys-job/export', state.queryForm, 'job.xlsx');
+};
+
+/** 查看作业日志 */
+const handleJobLog = (row) => {
+	jobLogRef.value.openDialog(row.jobId);
+};
+
+/** 编辑作业 */
+const handleEditJob = (row) => {
+	const jobStatus = row.jobStatus;
+	if (jobStatus === '1' || jobStatus === '3') {
+		formDialogRef.value.openDialog(row.jobId);
+	} else {
+		useMessage().error('运行中定时任务不可修改,请先暂停后操作');
+	}
+};
+
+/** 启动作业 */
+const handleStartJob = async (row) => {
+	const jobStatus = row.jobStatus;
+	if (jobStatus === '1' || jobStatus === '3') {
+		try {
+			await useMessageBox().confirm(`即将发布或启动(任务名称: ${row.jobName}), 是否继续?`);
+		} catch {
+			return;
+		}
+
+		try {
+			await startJobRa(row.jobId);
+			getDataList();
+			useMessage().success(t('common.optSuccessText'));
+		} catch (err: any) {
+			useMessage().error(err.msg);
+		}
+	} else {
+		useMessage().error('定时任务已运行');
+	}
+};
+
+/** 暂停作业 */
+const handleShutDownJob = async (row) => {
+	const jobStatus = row.jobStatus;
+	if (jobStatus === '2') {
+		try {
+			await useMessageBox().confirm(`即将暂停(任务名称: ${row.jobName}), 是否继续?`);
+		} catch {
+			return;
+		}
+
+		try {
+			await shutDownJobRa(row.jobId);
+			getDataList();
+			useMessage().success(t('common.optSuccessText'));
+		} catch (err: any) {
+			useMessage().error(err.msg);
+		}
+	} else {
+		useMessage().error('已暂停,不要重复操作');
+	}
+};
+
+/** 运行作业 */
+const handleRunJob = async (row) => {
+	try {
+		await useMessageBox().confirm(`立刻执行一次任务(任务名称: ${row.jobName}), 是否继续?`);
+	} catch {
+		return;
+	}
+
+	try {
+		await runJobRa(row.jobId);
+		getDataList();
+		useMessage().success(t('common.optSuccessText'));
+	} catch (err: any) {
+		useMessage().error('运行失败');
+	}
+};
+
+/** 删除操作 */
+const handleDelete = async (row) => {
+	if (!row) {
+		selectedRows.value.forEach(handleDelete);
+		return;
+	}
+
+	const { jobId, jobName } = row;
+	try {
+		await useMessageBox().confirm(`${t('common.delConfirmText')}(任务名称:${jobName})`);
+	} catch {
+		return;
+	}
+
+	try {
+		await delObj(jobId);
+		getDataList();
+		useMessage().success(t('common.delSuccessText'));
+	} catch (error: any) {
+		useMessage().error('删除失败');
+	}
+};
+</script>

+ 125 - 0
src/views/system/daemon/job-manage/job-log.vue

@@ -0,0 +1,125 @@
+<template>
+	<el-dialog v-model="visible" :close-on-click-modal="false" fullscreen title="运行日志" draggable>
+		<div class="layout-padding-auto layout-padding-view">
+			<el-row>
+				<div class="mb8" style="width: 100%">
+					<el-button
+						formDialogRef
+						:disabled="multiple"
+						icon="Delete"
+						type="primary"
+						class="ml10"
+						v-auth="'sys_log_del'"
+						@click="handleDelete(selectObjs)"
+					>
+						{{ $t('common.delBtn') }}
+					</el-button>
+					<right-toolbar
+						v-model:showSearch="showSearch"
+						class="ml10"
+						style="float: right; margin-right: 20px"
+						@queryTable="getDataList"
+					></right-toolbar>
+				</div>
+			</el-row>
+			<el-table
+				:data="state.dataList"
+				v-loading="state.loading"
+				style="width: 100%"
+				@selection-change="handleSelectionChange"
+				border
+				:cell-style="tableStyle.cellStyle"
+				:header-cell-style="tableStyle.headerCellStyle"
+			>
+				<el-table-column type="selection" width="40" align="center" />
+				<el-table-column type="index" :label="t('log.index')" width="80" />
+				<el-table-column prop="jobName" :label="t('log.jobName')" show-overflow-tooltip />
+				<el-table-column prop="jobMessage" :label="t('log.jobMessage')" show-overflow-tooltip />
+				<el-table-column prop="jobLogStatus" :label="t('log.jobLogStatus')" show-overflow-tooltip>
+					<template #default="scope">
+						<dict-tag :options="job_execute_status" :value="scope.row.jobLogStatus"></dict-tag>
+					</template>
+				</el-table-column>
+				<el-table-column prop="executeTime" :label="t('log.executeTime')" show-overflow-tooltip />
+				<el-table-column prop="exceptionInfo" :label="t('log.exceptionInfo')" show-overflow-tooltip />
+				<el-table-column prop="createTime" :label="t('log.createTime')" show-overflow-tooltip />
+				<el-table-column :label="$t('common.action')" width="150">
+					<template #default="scope">
+						<el-button text type="primary" v-auth="'pix_log_edit'" @click="formDialogRef.openDialog(scope.row.jobLogId)"
+							>{{ $t('common.editBtn') }}
+						</el-button>
+						<el-button text type="primary" v-auth="'sys_log_del'" @click="handleDelete([scope.row.jobLogId])">{{ $t('common.delBtn') }} </el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+			<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
+		</div>
+	</el-dialog>
+</template>
+
+<script setup lang="ts" name="job-log">
+import { BasicTableProps, useTable } from '/@/hooks/table';
+import { fetchList, delObjs } from '/@/api/daemon/job-log';
+import { useI18n } from 'vue-i18n';
+import { useDict } from '/@/hooks/dict';
+import { useMessage, useMessageBox } from '/@/hooks/message';
+
+const { t } = useI18n();
+const visible = ref(false);
+
+const { job_execute_status } = useDict('job_type', 'job_execute_status');
+
+// 定义变量内容
+const formDialogRef = ref();
+// 搜索变量
+const showSearch = ref(true);
+// 多选变量
+const selectObjs = ref([]) as any;
+const multiple = ref(true);
+
+const state: BasicTableProps = reactive<BasicTableProps>({
+	queryForm: {
+		jobId: '',
+	},
+	pageList: fetchList,
+	createdIsNeed: false,
+});
+
+const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
+
+const openDialog = (id: string) => {
+	visible.value = true;
+	state.queryForm.jobId = id;
+	getDataList();
+};
+
+// 多选事件
+const handleSelectionChange = (objs: { jobLogId: string }[]) => {
+	selectObjs.value = objs.map(({ jobLogId }) => jobLogId);
+	multiple.value = !objs.length;
+};
+
+// 删除操作
+const handleDelete = async (ids: string[]) => {
+	try {
+		await useMessageBox().confirm(t('common.delConfirmText'));
+	} catch {
+		return;
+	}
+
+	try {
+		await delObjs(ids);
+		getDataList();
+		useMessage().success(t('common.delSuccessText'));
+	} catch (err: any) {
+		useMessage().error(err.msg);
+	}
+};
+
+// 暴露变量
+defineExpose({
+	openDialog,
+});
+</script>
+
+<style scoped></style>

+ 0 - 0
src/views/admin/dict/dictItem/form.vue → src/views/system/dict/dictItem/form.vue


+ 0 - 0
src/views/admin/dict/dictItem/index.vue → src/views/system/dict/dictItem/index.vue


+ 0 - 0
src/views/admin/dict/form.vue → src/views/system/dict/form.vue


+ 0 - 0
src/views/admin/dict/i18n/en.ts → src/views/system/dict/i18n/en.ts


+ 0 - 0
src/views/admin/dict/i18n/zh-cn.ts → src/views/system/dict/i18n/zh-cn.ts


+ 0 - 0
src/views/admin/dict/index.vue → src/views/system/dict/index.vue


+ 0 - 0
src/views/admin/file/form.vue → src/views/system/file/form.vue


+ 0 - 0
src/views/admin/file/i18n/en.ts → src/views/system/file/i18n/en.ts


+ 0 - 0
src/views/admin/file/i18n/zh-cn.ts → src/views/system/file/i18n/zh-cn.ts


+ 0 - 0
src/views/admin/file/index.vue → src/views/system/file/index.vue


+ 0 - 0
src/views/admin/log/detail.vue → src/views/system/logs/detail.vue


+ 0 - 0
src/views/admin/log/i18n/en.ts → src/views/system/logs/i18n/en.ts


+ 0 - 0
src/views/admin/log/i18n/zh-cn.ts → src/views/system/logs/i18n/zh-cn.ts


+ 0 - 0
src/views/admin/log/index.vue → src/views/system/logs/index.vue


+ 0 - 0
src/views/admin/param/form.vue → src/views/system/param/form.vue


+ 0 - 0
src/views/admin/param/i18n/en.ts → src/views/system/param/i18n/en.ts


+ 0 - 0
src/views/admin/param/i18n/zh-cn.ts → src/views/system/param/i18n/zh-cn.ts


+ 0 - 0
src/views/admin/param/index.vue → src/views/system/param/index.vue


+ 14 - 0
src/views/system/token/i18n/en.ts

@@ -0,0 +1,14 @@
+export default {
+	systoken: {
+		index: '#',
+		userId: 'userId',
+		username: 'username',
+		clientId: 'clientId',
+		accessToken: 'accessToken',
+		expiresAt: 'expiresAt',
+		inputUsernameTip: 'input Username',
+		offlineBtn: 'offline',
+		offlineConfirmText: 'offline confirm',
+		offlineSuccessText: 'offline success',
+	},
+};

+ 14 - 0
src/views/system/token/i18n/zh-cn.ts

@@ -0,0 +1,14 @@
+export default {
+	systoken: {
+		index: '#',
+		userId: '用户ID',
+		username: '用户名',
+		clientId: '客户端',
+		accessToken: '令牌',
+		expiresAt: '过期时间',
+		inputUsernameTip: '请输入用户名',
+		offlineBtn: '下线',
+		offlineConfirmText: '确认下线',
+		offlineSuccessText: '下线成功',
+	},
+};

+ 122 - 0
src/views/system/token/index.vue

@@ -0,0 +1,122 @@
+<template>
+	<div class="layout-padding">
+		<div class="layout-padding-auto layout-padding-view">
+			<el-row class="ml10" v-show="showSearch">
+				<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
+					<el-form-item :label="$t('systoken.username')" prop="username">
+						<el-input :placeholder="$t('systoken.inputUsernameTip')" v-model="state.queryForm.username"></el-input>
+					</el-form-item>
+					<el-form-item>
+						<el-button @click="getDataList" icon="Search" type="primary">{{ $t('common.queryBtn') }} </el-button>
+						<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
+					</el-form-item>
+				</el-form>
+			</el-row>
+			<el-row>
+				<div class="mb8" style="width: 100%">
+					<el-button :disabled="multiple" @click="handleDelete(selectObjs)" class="ml10" icon="Delete" type="primary" v-auth="'sys_user_del'">
+						{{ $t('systoken.offlineBtn') }}
+					</el-button>
+					<right-toolbar
+						@queryTable="getDataList"
+						class="ml10"
+						style="float: right; margin-right: 20px"
+						v-model:showSearch="showSearch"
+					></right-toolbar>
+				</div>
+			</el-row>
+			<el-table
+				:data="state.dataList"
+				@selection-change="handleSelectionChange"
+				@sort-change="sortChangeHandle"
+				style="width: 100%"
+				v-loading="state.loading"
+				border
+				:cell-style="tableStyle.cellStyle"
+				:header-cell-style="tableStyle.headerCellStyle"
+			>
+				<el-table-column align="center" type="selection" width="40" />
+				<el-table-column :label="$t('systoken.index')" type="index" width="60" />
+				<el-table-column :label="$t('systoken.username')" prop="username" show-overflow-tooltip width="150"></el-table-column>
+				<el-table-column :label="$t('systoken.clientId')" prop="clientId" show-overflow-tooltip width="100"></el-table-column>
+				<el-table-column :label="$t('systoken.accessToken')" prop="accessToken" show-overflow-tooltip>
+					<template #default="scope">
+						<el-button link type="danger" v-if="filterOwnToken(scope.row)">
+							{{ scope.row.accessToken }}
+						</el-button>
+					</template>
+				</el-table-column>
+				<el-table-column :label="$t('systoken.expiresAt')" prop="expiresAt" show-overflow-tooltip></el-table-column>
+				<el-table-column :label="$t('common.action')" width="100">
+					<template #default="scope">
+						<el-button icon="delete" @click="handleDelete([scope.row.accessToken])" size="small" text type="primary" v-auth="'sys_user_del'">
+							{{ $t('systoken.offlineBtn') }}
+						</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+
+			<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination"> </pagination>
+		</div>
+	</div>
+</template>
+
+<script lang="ts" setup>
+import { BasicTableProps, useTable } from '/@/hooks/table';
+import { delObj, fetchList } from '/@/api/admin/token';
+import { useI18n } from 'vue-i18n';
+import { useMessage, useMessageBox } from '/@/hooks/message';
+import { Session } from '/@/utils/storage';
+
+const { t } = useI18n();
+// 定义变量内容
+const queryRef = ref();
+const showSearch = ref(true);
+// 多选rows
+const selectObjs = ref([]) as any;
+// 是否可以多选
+const multiple = ref(true);
+
+//  table hook
+const state: BasicTableProps = reactive<BasicTableProps>({
+	queryForm: {
+		username: '',
+	},
+	pageList: fetchList,
+});
+const { getDataList, currentChangeHandle, sortChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
+
+// 清空搜索条件
+const resetQuery = () => {
+	queryRef.value?.resetFields();
+	getDataList();
+};
+
+// 多选事件
+const handleSelectionChange = (objs: { accessToken: string }[]) => {
+	selectObjs.value = objs.map(({ accessToken }) => accessToken);
+	multiple.value = !objs.length;
+};
+
+// 删除操作
+const handleDelete = async (accessTokens: string[]) => {
+	try {
+		await useMessageBox().confirm(t('systoken.offlineConfirmText'));
+	} catch {
+		return; // 取消删除则直接跳过此方法
+	}
+
+	try {
+		await delObj(accessTokens);
+		getDataList();
+		useMessage().success(t('systoken.offlineSuccessText'));
+	} catch (err: any) {
+		useMessage().error(err.msg);
+	}
+};
+
+// 判断当前token 是否和登录token一致
+const filterOwnToken = (row: any) => {
+	return Session.getToken() === row.accessToken;
+};
+</script>