Pārlūkot izejas kodu

Merge branch 'fix/menu-filter' into dev-ly

zhaonan 13 stundas atpakaļ
vecāks
revīzija
746d072fde

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

@@ -47,9 +47,6 @@ const state = reactive<ParentViewState>({
 const menuLeftList = computed(() => { 
 	const parentName = route.path.split('/')[1];
 	const parentRoute = routeStores.routesList.filter((item: RouteItem) => {
-		if(route.path.includes("/admin/log")) {
-			return item.path.includes("/system")
-		}
 		return item.path.includes(parentName)
 	});
 	return parentRoute[0].children || [];

+ 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>