更新2.1.2版本,优化部门、角色权限,增加上下级关系;增加登录、系统、短信日志;优化省市区编码

This commit is contained in:
孟帅
2023-01-25 11:49:21 +08:00
parent 11fad0132d
commit 93e0fe7250
190 changed files with 35896 additions and 7208 deletions

25099
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -67,6 +67,7 @@
"autoprefixer": "^10.4.7",
"commitizen": "^4.2.4",
"core-js": "^3.22.5",
"crypto-js": "^4.1.1",
"dotenv": "^10.0.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.5.0",
@@ -97,7 +98,9 @@
"vite-plugin-compression": "^0.3.6",
"vite-plugin-html": "^2.1.2",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-require-transform": "^1.0.5",
"vite-plugin-style-import": "^1.4.1",
"vite-plugin-top-level-await": "^1.2.2",
"vue-eslint-parser": "^7.11.0"
},
"lint-staged": {

View File

@@ -39,3 +39,43 @@ export function View(params) {
params,
});
}
// 获取最大排序
export function MaxSort() {
return http.request({
url: '/provinces/maxSort',
method: 'GET',
});
}
/**
* 获取省市区关系树选项列表
*/
export function getProvincesTree() {
return http.request({
url: '/provinces/tree',
method: 'GET',
});
}
/**
* 获取省市区下级列表
*/
export function getProvincesChildrenList(params) {
return http.request({
url: '/provinces/childrenList',
method: 'GET',
params,
});
}
/**
* 唯一省市区ID
*/
export function CheckProvincesUniqueId(params) {
return http.request({
url: '/provinces/uniqueId',
method: 'GET',
params,
});
}

View File

@@ -0,0 +1,9 @@
import { http } from '@/utils/http/axios';
// 获取验证码
export function GetCaptcha() {
return http.request({
url: '/site/captcha',
method: 'get',
});
}

View File

@@ -18,8 +18,7 @@ export function Delete(params) {
});
}
// 新建/编辑生成演示
// 添加/编辑生成演示
export function Edit(params) {
return http.request({
url: '/curdDemo/edit',
@@ -28,7 +27,6 @@ export function Edit(params) {
});
}
// 修改生成演示状态
export function Status(params) {
return http.request({
@@ -38,7 +36,6 @@ export function Status(params) {
});
}
// 操作生成演示开关
export function Switch(params) {
return http.request({
@@ -48,7 +45,6 @@ export function Switch(params) {
});
}
// 获取生成演示指定详情
export function View(params) {
return http.request({
@@ -58,7 +54,6 @@ export function View(params) {
});
}
// 获取生成演示最大排序
export function MaxSort() {
return http.request({
@@ -67,8 +62,7 @@ export function MaxSort() {
});
}
// 导出生成演示
export function Export(params) {
jumpExport('/curdDemo/export', params);
}
}

24
web/src/api/log/smslog.ts Normal file
View File

@@ -0,0 +1,24 @@
import { http } from '@/utils/http/axios';
export function getLogList(params) {
return http.request({
url: '/smsLog/list',
method: 'get',
params,
});
}
export function Delete(params) {
return http.request({
url: '/smsLog/delete',
method: 'POST',
params,
});
}
export function View(params) {
return http.request({
url: '/smsLog/view',
method: 'GET',
params,
});
}

View File

@@ -0,0 +1,33 @@
import { http, jumpExport } from '@/utils/http/axios';
// 获取登录日志列表
export function List(params) {
return http.request({
url: '/loginLog/list',
method: 'get',
params,
});
}
// 删除/批量删除登录日志
export function Delete(params) {
return http.request({
url: '/loginLog/delete',
method: 'POST',
params,
});
}
// 获取登录日志指定详情
export function View(params) {
return http.request({
url: '/loginLog/view',
method: 'GET',
params,
});
}
// 导出登录日志
export function Export(params) {
jumpExport('/loginLog/export', params);
}

View File

@@ -34,7 +34,7 @@ export function Delete(params) {
export function ResetPwd(params) {
return http.request({
url: '/member/reset_pwd',
url: '/member/resetPwd',
method: 'POST',
params,
});

View File

@@ -0,0 +1,33 @@
import { http, jumpExport } from '@/utils/http/axios';
// 获取服务日志列表
export function List(params) {
return http.request({
url: '/serveLog/list',
method: 'get',
params,
});
}
// 删除/批量删除服务日志
export function Delete(params) {
return http.request({
url: '/serveLog/delete',
method: 'POST',
params,
});
}
// 获取服务日志指定详情
export function View(params) {
return http.request({
url: '/serveLog/view',
method: 'GET',
params,
});
}
// 导出服务日志
export function Export(params) {
jumpExport('/serveLog/export', params);
}

View File

@@ -29,3 +29,11 @@ export function sendTestEmail(params) {
params,
});
}
export function sendTestSms(params) {
return http.request({
url: '/sms/sendTest',
method: 'post',
params,
});
}

View File

@@ -42,7 +42,7 @@ export function View(params) {
export function GroupList(params) {
return http.request({
url: '/cron_group/list',
url: '/cronGroup/list',
method: 'get',
params,
});
@@ -50,7 +50,7 @@ export function GroupList(params) {
export function GroupDelete(params) {
return http.request({
url: '/cron_group/delete',
url: '/cronGroup/delete',
method: 'POST',
params,
});
@@ -58,7 +58,7 @@ export function GroupDelete(params) {
export function GroupEdit(params) {
return http.request({
url: '/cron_group/edit',
url: '/cronGroup/edit',
method: 'POST',
params,
});
@@ -66,7 +66,7 @@ export function GroupEdit(params) {
export function GroupStatus(params) {
return http.request({
url: '/cron_group/status',
url: '/cronGroup/status',
method: 'POST',
params,
});
@@ -74,7 +74,7 @@ export function GroupStatus(params) {
export function GroupView(params) {
return http.request({
url: '/cron_group/view',
url: '/cronGroup/view',
method: 'GET',
params,
});
@@ -82,8 +82,16 @@ export function GroupView(params) {
export function getSelect(params) {
return http.request({
url: '/cron_group/select',
url: '/cronGroup/select',
method: 'GET',
params,
});
}
export function OnlineExec(params) {
return http.request({
url: '/cron/onlineExec',
method: 'POST',
params,
});
}

View File

@@ -18,7 +18,7 @@ export function Delete(params) {
});
}
// 新建/编辑
// 添加/编辑
export function Edit(params) {
return http.request({
url: '/test/edit',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -93,6 +93,7 @@
import { useBattery } from '@/hooks/useBattery';
import { useLockscreenStore } from '@/store/modules/lockscreen';
import { useUserStore } from '@/store/modules/user';
import { aesEcb } from '@/utils/encrypt';
export default defineComponent({
name: 'Lockscreen',
@@ -140,7 +141,8 @@
}
const params = {
isLock: true,
...state.loginParams,
username: state.loginParams.username,
password: aesEcb.encrypt(state.loginParams.password),
};
state.loginLoading = true;
const { code, message } = await userStore.login(params);

View File

@@ -89,6 +89,7 @@
import { useGlobSetting } from '@/hooks/setting';
import { isJsonString, isNullOrUnDef } from '@/utils/is';
import { getFileExt } from '@/utils/urlUtils';
import { errorImg } from '@/utils/hotgo';
const globSetting = useGlobSetting();
export default defineComponent({
@@ -258,13 +259,6 @@
}
}
/**图片加载失败显示自定义默认图片(缺省图)*/
function errorImg(e) {
e.srcElement.src = '/onerror.png';
//这一句没用,如果默认图片的路径错了还是会一直闪屏,在方法的前面加个.once只让它执行一次也没用
e.srcElement.onerror = null; //防止闪图
}
onMounted(async () => {
setTimeout(function () {
if (props.maxNumber === 1) {

View File

@@ -1,3 +1,5 @@
import { Option } from '@/utils/hotgo';
export const switchOptions = [
{
value: 1,
@@ -85,7 +87,7 @@ export const tagOptions = [
value: 'success',
},
{
label: '色',
label: '色',
value: 'warning',
},
{
@@ -93,3 +95,19 @@ export const tagOptions = [
value: 'error',
},
];
// 登录状态
export const loginStatusOptions: Option[] = [
{
value: 1,
label: '成功',
key: 1,
listClass: 'success',
},
{
value: 2,
label: '失败',
key: 2,
listClass: 'warning',
},
];

View File

@@ -182,6 +182,7 @@
import { NotificationsOutline as NotificationsIcon } from '@vicons/ionicons5';
import PopoverMessage from './PopoverMessage.vue';
import { notificationStoreWidthOut } from '@/store/modules/notification';
import notificationImg from '@/assets/images/notification.png';
export default defineComponent({
name: 'PageHeader',
@@ -393,7 +394,7 @@
h(NAvatar, {
size: 'small',
round: true,
src: '/notification.png',
src: notificationImg,
}),
onClose: () => {
nRef.value = null;

25
web/src/utils/encrypt.ts Normal file
View File

@@ -0,0 +1,25 @@
import CryptoJS from 'crypto-js';
const defaultKey = 'f080a463654b2279';
export const aesEcb = {
// 加密
encrypt(word: string, keyStr: string = defaultKey): string {
const key = CryptoJS.enc.Utf8.parse(keyStr);
const src = CryptoJS.enc.Utf8.parse(word);
const encrypted = CryptoJS.AES.encrypt(src, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
return encrypted.toString();
},
// 解密
decrypt(word: string, keyStr: string = defaultKey): string {
const key = CryptoJS.enc.Utf8.parse(keyStr);
const decrypt = CryptoJS.AES.decrypt(word, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
return CryptoJS.enc.Utf8.stringify(decrypt).toString();
},
};

View File

@@ -1,10 +1,11 @@
import { Ref, UnwrapRef } from '@vue/reactivity';
import onerrorImg from '@/assets/images/onerror.png';
export interface Option {
label: string;
value: string;
key: string;
type: string;
value: string | number;
key: string | number;
// type: string;
listClass: 'default' | 'error' | 'primary' | 'info' | 'success' | 'warning';
}
@@ -51,3 +52,9 @@ export function adaModalWidth(dialogWidth: Ref<UnwrapRef<string>>) {
}
return dialogWidth.value;
}
// 图片加载失败显示自定义默认图片(缺省图)
export function errorImg(e: any): void {
e.target.src = onerrorImg;
e.target.onerror = null;
}

View File

@@ -5,6 +5,7 @@ import { Dicts } from '@/api/dict/dict';
import { getOptionLabel, getOptionTag, Options } from '@/utils/hotgo';
import { FormSchema } from '@/components/Form';
import { isNullOrUnDef } from '@/utils/is';
import { errorImg } from '@/utils/hotgo';
export const options = ref<Options>({
sys_normal_disable: [],
config_upload_drive: [],
@@ -116,6 +117,7 @@ export const columns = [
width: 40,
height: 40,
src: row.fileUrl,
onError: errorImg,
style: {
width: '40px',
height: '40px',

View File

@@ -31,7 +31,7 @@
<PlusOutlined />
</n-icon>
</template>
新建
添加
</n-button>
&nbsp;
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
@@ -45,7 +45,7 @@
</template>
</BasicTable>
<n-modal v-model:show="showModal" :show-icon="false" preset="dialog" title="新建">
<n-modal v-model:show="showModal" :show-icon="false" preset="dialog" title="添加">
<n-form
:model="formParams"
:rules="rules"

View File

@@ -1,71 +0,0 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
export const columns = [
{
title: 'ID',
key: 'id',
},
{
title: '地区名称',
key: 'title',
},
{
title: '父ID',
key: 'pid',
render(row) {
return row.pid;
},
},
{
title: '拼音',
key: 'pinyin',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: 'success',
bordered: false,
},
{
default: () => row.pinyin,
}
);
},
},
{
title: '经度',
key: 'lng',
},
{
title: '维度',
key: 'lat',
},
{
title: '状态',
key: 'status',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.status == 1 ? 'success' : 'warning',
bordered: false,
},
{
default: () => (row.status == 1 ? '正常' : '隐藏'),
}
);
},
},
{
title: '创建时间',
key: 'createdAt',
},
];

View File

@@ -0,0 +1,191 @@
<template>
<div>
<n-modal
v-model:show="isShowModal"
:show-icon="false"
preset="dialog"
:title="isUpdate ? '编辑 #' + params?.id : '添加'"
:style="{
width: dialogWidth,
}"
>
<n-form
:model="params"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="上级地区" path="pid">
<n-tree-select :options="optionTreeData" :default-value="params.pid" />
</n-form-item>
<n-form-item label="地区ID" path="id">
<n-input-number
style="width: 100%"
placeholder="请输入地区ID"
v-model:value="params.id"
:disabled="isUpdate"
path="handleChangeId"
/>
</n-form-item>
<n-form-item label="地区名称" path="title">
<n-input placeholder="请输入地区名称" v-model:value="params.title" />
</n-form-item>
<n-form-item label="拼音" path="pinyin">
<n-input placeholder="请输入拼音" v-model:value="params.pinyin" />
</n-form-item>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="经度" path="lng">
<n-input placeholder="经度" v-model:value="params.lng" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="纬度" path="lat">
<n-input placeholder="纬度" v-model:value="params.lat" />
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="params.sort" clearable />
</n-form-item>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="params.status" name="status">
<n-radio-button
v-for="status in options.sys_normal_disable"
:key="Number(status.value)"
:value="Number(status.value)"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="closeForm">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, computed, watch } from 'vue';
import { options, State, newState } from './model';
import { Edit, MaxSort, CheckProvincesUniqueId } from '@/api/apply/provinces';
import { FormItemRule, useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
const emit = defineEmits(['reloadTable', 'updateShowModal']);
interface Props {
showModal: boolean;
formParams?: State;
optionTreeData: any;
isUpdate: boolean;
}
const props = withDefaults(defineProps<Props>(), {
showModal: false,
formParams: () => {
return newState(null);
},
optionTreeData: [],
isUpdate: false,
});
const isShowModal = computed({
get: () => {
return props.showModal;
},
set: (value) => {
emit('updateShowModal', value);
},
});
const params = computed(() => {
return props.formParams;
});
const rules = {
id: {
required: true,
async validator(rule: FormItemRule, value: string, callback: Function) {
if (!value) {
callback(new Error('请填写地区ID'));
} else if (!/^\d*$/.test(value)) {
callback(new Error('地区ID应该为整数'));
} else if (!(await isUniqueId(value))) {
callback(new Error('地区ID已存在'));
} else {
callback();
}
},
trigger: ['input', 'blur'],
},
title: {
required: true,
trigger: ['blur', 'input'],
message: '请输入地区名称',
},
};
const message = useMessage();
const formRef = ref<any>({});
const dialogWidth = ref('75%');
const formBtnLoading = ref(false);
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
Edit(params.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
isShowModal.value = false;
emit('reloadTable');
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function closeForm() {
isShowModal.value = false;
}
watch(
() => params.value,
(value) => {
params.value.oldId = Number(value.id);
if (value.id === 0 || value.id === null) {
MaxSort().then((res) => {
params.value.sort = res.sort;
});
}
}
);
async function isUniqueId(newId: any) {
const res = await CheckProvincesUniqueId({ oldId: params.value.oldId, newId: newId });
return res.unique;
}
onMounted(async () => {
adaModalWidth(dialogWidth);
});
</script>
<style lang="less"></style>

View File

@@ -1,357 +1,231 @@
<template>
<div>
<n-card :bordered="false" class="proCard" title="省市区">
<BasicForm
@register="register"
@submit="handleSubmit"
@reset="handleReset"
@keyup.enter="handleSubmit"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:openChecked="true"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button type="primary" @click="addTable">
<template #icon>
<n-icon>
<PlusOutlined />
<div class="n-layout-page-header">
<n-card :bordered="false" title="省市区"> 中国省市区编码对照表 </n-card>
</div>
<n-grid class="mt-6" cols="1 s:1 m:1 l:4 xl:4 2xl:4" responsive="screen" :x-gap="12">
<n-gi span="1">
<n-card :segmented="{ content: true }" :bordered="false" size="small">
<template #header>
<n-space>
<n-button type="info" icon-placement="left" @click="openCreateDrawer">
<template #icon>
<div class="flex items-center">
<n-icon size="14">
<PlusOutlined />
</n-icon>
</div>
</template>
添加
</n-button>
<n-button
type="info"
icon-placement="left"
@click="openEditDrawer"
:disabled="formParams?.id === null || formParams?.id <= 0"
>
<template #icon>
<div class="flex items-center">
<n-icon size="14">
<EditOutlined />
</n-icon>
</div>
</template>
编辑
</n-button>
<n-button type="error" icon-placement="left" @click="handleDel">
<template #icon>
<div class="flex items-center">
<n-icon size="14">
<DeleteOutlined />
</n-icon>
</div>
</template>
删除
</n-button>
<n-button type="info" icon-placement="left" @click="packHandle">
{{ expandedKeys.length ? '收起' : '展开' }}
<template #icon>
<div class="flex items-center">
<n-icon size="14">
<AlignLeftOutlined />
</n-icon>
</div>
</template>
</n-button>
</n-space>
</template>
<div class="w-full menu">
<n-input v-model:value="pattern" placeholder="输入地区名称搜索">
<template #suffix>
<n-icon size="18" class="cursor-pointer">
<SearchOutlined />
</n-icon>
</template>
</n-input>
<div class="py-3 menu-list">
<template v-if="loading">
<div class="flex items-center justify-center py-4">
<n-spin size="medium" />
</div>
</template>
<template v-else>
<n-tree
block-line
cascade
checkable
:virtual-scroll="true"
:pattern="pattern"
:data="treeData"
:expandedKeys="expandedKeys"
style="height: 75vh"
@update:selected-keys="selectedTree"
@update:expanded-keys="onExpandedKeys"
/>
</template>
</div>
</div>
</n-card>
</n-gi>
<n-gi span="3">
<n-card :segmented="{ content: true }" :bordered="false" size="small">
<template #header>
<n-space>
<n-icon size="18">
<FormOutlined />
</n-icon>
</template>
新建
</n-button>
&nbsp;
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
</BasicTable>
<span>编辑省市区{{ treeItemTitle ? `${treeItemTitle}` : '' }}</span>
<span style="font-size: 14px">{{
treeItemTitle ? '' : '从列表选择一项后,进行编辑'
}}</span>
</n-space>
</template>
<n-modal v-model:show="showModal" :show-icon="false" preset="dialog" title="新建">
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="公告标题" path="title">
<n-input placeholder="请输入公告标题" v-model:value="formParams.title" />
</n-form-item>
<List :checkedId="checkedId" :optionTreeData="optionTreeData" @reloadTable="loadData" />
</n-card>
</n-gi>
</n-grid>
<n-form-item label="公告类型" path="type">
<n-radio-group v-model:value="formParams.type" name="type">
<n-radio-button
v-for="type in typeOptions"
:key="type.value"
:value="type.value"
:label="type.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item label="公告内容" path="content">
<n-input type="textarea" placeholder="请输入内容" v-model:value="formParams.content" />
</n-form-item>
<n-form-item label="接收人" path="receiver">
<n-input
type="textarea"
placeholder="多个用户ID用,隔开 不填则全部接收"
v-model:value="formParams.receiver"
/>
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="formParams.sort" clearable />
</n-form-item>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
<n-radio-button
v-for="status in statusOptions"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item label="备注" path="remark">
<n-input type="textarea" placeholder="请输入备注" v-model:value="formParams.remark" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</n-card>
<Edit
@reloadTable="loadData"
@updateShowModal="updateShowModal"
:showModal="showModal"
:formParams="formParams"
:optionTreeData="optionTreeData"
:isUpdate="isUpdate"
/>
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { onMounted, ref, unref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { Delete, Edit, List, Status } from '@/api/apply/provinces';
import { columns } from './columns';
import { DeleteOutlined, PlusOutlined } from '@vicons/antd';
import { statusActions, statusOptions } from '@/enums/optionsiEnum';
const typeOptions = [
{
value: 1,
label: '通知',
},
{
value: 2,
label: '公告',
},
].map((s) => {
return s;
});
const params = ref<any>({
pageSize: 10,
title: '',
content: '',
status: null,
});
const rules = {
title: {
// required: true,
trigger: ['blur', 'input'],
message: '请输入标题',
},
};
const schemas: FormSchema[] = [
{
field: 'title',
component: 'NInput',
label: '公告标题',
componentProps: {
placeholder: '请输入公告标题',
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules: [{ message: '请输入公告标题', trigger: ['blur'] }],
},
{
field: 'content',
component: 'NInput',
label: '内容',
componentProps: {
placeholder: '请输入内容关键词',
showButton: false,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '状态',
defaultValue: null,
componentProps: {
placeholder: '请选择类型',
options: statusOptions,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const message = useMessage();
const actionRef = ref();
const dialog = useDialog();
import {
AlignLeftOutlined,
FormOutlined,
PlusOutlined,
EditOutlined,
SearchOutlined,
DeleteOutlined,
} from '@vicons/antd';
import { getTreeItem } from '@/utils';
import List from './list.vue';
import { getProvincesTree, Delete } from '@/api/apply/provinces';
import Edit from './edit.vue';
import { newState } from './model';
const isUpdate = ref(false);
const showModal = ref(false);
const formBtnLoading = ref(false);
const searchFormRef = ref<any>({});
const formRef = ref<any>({});
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const message = useMessage();
const dialog = useDialog();
let treeItemKey = ref([]);
let expandedKeys = ref([]);
const treeData = ref([]);
const loading = ref(true);
const treeItemTitle = ref('');
const checkedId = ref(0);
const pattern = ref('');
const optionTreeData = ref<any>([]);
const formParams = ref(newState(null));
const resetFormParams = {
id: 0,
title: '',
name: '',
type: 1,
receiver: '',
remark: '',
sort: 0,
status: 1,
created_at: '',
updated_at: '',
};
let formParams = ref<any>(resetFormParams);
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
dropDownActions: statusActions,
select: (key) => {
updateStatus(record.id, key);
},
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function addTable() {
function openCreateDrawer() {
showModal.value = true;
formParams.value = resetFormParams;
formParams.value = newState(null);
isUpdate.value = false;
}
const loadDataTable = async (res) => {
return await List({ ...params.value, ...res, ...searchFormRef.value?.formModel });
};
function openEditDrawer() {
showModal.value = true;
isUpdate.value = true;
}
function onCheckedRow(rowKeys) {
if (rowKeys.length > 0) {
batchDeleteDisabled.value = false;
function selectedTree(keys) {
if (keys.length) {
const treeItem = getTreeItem(unref(treeData), keys[0]);
treeItemKey.value = keys;
treeItemTitle.value = treeItem.label;
formParams.value = newState(treeItem);
checkedId.value = treeItem.id;
} else {
batchDeleteDisabled.value = true;
treeItemKey.value = [];
treeItemTitle.value = '';
}
checkedIds.value = rowKeys;
}
function reloadTable() {
actionRef.value.reload();
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
Edit(formParams.value)
.then((_res) => {
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
reloadTable();
formParams.value = ref(resetFormParams);
});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function handleEdit(record: Recordable) {
showModal.value = true;
formParams.value = record;
}
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
content: '你确定要删除?',
function handleDel() {
dialog.info({
title: '提示',
content: `您确定想删除吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record).then((_res) => {
Delete({ ...formParams.value }).then(async (_res) => {
message.success('操作成功');
reloadTable();
await loadData();
});
},
onNegativeClick: () => {
// message.error('取消');
message.error('已取消');
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ id: checkedIds.value }).then((_res) => {
message.success('操作成功');
reloadTable();
});
function packHandle() {
if (expandedKeys.value.length) {
expandedKeys.value = [];
} else {
expandedKeys.value = unref(treeData).map((item: any) => item.key as string) as [];
}
}
onMounted(async () => {
await loadData();
});
async function loadData() {
const treeMenuList = await getProvincesTree();
Object.assign(
formParams,
treeMenuList.list.map((item) => item.key)
);
treeData.value = [];
optionTreeData.value = [
{
id: 0,
key: 0,
label: '顶级地区',
pid: 0,
title: '顶级地区',
},
onNegativeClick: () => {
// message.error('取消');
},
});
];
treeData.value = treeMenuList.list;
optionTreeData.value = optionTreeData.value.concat(treeMenuList.list);
loading.value = false;
}
function handleSubmit(values: Recordable) {
console.log(values);
params.value = values;
reloadTable();
function onExpandedKeys(keys) {
expandedKeys.value = keys;
}
function handleReset(values: Recordable) {
params.value = values;
reloadTable();
}
function updateStatus(id, status) {
Status({ id: id, status: status }).then((_res) => {
message.success('操作成功');
setTimeout(() => {
reloadTable();
});
});
function updateShowModal(value) {
showModal.value = value;
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,217 @@
<template>
<n-card :bordered="false" class="proCard">
<n-result
v-show="checkedId <= 0"
status="info"
title="提示"
description="请选择一个想要编辑的省市区"
/>
<div v-show="checkedId > 0">
<BasicForm
@register="register"
@submit="handleSubmit"
@reset="handleReset"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:columns="listColumns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button type="primary" @click="addTable">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
添加数据
</n-button>
</template>
</BasicTable>
</div>
<Edit
@reloadTable="reloadTable"
@updateShowModal="updateShowModal"
:showModal="showModal"
:formParams="formParams"
:optionTreeData="optionTreeData"
:isUpdate="isUpdate"
/>
</n-card>
</template>
<script lang="ts" setup>
import { h, reactive, ref, watch } from 'vue';
import { useMessage, useDialog } from 'naive-ui';
import { BasicColumn, BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { listColumns, newState, State } from './model';
import { PlusOutlined } from '@vicons/antd';
import { getProvincesChildrenList, Delete } from '@/api/apply/provinces';
import Edit from './edit.vue';
const emit = defineEmits(['reloadTable']);
interface Props {
checkedId?: number;
optionTreeData: any;
}
const props = withDefaults(defineProps<Props>(), { checkedId: 0, optionTreeData: [] });
const searchFormRef = ref<any>({});
const message = useMessage();
const dialog = useDialog();
const actionRef = ref();
const isUpdate = ref(false);
const showModal = ref(false);
const formParams = ref<State>(newState(null));
const params = ref({
pageSize: 10,
pid: props.checkedId,
label: '',
});
const schemas: FormSchema[] = [
{
field: 'id',
component: 'NInput',
label: '地区ID',
componentProps: {
placeholder: '请输入地区ID',
onInput: (e: any) => {
console.log(e);
params.value.label = e;
},
},
},
{
field: 'title',
component: 'NInput',
label: '地区名称',
componentProps: {
placeholder: '请输入地区名称',
onInput: (e: any) => {
console.log(e);
params.value.label = e;
},
},
},
];
const actionColumn = reactive<BasicColumn>({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:1 l:2 xl:2 2xl:2' },
labelWidth: 80,
schemas,
});
function addTable() {
showModal.value = true;
formParams.value = newState(null);
formParams.value.pid = props.checkedId;
isUpdate.value = false;
}
const loadDataTable = async (res) => {
if (props.checkedId <= 0) {
return [];
}
return await getProvincesChildrenList({
...{ pid: props.checkedId },
...searchFormRef.value?.formModel,
...res,
});
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
actionRef.value.reload();
emit('reloadTable');
}
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
content: '您确定想删除吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
function handleEdit(record: Recordable) {
showModal.value = true;
formParams.value = newState(record as State);
isUpdate.value = true;
}
function handleSubmit(_values: Recordable) {
reloadTable();
}
function handleReset(_values: Recordable) {
params.value.label = '';
reloadTable();
}
watch(props, (_newVal, _oldVal) => {
if (params.value.pid === _newVal.checkedId) {
return;
}
params.value.pid = _newVal.checkedId;
formParams.value.pid = Number(_newVal.checkedId);
if (_newVal.checkedId > 0) {
reloadTable();
}
});
function updateShowModal(value) {
showModal.value = value;
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,117 @@
import { h, ref } from 'vue';
import { NTag } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { getOptionLabel, getOptionTag, Options } from '@/utils/hotgo';
import { Dicts } from '@/api/dict/dict';
import { schemas } from '@/views/test/model';
import { isNullObject } from '@/utils/is';
export const listColumns = [
{
title: '地区ID',
key: 'id',
},
{
title: '地区名称',
key: 'title',
},
{
title: '拼音',
key: 'pinyin',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: 'success',
bordered: false,
},
{
default: () => row.pinyin,
}
);
},
},
{
title: '经度',
key: 'lng',
},
{
title: '维度',
key: 'lat',
},
{
title: '状态',
key: 'status',
render(row) {
if (isNullObject(row.status)) {
return ``;
}
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: getOptionTag(options.value.sys_normal_disable, row.status),
bordered: false,
},
{
default: () => getOptionLabel(options.value.sys_normal_disable, row.status),
}
);
},
},
];
export interface State {
id: number | null;
title: string;
pinyin: string;
lng: string;
lat: string;
pid: number;
sort: number;
status: number;
oldId: number;
}
export const defaultState = {
id: null,
title: '',
pinyin: '',
lng: '',
lat: '',
pid: 0,
sort: 0,
status: 1,
oldId: 0,
};
export function newState(state: State | null): State {
if (state !== null) {
return cloneDeep(state);
}
return cloneDeep(defaultState);
}
export const options = ref<Options>({
sys_normal_disable: [],
});
async function loadOptions() {
options.value = await Dicts({
types: ['sys_normal_disable'],
});
for (const item of schemas.value) {
switch (item.field) {
case 'status':
item.componentProps.options = options.value.sys_normal_disable;
break;
}
}
}
await loadOptions();

View File

@@ -4,7 +4,7 @@
v-model:show="isShowModal"
:show-icon="false"
preset="dialog"
:title="params?.id > 0 ? '编辑 #' + params?.id : '新建'"
:title="params?.id > 0 ? '编辑 #' + params?.id : '添加'"
:style="{
width: dialogWidth,
}"

View File

@@ -42,7 +42,7 @@
<PlusOutlined />
</n-icon>
</template>
新建
添加
</n-button>
<n-button
type="error"
@@ -246,4 +246,4 @@
}
</script>
<style lang="less" scoped></style>
<style lang="less" scoped></style>

View File

@@ -9,12 +9,11 @@ import { getFileExt } from '@/utils/urlUtils';
import { defRangeShortcuts, defShortcuts, formatToDate } from '@/utils/dateUtil';
import { validate } from '@/utils/validateUtil';
import { getOptionLabel, getOptionTag, Options } from '@/utils/hotgo';
import { errorImg } from '@/utils/hotgo';
import { usePermission } from '@/hooks/web/usePermission';
const { hasPermission } = usePermission();
const $message = window['$message'];
export interface State {
id: number;
categoryId: number;
@@ -62,8 +61,7 @@ export const options = ref<Options>({
sys_normal_disable: [],
});
export const rules = {
};
export const rules = {};
export const schemas = ref<FormSchema[]>([
{
@@ -141,6 +139,7 @@ export const columns = [
width: 32,
height: 32,
src: row.image,
onError: errorImg,
style: {
width: '32px',
height: '32px',
@@ -226,17 +225,15 @@ export const columns = [
async function loadOptions() {
options.value = await Dicts({
types: [
'sys_normal_disable',
],
types: ['sys_normal_disable'],
});
for (const item of schemas.value) {
switch (item.field) {
case 'status':
item.componentProps.options = options.value.sys_normal_disable;
break;
}
}
}
}
await loadOptions();
await loadOptions();

View File

@@ -1,6 +1,6 @@
<template>
<n-card :bordered="false" class="proCard">
<n-card :bordered="false" title="代码生成"> 你可以在这里查看到平台所有的短信发送记录 </n-card>
<n-card :bordered="false" class="proCard" title="代码生成">
<!-- <n-card :bordered="false" title="代码生成"> 你可以在这里查看到平台所有的短信发送记录 </n-card>-->
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" ref="searchFormRef">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
@@ -23,7 +23,7 @@
<PlusOutlined />
</n-icon>
</template>
生成
立即生成
</n-button>
&nbsp;
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
@@ -41,7 +41,7 @@
v-model:show="showModal"
:show-icon="false"
preset="dialog"
title="生成"
title="立即生成"
:style="{
width: dialogWidth,
}"
@@ -142,7 +142,7 @@
{
title: '生成ID',
key: 'id',
width: 100,
width: 80,
},
{
title: '生成类型',
@@ -162,7 +162,7 @@
}
);
},
width: 200,
width: 180,
},
{
title: '实体命名',
@@ -175,7 +175,7 @@
{
title: '数据库',
key: 'dbName',
width: 200,
width: 150,
},
{
title: '数据表',
@@ -212,11 +212,11 @@
key: 'createdAt',
width: 180,
},
{
title: '更新时间',
key: 'updatedAt',
width: 180,
},
// {
// title: '更新时间',
// key: 'updatedAt',
// width: 180,
// },
];
const dialog = useDialog();

View File

@@ -22,7 +22,7 @@
<PlusOutlined />
</n-icon>
</template>
新建
添加
</n-button>
</template>
@@ -31,7 +31,7 @@
</template>
</BasicTable>
<n-modal v-model:show="showModal" :show-icon="false" preset="dialog" title="新建">
<n-modal v-model:show="showModal" :show-icon="false" preset="dialog" title="添加">
<n-form
:model="formParams"
:rules="rules"
@@ -312,7 +312,7 @@
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
message.success('新建成功');
message.success('添加成功');
setTimeout(() => {
showModal.value = false;
reloadTable();

View File

@@ -1,5 +1,5 @@
<template>
<n-card :bordered="false" class="proCard">
<n-card :bordered="false" class="proCard" title="任务日志">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
@@ -26,10 +26,6 @@
批量删除
</n-button>
</template>
<template #toolbar>
<n-button type="primary" @click="reloadTable">cron刷新数据</n-button>
</template>
</BasicTable>
</n-card>
</template>

View File

@@ -20,6 +20,7 @@ export const columns = [
}
);
},
width: 100,
},
{
title: '操作人',
@@ -30,18 +31,22 @@ export const columns = [
}
return row.member_name + '(' + row.memberId + ')';
},
width: 150,
},
{
title: '请求方式',
key: 'method',
width: 80,
},
{
title: '请求路径',
key: 'url',
width: 200,
},
{
title: '访问IP',
key: 'ip',
width: 150,
},
// {
// title: 'IP地区',
@@ -65,16 +70,19 @@ export const columns = [
}
);
},
width: 150,
},
{
title: 'Goroutine耗时',
title: '处理耗时',
key: 'takeUpTime',
render(row) {
return row.takeUpTime + ' ms';
},
width: 120,
},
{
title: '访问时间',
key: 'createdAt',
width: 150,
},
];

View File

@@ -1,5 +1,10 @@
<template>
<n-card :bordered="false" class="proCard">
<div class="n-layout-page-header">
<n-card :bordered="false" title="访问日志">
全局的访问日志记录了管理后台中人员的操作记录和服务响应情况
</n-card>
</div>
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
@@ -15,6 +20,7 @@
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
:resizeHeightOffset="-20000"
>
<template #tableTitle>
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
@@ -26,10 +32,6 @@
批量删除
</n-button>
</template>
<template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template>
</BasicTable>
</n-card>
</template>

View File

@@ -41,16 +41,30 @@
title="报错信息"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="错状态码"> {{ data.errorCode }} </n-descriptions-item>
<n-descriptions-item label="报错消息">
<n-tag type="success"> {{ data.errorMsg }} </n-tag>
</n-descriptions-item>
<n-descriptions-item label="报错日志">
<n-tag type="success"> {{ data.errorData }} </n-tag>
<n-descriptions-item label="错状态码"> {{ data.errorCode }} </n-descriptions-item>
<n-descriptions-item label="错误提示">
<n-tag type="error"> {{ data.errorMsg }} </n-tag>
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="堆栈打印"
>
<JsonViewer
:value="data.errorData"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
@@ -59,7 +73,7 @@
title="Header请求头"
>
<JsonViewer
:value="JSON.parse(data.headerData ?? '{}')"
:value="data.headerData"
:expand-depth="5"
copyable
boxed
@@ -76,7 +90,7 @@
title="GET参数"
>
<JsonViewer
:value="JSON.parse(data.getData ?? '{}')"
:value="data.getData"
:expand-depth="5"
copyable
boxed
@@ -93,7 +107,7 @@
title="POST参数"
>
<JsonViewer
:value="JSON.parse(data.postData ?? '{}')"
:value="data.postData"
:expand-depth="5"
copyable
boxed

View File

@@ -1,80 +0,0 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
export const columns = [
{
title: '模块',
key: 'module',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.module == 'admin' ? 'info' : 'success',
bordered: false,
},
{
default: () => row.module,
}
);
},
},
{
title: '操作人',
key: 'member_name',
render(row) {
if (row.memberId === 0) {
return row.member_name;
}
return row.member_name + '(' + row.memberId + ')';
},
},
{
title: '请求方式',
key: 'method',
},
{
title: '请求路径',
key: 'url',
},
{
title: '访问IP',
key: 'ip',
},
// {
// title: 'IP地区',
// key: 'region',
// },
{
title: '状态码',
key: 'errorCode',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.errorCode == 0 ? 'success' : 'warning',
bordered: false,
},
{
default: () => row.errorMsg + '(' + row.errorCode + ')',
}
);
},
},
{
title: 'Goroutine耗时',
key: 'takeUpTime',
render(row) {
return row.takeUpTime + ' ms';
},
},
{
title: '访问时间',
key: 'createdAt',
},
];

View File

@@ -1,200 +1,104 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<div>
<n-card :bordered="false" class="proCard">
<div class="n-layout-page-header">
<n-card :bordered="false" title="登录日志"> 在这里会记录管理后台所有的来访登录情况 </n-card>
</div>
<BasicForm
@register="register"
@submit="reloadTable"
@reset="reloadTable"
@keyup.enter="reloadTable"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:openChecked="true"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
>
<template #tableTitle>
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
<template #toolbar>
<n-button type="primary" @click="reloadTable">login-log刷新数据</n-button>
</template>
</BasicTable>
</n-card>
<BasicTable
:openChecked="true"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button
type="error"
@click="handleBatchDelete"
:disabled="batchDeleteDisabled"
class="min-left-space"
v-if="hasPermission(['/loginLog/delete'])"
>
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
<n-button
type="primary"
@click="handleExport"
class="min-left-space"
v-if="hasPermission(['/loginLog/export'])"
>
<template #icon>
<n-icon>
<ExportOutlined />
</n-icon>
</template>
导出
</n-button>
</template>
</BasicTable>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { getLogList, Delete } from '@/api/log/log';
import { columns } from './columns';
import { BasicForm, useForm } from '@/components/Form/index';
import { usePermission } from '@/hooks/web/usePermission';
import { List, Export, Delete } from '@/api/loginLog';
import { State, columns, schemas } from './model';
import { ExportOutlined, DeleteOutlined } from '@vicons/antd';
import { useRouter } from 'vue-router';
import { DeleteOutlined } from '@vicons/antd';
const { hasPermission } = usePermission();
const router = useRouter();
const actionRef = ref();
const dialog = useDialog();
const message = useMessage();
const searchFormRef = ref<any>({});
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const schemas: FormSchema[] = [
{
field: 'member_id',
component: 'NInput',
label: '操作人员',
componentProps: {
placeholder: '请输入操作人员ID',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ trigger: ['blur'] }],
},
{
field: 'url',
component: 'NInput',
label: '访问路径',
componentProps: {
placeholder: '请输入手机访问路径',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'ip',
component: 'NInput',
label: '访问IP',
componentProps: {
placeholder: '请输入IP地址',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'method',
component: 'NSelect',
label: '请求方式',
componentProps: {
placeholder: '请选择请求方式',
options: [
{
label: 'GET',
value: 'GET',
},
{
label: 'POST',
value: 'POST',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'created_at',
component: 'NDatePicker',
label: '访问时间',
componentProps: {
type: 'datetimerange',
clearable: true,
// defaultValue: [new Date() - 86400000 * 30, new Date()],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'take_up_time',
component: 'NSelect',
label: '请求耗时',
componentProps: {
placeholder: '请选择请求耗时',
options: [
{
label: '50ms内',
value: '50',
},
{
label: '100ms内',
value: '100',
},
{
label: '200ms内',
value: '200',
},
{
label: '500ms内',
value: '500',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'error_code',
component: 'NSelect',
label: '状态码',
componentProps: {
placeholder: '请选择状态码',
options: [
{
label: '0 成功',
value: '0',
},
{
label: '-1 失败',
value: '-1',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const router = useRouter();
const formRef: any = ref(null);
const message = useMessage();
const actionRef = ref();
const formParams = ref({});
const params = ref({
pageSize: 10,
});
const actionColumn = reactive({
width: 220,
width: 300,
title: '操作',
key: 'action',
fixed: 'right',
// fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '查看详情',
onClick: handleEdit.bind(null, record),
onClick: handleView.bind(null, record),
auth: ['/loginLog/view'],
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
auth: ['/loginLog/delete'],
},
],
});
@@ -207,87 +111,62 @@
schemas,
});
function onCheckedRow(rowKeys) {
console.log(rowKeys);
if (rowKeys.length > 0) {
batchDeleteDisabled.value = false;
} else {
batchDeleteDisabled.value = true;
}
const loadDataTable = async (res) => {
return await List({ ...searchFormRef.value?.formModel, ...res });
};
function onCheckedRow(rowKeys) {
batchDeleteDisabled.value = rowKeys.length <= 0;
checkedIds.value = rowKeys;
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
// message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ id: checkedIds.value })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
const loadDataTable = async (res) => {
return await getLogList({ ...formParams.value, ...params.value, ...res });
};
function reloadTable() {
actionRef.value.reload();
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
router.push({ name: 'login_log_view', params: { id: record.id } });
function handleView(record: Recordable) {
router.push({ name: 'log_view', params: { id: record.sysLogId } });
}
function handleSubmit(values: Recordable) {
console.log(values);
formParams.value = values;
reloadTable();
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record).then((_res) => {
message.success('删除成功');
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
function handleReset(values: Recordable) {
console.log(values);
formParams.value = {};
reloadTable();
function handleBatchDelete() {
dialog.warning({
title: '警告',
content: '你确定要批量删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ id: checkedIds.value }).then((_res) => {
message.success('删除成功');
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
function handleExport() {
message.loading('正在导出列表...', { duration: 1200 });
Export(searchFormRef.value?.formModel);
}
</script>

View File

@@ -0,0 +1,189 @@
import { h, ref } from 'vue';
import { NTag } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { isNullObject } from '@/utils/is';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { getOptionLabel, getOptionTag, Options } from '@/utils/hotgo';
import { loginStatusOptions } from '@/enums/optionsiEnum';
export interface State {
id: number;
reqId: string;
memberId: number;
username: string;
response: any;
loginAt: number;
errMsg: string;
status: number;
createdAt: string;
updatedAt: string;
}
export const defaultState = {
id: 0,
reqId: '',
memberId: 0,
username: '',
response: null,
loginAt: 0,
errMsg: '',
status: 1,
createdAt: '',
updatedAt: '',
};
export function newState(state: State | null): State {
if (state !== null) {
return cloneDeep(state);
}
return cloneDeep(defaultState);
}
export const options = ref<Options>({
sys_normal_disable: [],
});
export const rules = {};
export const schemas = ref<FormSchema[]>([
{
field: 'username',
component: 'NInput',
label: '用户名',
componentProps: {
placeholder: '请输入用户名',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'sysLogIp',
component: 'NInput',
label: 'IP地址',
componentProps: {
placeholder: '请输入IP地址',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '状态',
defaultValue: null,
componentProps: {
placeholder: '请选择状态',
options: loginStatusOptions,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'loginAt',
component: 'NDatePicker',
label: '登录时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
]);
export const columns = [
{
title: '记录ID',
key: 'id',
width: 80,
},
{
title: '用户名',
key: 'username',
width: 120,
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: 'info',
bordered: false,
},
{
default: () => row.username,
}
);
},
},
{
title: '登录IP',
key: 'sysLogIp',
width: 160,
},
{
title: 'IP归属地',
key: 'region',
width: 180,
},
{
title: '浏览器',
key: 'browser',
width: 200,
},
{
title: '操作系统',
key: 'os',
width: 150,
},
{
title: '状态',
key: 'status',
render(row) {
if (isNullObject(row.status)) {
return ``;
}
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: getOptionTag(loginStatusOptions, row.status),
bordered: false,
},
{
default: () => getOptionLabel(loginStatusOptions, row.status),
}
);
},
width: 150,
},
{
title: '提示信息',
key: 'errMsg',
render(row) {
if (row.errMsg !== '') {
return row.errMsg;
}
if (row.status === 1) {
return '登录成功';
}
return ``;
},
width: 200,
},
{
title: '登录时间',
key: 'loginAt',
width: 180,
},
];

View File

@@ -1,80 +0,0 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
export const columns = [
{
title: '模块',
key: 'module',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.module == 'admin' ? 'info' : 'success',
bordered: false,
},
{
default: () => row.module,
}
);
},
},
{
title: '操作人',
key: 'member_name',
render(row) {
if (row.memberId === 0) {
return row.member_name;
}
return row.member_name + '(' + row.memberId + ')';
},
},
{
title: '请求方式',
key: 'method',
},
{
title: '请求路径',
key: 'url',
},
{
title: '访问IP',
key: 'ip',
},
// {
// title: 'IP地区',
// key: 'region',
// },
{
title: '状态码',
key: 'errorCode',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.errorCode == 0 ? 'success' : 'warning',
bordered: false,
},
{
default: () => row.errorMsg + '(' + row.errorCode + ')',
}
);
},
},
{
title: 'Goroutine耗时',
key: 'takeUpTime',
render(row) {
return row.takeUpTime + ' ms';
},
},
{
title: '访问时间',
key: 'createdAt',
},
];

View File

@@ -1,6 +1,7 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<n-card :bordered="false" title="短信记录"> 你可以在这里查看到平台所有的短信发送记录 </n-card>
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" ref="searchFormRef">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
@@ -26,140 +27,161 @@
批量删除
</n-button>
</template>
<template #toolbar>
<n-button type="primary" @click="reloadTable">sms-log刷新数据</n-button>
</template>
</BasicTable>
</n-card>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { NTag, useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { getLogList, Delete } from '@/api/log/log';
import { columns } from './columns';
import { getLogList, Delete } from '@/api/log/smslog';
import { useRouter } from 'vue-router';
import { DeleteOutlined } from '@vicons/antd';
import { Dicts } from '@/api/dict/dict';
import { getOptionLabel, getOptionTag, Options } from '@/utils/hotgo';
const options = ref<Options>({
config_sms_template: [],
});
const columns = [
{
title: 'ID',
key: 'id',
width: 100,
},
{
title: '事件模板',
key: 'event',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: getOptionTag(options.value.config_sms_template, row.event),
bordered: false,
},
{
default: () => getOptionLabel(options.value.config_sms_template, row.event),
}
);
},
width: 150,
},
{
title: '手机号',
key: 'mobile',
render(row) {
return row.mobile;
},
width: 180,
},
{
title: '验证码或短信内容',
key: 'code',
width: 200,
},
{
title: '验证次数',
key: 'times',
width: 100,
},
{
title: '发送者IP',
key: 'ip',
width: 200,
},
{
title: '状态码',
key: 'status',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.status == 2 ? 'success' : 'warning',
bordered: false,
},
{
default: () => (row.status == 2 ? '已使用' : '未使用'),
}
);
},
width: 100,
},
{
title: '发送时间',
key: 'createdAt',
width: 180,
},
{
title: '更新时间',
key: 'updatedAt',
width: 180,
},
];
const dialog = useDialog();
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const searchFormRef = ref<any>({});
const schemas: FormSchema[] = [
const schemas = ref<FormSchema[]>([
{
field: 'member_id',
component: 'NInput',
label: '操作人员',
field: 'event',
component: 'NSelect',
label: '事件模板',
componentProps: {
placeholder: '请输入操作人员ID',
placeholder: '请选择事件模板',
options: [],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'mobile',
component: 'NInput',
label: '手机号',
componentProps: {
placeholder: '请输入手机号',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ trigger: ['blur'] }],
},
{
field: 'url',
component: 'NInput',
label: '访问路径',
componentProps: {
placeholder: '请输入手机访问路径',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'ip',
component: 'NInput',
label: '访问IP',
label: '发送者IP',
componentProps: {
placeholder: '请输入IP地址',
placeholder: '请输入IP',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'method',
component: 'NSelect',
label: '请求方式',
componentProps: {
placeholder: '请选择请求方式',
options: [
{
label: 'GET',
value: 'GET',
},
{
label: 'POST',
value: 'POST',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'created_at',
component: 'NDatePicker',
label: '访问时间',
componentProps: {
type: 'datetimerange',
clearable: true,
// defaultValue: [new Date() - 86400000 * 30, new Date()],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'take_up_time',
component: 'NSelect',
label: '请求耗时',
componentProps: {
placeholder: '请选择请求耗时',
options: [
{
label: '50ms内',
value: '50',
},
{
label: '100ms内',
value: '100',
},
{
label: '200ms内',
value: '200',
},
{
label: '500ms内',
value: '500',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'error_code',
field: 'status',
component: 'NSelect',
label: '状态码',
componentProps: {
placeholder: '请选择状态码',
options: [
{
label: '0 成功',
value: '0',
label: '未使用',
value: '1',
},
{
label: '-1 失败',
value: '-1',
label: '已使用',
value: '2',
},
],
onUpdateValue: (e: any) => {
@@ -167,30 +189,21 @@
},
},
},
];
]);
const router = useRouter();
const message = useMessage();
const actionRef = ref();
const formParams = ref({});
const params = ref({
pageSize: 10,
});
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
// fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '查看详情',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
@@ -207,6 +220,7 @@
});
function onCheckedRow(rowKeys) {
console.log(rowKeys);
if (rowKeys.length > 0) {
batchDeleteDisabled.value = false;
} else {
@@ -217,19 +231,25 @@
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
negativeText: '不确定',
onPositiveClick: () => {
Delete(record).then((_res) => {
message.success('操作成功');
reloadTable();
});
Delete(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
// message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('取消');
// message.error('不确定');
},
});
}
@@ -239,10 +259,11 @@
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
negativeText: '不确定',
onPositiveClick: () => {
Delete({ id: checkedIds.value })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
@@ -251,13 +272,14 @@
});
},
onNegativeClick: () => {
// message.error('取消');
// message.error('不确定');
},
});
}
const loadDataTable = async (res) => {
return await getLogList({ ...formParams.value, ...params.value, ...res });
await loadOptions();
return await getLogList({ ...searchFormRef.value?.formModel, ...res });
};
function reloadTable() {
@@ -265,19 +287,32 @@
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
router.push({ name: 'sms_view', params: { id: record.id } });
}
function handleSubmit(values: Recordable) {
formParams.value = values;
console.log(values);
reloadTable();
}
function handleReset(values: Recordable) {
console.log(values);
formParams.value = {};
reloadTable();
}
async function loadOptions() {
options.value = await Dicts({
types: ['config_sms_template'],
});
for (const item of schemas.value) {
switch (item.field) {
case 'event':
item.componentProps.options = options.value.config_sms_template;
break;
}
}
}
</script>
<style lang="less" scoped></style>

View File

@@ -1,136 +0,0 @@
<template>
<div>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
:title="data.id ? '日志详情 ID' + data.id : '日志详情'"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="请求方式">{{ data.method }}</n-descriptions-item>
<n-descriptions-item>
<template #label>请求地址</template>
{{ data.url }}
</n-descriptions-item>
<n-descriptions-item label="请求耗时">{{ data.takeUpTime }} ms</n-descriptions-item>
<n-descriptions-item label="访问IP">{{ data.ip }}</n-descriptions-item>
<n-descriptions-item label="IP归属地">河南 郑州</n-descriptions-item>
<n-descriptions-item label="链路ID">{{ data.reqId }}</n-descriptions-item>
<n-descriptions-item label="响应时间">{{
timestampToTime(data.timestamp)
}}</n-descriptions-item>
<n-descriptions-item label="创建时间">{{ data.createdAt }}</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="访问代理"
>
{{ data.userAgent }}
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="报错信息"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="报错状态码"> {{ data.errorCode }} </n-descriptions-item>
<n-descriptions-item label="报错消息">
<n-tag type="success"> {{ data.errorMsg }} </n-tag>
</n-descriptions-item>
<n-descriptions-item label="报错日志">
<n-tag type="success"> {{ data.errorData }} </n-tag>
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="Header请求头"
>
<JsonViewer
:value="JSON.parse(data.headerData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="GET参数"
>
<JsonViewer
:value="JSON.parse(data.getData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="POST参数"
>
<JsonViewer
:value="JSON.parse(data.postData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { JsonViewer } from 'vue3-json-viewer';
import 'vue3-json-viewer/dist/index.css';
import { useRouter } from 'vue-router';
import { useMessage } from 'naive-ui';
import { View } from '@/api/log/log';
import { timestampToTime } from '@/utils/dateUtil';
const message = useMessage();
const router = useRouter();
const logId = Number(router.currentRoute.value.params.id);
onMounted(async () => {
if (logId === undefined || logId < 1) {
message.error('ID不正确请检查');
return;
}
await getInfo();
});
const data = ref({});
const getInfo = async () => {
data.value = await View({ id: logId });
};
</script>
<style lang="less" scoped></style>

View File

@@ -29,12 +29,12 @@
</template>
</n-input>
</n-form-item>
<n-form-item path="password">
<n-form-item path="pass">
<n-input
@keyup.enter="handleSubmit"
v-model:value="formInline.password"
type="password"
showPasswordOn="click"
v-model:value="formInline.pass"
type="pass"
showpassOn="click"
placeholder="请输入密码"
>
<template #prefix>
@@ -44,6 +44,28 @@
</template>
</n-input>
</n-form-item>
<n-form-item path="code">
<n-input-group>
<n-input
:style="{ width: '100%' }"
placeholder="验证码"
@keyup.enter="handleSubmit"
v-model:value="formInline.code"
>
<template #prefix>
<n-icon size="18" color="#808695" :component="SafetyCertificateOutlined" />
</template>
<template #suffix> </template>
</n-input>
<img
style="width: 100px"
:src="codeBase64"
@click="refreshCode"
loading="lazy"
alt="点击获取"
/>
</n-input-group>
</n-form-item>
<n-form-item class="default-color">
<div class="flex justify-between">
<div class="flex-initial">
@@ -90,16 +112,22 @@
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { ref, unref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useUserStore } from '@/store/modules/user';
import { useMessage } from 'naive-ui';
import { ResultEnum } from '@/enums/httpEnum';
import { PersonOutline, LockClosedOutline, LogoGithub, LogoFacebook } from '@vicons/ionicons5';
import { PageEnum } from '@/enums/pageEnum';
import { SafetyCertificateOutlined } from '@vicons/antd';
import { GetCaptcha } from '@/api/base';
import { aesEcb } from '@/utils/encrypt';
interface FormState {
username: string;
pass: string;
cid: string;
code: string;
password: string;
}
@@ -107,17 +135,21 @@
const message = useMessage();
const loading = ref(false);
const autoLogin = ref(true);
const codeBase64 = ref('');
const LOGIN_NAME = PageEnum.BASE_LOGIN_NAME;
const formInline = reactive({
const formInline = ref<FormState>({
username: '',
pass: '',
cid: '',
code: '',
password: '',
isCaptcha: true,
});
const rules = {
username: { required: true, message: '请输入用户名', trigger: 'blur' },
password: { required: true, message: '请输入密码', trigger: 'blur' },
pass: { required: true, message: '请输入密码', trigger: 'blur' },
code: { required: true, message: '请输入验证码', trigger: 'blur' },
};
const userStore = useUserStore();
@@ -129,17 +161,15 @@
e.preventDefault();
formRef.value.validate(async (errors) => {
if (!errors) {
const { username, password } = formInline;
message.loading('登录中...');
loading.value = true;
const params: FormState = {
username,
password,
};
try {
const { code, message: msg } = await userStore.login(params);
const { code, message: msg } = await userStore.login({
username: formInline.value.username,
password: aesEcb.encrypt(formInline.value.pass),
cid: formInline.value.cid,
code: formInline.value.code,
});
message.destroyAll();
if (code == ResultEnum.SUCCESS) {
const toPath = decodeURIComponent((route.query?.redirect || '/') as string);
@@ -149,6 +179,7 @@
} else await router.replace(toPath);
} else {
message.info(msg || '登录失败');
await refreshCode();
}
} finally {
loading.value = false;
@@ -158,6 +189,19 @@
}
});
};
async function refreshCode() {
const data = await GetCaptcha();
codeBase64.value = data.base64;
formInline.value.cid = data.cid;
formInline.value.code = '';
}
onMounted(() => {
setTimeout(function () {
refreshCode();
});
});
</script>
<style lang="less" scoped>

View File

@@ -6,7 +6,7 @@ export const columns = [
{
title: '会话编号',
key: 'id',
width: 240,
width: 280,
render(row) {
return h(
NTag,

View File

@@ -12,7 +12,6 @@
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
/>
</n-card>
@@ -40,21 +39,29 @@
},
rules: [{ trigger: ['blur'] }],
},
{
field: 'addr',
component: 'NInput',
label: '登录地址',
componentProps: {
placeholder: '请输入登录地址',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ trigger: ['blur'] }],
},
];
const message = useMessage();
const actionRef = ref();
const formParams = ref({});
const params = ref({
pageSize: 10,
});
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
// fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
@@ -93,7 +100,7 @@
}
const loadDataTable = async (res) => {
return await OnlineList({ ...formParams.value, ...params.value, ...res });
return await OnlineList({ ...formParams.value, ...res });
};
function reloadTable() {

View File

@@ -1,80 +0,0 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
export const columns = [
{
title: '模块',
key: 'module',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.module == 'admin' ? 'info' : 'success',
bordered: false,
},
{
default: () => row.module,
}
);
},
},
{
title: '操作人',
key: 'member_name',
render(row) {
if (row.memberId === 0) {
return row.member_name;
}
return row.member_name + '(' + row.memberId + ')';
},
},
{
title: '请求方式',
key: 'method',
},
{
title: '请求路径',
key: 'url',
},
{
title: '访问IP',
key: 'ip',
},
// {
// title: 'IP地区',
// key: 'region',
// },
{
title: '状态码',
key: 'errorCode',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.errorCode == 0 ? 'success' : 'warning',
bordered: false,
},
{
default: () => row.errorMsg + '(' + row.errorCode + ')',
}
);
},
},
{
title: 'Goroutine耗时',
key: 'takeUpTime',
render(row) {
return row.takeUpTime + ' ms';
},
},
{
title: '访问时间',
key: 'createdAt',
},
];

View File

@@ -1,199 +1,149 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<div>
<n-card :bordered="false" class="proCard">
<div class="n-layout-page-header">
<n-card :bordered="false" title="服务日志">
在这里开发者可以快速定位服务端在运行时产生的重要日志方便排查系统异常和日常运维
</n-card>
</div>
<BasicForm
@register="register"
@submit="reloadTable"
@reset="reloadTable"
@keyup.enter="reloadTable"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:openChecked="true"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
>
<template #tableTitle>
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
<BasicTable
:openChecked="true"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
:resizeHeightOffset="-10000"
size="small"
>
<template #tableTitle>
<n-button
type="error"
@click="handleBatchDelete"
:disabled="batchDeleteDisabled"
class="min-left-space"
v-if="hasPermission(['/serveLog/delete'])"
>
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
<n-button
type="primary"
@click="handleExport"
class="min-left-space"
v-if="hasPermission(['/serveLog/delete'])"
>
<template #icon>
<n-icon>
<ExportOutlined />
</n-icon>
</template>
导出
</n-button>
</template>
</BasicTable>
<template #toolbar>
<n-button type="primary" @click="reloadTable">系统刷新数据</n-button>
</template>
</BasicTable>
</n-card>
<n-modal v-model:show="showModal" :show-icon="false" preset="dialog" style="width: 920px">
<n-card
:bordered="false"
title="日志内容"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
>
<n-alert type="error" :show-icon="false">
{{ preview?.content }}
</n-alert>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="堆栈打印"
>
<JsonViewer
:value="JSON.parse(preview?.stack)"
:expand-depth="10"
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">关闭</n-button>
</n-space>
</template>
</n-modal>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { getLogList, Delete } from '@/api/log/log';
import { columns } from './columns';
import { BasicForm, useForm } from '@/components/Form/index';
import { usePermission } from '@/hooks/web/usePermission';
import { List, Export, Delete } from '@/api/serveLog';
import { State, columns, schemas } from './model';
import { ExportOutlined, DeleteOutlined } from '@vicons/antd';
import { useRouter } from 'vue-router';
import { DeleteOutlined } from '@vicons/antd';
import { JsonViewer } from 'vue3-json-viewer';
import 'vue3-json-viewer/dist/index.css';
const { hasPermission } = usePermission();
const router = useRouter();
const actionRef = ref();
const dialog = useDialog();
const message = useMessage();
const searchFormRef = ref<any>({});
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const schemas: FormSchema[] = [
{
field: 'member_id',
component: 'NInput',
label: '操作人员',
componentProps: {
placeholder: '请输入操作人员ID',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ trigger: ['blur'] }],
},
{
field: 'url',
component: 'NInput',
label: '访问路径',
componentProps: {
placeholder: '请输入手机访问路径',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'ip',
component: 'NInput',
label: '访问IP',
componentProps: {
placeholder: '请输入IP地址',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'method',
component: 'NSelect',
label: '请求方式',
componentProps: {
placeholder: '请选择请求方式',
options: [
{
label: 'GET',
value: 'GET',
},
{
label: 'POST',
value: 'POST',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'created_at',
component: 'NDatePicker',
label: '访问时间',
componentProps: {
type: 'datetimerange',
clearable: true,
// defaultValue: [new Date() - 86400000 * 30, new Date()],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'take_up_time',
component: 'NSelect',
label: '请求耗时',
componentProps: {
placeholder: '请选择请求耗时',
options: [
{
label: '50ms内',
value: '50',
},
{
label: '100ms内',
value: '100',
},
{
label: '200ms内',
value: '200',
},
{
label: '500ms内',
value: '500',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'error_code',
component: 'NSelect',
label: '状态码',
componentProps: {
placeholder: '请选择状态码',
options: [
{
label: '0 成功',
value: '0',
},
{
label: '-1 失败',
value: '-1',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const router = useRouter();
const message = useMessage();
const actionRef = ref();
const formParams = ref({});
const params = ref({
pageSize: 10,
});
const showModal = ref(false);
const formParams = ref<State>();
const actionColumn = reactive({
width: 220,
width: 300,
title: '操作',
key: 'action',
fixed: 'right',
// fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '查看详情',
onClick: handleEdit.bind(null, record),
label: '详细报错',
onClick: handleStack.bind(null, record),
},
{
label: '访问日志',
onClick: handleView.bind(null, record),
ifShow: record.sysLogId > 0,
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
auth: ['/serveLog/delete'],
},
],
});
@@ -206,17 +156,30 @@
schemas,
});
function onCheckedRow(rowKeys) {
console.log(rowKeys);
if (rowKeys.length > 0) {
batchDeleteDisabled.value = false;
} else {
batchDeleteDisabled.value = true;
}
const loadDataTable = async (res) => {
return await List({ ...searchFormRef.value?.formModel, ...res });
};
function onCheckedRow(rowKeys) {
batchDeleteDisabled.value = rowKeys.length <= 0;
checkedIds.value = rowKeys;
}
function reloadTable() {
actionRef.value.reload();
}
const preview = ref<Recordable>();
function handleStack(record: Recordable) {
console.log('handleStack record:' + JSON.stringify(record));
showModal.value = true;
preview.value = record;
}
function handleView(record: Recordable) {
router.push({ name: 'log_view', params: { id: record.sysLogId } });
}
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
@@ -225,7 +188,7 @@
negativeText: '取消',
onPositiveClick: () => {
Delete(record).then((_res) => {
message.success('操作成功');
message.success('删除成功');
reloadTable();
});
},
@@ -235,15 +198,15 @@
});
}
function batchDelete() {
function handleBatchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
content: '你确定要批量删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ id: checkedIds.value }).then((_res) => {
message.success('操作成功');
message.success('删除成功');
reloadTable();
});
},
@@ -253,26 +216,9 @@
});
}
const loadDataTable = async (res) => {
return await getLogList({ ...formParams.value, ...params.value, ...res });
};
function reloadTable() {
actionRef.value.reload();
}
function handleEdit(record: Recordable) {
router.push({ name: 'serve_log_view', params: { id: record.id } });
}
function handleSubmit(values: Recordable) {
formParams.value = values;
reloadTable();
}
function handleReset(_values: Recordable) {
formParams.value = {};
reloadTable();
function handleExport() {
message.loading('正在导出列表...', { duration: 1200 });
Export(searchFormRef.value?.formModel);
}
</script>

View File

@@ -0,0 +1,189 @@
import { h, ref } from 'vue';
import { NAvatar, NImage, NTag, NSwitch, NRate } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { Dicts } from '@/api/dict/dict';
import { isArray, isNullObject } from '@/utils/is';
import { getFileExt } from '@/utils/urlUtils';
import { defRangeShortcuts, defShortcuts, formatToDate } from '@/utils/dateUtil';
import { format } from 'date-fns';
import { getOptionLabel, getOptionTag, Options, errorImg } from '@/utils/hotgo';
export interface State {
id: number;
env: string;
traceid: string;
levelFormat: string;
content: string;
stack: any;
line: string;
triggerNs: number;
status: number;
createdAt: string;
updatedAt: string;
}
export const defaultState = {
id: 0,
env: '',
traceid: '',
levelFormat: '',
content: '',
stack: null,
line: '',
triggerNs: 0,
status: 1,
createdAt: '',
updatedAt: '',
};
export function newState(state: State | null): State {
if (state !== null) {
return cloneDeep(state);
}
return cloneDeep(defaultState);
}
export const options = ref<Options>({
sys_normal_disable: [],
sys_log_type: [],
});
export const rules = {};
export const schemas = ref<FormSchema[]>([
{
field: 'traceId',
component: 'NInput',
label: '链路ID',
componentProps: {
placeholder: '请输入链路ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'levelFormat',
component: 'NSelect',
label: '日志级别',
defaultValue: null,
componentProps: {
placeholder: '请选择日志级别',
options: [],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'createdAt',
component: 'NDatePicker',
label: '创建时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
]);
export const columns = [
{
title: '日志ID',
key: 'id',
width: 80,
},
{
title: '链路ID',
key: 'traceId',
width: 280,
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: 'default',
bordered: false,
checkable: true,
},
{
default: () => row.traceId,
}
);
},
},
{
title: '日志级别',
key: 'levelFormat',
render(row) {
if (isNullObject(row.levelFormat)) {
return ``;
}
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: getOptionTag(options.value.sys_log_type, row.levelFormat),
bordered: false,
},
{
default: () => getOptionLabel(options.value.sys_log_type, row.levelFormat),
}
);
},
width: 120,
},
{
title: '日志内容',
key: 'content',
width: 320,
},
{
title: '调用行',
key: 'line',
width: 150,
},
{
title: '触发时间',
key: 'triggerNs',
width: 200,
render(row) {
if (row.triggerNs <= 0) {
return '-';
}
return format(new Date(row.triggerNs / 1000000), 'yyyy-MM-dd HH:mm:ss.SSS');
},
},
{
title: '记录时间',
key: 'createdAt',
width: 150,
},
];
async function loadOptions() {
options.value = await Dicts({
types: ['sys_normal_disable', 'sys_log_type'],
});
for (const item of schemas.value) {
switch (item.field) {
case 'status':
item.componentProps.options = options.value.sys_normal_disable;
break;
case 'levelFormat':
item.componentProps.options = options.value.sys_log_type;
break;
}
}
}
await loadOptions();

View File

@@ -1,136 +0,0 @@
<template>
<div>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
:title="data.id ? '日志详情 ID' + data.id : '日志详情'"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="请求方式">{{ data.method }}</n-descriptions-item>
<n-descriptions-item>
<template #label>请求地址</template>
{{ data.url }}
</n-descriptions-item>
<n-descriptions-item label="请求耗时">{{ data.takeUpTime }} ms</n-descriptions-item>
<n-descriptions-item label="访问IP">{{ data.ip }}</n-descriptions-item>
<n-descriptions-item label="IP归属地">河南 郑州</n-descriptions-item>
<n-descriptions-item label="链路ID">{{ data.reqId }}</n-descriptions-item>
<n-descriptions-item label="响应时间">{{
timestampToTime(data.timestamp)
}}</n-descriptions-item>
<n-descriptions-item label="创建时间">{{ data.createdAt }}</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="访问代理"
>
{{ data.userAgent }}
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="报错信息"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="报错状态码"> {{ data.errorCode }} </n-descriptions-item>
<n-descriptions-item label="报错消息">
<n-tag type="success"> {{ data.errorMsg }} </n-tag>
</n-descriptions-item>
<n-descriptions-item label="报错日志">
<n-tag type="success"> {{ data.errorData }} </n-tag>
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="Header请求头"
>
<JsonViewer
:value="JSON.parse(data.headerData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="GET参数"
>
<JsonViewer
:value="JSON.parse(data.getData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="POST参数"
>
<JsonViewer
:value="JSON.parse(data.postData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { JsonViewer } from 'vue3-json-viewer';
import 'vue3-json-viewer/dist/index.css';
import { useRouter } from 'vue-router';
import { useMessage } from 'naive-ui';
import { View } from '@/api/log/log';
import { timestampToTime } from '@/utils/dateUtil';
const message = useMessage();
const router = useRouter();
const logId = Number(router.currentRoute.value.params.id);
onMounted(async () => {
if (logId === undefined || logId < 1) {
message.error('ID不正确请检查');
return;
}
await getInfo();
});
const data = ref({});
const getInfo = async () => {
data.value = await View({ id: logId });
};
</script>
<style lang="less" scoped></style>

View File

@@ -20,7 +20,7 @@
<PlusOutlined />
</n-icon>
</template>
新建部门
添加部门
</n-button>
</n-space>
@@ -29,6 +29,7 @@
:data="data"
:row-key="rowKey"
:loading="loading"
:resizeHeightOffset="-20000"
default-expand-all
/>
</n-space>
@@ -37,7 +38,7 @@
v-model:show="showModal"
:show-icon="false"
preset="dialog"
:title="formParams?.id > 0 ? '编辑部门 #' + formParams?.id : '新建部门'"
:title="formParams?.id > 0 ? '编辑部门 #' + formParams?.id : '添加部门'"
>
<n-form
:model="formParams"
@@ -73,9 +74,9 @@
<n-input placeholder="请输入邮箱" v-model:value="formParams.email" />
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="formParams.sort" clearable />
</n-form-item>
<!-- <n-form-item label="排序" path="sort">-->
<!-- <n-input-number v-model:value="formParams.sort" clearable />-->
<!-- </n-form-item>-->
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
@@ -198,18 +199,26 @@
const data = ref([]);
const columns: DataTableColumns<RowData> = [
{
type: 'selection',
},
{
title: '部门名称',
title: '部门',
key: 'name',
render(row) {
return h(
NTag,
{
type: 'info',
},
{
default: () => row.name,
}
);
},
width: 200,
},
{
title: '部门ID',
key: 'index',
width: 100,
},
// {
// title: '部门ID',
// key: 'index',
// width: 100,
// },
{
title: '部门编码',
key: 'code',
@@ -250,15 +259,15 @@
);
},
},
{
title: '排序',
key: 'sort',
width: 80,
},
// {
// title: '排序',
// key: 'sort',
// width: 80,
// },
{
title: '创建时间',
key: 'createdAt',
width: 200,
width: 150,
render: (rows, _) => {
return rows.createdAt; //timestampToTime();
},
@@ -300,7 +309,7 @@
function handleEdit(record: Recordable) {
showModal.value = true;
formParams.value = record;
formParams.value = cloneDeep(record);
formParams.value.children = null;
optionsDefaultValue.value = formParams.value.pid;
}

View File

@@ -5,17 +5,23 @@ export const columns = [
{
title: 'ID',
key: 'id',
width: 100,
width: 80,
},
{
title: '岗位名称',
key: 'name',
width: 100,
},
{
title: '岗位名称',
title: '岗位',
key: 'name',
width: 100,
render(row) {
return h(
NTag,
{
type: 'info',
},
{
default: () => row.name,
}
);
},
},
{
title: '岗位编码',
@@ -42,15 +48,15 @@ export const columns = [
);
},
},
{
title: '排序',
key: 'sort',
width: 100,
},
// {
// title: '排序',
// key: 'sort',
// width: 100,
// },
{
title: '创建时间',
key: 'createdAt',
width: 100,
width: 150,
render: (rows, _) => {
return rows.createdAt;
},

View File

@@ -30,7 +30,7 @@
<PlusOutlined />
</n-icon>
</template>
新建岗位
添加岗位
</n-button>
&nbsp;
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
@@ -48,7 +48,7 @@
v-model:show="showModal"
:show-icon="false"
preset="dialog"
:title="formParams?.id > 0 ? '编辑岗位 #' + formParams?.id : '新建岗位'"
:title="formParams?.id > 0 ? '编辑岗位 #' + formParams?.id : '添加岗位'"
>
<n-form
:model="formParams"
@@ -65,9 +65,9 @@
<n-input placeholder="请输入岗位编码" v-model:value="formParams.code" />
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="formParams.sort" clearable />
</n-form-item>
<!-- <n-form-item label="排序" path="sort">-->
<!-- <n-input-number v-model:value="formParams.sort" clearable />-->
<!-- </n-form-item>-->
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
@@ -244,18 +244,14 @@
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
Edit(formParams.value)
.then((_res) => {
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
reloadTable();
formParams.value = ref(resetFormParams);
});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
Edit(formParams.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
reloadTable();
formParams.value = ref(resetFormParams);
});
});
} else {
message.error('请填写完整信息');
}
@@ -293,15 +289,11 @@
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ id: checkedIds.value })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
Delete({ id: checkedIds.value }).then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');

View File

@@ -30,7 +30,7 @@
<PlusOutlined />
</n-icon>
</template>
新建用户
添加用户
</n-button>
<n-button
type="error"
@@ -52,7 +52,7 @@
v-model:show="showModal"
:show-icon="false"
preset="dialog"
:title="formParams?.id > 0 ? '编辑用户 #' + formParams?.id : '新建用户'"
:title="formParams?.id > 0 ? '编辑用户 #' + formParams?.id : '添加用户'"
:style="{
width: dialogWidth,
}"

View File

@@ -355,7 +355,6 @@
import CreateDrawer from './CreateDrawer.vue';
import IconSelector from '@/components/IconSelector/index.vue';
import { State, newState } from '@/views/permission/menu/model';
import { validate } from '@/utils/validateUtil';
const menuTypes = [
{

View File

@@ -2,12 +2,12 @@ import { h } from 'vue';
import { NTag } from 'naive-ui';
export const columns = [
// {
// title: '角色ID',
// key: 'id',
// },
{
title: '角色ID',
key: 'id',
},
{
title: '角色名称',
title: '角色',
key: 'name',
render(row) {
return h(
@@ -20,13 +20,19 @@ export const columns = [
}
);
},
width: 150,
},
{
title: '上级角色',
key: 'pid',
title: '角色编码',
key: 'key',
width: 150,
},
// {
// title: '上级角色',
// key: 'pid',
// },
{
title: '是否默认角色',
title: '默认角色',
key: 'isDefault',
render(row) {
return h(
@@ -39,17 +45,41 @@ export const columns = [
}
);
},
width: 80,
},
{
title: '排序',
key: 'sort',
width: 100,
},
{
title: '备注',
key: 'remark',
width: 300,
},
{
title: '状态',
key: 'status',
width: 80,
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.status == 1 ? 'info' : 'error',
bordered: false,
},
{
default: () => (row.status == 1 ? '正常' : '已禁用'),
}
);
},
},
{
title: '创建时间',
key: 'createdAt',
width: 180,
},
];

View File

@@ -11,6 +11,8 @@
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:pagination="false"
:resizeHeightOffset="-20000"
>
<template #tableTitle>
<n-button type="primary" @click="addTable">
@@ -22,15 +24,11 @@
添加角色
</n-button>
</template>
<template #action>
<TableAction />
</template>
</BasicTable>
</n-card>
<n-modal v-model:show="showModal" :show-icon="false" preset="dialog" :title="editRoleTitle">
<div class="py-3 menu-list">
<div class="py-3 menu-list" :style="{ maxHeight: '90vh', height: '70vh' }">
<n-tree
block-line
cascade
@@ -58,7 +56,12 @@
</template>
</n-modal>
<n-modal v-model:show="showModal2" :show-icon="false" preset="dialog" title="添加角色">
<n-modal
v-model:show="showModal2"
:show-icon="false"
preset="dialog"
:title="formParams.id > 0 ? '编辑角色 #' + formParams.id : '添加角色'"
>
<n-form
:model="formParams"
:rules="rules"
@@ -68,7 +71,13 @@
class="py-4"
>
<n-form-item label="上级角色" path="pid">
<n-input placeholder="请输入上级角色ID" v-model:value="formParams.pid" />
<n-tree-select
:options="optionTreeData"
:default-value="formParams.pid"
key-field="id"
label-field="name"
:on-update:value="onUpdateValuePid"
/>
</n-form-item>
<n-form-item label="角色名称" path="name">
<n-input placeholder="请输入名称" v-model:value="formParams.name" />
@@ -76,9 +85,9 @@
<n-form-item label="权限编码" path="key">
<n-input placeholder="请输入" v-model:value="formParams.key" />
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="formParams.sort" clearable />
</n-form-item>
<!-- <n-form-item label="排序" path="sort">-->
<!-- <n-input-number v-model:value="formParams.sort" clearable />-->
<!-- </n-form-item>-->
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
@@ -117,11 +126,7 @@
class="py-4"
>
<n-form-item label="数据范围" path="dataScope">
<n-select
v-model:value="dataForm.dataScope"
:options="dataScopeOption"
@update:value="handleUpdateDataScopeValue"
/>
<n-select v-model:value="dataForm.dataScope" :options="dataScopeOption" />
</n-form-item>
<n-form-item label="自定义权限" path="customDept" v-if="dataForm.dataScope === 4">
<n-tree-select
@@ -148,8 +153,8 @@
<script lang="ts" setup>
import { h, onMounted, reactive, ref } from 'vue';
import { TreeSelectOption, useDialog, useMessage, SelectOption } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { TreeSelectOption, useDialog, useMessage } from 'naive-ui';
import { BasicColumn, BasicTable, TableAction } from '@/components/Table';
import {
Delete,
Edit,
@@ -181,6 +186,7 @@
const expandedKeys = ref([]);
const checkedKeys = ref<any>([]);
const updatePermissionsParams = ref<any>({});
const optionTreeData = ref<any>([]);
const rules = {
name: {
@@ -211,7 +217,7 @@
let formParams = ref<any>(cloneDeep(defaultState));
const actionColumn = reactive({
const actionColumn = reactive<BasicColumn>({
width: 320,
title: '操作',
key: 'action',
@@ -254,7 +260,7 @@
});
const loadDataTable = async (res: any) => {
return await getRoleList({ ...res });
return await getRoleList({ ...res, ...{ pageSize: 100, page: 1 } });
};
function onCheckedRow(rowKeys: any[]) {
@@ -263,6 +269,7 @@
function reloadTable() {
actionRef.value.reload();
loadDataList();
}
function confirmForm(e: any) {
@@ -332,7 +339,7 @@
async function handleMenuAuth(record: Recordable) {
editRoleTitle.value = `分配 ${record.name} 的菜单权限`;
const data = await GetPermissions({ ...{ id: record.id } });
checkedKeys.value = data.menuIds; //record.menu_keys;
checkedKeys.value = data.menuIds;
updatePermissionsParams.value.id = record.id;
showModal.value = true;
}
@@ -348,8 +355,6 @@
showDataModal.value = true;
}
function handleUpdateDataScopeValue(value: string, option: SelectOption) {}
function handleUpdateDeptValue(
value: string | number | Array<string | number> | null,
_option: TreeSelectOption | null | Array<TreeSelectOption | null>
@@ -362,7 +367,6 @@
dataFormBtnLoading.value = true;
dataFormRef.value.validate((errors) => {
if (!errors) {
console.log('dataForm.value:' + JSON.stringify(dataForm.value));
DataScopeEdit(dataForm.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
@@ -404,11 +408,26 @@
}
onMounted(async () => {
await loadDataList();
await loadMenuList();
await loadDeptList();
await loadDataScopeSelect();
});
async function loadDataList() {
const data = await getRoleList({ pageSize: 100, page: 1 });
optionTreeData.value = [
{
id: 0,
key: 0,
label: '顶级角色',
pid: 0,
name: '顶级角色',
},
];
optionTreeData.value = optionTreeData.value.concat(data.list);
}
async function loadMenuList() {
const treeMenuList = await getMenuList();
expandedKeys.value = treeMenuList.list.map((item) => item.key);
@@ -426,6 +445,10 @@
const option = await DataScopeSelect();
dataScopeOption.value = option.list;
}
function onUpdateValuePid(value: string | number) {
formParams.value.pid = value;
}
</script>
<style lang="less" scoped></style>

View File

@@ -30,7 +30,7 @@
<PlusOutlined />
</n-icon>
</template>
新建
添加策略
</n-button>
&nbsp;
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
@@ -48,7 +48,7 @@
v-model:show="showModal"
:show-icon="false"
preset="dialog"
title="新建"
:title="formParams?.id > 0 ? '编辑策略 #' + formParams.id : '添加策略'"
style="width: 720px"
>
<n-form
@@ -59,8 +59,8 @@
:label-width="80"
class="py-4"
>
<n-form-item label="IP地址" path="ip">
<n-input type="textarea" placeholder="请输入IP地址" v-model:value="formParams.ip" />
<n-form-item label="IP策略" path="ip">
<n-input type="textarea" placeholder="请输入IP策略" v-model:value="formParams.ip" />
<template #feedback>
<p>支持添加IP如果添加多个IP请用","隔开</p>
<p>支持添加IP段,如192.168.0.0/24</p>
@@ -72,7 +72,7 @@
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
<n-radio-button
v-for="status in statusOptions"
v-for="status in blacklistOptions"
:key="status.value"
:value="status.value"
:label="status.label"
@@ -107,8 +107,23 @@
import { Dict } from '@/api/dict/dict';
import { getOptionLabel, getOptionTag } from '@/utils/hotgo';
const blacklistOptions = [
{
value: 1,
label: '封禁中',
listClass: 'warning',
},
{
value: 2,
label: '已解封',
listClass: 'success',
},
].map((s) => {
return s;
});
const options = ref({
status: [],
status: blacklistOptions,
});
const columns = [
@@ -185,7 +200,7 @@
defaultValue: null,
componentProps: {
placeholder: '请选择类型',
options: [],
options: blacklistOptions,
onUpdateValue: (e: any) => {
console.log(e);
},
@@ -350,19 +365,6 @@
});
});
}
async function loadOptions() {
options.value.status = await Dict('sys_normal_disable');
for (const item of schemas.value) {
if (item.field === 'status') {
item.componentProps.options = options.value.status;
}
}
}
onMounted(async () => {
await loadOptions();
});
</script>
<style lang="less" scoped></style>

View File

@@ -35,13 +35,10 @@
<n-input v-model:value="formValue.smtpAdminMailbox" placeholder="" />
</n-form-item>
<n-form-item>
<n-button size="small" type="default" @click="sendTest">发送测试邮件</n-button>
</n-form-item>
<div>
<n-space>
<n-button type="primary" @click="formSubmit">保存更新</n-button>
<n-button type="default" @click="sendTest">发送测试邮件</n-button>
</n-space>
</div>
</n-form>
@@ -118,11 +115,9 @@
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
console.log('formParams:' + JSON.stringify(formParams.value));
showModal.value = false;
sendTestEmail(formParams.value).then((_res) => {
message.success('发送成功');
showModal.value = false;
});
} else {
message.error('请填写完整信息');
@@ -139,8 +134,6 @@
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
console.log('formValue.value:' + JSON.stringify(formValue.value));
updateConfig({ group: group.value, list: formValue.value })
.then((res) => {
console.log('res:' + JSON.stringify(res));

View File

@@ -8,7 +8,7 @@
<n-form-item label="默认驱动" path="smsDrive">
<n-select
placeholder="默认发送驱动"
:options="driveList"
:options="options.config_sms_drive"
v-model:value="formValue.smsDrive"
/>
</n-form-item>
@@ -39,11 +39,7 @@
<n-divider title-placement="left">阿里云</n-divider>
<n-form-item label="AccessKeyID" path="smsAliyunAccessKeyID">
<n-input
v-model:value="formValue.smsAliyunAccessKeyID"
placeholder=""
type="password"
/>
<n-input v-model:value="formValue.smsAliyunAccessKeyID" placeholder="" />
<template #feedback
>应用key和密钥你可以通过 https://ram.console.aliyun.com/manage/ak 获取</template
>
@@ -53,8 +49,15 @@
<n-input
type="password"
v-model:value="formValue.smsAliyunAccessKeySecret"
placeholder=""
/>
show-password-on="click"
>
<template #password-visible-icon>
<n-icon :size="16" :component="GlassesOutline" />
</template>
<template #password-invisible-icon>
<n-icon :size="16" :component="Glasses" />
</template>
</n-input>
</n-form-item>
<n-form-item label="签名" path="smsAliyunSign">
@@ -68,7 +71,7 @@
<n-dynamic-input
v-model:value="formValue.smsAliyunTemplate"
preset="pair"
key-placeholder="key"
key-placeholder="事件KEY"
value-placeholder="模板CODE"
/>
</n-form-item>
@@ -76,22 +79,74 @@
<div>
<n-space>
<n-button type="primary" @click="formSubmit">保存更新</n-button>
<n-button type="default" @click="sendTest">发送测试短信</n-button>
</n-space>
</div>
</n-form>
</n-grid-item>
</n-grid>
</n-spin>
<n-modal
:block-scroll="false"
:mask-closable="false"
v-model:show="showModal"
:show-icon="false"
preset="dialog"
title="发送测试短信"
>
<n-form
:model="formParams"
:rules="rules"
ref="formTestRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="事件模板" path="event">
<n-select :options="options.config_sms_template" v-model:value="formParams.event" />
</n-form-item>
<n-form-item label="手机号" path="mobile">
<n-input
placeholder="请输入接收手机号"
v-model:value="formParams.mobile"
:required="true"
/>
</n-form-item>
<n-form-item label="验证码" path="code">
<n-input
placeholder="请输入要接收的验证码"
v-model:value="formParams.code"
:required="true"
/>
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">关闭</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">发送</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useMessage } from 'naive-ui';
import { getConfig, updateConfig } from '@/api/sys/config';
import { getConfig, sendTestSms, updateConfig } from '@/api/sys/config';
import { Dicts } from '@/api/dict/dict';
import { Options } from '@/utils/hotgo';
import { GlassesOutline, Glasses } from '@vicons/ionicons5';
const group = ref('sms');
const show = ref(false);
const showModal = ref(false);
const formBtnLoading = ref(false);
const formParams = ref({ mobile: '', event: '', code: '1234' });
const rules = {
smsDrive: {
@@ -101,19 +156,15 @@
},
};
const driveList = [
{
label: '阿里云',
value: 'aliyun',
},
{
label: '腾讯云',
value: 'tencent',
},
];
const formTestRef = ref<any>();
const formRef: any = ref(null);
const message = useMessage();
const options = ref<Options>({
config_sms_template: [],
config_sms_drive: [],
});
const formValue = ref({
smsDrive: 'aliyun',
smsAliyunAccessKeyID: '',
@@ -125,6 +176,11 @@
smsCodeExpire: 600,
});
function sendTest() {
showModal.value = true;
formBtnLoading.value = false;
}
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
@@ -149,8 +205,9 @@
load();
});
function load() {
async function load() {
show.value = true;
await loadOptions();
new Promise((_resolve, _reject) => {
getConfig({ group: group.value })
.then((res) => {
@@ -164,4 +221,26 @@
});
});
}
async function loadOptions() {
options.value = await Dicts({
types: ['config_sms_template', 'config_sms_drive'],
});
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formTestRef.value.validate((errors) => {
if (!errors) {
sendTestSms(formParams.value).then((_res) => {
message.success('发送成功');
showModal.value = false;
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
</script>

View File

@@ -30,7 +30,7 @@
<PlusOutlined />
</n-icon>
</template>
新建
添加任务
</n-button>
&nbsp;
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
@@ -57,7 +57,7 @@
v-model:show="showModal"
:show-icon="false"
preset="dialog"
title="新建"
:title="formParams?.id > 0 ? '编辑任务 #' + formParams.id : '添加任务'"
style="width: 720px"
>
<n-form
@@ -100,9 +100,8 @@
</n-radio-group>
</n-form-item>
<n-form-item label="执行次数" path="count">
<n-form-item label="执行次数" path="count" v-if="formParams.policy === 4">
<n-input placeholder="请输入执行次数" v-model:value="formParams.count" />
<template #feedback> 仅在单次、多次策略时生效</template>
</n-form-item>
<n-form-item label="定时表达式" path="pattern">
@@ -154,7 +153,7 @@
import { TreeSelectOption, useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { Delete, Edit, getSelect, List, Status } from '@/api/sys/cron';
import { Delete, Edit, getSelect, List, Status, OnlineExec } from '@/api/sys/cron';
import { columns } from './columns';
import { DeleteOutlined, GroupOutlined, PlusOutlined } from '@vicons/antd';
import { statusActions } from '@/enums/optionsiEnum';
@@ -369,8 +368,21 @@
}
function handleExecute(record: Recordable) {
console.log('点击了handleExecute', record);
message.error('暂未配置');
dialog.warning({
title: '警告',
content: '提交成功后将立即执行一次,你确定要执行吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
OnlineExec(record).then((_res) => {
message.success('提交成功,执行结果请登录控制台查看日志!');
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
function handleDelete(record: Recordable) {

View File

@@ -20,7 +20,7 @@
<PlusOutlined />
</n-icon>
</template>
新建分组
添加分组
</n-button>
</template>
</BasicTable>
@@ -82,7 +82,7 @@
import { BasicTable, TableAction } from '@/components/Table';
import { columns } from './columns';
import { PlusOutlined } from '@vicons/antd';
import { GroupDelete, GroupEdit, GroupList, getSelect } from '@/api/sys/cron';
import { GroupDelete, GroupEdit, GroupList, getSelect } from '@/api/sys/cron';
import { statusOptions } from '@/enums/optionsiEnum';
const optionTreeData = ref([]);
@@ -96,7 +96,7 @@
remark: '',
status: statusValue.value,
});
const modalTitle = ref('新建分组');
const modalTitle = ref('添加分组');
const showModal = ref(false);
const formBtnLoading = ref(false);
const rules = {
@@ -109,7 +109,7 @@
function addTable() {
showModal.value = true;
modalTitle.value = '新建分组';
modalTitle.value = '添加分组';
formParams.value = defaultValueRef();
}
@@ -179,11 +179,10 @@
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
GroupDelete(record)
.then((_res) => {
message.success('操作成功');
reloadTable();
});
GroupDelete(record).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');

View File

@@ -58,7 +58,7 @@
</n-space>
</template>
<div class="w-full menu">
<n-input type="input" v-model:value="pattern" placeholder="输入菜单名称搜索">
<n-input type="input" v-model:value="pattern" placeholder="输入字典名称搜索">
<template #suffix>
<n-icon size="18" class="cursor-pointer">
<SearchOutlined />

View File

@@ -23,7 +23,7 @@
<PlusOutlined />
</n-icon>
</template>
新建
添加数据
</n-button>
</template>
</BasicTable>
@@ -32,7 +32,7 @@
v-model:show="showModal"
:show-icon="false"
preset="dialog"
:title="formParams?.id > 0 ? '编辑' : '新建'"
:title="formParams?.id > 0 ? '编辑数据' : '添加数据'"
>
<n-form
:model="formParams"
@@ -53,15 +53,19 @@
<n-form-item label="标签" path="label">
<n-input placeholder="请输入标签名称" v-model:value="formParams.label" />
</n-form-item>
<n-form-item label="标签样式" path="listClass">
<n-select
:render-tag="renderTag"
v-model:value="formParams.listClass"
:options="labelOptions"
/>
</n-form-item>
<n-form-item label="字典键值" path="value">
<n-input placeholder="请输入键值" v-model:value="formParams.value" />
</n-form-item>
<n-form-item label="键值类型" path="valueType">
<n-select v-model:value="formParams.valueType" :options="options" />
</n-form-item>
<n-form-item label="标签样式" path="listClass">
<n-select v-model:value="formParams.listClass" :options="tagOptions" />
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number placeholder="请输入" v-model:value="formParams.sort" />
</n-form-item>
@@ -93,13 +97,13 @@
<script lang="ts" setup>
import { h, reactive, ref, watch, onMounted } from 'vue';
import { TreeSelectOption, useMessage, useDialog } from 'naive-ui';
import { TreeSelectOption, useMessage, useDialog, NTag, SelectRenderTag } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { getDataList, getDictSelect, EditData, DeleteData } from '@/api/dict/dict';
import { columns } from './columns';
import { PlusOutlined } from '@vicons/antd';
import { statusOptions, tagOptions } from '@/enums/optionsiEnum';
import { statusOptions } from '@/enums/optionsiEnum';
import { TypeSelect } from '@/api/sys/config';
import { Option } from '@/utils/hotgo';
const options = ref<Option>();
@@ -138,6 +142,49 @@
},
];
const renderTag: SelectRenderTag = ({ option }) => {
return h(
NTag,
{
type: option.type as 'success' | 'warning' | 'error' | 'info' | 'primary' | 'default',
},
{ default: () => option.label }
);
};
const labelOptions = ref([
{
label: '绿色',
value: 'success',
type: 'success',
},
{
label: '橙色',
value: 'warning',
type: 'warning',
},
{
label: '红色',
value: 'error',
type: 'error',
},
{
label: '蓝色',
value: 'info',
type: 'info',
},
{
label: '灰色',
value: 'default',
type: 'default',
},
{
label: '主题色',
value: 'primary',
type: 'primary',
},
]);
const formRef: any = ref(null);
const message = useMessage();
const dialog = useDialog();

View File

@@ -4,7 +4,7 @@
v-model:show="isShowModal"
:show-icon="false"
preset="dialog"
:title="params?.id > 0 ? '编辑 #' + params?.id : '新建'"
:title="params?.id > 0 ? '编辑 #' + params?.id : '添加'"
:style="{
width: dialogWidth,
}"

View File

@@ -37,7 +37,7 @@
<PlusOutlined />
</n-icon>
</template>
新建
添加
</n-button>
<n-button
type="error"

View File

@@ -8,7 +8,7 @@ import { isNullObject } from '@/utils/is';
import { getFileExt } from '@/utils/urlUtils';
import { defRangeShortcuts, defShortcuts, formatToDate } from '@/utils/dateUtil';
import { validate } from '@/utils/validateUtil';
import { getOptionLabel, getOptionTag, Options } from '@/utils/hotgo';
import { errorImg, getOptionLabel, getOptionTag, Options } from '@/utils/hotgo';
const $message = window['$message'];
export interface State {
id: number;
@@ -298,6 +298,7 @@ export const columns = [
width: 32,
height: 32,
src: row.image,
onError: errorImg,
style: {
width: '32px',
height: '32px',
@@ -319,6 +320,7 @@ export const columns = [
width: 32,
height: 32,
src: image,
onError: errorImg,
style: {
width: '32px',
height: '32px',

File diff suppressed because it is too large Load Diff