v3.9.0【优化】typescript版本;【优化】App端消息;【优化】弹出层z-index;

This commit is contained in:
zhuoda
2024-11-04 20:15:49 +08:00
parent 17a3e1fd86
commit 69fa9088f5
1376 changed files with 10373 additions and 9712 deletions

View File

@@ -0,0 +1,96 @@
<!--
* 目录 表单 弹窗
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-21 19:52:43
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal :open="visible" :title="form.categoryId ? '编辑' : '添加'" ok-text="确认" cancel-text="取消" @ok="onSubmit" @cancel="onClose">
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }" :wrapper-col="{ span: 12 }">
<a-form-item label="分类名称" name="categoryName">
<a-input v-model:value="form.categoryName" placeholder="请输入分类名称" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import _ from 'lodash';
import { categoryApi } from '/@/api/business/category/category-api';
import { smartSentry } from '/@/lib/smart-sentry';
// emit
const emit = defineEmits(['reloadList']);
// 组件
const formRef = ref();
// ------------------------------ 显示 、隐藏 ------------------------------
// 是否展示抽屉
const visible = ref(false);
function showModal(categoryType, parentId, rowData) {
Object.assign(form, formDefault);
form.categoryType = categoryType;
form.parentId = parentId;
if (rowData && !_.isEmpty(rowData)) {
Object.assign(form, rowData);
}
visible.value = true;
}
function onClose() {
Object.assign(form, formDefault);
visible.value = false;
}
// ------------------------------ 表单 ------------------------------
const formDefault = {
categoryId: undefined,
categoryName: '',
categoryType: 1,
parentId: undefined,
disabledFlag: false,
};
let form = reactive({ ...formDefault });
const rules = {
categoryName: [{ required: true, message: '请输入分类名称' }],
};
function onSubmit() {
formRef.value
.validate()
.then(async () => {
SmartLoading.show();
try {
if (form.categoryId) {
await categoryApi.updateCategory(form);
} else {
await categoryApi.addCategory(form);
}
message.success(`${form.categoryId ? '修改' : '添加'}成功`);
emit('reloadList', form.parentId);
onClose();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
})
.catch((error) => {
console.log('error', error);
message.error('参数验证错误,请仔细填写表单数据!');
});
}
defineExpose({
showModal,
});
</script>

View File

@@ -0,0 +1,164 @@
<!--
* 目录 表格
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-21 19:52:43
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-card size="small" :bordered="false" :hoverable="true">
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button @click="addCategory()" type="primary" v-privilege="`${privilegePrefix}category:add`">
<template #icon>
<PlusOutlined />
</template>
新建
</a-button>
</div>
<div class="smart-table-setting-block"></div>
</a-row>
<a-table
:scroll="{ x: 1000 }"
size="small"
:dataSource="tableData"
:columns="columns"
rowKey="categoryId"
bordered
:pagination="false"
@expandedRowsChange="changeExand"
:expanded-row-keys="expandedRowKeys"
>
<template #bodyCell="{ record, column }">
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button @click="addCategory(record.categoryId)" type="link" v-privilege="`${privilegePrefix}category:addChild`">增加子分类</a-button>
<a-button @click="addCategory(undefined, record)" type="link" v-privilege="`${privilegePrefix}category:update`">编辑</a-button>
<a-button @click="confirmDeleteCategory(record.categoryId)" danger type="link" v-privilege="`${privilegePrefix}category:delete`"
>删除</a-button
>
</div>
</template>
</template>
</a-table>
<CategoryFormModal ref="formModal" @reloadList="reloadList" />
</a-card>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import CategoryFormModal from './category-form-modal.vue';
import { categoryApi } from '/@/api/business/category/category-api';
import { CATEGORY_TYPE_ENUM } from '/@/constants/business/erp/category-const';
import { smartSentry } from '/@/lib/smart-sentry';
const columnNameList = [
{
categoryType: CATEGORY_TYPE_ENUM.GOODS.value,
columnName: '商品分类',
},
{
categoryType: CATEGORY_TYPE_ENUM.DEMO.value,
columnName: '演示分类',
},
];
const columName = computed(() => {
let find = columnNameList.find((e) => e.categoryType === props.categoryType);
return find ? find.columnName : '';
});
const props = defineProps({
// 分组类型
categoryType: Number,
privilegePrefix: {
type: String,
default: '',
},
});
// ------------------------------ 查询 ------------------------------
const tableLoading = ref(false);
const tableData = ref([]);
const columns = reactive([
{
title: columName,
dataIndex: 'categoryName',
},
{
title: '操作',
dataIndex: 'action',
width: 200,
},
]);
async function queryList() {
try {
tableLoading.value = true;
let queryForm = {
categoryType: props.categoryType,
};
let responseModel = await categoryApi.queryCategoryTree(queryForm);
tableData.value = responseModel.data;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
const expandedRowKeys = ref([]);
function reloadList(parentId) {
queryList();
if (parentId) {
expandedRowKeys.value.push(parentId);
}
}
onMounted(queryList);
defineExpose({
queryList,
});
function changeExand(val) {
expandedRowKeys.value = val;
}
// ------------------------------ 添加 ------------------------------
const formModal = ref();
function addCategory(parentId, rowData) {
formModal.value.showModal(props.categoryType, parentId, rowData);
}
// ------------------------------ 删除 ------------------------------
function confirmDeleteCategory(categoryId) {
Modal.confirm({
title: '提示',
content: '确定要删除当前分类吗?',
okText: '确定',
okType: 'danger',
async onOk() {
deleteCategory(categoryId);
},
cancelText: '取消',
onCancel() {},
});
}
async function deleteCategory(categoryId) {
try {
SmartLoading.show();
await categoryApi.deleteCategoryById(categoryId);
message.success('删除成功');
queryList();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
</script>

View File

@@ -0,0 +1,18 @@
<!--
* 目录 demo
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-21 19:52:43
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div>
<CategoryTreeTable :category-type="CATEGORY_TYPE_ENUM.DEMO.value" :privilegePrefix="'custom:'"/>
</div>
</template>
<script setup>
import CategoryTreeTable from './components/category-tree-table.vue';
import { CATEGORY_TYPE_ENUM } from '/@/constants/business/erp/category-const';
</script>

View File

@@ -0,0 +1,18 @@
<!--
* 目录 商品
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-21 19:52:43
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div>
<CategoryTreeTable :category-type="CATEGORY_TYPE_ENUM.GOODS.value" />
</div>
</template>
<script setup>
import CategoryTreeTable from './components/category-tree-table.vue';
import { CATEGORY_TYPE_ENUM } from '/@/constants/business/erp/category-const';
</script>

View File

@@ -0,0 +1,152 @@
<!--
* 商品表单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-07-21 21:55:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-drawer :title="form.goodsId ? '编辑' : '添加'" :width="500" :open="visible" :body-style="{ paddingBottom: '80px' }" @close="onClose">
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }">
<a-form-item label="商品分类" name="categoryId">
<CategoryTree v-model:value="form.categoryId" placeholder="请选择商品分类" :categoryType="CATEGORY_TYPE_ENUM.GOODS.value" />
</a-form-item>
<a-form-item label="商品名称" name="goodsName">
<a-input v-model:value="form.goodsName" placeholder="请输入商品名称" />
</a-form-item>
<a-form-item label="商品状态" name="goodsStatus">
<SmartEnumSelect enum-name="GOODS_STATUS_ENUM" v-model:value="form.goodsStatus" />
</a-form-item>
<a-form-item label="产地" name="place">
<DictSelect width="100%" key-code="GODOS_PLACE" v-model:value="form.place" mode="tags" />
</a-form-item>
<a-form-item label="上架状态" name="shelvesFlag">
<a-radio-group v-model:value="form.shelvesFlag">
<a-radio :value="true">上架</a-radio>
<a-radio :value="false">下架</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="商品价格" name="price">
<a-input-number style="width: 100%" placeholder="请输入商品价格" v-model:value="form.price" :min="0" />
</a-form-item>
<a-form-item label="备注" name="remark">
<a-input style="width: 100%" placeholder="请输入备注" v-model:value="form.remark" />
</a-form-item>
</a-form>
<div
:style="{
position: 'absolute',
right: 0,
bottom: 0,
width: '100%',
borderTop: '1px solid #e9e9e9',
padding: '10px 16px',
background: '#fff',
textAlign: 'right',
zIndex: 1,
}"
>
<a-button style="margin-right: 8px" @click="onClose">取消</a-button>
<a-button type="primary" @click="onSubmit">提交</a-button>
</div>
</a-drawer>
</template>
<script setup>
import { ref, nextTick, reactive } from 'vue';
import CategoryTree from '/@/components/business/category-tree-select/index.vue';
import { CATEGORY_TYPE_ENUM } from '/@/constants/business/erp/category-const';
import { message } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { GOODS_STATUS_ENUM } from '/@/constants/business/erp/goods-const';
import _ from 'lodash';
import { goodsApi } from '/@/api/business/goods/goods-api';
import { smartSentry } from '/@/lib/smart-sentry';
import SmartEnumSelect from '/@/components/framework/smart-enum-select/index.vue';
import DictSelect from '/@/components/support/dict-select/index.vue';
// emit
const emit = defineEmits(['reloadList']);
// 组件ref
const formRef = ref();
const formDefault = {
//商品分类
categoryId: undefined,
//商品名称
goodsName: undefined,
//商品状态
goodsStatus: GOODS_STATUS_ENUM.APPOINTMENT.value,
//产地
place: [],
//商品价格
price: undefined,
//上架状态
shelvesFlag: true,
//备注
remark: '',
//商品id
goodsId: undefined,
};
let form = reactive({ ...formDefault });
const rules = {
categoryId: [{ required: true, message: '请选择商品分类' }],
goodsName: [{ required: true, message: '商品名称不能为空' }],
goodsStatus: [{ required: true, message: '商品状态不能为空' }],
price: [{ required: true, message: '商品价格不能为空' }],
place: [{ required: true, message: '产地不能为空' }],
};
// 是否展示抽屉
const visible = ref(false);
function showDrawer(rowData) {
Object.assign(form, formDefault);
if (rowData && !_.isEmpty(rowData)) {
Object.assign(form, rowData);
}
if (form.place && form.place.length > 0) {
form.place = form.place.map((e) => e.valueCode);
}
visible.value = true;
nextTick(() => {
formRef.value.clearValidate();
});
}
function onClose() {
Object.assign(form, formDefault);
visible.value = false;
}
function onSubmit() {
formRef.value
.validate()
.then(async () => {
SmartLoading.show();
try {
if (form.goodsId) {
await goodsApi.updateGoods(form);
} else {
await goodsApi.addGoods(form);
}
message.success(`${form.goodsId ? '修改' : '添加'}成功`);
onClose();
emit('reloadList');
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
})
.catch((error) => {
console.log('error', error);
message.error('参数验证错误,请仔细填写表单数据!');
});
}
defineExpose({
showDrawer,
});
</script>

View File

@@ -0,0 +1,446 @@
<!--
* 商品列表
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-07-21 21:55:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<!---------- 查询表单form begin ----------->
<a-form class="smart-query-form">
<a-row class="smart-query-form-row" v-privilege="'goods:query'">
<a-form-item label="商品分类" class="smart-query-form-item">
<category-tree
width="150px"
v-model:value="queryForm.categoryId"
placeholder="请选择商品分类"
:categoryType="CATEGORY_TYPE_ENUM.GOODS.value"
/>
</a-form-item>
<a-form-item label="商品名称" class="smart-query-form-item">
<a-input style="width: 200px" v-model:value="queryForm.searchWord" placeholder="商品名称" />
</a-form-item>
<a-form-item label="产地" name="place" class="smart-query-form-item">
<DictSelect key-code="GODOS_PLACE" v-model:value="queryForm.place" width="120px" />
</a-form-item>
<a-form-item label="商品状态" name="goodsStatus" class="smart-query-form-item">
<SmartEnumSelect enum-name="GOODS_STATUS_ENUM" v-model:value="queryForm.goodsStatus" width="160px" />
</a-form-item>
<a-form-item label="快速筛选" class="smart-query-form-item">
<a-radio-group v-model:value="queryForm.shelvesFlag" @change="onSearch">
<a-radio-button :value="undefined">全部</a-radio-button>
<a-radio-button :value="true">上架</a-radio-button>
<a-radio-button :value="false">下架</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item class="smart-query-form-item">
<a-button-group>
<a-button type="primary" @click="onSearch" v-privilege="'goods:query'">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery" v-privilege="'goods:query'">
<template #icon>
<SearchOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<!---------- 查询表单form end ----------->
<a-card size="small" :bordered="false" :hoverable="true">
<!---------- 表格操作行 begin ----------->
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button @click="addGoods" type="primary" v-privilege="'goods:add'">
<template #icon>
<PlusOutlined />
</template>
新建
</a-button>
<a-button @click="confirmBatchDelete" danger :disabled="selectedRowKeyList.length === 0" v-privilege="'goods:batchDelete'">
<template #icon>
<DeleteOutlined />
</template>
批量删除
</a-button>
<a-button @click="showImportModal" type="primary" v-privilege="'goods:importGoods'">
<template #icon>
<ImportOutlined />
</template>
导入
</a-button>
<a-button @click="onExportGoods" type="primary" v-privilege="'goods:exportGoods'">
<template #icon>
<ExportOutlined />
</template>
导出
</a-button>
</div>
<div class="smart-table-setting-block">
<TableOperator v-model="columns" :tableId="TABLE_ID_CONST.BUSINESS.ERP.GOODS" :refresh="queryData" />
</div>
</a-row>
<!---------- 表格操作行 end ----------->
<a-table
size="small"
:dataSource="tableData"
:columns="columns"
rowKey="goodsId"
bordered
:pagination="false"
:showSorterTooltip="false"
:row-selection="{ selectedRowKeys: selectedRowKeyList, onChange: onSelectChange }"
@change="onChange"
>
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'place'">
<span>{{ text && text.length > 0 ? text.map((e) => e.valueName).join(',') : '' }}</span>
</template>
<template v-if="column.dataIndex === 'goodsStatus'">
<span>{{ $smartEnumPlugin.getDescByValue('GOODS_STATUS_ENUM', text) }}</span>
</template>
<template v-if="column.dataIndex === 'shelvesFlag'">
<span>{{ text ? '上架' : '下架' }}</span>
</template>
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button @click="addGoods(record)" type="link" v-privilege="'goods:update'">编辑</a-button>
<a-button @click="deleteGoods(record)" danger type="link" v-privilege="'goods:delete'">删除</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="queryData"
@showSizeChange="queryData"
:show-total="(total) => `${total}`"
/>
</div>
<GoodsFormModal ref="formModal" @reloadList="queryData" />
<a-modal v-model:open="importModalShowFlag" title="导入" @onCancel="hideImportModal" @ok="hideImportModal">
<div style="text-align: center; width: 400px; margin: 0 auto">
<a-button @click="downloadExcel"> <download-outlined />第一步:下载模板</a-button>
<br />
<br />
<a-upload
v-model:fileList="fileList"
name="file"
:multiple="false"
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
accept=".xls,.xlsx"
:before-upload="beforeUpload"
@remove="handleRemove"
>
<a-button>
<upload-outlined />
第二步选择文件
</a-button>
</a-upload>
<br />
<a-button @click="onImportGoods">
<ImportOutlined />
第三步开始导入
</a-button>
</div>
</a-modal>
</a-card>
</template>
<script setup>
import GoodsFormModal from './components/goods-form-modal.vue';
import { reactive, ref, onMounted } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { goodsApi } from '/@/api/business/goods/goods-api';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import CategoryTree from '/@/components/business/category-tree-select/index.vue';
import { CATEGORY_TYPE_ENUM } from '/@/constants/business/erp/category-const';
import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
import { GOODS_STATUS_ENUM } from '/@/constants/business/erp/goods-const';
import DictSelect from '/@/components/support/dict-select/index.vue';
import SmartEnumSelect from '/@/components/framework/smart-enum-select/index.vue';
import { FILE_FOLDER_TYPE_ENUM } from '/@/constants/support/file-const.js';
import FileUpload from '/@/components/support/file-upload/index.vue';
import _ from 'lodash';
// ---------------------------- 表格列 ----------------------------
const columns = ref([
{
title: '商品分类',
dataIndex: 'categoryName',
},
{
title: '商品名称',
dataIndex: 'goodsName',
},
{
title: '商品状态',
dataIndex: 'goodsStatus',
sorter: true
},
{
title: '产地',
dataIndex: 'place',
},
{
title: '商品价格',
dataIndex: 'price',
sorter: true
},
{
title: '上架状态',
dataIndex: 'shelvesFlag',
sorter: true
},
{
title: '备注',
dataIndex: 'remark',
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 150,
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 100,
},
]);
// ---------------------------- 查询数据表单和方法 ----------------------------
const queryFormState = {
categoryId: undefined,
searchWord: '',
goodsStatus: undefined,
place: undefined,
shelvesFlag: undefined,
goodsType: undefined,
pageNum: 1,
pageSize: 10,
sortItemList: []
};
// 查询表单form
const queryForm = reactive(_.cloneDeep(queryFormState));
// 表格加载loading
const tableLoading = ref(false);
// 表格数据
const tableData = ref([]);
// 总数
const total = ref(0);
// 重置查询条件
function resetQuery() {
let pageSize = queryForm.pageSize;
Object.assign(queryForm, _.cloneDeep(queryFormState));
queryForm.pageSize = pageSize;
queryData();
}
// 搜索
function onSearch() {
queryForm.pageNum = 1;
queryData();
}
// 查询数据
async function queryData() {
tableLoading.value = true;
try {
let queryResult = await goodsApi.queryGoodsList(queryForm);
tableData.value = queryResult.data.list;
total.value = queryResult.data.total;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
onMounted(queryData);
// ---------------------------- 添加/修改 ----------------------------
const formModal = ref();
function addGoods(goodsData) {
formModal.value.showDrawer(goodsData);
}
// ---------------------------- 单个删除 ----------------------------
function deleteGoods(goodsData) {
Modal.confirm({
title: '提示',
content: '确定要删除【' + goodsData.goodsName + '】吗?',
okText: '删除',
okType: 'danger',
onOk() {
singleDelete(goodsData);
},
cancelText: '取消',
onCancel() {},
});
}
async function singleDelete(goodsData) {
try {
SmartLoading.show();
await goodsApi.deleteGoods(goodsData.goodsId);
message.success('删除成功');
queryData();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
// ---------------------------- 批量删除 ----------------------------
// 选择表格行
const selectedRowKeyList = ref([]);
function onSelectChange(selectedRowKeys) {
selectedRowKeyList.value = selectedRowKeys;
}
// 批量删除
function confirmBatchDelete() {
Modal.confirm({
title: '提示',
content: '确定要删除选中商品吗?',
okText: '删除',
okType: 'danger',
onOk() {
batchDelete();
},
cancelText: '取消',
onCancel() {},
});
}
async function batchDelete() {
try {
SmartLoading.show();
await goodsApi.batchDelete(selectedRowKeyList.value);
message.success('删除成功');
queryData();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
// ------------------------------- 导出和导入 ---------------------------------
// 导入弹窗
const importModalShowFlag = ref(false);
const fileList = ref([]);
// 显示导入
function showImportModal() {
fileList.value = [];
importModalShowFlag.value = true;
}
// 关闭 导入
function hideImportModal() {
importModalShowFlag.value = false;
}
function handleChange() {}
function handleDrop() {}
function handleRemove(file) {
const index = fileList.value.indexOf(file);
const newFileList = fileList.value.slice();
newFileList.splice(index, 1);
fileList.value = newFileList;
}
function beforeUpload(file) {
fileList.value = [...(fileList.value || []), file];
return false;
}
function downloadExcel() {
window.open('https://smartadmin.vip/cdn/%E5%95%86%E5%93%81%E6%A8%A1%E6%9D%BF.xls');
}
async function onImportGoods() {
const formData = new FormData();
fileList.value.forEach((file) => {
formData.append('file', file.originFileObj);
});
SmartLoading.show();
try {
let res = await goodsApi.importGoods(formData);
message.success(res.msg);
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
async function onExportGoods() {
await goodsApi.exportGoods();
}
function onChange(pagination, filters, sorter, { action }){
if (action === 'sort') {
const { order, field } = sorter;
let column = camelToUnderscore(field);
let findIndex = queryForm.sortItemList.findIndex(e => e.column === column);
if (findIndex !== -1) {
queryForm.sortItemList.splice(findIndex, 1);
}
if (order) {
let isAsc = order !== 'ascend';
queryForm.sortItemList.push({
column,
isAsc
});
}
queryData();
}
}
function camelToUnderscore(str) {
return str.replace(/([A-Z])/g, "_$1").toLowerCase();
}
</script>

View File

@@ -0,0 +1,246 @@
<!--
* 企业 银行列表
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-15 20:15:49
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="关键字" class="smart-query-form-item">
<a-input style="width: 300px" v-model:value="queryForm.keywords" placeholder="开户银行/账户名称/账户/创建人" />
</a-form-item>
<a-form-item label="创建时间" class="smart-query-form-item">
<a-space direction="vertical" :size="12">
<a-range-picker v-model:value="searchDate" @change="dateChange" />
</a-space>
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
</a-button-group>
<a-button @click="addOrUpdate()" type="primary" class="smart-margin-left20">
<template #icon>
<PlusOutlined />
</template>
新建账户
</a-button>
</a-form-item>
</a-row>
</a-form>
<a-card size="small" :bordered="false" :hoverable="false">
<a-row justify="end">
<TableOperator class="smart-margin-bottom5" v-model="columns" :tableId="TABLE_ID_CONST.BUSINESS.OA.ENTERPRISE_BANK" :refresh="ajaxQuery" />
</a-row>
<a-table :scroll="{ x: 1300 }" size="small" :dataSource="tableData" bordered :columns="columns" rowKey="bankId" :pagination="false">
<template #bodyCell="{ record, column }">
<template v-if="column.dataIndex === 'disabledFlag'">
{{ record.disabledFlag ? '禁用' : '启用' }}
</template>
<template v-else-if="column.dataIndex === 'businessFlag'">
{{ record.businessFlag ? '' : '' }}
</template>
<template v-else-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button @click="addOrUpdate(record)" type="link">编辑</a-button>
<a-button @click="confirmDelete(record.bankId)" danger type="link">删除</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `${total}`"
/>
</div>
<!--新建编辑modal-->
<BankOperateModal ref="operateModal" :enterpriseId="enterpriseId" @reloadList="ajaxQuery" />
</a-card>
</template>
<script setup>
import { reactive, ref, watch } from 'vue';
import { bankApi } from '/@/api/business/oa/bank-api';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import BankOperateModal from './enterprise-bank-operate-modal.vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { message, Modal } from 'ant-design-vue';
import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
const props = defineProps({
enterpriseId: {
type: Number,
default: null,
},
});
const columns = ref([
{
title: '开户银行',
dataIndex: 'bankName',
},
{
title: '账户名称',
dataIndex: 'accountName',
ellipsis: true,
},
{
title: '账号',
width: 100,
dataIndex: 'accountNumber',
ellipsis: true,
},
{
title: '是否对公',
width: 120,
dataIndex: 'businessFlag',
},
{
title: '状态',
width: 80,
dataIndex: 'disabledFlag',
},
{
title: '备注',
width: 100,
dataIndex: 'remark',
},
{
title: '创建人',
width: 100,
dataIndex: 'createUserName',
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 150,
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 100,
},
]);
const queryFormState = {
enterpriseId: props.enterpriseId,
keywords: '',
endTime: null,
startTime: null,
pageNum: 1,
pageSize: PAGE_SIZE,
searchCount: true,
};
const queryForm = reactive({ ...queryFormState });
const tableLoading = ref(false);
const tableData = ref([]);
const total = ref(0);
const operateModal = ref();
// 日期选择
let searchDate = ref();
function dateChange(dates, dateStrings) {
queryForm.startTime = dateStrings[0];
queryForm.endTime = dateStrings[1];
}
function resetQuery() {
searchDate.value = [];
Object.assign(queryForm, queryFormState, { enterpriseId: props.enterpriseId });
ajaxQuery();
}
function onSearch() {
queryForm.pageNum = 1;
ajaxQuery();
}
async function ajaxQuery() {
try {
tableLoading.value = true;
let responseModel = await bankApi.pageQuery(queryForm);
const list = responseModel.data.list;
total.value = responseModel.data.total;
tableData.value = list;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
function confirmDelete(bankId) {
Modal.confirm({
title: '确定要删除吗?',
content: '删除后,该信息将不可恢复',
okText: '删除',
okType: 'danger',
onOk() {
del(bankId);
},
cancelText: '取消',
onCancel() {},
});
}
async function del(bankId) {
try {
SmartLoading.show();
await bankApi.delete(bankId);
message.success('删除成功');
ajaxQuery();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
function addOrUpdate(rowData) {
operateModal.value.showModal(rowData);
}
watch(
() => props.enterpriseId,
(value) => {
if (value) {
queryForm.enterpriseId = value;
ajaxQuery();
}
},
{
immediate: true,
}
);
</script>

View File

@@ -0,0 +1,131 @@
<!--
* 企业 银行 表单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-15 20:15:49
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal :open="visible" :title="form.bankId ? '编辑' : '添加'" ok-text="确认" cancel-text="取消" @ok="onSubmit" @cancel="onClose">
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
<a-form-item label="开户银行" name="bankName">
<a-input v-model:value="form.bankName" placeholder="请输入开户银行" />
</a-form-item>
<a-form-item label="账户名称" name="accountName">
<a-input v-model:value="form.accountName" placeholder="请输入账户名称" />
</a-form-item>
<a-form-item label="账号" name="accountNumber">
<a-input v-model:value="form.accountNumber" placeholder="请输入账号" />
</a-form-item>
<a-form-item label="是否对公" name="businessFlag">
<a-switch v-model:checked="businessFlagChecked" @change="businessFlagCheckedChange" />
</a-form-item>
<a-form-item label="启用状态" name="disabledFlag">
<a-switch v-model:checked="enabledChecked" @change="enabledCheckedChange" />
</a-form-item>
<a-form-item label="备注" name="remark">
<a-textarea v-model:value="form.remark" :rows="2" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { bankApi } from '/@/api/business/oa/bank-api';
import { smartSentry } from '/@/lib/smart-sentry';
const props = defineProps({
enterpriseId: {
type: Number,
default: null,
},
});
// emit
const emit = defineEmits(['reloadList']);
// ---------------------- 显示、隐藏 ----------------------
// 是否展示
const visible = ref(false);
function showModal(rowData) {
Object.assign(form, formDefault);
if (rowData) {
Object.assign(form, rowData);
businessFlagChecked.value = rowData.businessFlag;
enabledChecked.value = !rowData.disabledFlag;
}
form.enterpriseId = props.enterpriseId;
visible.value = true;
}
function onClose() {
Object.assign(form, formDefault);
formRef.value.resetFields();
visible.value = false;
}
// ---------------------- 表单 ----------------------
// 组件
const formRef = ref();
const formDefault = {
bankId: undefined,
enterpriseId: undefined,
bankName: '',
accountName: '',
accountNumber: '',
businessFlag: false,
disabledFlag: false,
remark: '',
};
let form = reactive({ ...formDefault });
const rules = {
bankName: [{ required: true, message: '请输入开户银行' }],
accountName: [{ required: true, message: '请输入账户名称' }],
accountNumber: [{ required: true, message: '请输入账号' }],
};
const businessFlagChecked = ref(false);
const enabledChecked = ref(true);
function businessFlagCheckedChange(checked) {
form.businessFlag = checked;
}
function enabledCheckedChange(checked) {
form.disabledFlag = !checked;
}
function onSubmit() {
formRef.value
.validate()
.then(async () => {
SmartLoading.show();
try {
if (form.bankId) {
await bankApi.update(form);
} else {
await bankApi.create(form);
}
message.success(`${form.bankId ? '修改' : '添加'}成功`);
emit('reloadList');
onClose();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
})
.catch((error) => {
console.log('error', error);
message.error('参数验证错误,请仔细填写表单数据!');
});
}
defineExpose({
showModal,
});
</script>

View File

@@ -0,0 +1,280 @@
<!--
* 企业 员工
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-15 20:15:49
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div>
<div class="header">
<div>
关键字
<a-input style="width: 250px" v-model:value="queryForm.keyword" placeholder="姓名/手机号/登录账号" />
<a-button class="button-style" type="primary" @click="onSearch">搜索</a-button>
<a-button class="button-style" type="default" @click="resetQueryEmployee">重置</a-button>
</div>
<div>
<a-button class="button-style" type="primary" @click="addEmployee" v-privilege="'oa:enterprise:addEmployee'"> 添加员工 </a-button>
<a-button class="button-style" type="primary" danger @click="batchDelete" v-privilege="'oa:enterprise:deleteEmployee'"> 批量移除 </a-button>
</div>
</div>
<a-table
:loading="tableLoading"
:dataSource="tableData"
:columns="columns"
:pagination="false"
rowKey="employeeId"
:row-selection="{ selectedRowKeys: selectedRowKeyList, onChange: onSelectChange }"
size="small"
bordered
>
<template #bodyCell="{ text, record, index, column }">
<template v-if="column.dataIndex === 'disabledFlag'">
<a-tag :color="text ? 'error' : 'processing'">{{ text ? '禁用' : '启用' }}</a-tag>
</template>
<template v-else-if="column.dataIndex === 'gender'">
<span>{{ $smartEnumPlugin.getDescByValue('GENDER_ENUM', text) }}</span>
</template>
<template v-if="column.dataIndex === 'operate'">
<a @click="deleteEmployee(record.employeeId)" v-privilege="'oa:enterprise:deleteEmployee'">移除</a>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="queryEmployee"
@showSizeChange="queryEmployee"
:show-total="showTableTotal"
/>
</div>
<EmployeeTableSelectModal ref="selectEmployeeModal" @selectData="selectData" />
</div>
</template>
<script setup>
import EmployeeTableSelectModal from '/@/components/system/employee-table-select-modal/index.vue';
import { message, Modal } from 'ant-design-vue';
import _ from 'lodash';
import { computed, reactive, ref, watch } from 'vue';
import { enterpriseApi } from '/@/api/business/oa/enterprise-api';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS, showTableTotal } from '/@/constants/common-const';
import { smartSentry } from '/@/lib/smart-sentry';
const props = defineProps({
enterpriseId: {
type: Number,
default: null,
},
});
const columns = reactive([
{
title: '姓名',
dataIndex: 'actualName',
},
{
title: '手机号',
dataIndex: 'phone',
width: 120,
},
{
title: '登录账号',
dataIndex: 'loginName',
},
{
title: '企业',
dataIndex: 'enterpriseName',
ellipsis: true,
},
{
title: '部门',
dataIndex: 'departmentName',
ellipsis: true,
},
{
title: '状态',
dataIndex: 'disabledFlag',
width: 80,
},
{
title: '操作',
dataIndex: 'operate',
width: 60,
},
]);
// --------------------------- 查询 ---------------------------
const defaultQueryForm = {
pageNum: 1,
pageSize: PAGE_SIZE,
enterpriseId: undefined,
keyword: undefined,
};
// 查询表单
const queryForm = reactive({ ...defaultQueryForm });
const total = ref(0);
const tableData = ref([]);
const tableLoading = ref(false);
function resetQueryEmployee() {
queryForm.keyword = '';
queryEmployee();
}
function onSearch() {
queryForm.pageNum = 1;
queryEmployee();
}
async function queryEmployee() {
try {
tableLoading.value = true;
queryForm.enterpriseId = props.enterpriseId;
let res = await enterpriseApi.queryPageEmployeeList(queryForm);
tableData.value = res.data.list;
total.value = res.data.total;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
async function selectData(list) {
if (_.isEmpty(list)) {
message.warning('请选择员工');
return;
}
SmartLoading.show();
try {
let params = {
employeeIdList: list,
enterpriseId: props.enterpriseId,
};
await enterpriseApi.addEmployee(params);
message.success('添加成功');
await queryEmployee();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
// --------------------------- 添加员工 ---------------------------
// 添加员工
const selectEmployeeModal = ref();
async function addEmployee() {
let res = await enterpriseApi.employeeList([props.enterpriseId]);
let selectedIdList = res.data.map((e) => e.employeeId) || [];
selectEmployeeModal.value.showModal(selectedIdList);
}
// --------------------------- 删除 ---------------------------
// 删除员工方法
async function deleteEmployee(employeeId) {
Modal.confirm({
title: '提示',
content: '确定要删除该企业下的员工么?',
okText: '确定',
okType: 'danger',
async onOk() {
SmartLoading.show();
try {
let param = {
employeeIdList: [employeeId],
enterpriseId: props.enterpriseId,
};
await enterpriseApi.deleteEmployee(param);
message.success('移除成功');
await queryEmployee();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
},
cancelText: '取消',
onCancel() {},
});
}
// 批量删除
const selectedRowKeyList = ref([]);
const hasSelected = computed(() => selectedRowKeyList.value.length > 0);
function onSelectChange(selectedRowKeys) {
selectedRowKeyList.value = selectedRowKeys;
}
// 批量移除
function batchDelete() {
if (!hasSelected.value) {
message.warning('请选择要删除的员工');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除该企业下的员工么?',
okText: '确定',
okType: 'danger',
async onOk() {
SmartLoading.show();
try {
let params = {
employeeIdList: selectedRowKeyList.value,
enterpriseId: props.enterpriseId,
};
await enterpriseApi.deleteEmployee(params);
message.success('移除成功');
selectedRowKeyList.value = [];
await queryEmployee();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
},
cancelText: '取消',
onCancel() {},
});
}
watch(
() => props.enterpriseId,
(e) => {
if (e) {
queryEmployee();
}
},
{ immediate: true }
);
</script>
<style scoped lang="less">
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 20px 0;
}
.button-style {
margin: 0 10px;
}
</style>

View File

@@ -0,0 +1,250 @@
<!--
* 企业 发票信息
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-15 20:15:49
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="关键字" class="smart-query-form-item">
<a-input style="width: 300px" v-model:value="queryForm.keywords" placeholder="开票抬头/银行账户/创建人" />
</a-form-item>
<a-form-item label="创建时间" class="smart-query-form-item">
<a-space direction="vertical" :size="12">
<a-range-picker v-model:value="searchDate" @change="dateChange" />
</a-space>
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
</a-button-group>
<a-button @click="addOrUpdate()" type="primary" class="smart-margin-left20">
<template #icon>
<PlusOutlined />
</template>
新建发票
</a-button>
</a-form-item>
</a-row>
</a-form>
<a-card size="small" :bordered="false" :hoverable="false">
<a-row justify="end">
<TableOperator class="smart-margin-bottom5" v-model="columns" :tableId="TABLE_ID_CONST.BUSINESS.OA.ENTERPRISE_INVOICE" :refresh="ajaxQuery" />
</a-row>
<a-table :scroll="{ x: 1300 }" size="small" :dataSource="tableData" :columns="columns" rowKey="invoiceId" :pagination="false" bordered>
<template #bodyCell="{ text, record, index, column }">
<template v-if="column.dataIndex === 'disabledFlag'">
{{ record.disabledFlag ? '禁用' : '启用' }}
</template>
<template v-else-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button @click="addOrUpdate(record)" type="link">编辑</a-button>
<a-button @click="confirmDelete(record.invoiceId)" type="link" danger>删除</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `${total}`"
/>
</div>
<!--新建编辑modal-->
<InvoiceOperateModal ref="operateModal" :enterpriseId="enterpriseId" @reloadList="ajaxQuery" />
</a-card>
</template>
<script setup>
import { reactive, ref, watch } from 'vue';
import { invoiceApi } from '/@/api/business/oa/invoice-api';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import InvoiceOperateModal from './enterprise-invoice-operate-modal.vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { message, Modal } from 'ant-design-vue';
import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
const props = defineProps({
enterpriseId: {
type: Number,
default: null,
},
});
const columns = ref([
{
title: 'ID',
width: 50,
dataIndex: 'invoiceId',
},
{
title: '开票抬头',
dataIndex: 'invoiceHeads',
ellipsis: true,
},
{
title: '纳税人识别号',
dataIndex: 'taxpayerIdentificationNumber',
ellipsis: true,
},
{
title: '银行账号',
width: 100,
dataIndex: 'accountNumber',
ellipsis: true,
},
{
title: '开户行',
width: 120,
dataIndex: 'bankName',
},
{
title: '状态',
width: 80,
dataIndex: 'disabledFlag',
},
{
title: '备注',
width: 100,
dataIndex: 'remark',
},
{
title: '创建人',
width: 100,
dataIndex: 'createUserName',
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 150,
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 100,
},
]);
const queryFormState = {
enterpriseId: props.enterpriseId,
keywords: '',
endTime: null,
startTime: null,
pageNum: 1,
pageSize: PAGE_SIZE,
searchCount: true,
};
const queryForm = reactive({ ...queryFormState });
const tableLoading = ref(false);
const tableData = ref([]);
const total = ref(0);
const operateModal = ref();
// 日期选择
let searchDate = ref();
function dateChange(dates, dateStrings) {
queryForm.startTime = dateStrings[0];
queryForm.endTime = dateStrings[1];
}
function resetQuery() {
searchDate.value = [];
Object.assign(queryForm, queryFormState);
ajaxQuery();
}
function onSearch() {
queryForm.pageNum = 1;
ajaxQuery();
}
async function ajaxQuery() {
try {
tableLoading.value = true;
let responseModel = await invoiceApi.pageQuery(queryForm);
const list = responseModel.data.list;
total.value = responseModel.data.total;
tableData.value = list;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
function confirmDelete(invoiceId) {
Modal.confirm({
title: '确定要删除吗?',
content: '删除后,该信息将不可恢复',
okText: '删除',
okType: 'danger',
onOk() {
del(invoiceId);
},
cancelText: '取消',
onCancel() {},
});
}
async function del(invoiceId) {
try {
SmartLoading.show();
await invoiceApi.delete(invoiceId);
message.success('删除成功');
ajaxQuery();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
function addOrUpdate(rowData) {
operateModal.value.showModal(rowData);
}
watch(
() => props.enterpriseId,
(value) => {
if (value) {
queryForm.enterpriseId = value;
ajaxQuery();
}
},
{
immediate: true,
}
);
</script>

View File

@@ -0,0 +1,127 @@
<!--
* 企业 发票 表单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-15 20:15:49
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal :open="visible" :title="form.invoiceId ? '编辑' : '添加'" ok-text="确认" cancel-text="取消" @ok="onSubmit" @cancel="onClose">
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }" :wrapper-col="{ span: 12 }">
<a-form-item label="开票抬头" name="invoiceHeads">
<a-input v-model:value="form.invoiceHeads" placeholder="请输入开票抬头" />
</a-form-item>
<a-form-item label="纳税人识别号" name="taxpayerIdentificationNumber">
<a-input v-model:value="form.taxpayerIdentificationNumber" placeholder="请输入纳税人识别号" />
</a-form-item>
<a-form-item label="银行账号" name="accountNumber">
<a-input v-model:value="form.accountNumber" placeholder="请输入银行账号" />
</a-form-item>
<a-form-item label="开户行" name="bankName">
<a-input v-model:value="form.bankName" placeholder="请输入开户行" />
</a-form-item>
<a-form-item label="启用状态" name="disabledFlag">
<a-switch v-model:checked="enabledChecked" @change="enabledCheckedChange" />
</a-form-item>
<a-form-item label="备注" name="remark">
<a-textarea v-model:value="form.remark" :rows="2" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { invoiceApi } from '/@/api/business/oa/invoice-api';
import { smartSentry } from '/@/lib/smart-sentry';
const props = defineProps({
enterpriseId: {
type: Number,
default: null,
},
});
// emit
const emit = defineEmits(['reloadList']);
// --------------------- modal 显示与隐藏 ---------------------
// 是否展示
const visible = ref(false);
const enabledChecked = ref(true);
function enabledCheckedChange(checked) {
form.disabledFlag = !checked;
}
function showModal(rowData) {
Object.assign(form, formDefault);
if (rowData) {
Object.assign(form, rowData);
enabledChecked.value = !rowData.disabledFlag;
}
form.enterpriseId = props.enterpriseId;
visible.value = true;
}
function onClose() {
formRef.value.resetFields();
Object.assign(form, formDefault);
visible.value = false;
}
// --------------------- 表单 ---------------------
// 组件
const formRef = ref();
const formDefault = {
invoiceId: undefined,
enterpriseId: undefined,
bankName: '',
accountNumber: '',
invoiceHeads: '',
taxpayerIdentificationNumber: '',
disabledFlag: false,
remark: '',
};
let form = reactive({ ...formDefault });
const rules = {
invoiceHeads: [{ required: true, message: '请输入开票抬头' }],
taxpayerIdentificationNumber: [{ required: true, message: '请输入纳税人识别号' }],
accountNumber: [{ required: true, message: '请输入银行账号' }],
bankName: [{ required: true, message: '请输入开户行' }],
};
function onSubmit() {
formRef.value
.validate()
.then(async () => {
SmartLoading.show();
try {
if (form.invoiceId) {
await invoiceApi.update(form);
} else {
await invoiceApi.create(form);
}
message.success(`${form.invoiceId ? '修改' : '添加'}成功`);
emit('reloadList');
onClose();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
})
.catch((error) => {
console.log('error', error);
message.error('参数验证错误,请仔细填写表单数据!');
});
}
defineExpose({
showModal,
});
</script>

View File

@@ -0,0 +1,243 @@
<template>
<a-modal :open="visible" title="添加" :width="700" forceRender ok-text="确认" cancel-text="取消" @ok="onSubmit" @cancel="onClose">
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 6 }">
<a-form-item label="企业名称" name="enterpriseName">
<a-input v-model:value="form.enterpriseName" placeholder="请输入企业名称" />
</a-form-item>
<a-form-item label="企业logo" name="enterpriseLogo">
<Upload
accept=".jpg,.jpeg,.png,.gif"
:maxUploadSize="1"
buttonText="点击上传企业logo"
:default-file-list="form.enterpriseLogo"
@change="enterpriseLogoChange"
/>
</a-form-item>
<a-form-item label="统一社会信用代码" name="unifiedSocialCreditCode">
<a-input v-model:value="form.unifiedSocialCreditCode" placeholder="请输入统一社会信用代码" />
</a-form-item>
<a-form-item label="类型" name="type">
<SmartEnumSelect width="100%" v-model:value="form.type" placeholder="请选择类型" enum-name="ENTERPRISE_TYPE_ENUM" />
</a-form-item>
<a-form-item label="联系人" name="contact">
<a-input v-model:value="form.contact" placeholder="请输入联系人" />
</a-form-item>
<a-form-item label="联系人电话" name="contactPhone">
<a-input v-model:value="form.contactPhone" placeholder="请输入联系人电话" />
</a-form-item>
<a-form-item label="所在城市" name="provinceCityDistrict">
<AreaCascader type="province_city_district" style="width: 100%" v-model:value="area" placeholder="请选择所在城市" @change="changeArea" />
</a-form-item>
<a-form-item label="详细地址" name="address">
<a-input v-model:value="form.address" placeholder="请输入详细地址" />
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input v-model:value="form.email" placeholder="请输入邮箱" />
</a-form-item>
<a-form-item label="启用状态" name="disabledFlag">
<a-switch v-model:checked="enabledChecked" @change="enabledCheckedChange" />
</a-form-item>
<a-form-item label="营业执照" name="businessLicense">
<Upload
accept=".jpg,.jpeg,.png,.gif"
:maxUploadSize="1"
buttonText="点击上传营业执照"
:default-file-list="form.businessLicense"
@change="businessLicenseChange"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { message } from 'ant-design-vue';
import _ from 'lodash';
import { nextTick, reactive, ref } from 'vue';
import { enterpriseApi } from '/@/api/business/oa/enterprise-api';
import AreaCascader from '/@/components/framework/area-cascader/index.vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import Upload from '/@/components/support/file-upload/index.vue';
import { regular } from '/@/constants/regular-const';
import { smartSentry } from '/@/lib/smart-sentry';
import SmartEnumSelect from '/@/components/framework/smart-enum-select/index.vue';
defineExpose({
showModal,
});
const emit = defineEmits(['refresh']);
// --------------------- modal 显示与隐藏 ---------------------
// 是否展示
const visible = ref(false);
function showModal(enterpriseId) {
Object.assign(form, formDefault);
area.value = [];
if (enterpriseId) {
detail(enterpriseId);
}
visible.value = true;
}
function onClose() {
visible.value = false;
}
async function detail(enterpriseId) {
try {
let result = await enterpriseApi.detail(enterpriseId);
let data = result.data;
Object.assign(form, data);
nextTick(() => {
// 省市区不存在,不需要赋值
if (!data.provinceName) {
return;
}
area.value = [
{
value: data.province,
label: data.provinceName,
},
{
value: data.city,
label: data.cityName,
},
{
value: data.district,
label: data.districtName,
},
];
});
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
}
// --------------------- 表单 ---------------------
// 组件
const formRef = ref();
const formDefault = {
enterpriseId: undefined,
enterpriseName: undefined,
unifiedSocialCreditCode: undefined,
businessLicense: undefined,
contact: undefined,
enterpriseLogo: undefined,
contactPhone: undefined,
email: undefined,
province: undefined,
provinceName: undefined,
city: undefined,
cityName: undefined,
district: undefined,
districtName: undefined,
address: undefined,
disabledFlag: false,
};
let form = reactive({ ...formDefault });
const rules = {
enterpriseName: [{ required: true, message: '请输入企业名称' }],
unifiedSocialCreditCode: [{ required: true, message: '请输入统一社会信用代码' }],
contact: [{ required: true, message: '请输入联系人' }],
contactPhone: [
{ required: true, message: '请输入联系人电话' },
{ pattern: regular.phone, message: '请输入正确的联系人电话', trigger: 'blur' },
],
type: [{ required: true, message: '请选择类型' }],
};
function onSubmit() {
formRef.value
.validate()
.then(async () => {
SmartLoading.show();
try {
if (form.enterpriseId) {
await enterpriseApi.update(form);
} else {
await enterpriseApi.create(form);
}
message.success(`${form.enterpriseId ? '修改' : '添加'}成功`);
emit('refresh');
onClose();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
})
.catch((error) => {
console.log('error', error);
message.error('参数验证错误,请仔细填写表单数据!');
});
}
// 状态
const enabledChecked = ref(true);
function enabledCheckedChange(checked) {
form.disabledFlag = !checked;
}
// 地区
const area = ref([]);
function changeArea(value, selectedOptions) {
Object.assign(form, {
province: '',
provinceName: '',
city: '',
cityName: '',
district: '',
districtName: '',
});
if (!_.isEmpty(selectedOptions)) {
// 地区信息
form.province = area.value[0].value;
form.provinceName = area.value[0].label;
form.city = area.value[1].value;
form.cityName = area.value[1].label;
if (area.value[2]) {
form.district = area.value[2].value;
form.districtName = area.value[2].label;
}
}
}
function enterpriseLogoChange(fileList) {
form.enterpriseLogo = fileList;
}
function businessLicenseChange(fileList) {
form.businessLicense = fileList;
}
</script>
<style lang="less" scoped>
.form-width {
width: 100% !important;
}
.footer {
width: 100%;
display: flex;
justify-content: flex-end;
}
:deep(.ant-card-body) {
padding: 10px;
}
</style>

View File

@@ -0,0 +1,130 @@
<!--
* 公司 详情
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-15 20:15:49
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div class="detail-header">
<a-page-header :title="detail.enterpriseName" :avatar="{ src: logo }">
<template #extra>
<a-button @click="showUpdate" type="primary">编辑</a-button>
</template>
<div>
<a-descriptions size="small" :column="3">
<a-descriptions-item label="统一社会信用代码">{{ detail.unifiedSocialCreditCode }}</a-descriptions-item>
<a-descriptions-item label="联系人">{{ detail.contact }}</a-descriptions-item>
<a-descriptions-item label="联系人电话">{{ detail.contactPhone }}</a-descriptions-item>
<a-descriptions-item label="邮箱">{{ detail.email }}</a-descriptions-item>
<a-descriptions-item label="所在城市">{{ area }}</a-descriptions-item>
<a-descriptions-item label="详细地址">{{ detail.address }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ detail.createTime }}</a-descriptions-item>
<a-descriptions-item label="创建人">{{ detail.createUserName }}</a-descriptions-item>
<a-descriptions-item label="营业执照">
<FilePreview :file-list="detail.businessLicense" />
</a-descriptions-item>
</a-descriptions>
</div>
</a-page-header>
</div>
<a-card class="smart-margin-top10" size="small">
<a-tabs>
<a-tab-pane key="employee" tab="员工信息">
<EmployeeList :enterpriseId="enterpriseId" />
</a-tab-pane>
<a-tab-pane key="bank" tab="银行信息">
<BankList :enterpriseId="enterpriseId" />
</a-tab-pane>
<a-tab-pane key="invoice" tab="发票信息">
<InvoiceList :enterpriseId="enterpriseId" />
</a-tab-pane>
<a-tab-pane key="dataTracer" tab="变更记录">
<DataTracer :dataId="enterpriseId" :type="DATA_TRACER_TYPE_ENUM.OA_ENTERPRISE.value" />
</a-tab-pane>
</a-tabs>
<EnterpriseOperate ref="operateRef" @refresh="getDetail" />
</a-card>
</template>
<script setup>
import _ from 'lodash';
import { computed, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import BankList from './components/enterprise-bank-list.vue';
import EmployeeList from './components/enterprise-employee-list.vue';
import InvoiceList from './components/enterprise-invoice-list.vue';
import EnterpriseOperate from './components/enterprise-operate-modal.vue';
import { enterpriseApi } from '/@/api/business/oa/enterprise-api';
import { SmartLoading } from '/@/components/framework/smart-loading';
import DataTracer from '/@/components/support/data-tracer/index.vue';
import FilePreview from '/@/components/support/file-preview/index.vue';
import { DATA_TRACER_TYPE_ENUM } from '/@/constants/support/data-tracer-const';
import { smartSentry } from '/@/lib/smart-sentry';
const route = useRoute();
let enterpriseId = ref();
onMounted(() => {
if (route.query.enterpriseId) {
enterpriseId.value = Number(route.query.enterpriseId);
getDetail();
}
});
//编辑
const operateRef = ref();
function showUpdate() {
operateRef.value.showModal(enterpriseId.value);
}
// 详情
let detail = ref({});
async function getDetail() {
try {
let result = await enterpriseApi.detail(enterpriseId.value);
detail.value = result.data;
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
}
// 地区
const area = computed(() => {
let area = '';
if (!detail.value) {
return area;
}
if (detail.value.provinceName) {
area = area + detail.value.provinceName;
}
if (detail.value.cityName) {
area = area + detail.value.cityName;
}
if (detail.value.districtName) {
area = area + detail.value.districtName;
}
return area;
});
const logo = computed(() => {
if (!detail.value) {
return '';
}
if (!_.isEmpty(detail.value.enterpriseLogo)) {
return detail.value.enterpriseLogo[0].fileUrl;
}
return '';
});
</script>
<style lang="less" scoped>
.detail-header {
background-color: #fff;
padding: 10px;
}
</style>

View File

@@ -0,0 +1,283 @@
<!--
* 公司列表
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-15 20:15:49
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-form class="smart-query-form" v-privilege="'oa:enterprise:query'">
<a-row class="smart-query-form-row">
<a-form-item label="关键字" class="smart-query-form-item">
<a-input style="width: 300px" v-model:value="queryForm.keywords" placeholder="企业名称/联系人/联系电话" />
</a-form-item>
<a-form-item label="创建时间" class="smart-query-form-item">
<a-space direction="vertical" :size="12">
<a-range-picker v-model:value="searchDate" :presets="defaultTimeRanges" @change="dateChange" />
</a-space>
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-card size="small" :bordered="false" :hoverable="true">
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button @click="add()" v-privilege="'oa:enterprise:add'" type="primary">
<template #icon>
<PlusOutlined />
</template>
新建企业
</a-button>
<a-button @click="exportExcel()" v-privilege="'oa:enterprise:exportExcel'" type="primary">
<template #icon>
<FileExcelOutlined />
</template>
导出数据带水印
</a-button>
</div>
<div class="smart-table-setting-block">
<TableOperator v-model="columns" :tableId="TABLE_ID_CONST.BUSINESS.OA.ENTERPRISE" :refresh="ajaxQuery" />
</div>
</a-row>
<a-table
:scroll="{ x: 1300 }"
size="small"
:dataSource="tableData"
:columns="columns"
rowKey="enterpriseId"
:pagination="false"
:loading="tableLoading"
bordered
>
<template #bodyCell="{ column, record, text }">
<template v-if="column.dataIndex === 'disabledFlag'">
{{ text ? '禁用' : '启用' }}
</template>
<template v-if="column.dataIndex === 'enterpriseName'">
<a @click="detail(record.enterpriseId)">{{ record.enterpriseName }}</a>
</template>
<template v-if="column.dataIndex === 'type'">
<span>{{ $smartEnumPlugin.getDescByValue('ENTERPRISE_TYPE_ENUM', text) }}</span>
</template>
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button @click="update(record.enterpriseId)" size="small" v-privilege="'oa:enterprise:update'" type="link">编辑</a-button>
<a-button @click="confirmDelete(record.enterpriseId)" size="small" danger v-privilege="'oa:enterprise:delete'" type="link">删除</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `${total}`"
/>
</div>
<EnterpriseOperate ref="operateRef" @refresh="ajaxQuery" />
</a-card>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { enterpriseApi } from '/@/api/business/oa/enterprise-api';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { useRouter } from 'vue-router';
import EnterpriseOperate from './components/enterprise-operate-modal.vue';
import { smartSentry } from '/@/lib/smart-sentry';
import { defaultTimeRanges } from '/@/lib/default-time-ranges';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
// --------------------------- 企业表格 列 ---------------------------
const columns = ref([
{
title: '企业名称',
dataIndex: 'enterpriseName',
minWidth: 180,
ellipsis: true,
},
{
title: '统一社会信用代码',
dataIndex: 'unifiedSocialCreditCode',
minWidth: 170,
ellipsis: true,
},
{
title: '企业类型',
dataIndex: 'type',
width: 100,
},
{
title: '联系人',
width: 100,
dataIndex: 'contact',
ellipsis: true,
},
{
title: '联系人电话',
width: 120,
dataIndex: 'contactPhone',
ellipsis: true,
},
{
title: '邮箱',
minWidth: 100,
dataIndex: 'email',
ellipsis: true,
},
{
title: '状态',
width: 50,
dataIndex: 'disabledFlag',
},
{
title: '创建人',
width: 60,
dataIndex: 'createUserName',
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 150,
},
{
title: '操作',
dataIndex: 'action',
// fixed: 'right',
width: 100,
},
]);
// --------------------------- 查询 ---------------------------
const queryFormState = {
keywords: '',
endTime: null,
startTime: null,
pageNum: 1,
pageSize: PAGE_SIZE,
searchCount: true,
};
const queryForm = reactive({ ...queryFormState });
const tableLoading = ref(false);
const tableData = ref([]);
const total = ref(0);
// 日期选择
let searchDate = ref();
function dateChange(dates, dateStrings) {
queryForm.startTime = dateStrings[0];
queryForm.endTime = dateStrings[1];
}
function onSearch() {
queryForm.pageNum = 1;
ajaxQuery();
}
function resetQuery() {
searchDate.value = [];
Object.assign(queryForm, queryFormState);
ajaxQuery();
}
async function ajaxQuery() {
try {
tableLoading.value = true;
let responseModel = await enterpriseApi.pageQuery(queryForm);
const list = responseModel.data.list;
total.value = responseModel.data.total;
tableData.value = list;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
// --------------------------- 导出 ---------------------------
async function exportExcel() {
await enterpriseApi.exportExcel(queryForm);
}
// --------------------------- 删除 ---------------------------
function confirmDelete(enterpriseId) {
Modal.confirm({
title: '确定要删除吗?',
content: '删除后,该信息将不可恢复',
okText: '删除',
okType: 'danger',
onOk() {
del(enterpriseId);
},
cancelText: '取消',
onCancel() {},
});
}
async function del(enterpriseId) {
try {
SmartLoading.show();
await enterpriseApi.delete(enterpriseId);
message.success('删除成功');
ajaxQuery();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
// --------------------------- 增加、修改、详情 ---------------------------
let router = useRouter();
const operateRef = ref();
function add() {
operateRef.value.showModal();
}
function update(enterpriseId) {
operateRef.value.showModal(enterpriseId);
}
function detail(enterpriseId) {
router.push({ path: '/oa/enterprise/enterprise-detail', query: { enterpriseId: enterpriseId } });
}
onMounted(ajaxQuery);
</script>

View File

@@ -0,0 +1,307 @@
<!--
* 通知 表单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-21 19:52:43
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-drawer
:title="formData.noticeId ? '编辑' : '新建'"
:open="visibleFlag"
:width="1000"
:footerStyle="{ textAlign: 'right' }"
@close="onClose"
:destroyOnClose="true"
>
<a-form ref="formRef" :model="formData" :rules="formRules" :label-col="{ span: 3 }" :wrapper-col="{ span: 20 }">
<a-form-item label="公告标题" name="title">
<a-input v-model:value="formData.title" placeholder="请输入公告标题" />
</a-form-item>
<a-form-item label="分类" name="noticeTypeId">
<a-select v-model:value="formData.noticeTypeId" style="width: 100%" :showSearch="true" :allowClear="true">
<a-select-option v-for="item in noticeTypeList" :key="item.noticeTypeId" :value="item.noticeTypeId">
{{ item.noticeTypeName }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="文号">
<a-input v-model:value="formData.documentNumber" placeholder="文号1024创新实验室发2022字第36号" />
</a-form-item>
<a-form-item label="作者" name="author">
<a-input v-model:value="formData.author" placeholder="请输入作者" />
</a-form-item>
<a-form-item label="来源" name="source">
<a-input v-model:value="formData.source" placeholder="请输入来源" />
</a-form-item>
<a-form-item label="可见范围" name="allVisibleFlag">
<a-select v-model:value="formData.allVisibleFlag" placeholder="请选择可见范围">
<a-select-option :value="1">全部可见</a-select-option>
<a-select-option :value="0">部分可见</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-show="!formData.allVisibleFlag" label="可见员工/部门">
<a-button type="primary" @click="showNoticeVisibleModal">选择</a-button>
<div class="visible-list">
<div class="visible-item" v-for="(item, index) in formData.visibleRangeList" :key="item.dataId">
<a-tag>
<span>{{ item.dataName }}</span>
<close-outlined @click="removeVisibleItem(index)" />
</a-tag>
</div>
</div>
</a-form-item>
<a-form-item label="定时发布">
<a-switch
v-model:checked="formData.scheduledPublishFlag"
checked-children=""
un-checked-children=""
@change="changesSheduledPublishFlag"
/>
</a-form-item>
<a-form-item v-show="formData.scheduledPublishFlag" label="发布时间">
<a-date-picker
v-model:value="releaseTime"
:format="timeFormat"
showTime
:allowClear="false"
placeholder="请选择发布时间"
style="width: 200px"
@change="changeTime"
/>
</a-form-item>
<a-form-item label="公告内容" name="contentHtml">
<SmartWangeditor ref="contentRef" :modelValue="formData.contentHtml" :height="300" />
</a-form-item>
<a-form-item label="附件">
<Upload
:defaultFileList="defaultFileList"
:maxUploadSize="10"
:folder="FILE_FOLDER_TYPE_ENUM.NOTICE.value"
buttonText="上传附件"
listType="text"
extraMsg="最多上传10个附件"
@change="changeAttachment"
/>
</a-form-item>
</a-form>
<template #footer>
<a-space>
<a-button @click="onClose">取消</a-button>
<a-button type="primary" @click="onSubmit">保存</a-button>
</a-space>
</template>
</a-drawer>
<!-- 选择可见范围弹窗 -->
<NoticeFormVisibleModal ref="noticeFormVisibleModal" @selectedFinish="finishCanSelectedVisibleRange" />
</template>
<script setup>
import { reactive, ref, onMounted, watch, computed, nextTick } from 'vue';
import { message, Modal } from 'ant-design-vue';
import _ from 'lodash';
import dayjs, { Dayjs } from 'dayjs';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { FILE_FOLDER_TYPE_ENUM } from '/@/constants/support/file-const';
import { noticeApi } from '/@/api/business/oa/notice-api';
import SmartWangeditor from '/@/components/framework/wangeditor/index.vue';
import Upload from '/@/components/support/file-upload/index.vue';
import NoticeFormVisibleModal from './notice-form-visible-modal.vue';
import { smartSentry } from '/@/lib/smart-sentry';
const emits = defineEmits(['reloadList']);
// ------------------ 显示,关闭 ------------------
// 显示
const visibleFlag = ref(false);
function showModal(noticeId) {
Object.assign(formData, defaultFormData);
releaseTime.value = null;
defaultFileList.value = [];
queryNoticeTypeList();
if (noticeId) {
getNoticeUpdate(noticeId);
}
visibleFlag.value = true;
nextTick(() => {
formRef.value.clearValidate();
});
}
// 关闭
function onClose() {
visibleFlag.value = false;
}
// ------------------ 表单 ------------------
const formRef = ref();
const contentRef = ref();
const noticeFormVisibleModal = ref();
const defaultFormData = {
noticeId: undefined,
noticeTypeId: undefined,
title: undefined, // 标题
categoryId: undefined, // 分类
source: undefined, // 来源
documentNumber: undefined, // 文号
author: undefined, // 作者
allVisibleFlag: 1, // 是否全部可见
visibleRangeList: [], // 可见范围
scheduledPublishFlag: false, // 是否定时发布
publishTime: undefined, // 发布时间
attachment: [], // 附件
contentHtml: '', // html内容
contentText: '', // 纯文本内容
};
const formData = reactive({ ...defaultFormData });
const formRules = {
title: [{ required: true, message: '请输入' }],
noticeTypeId: [{ required: true, message: '请选择分类' }],
allVisibleFlag: [{ required: true, message: '请选择' }],
source: [{ required: true, message: '请输入来源' }],
author: [{ required: true, message: '请输入作者' }],
contentHtml: [{ required: true, message: '请输入内容' }],
};
// 查询详情
async function getNoticeUpdate(noticeId) {
try {
SmartLoading.show();
const result = await noticeApi.getUpdateNoticeInfo(noticeId);
const attachment = result.data.attachment;
if (!_.isEmpty(attachment)) {
defaultFileList.value = attachment;
} else {
defaultFileList.value = [];
}
Object.assign(formData, result.data);
formData.allVisibleFlag = formData.allVisibleFlag ? 1 : 0;
releaseTime.value = dayjs(result.data.publishTime);
visibleFlag.value = true;
} catch (err) {
smartSentry.captureError(err);
} finally {
SmartLoading.hide();
}
}
// 点击确定,验证表单
async function onSubmit() {
try {
formData.contentHtml = contentRef.value.getHtml();
formData.contentText = contentRef.value.getText();
await formRef.value.validateFields();
save();
} catch (err) {
message.error('参数验证错误,请仔细填写表单数据!');
}
}
// 新建、编辑API
async function save() {
try {
SmartLoading.show();
if (formData.allVisibleFlag) {
formData.visibleRangeList = [];
}
if (!formData.publishTime) {
formData.publishTime = dayjs().format(timeFormat);
}
if (formData.noticeId) {
await noticeApi.updateNotice(formData);
} else {
await noticeApi.addNotice(formData);
}
message.success('保存成功');
emits('reloadList');
onClose();
} catch (err) {
smartSentry.captureError(err);
} finally {
SmartLoading.hide();
}
}
// ------------------ 通知分类 ------------------
// 查询分类列表
const noticeTypeList = ref([]);
async function queryNoticeTypeList() {
try {
const result = await noticeApi.getAllNoticeTypeList();
noticeTypeList.value = result.data;
if (noticeTypeList.value.length > 0 && !formData.noticeId) {
formData.noticeTypeId = noticeTypeList.value[0].noticeTypeId;
}
} catch (err) {
smartSentry.captureError(err);
}
}
// ----------------------- 可见员工/部门 ----------------------------
// 点击显示选择可见员工/部门
function showNoticeVisibleModal() {
const visibleRangeList = formData.visibleRangeList || [];
noticeFormVisibleModal.value.showModal(visibleRangeList);
}
// 选择完成回调
function finishCanSelectedVisibleRange(selectedList) {
formData.visibleRangeList = selectedList;
}
// 移除某个员工/部门
function removeVisibleItem(index) {
Modal.confirm({
title: '提示',
content: '确定移除吗?',
onOk() {
formData.visibleRangeList.splice(index, 1);
},
});
}
// ----------------------- 发布时间 ----------------------------
const timeFormat = 'YYYY-MM-DD HH:mm:ss';
const releaseTime = ref(null);
function changeTime(date, dateString) {
formData.publishTime = dateString;
}
function changesSheduledPublishFlag(checked) {
releaseTime.value = checked ? dayjs() : null;
formData.publishTime = checked ? dayjs().format(timeFormat) : null;
}
// ----------------------- 上传附件 ----------------------------
// 已上传的附件列表
const defaultFileList = ref([]);
function changeAttachment(fileList) {
defaultFileList.value = fileList;
formData.attachment = _.isEmpty(fileList) ? [] : fileList;
}
// ----------------------- 以下是暴露的方法内容 ------------------------
defineExpose({
showModal,
});
</script>
<style lang="less" scoped>
.visible-list {
display: flex;
flex-wrap: wrap;
.visible-item {
padding-top: 8px;
}
}
</style>

View File

@@ -0,0 +1,71 @@
<!--
* 通知 可见范围
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-21 19:52:43
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal title="选择部门" v-model:open="visibleFlag" :maskClosable="false" :width="768" @ok="onSubmit" @cancel="onClose">
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane :key="1" tab="选择员工">
<NoticeFormVisibleTransferEmployee :employeeList="employeeList" @onChange="onChangeEmployee" />
</a-tab-pane>
<a-tab-pane :key="2" tab="选择部门">
<NoticeFormVisibleTransferDepartment :departmentList="departmentList" @onChange="onChangeDepartment" />
</a-tab-pane>
</a-tabs>
</a-modal>
</template>
<script setup>
import { reactive, ref, onMounted, watch } from 'vue';
import { NOTICE_VISIBLE_RANGE_DATA_TYPE_ENUM } from '/@/constants/business/oa/notice-const';
import NoticeFormVisibleTransferDepartment from './notice-form-visible-transfer-department.vue';
import NoticeFormVisibleTransferEmployee from './notice-form-visible-transfer-employee.vue';
const emits = defineEmits('selectedFinish');
const visibleFlag = ref(false);
function onClose() {
visibleFlag.value = false;
}
const activeKey = ref(1);
// 已选的员工列表
const employeeList = ref([]);
// 已选的部门列表
const departmentList = ref([]);
// 显示弹窗
function showModal(visibleRangeList = []) {
employeeList.value = visibleRangeList.filter((item) => item.dataType === NOTICE_VISIBLE_RANGE_DATA_TYPE_ENUM.EMPLOYEE.value);
departmentList.value = visibleRangeList.filter((item) => item.dataType === NOTICE_VISIBLE_RANGE_DATA_TYPE_ENUM.DEPARTMENT.value);
activeKey.value = 1;
visibleFlag.value = true;
}
function onSubmit() {
const selectedList = [...employeeList.value, ...departmentList.value];
emits('selectedFinish', selectedList);
onClose();
}
// 选择员工改变
function onChangeEmployee({ selectedList }) {
employeeList.value = selectedList;
}
// 选择部门改变
function onChangeDepartment({ selectedList }) {
departmentList.value = selectedList;
}
// ----------------------- 以下是暴露的方法内容 ------------------------
defineExpose({
showModal,
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,204 @@
<!--
* 通知 可见范围 选择部门
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-21 19:52:43
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div class="wrapper">
<div class="sider-fl">
<a-tree :tree-data="treeData" :fieldNames="{ title: 'name', key: 'departmentId' }" :selectable="false" v-model:expandedKeys="expandedKeys">
<template #switcherIcon="{ switcherCls }">
<caret-down-outlined :class="switcherCls" />
</template>
<template #title="{ name, departmentId }">
<div class="list-item" :class="{ active: checkExists(departmentId) }">
<div class="list-item-title">{{ name }}</div>
<check-circle-filled class="check-icon-style" @click="onSelectAdd(name, departmentId)" />
</div>
</template>
</a-tree>
</div>
<div class="sider-fr">
<div class="selected-list">
<div class="list-item" v-for="(item, index) in selectedList" :key="item.dataId">
<div class="list-item-title">{{ item.dataName }}</div>
<close-circle-two-tone @click="onRemove(index)" />
</div>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, ref, onMounted, watch, computed, nextTick } from 'vue';
import _ from 'lodash';
import { NOTICE_VISIBLE_RANGE_DATA_TYPE_ENUM } from '/@/constants/business/oa/notice-const';
import { departmentApi } from '/@/api/system/department-api';
import { smartSentry } from '/@/lib/smart-sentry';
const props = defineProps({
// 已选择的部门数据列表
departmentList: {
type: Array,
default: () => [],
},
});
const emits = defineEmits(['onChange']);
const treeData = ref([]);
async function queryDepartmentTree() {
try {
const result = await departmentApi.queryDepartmentTree();
if (!_.isEmpty(result.data)) {
treeData.value = result.data;
setExpanded();
}
} catch (err) {
smartSentry.captureError(err);
}
}
// 设置默认展开的节点
const expandedKeys = ref([]);
function setExpanded() {
expandedKeys.value = [treeData.value[0].departmentId];
}
// 选择的部门列表数据
const selectedList = ref([]);
// 选择的部门列表Ids
const selectedIds = computed(() => {
return selectedList.value.map((item) => item.dataId);
});
watch(
() => props.departmentList,
(newVal) => {
selectedList.value = newVal;
},
{ immediate: true }
);
// 检查是否已选
function checkExists(dataId) {
return selectedIds.value.includes(dataId);
}
// 点击左边添加
function onSelectAdd(name, departmentId) {
if (checkExists(departmentId)) {
return;
}
selectedList.value.push({
dataName: name,
dataId: departmentId,
dataType: NOTICE_VISIBLE_RANGE_DATA_TYPE_ENUM.DEPARTMENT.value,
});
onChangeEmit();
}
// 点击右边移除
function onRemove(index) {
selectedList.value.splice(index, 1);
onChangeEmit();
}
function onChangeEmit() {
emits('onChange', { selectedList: selectedList.value, selectedIds: selectedIds.value });
}
onMounted(() => {
queryDepartmentTree();
});
</script>
<style lang="less" scoped>
:deep(.ant-tree-list-holder-inner) {
display: block !important;
.ant-tree-treenode {
align-items: center;
padding-bottom: 0;
&:hover {
background-color: #f9f9f9;
}
.ant-tree-switcher {
display: flex;
align-items: center;
justify-content: center;
.ant-tree-switcher-icon {
font-size: 12px;
}
}
.ant-tree-node-content-wrapper {
display: block;
flex: 1;
&:hover {
cursor: auto;
}
.ant-tree-title {
display: block;
}
}
}
}
.wrapper {
display: flex;
.sider-fl,
.sider-fr {
flex: 1;
height: 500px;
border: 1px solid #d9d9d9;
overflow: hidden;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track-piece {
background-color: #ededed;
}
&::-webkit-scrollbar-thumb {
height: 50px;
background-color: #a1a1a1;
border-radius: 4px;
}
}
.sider-fr {
margin-left: 15px;
.list-item {
padding-left: 14px;
}
}
}
.list-item {
display: flex;
align-items: center;
padding: 0 14px 0 0;
height: 32px;
&:hover {
background-color: #f9f9f9;
}
&.active {
.check-icon-style {
cursor: auto;
color: @primary-color;
}
}
.list-item-title {
flex: 1;
margin-right: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.check-icon-style {
color: #d9d9d9;
}
}
</style>

View File

@@ -0,0 +1,252 @@
<!--
* 通知 可见范围 选择员工
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-21 19:52:43
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div class="wrapper">
<div class="sider-left">
<a-tree :tree-data="treeData" :fieldNames="{ title: 'name' }" :selectable="false" v-model:expandedKeys="expandedKeys">
<template #switcherIcon="{ switcherCls }">
<caret-down-outlined :class="switcherCls" />
</template>
<template #title="{ name, id, dataType }">
<div class="list-item" :class="{ active: checkExists(id) }">
<div class="list-item-title">
<user-outlined v-if="dataType === NOTICE_VISIBLE_RANGE_DATA_TYPE_ENUM.EMPLOYEE.value" />
{{ name }}
</div>
<check-circle-filled
v-if="dataType === NOTICE_VISIBLE_RANGE_DATA_TYPE_ENUM.EMPLOYEE.value"
class="check-icon-style"
@click="onSelectAdd(name, id, dataType)"
/>
</div>
</template>
</a-tree>
</div>
<div class="sider-right">
<div class="selected-list">
<div class="list-item" v-for="(item, index) in selectedList" :key="item.id">
<div class="list-item-title">{{ item.dataName }}</div>
<close-circle-two-tone @click="onRemove(index)" />
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import _ from 'lodash';
import { NOTICE_VISIBLE_RANGE_DATA_TYPE_ENUM } from '/@/constants/business/oa/notice-const';
import { departmentApi } from '/@/api/system/department-api';
import { employeeApi } from '/@/api/system/employee-api';
import { smartSentry } from '/@/lib/smart-sentry';
const props = defineProps({
// 已选择的员工数据列表
employeeList: {
type: Array,
default: () => [],
},
});
const emits = defineEmits(['onChange']);
const treeData = ref([]);
// 查询部门树形
async function queryDepartmentTree() {
try {
const departmentResult = await departmentApi.queryDepartmentTree();
const employeeResult = await employeeApi.queryAll();
const departmentTree = departmentResult.data;
buildDepartmentEmployeeTree(departmentTree, employeeResult.data);
if (!_.isEmpty(departmentTree)) {
treeData.value = departmentTree;
console.log(treeData.value);
nextTick(() => {
setExpanded();
});
}
} catch (err) {
smartSentry.captureError(err);
}
}
// 递归构建部门员工树
function buildDepartmentEmployeeTree(departmentTree, employeeList) {
for (const department of departmentTree) {
if (department.dataType && department.dataType === NOTICE_VISIBLE_RANGE_DATA_TYPE_ENUM.EMPLOYEE.value) {
continue;
}
department.id = department.departmentId;
department.key = 'department_' + department.departmentId;
department.dataType = NOTICE_VISIBLE_RANGE_DATA_TYPE_ENUM.DEPARTMENT.value;
let employeeChildren = employeeList
.filter((e) => e.departmentId === department.departmentId)
.map((e) =>
Object.assign(
{},
{
id: e.employeeId,
key: 'employee_' + e.employeeId,
name: e.actualName,
dataType: NOTICE_VISIBLE_RANGE_DATA_TYPE_ENUM.EMPLOYEE.value,
}
)
);
if (!department.children) {
department.children = [];
}
department.children.push(...employeeChildren);
buildDepartmentEmployeeTree(department.children, employeeList);
}
}
// 设置默认展开的节点
const expandedKeys = ref([]);
function setExpanded() {
expandedKeys.value = [treeData.value[0].key];
}
// 选择的员工列表数据
const selectedList = ref([]);
// 选择的员工列表Ids
const selectedIds = computed(() => {
return selectedList.value.map((item) => item.dataId);
});
watch(
() => props.employeeList,
(newVal) => {
selectedList.value = newVal;
},
{ immediate: true }
);
// 检查是否已选
function checkExists(id) {
return selectedIds.value.includes(id);
}
// 点击左边添加
function onSelectAdd(name, id, dataType) {
if (checkExists(id)) {
return;
}
selectedList.value.push({
dataName: name,
dataId: id,
dataType: NOTICE_VISIBLE_RANGE_DATA_TYPE_ENUM.EMPLOYEE.value,
});
onChangeEmit();
}
// 点击右边移除
function onRemove(index) {
selectedList.value.splice(index, 1);
onChangeEmit();
}
function onChangeEmit() {
emits('onChange', { selectedList: selectedList.value, selectedIds: selectedIds.value });
}
onMounted(() => {
queryDepartmentTree();
});
</script>
<style lang="less" scoped>
:deep(.ant-tree-list-holder-inner) {
display: block !important;
.ant-tree-treenode {
align-items: center;
padding-bottom: 0;
&:hover {
background-color: #f9f9f9;
}
.ant-tree-switcher {
display: flex;
align-items: center;
justify-content: center;
.ant-tree-switcher-icon {
font-size: 12px;
}
}
.ant-tree-node-content-wrapper {
display: block;
flex: 1;
&:hover {
cursor: auto;
}
.ant-tree-title {
display: block;
}
}
}
}
.wrapper {
display: flex;
.sider-left,
.sider-right {
flex: 1;
height: 500px;
border: 1px solid #d9d9d9;
overflow: hidden;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track-piece {
background-color: #ededed;
}
&::-webkit-scrollbar-thumb {
height: 50px;
background-color: #a1a1a1;
border-radius: 4px;
}
}
.sider-right {
margin-left: 15px;
.list-item {
padding-left: 14px;
}
}
}
.list-item {
display: flex;
align-items: center;
padding: 0 14px 0 0;
height: 32px;
&:hover {
background-color: #f9f9f9;
}
&.active {
.check-icon-style {
cursor: auto;
color: @primary-color;
}
}
.list-item-title {
flex: 1;
margin-right: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.check-icon-style {
color: #d9d9d9;
}
}
</style>

View File

@@ -0,0 +1,161 @@
<!--
* 通知 查看记录
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-21 19:52:43
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="部门" class="smart-query-form-item" style="width: 280px; margin-right: 20px">
<DepartmentTreeSelect v-model:value="queryForm.departmentId" width="100%" />
</a-form-item>
<a-form-item label="关键字" class="smart-query-form-item" style="width: 280px">
<a-input v-model:value="queryForm.keywords" placeholder="姓名/IP/设备" />
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-table rowKey="employeeId" :columns="tableColumns" :dataSource="tableData" :pagination="false" :loading="tableLoading" size="small" bordered>
<template #bodyCell="{ column, record, text }">
<template v-if="column.dataIndex === 'employeeName'"> {{ text }}({{ record.departmentName }}) </template>
<template v-if="column.dataIndex === 'firstIp'"> {{ text }} ({{ record.firstDevice }}) </template>
<template v-if="column.dataIndex === 'lastIp'"> {{ text }} ({{ record.lastDevice }}) </template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="queryViewRecord"
@showSizeChange="queryViewRecord"
:show-total="(total) => `${total}`"
/>
</div>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { noticeApi } from '/@/api/business/oa/notice-api';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import DepartmentTreeSelect from '/@/components/system/department-tree-select/index.vue';
import uaparser from 'ua-parser-js';
const props = defineProps({
noticeId: {
type: [Number, String],
},
});
defineExpose({
onSearch,
});
const tableColumns = [
{
title: '姓名',
dataIndex: 'employeeName',
},
{
title: '查看次数',
dataIndex: 'pageViewCount',
},
{
title: '首次查看设备',
dataIndex: 'firstIp',
},
{
title: '首次查看时间',
dataIndex: 'createTime',
},
{
title: '最后一次查看设备',
dataIndex: 'lastIp',
},
{
title: '最后一次查看时间',
dataIndex: 'updateTime',
with: 80,
},
];
const tableData = ref([]);
const total = ref(0);
const tableLoading = ref(false);
const defaultQueryForm = {
noticeId: props.noticeId,
departmentId: null,
keywords: '',
pageNum: 1,
pageSize: PAGE_SIZE,
};
const queryForm = reactive({ ...defaultQueryForm });
function buildDeviceInfo(userAgent) {
if (!userAgent) {
return '';
}
let ua = uaparser(userAgent);
let browser = ua.browser.name;
let os = ua.os.name;
return browser + '/' + os + '/' + (ua.device.vendor ? ua.device.vendor + ua.device.model : '');
}
async function queryViewRecord() {
try {
tableLoading.value = true;
const result = await noticeApi.queryViewRecord(queryForm);
for (const e of result.data.list) {
e.firstDevice = buildDeviceInfo(e.firstUserAgent);
e.lastDevice = buildDeviceInfo(e.lastUserAgent);
}
tableData.value = result.data.list;
total.value = result.data.total;
} catch (err) {
console.log(err);
} finally {
tableLoading.value = false;
}
}
// 点击查询
function onSearch() {
queryForm.pageNum = 1;
queryViewRecord();
}
// 点击重置
function resetQuery() {
Object.assign(queryForm, defaultQueryForm);
queryViewRecord();
}
</script>

View File

@@ -0,0 +1,144 @@
<!--
* 通知 详情
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-21 19:52:43
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-card style="margin-bottom: 15px" size="small">
<a-descriptions :title="noticeDetail.title" :column="4" size="small">
<template #extra>
<a-button v-if="!noticeDetail.publishFlag" type="primary" size="small" @click="onEdit">编辑</a-button>
</template>
<a-descriptions-item label="分类">{{ noticeDetail.noticeTypeName }}</a-descriptions-item>
<a-descriptions-item label="文号">{{ noticeDetail.documentNumber }}</a-descriptions-item>
<a-descriptions-item label="来源">{{ noticeDetail.source }}</a-descriptions-item>
<a-descriptions-item label="作者">{{ noticeDetail.author }}</a-descriptions-item>
<a-descriptions-item label="页面浏览量">{{ noticeDetail.pageViewCount }}</a-descriptions-item>
<a-descriptions-item label="用户浏览量">{{ noticeDetail.userViewCount }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ noticeDetail.createTime }}</a-descriptions-item>
<a-descriptions-item label="发布时间">{{ noticeDetail.publishTime }}</a-descriptions-item>
<a-descriptions-item label="定时发布">{{ noticeDetail.publishFlag ? '已发布' : '待发布' }}</a-descriptions-item>
<a-descriptions-item label="删除状态">{{ noticeDetail.deletedFlag ? '已删除' : '未删除' }}</a-descriptions-item>
<a-descriptions-item v-if="!$lodash.isEmpty(noticeDetail.attachment)" label="附件">
<div class="file-list">
<a class="file-item" v-for="item in noticeDetail.attachment" :key="item.fileId" @click="onPrevFile(item)">{{ item.fileName }}</a>
</div>
</a-descriptions-item>
<a-descriptions-item label="可见范围">
<template v-if="noticeDetail.allVisibleFlag">全部可见</template>
<div class="visible-list">
<div class="visible-item" v-for="item in noticeDetail.visibleRangeList" :key="item.dataId">
{{ item.dataName }}
</div>
</div>
</a-descriptions-item>
</a-descriptions>
</a-card>
<a-card size="small">
<a-tabs v-model:activeKey="activeKey" size="small">
<a-tab-pane :key="1" tab="内容">
<div class="content-html" v-html="noticeDetail.contentHtml"></div>
</a-tab-pane>
<a-tab-pane :key="2" tab="查看记录" force-render>
<NoticeViewRecordList ref="noticeViewRecordList" :noticeId="route.query.noticeId" />
</a-tab-pane>
<a-tab-pane :key="3" tab="操作记录" />
</a-tabs>
</a-card>
<!-- 编辑 -->
<NoticeFormDrawer ref="noticeFormDrawerRef" @reloadList="queryNoticeDetail" />
<!-- 预览附件 -->
<FilePreviewModal ref="filePreviewRef" />
</template>
<script setup>
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import NoticeFormDrawer from './components/notice-form-drawer.vue';
import NoticeViewRecordList from './components/notice-view-record-list.vue';
import { noticeApi } from '/@/api/business/oa/notice-api';
import { SmartLoading } from '/@/components/framework/smart-loading';
import FilePreviewModal from '/@/components/support/file-preview-modal/index.vue';
import { smartSentry } from '/@/lib/smart-sentry';
const route = useRoute();
const props = defineProps({
newsType: {
type: Number,
},
});
const activeKey = ref(1);
const noticeDetail = ref({});
const noticeViewRecordList = ref();
onMounted(() => {
if (route.query.noticeId) {
queryNoticeDetail();
noticeViewRecordList.value.onSearch();
}
});
// 查询详情
async function queryNoticeDetail() {
try {
SmartLoading.show();
const result = await noticeApi.getUpdateNoticeInfo(route.query.noticeId);
noticeDetail.value = result.data;
} catch (err) {
smartSentry.captureError(err);
} finally {
SmartLoading.hide();
}
}
// 点击编辑
const noticeFormDrawerRef = ref();
function onEdit() {
noticeFormDrawerRef.value.showModal(noticeDetail.value.noticeId);
}
// 预览附件
const filePreviewRef = ref();
function onPrevFile(fileItem) {
filePreviewRef.value.showPreview(fileItem);
}
</script>
<style lang="less" scoped>
:deep(.ant-descriptions-item-content) {
flex: 1;
overflow: hidden;
}
.file-list {
width: 100%;
display: flex;
flex-wrap: wrap;
.file-item {
display: block;
margin-right: 10px;
}
}
.visible-list {
display: flex;
flex-wrap: wrap;
.visible-item {
margin-right: 10px;
color: #666;
}
}
.content-html {
img {
max-width: 100%;
}
}
</style>

View File

@@ -0,0 +1,145 @@
<!--
* 通知 详情 员工
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-21 19:52:43
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-card size="small">
<div>
<div class="content-header">
<!--startprint-->
<div class="content-header-title">
{{ noticeDetail.title }}
</div>
<div class="content-header-info">
<span v-show="noticeDetail.author">作者{{ noticeDetail.author }}</span>
<span v-show="noticeDetail.source">来源{{ noticeDetail.source }}</span>
<span>发布时间{{ noticeDetail.publishTime }}</span>
<span>阅读量{{ noticeDetail.pageViewCount }}</span>
<span @click="print">打印本页</span>
</div>
</div>
<div class="content-html" v-html="noticeDetail.contentHtml"></div>
<!--endprint-->
</div>
<a-divider />
<div>
附件
<file-preview v-if="!$lodash.isEmpty(noticeDetail.attachment)" :fileList="noticeDetail.attachment" />
<span v-else></span>
</div>
</a-card>
<a-card title="记录" size="small" class="smart-margin-top10">
<NoticeViewRecordList ref="noticeViewRecordList" :noticeId="route.query.noticeId" />
</a-card>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import NoticeViewRecordList from './components/notice-view-record-list.vue';
import { noticeApi } from '/@/api/business/oa/notice-api';
import { SmartLoading } from '/@/components/framework/smart-loading';
import FilePreview from '/@/components/support/file-preview/index.vue';
import { smartSentry } from '/@/lib/smart-sentry';
const route = useRoute();
const noticeDetail = ref({});
onMounted(() => {
if (route.query.noticeId) {
queryNoticeDetail();
}
});
const noticeViewRecordList = ref();
// 查询详情
async function queryNoticeDetail() {
try {
SmartLoading.show();
const result = await noticeApi.view(route.query.noticeId);
noticeDetail.value = result.data;
noticeViewRecordList.value.onSearch();
} catch (err) {
smartSentry.captureError(err);
} finally {
SmartLoading.hide();
}
}
// 点击编辑
const noticeFormDrawerRef = ref();
function onEdit() {
noticeFormDrawerRef.value.showModal(noticeDetail.value.noticeId);
}
// 打印
function print() {
let bdhtml = window.document.body.innerHTML;
let sprnstr = '<!--startprint-->'; //必须在页面添加<!--startprint-->和<!--endprint-->而且需要打印的内容必须在它们之间
let eprnstr = '<!--endprint-->';
let prnhtml = bdhtml.substr(bdhtml.indexOf(sprnstr));
prnhtml = prnhtml.substring(0, prnhtml.indexOf(eprnstr));
let newWin = window.open(''); //新打开一个空窗口
newWin.document.body.innerHTML = prnhtml;
newWin.document.close(); //在IE浏览器中使用必须添加这一句
newWin.focus(); //在IE浏览器中使用必须添加这一句
newWin.print(); //打印
newWin.close(); //关闭窗口
}
</script>
<style lang="less" scoped>
:deep(.ant-descriptions-item-content) {
flex: 1;
overflow: hidden;
}
.file-list {
width: 100%;
display: flex;
flex-wrap: wrap;
.file-item {
display: block;
margin-right: 10px;
}
}
.visible-list {
display: flex;
flex-wrap: wrap;
.visible-item {
margin-right: 10px;
color: #666;
}
}
.content-header {
.content-header-title {
margin: 10px 0px;
font-size: 18px;
font-weight: bold;
text-align: center;
}
.content-header-info {
margin: 10px 0px;
font-size: 14px;
color: #888;
text-align: center;
span {
margin: 0 10px;
cursor: pointer;
}
}
}
.content-html {
img {
max-width: 100%;
}
}
</style>

View File

@@ -0,0 +1,212 @@
<!--
* 通知 详情 员工列表
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-21 19:52:43
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="关键字" class="smart-query-form-item">
<a-input style="width: 200px" v-model:value="queryForm.keywords" placeholder="标题、作者、来源、文号" />
</a-form-item>
<a-form-item label="发布时间" class="smart-query-form-item">
<a-range-picker v-model:value="publishDate" @change="publishDateChange" style="width: 220px" />
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="onReload">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
<a-row class="smart-query-form-row"> </a-row>
</a-form>
<a-card size="small" :bordered="false">
<a-tabs @change="changeNotViewFlag" size="small">
<a-tab-pane :key="0" tab="全部" />
<a-tab-pane :key="1" tab="未读" />
</a-tabs>
<a-table rowKey="noticeId" :columns="tableColumns" :dataSource="tableData" :pagination="false" :loading="tableLoading" bordered size="small">
<template #bodyCell="{ column, record, text }">
<template v-if="column.dataIndex === 'title'">
<span v-show="record.viewFlag">
<a @click="toDetail(record.noticeId)" style="color: #8c8c8c">{{ record.noticeTypeName }}{{ text }}已读</a>
</span>
<span v-show="!record.viewFlag">
<a @click="toDetail(record.noticeId)"
>{{ record.noticeTypeName }}{{ text }}
<span style="color: red">未读</span>
</a>
</span>
</template>
<template v-if="column.dataIndex === 'pageViewCount'"> {{ record.userViewCount }} / {{ record.pageViewCount }} </template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="queryNoticeList"
@showSizeChange="queryNoticeList"
:show-total="(total) => `${total}`"
/>
</div>
</a-card>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { noticeApi } from '/@/api/business/oa/notice-api';
import { smartSentry } from '/@/lib/smart-sentry';
const tableColumns = reactive([
{
title: `标题`,
dataIndex: 'title',
ellipsis: true,
},
{
title: '访问量',
dataIndex: 'pageViewCount',
width: 90,
},
{
title: `来源`,
dataIndex: 'source',
width: 150,
ellipsis: true,
},
{
title: `作者`,
dataIndex: 'author',
width: 80,
ellipsis: true,
},
{
title: '发布时间',
dataIndex: 'publishTime',
width: 150,
},
]);
// ------------------ 通知分类 ------------------
// 查询分类列表
const noticeTypeList = ref([]);
async function queryNoticeTypeList() {
try {
const result = await noticeApi.getAllNoticeTypeList();
noticeTypeList.value = result.data;
} catch (err) {
smartSentry.captureError(err);
}
}
// ------------------ 查询相关 ------------------
const queryFormState = {
noticeTypeId: undefined, //分类
keywords: '', //标题、作者、来源
publishTimeBegin: null, //发布-开始时间
publishTimeEnd: null, //发布-截止时间
notViewFlag: false, //未读
pageNum: 1,
pageSize: PAGE_SIZE,
};
const queryForm = reactive({ ...queryFormState });
const tableData = ref([]);
const total = ref(0);
const tableLoading = ref(false);
onMounted(() => {
queryNoticeTypeList();
queryNoticeList();
});
function changeNotViewFlag(value) {
queryForm.notViewFlag = value === 0 ? null : true;
onSearch();
}
// 查询列表
async function queryNoticeList() {
try {
tableLoading.value = true;
const result = await noticeApi.queryEmployeeNotice(queryForm);
tableData.value = result.data.list;
total.value = result.data.total;
} catch (err) {
smartSentry.captureError(err);
} finally {
tableLoading.value = false;
}
}
// 点击查询
function onSearch() {
queryForm.pageNum = 1;
queryNoticeList();
}
// 点击重置
function onReload() {
Object.assign(queryForm, queryFormState);
publishDate.value = [];
createDate.value = [];
queryNoticeList();
}
// 发布日期选择
const publishDate = ref([]);
function publishDateChange(dates, dateStrings) {
queryForm.publishTimeBegin = dateStrings[0];
queryForm.publishTimeEnd = dateStrings[1];
}
// 创建日期选择
const createDate = ref([]);
function createDateChange(dates, dateStrings) {
queryForm.createTimeBegin = dateStrings[0];
queryForm.createTimeEnd = dateStrings[1];
}
// ------------------ 详情 ------------------
// 进入详情
const router = useRouter();
function toDetail(noticeId) {
router.push({
path: '/oa/notice/notice-employee-detail',
query: { noticeId },
});
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,352 @@
<!--
* 通知 管理列表
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-21 19:52:43
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-form class="smart-query-form" v-privilege="'oa:notice:query'">
<a-row class="smart-query-form-row">
<a-form-item label="分类" class="smart-query-form-item">
<a-select v-model:value="queryForm.noticeTypeId" style="width: 100px" :showSearch="true" :allowClear="true" placeholder="分类">
<a-select-option v-for="item in noticeTypeList" :key="item.noticeTypeId" :value="item.noticeTypeId">
{{ item.noticeTypeName }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="关键字" class="smart-query-form-item">
<a-input style="width: 300px" v-model:value="queryForm.keywords" placeholder="标题、作者、来源" />
</a-form-item>
<a-form-item label="文号" class="smart-query-form-item">
<a-input style="width: 150px" v-model:value="queryForm.documentNumber" placeholder="文号" />
</a-form-item>
<a-form-item label="创建人" class="smart-query-form-item">
<a-input style="width: 100px" v-model:value="queryForm.createUserId" placeholder="创建人" />
</a-form-item>
<a-form-item label="是否删除" class="smart-query-form-item">
<SmartBooleanSelect v-model:value="queryForm.deletedFlag" style="width: 70px" />
</a-form-item>
<a-form-item label="发布时间" class="smart-query-form-item">
<a-range-picker v-model:value="publishDate" :presets="defaultTimeRanges" @change="publishDateChange" style="width: 220px" />
</a-form-item>
<a-form-item label="创建时间" class="smart-query-form-item">
<a-range-picker v-model:value="createDate" :presets="defaultTimeRanges" @change="createDateChange" style="width: 220px" />
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="onReload">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-card size="small" :bordered="false">
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button type="primary" @click="addOrUpdate()" v-privilege="'oa:notice:add'">
<template #icon>
<PlusOutlined />
</template>
新建
</a-button>
</div>
<div class="smart-table-setting-block">
<TableOperator v-model="tableColumns" :tableId="TABLE_ID_CONST.BUSINESS.OA.NOTICE" :refresh="queryNoticeList" />
</div>
</a-row>
<a-table
rowKey="noticeId"
:columns="tableColumns"
:dataSource="tableData"
:scroll="{ x: 1510 }"
:pagination="false"
:loading="tableLoading"
size="small"
bordered
>
<template #bodyCell="{ column, record, text }">
<template v-if="column.dataIndex === 'title'">
<a @click="toDetail(record.noticeId)">{{ text }}</a>
</template>
<template v-else-if="column.dataIndex === 'allVisibleFlag'"> {{ text ? '全部可见' : '部分可见' }} </template>
<template v-else-if="column.dataIndex === 'publishFlag'">
{{ text ? '已发布' : '待发布' }}
</template>
<template v-else-if="column.dataIndex === 'deletedFlag'">
<a-tag v-show="text" color="error">已删除</a-tag>
<a-tag v-show="!text" color="success">未删除</a-tag>
</template>
<template v-else-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button type="link" @click="addOrUpdate(record.noticeId)" v-privilege="'oa:notice:update'">编辑</a-button>
<a-button type="link" @click="onDelete(record.noticeId)" v-privilege="'oa:notice:delete'" danger>删除</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="queryNoticeList"
@showSizeChange="queryNoticeList"
:show-total="(total) => `${total}`"
/>
</div>
</a-card>
<NoticeFormDrawer ref="noticeFormDrawer" @reloadList="queryNoticeList" />
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { useRouter } from 'vue-router';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import SmartBooleanSelect from '/@/components/framework/boolean-select/index.vue';
import { noticeApi } from '/@/api/business/oa/notice-api';
import NoticeFormDrawer from './components/notice-form-drawer.vue';
import { defaultTimeRanges } from '/@/lib/default-time-ranges';
import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
const queryFormState = {
noticeTypeId: undefined, //分类
keywords: '', //标题、作者、来源
documentNumber: '', //文号
createUserId: undefined, //创建人
deletedFlag: undefined, //删除标识
createTimeBegin: null, //创建-开始时间
createTimeEnd: null, //创建-截止时间
publishTimeBegin: null, //发布-开始时间
publishTimeEnd: null, //发布-截止时间
pageNum: 1,
pageSize: PAGE_SIZE,
};
const queryForm = reactive({ ...queryFormState });
const tableColumns = ref([
{
title: `标题`,
dataIndex: 'title',
width: 300,
ellipsis: true,
},
{
title: `文号`,
dataIndex: 'documentNumber',
width: 100,
ellipsis: true,
},
{
title: '分类',
dataIndex: 'noticeTypeName',
width: 60,
ellipsis: true,
},
{
title: `作者`,
dataIndex: 'author',
width: 80,
ellipsis: true,
},
{
title: `来源`,
dataIndex: 'source',
width: 90,
ellipsis: true,
},
{
title: '可见范围',
dataIndex: 'allVisibleFlag',
width: 90,
ellipsis: true,
},
{
title: '发布',
dataIndex: 'publishFlag',
width: 80,
},
{
title: '删除',
dataIndex: 'deletedFlag',
width: 80,
},
{
title: '发布时间',
dataIndex: 'publishTime',
width: 150,
},
{
title: '页面浏览量',
dataIndex: 'pageViewCount',
width: 90,
},
{
title: '用户浏览量',
dataIndex: 'userViewCount',
width: 90,
},
{
title: '创建人',
dataIndex: 'createUserName',
width: 80,
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 150,
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 90,
},
]);
// ------------------ 通知分类 ------------------
// 查询分类列表
const noticeTypeList = ref([]);
async function queryNoticeTypeList() {
try {
const result = await noticeApi.getAllNoticeTypeList();
noticeTypeList.value = result.data;
} catch (err) {
smartSentry.captureError(err);
}
}
// ------------------ 查询相关 ------------------
const tableData = ref([]);
const total = ref(0);
const tableLoading = ref(false);
onMounted(() => {
queryNoticeTypeList();
queryNoticeList();
});
// 查询列表
async function queryNoticeList() {
try {
tableLoading.value = true;
const result = await noticeApi.queryNotice(queryForm);
tableData.value = result.data.list;
total.value = result.data.total;
} catch (err) {
smartSentry.captureError(err);
} finally {
tableLoading.value = false;
}
}
// 点击查询
function onSearch() {
queryForm.pageNum = 1;
queryNoticeList();
}
// 点击重置
function onReload() {
Object.assign(queryForm, queryFormState);
publishDate.value = [];
createDate.value = [];
queryNoticeList();
}
// 发布日期选择
const publishDate = ref([]);
function publishDateChange(dates, dateStrings) {
queryForm.publishTimeBegin = dateStrings[0];
queryForm.publishTimeEnd = dateStrings[1];
}
// 创建日期选择
const createDate = ref([]);
function createDateChange(dates, dateStrings) {
queryForm.createTimeBegin = dateStrings[0];
queryForm.createTimeEnd = dateStrings[1];
}
// ------------------ 新建、编辑 ------------------
// 新建、编辑
const noticeFormDrawer = ref();
function addOrUpdate(noticeId) {
noticeFormDrawer.value.showModal(noticeId);
}
// ------------------ 删除 ------------------
// 删除
function onDelete(noticeId) {
Modal.confirm({
title: '提示',
content: '确认删除此数据吗?',
onOk() {
deleteNotice(noticeId);
},
});
}
// 删除API
async function deleteNotice(noticeId) {
try {
tableLoading.value = true;
await noticeApi.deleteNotice(noticeId);
message.success('删除成功');
queryNoticeList();
} catch (err) {
smartSentry.captureError(err);
} finally {
tableLoading.value = false;
}
}
// ------------------ 详情 ------------------
// 进入详情
const router = useRouter();
function toDetail(noticeId) {
router.push({
path: '/oa/notice/notice-detail',
query: { noticeId },
});
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,238 @@
<!--
* 接口加密解密
*
* @Author: 1024创新实验室-主任-卓大
* @Date: 2023-10-17 22:02:37
* @Copyright 1024创新实验室
-->
<template>
<a-alert closable>
<template v-slot:message>
<h4>接口加解密</h4>
</template>
<template v-slot:description>
<pre>
简介接口加解密分为 前端请求参数加解密 后端返回结果加解密
- 支持国密SMAES加密算法前端修改:/lib/encrypt.js ApiEncryptServiceAesImpl ApiEncryptServiceSmImpl
- 前端请看/lib/encrypt.js/lib/axios.js /api/support/api-encrypt/api-encrypt-api.js 等文件
- 后端请看@ApiEncrypt @ApiDecrypt 注解具体请看 sa-common项目中的 net.lab1024.sa.common.module.support.apiencrypt
- demo请看前端/views/support/api-encrypt 目录后端 请看sa-admin项目的net.lab1024.sa.admin.module.system.support.AdminApiEncryptController
</pre
>
</template>
</a-alert>
<br />
<a-alert
message="当前加密算法为SM4若想改为 AES前端请修改 'lib/encrypt.js'文件中的EncryptObject后端请修改 ApiEncryptService 的实现类"
type="error"
/>
<br />
<!---------- 请求参数加密 begin ----------->
<a-card title="一、请求加密 Demo">
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="姓名" class="smart-query-form-item">
<a-input v-model:value="requestEncryptForm.name" placeholder="姓名" />
</a-form-item>
<a-form-item label="年龄" class="smart-query-form-item">
<a-input-number v-model:value="requestEncryptForm.age" placeholder="年龄" />
</a-form-item>
<a-form-item class="smart-query-form-item">
<a-button type="primary" @click="testRequestEncrypt"> 测试请求加密</a-button>
</a-form-item>
</a-row>
<a-row class="smart-query-form-row">
<div v-if="requestEncryptFormStr">请求参数{{ requestEncryptFormStr }}</div>
</a-row>
<a-row class="smart-query-form-row">
<div v-if="requestEncryptFormEncryptStr">请求参数加密{{ requestEncryptFormEncryptStr }}</div>
</a-row>
<a-row class="smart-query-form-row">
<div v-if="requestEncryptResponse">返回结果不加密{{ requestEncryptResponse }}</div>
</a-row>
</a-form>
</a-card>
<!---------- 请求参数加密 end ----------->
<br />
<!---------- 返回结果解密 begin ----------->
<a-card title="二、返回加密 Demo">
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="姓名" class="smart-query-form-item">
<a-input v-model:value="responseEncryptForm.name" placeholder="姓名" />
</a-form-item>
<a-form-item label="年龄" class="smart-query-form-item">
<a-input-number v-model:value="responseEncryptForm.age" placeholder="年龄" />
</a-form-item>
<a-form-item class="smart-query-form-item">
<a-button type="primary" @click="testResponseEncrypt"> 测试返回加密 </a-button>
</a-form-item>
</a-row>
<a-row class="smart-query-form-row">
<div v-if="responseEncryptFormStr">请求参数 {{ responseEncryptFormStr }}</div>
</a-row>
<a-row class="smart-query-form-row">
<div v-if="responseEncryptStr">返回结果{{ responseEncryptStr }}</div>
</a-row>
<a-row class="smart-query-form-row">
<div v-if="responseStr">返回结果 解密{{ responseStr }}</div>
</a-row>
</a-form>
</a-card>
<!---------- 返回结果解密 end ----------->
<br />
<!---------- 请求和返回都加密 begin ----------->
<a-card title="三、请求和返回都加密 Demo">
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="姓名" class="smart-query-form-item">
<a-input v-model:value="form.name" placeholder="姓名" />
</a-form-item>
<a-form-item label="年龄" class="smart-query-form-item">
<a-input-number v-model:value="form.age" placeholder="年龄" />
</a-form-item>
<a-form-item class="smart-query-form-item">
<a-button type="primary" @click="testBoth"> 测试请求和返回都加密 </a-button>
</a-form-item>
</a-row>
<a-row class="smart-query-form-row">
<div v-if="formStr">请求参数 {{ formStr }}</div>
</a-row>
<a-row class="smart-query-form-row">
<div v-if="formEncryptStr">请求参数加密 {{ formEncryptStr }}</div>
</a-row>
<a-row class="smart-query-form-row">
<div v-if="responseEncrypt">返回结果{{ responseEncrypt }}</div>
</a-row>
<a-row class="smart-query-form-row">
<div v-if="responseDecryptStr">返回结果 解密{{ responseDecryptStr }}</div>
</a-row>
</a-form>
</a-card>
<!---------- 返回结果解密 end ----------->
<br />
<!---------- 测试数组 begin ----------->
<a-card title="四、测试数组 Demo">
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item class="smart-query-form-item">
<a-button type="primary" @click="testArray"> 测试数组加解密 </a-button>
</a-form-item>
</a-row>
<a-row class="smart-query-form-row">
<div v-if="arrayFormStr">请求参数 {{ arrayFormStr }}</div>
</a-row>
<a-row class="smart-query-form-row">
<div v-if="arrayFormEncryptStr">请求参数加密 {{ arrayFormEncryptStr }}</div>
</a-row>
<a-row class="smart-query-form-row">
<div v-if="arrayFormResponseEncrypt">返回结果{{ arrayFormResponseEncrypt }}</div>
</a-row>
<a-row class="smart-query-form-row">
<div v-if="arrayFormResponseDecryptStr">返回结果 解密{{ arrayFormResponseDecryptStr }}</div>
</a-row>
</a-form>
</a-card>
<!---------- 返回结果解密 end ----------->
</template>
<script setup>
import { reactive, ref } from 'vue';
import { encryptApi } from '/@/api/support/api-encrypt-api';
import { encryptData } from '/@/lib/encrypt';
// ---------------------------- 第一种:请求参数加密 ----------------------------
//请求参数加密
const requestEncryptForm = reactive({
age: 100, // 年龄
name: '卓大', //姓名
});
// 参数字符串
const requestEncryptFormStr = ref('');
// 参数字符串 加密
const requestEncryptFormEncryptStr = ref('');
// 返回结果
const requestEncryptResponse = ref('');
async function testRequestEncrypt() {
// 参数加密
requestEncryptFormStr.value = JSON.stringify(requestEncryptForm);
requestEncryptFormEncryptStr.value = encryptData(requestEncryptForm);
// 发送请求
const result = await encryptApi.testRequestEncrypt(requestEncryptForm);
requestEncryptResponse.value = JSON.stringify(result.data);
}
// ---------------------------- 第二种:返回结果解密 ----------------------------
const responseEncryptForm = reactive({
age: 100, // 年龄
name: '卓大', //姓名
});
const responseEncryptFormStr = ref('');
const responseEncryptStr = ref('');
const responseStr = ref('');
async function testResponseEncrypt() {
responseEncryptFormStr.value = JSON.stringify(responseEncryptForm);
const result = await encryptApi.testResponseEncrypt(responseEncryptForm);
responseEncryptStr.value = result.encryptData;
responseStr.value = JSON.stringify(result.data);
}
// ---------------------------- 第三种:请求加密、返回解密 ----------------------------
const form = reactive({
age: 100, // 年龄
name: '卓大', //姓名
});
const formStr = ref('');
const formEncryptStr = ref('');
const responseEncrypt = ref('');
const responseDecryptStr = ref('');
async function testBoth() {
formStr.value = JSON.stringify(form);
formEncryptStr.value = encryptData(form);
const result = await encryptApi.testDecryptAndEncrypt(form);
responseEncrypt.value = result.encryptData;
responseDecryptStr.value = JSON.stringify(result.data);
}
// ---------------------------- 第四种:测试数组 ----------------------------
const arrayForm = reactive([
{
age: 1, // 年龄
name: '卓1', //姓名
},
{
age: 2, // 年龄
name: '卓2', //姓名
},
{
age: 3, // 年龄
name: '卓3', //姓名
},
]);
const arrayFormStr = ref('');
const arrayFormEncryptStr = ref('');
const arrayFormResponseEncrypt = ref('');
const arrayFormResponseDecryptStr = ref('');
async function testArray() {
arrayFormStr.value = JSON.stringify(arrayForm);
arrayFormEncryptStr.value = encryptData(arrayForm);
const result = await encryptApi.testArray(arrayForm);
arrayFormResponseEncrypt.value = result.encryptData;
arrayFormResponseDecryptStr.value = JSON.stringify(result.data);
}
</script>

View File

@@ -0,0 +1,112 @@
<!--
* 缓存
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-06-08 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-card size="small" :bordered="false" :hoverable="true">
<a-alert>
<template v-slot:message>
<h4>缓存 介绍</h4>
</template>
<template v-slot:description>
<pre>
简介SmartAdmin使用的是SpringCache进行管理缓存SpringCache有多种实现方式本项目默认采用的是caffeine
Caffeine
- Caffeine是一个进程内部缓存框架使用了Java 8最新的[StampedLock]乐观锁技术极大提高缓存并发吞吐量一个高性能的 Java 缓存库被称为最快缓存
其他
· 对于分布式集群等应用实现方式可以改为 RedisCouchBase等
</pre
>
</template>
</a-alert>
<a-table size="small" bordered class="smart-margin-top10" :dataSource="tableData" :columns="columns" rowKey="tag" :pagination="false" >
<template #bodyCell="{ record, column }">
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button @click="remove(record.key)" v-privilege="'support:cache:delete'" type="link">清除</a-button>
<a-button @click="getAllKeys(record.key)" v-privilege="'support:cache:keys'" type="link">获取所有key</a-button>
</div>
</template>
</template>
</a-table>
</a-card>
</template>
<script setup>
import { message } from 'ant-design-vue';
import { onMounted, reactive, ref, h } from 'vue';
import { cacheApi } from '/@/api/support/cache-api';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { Modal } from 'ant-design-vue';
import _ from 'lodash';
import { smartSentry } from '/@/lib/smart-sentry';
//------------------------ 删除 ---------------------
async function remove(key) {
try {
await cacheApi.remove(key);
message.success('删除成功');
ajaxQuery();
} catch (e) {
smartSentry.captureError(e);
}
}
//------------------------ 获取所有key ---------------------
async function getAllKeys(cacheName) {
SmartLoading.show();
try {
let res = await cacheApi.getKeys(cacheName);
SmartLoading.hide();
Modal.info({
title: '所有Key:' + cacheName,
content: h('div', {}, [h('p', _.join(res.data, ' , '))]),
onOk() {
ajaxQuery();
},
});
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
//------------------------ 表格渲染 ---------------------
const columns = reactive([
{
title: 'Key',
dataIndex: 'key',
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 160,
},
]);
const tableLoading = ref(false);
const tableData = ref([]);
async function ajaxQuery() {
try {
tableLoading.value = true;
let res = await cacheApi.getAllCacheNames();
tableData.value = res.data.map((e) => Object.assign({}, { key: e }));
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
onMounted(ajaxQuery);
</script>

View File

@@ -0,0 +1,138 @@
<!--
* 系统更新日志
*
* @Author: 卓大
* @Date: 2022-09-26 14:53:50
* @Copyright 1024创新实验室
-->
<template>
<a-modal
:title="form.changeLogId ? '编辑' : '添加'"
width="600px"
:closable="true"
:open="visibleFlag"
@close="onClose"
:onCancel="onClose"
:maskClosable="false"
:destroyOnClose="true"
>
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }">
<a-form-item label="版本" name="version">
<a-input style="width: 100%" v-model:value="form.version" placeholder="版本" />
</a-form-item>
<a-form-item label="更新类型" name="type">
<SmartEnumSelect width="100%" v-model:value="form.type" enumName="CHANGE_LOG_TYPE_ENUM" placeholder="更新类型" />
</a-form-item>
<a-form-item label="发布人" name="publishAuthor">
<a-input style="width: 100%" v-model:value="form.publishAuthor" placeholder="发布人" />
</a-form-item>
<a-form-item label="发布日期" name="publicDate">
<a-date-picker valueFormat="YYYY-MM-DD" v-model:value="form.publicDate" style="width: 100%" placeholder="发布日期" />
</a-form-item>
<a-form-item label="跳转链接" name="link">
<a-input style="width: 100%" v-model:value="form.link" placeholder="跳转链接" />
</a-form-item>
<a-form-item label="更新内容" name="content">
<a-textarea style="width: 100%" :rows="15" v-model:value="form.content" placeholder="更新内容" />
</a-form-item>
</a-form>
<template #footer>
<a-space>
<a-button @click="onClose">取消</a-button>
<a-button type="primary" @click="onSubmit">保存</a-button>
</a-space>
</template>
</a-modal>
</template>
<script setup>
import { reactive, ref, nextTick } from 'vue';
import _ from 'lodash';
import { message } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { changeLogApi } from '/@/api/support/change-log-api';
import { smartSentry } from '/@/lib/smart-sentry';
import SmartEnumSelect from '/@/components/framework/smart-enum-select/index.vue';
// ------------------------ 事件 ------------------------
const emits = defineEmits(['reloadList']);
// ------------------------ 显示与隐藏 ------------------------
// 是否显示
const visibleFlag = ref(false);
function show(rowData) {
Object.assign(form, formDefault);
if (rowData && !_.isEmpty(rowData)) {
Object.assign(form, rowData);
}
visibleFlag.value = true;
nextTick(() => {
formRef.value.clearValidate();
});
}
function onClose() {
Object.assign(form, formDefault);
visibleFlag.value = false;
}
// ------------------------ 表单 ------------------------
// 组件ref
const formRef = ref();
const formDefault = {
changeLogId: undefined,
version: undefined, //版本
type: undefined, //更新类型:[1:特大版本功能更新;2:功能更新;3:bug修复]
publishAuthor: undefined, //发布人
publicDate: undefined, //发布日期
content: undefined, //更新内容
link: undefined, //跳转链接
};
let form = reactive({ ...formDefault });
const rules = {
version: [{ required: true, message: '版本 必填' }],
type: [{ required: true, message: '更新类型:[1:特大版本功能更新;2:功能更新;3:bug修复] 必填' }],
publishAuthor: [{ required: true, message: '发布人 必填' }],
publicDate: [{ required: true, message: '发布日期 必填' }],
content: [{ required: true, message: '更新内容 必填' }],
};
// 点击确定,验证表单
async function onSubmit() {
try {
await formRef.value.validateFields();
save();
} catch (err) {
message.error('参数验证错误,请仔细填写表单数据!');
}
}
// 新建、编辑API
async function save() {
SmartLoading.show();
try {
if (form.changeLogId) {
await changeLogApi.update(form);
} else {
await changeLogApi.add(form);
}
message.success('操作成功');
emits('reloadList');
onClose();
} catch (err) {
smartSentry.captureError(err);
} finally {
SmartLoading.hide();
}
}
defineExpose({
show,
});
</script>

View File

@@ -0,0 +1,322 @@
<!--
* 系统更新日志
*
* @Author: 卓大
* @Date: 2022-09-26 14:53:50
* @Copyright 1024创新实验室
-->
<template>
<!---------- 查询表单form begin ----------->
<a-form class="smart-query-form" v-privilege="'support:changeLog:query'">
<a-row class="smart-query-form-row">
<a-form-item label="更新类型" class="smart-query-form-item">
<SmartEnumSelect width="200px" v-model:value="queryForm.type" enumName="CHANGE_LOG_TYPE_ENUM" placeholder="更新类型" />
</a-form-item>
<a-form-item label="关键字" class="smart-query-form-item">
<a-input style="width: 200px" v-model:value="queryForm.keyword" placeholder="关键字" />
</a-form-item>
<a-form-item label="发布日期" class="smart-query-form-item">
<a-range-picker v-model:value="queryForm.publicDate" :presets="defaultTimeRanges" style="width: 240px" @change="onChangePublicDate" />
</a-form-item>
<a-form-item label="创建时间" class="smart-query-form-item">
<a-date-picker valueFormat="YYYY-MM-DD" v-model:value="queryForm.createTime" style="width: 150px" />
</a-form-item>
<a-form-item class="smart-query-form-item">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery" class="smart-margin-left10">
<template #icon>
<SearchOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<!---------- 查询表单form end ----------->
<a-card size="small" :bordered="false" :hoverable="true">
<!---------- 表格操作行 begin ----------->
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button @click="showForm" type="primary" v-privilege="'support:changeLog:add'">
<template #icon>
<PlusOutlined />
</template>
新建
</a-button>
<a-button @click="confirmBatchDelete" danger :disabled="selectedRowKeyList.length === 0" v-privilege="'support:changeLog:batchDelete'">
<template #icon>
<DeleteOutlined />
</template>
批量删除
</a-button>
</div>
<div class="smart-table-setting-block">
<TableOperator v-model="columns" :tableId="null" :refresh="queryData" />
</div>
</a-row>
<!---------- 表格操作行 end ----------->
<!---------- 表格 begin ----------->
<a-table
size="small"
:dataSource="tableData"
:columns="columns"
rowKey="changeLogId"
bordered
:pagination="false"
:row-selection="{ selectedRowKeys: selectedRowKeyList, onChange: onSelectChange }"
>
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'version'">
<a-button @click="showModal(record)" type="link">{{ text }}</a-button>
</template>
<template v-if="column.dataIndex === 'type'">
<a-tag color="success">
<template #icon>
<check-circle-outlined />
</template>
{{ $smartEnumPlugin.getDescByValue('CHANGE_LOG_TYPE_ENUM', text) }}
</a-tag>
</template>
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button @click="showForm(record)" type="link" v-privilege="'support:changeLog:update'">编辑</a-button>
<a-button @click="onDelete(record)" danger type="link" v-privilege="'support:changeLog:delete'">删除</a-button>
</div>
</template>
</template>
</a-table>
<!---------- 表格 end ----------->
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="queryData"
@showSizeChange="queryData"
:show-total="(total) => `${total}`"
/>
</div>
<ChangeLogForm ref="formRef" @reloadList="queryData" />
<ChangeLogModal ref="modalRef" />
</a-card>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { changeLogApi } from '/@/api/support/change-log-api';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue';
import DictSelect from '/@/components/support/dict-select/index.vue';
import SmartEnumSelect from '/@/components/framework/smart-enum-select/index.vue';
import { defaultTimeRanges } from '/@/lib/default-time-ranges';
import ChangeLogModal from './change-log-modal.vue';
import ChangeLogForm from './change-log-form.vue';
// ---------------------------- 表格列 ----------------------------
const columns = ref([
{
title: '版本',
dataIndex: 'version',
ellipsis: true,
},
{
title: '更新类型',
dataIndex: 'type',
ellipsis: true,
},
{
title: '发布人',
dataIndex: 'publishAuthor',
ellipsis: true,
},
{
title: '发布日期',
dataIndex: 'publicDate',
ellipsis: true,
},
{
title: '更新内容',
dataIndex: 'content',
ellipsis: true,
},
{
title: '跳转链接',
dataIndex: 'link',
ellipsis: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
ellipsis: true,
},
{
title: '更新时间',
dataIndex: 'updateTime',
ellipsis: true,
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 90,
},
]);
// ---------------------------- 查询数据表单和方法 ----------------------------
const queryFormState = {
type: undefined, //更新类型:[1:特大版本功能更新;2:功能更新;3:bug修复]
keyword: undefined, //关键字
publicDate: [], //发布日期
publicDateBegin: undefined, //发布日期 开始
publicDateEnd: undefined, //发布日期 结束
createTime: undefined, //创建时间
link: undefined, //跳转链接
pageNum: 1,
pageSize: 10,
};
// 查询表单form
const queryForm = reactive({ ...queryFormState });
// 表格加载loading
const tableLoading = ref(false);
// 表格数据
const tableData = ref([]);
// 总数
const total = ref(0);
// 重置查询条件
function resetQuery() {
let pageSize = queryForm.pageSize;
Object.assign(queryForm, queryFormState);
queryForm.pageSize = pageSize;
queryData();
}
// 搜索
function onSearch() {
queryForm.pageNum = 1;
queryData();
}
// 查询数据
async function queryData() {
tableLoading.value = true;
try {
let queryResult = await changeLogApi.queryPage(queryForm);
tableData.value = queryResult.data.list;
total.value = queryResult.data.total;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
function onChangePublicDate(dates, dateStrings) {
queryForm.publicDateBegin = dateStrings[0];
queryForm.publicDateEnd = dateStrings[1];
}
onMounted(queryData);
// ---------------------------- 查看 ----------------------------
const modalRef = ref();
function showModal(data) {
modalRef.value.show(data);
}
// ---------------------------- 添加/修改 ----------------------------
const formRef = ref();
function showForm(data) {
formRef.value.show(data);
}
// ---------------------------- 单个删除 ----------------------------
//确认删除
function onDelete(data) {
Modal.confirm({
title: '提示',
content: '确定要删除选吗?',
okText: '删除',
okType: 'danger',
onOk() {
requestDelete(data);
},
cancelText: '取消',
onCancel() {},
});
}
//请求删除
async function requestDelete(data) {
SmartLoading.show();
try {
await changeLogApi.delete(data.changeLogId);
message.success('删除成功');
queryData();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
// ---------------------------- 批量删除 ----------------------------
// 选择表格行
const selectedRowKeyList = ref([]);
function onSelectChange(selectedRowKeys) {
selectedRowKeyList.value = selectedRowKeys;
}
// 批量删除
function confirmBatchDelete() {
Modal.confirm({
title: '提示',
content: '确定要批量删除这些数据吗?',
okText: '删除',
okType: 'danger',
onOk() {
requestBatchDelete();
},
cancelText: '取消',
onCancel() {},
});
}
//请求批量删除
async function requestBatchDelete() {
try {
SmartLoading.show();
await changeLogApi.batchDelete(selectedRowKeyList.value);
message.success('删除成功');
queryData();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
</script>

View File

@@ -0,0 +1,44 @@
<!--
* 系统更新日志 查看
*
* @Author: 卓大
* @Date: 2022-09-26 14:53:50
* @Copyright 1024创新实验室
-->
<template>
<a-modal title="更新日志" width="700px" :open="visibleFlag" @cancel="onClose">
<div>
<pre>{{ content }}</pre>
<div v-if="link">
链接<a :href="link" target="_blank">{{ link }}</a>
</div>
</div>
<template #footer>
<a-space>
<a-button type="primary" @click="onClose">关闭</a-button>
</a-space>
</template>
</a-modal>
</template>
<script setup>
import { ref } from 'vue';
const visibleFlag = ref(false);
const content = ref('');
const link = ref('');
function show(changeLog) {
content.value = changeLog.content;
link.value = changeLog.link;
visibleFlag.value = true;
}
function onClose() {
visibleFlag.value = false;
}
defineExpose({
show,
});
</script>

View File

@@ -0,0 +1,186 @@
<!--
* 代码生成 列表
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-06-08 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="表名" class="smart-query-form-item">
<a-input style="width: 300px" v-model:value="queryForm.tableNameKeywords" placeholder="请输入表名关键字" />
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<SearchOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-card size="small" :bordered="false" :hoverable="true">
<a-row justify="end">
<TableOperator class="smart-margin-bottom5" v-model="columns" :tableId="TABLE_ID_CONST.SUPPORT.CONFIG" :refresh="ajaxQuery" />
</a-row>
<a-table size="small" :loading="tableLoading" bordered :dataSource="tableData" :columns="columns" rowKey="configId" :pagination="false">
<template #bodyCell="{ record, index, column }">
<template v-if="column.dataIndex === 'seq'">
{{ index + 1 }}
</template>
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button @click="showConfig(record)" type="link">代码配置</a-button>
<a-button @click="showPreview(record)" type="link">代码预览</a-button>
<a-button @click="download(record)" type="link">下载代码</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `${total}`"
/>
</div>
</a-card>
<CodeGeneratorTableConfigForm ref="codeGeneratorTableConfigFormRef" @reloadList="ajaxQuery" />
<CodeGeneratorPreviewModal ref="codeGeneratorPreviewModalRef" />
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { codeGeneratorApi } from '/@/api/support/code-generator-api';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { smartSentry } from '/@/lib/smart-sentry';
import CodeGeneratorTableConfigForm from './components/form/code-generator-table-config-form.vue';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
import CodeGeneratorPreviewModal from './components/preview/code-generator-preview-modal.vue';
const columns = ref([
{
title: '序号',
width: 50,
dataIndex: 'seq',
},
{
title: '表名',
dataIndex: 'tableName',
},
{
title: '备注',
dataIndex: 'tableComment',
ellipsis: true,
},
{
title: '代码配置',
dataIndex: 'configTime',
width: 150,
},
{
title: '表创建时间',
dataIndex: 'createTime',
width: 150,
},
{
title: '表修改时间',
dataIndex: 'updateTime',
width: 150,
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 210,
},
]);
// ---------------- 查询数据 -----------------------
const queryFormState = {
configKey: '',
pageNum: 1,
pageSize: 10,
tableNameKeywords: undefined,
};
const queryForm = reactive({ ...queryFormState });
const tableLoading = ref(false);
const tableData = ref([]);
const total = ref(0);
function resetQuery() {
Object.assign(queryForm, queryFormState);
ajaxQuery();
}
function onSearch() {
queryForm.pageNum = 1;
ajaxQuery();
}
async function ajaxQuery() {
try {
tableLoading.value = true;
let responseModel = await codeGeneratorApi.queryTableList(queryForm);
const list = responseModel.data.list;
total.value = responseModel.data.total;
tableData.value = list;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
// ------------------------- 表单操作 弹窗 ------------------------------
const codeGeneratorTableConfigFormRef = ref();
function showConfig(rowData) {
codeGeneratorTableConfigFormRef.value.showModal(rowData);
}
// ------------------------- 预览 弹窗 ------------------------------
const codeGeneratorPreviewModalRef = ref();
function showPreview(rowData) {
codeGeneratorPreviewModalRef.value.showModal(rowData);
}
// ------------------------- 下载 ------------------------------
function download(rowData) {
codeGeneratorApi.downloadCode(rowData.tableName);
}
onMounted(ajaxQuery);
</script>

View File

@@ -0,0 +1,173 @@
import { convertUpperCamel } from '/@/utils/str-util';
// -------------------------------- java 类型 --------------------------------
export const JavaTypeMap = new Map();
JavaTypeMap.set('bit', 'Boolean');
JavaTypeMap.set('int', 'Integer');
JavaTypeMap.set('tinyint', 'Integer');
JavaTypeMap.set('smallint', 'Integer');
JavaTypeMap.set('integer', 'Integer');
JavaTypeMap.set('year', 'Integer');
JavaTypeMap.set('bigint', 'Long');
JavaTypeMap.set('float', 'BigDecimal');
JavaTypeMap.set('double', 'BigDecimal');
JavaTypeMap.set('decimal', 'BigDecimal');
JavaTypeMap.set('char', 'String');
JavaTypeMap.set('varchar', 'String');
JavaTypeMap.set('tinytext', 'String');
JavaTypeMap.set('text', 'String');
JavaTypeMap.set('longtext', 'String');
JavaTypeMap.set('blob', 'String');
JavaTypeMap.set('date', 'LocalDate');
JavaTypeMap.set('datetime', 'LocalDateTime');
export const JavaTypeList = [
'Boolean', //
'Integer', //
'Long', //
'Double', //
'String', //
'BigDecimal', //
'LocalDate', //
'LocalDateTime', //
];
export function getJavaType(dataType) {
return JavaTypeMap.get(dataType);
}
// -------------------------------- js 类型 --------------------------------
export const JsTypeMap = new Map();
JsTypeMap.set('bit', 'Boolean');
JsTypeMap.set('int', 'Number');
JsTypeMap.set('tinyint', 'Number');
JsTypeMap.set('smallint', 'Number');
JsTypeMap.set('integer', 'Number');
JsTypeMap.set('year', 'Number');
JsTypeMap.set('bigint', 'Number');
JsTypeMap.set('float', 'Number');
JsTypeMap.set('double', 'Number');
JsTypeMap.set('decimal', 'Number');
JsTypeMap.set('char', 'String');
JsTypeMap.set('varchar', 'String');
JsTypeMap.set('tinytext', 'String');
JsTypeMap.set('text', 'String');
JsTypeMap.set('longtext', 'String');
JsTypeMap.set('blob', 'String');
JsTypeMap.set('date', 'Date');
JsTypeMap.set('datetime', 'Date');
export const JsTypeList = [
'Boolean', //
'Number', //
'String', //
'Date', //
];
export function getJsType(dataType) {
return JsTypeMap.get(dataType);
}
// -------------------------------- 前端组件 --------------------------------
export const FrontComponentMap = new Map();
FrontComponentMap.set('bit', 'BooleanSelect');
FrontComponentMap.set('int', 'InputNumber');
FrontComponentMap.set('tinyint', 'InputNumber');
FrontComponentMap.set('smallint', 'InputNumber');
FrontComponentMap.set('integer', 'InputNumber');
FrontComponentMap.set('year', 'Date');
FrontComponentMap.set('bigint', 'InputNumber');
FrontComponentMap.set('float', 'InputNumber');
FrontComponentMap.set('double', 'InputNumber');
FrontComponentMap.set('decimal', 'InputNumber');
FrontComponentMap.set('char', 'Input');
FrontComponentMap.set('varchar', 'Input');
FrontComponentMap.set('tinytext', 'Input');
FrontComponentMap.set('text', 'Textarea');
FrontComponentMap.set('longtext', 'Textarea');
FrontComponentMap.set('blob', 'FileUpload');
FrontComponentMap.set('date', 'Date');
FrontComponentMap.set('datetime', 'DateTime');
export function getFrontComponent(dataType) {
return FrontComponentMap.get(dataType);
}
// -------------------------------- 前端文件 --------------------------------
export const LANGUAGE_LIST = [
'js', //
'ts', //
'java', //
];
export const JS_FILE_LIST = [
'js/list.vue', //
'js/form.vue', //
'js/api.js', //
'js/const.js', //
];
export const TS_FILE_LIST = [
'ts/list.vue', //
'ts/form.vue', //
'ts/api.js', //
'ts/const.js', //
];
// -------------------------------- 后端文件 --------------------------------
export const JAVA_DOMAIN_FILE_LIST = [
'Entity.java', //
'AddForm.java', //
'UpdateForm.java', //
'QueryForm.java', //
'VO.java', //
];
export const JAVA_FILE_LIST = [
'Controller.java', //
'Service.java', //
'Manager.java', //
'Dao.java', //
'Mapper.xml', //
...JAVA_DOMAIN_FILE_LIST,
'Menu.sql', //
];
// -------------------------------- 枚举enum --------------------------------
export function convertJavaEnumName(moduleName, columnName) {
return moduleName + convertUpperCamel(columnName) + 'Enum';
}
/**
* 检测是否有枚举
*/
export function checkExistEnum(comment) {
if (!comment) {
return false;
}
// 检测是否存在 [ ] 或者 【 】
let leftBracketIndex = comment.indexOf('[');
if (leftBracketIndex === -1) {
leftBracketIndex = comment.indexOf('【');
}
let rightBracketIndex = comment.indexOf(']');
if (rightBracketIndex === -1) {
leftBracketIndex = comment.indexOf('】');
}
if (leftBracketIndex === -1 || rightBracketIndex === -1) {
return false;
}
if (comment.indexOf(':') === -1) {
return false;
}
return true;
}

View File

@@ -0,0 +1,295 @@
<!--
* 代码生成 配置信息
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-22 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-alert
:closable="true"
message="默认数据库表名前缀为t_ 如果想修改默认前缀,请修改前端 code-generator-table-config-form-basic.vue 文件的 tablePrefix 变量"
type="success"
show-icon
>
<template #icon><smile-outlined /></template>
</a-alert>
<a-row type="flex" class="smart-margin-top10">
<a-col flex="350px">
<a-form ref="formRef" :model="formData" :rules="formRules" :label-col="{ span: 5 }" :wrapper-col="{ span: 16 }">
<a-form-item label="表"> {{ tableInfo.tableName }} </a-form-item>
<a-form-item label="表备注"> {{ tableInfo.tableComment }} </a-form-item>
<a-form-item label="表前缀" name="tablePrefix">
<a-input v-model:value="tablePrefix" @change="onChangeTablePrefix" placeholder="请输入 表前缀 " />
</a-form-item>
<a-form-item label="单词命名" name="moduleName">
<a-input v-model:value="formData.moduleName" placeholder="请输入 单词命名 " />
</a-form-item>
<a-form-item label="Java包名" name="javaPackageName">
<a-input v-model:value="formData.javaPackageName" placeholder="请输入 Java包名 " />
</a-form-item>
<a-form-item label="注释信息" name="description">
<a-input v-model:value="formData.description" placeholder="请输入 注释信息 " />
</a-form-item>
<a-form-item label="前端作者" name="frontAuthor">
<a-input v-model:value="formData.frontAuthor" placeholder="请输入 前端作者" />
</a-form-item>
<a-form-item label="前端时间" name="frontDate">
<a-date-picker
style="width: 100%"
show-time
format="YYYY-MM-DD HH:mm:ss"
v-model:value="formData.frontDate"
placeholder="请输入 前端时间"
/>
</a-form-item>
<a-form-item label="后端作者" name="backendAuthor">
<a-input v-model:value="formData.backendAuthor" placeholder="请输入 后端作者" />
</a-form-item>
<a-form-item label="后端时间" name="backendDate">
<a-date-picker
style="width: 100%"
show-time
format="YYYY-MM-DD HH:mm:ss"
v-model:value="formData.backendDate"
placeholder="请输入 后端时间"
/>
</a-form-item>
<a-form-item label="版权信息" name="copyright">
<a-input v-model:value="formData.copyright" placeholder="请输入 版权信息" />
</a-form-item>
</a-form>
</a-col>
<a-col flex="auto" style="height: 100vh; overflow-y: scroll">
<a-tabs v-model:activeKey="activeKey" size="small">
<a-tab-pane key="1" tab="前端文件命名">
<div class="preview-title">前端文件名</div>
<div class="preview-block">
<div v-for="item in frontNameList" :key="item">
{{ item }}
</div>
</div>
<div class="preview-title">前端Vue文件注释</div>
<div>
<pre class="preview-block">
&lt;!--
* {{ formData.description }}
*
* @Author: {{ formData.frontAuthor }}
* @Date: {{ formData.frontDate }}
* @Copyright {{ formData.copyright }}
--&gt;</pre
>
</div>
<div class="preview-title">前端Js文件注释</div>
<div>
<pre class="preview-block">
/*
* {{ formData.description }}
*
* @Author: {{ formData.frontAuthor }}
* @Date: {{ formData.frontDate }}
* @Copyright {{ formData.copyright }}
*/
</pre
>
</div>
</a-tab-pane>
<a-tab-pane key="2" tab="后端文件命名">
<div>
<div class="preview-title">后端-四层代码</div>
<div class="preview-block">
<div v-for="item in backendMvcNameList" :key="item">
{{ item }}
</div>
</div>
<div class="preview-title">JavaBean代码</div>
<div class="preview-block">
<div v-for="item in backendJavaBeanNameList" :key="item">
{{ item }}
</div>
</div>
<div class="preview-title">常量代码</div>
<div class="preview-block">
<div v-for="item in backendConstNameList" :key="item">
{{ item }}
</div>
</div>
<div class="preview-title">注释</div>
<pre class="preview-block">
/**
* {{ formData.description }}
*
* @Author: {{ formData.backendAuthor }}
* @Date: {{ formData.backendDate }}
* @Copyright {{ formData.copyright }}
*/
</pre
>
</div>
</a-tab-pane>
</a-tabs>
</a-col>
</a-row>
</template>
<script setup>
import { message } from 'ant-design-vue';
import dayjs from 'dayjs';
import _ from 'lodash';
import { computed, inject, reactive, ref } from 'vue';
import { convertLowerHyphen, convertUpperCamel } from '/@/utils/str-util';
const tableInfo = inject('tableInfo');
const activeKey = ref('1');
// ------------- 表单 -------------
const formRef = ref();
const defaultFormData = {
moduleName: undefined, // 单词命名
javaPackageName: undefined, // java包名
description: undefined, // 注释信息
frontAuthor: undefined, // 前端作者
frontDate: undefined, // 前端时间
backendAuthor: undefined, // 后端作者
backendDate: undefined, // 后端时间
copyright: undefined, //版权
};
const tablePrefix = ref('t_');
const formData = reactive({ ...defaultFormData });
const formRules = {
moduleName: [{ required: true, message: '请输入 单词命名' }],
javaPackageName: [{ required: true, message: '请输入 java包名' }],
frontAuthor: [{ required: true, message: '请输入 前端作者' }],
frontDate: [{ required: true, message: '请输入 前端时间' }],
backendAuthor: [{ required: true, message: '请输入 后端作者' }],
backendDate: [{ required: true, message: '请输入 后端时间' }],
copyright: [{ required: true, message: '请输入 版权' }],
};
//初始化设置数据
function setData(config) {
//基础信息
let basic = config.basic;
//命名
let removePrefixTableName = tableInfo.tableName;
if (_.startsWith(tableInfo.tableName, tablePrefix.value)) {
removePrefixTableName = _.trim(removePrefixTableName, tablePrefix.value);
}
formData.moduleName = basic && basic.moduleName ? basic.moduleName : removePrefixTableName;
formData.moduleName = convertUpperCamel(formData.moduleName);
//注释
formData.description = basic && basic.description ? basic.description : tableInfo.tableComment;
//时间
formData.frontDate = basic && basic.frontDate ? basic.frontDate : tableInfo.createTime;
formData.frontDate = dayjs(formData.frontDate);
formData.backendDate = basic && basic.backendDate ? basic.backendDate : tableInfo.createTime;
formData.backendDate = dayjs(formData.backendDate);
//其他字段
formData.frontAuthor = basic && basic.frontAuthor ? basic.frontAuthor : null;
formData.javaPackageName = basic && basic.javaPackageName ? basic.javaPackageName : null;
formData.backendAuthor = basic && basic.backendAuthor ? basic.backendAuthor : null;
formData.copyright = basic && basic.copyright ? basic.copyright : null;
}
function onChangeTablePrefix(e) {
let removePrefixTableName = tableInfo.tableName;
if (_.startsWith(tableInfo.tableName, tablePrefix.value)) {
removePrefixTableName = _.trim(removePrefixTableName, tablePrefix.value);
}
formData.moduleName = convertUpperCamel(removePrefixTableName);
}
// 获取表单数据
const timeFormat = 'YYYY-MM-DD HH:mm:ss';
function getBasicForm() {
return Object.assign({}, formData, {
frontDate: dayjs(formData.frontDate).format(timeFormat),
backendDate: dayjs(formData.backendDate).format(timeFormat),
});
}
// 校验表单
function validateForm() {
return new Promise((resolve, reject) => {
formRef.value
.validate()
.then(() => {
resolve(true);
})
.catch((error) => {
message.error(' 请检查【1.基础命名】表单,有参数验证错误');
reject(error);
});
});
}
defineExpose({
setData,
getBasicForm,
validateForm,
});
// ------------- 预览 -------------
const frontName = computed(() => convertLowerHyphen(formData.moduleName));
const frontNameList = computed(() => {
return [
'请求:' + frontName.value + '-api.js', //
'常量:' + frontName.value + '-const.js', //
'列表:' + frontName.value + '-list.vue', //
'表单:' + frontName.value + '-form-modal.vue', //
'详情:' + frontName.value + '-detail.vue', //
];
});
const backendMvcNameList = computed(() => {
return [
'控制层:' + formData.moduleName + 'Controller.java', //
'业务层:' + formData.moduleName + 'Service.java', //
'中间层:' + formData.moduleName + 'Manager.java', //
'持久层:' + formData.moduleName + 'Dao.java', //
'SQL层 ' + formData.moduleName + 'Mapper.xml', //
];
});
const backendJavaBeanNameList = computed(() => {
return [
'实体类:' + formData.moduleName + 'Entity.java', //
'表现类:' + formData.moduleName + 'VO.java', //
'新建类:' + formData.moduleName + 'AddForm.java', //
'更新类:' + formData.moduleName + 'UpdateForm.java', //
'查询类:' + formData.moduleName + 'QueryForm.java', //
];
});
const backendConstNameList = computed(() => {
return [
//
'枚举类:' + formData.moduleName + 'Enum.java', //
'常量类:' + formData.moduleName + 'Const.java', //
];
});
</script>
<style lang="less" scoped>
.preview-title {
font-weight: 600;
margin: 5px 0px;
}
.preview-block {
font-size: 14px;
background-color: #f9f9f9;
padding: 10px 5px;
}
</style>

View File

@@ -0,0 +1,129 @@
<!--
* 代码生成 删除
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-22 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-form ref="formRef" :model="formData" :rules="formRules" :label-col="{ span: 6 }" style="width: 600px">
<a-form-item label="数据库表名词"> {{ tableInfo.tableName }} </a-form-item>
<a-form-item label="数据库表备注"> {{ tableInfo.tableComment }} </a-form-item>
<a-form-item label="是否允许删除" name="isSupportDelete">
<a-radio-group v-model:value="formData.isSupportDelete" button-style="solid">
<a-radio-button :value="true">支持删除</a-radio-button>
<a-radio-button :value="false">不允许删除</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="是否为物理删除" name="isPhysicallyDeleted" v-if="formData.isSupportDelete">
<a-radio-group v-model:value="formData.isPhysicallyDeleted" button-style="solid">
<a-radio-button :value="true">物理删除</a-radio-button>
<a-radio-button :value="false">假删</a-radio-button>
</a-radio-group>
<div class="smart-margin-top10" v-if="!formData.isPhysicallyDeleted">
<span v-if="deleteFlagColumnName"> 假删字段为{{ deleteFlagColumnName }} </span>
<span stlye="color:red" v-else> 系统未检测出假删字段假删字段名词应该为 <strong>deleted_flag</strong> </span>
</div>
</a-form-item>
<a-form-item label="删除类型" name="deleteEnum" v-if="formData.isSupportDelete">
<SmartEnumSelect enumName="CODE_DELETE_ENUM" v-model:value="formData.deleteEnum" width="200px" />
</a-form-item>
</a-form>
</template>
<script setup>
import { message } from 'ant-design-vue';
import _ from 'lodash';
import { inject, reactive, ref } from 'vue';
import SmartEnumSelect from '/@/components/framework/smart-enum-select/index.vue';
import { CODE_DELETE_ENUM } from '/@/constants/support/code-generator-const';
const tableInfo = inject('tableInfo');
// ------------- 表单 -------------
const deleteFlagColumnName = ref('');
const formRef = ref();
const defaultFormData = {
isSupportDelete: true, // 是否支持删除
isPhysicallyDeleted: undefined, // 是否为物理删除
deleteEnum: undefined, // 删除类型
};
const formData = reactive({ ...defaultFormData });
const formRules = {
isSupportDelete: [{ required: true, message: '请输入 isSupportDelete' }],
isPhysicallyDeleted: [{ required: true, message: '请输入 是否为物理删除' }],
deleteEnum: [{ required: true, message: '请输入 删除类型' }],
};
//初始化设置数据
function setData(tableColumns, config) {
//删除字段
let deletedFlagColumn = getDeleteFlagColumn(tableColumns);
if (deletedFlagColumn) {
deleteFlagColumnName.value = deletedFlagColumn.columnName;
}
console.log(deletedFlagColumn);
//表单
let deleteInfo = config.deleteInfo;
formData.isSupportDelete = deleteInfo ? deleteInfo.isSupportDelete : true;
formData.isPhysicallyDeleted = deleteInfo && deleteInfo.isPhysicallyDeleted ? deleteInfo.isPhysicallyDeleted : !deletedFlagColumn;
formData.deleteEnum = deleteInfo && deleteInfo.deleteEnum ? deleteInfo.deleteEnum : CODE_DELETE_ENUM.SINGLE_AND_BATCH.value;
}
// 获取配置过的字段信息
function getDeleteFlagColumn(configFields) {
if (!configFields) {
return null;
}
let result = configFields.filter((e) => _.startsWith(e.columnName, 'deleted_flag' || _.startsWith(e.columnName, 'delete_flag')));
return result && result.length > 0 ? result[0] : null;
}
// 获取表单数据
function getForm() {
return Object.assign({}, formData);
}
// 校验表单
function validateForm() {
return new Promise((resolve, reject) => {
formRef.value
.validate()
.then(() => {
resolve(true);
})
.catch((error) => {
message.error(' 请检查【4.删除】表单,有参数验证错误');
reject(error);
});
});
}
defineExpose({
setData,
getForm,
validateForm,
});
</script>
<style lang="less" scoped>
.preview-title {
font-weight: 600;
margin: 5px 0px;
}
.preview-block {
font-size: 14px;
background-color: #f9f9f9;
padding: 10px 5px;
}
</style>

View File

@@ -0,0 +1,234 @@
<!--
* 代码生成 配置信息
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-22 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-alert :closable="true" message="请务必将每一个字段的 “ 字段名词 ” 填写完整!!!" type="success" show-icon>
<template #icon><smile-outlined /></template>
</a-alert>
<!-- 为了方便再配置时中途新增字典后 可以重新刷新字典下拉 (需要先随便选择一个字典后才能看到最新的字典) -->
<div style="float: right; padding: 10px 0px">
<a-button type="primary" @click="refreshDict">刷新字典</a-button>
</div>
<a-table
:scroll="{ x: 1300 }"
size="small"
bordered
class="smart-margin-top10"
:dataSource="tableData"
:columns="columns"
rowKey="columnName"
:pagination="false"
>
<template #bodyCell="{ text, record, index, column }">
<template v-if="column.dataIndex === 'no'">
{{ index + 1 }}
</template>
<template v-if="column.dataIndex === 'columnName'">
<span>
<template v-if="record.primaryKeyFlag">
<a-tag color="#f50" style="line-height: 12px">主键</a-tag>
</template>
<template v-if="record.autoIncreaseFlag">
<a-tag color="#f50" style="line-height: 12px">自增</a-tag>
</template>
<br />
{{ text }}
</span>
</template>
<template v-if="column.dataIndex === 'nullableFlag'">
<a-tag color="error" v-if="text">非空</a-tag>
</template>
<template v-if="column.dataIndex === 'fieldName'">
<a-input v-model:value="record.fieldName" />
</template>
<template v-if="column.dataIndex === 'label'">
<a-input v-model:value="record.label" />
</template>
<template v-if="column.dataIndex === 'javaType'">
<a-select v-model:value="record.javaType" style="width: 100%">
<a-select-option v-for="item in JavaTypeList" :value="item" :key="item">{{ item }}</a-select-option>
</a-select>
</template>
<template v-if="column.dataIndex === 'jsType'">
<a-select v-model:value="record.jsType" style="width: 100%">
<a-select-option v-for="item in JsTypeList" :value="item" :key="item">{{ item }}</a-select-option>
</a-select>
</template>
<template v-if="column.dataIndex === 'dict'">
<DictKeySelect ref="dictRef" v-model:value="record.dict" />
</template>
<template v-if="column.dataIndex === 'enumName'">
<a-input v-model:value="record.enumName" />
</template>
</template>
</a-table>
</template>
<script setup>
import { inject, ref } from 'vue';
import { checkExistEnum, convertJavaEnumName, getJavaType, getJsType, JavaTypeList, JsTypeList } from '../../code-generator-util';
import DictKeySelect from '/@/components/support/dict-key-select/index.vue';
import { convertUpperCamel, convertLowerCamel } from '/@/utils/str-util';
import _ from 'lodash';
const dictRef = ref();
function refreshDict() {
dictRef.value.queryDict();
}
//------------------------ 全局数据 ---------------------
const tableInfo = inject('tableInfo');
//------------------------ 表格渲染 ---------------------
const columns = ref([
{
title: '列名',
dataIndex: 'columnName',
width: 120,
ellipsis: true,
},
{
title: '列描述',
dataIndex: 'columnComment',
width: 120,
ellipsis: true,
},
{
title: '列类型',
dataIndex: 'dataType',
width: 100,
ellipsis: true,
},
{
title: '非空',
dataIndex: 'nullableFlag',
width: 60,
},
{
title: '字段命名',
dataIndex: 'fieldName',
width: 150,
},
{
title: '字段名词',
dataIndex: 'label',
width: 150,
},
{
title: 'Java类型',
dataIndex: 'javaType',
width: 150,
},
{
title: '前端类型',
dataIndex: 'jsType',
width: 130,
},
{
title: '字典',
dataIndex: 'dict',
width: 150,
},
{
title: '枚举',
dataIndex: 'enumName',
width: 150,
},
]);
const tableData = ref([]);
// ------------------- 表格数据 -------------------
//初始化设置数据
function setData(tableColumns, config) {
let fields = [];
//基础信息
let basic = config.basic;
//命名
let removePrefixTableName = tableInfo.tableName;
if (_.startsWith(tableInfo.tableName, 't_')) {
removePrefixTableName = _.trim(removePrefixTableName, '_t');
}
let moduleName = basic && basic.moduleName ? basic.moduleName : removePrefixTableName;
moduleName = convertUpperCamel(moduleName);
for (let column of tableColumns) {
let configField = getConfigField(config.fields, column.columnName);
let field = {
columnName: column.columnName,
columnComment: column.columnComment,
dataType: column.dataType,
nullableFlag: column.isNullable === 'NO',
primaryKeyFlag: column.columnKey === 'PRI',
autoIncreaseFlag: column.extra === 'auto_increment',
//表单
fieldName: configField ? configField.fieldName : convertLowerCamel(column.columnName),
label: configField ? configField.label : column.columnComment,
javaType: configField ? configField.javaType : getJavaType(column.dataType),
jsType: configField ? configField.jsType : getJsType(column.dataType),
dict: configField ? configField.dict : null,
enumName: configField
? configField.enumName
: checkExistEnum(column.columnComment)
? convertJavaEnumName(moduleName, column.columnName)
: null,
};
fields.push(field);
}
tableData.value = fields;
}
// 获取配置过的字段信息
function getConfigField(configFields, columnName) {
if (!configFields) {
return null;
}
let result = configFields.filter((e) => e.columnName === columnName);
return result && result.length > 0 ? result[0] : null;
}
// 获取表单数据
function getFieldsForm() {
return tableData.value.map((e) => {
return {
columnName: e.columnName,
columnComment: e.columnComment,
label: e.label,
fieldName: e.fieldName,
javaType: e.javaType,
jsType: e.jsType,
dict: e.dict,
enumName: e.enumName,
primaryKeyFlag: e.primaryKeyFlag,
autoIncreaseFlag: e.autoIncreaseFlag,
};
});
}
defineExpose({
setData,
getFieldsForm,
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,305 @@
<!--
* 代码生成 新增和更新
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-22 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-row class="smart-margin-top10">
<a-col flex="350px">
<a-form ref="formRef" :model="formData" style="width: 350px" :rules="formRules" :label-col="{ span: 5 }" :wrapper-col="{ span: 16 }">
<a-form-item label="是否支持" name="isSupportInsertAndUpdate">
<a-radio-group v-model:value="formData.isSupportInsertAndUpdate" button-style="solid">
<a-radio-button :value="true">支持</a-radio-button>
<a-radio-button :value="false">不支持添加修改</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="页面方式" name="pageType" v-if="formData.isSupportInsertAndUpdate">
<a-radio-group v-model:value="formData.pageType" button-style="solid">
<a-radio-button :value="CODE_INSERT_AND_UPDATE_PAGE_ENUM.MODAL.value">{{ CODE_INSERT_AND_UPDATE_PAGE_ENUM.MODAL.desc }}</a-radio-button>
<a-radio-button :value="CODE_INSERT_AND_UPDATE_PAGE_ENUM.DRAWER.value">{{ CODE_INSERT_AND_UPDATE_PAGE_ENUM.DRAWER.desc }}</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item
label="页面宽度"
v-show="formData.pageType !== CODE_INSERT_AND_UPDATE_PAGE_ENUM.PAGE.value"
name="width"
v-if="formData.isSupportInsertAndUpdate"
>
<a-input v-model:value="formData.width" placeholder="Modal或者Drawer的width属性 " />
</a-form-item>
<a-form-item label="每行数量" name="countPerLine" v-if="formData.isSupportInsertAndUpdate">
<a-input-number style="width: 100%" :max="24" v-model:value="formData.countPerLine" placeholder="请输入 每行数量 " />
</a-form-item>
</a-form>
</a-col>
<a-col flex="auto" style="height: auto; width: 500px" v-if="formData.isSupportInsertAndUpdate">
<div class="form-preview">
<a-row :gutter="20" justify="space-around">
<a-col class="form-item" :span="spanPerLine" v-for="i of formData.countPerLine" :key="i">
<div class="gutter-box">字段</div>
</a-col>
</a-row>
<a-row :gutter="20" class="smart-margin-top10" justify="space-around">
<a-col class="form-item" :span="spanPerLine" v-for="i of formData.countPerLine" :key="i">
<div class="gutter-box">字段</div>
</a-col>
</a-row>
<a-row :gutter="20" class="smart-margin-top10" justify="space-around">
<a-col class="form-item" :span="spanPerLine" v-for="i of formData.countPerLine" :key="i">
<div class="gutter-box">字段</div>
</a-col>
</a-row>
</div>
</a-col>
</a-row>
<a-table
size="small"
bordered
class="smart-margin-top10"
:dataSource="tableData"
:columns="columns"
rowKey="columnName"
:pagination="false"
v-if="formData.isSupportInsertAndUpdate"
>
<template #bodyCell="{ text, record, index, column }">
<template v-if="column.dataIndex === 'no'">
{{ index + 1 }}
</template>
<template v-if="column.dataIndex === 'columnName'">
<span>
<template v-if="record.primaryKeyFlag">
<a-tag color="#f50" style="line-height: 12px">主键</a-tag>
</template>
<template v-if="record.autoIncreaseFlag">
<a-tag color="#f50" style="line-height: 12px">自增</a-tag>
</template>
<br />
{{ text }}
</span>
</template>
<template v-if="column.dataIndex === 'nullableFlag'">
<a-tag color="error" v-if="text">非空</a-tag>
</template>
<template v-if="column.dataIndex === 'required'">
<a-checkbox v-model:checked="record.requiredFlag" />
</template>
<template v-if="column.dataIndex === 'insertFlag'">
<a-checkbox v-model:checked="record.insertFlag" />
</template>
<template v-if="column.dataIndex === 'updateFlag'">
<a-checkbox v-model:checked="record.updateFlag" />
</template>
<template v-if="column.dataIndex === 'frontComponent'">
<SmartEnumSelect width="100%" enum-name="CODE_FRONT_COMPONENT_ENUM" v-model:value="record.frontComponent" />
</template>
</template>
</a-table>
</template>
<script setup>
import { computed, inject, reactive, ref } from 'vue';
import { checkExistEnum, getFrontComponent } from '../../code-generator-util';
import { CODE_FRONT_COMPONENT_ENUM, CODE_INSERT_AND_UPDATE_PAGE_ENUM } from '/@/constants/support/code-generator-const';
import SmartEnumSelect from '/@/components/framework/smart-enum-select/index.vue';
import { message } from 'ant-design-vue';
//------------------------ 全局数据 ---------------------
const tableInfo = inject('tableInfo');
// ------------- 表单 -------------
const formRef = ref();
const defaultFormData = {
isSupportInsertAndUpdate: true, // 是否允许增加、删除
pageType: CODE_INSERT_AND_UPDATE_PAGE_ENUM.MODAL.value, // 类型
width: undefined, // 宽度
countPerLine: 1, // 每行数量
};
const formData = reactive({ ...defaultFormData });
const formRules = {
isSupportInsertAndUpdate: [{ required: true, message: '请输入 是否允许增加、删除' }],
pageType: [{ required: true, message: '请输入 页面方式' }],
width: [{ required: true, message: '请输入 宽度' }],
countPerLine: [{ required: true, message: '请输入 每行数量' }],
};
// ------------- 预览 -------------
const spanPerLine = computed(() => {
return parseInt(20 / formData.countPerLine);
});
//------------------------ 表格渲染 ---------------------
const columns = ref([
{
title: '列名',
dataIndex: 'columnName',
width: 120,
ellipsis: true,
},
{
title: '列描述',
dataIndex: 'columnComment',
width: 120,
ellipsis: true,
},
{
title: '列类型',
dataIndex: 'dataType',
width: 100,
ellipsis: true,
},
{
title: '非空',
dataIndex: 'nullableFlag',
width: 60,
},
{
title: '必填',
dataIndex: 'required',
width: 35,
},
{
title: '新增',
dataIndex: 'insertFlag',
width: 35,
},
{
title: '更新',
dataIndex: 'updateFlag',
width: 35,
},
{
title: '前端组件',
dataIndex: 'frontComponent',
width: 100,
},
]);
const tableData = ref([]);
// ------------------- 表格数据 -------------------
//初始化设置数据
function setData(tableColumns, config) {
//------------- 更新基础信息 -----------------
if (config.insertAndUpdate) {
formData.isSupportInsertAndUpdate = config.insertAndUpdate.isSupportInsertAndUpdate ? config.insertAndUpdate.isSupportInsertAndUpdate : true;
formData.pageType = config.insertAndUpdate.pageType;
formData.width = config.insertAndUpdate.width;
formData.countPerLine = config.insertAndUpdate.countPerLine;
}
//------------- 更新字段信息 -----------------
let insertAndUpdateFields = config.insertAndUpdate && config.insertAndUpdate.fieldList ? config.insertAndUpdate.fieldList : null;
let fields = [];
for (let column of tableColumns) {
let configField = getConfigField(insertAndUpdateFields, column.columnName);
let field = {
columnName: column.columnName,
columnComment: column.columnComment,
dataType: column.dataType,
nullableFlag: column.isNullable === 'NO',
primaryKeyFlag: column.columnKey === 'PRI',
autoIncreaseFlag: column.extra === 'auto_increment',
};
//表单
field.requiredFlag = configField ? configField.requiredFlag : field.nullableFlag;
field.insertFlag = configField ? configField.insertFlag : field.nullableFlag;
field.updateFlag = configField ? configField.updateFlag : false;
if (configField && configField.frontComponent) {
field.frontComponent = configField.frontComponent;
} else {
field.frontComponent = checkExistEnum(column.columnComment)
? CODE_FRONT_COMPONENT_ENUM.ENUM_SELECT.value
: getFrontComponent(column.dataType);
}
fields.push(field);
}
tableData.value = fields;
}
// 获取配置过的字段信息
function getConfigField(configFields, columnName) {
if (!configFields) {
return null;
}
let result = configFields.filter((e) => e.columnName === columnName);
return result && result.length > 0 ? result[0] : null;
}
// 获取表单数据
function getFieldsForm() {
let fieldList = tableData.value.map((e) => {
return {
columnName: e.columnName,
requiredFlag: e.requiredFlag,
insertFlag: e.insertFlag,
updateFlag: e.updateFlag,
frontComponent: e.frontComponent,
};
});
return {
isSupportInsertAndUpdate: formData.isSupportInsertAndUpdate,
pageType: formData.pageType,
width: formData.width,
countPerLine: formData.countPerLine,
fieldList,
};
}
// 校验表单
function validateForm() {
return new Promise((resolve, reject) => {
formRef.value
.validate()
.then(() => {
resolve(true);
})
.catch((error) => {
message.error(' 请检查【3.增加、修改】表单,有参数验证错误');
reject(error);
});
});
}
defineExpose({
setData,
getFieldsForm,
validateForm,
});
</script>
<style scoped lang="less">
.form-preview {
background-color: #efefef;
padding: 15px 20px;
border: 0;
}
.form-item {
background: #00a0e9;
padding: 5px 0;
text-align: center;
color: white;
}
</style>

View File

@@ -0,0 +1,241 @@
<!--
* 代码生成 查询条件
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-22 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-row>
<a-button type="primary" @click="addQuery">添加查询条件</a-button>
</a-row>
<a-table
size="small"
bordered
id="smartCodeQueryFieldsTable"
class="smart-margin-top10"
:dataSource="tableData"
row-class-name="column-row"
:columns="columns"
rowKey="rowKey"
:pagination="false"
>
<template #bodyCell="{ record, index, column }">
<template v-if="column.dataIndex === 'drag'">
<a-button type="text" class="handle" size="small" style="width: 100%; text-align: left">
<template #icon> <drag-outlined /> </template>
</a-button>
</template>
<template v-if="column.dataIndex === 'label'">
<a-input v-model:value="record.label" placeholder="关键字查询" />
</template>
<template v-if="column.dataIndex === 'fieldName'">
<a-input v-model:value="record.fieldName" placeholder="keywords" />
</template>
<template v-if="column.dataIndex === 'width'">
<a-input v-model:value="record.width" placeholder="150px" />
</template>
<template v-if="column.dataIndex === 'queryTypeEnum'">
<SmartEnumSelect
@change="(value) => onChangeQueryType(value, record)"
enumName="CODE_QUERY_FIELD_QUERY_TYPE_ENUM"
v-model:value="record.queryTypeEnum"
width="100%"
/>
</template>
<template v-if="column.dataIndex === 'columnNameList'">
<a-select
show-search
:mode="record.queryTypeEnum && record.queryTypeEnum === CODE_QUERY_FIELD_QUERY_TYPE_ENUM.LIKE.value ? 'multiple' : ''"
v-model:value="record.columnNameList"
@change="onSelectColumn(record)"
style="width: 100%"
>
<a-select-option v-for="item in tableColumns" :value="item.columnName" :key="item.columnName">
{{ item.columnName }}
<span v-show="item.columnComment"> {{ item.columnComment }} </span>
</a-select-option>
</a-select>
</template>
<template v-if="column.dataIndex === 'operate'">
<div class="smart-table-operate">
<a-button type="link" @click="onDelete(index)" danger>删除</a-button>
</div>
</template>
</template>
</a-table>
</template>
<script setup>
import Sortable from 'sortablejs';
import { inject, nextTick, ref } from 'vue';
import SmartEnumSelect from '/@/components/framework/smart-enum-select/index.vue';
import { CODE_QUERY_FIELD_QUERY_TYPE_ENUM } from '/@/constants/support/code-generator-const';
import { convertLowerCamel } from '/@/utils/str-util';
//------------------------ 全局数据 ---------------------
const tableInfo = inject('tableInfo');
//------------------------ 表格渲染 ---------------------
const columns = ref([
{
title: '拖拽',
dataIndex: 'drag',
width: 60,
},
{
title: '查询类型',
dataIndex: 'queryTypeEnum',
width: 130,
},
{
title: '查询列',
dataIndex: 'columnNameList',
},
{
title: '条件名称',
dataIndex: 'label',
width: 150,
},
{
title: '字段命名',
dataIndex: 'fieldName',
width: 150,
},
{
title: '宽度',
dataIndex: 'width',
width: 150,
},
{
title: '操作',
dataIndex: 'operate',
width: 60,
},
]);
const tableData = ref([]);
const tableColumns = ref([]);
let rowKeyCounter = 1;
//初始化设置数据
function setData(tableColumnInfos, config) {
rowKeyCounter = 1;
let data = config && config.queryFields ? config.queryFields : [];
for (let index = 0; index < data.length; index++) {
data[index].rowKey = 'rowKey' + rowKeyCounter;
rowKeyCounter++;
}
tableData.value = data;
tableColumns.value = tableColumnInfos;
nextTick(() => {
initDrag();
});
}
// ------------------- 增加、删除 -------------------
function addQuery() {
tableData.value.push({
rowKey: 'rowKey' + rowKeyCounter,
label: '',
fieldName: '',
queryTypeEnum: '',
columnNameList: null,
width: '200px',
});
rowKeyCounter++;
}
function onDelete(index) {
// 以这种方式删除 列表才会重新渲染
const tempList = [...tableData.value];
tempList.splice(index, 1);
tableData.value = [];
nextTick(() => {
tableData.value = tempList;
});
}
//初始化拖拽
function initDrag() {
let tbody = document.querySelector('#smartCodeQueryFieldsTable tbody');
Sortable.create(tbody, {
animation: 300,
dragClass: 'smart-ghost-class', //设置拖拽样式类名
ghostClass: 'smart-ghost-class', //设置拖拽停靠样式类名
chosenClass: 'smart-ghost-class', //设置选中样式类名
handle: '.handle',
onEnd: ({ oldIndex, newIndex }) => {
const oldRow = tableData.value.splice(oldIndex, 1)[0];
tableData.value.splice(newIndex, 0, oldRow);
},
});
}
// ------------------- 监听数据变化 -------------------
function onChangeQueryType(queryType, record) {
if (queryType === CODE_QUERY_FIELD_QUERY_TYPE_ENUM.LIKE.value) {
record.columnNameList = [];
} else {
record.columnNameList = null;
}
}
function onSelectColumn(record) {
if (Array.isArray(record.columnNameList)) {
return;
}
let columnName = record.columnNameList;
let column = getConfigField(tableColumns.value, columnName);
//表单
record.fieldName = column && column.columnName ? convertLowerCamel(column.columnName) : '';
record.label = column && column.columnComment ? convertLowerCamel(column.columnComment) : '';
}
// 获取配置过的字段信息
function getConfigField(configFields, columnName) {
if (!configFields) {
return null;
}
let result = configFields.filter((e) => e.columnName === columnName);
return result && result.length > 0 ? result[0] : null;
}
// ------------------- 获取表单数据 -------------------
// 获取表单数据
function getFieldsForm() {
let result = tableData.value.map((item) => {
return {
label: item.label,
width: item.width,
fieldName: item.fieldName,
queryTypeEnum: item.queryTypeEnum,
// 字符串转为数组
columnNameList: item.columnNameList && typeof item.columnNameList === 'string' ? [item.columnNameList] : item.columnNameList,
};
});
return result;
}
defineExpose({
setData,
getFieldsForm,
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,162 @@
<!--
* 代码生成 列表
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-22 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-alert :closable="true" message="请务必将每一个字段的 “ 字段名词 ” 填写完整!!!" type="success" show-icon>
<template #icon><smile-outlined /></template>
</a-alert>
<a-table
size="small"
bordered
:scroll="{ x: 1000 }"
class="smart-margin-top10"
:dataSource="tableData"
:columns="columns"
rowKey="columnName"
:pagination="false"
>
<template #bodyCell="{ record, index, column }">
<template v-if="column.dataIndex === 'no'">
{{ index + 1 }}
</template>
<template v-if="column.dataIndex === 'showFlag'">
<a-checkbox v-model:checked="record.showFlag" />
</template>
<template v-if="column.dataIndex === 'fieldName'">
<a-input v-model:value="record.fieldName" />
</template>
<template v-if="column.dataIndex === 'label'">
<a-input v-model:value="record.label" />
</template>
<template v-if="column.dataIndex === 'width'">
<a-input-number v-model:value="record.width" />
</template>
<template v-if="column.dataIndex === 'ellipsisFlag'">
<a-switch v-model:checked="record.ellipsisFlag" checked-children="自动省略" un-checked-children="换行显示" />
</template>
</template>
</a-table>
</template>
<script setup>
import { inject, ref } from 'vue';
import { convertLowerCamel } from '/@/utils/str-util';
//------------------------ 全局数据 ---------------------
const tableInfo = inject('tableInfo');
//------------------------ 表格渲染 ---------------------
const columns = ref([
{
title: '序号',
dataIndex: 'no',
width: 60,
},
{
title: '列名',
dataIndex: 'columnName',
width: 120,
ellipsis: true,
},
{
title: '列描述',
dataIndex: 'columnComment',
width: 120,
ellipsis: true,
},
{
title: '显示',
dataIndex: 'showFlag',
width: 50,
},
{
title: '字段名词',
dataIndex: 'label',
width: 120,
},
{
title: '字段命名',
dataIndex: 'fieldName',
width: 120,
},
{
title: '宽度',
dataIndex: 'width',
width: 80,
},
{
title: 'ellipsis',
dataIndex: 'ellipsisFlag',
width: 100,
},
]);
const tableData = ref([]);
// ------------------- 表格数据 -------------------
//初始化设置数据
function setData(tableColumns, config) {
let fields = [];
for (let column of tableColumns) {
let configField = getConfigField(config.tableFields, column.columnName);
let field = {
columnName: column.columnName,
columnComment: column.columnComment,
dataType: column.dataType,
//表单
showFlag: configField ? configField.showFlag : true,
label: configField ? configField.label : column.columnComment,
fieldName: configField ? configField.fieldName : convertLowerCamel(column.columnName),
width: configField ? configField.width : null,
ellipsisFlag: configField ? configField.ellipsisFlag : true,
};
fields.push(field);
}
tableData.value = fields;
}
// 获取配置过的字段信息
function getConfigField(configFields, columnName) {
if (!configFields) {
return null;
}
let result = configFields.filter((e) => e.columnName === columnName);
return result && result.length > 0 ? result[0] : null;
}
// 获取表单数据
function getTaleFieldsForm() {
return tableData.value.map((e) => {
return {
columnName: e.columnName,
label: e.label,
fieldName: e.fieldName,
showFlag: e.showFlag,
width: e.width,
ellipsisFlag: e.ellipsisFlag,
};
});
}
defineExpose({
setData,
getTaleFieldsForm,
});
</script>

View File

@@ -0,0 +1,228 @@
<!--
* 代码生成 配置信息
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-22 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-drawer
title="代码配置"
style=""
:open="visibleFlag"
:width="1200"
:footerStyle="{ textAlign: 'right' }"
@close="onClose"
:maskClosable="false"
:destroyOnClose="true"
>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="1" :forceRender="true">
<template #tab>
<span>
<info-circle-outlined />
1.基础命名
</span>
</template>
<CodeGeneratorTableConfigFormBasic ref="basicRef" />
</a-tab-pane>
<a-tab-pane key="2" :forceRender="true">
<template #tab>
<span>
<unordered-list-outlined />
2.字段列表
</span>
</template>
<CodeGeneratorTableConfigFormField ref="fieldRef" />
</a-tab-pane>
<a-tab-pane key="3" :forceRender="true">
<template #tab>
<span>
<save-outlined />
3.增加修改
</span>
</template>
<CodeGeneratorTableConfigFormInsertAndUpdate ref="insertAndUpdateRef" />
</a-tab-pane>
<a-tab-pane key="4" :forceRender="true">
<template #tab>
<span>
<delete-outlined />
4.删除
</span>
</template>
<CodeGeneratorTableConfigFormDelete ref="deleteRef" />
</a-tab-pane>
<a-tab-pane key="5" :forceRender="true">
<template #tab>
<span>
<file-search-outlined />
5查询条件
</span>
</template>
<CodeGeneratorTableConfigFormQueryField ref="queryRef" />
</a-tab-pane>
<a-tab-pane key="6" :forceRender="true">
<template #tab>
<span>
<table-outlined />
6列表
</span>
</template>
<CodeGeneratorTableConfigFormTableField ref="tableFieldRef" />
</a-tab-pane>
</a-tabs>
<template #footer>
<a-space>
<a-button @click="onClose">取消</a-button>
<a-button type="primary" @click="save">保存</a-button>
</a-space>
</template>
</a-drawer>
</template>
<script setup>
import { reactive, ref, provide, nextTick } from 'vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { smartSentry } from '/@/lib/smart-sentry';
import CodeGeneratorTableConfigFormBasic from './code-generator-table-config-form-basic.vue';
import { codeGeneratorApi } from '/@/api/support/code-generator-api';
import CodeGeneratorTableConfigFormField from './code-generator-table-config-form-field.vue';
import CodeGeneratorTableConfigFormInsertAndUpdate from './code-generator-table-config-form-insert-and-update.vue';
import CodeGeneratorTableConfigFormDelete from './code-generator-table-config-form-delete.vue';
import CodeGeneratorTableConfigFormQueryField from './code-generator-table-config-form-query-field.vue';
import CodeGeneratorTableConfigFormTableField from './code-generator-table-config-form-table-field.vue';
import { message } from 'ant-design-vue';
// ------------------ 显示,关闭 ------------------
// 显示
const visibleFlag = ref(false);
function showModal(table) {
Object.assign(tableInfo, table);
activeKey.value = '1';
visibleFlag.value = true;
nextTick(() => {
getTableColumns();
});
}
// 关闭
function onClose() {
visibleFlag.value = false;
}
// ------------------ 组件------------------
const basicRef = ref();
const fieldRef = ref();
const insertAndUpdateRef = ref();
const deleteRef = ref();
const queryRef = ref();
const tableFieldRef = ref();
// ------------------ 表的列信息 、配置信息------------------
const tableColumns = ref([]);
const tableConfig = ref({});
// 查询表的列
async function getTableColumns() {
try {
SmartLoading.show();
let columnResult = await codeGeneratorApi.getTableColumns(tableInfo.tableName);
tableColumns.value = columnResult.data;
let configResult = await codeGeneratorApi.getConfig(tableInfo.tableName);
tableConfig.value = configResult.data;
//基础命名
basicRef.value.setData(tableConfig.value);
//字段列表
fieldRef.value.setData(tableColumns.value, tableConfig.value);
//新增和修改
insertAndUpdateRef.value.setData(tableColumns.value, tableConfig.value);
//删除
deleteRef.value.setData(tableColumns.value, tableConfig.value);
//查询
queryRef.value.setData(tableColumns.value, tableConfig.value);
//表格列表
tableFieldRef.value.setData(tableColumns.value, tableConfig.value);
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
// ------------------ 表信息 ------------------
const tableInfo = reactive({
tableName: '', //表名
tableComment: '', //备注
createTime: '', //表创建时间
updateTime: '', //表修改时间
});
// ------------------ 标签页 ------------------
const activeKey = ref('1');
// ------------------ 提交表单 ------------------
const emits = defineEmits(['reloadList']);
async function save() {
SmartLoading.show();
try {
let basicValidated = await basicRef.value.validateForm();
let insertAndUpdateValidated = await insertAndUpdateRef.value.validateForm();
let deleteValidated = await deleteRef.value.validateForm();
if (!basicValidated || !insertAndUpdateValidated || !deleteValidated) {
return;
}
let fields = fieldRef.value.getFieldsForm();
let basic = basicRef.value.getBasicForm();
let insertAndUpdate = insertAndUpdateRef.value.getFieldsForm();
let deleteInfo = deleteRef.value.getForm();
let queryFields = queryRef.value.getFieldsForm();
let tableFields = tableFieldRef.value.getTaleFieldsForm();
await codeGeneratorApi.updateConfig({
tableName: tableInfo.tableName,
basic,
fields,
insertAndUpdate,
deleteInfo: deleteInfo,
queryFields,
tableFields,
});
message.success('保存成功');
emits('reloadList');
onClose();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
defineExpose({
showModal,
});
// ------------------ provide ------------------
provide('tableInfo', tableInfo);
provide('tableColumns', tableColumns);
provide('tableConfig', tableConfig);
</script>
<style lang="less" scoped>
.visible-list {
display: flex;
flex-wrap: wrap;
.visible-item {
padding-top: 8px;
}
}
</style>

View File

@@ -0,0 +1,202 @@
<!--
* 代码生成 预览代码
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-22 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-drawer
title="代码预览"
:open="visibleFlag"
:width="1200"
:footerStyle="{ textAlign: 'right' }"
:bodyStyle="{ padding: '8px 24px' }"
@close="onClose"
:maskClosable="false"
:destroyOnClose="true"
>
<a-row justify="space-between" class="smart-margin-bottom10">
<a-radio-group v-model:value="languageType" button-style="solid" @change="onChangeLanguageType">
<a-radio-button :value="LANGUAGE_LIST[0]">JavaScript代码</a-radio-button>
<a-radio-button :value="LANGUAGE_LIST[1]">TypeScript代码</a-radio-button>
<a-radio-button :value="LANGUAGE_LIST[2]">Java代码</a-radio-button>
</a-radio-group>
<a-button type="link" @click="download" danger size="small"><strong>下载代码</strong></a-button>
</a-row>
<a-tabs v-model:activeKey="fileKey" size="small" @change="onChangeTab">
<a-tab-pane v-for="item in tabList" :key="item" :tab="item" />
</a-tabs>
<div>
<pre><code :class="codeClass">{{resultCode}}</code></pre>
</div>
</a-drawer>
</template>
<script setup>
import { computed, nextTick, ref } from 'vue';
import { codeGeneratorApi } from '/@/api/support/code-generator-api';
import { JAVA_FILE_LIST, LANGUAGE_LIST, JS_FILE_LIST, TS_FILE_LIST } from '../../code-generator-util';
import { smartSentry } from '/@/lib/smart-sentry';
import { lineNumbersBlock } from '/@/lib/highlight-line-number';
import hljs from 'highlight.js';
import 'highlight.js/styles/github-dark.css';
import javascript from 'highlight.js/lib/languages/javascript';
import typescript from 'highlight.js/lib/languages/typescript';
import java from 'highlight.js/lib/languages/java';
import { message } from 'ant-design-vue';
// ------------------ 显示,关闭 ------------------
// 显示
const visibleFlag = ref(false);
function showModal(tableInfo) {
tableName.value = tableInfo.tableName;
tableComment.value = tableInfo.tableComment;
visibleFlag.value = true;
nextTick(() => {
onChangeTab(fileKey.value);
});
}
// 关闭
function onClose() {
visibleFlag.value = false;
}
// ------------------ 表------------------
const tableName = ref('');
const tableComment = ref('');
// ------------------ 标签页 ------------------
const languageType = ref(LANGUAGE_LIST[0]);
const tabList = computed(() => {
if (languageType.value === LANGUAGE_LIST[0]) {
return JS_FILE_LIST;
} else if (languageType.value === LANGUAGE_LIST[1]) {
return TS_FILE_LIST;
} else {
return JAVA_FILE_LIST;
}
});
const fileKey = ref(tabList.value[0]);
function getLanguage() {
if (languageType.value === LANGUAGE_LIST[0]) {
return 'javascript';
} else if (languageType.value === LANGUAGE_LIST[1]) {
return 'typescript';
} else {
return 'java';
}
}
function onChangeLanguageType(e) {
if (e.target.value === LANGUAGE_LIST[0]) {
fileKey.value = JS_FILE_LIST[0];
} else if (e.target.value === LANGUAGE_LIST[1]) {
fileKey.value = TS_FILE_LIST[0];
} else {
fileKey.value = JAVA_FILE_LIST[0];
}
onChangeTab(fileKey.value);
}
// ------------------ 下载代码 ------------------
function download(rowData) {
codeGeneratorApi.downloadCode(tableName.value);
}
// ------------------ 查询代码 ------------------
const codeClass = ref('language-javascript');
function onChangeTab(tab) {
let templateFile = tab;
let language = getLanguage();
hljs.registerLanguage(language, language == 'java' ? java : javascript);
codeClass.value = 'language-' + language;
console.log(templateFile);
nextTick(() => {
generateCode(templateFile, tableName.value);
});
}
const resultCode = ref('');
async function generateCode(templateFile, tableName) {
try {
let result = await codeGeneratorApi.preview({
templateFile,
tableName,
});
resultCode.value = result.data;
nextTick(() => {
document.querySelectorAll('pre code').forEach((block) => {
block.setAttribute('highlighted', 'true');
hljs.highlightElement(block);
lineNumbersBlock(block);
block.innerHTML =
"<div><div style='padding: 5px 0px 10px 20px;float:right'><span style='margin-right: 10px;padding: 5px;border: white solid 1px;color:white;border-radius: 2px'>" +
block.className.match(/(?<=language-).*(?= hljs)/).toString() +
"</span><button class='ant-btn ant-btn-sm' >复制代码</button></div>" +
block.innerHTML +
'</div>';
let copyButton = block.querySelector('button');
if (copyButton != null) {
copyButton.onclick = function () {
copy(resultCode.value);
message.success('复制成功!');
};
}
});
});
} catch (e) {
smartSentry.captureError(e);
}
}
function copy(value) {
let textarea = document.createElement('textarea');
document.body.appendChild(textarea);
textarea.value = value;
textarea.select();
document.execCommand('Copy');
document.body.removeChild(textarea);
}
defineExpose({
showModal,
});
</script>
<style lang="less" scoped>
.preview-title {
font-weight: 600;
margin: 5px 0px;
}
.preview-block {
font-size: 12px;
background-color: #f9f9f9;
padding: 10px 5px;
}
:deep(.hljs) {
.hljs-ln-numbers {
text-align: center;
color: #ccc;
border-right: 1px solid #ccc;
vertical-align: top;
padding-right: 5px !important;
}
.hljs-ln-code {
padding-left: 5px !important;
}
}
</style>

View File

@@ -0,0 +1,99 @@
<!--
* 系统设置表单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-06-08 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal :open="visible" :title="form.configId ? '编辑' : '添加'" ok-text="确认" cancel-text="取消" @ok="onSubmit" @cancel="onClose">
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }">
<a-form-item label="参数Key" name="configKey">
<a-input v-model:value="form.configKey" placeholder="请输入参数Key" />
</a-form-item>
<a-form-item label="参数名称" name="configName">
<a-input v-model:value="form.configName" placeholder="请输入参数名称" />
</a-form-item>
<a-form-item label="参数值" name="configValue">
<a-input v-model:value="form.configValue" placeholder="请输入参数值" />
</a-form-item>
<a-form-item label="备注" name="remark">
<textarea v-model="form.remark" style="width: 100%; height: 100px; outline: none"></textarea>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { message } from 'ant-design-vue';
import { reactive, ref } from 'vue';
import { configApi } from '/@/api/support/config-api';
import { smartSentry } from '/@/lib/smart-sentry';
import { SmartLoading } from '/@/components/framework/smart-loading';
// emit
const emit = defineEmits(['reloadList']);
// 组件
const formRef = ref();
const formDefault = {
configId: undefined,
configKey: '',
configName: '',
configValue: '',
remark: '',
};
let form = reactive({ ...formDefault });
const rules = {
configKey: [{ required: true, message: '请输入参数key' }],
configName: [{ required: true, message: '请输入参数名称' }],
configValue: [{ required: true, message: '请输入参数值' }],
};
// 是否展示
const visible = ref(false);
function showModal(rowData) {
Object.assign(form, formDefault);
if (rowData) {
Object.assign(form, rowData);
}
visible.value = true;
}
function onClose() {
Object.assign(form, formDefault);
visible.value = false;
}
function onSubmit() {
formRef.value
.validate()
.then(async () => {
SmartLoading.show();
try {
if (form.configId) {
await configApi.updateConfig(form);
} else {
await configApi.addConfig(form);
}
message.success(`${form.configId ? '修改' : '添加'}成功`);
emit('reloadList');
onClose();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
})
.catch((error) => {
console.log('error', error);
message.error('参数验证错误,请仔细填写表单数据!');
});
}
defineExpose({
showModal,
});
</script>

View File

@@ -0,0 +1,177 @@
<!--
* 系统设置 列表
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-06-08 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="参数Key" class="smart-query-form-item">
<a-input style="width: 300px" v-model:value="queryForm.configKey" placeholder="请输入key" />
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch" v-privilege="'support:config:query'">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery" v-privilege="'support:config:query'">
<template #icon>
<SearchOutlined />
</template>
重置
</a-button>
</a-button-group>
<a-button @click="toEditOrAdd()" v-privilege="'support:config:add'" type="primary" class="smart-margin-left20">
<template #icon>
<PlusOutlined />
</template>
新建
</a-button>
</a-form-item>
</a-row>
</a-form>
<a-card size="small" :bordered="false" :hoverable="true">
<a-row justify="end">
<TableOperator class="smart-margin-bottom5" v-model="columns" :tableId="TABLE_ID_CONST.SUPPORT.CONFIG" :refresh="ajaxQuery" />
</a-row>
<a-table size="small" :loading="tableLoading" bordered :dataSource="tableData" :columns="columns" rowKey="configId" :pagination="false">
<template #bodyCell="{ record, column }">
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button @click="toEditOrAdd(record)" v-privilege="'support:config:update'" type="link">编辑</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `${total}`"
/>
</div>
</a-card>
<ConfigFormModal ref="configFormModal" @reloadList="resetQuery" />
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { configApi } from '/@/api/support/config-api';
import ConfigFormModal from './config-form-modal.vue';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
const columns = ref([
{
title: 'id',
width: 50,
dataIndex: 'configId',
},
{
title: '参数key',
dataIndex: 'configKey',
ellipsis: true,
},
{
title: '参数名称',
dataIndex: 'configName',
ellipsis: true,
},
{
title: '参数值',
dataIndex: 'configValue',
ellipsis: true,
},
{
title: '备注',
dataIndex: 'remark',
ellipsis: true,
width: 150,
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 150,
},
{
title: '修改时间',
dataIndex: 'updateTime',
width: 150,
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 60,
},
]);
// ---------------- 查询数据 -----------------------
const queryFormState = {
configKey: '',
pageNum: 1,
pageSize: 10,
};
const queryForm = reactive({ ...queryFormState });
const tableLoading = ref(false);
const tableData = ref([]);
const total = ref(0);
function resetQuery() {
Object.assign(queryForm, queryFormState);
ajaxQuery();
}
function onSearch() {
queryForm.pageNum = 1;
ajaxQuery();
}
async function ajaxQuery() {
try {
tableLoading.value = true;
let responseModel = await configApi.queryList(queryForm);
const list = responseModel.data.list;
total.value = responseModel.data.total;
tableData.value = list;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
// ------------------------- 表单操作 弹窗 ------------------------------
const configFormModal = ref();
function toEditOrAdd(rowData) {
configFormModal.value.showModal(rowData);
}
onMounted(ajaxQuery);
</script>

View File

@@ -0,0 +1,95 @@
<!--
* 字典key 弹窗
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-06-08 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal :open="visible" :title="form.dictKeyId ? '编辑' : '添加'" ok-text="确认" cancel-text="取消" @ok="onSubmit" @cancel="onClose">
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }" :wrapper-col="{ span: 12 }">
<a-form-item label="编码" name="keyCode">
<a-input v-model:value="form.keyCode" placeholder="请输入编码" />
</a-form-item>
<a-form-item label="名称" name="keyName">
<a-input v-model:value="form.keyName" placeholder="请输入名称" />
</a-form-item>
<a-form-item label="备注" name="remark">
<textarea v-model="form.remark" style="width: 100%; height: 100px; outline: none"></textarea>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { dictApi } from '/@/api/support/dict-api';
import { smartSentry } from '/@/lib/smart-sentry';
// emit
const emit = defineEmits(['reloadList']);
// 组件
const formRef = ref();
const formDefault = {
dictKeyId: undefined,
keyCode: '',
keyName: '',
remark: '',
};
let form = reactive({ ...formDefault });
const rules = {
keyCode: [{ required: true, message: '请输入编码' }],
keyName: [{ required: true, message: '请输入名称' }],
};
// 是否展示
const visible = ref(false);
function showModal(rowData) {
Object.assign(form, formDefault);
if (rowData) {
Object.assign(form, rowData);
}
visible.value = true;
}
function onClose() {
Object.assign(form, formDefault);
visible.value = false;
}
function onSubmit() {
formRef.value
.validate()
.then(async () => {
SmartLoading.show();
try {
if (form.dictKeyId) {
await dictApi.keyEdit(form);
} else {
await dictApi.keyAdd(form);
}
message.success(`${form.dictKeyId ? '修改' : '添加'}成功`);
emit('reloadList');
onClose();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
})
.catch((error) => {
message.error('参数验证错误,请仔细填写表单数据!');
});
}
// ----------------------- 以下是暴露的方法内容 ------------------------
defineExpose({
showModal,
});
</script>

View File

@@ -0,0 +1,221 @@
<!--
* 字典 value 弹窗
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-06-08 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-drawer :width="800" :open="visible" :body-style="{ paddingBottom: '80px' }" title="字典值" @close="onClose">
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="关键字" class="smart-query-form-item">
<a-input style="width: 300px" v-model:value="queryForm.searchWord" placeholder="关键字" />
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button type="primary" @click="ajaxQuery">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<SearchOutlined />
</template>
重置
</a-button>
</a-form-item>
</a-row>
</a-form>
<a-card size="small" :bordered="false">
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button @click="addOrUpdateValue" type="primary" size="small">
<template #icon>
<PlusOutlined />
</template>
新建
</a-button>
<a-button @click="confirmBatchDelete" type="text" danger size="small" :disabled="selectedRowKeyList.length == 0">
<template #icon>
<DeleteOutlined />
</template>
批量删除
</a-button>
</div>
<div class="smart-table-setting-block"></div>
</a-row>
<a-table
size="small"
:dataSource="tableData"
:columns="columns"
rowKey="dictValueId"
:pagination="false"
:row-selection="{ selectedRowKeys: selectedRowKeyList, onChange: onSelectChange }"
bordered
>
<template #bodyCell="{ record, column }">
<template v-if="column.dataIndex === 'action'">
<a-button @click="addOrUpdateValue(record)" type="link">编辑</a-button>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `${total}`"
/>
</div>
</a-card>
<DictValueOperateModal ref="operateModal" @reloadList="ajaxQuery" />
</a-drawer>
</template>
<script setup>
import { reactive, ref } from 'vue';
import DictValueOperateModal from './dict-value-operate-modal.vue';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { dictApi } from '/@/api/support/dict-api';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { Modal } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { smartSentry } from '/@/lib/smart-sentry';
// 是否展示抽屉
const visible = ref(false);
const dictKeyId = ref(undefined);
function showModal(keyId) {
dictKeyId.value = keyId;
visible.value = true;
ajaxQuery();
}
function onClose() {
visible.value = false;
dictKeyId.value = undefined;
}
const columns = reactive([
{
title: 'ID',
width: 80,
dataIndex: 'dictValueId',
},
{
title: '编码',
dataIndex: 'valueCode',
},
{
title: '名称',
dataIndex: 'valueName',
},
{
title: '排序',
width: 80,
dataIndex: 'sort',
},
{
title: '备注',
dataIndex: 'remark',
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
},
]);
// ----------------------- 表格 查询 ------------------------
const queryFormState = {
dictKeyId: undefined,
searchWord: '',
pageNum: 1,
pageSize: 10,
};
const queryForm = reactive({ ...queryFormState });
const selectedRowKeyList = ref([]);
const tableLoading = ref(false);
const tableData = ref([]);
const total = ref(0);
function onSelectChange(selectedRowKeys) {
selectedRowKeyList.value = selectedRowKeys;
}
function resetQuery() {
Object.assign(queryForm, queryFormState);
ajaxQuery();
}
async function ajaxQuery() {
try {
tableLoading.value = true;
queryForm.dictKeyId = dictKeyId.value;
let responseModel = await dictApi.valueQuery(queryForm);
const list = responseModel.data.list;
total.value = responseModel.data.total;
tableData.value = list;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
// ----------------------- 批量 删除 ------------------------
function confirmBatchDelete() {
Modal.confirm({
title: '提示',
content: '确定要删除选中值吗?',
okText: '删除',
okType: 'danger',
onOk() {
batchDelete();
},
cancelText: '取消',
onCancel() {},
});
}
const batchDelete = async () => {
try {
SmartLoading.show();
await dictApi.valueDelete(selectedRowKeyList.value);
message.success('删除成功');
ajaxQuery();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
};
// ----------------------- 弹窗表单操作 ------------------------
const operateModal = ref();
function addOrUpdateValue(rowData) {
operateModal.value.showModal(rowData, dictKeyId.value);
}
defineExpose({
showModal,
});
</script>

View File

@@ -0,0 +1,101 @@
<!--
* 字典 value 弹窗
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-06-08 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal :open="visible" :title="form.dictValueId ? '编辑' : '添加'" ok-text="确认" cancel-text="取消" @ok="onSubmit" @cancel="onClose">
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }" :wrapper-col="{ span: 12 }">
<a-form-item label="编码" name="valueCode">
<a-input v-model:value="form.valueCode" placeholder="请输入编码" />
</a-form-item>
<a-form-item label="名称" name="valueName">
<a-input v-model:value="form.valueName" placeholder="请输入名称" />
</a-form-item>
<a-form-item label="排序" name="sort">
<a-input-number v-model:value="form.sort" :min="0" :max="1000" />
</a-form-item>
<a-form-item label="备注" name="remark">
<textarea v-model="form.remark" style="width: 100%; height: 100px; outline: none"></textarea>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { dictApi } from '/@/api/support/dict-api';
import { smartSentry } from '/@/lib/smart-sentry';
// emit
const emit = defineEmits(['reloadList']);
// 组件
const formRef = ref();
const formDefault = {
dictValueId: undefined,
dictKeyId: undefined,
sort: 1,
valueCode: '',
valueName: '',
remark: '',
};
let form = reactive({ ...formDefault });
const rules = {
valueCode: [{ required: true, message: '请输入编码' }],
valueName: [{ required: true, message: '请输入名称' }],
sort: [{ required: true, message: '请输入排序' }],
};
// 是否展示
const visible = ref(false);
function showModal(rowData, dictKeyId) {
Object.assign(form, formDefault);
if (rowData) {
Object.assign(form, rowData);
}
form.dictKeyId = dictKeyId;
visible.value = true;
}
function onClose() {
Object.assign(form, formDefault);
visible.value = false;
}
function onSubmit() {
formRef.value
.validate()
.then(async () => {
SmartLoading.show();
try {
if (form.dictValueId) {
await dictApi.valueEdit(form);
} else {
await dictApi.valueAdd(form);
}
message.success(`${form.dictKeyId ? '修改' : '添加'}成功`);
emit('reloadList');
onClose();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
})
.catch((error) => {
console.log('error', error);
message.error('参数验证错误,请仔细填写表单数据!');
});
}
defineExpose({
showModal,
});
</script>

View File

@@ -0,0 +1,243 @@
<!--
* 数据 字典
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-06-08 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="关键字" class="smart-query-form-item">
<a-input style="width: 300px" v-model:value="queryForm.searchWord" placeholder="关键字" />
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<SearchOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-card size="small" :bordered="false" :hoverable="true">
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button @click="addOrUpdateKey" v-privilege="'support:dict:add'" type="primary">
<template #icon>
<PlusOutlined />
</template>
新建
</a-button>
<a-button @click="confirmBatchDelete" v-privilege="'support:dict:batchDelete'" type="text" danger :disabled="selectedRowKeyList.length === 0">
<template #icon>
<DeleteOutlined />
</template>
批量删除
</a-button>
<a-button @click="cacheRefresh" v-privilege="'support:dict:refresh'" type="primary">
<template #icon>
<cloud-sync-outlined />
</template>
缓存刷新
</a-button>
</div>
<div class="smart-table-setting-block">
<TableOperator class="smart-margin-bottom5" v-model="columns" :tableId="TABLE_ID_CONST.SUPPORT.DICT" :refresh="ajaxQuery" />
</div>
</a-row>
<a-table
size="small"
:dataSource="tableData"
:columns="columns"
:loading="tableLoading"
rowKey="dictKeyId"
:pagination="false"
bordered
:row-selection="{ selectedRowKeys: selectedRowKeyList, onChange: onSelectChange }"
>
<template #bodyCell="{ record, column }">
<template v-if="column.dataIndex === 'keyCode'">
<a @click="showValueList(record.dictKeyId)">{{ record.keyCode }}</a>
</template>
<template v-else-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button @click="addOrUpdateKey(record)" v-privilege="'support:dict:edit'" type="link">编辑</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `${total}`"
/>
</div>
<DictKeyOperateModal ref="operateModal" @reloadList="ajaxQuery" />
<!-- 值列表 -->
<DictValueModal ref="dictValueModal" />
</a-card>
</template>
<script setup>
import DictKeyOperateModal from './components/dict-key-operate-modal.vue';
import DictValueModal from './components/dict-value-modal.vue';
import { reactive, ref, onMounted } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { dictApi } from '/@/api/support/dict-api';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
const columns = ref([
{
title: 'ID',
width: 90,
dataIndex: 'dictKeyId',
},
{
title: '编码',
dataIndex: 'keyCode',
},
{
title: '名称',
dataIndex: 'keyName',
},
{
title: '备注',
dataIndex: 'remark',
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 50,
},
]);
// ---------------- 查询数据 -----------------
const queryFormState = {
searchWord: '',
pageNum: 1,
pageSize: 10,
};
const queryForm = reactive({ ...queryFormState });
const tableLoading = ref(false);
const selectedRowKeyList = ref([]);
const tableData = ref([]);
const total = ref(0);
const operateModal = ref();
const dictValueModal = ref();
// 显示操作记录弹窗
function showValueList(dictKeyId) {
dictValueModal.value.showModal(dictKeyId);
}
function onSelectChange(selectedRowKeys) {
selectedRowKeyList.value = selectedRowKeys;
}
function resetQuery() {
Object.assign(queryForm, queryFormState);
ajaxQuery();
}
function onSearch() {
queryForm.pageNum = 1;
ajaxQuery();
}
async function ajaxQuery() {
try {
tableLoading.value = true;
let responseModel = await dictApi.keyQuery(queryForm);
const list = responseModel.data.list;
total.value = responseModel.data.total;
tableData.value = list;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
// ---------------- 刷新缓存 -----------------
async function cacheRefresh() {
try {
SmartLoading.show();
await dictApi.cacheRefresh();
message.success('缓存刷新成功');
ajaxQuery();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
// ---------------- 批量 删除 -----------------
function confirmBatchDelete() {
Modal.confirm({
title: '提示',
content: '确定要删除选中Key吗?',
okText: '删除',
okType: 'danger',
onOk() {
batchDelete();
},
cancelText: '取消',
onCancel() {},
});
}
const batchDelete = async () => {
try {
SmartLoading.show();
await dictApi.keyDelete(selectedRowKeyList.value);
message.success('删除成功');
ajaxQuery();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
};
// ---------------- 添加/更新 -----------------
function addOrUpdateKey(rowData) {
operateModal.value.showModal(rowData);
}
onMounted(ajaxQuery);
</script>

View File

@@ -0,0 +1,166 @@
<!--
* 意见反馈
*
* @Author: 1024创新实验室开云
* @Date: 2022-07-21 21:55:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="关键字" class="smart-query-form-item" style="margin-right: 20px">
<a-input style="width: 240px" v-model:value.trim="queryForm.searchWord" placeholder="反馈内容/创建人" />
</a-form-item>
<a-form-item label="创建日期" class="smart-query-form-item" style="margin-right: 20px">
<a-range-picker
v-model:value="chooseTimeRange"
@change="changeCreateDate"
:presets="defaultTimeRanges"
format="YYYY-MM-DD"
style="width: 240px"
/>
</a-form-item>
<a-form-item class="smart-query-form-item">
<a-button-group v-privilege="'feedback:query'">
<a-button type="primary" @click="onSearch">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="onReset">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-card size="small">
<a-table rowKey="feedbackId" :dataSource="tableData" :columns="tableColumns" :pagination="false" :loading="tableLoading" size="small" bordered>
<template #bodyCell="{ text, column }">
<template v-if="column.dataIndex === 'feedbackAttachment'">
<FilePreview :fileList="text" type="picture" />
</template>
<template v-if="column.dataIndex === 'userType'">
<span>{{ $smartEnumPlugin.getDescByValue('USER_TYPE_ENUM', text) }}</span>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="queryList"
@showSizeChange="queryList"
:show-total="(total) => `${total}`"
/>
</div>
</a-card>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { defaultTimeRanges } from '/@/lib/default-time-ranges';
import { feedbackApi } from '/@/api/support/feedback-api';
import FilePreview from '/@/components/support/file-preview/index.vue';
import { smartSentry } from '/@/lib/smart-sentry';
// ----------------------- 表格列 --------------------------------------
const tableColumns = reactive([
{
title: '编号',
dataIndex: 'feedbackId',
width: 80,
},
{
title: '反馈内容',
dataIndex: 'feedbackContent',
},
{
title: '反馈图片',
dataIndex: 'feedbackAttachment',
},
{
title: '反馈人',
dataIndex: 'userName',
width: 100,
},
{
title: '反馈人类型',
dataIndex: 'userType',
width: 100,
},
{
title: '反馈时间',
dataIndex: 'createTime',
width: 150,
},
]);
// ----------------------- 查询参数 ------------------------------------
const defaultQueryForm = {
startDate: undefined,
endDate: undefined,
searchWord: undefined,
pageNum: 1,
pageSize: PAGE_SIZE,
};
const queryForm = reactive({ ...defaultQueryForm });
const tableLoading = ref(false);
const tableData = ref([]);
const total = ref(0);
onMounted(() => {
queryList();
});
async function queryList() {
try {
tableLoading.value = true;
const result = await feedbackApi.queryFeedback(queryForm);
tableData.value = result.data.list;
total.value = result.data.total;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
// 处理选择日期范围
const chooseTimeRange = ref([]);
function changeCreateDate(value, dateString) {
queryForm.startDate = dateString[0];
queryForm.endDate = dateString[1];
}
// 点击查询
function onSearch() {
queryForm.pageNum = 1;
queryList();
}
// 点击重置
function onReset() {
Object.assign(queryForm, defaultQueryForm);
chooseTimeRange.value = [];
queryList();
}
// ----------------------- 分页方法 ------------------------------------
</script>

View File

@@ -0,0 +1,292 @@
<!--
* 文件
*
* @Author: 1024创新实验室-主任-卓大
* @Date: 2020-10-10 22:13:18
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<!---------- 查询表单form begin ----------->
<a-form class="smart-query-form" v-privilege="'support:file:query'">
<a-row class="smart-query-form-row">
<a-form-item label="文件夹类型" class="smart-query-form-item">
<SmartEnumSelect width="150px" v-model:value="queryForm.folderType" enumName="FILE_FOLDER_TYPE_ENUM" placeholder="文件夹类型" />
</a-form-item>
<a-form-item label="文件名" class="smart-query-form-item">
<a-input style="width: 150px" v-model:value="queryForm.fileName" placeholder="文件名" />
</a-form-item>
<a-form-item label="文件Key" class="smart-query-form-item">
<a-input style="width: 150px" v-model:value="queryForm.fileKey" placeholder="文件Key" />
</a-form-item>
<a-form-item label="文件类型" class="smart-query-form-item">
<a-input style="width: 150px" v-model:value="queryForm.fileType" placeholder="文件类型" />
</a-form-item>
<a-form-item label="创建人" class="smart-query-form-item">
<a-input style="width: 150px" v-model:value="queryForm.creatorName" placeholder="创建人" />
</a-form-item>
<a-form-item label="创建时间" class="smart-query-form-item">
<a-range-picker v-model:value="queryForm.createTime" :presets="defaultTimeRanges" style="width: 220px" @change="onChangeCreateTime" />
</a-form-item>
<a-form-item class="smart-query-form-item">
<a-button-group>
<a-button type="primary" @click="queryData">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<SearchOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<!---------- 查询表单form end ----------->
<a-card size="small" :bordered="false" :hoverable="true">
<!---------- 表格操作行 begin ----------->
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button type="primary" @click="showUploadModal">
<template #icon>
<cloud-upload-outlined />
</template>
上传文件
</a-button>
</div>
<div class="smart-table-setting-block">
<TableOperator v-model="columns" :tableId="null" :refresh="queryData" />
</div>
</a-row>
<!---------- 表格操作行 end ----------->
<!---------- 表格 begin ----------->
<a-table
size="small"
:scroll="{ x: 1300 }"
:dataSource="tableData"
:columns="columns"
rowKey="fileId"
bordered
:loading="tableLoading"
:pagination="false"
>
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'folderType'">
<span>{{ $smartEnumPlugin.getDescByValue('FILE_FOLDER_TYPE_ENUM', text) }}</span>
</template>
<template v-if="column.dataIndex === 'creatorUserType'">
<span>{{ $smartEnumPlugin.getDescByValue('USER_TYPE_ENUM', text) }}</span>
</template>
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button @click="view(record)" type="link">查看</a-button>
<a-button @click="download(record)" type="link">下载</a-button>
</div>
</template>
</template>
</a-table>
<!---------- 表格 end ----------->
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="queryData"
@showSizeChange="queryData"
:show-total="(total) => `${total}`"
/>
</div>
<FilePreviewModal ref="filePreviewModalRef" />
<a-modal v-model:open="uploadModalFlag" title="上传文件" @onCancel="hideUploadModal" @ok="hideUploadModal">
<FileUpload
list-type="text"
:maxUploadSize="5"
buttonText="点击上传文件"
:defaultFileList="[]"
:multiple="true"
:folder="FILE_FOLDER_TYPE_ENUM.COMMON.value"
/>
</a-modal>
</a-card>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { fileApi } from '/@/api/support/file-api';
import SmartEnumSelect from '/@/components/framework/smart-enum-select/index.vue';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { defaultTimeRanges } from '/@/lib/default-time-ranges';
import { smartSentry } from '/@/lib/smart-sentry';
import FilePreviewModal from '/@/components/support/file-preview-modal/index.vue';
import FileUpload from '/@/components/support/file-upload/index.vue';
import { FILE_FOLDER_TYPE_ENUM } from '/@/constants/support/file-const';
// ---------------------------- 表格列 ----------------------------
const columns = ref([
{
title: '主键ID',
dataIndex: 'fileId',
ellipsis: true,
width: 70,
},
{
title: '文件夹',
dataIndex: 'folderType',
ellipsis: true,
width: 100,
},
{
title: '文件名称',
dataIndex: 'fileName',
width: 200,
},
{
title: '文件大小',
dataIndex: 'fileSize',
ellipsis: true,
width: 100,
},
{
title: '文件类型',
dataIndex: 'fileType',
ellipsis: true,
width: 80,
},
{
title: '上传人',
dataIndex: 'creatorName',
ellipsis: true,
width: 100,
},
{
title: '人类型',
dataIndex: 'creatorUserType',
ellipsis: true,
width: 100,
},
{
title: '上传时间',
dataIndex: 'createTime',
ellipsis: true,
width: 150,
},
{
title: '操作',
dataIndex: 'action',
width: 120,
fixed: 'right',
},
]);
// ---------------------------- 查询数据表单和方法 ----------------------------
const queryFormState = {
folderType: undefined, //文件夹类型
fileName: undefined, //文件名词
fileKey: undefined, //文件Key
fileType: undefined, //文件类型
creatorName: undefined, //创建人
createTime: [], //创建时间
createTimeBegin: undefined, //创建时间 开始
createTimeEnd: undefined, //创建时间 结束
pageNum: 1,
pageSize: 10,
};
// 查询表单form
const queryForm = reactive({ ...queryFormState });
// 表格加载loading
const tableLoading = ref(false);
// 表格数据
const tableData = ref([]);
// 总数
const total = ref(0);
// 重置查询条件
function resetQuery() {
let pageSize = queryForm.pageSize;
Object.assign(queryForm, queryFormState);
queryForm.pageSize = pageSize;
queryData();
}
// 查询数据
function onSearch() {
queryForm.pageNum = 1;
queryData();
}
async function queryData() {
tableLoading.value = true;
try {
let queryResult = await fileApi.queryPage(queryForm);
for (const file of queryResult.data.list) {
file.fileSize = getFileSize(file.fileSize);
}
tableData.value = queryResult.data.list;
total.value = queryResult.data.total;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
function onChangeCreateTime(dates, dateStrings) {
queryForm.createTimeBegin = dateStrings[0];
queryForm.createTimeEnd = dateStrings[1];
}
function getFileSize(size) {
//把字节转换成正常文件大小
if (!size) return '';
var num = 1024.0; //byte
if (size < num) return size + 'B';
if (size < Math.pow(num, 2)) return (size / num).toFixed(2) + 'KB'; //kb
if (size < Math.pow(num, 3)) return (size / Math.pow(num, 2)).toFixed(2) + 'MB'; //M
if (size < Math.pow(num, 4)) return (size / Math.pow(num, 3)).toFixed(2) + 'G'; //G
return (size / Math.pow(num, 4)).toFixed(2) + 'T'; //T
}
// 查看文件
const filePreviewModalRef = ref();
function view(file) {
filePreviewModalRef.value.showPreview(file);
}
// 下载文件
async function download(file) {
try {
await fileApi.downLoadFile(file.fileKey);
} catch (e) {
smartSentry.captureError(e);
}
}
onMounted(queryData);
// ------------- 上传文件 --------------------
const uploadModalFlag = ref(false);
function showUploadModal() {
uploadModalFlag.value = true;
}
function hideUploadModal() {
uploadModalFlag.value = false;
queryData();
}
</script>

View File

@@ -0,0 +1,174 @@
<!--
* 心跳记录
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-06-02 20:23:08
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-card size="small" :bordered="false" :hoverable="true">
<a-alert>
<template v-slot:message>
<h4>Smart-Heart-Beat 心跳服务介绍</h4>
</template>
<template v-slot:description>
<pre>
简介Smart-Heart-Beat 是心跳服务用于监测Java应用的状态等其他信息
原理Java后端会在项目启动的时候开启一个线程每隔一段时间将该应用的IP进程号更新到数据库t_heart_beat_record表中
用途
1在各个环境无论开发测试生产能统一看到所有启动的服务列表
2检测Java应用是否存活
3当某些业务只允许有一个服务启动的时候用于排查是否别人也启动的服务
4 强烈推荐</pre
>
</template>
</a-alert>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="关键字" class="smart-query-form-item">
<a-input style="width: 300px" v-model:value="queryForm.keywords" placeholder="关键字" />
</a-form-item>
<a-form-item label="心跳时间" class="smart-query-form-item">
<a-range-picker @change="changeCreateDate" v-model:value="createDateRange" :presets="defaultChooseTimeRange" style="width: 240px" />
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<SearchOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-row justify="end">
<TableOperator class="smart-margin-bottom5" v-model="columns" :tableId="TABLE_ID_CONST.SUPPORT.HEART_BEAT" :refresh="ajaxQuery" />
</a-row>
<a-table
size="small"
bordered
:loading="tableLoading"
class="smart-margin-top10"
:dataSource="tableData"
:columns="columns"
rowKey="goodsId"
:pagination="false"
/>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `${total}`"
/>
</div>
</a-card>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { heartBeatApi } from '/@/api/support/heart-beat-api';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { defaultTimeRanges } from '/@/lib/default-time-ranges';
import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
//------------------------ 时间选择 ---------------------
const defaultChooseTimeRange = defaultTimeRanges;
const createDateRange = ref([]);
// 时间变动
function changeCreateDate(dates, dateStrings) {
queryForm.startDate = dateStrings[0];
queryForm.endDate = dateStrings[1];
}
//------------------------ 表格渲染 ---------------------
const columns = ref([
{
title: '项目路径',
dataIndex: 'projectPath',
ellipsis: true,
},
{
title: '服务器ip',
dataIndex: 'serverIp',
ellipsis: true,
},
{
title: '进程号',
dataIndex: 'processNo',
width: 100,
},
{
title: '进程开启时间',
dataIndex: 'processStartTime',
width: 150,
},
{
title: '心跳当前时间',
dataIndex: 'heartBeatTime',
width: 150,
},
]);
const queryFormState = {
pageNum: 1,
pageSize: 10,
keywords: '',
startDate: undefined,
endDate: undefined,
};
const queryForm = reactive({ ...queryFormState });
const tableLoading = ref(false);
const tableData = ref([]);
const total = ref(0);
function resetQuery() {
Object.assign(queryForm, queryFormState);
createDateRange.value = [];
ajaxQuery();
}
function onSearch() {
queryForm.pageNum = 1;
ajaxQuery();
}
async function ajaxQuery() {
try {
tableLoading.value = true;
let responseModel = await heartBeatApi.queryList(queryForm);
const list = responseModel.data.list;
total.value = responseModel.data.total;
tableData.value = list;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
onMounted(ajaxQuery);
</script>

View File

@@ -0,0 +1,132 @@
<!--
* 目录表单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-07-21 21:55:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal v-model:open="visible" :title="formState.helpDocCatalogId ? '编辑目录' : '添加目录'" @ok="handleOk" destroyOnClose>
<a-form ref="formRef" :model="formState" :rules="rules" layout="vertical">
<a-form-item label="上级目录" name="parentId" v-if="formState.parentId !== 0">
<HelpDocCatalogTreeSelect ref="helpDocCatalogTreeSelect" v-model:value="formState.parentId" :defaultValueFlag="false" width="100%" />
</a-form-item>
<a-form-item label="目录名称" name="name">
<a-input v-model:value.trim="formState.name" placeholder="请输入目录名称" />
</a-form-item>
<a-form-item label="目录排序 (值越小越靠前!)" name="sort">
<a-input-number style="width: 100%" v-model:value="formState.sort" :min="0" placeholder="请输入目录名称" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import message from 'ant-design-vue/lib/message';
import { reactive, ref } from 'vue';
import { helpDocCatalogApi } from '/@/api/support/help-doc-catalog-api';
import HelpDocCatalogTreeSelect from './help-doc-catalog-tree-select.vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { smartSentry } from '/@/lib/smart-sentry';
// ----------------------- 对外暴漏 ---------------------
defineExpose({
showModal,
});
// ----------------------- modal 的显示与隐藏 ---------------------
const emits = defineEmits(['refresh']);
const visible = ref(false);
function showModal(data) {
visible.value = true;
updateFormData(data);
}
function closeModal() {
visible.value = false;
resetFormData();
}
// ----------------------- form 表单操作 ---------------------
const formRef = ref();
const helpDocCatalogTreeSelect = ref();
const defaultHelpDocCatalogForm = {
helpDocCatalogId: undefined,
name: undefined,
parentId: undefined,
sort: 0,
};
const employeeSelect = ref();
let formState = reactive({
...defaultHelpDocCatalogForm,
});
// 表单校验规则
const rules = {
parentId: [{ required: true, message: '上级目录不能为空' }],
name: [
{ required: true, message: '目录名称不能为空' },
{ max: 50, message: '目录名称不能大于20个字符', trigger: 'blur' },
],
};
// 更新表单数据
function updateFormData(data) {
Object.assign(formState, defaultHelpDocCatalogForm);
if (data) {
Object.assign(formState, data);
}
visible.value = true;
}
// 重置表单数据
function resetFormData() {
Object.assign(formState, defaultHelpDocCatalogForm);
}
async function handleOk() {
try {
await formRef.value.validate();
if (formState.helpDocCatalogId) {
updateHelpDocCatalog();
} else {
addHelpDocCatalog();
}
} catch (error) {
message.error('参数验证错误,请仔细填写表单数据!');
}
}
// ----------------------- form 表单 ajax 操作 ---------------------
//添加目录ajax请求
async function addHelpDocCatalog() {
SmartLoading.show();
try {
await helpDocCatalogApi.add(formState);
emits('refresh');
closeModal();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
}
//更新目录ajax请求
async function updateHelpDocCatalog() {
SmartLoading.show();
try {
if (formState.parentId === formState.helpDocCatalogId) {
message.warning('上级菜单不能为自己');
return;
}
await helpDocCatalogApi.update(formState);
emits('refresh');
closeModal();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
}
</script>

View File

@@ -0,0 +1,89 @@
<!--
* 目录下拉框
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-07-21 21:55:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-tree-select
:value="props.value"
:treeData="treeData"
:fieldNames="{ label: 'name', key: 'helpDocCatalogId', value: 'helpDocCatalogId' }"
show-search
style="width: 100%"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
placeholder="请选择目录"
allow-clear
tree-default-expand-all
:multiple="props.multiple"
@change="treeSelectChange"
/>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import _ from 'lodash';
import { helpDocCatalogApi } from '/@/api/support/help-doc-catalog-api';
const props = defineProps({
// 绑定值
value: Number,
// 单选多选
multiple: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['update:value']);
let treeData = ref([]);
onMounted(queryCatalogTree);
// 外部调用初始化
async function queryCatalogTree() {
let res = await helpDocCatalogApi.getAll();
let children = buildHelpDocCatalogTree(res.data, 0);
treeData.value = children;
}
// 构建目录树
function buildHelpDocCatalogTree(data, parentId) {
let children = data.filter((e) => e.parentId === parentId) || [];
children = _.sortBy(children, (e) => e.sort);
children.forEach((e) => {
e.children = buildHelpDocCatalogTree(data, e.helpDocCatalogId);
});
updateHelpDocCatalogPreIdAndNextId(children);
return children;
}
// 更新树的前置id和后置id
function updateHelpDocCatalogPreIdAndNextId(data) {
for (let index = 0; index < data.length; index++) {
if (index === 0) {
data[index].nextId = data.length > 1 ? data[1].helpDocCatalogId : undefined;
continue;
}
if (index === data.length - 1) {
data[index].preId = data[index - 1].helpDocCatalogId;
data[index].nextId = undefined;
continue;
}
data[index].preId = data[index - 1].helpDocCatalogId;
data[index].nextId = data[index + 1].helpDocCatalogId;
}
}
function treeSelectChange(e) {
emit('update:value', e);
}
// ----------------------- 以下是暴露的方法内容 ------------------------
defineExpose({
queryCatalogTree,
});
</script>

View File

@@ -0,0 +1,353 @@
<!--
* 目录树
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-07-21 21:55:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-card class="tree-container" size="small">
<a-row>
<a-input v-model:value.trim="keywords" placeholder="请输入目录名称" />
</a-row>
<a-row class="sort-flag-row" v-if="props.showMenu">
<span>
排序
<template v-if="showSortFlag"> 越小越靠前 </template>
<a-switch v-model:checked="showSortFlag" />
</span>
<a-button type="primary" @click="addTop" size="small" v-privilege="'support:helpDocCatalog:addCategory'">新建</a-button>
</a-row>
<a-tree
v-if="!_.isEmpty(helpDocCatalogTreeData)"
v-model:selectedKeys="selectedKeys"
v-model:checkedKeys="checkedKeys"
class="tree"
:treeData="helpDocCatalogTreeData"
:fieldNames="{ title: 'name', key: 'helpDocCatalogId', value: 'helpDocCatalogId' }"
style="width: 100%; overflow-x: auto"
:style="[!height ? '' : { height: `${height}px`, overflowY: 'auto' }]"
:showLine="!props.checkable"
:checkable="props.checkable"
:checkStrictly="props.checkStrictly"
:selectable="!props.checkable"
:defaultExpandAll="true"
@select="treeSelectChange"
>
<template #title="item">
<a-popover placement="right" v-if="props.showMenu">
<template #content>
<div style="display: flex; flex-direction: column">
<a-button type="text" @click="addHelpDocCatalog(item.dataRef)" v-privilege="'support:helpDocCatalog:addCategory'">添加下级</a-button>
<a-button type="text" @click="updateHelpDocCatalog(item.dataRef)" v-privilege="'support:helpDocCatalog:edit'">修改</a-button>
<a-button
type="text"
v-if="item.helpDocCatalogId !== topHelpDocCatalogId"
@click="deleteHelpDocCatalog(item.helpDocCatalogId)"
v-privilege="'support:helpDocCatalog:delete'"
>删除</a-button
>
</div>
</template>
{{ item.name }}
<!--显示排序字段-->
<template v-if="showSortFlag">
<span class="sort-span">({{ item.sort }})</span>
</template>
</a-popover>
<div v-else>{{ item.name }}</div>
</template>
</a-tree>
<div class="no-data" v-else>暂无结果</div>
<!-- 添加编辑目录弹窗 -->
<HelpDocCatalogFormModal ref="helpDocCatalogFormModal" @refresh="refresh" />
</a-card>
</template>
<script setup>
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { ref } from 'vue';
import { onUnmounted, watch } from 'vue';
import { Modal } from 'ant-design-vue';
import _ from 'lodash';
import { createVNode, onMounted } from 'vue';
import HelpDocCatalogFormModal from './help-doc-catalog-form-modal.vue';
import { helpDocCatalogApi } from '/@/api/support/help-doc-catalog-api';
import { SmartLoading } from '/@/components/framework/smart-loading';
import helpDocCatalogEmitter from '../help-doc-mitt';
import { smartSentry } from '/@/lib/smart-sentry';
const HELP_DOC_CATALOG_PARENT_ID = 0;
// ----------------------- 组件参数 ---------------------
const props = defineProps({
// 是否可以选中
checkable: {
type: Boolean,
default: false,
},
// 父子节点选中状态不再关联
checkStrictly: {
type: Boolean,
default: false,
},
// 树高度 超出出滚动条
height: Number,
// 显示菜单
showMenu: {
type: Boolean,
default: true,
},
});
// ----------------------- 目录树的展示 ---------------------
const topHelpDocCatalogId = ref();
// 所有目录列表
const helpDocCatalogList = ref([]);
// 目录树形数据
const helpDocCatalogTreeData = ref([]);
// 存放目录id和目录用于查找
const idInfoMap = ref(new Map());
// 是否显示排序字段
const showSortFlag = ref(false);
onMounted(() => {
queryHelpDocCatalogTree();
});
// 刷新
async function refresh() {
await queryHelpDocCatalogTree();
if (currentSelectedHelpDocCatalogId.value) {
selectTree(currentSelectedHelpDocCatalogId.value);
}
}
// 查询目录列表并构建 目录树
async function queryHelpDocCatalogTree() {
let res = await helpDocCatalogApi.getAll();
let data = res.data;
helpDocCatalogList.value = data;
helpDocCatalogTreeData.value = buildHelpDocCatalogTree(data, HELP_DOC_CATALOG_PARENT_ID);
data.forEach((e) => {
idInfoMap.value.set(e.helpDocCatalogId, e);
});
// 默认显示 最顶级ID为列表中返回的第一条数据的ID
if (!_.isEmpty(helpDocCatalogTreeData.value) && helpDocCatalogTreeData.value.length > 0) {
topHelpDocCatalogId.value = helpDocCatalogTreeData.value[0].helpDocCatalogId;
selectTree(helpDocCatalogTreeData.value[0].helpDocCatalogId);
}
}
// 构建目录树
function buildHelpDocCatalogTree(data, parentId) {
let children = data.filter((e) => e.parentId === parentId) || [];
children = _.sortBy(children, (e) => e.sort);
children.forEach((e) => {
e.children = buildHelpDocCatalogTree(data, e.helpDocCatalogId);
});
updateHelpDocCatalogPreIdAndNextId(children);
return children;
}
// 更新树的前置id和后置id
function updateHelpDocCatalogPreIdAndNextId(data) {
for (let index = 0; index < data.length; index++) {
if (index === 0) {
data[index].nextId = data.length > 1 ? data[1].helpDocCatalogId : undefined;
continue;
}
if (index === data.length - 1) {
data[index].preId = data[index - 1].helpDocCatalogId;
data[index].nextId = undefined;
continue;
}
data[index].preId = data[index - 1].helpDocCatalogId;
data[index].nextId = data[index + 1].helpDocCatalogId;
}
}
// ----------------------- 树的选中 ---------------------
const selectedKeys = ref([]);
const checkedKeys = ref([]);
const breadcrumb = ref([]);
const currentSelectedHelpDocCatalogId = ref();
const selectedHelpDocCatalogChildren = ref([]);
helpDocCatalogEmitter.on('selectTree', selectTree);
function selectTree(id) {
selectedKeys.value = [id];
treeSelectChange(selectedKeys.value);
}
function treeSelectChange(idList) {
if (_.isEmpty(idList)) {
breadcrumb.value = [];
selectedHelpDocCatalogChildren.value = [];
return;
}
let id = idList[0];
selectedHelpDocCatalogChildren.value = helpDocCatalogList.value.filter((e) => e.parentId === id);
let filterHelpDocCatalogList = [];
recursionFilterHelpDocCatalog(filterHelpDocCatalogList, id, true);
breadcrumb.value = filterHelpDocCatalogList.map((e) => e.name);
}
// ----------------------- 筛选 ---------------------
const keywords = ref('');
watch(
() => keywords.value,
() => {
onSearch();
}
);
// 筛选
function onSearch() {
if (!keywords.value) {
helpDocCatalogTreeData.value = buildHelpDocCatalogTree(helpDocCatalogList.value, HELP_DOC_CATALOG_PARENT_ID);
return;
}
let originData = helpDocCatalogList.value.concat();
if (!originData) {
return;
}
// 筛选出名称符合的目录
let filterDepartmenet = originData.filter((e) => e.name.indexOf(keywords.value) > -1);
let filterHelpDocCatalogList = [];
// 循环筛选出的目录 构建目录树
filterDepartmenet.forEach((e) => {
recursionFilterHelpDocCatalog(filterHelpDocCatalogList, e.helpDocCatalogId, false);
});
helpDocCatalogTreeData.value = buildHelpDocCatalogTree(filterHelpDocCatalogList, HELP_DOC_CATALOG_PARENT_ID);
}
// 根据ID递归筛选目录
function recursionFilterHelpDocCatalog(resList, id, unshift) {
let info = idInfoMap.value.get(id);
if (!info || resList.some((e) => e.helpDocCatalogId === id)) {
return;
}
if (unshift) {
resList.unshift(info);
} else {
resList.push(info);
}
if (info.parentId && info.parentId !== 0) {
recursionFilterHelpDocCatalog(resList, info.parentId, unshift);
}
}
// ----------------------- 表单操作:添加目录/修改目录/删除目录/上下移动 ---------------------
const helpDocCatalogFormModal = ref();
// 添加
function addHelpDocCatalog(e) {
let data = {
helpDocCatalogId: 0,
name: '',
parentId: e.helpDocCatalogId,
};
currentSelectedHelpDocCatalogId.value = e.helpDocCatalogId;
helpDocCatalogFormModal.value.showModal(data);
}
// 添加
function addTop() {
let data = {
helpDocCatalogId: 0,
name: '',
parentId: 0,
};
helpDocCatalogFormModal.value.showModal(data);
}
// 编辑
function updateHelpDocCatalog(e) {
currentSelectedHelpDocCatalogId.value = e.helpDocCatalogId;
helpDocCatalogFormModal.value.showModal(e);
}
// 删除
function deleteHelpDocCatalog(id) {
Modal.confirm({
title: '提醒',
icon: createVNode(ExclamationCircleOutlined),
content: '确定要删除该目录吗?',
okText: '删除',
okType: 'danger',
async onOk() {
SmartLoading.show();
try {
// 若删除的是当前的目录 先找到上级目录
let selectedKey = null;
if (!_.isEmpty(selectedKeys.value)) {
selectedKey = selectedKeys.value[0];
if (selectedKey === id) {
let selectInfo = helpDocCatalogList.value.find((e) => e.helpDocCatalogId === id);
if (selectInfo && selectInfo.parentId) {
selectedKey = selectInfo.parentId;
}
}
}
await helpDocCatalogApi.delete(id);
await queryHelpDocCatalogTree();
// 刷新选中目录
if (selectedKey) {
selectTree(selectedKey);
}
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
},
cancelText: '取消',
onCancel() {},
});
}
onUnmounted(() => {
helpDocCatalogEmitter.all.clear();
});
// ----------------------- 以下是暴露的方法内容 ----------------------------
defineExpose({
queryHelpDocCatalogTree,
selectedHelpDocCatalogChildren,
breadcrumb,
selectedKeys,
checkedKeys,
keywords,
});
</script>
<style scoped lang="less">
.tree-container {
height: 100%;
.tree {
height: 618px;
margin-top: 10px;
overflow-x: hidden;
}
.sort-flag-row {
display: flex;
justify-content: space-between;
margin-top: 10px;
margin-bottom: 10px;
}
.sort-span {
margin-left: 5px;
}
.no-data {
margin: 10px;
}
}
</style>

View File

@@ -0,0 +1,224 @@
<!--
* 帮助文档表单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-07-21 21:55:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-drawer
:title="formData.helpDocId ? '编辑系统手册' : '新建系统手册'"
:open="visibleFlag"
:width="1000"
:footerStyle="{ textAlign: 'right' }"
@close="onClose"
:destroyOnClose="true"
>
<a-form ref="formRef" :model="formData" :rules="formRules" :label-col="{ span: 3 }" :wrapper-col="{ span: 20 }">
<a-form-item label="标题" name="title">
<a-input v-model:value="formData.title" placeholder="请输入标题" />
</a-form-item>
<a-form-item label="目录" name="helpDocCatalogId">
<HelpDocCatalogTreeSelect v-model:value="formData.helpDocCatalogId" style="width: 100%" />
</a-form-item>
<a-form-item label="作者" name="author">
<a-input v-model:value="formData.author" placeholder="请输入作者" />
</a-form-item>
<a-form-item label="排序" name="sort">
<a-input-number v-model:value="formData.sort" placeholder="值越小越靠前" />值越小越靠前
</a-form-item>
<a-form-item label="是否首页显示">
<a-radio-group v-model:value="relateHomeFlag" button-style="solid">
<a-radio-button :value="true">首页显示</a-radio-button>
<a-radio-button :value="false">首页不用显示</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="关联菜单" v-if="!relateHomeFlag">
<MenuTreeSelect v-model:value="formData.relationIdList" ref="menuTreeSelect" />
</a-form-item>
<a-form-item label="公告内容" name="contentHtml">
<Wangeditor ref="contentRef" :modelValue="formData.contentHtml" :height="300" />
</a-form-item>
<a-form-item label="附件">
<Upload
:defaultFileList="defaultFileList"
:maxUploadSize="10"
:folder="FILE_FOLDER_TYPE_ENUM.HELP_DOC.value"
buttonText="上传附件"
listType="text"
extraMsg="最多上传10个附件"
@change="changeAttachment"
/>
</a-form-item>
</a-form>
<template #footer>
<a-space>
<a-button @click="onClose">取消</a-button>
<a-button type="primary" @click="onSubmit">保存</a-button>
</a-space>
</template>
</a-drawer>
</template>
<script setup>
import { nextTick, reactive, ref } from 'vue';
import { message } from 'ant-design-vue';
import _ from 'lodash';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { FILE_FOLDER_TYPE_ENUM } from '/@/constants/support/file-const';
import { helpDocApi } from '/@/api/support/help-doc-api';
import Wangeditor from '/@/components/framework/wangeditor/index.vue';
import Upload from '/@/components/support/file-upload/index.vue';
import HelpDocCatalogTreeSelect from './help-doc-catalog-tree-select.vue';
import MenuTreeSelect from '/@/components/system/menu-tree-select/index.vue';
import { smartSentry } from '/@/lib/smart-sentry';
const emits = defineEmits(['reloadList']);
// ------------------ 显示,关闭 ------------------
// 显示
const visibleFlag = ref(false);
function showModal(helpDocId) {
Object.assign(formData, defaultFormData);
defaultFileList.value = [];
if (helpDocId) {
getDetail(helpDocId);
}
visibleFlag.value = true;
nextTick(() => {
formRef.value.clearValidate();
});
}
// 关闭
function onClose() {
visibleFlag.value = false;
}
// ------------------ 表单 ------------------
const formRef = ref();
const contentRef = ref();
const relateHomeFlag = ref(false);
const defaultFormData = {
helpDocId: undefined,
helpDocCatalogId: undefined,
title: undefined, // 标题
author: undefined, // 作者
sort: 0, // 排序
attachment: [], // 附件
relationIdList: [], //关联id集合
contentHtml: '', // html内容
contentText: '', // 纯文本内容
};
const formData = reactive({ ...defaultFormData });
const formRules = {
title: [{ required: true, message: '请输入' }],
helpDocCatalogId: [{ required: true, message: '请选择目录' }],
author: [{ required: true, message: '请输入作者' }],
sort: [{ required: true, message: '请输入排序' }],
contentHtml: [{ required: true, message: '请输入内容' }],
};
// 查询详情
async function getDetail(helpDocId) {
try {
SmartLoading.show();
const result = await helpDocApi.getDetail(helpDocId);
const attachment = result.data.attachment;
if (!_.isEmpty(attachment)) {
defaultFileList.value = attachment;
} else {
defaultFileList.value = [];
}
Object.assign(formData, result.data);
formData.relationIdList = result.data.relationList ? result.data.relationList.map((e) => e.relationId) : [];
if (formData.relationIdList.length === 1 && formData.relationIdList[0].relationId === 0) {
relateHomeFlag.value = true;
} else {
relateHomeFlag.value = false;
}
} catch (err) {
smartSentry.captureError(err);
} finally {
SmartLoading.hide();
}
}
// 点击确定,验证表单
async function onSubmit() {
try {
formData.contentHtml = contentRef.value.getHtml();
formData.contentText = contentRef.value.getText();
await formRef.value.validateFields();
await save();
} catch (err) {
message.error('参数验证错误,请仔细填写表单数据!');
}
}
// 新建、编辑API
const menuTreeSelect = ref();
async function save() {
try {
SmartLoading.show();
let param = _.cloneDeep(formData);
// 首页显示的话为0
if (relateHomeFlag.value) {
param.relationList = [
{
relationName: '首页',
relationId: 0,
},
];
} else {
let relationList = menuTreeSelect.value.getMenuListByIdList(formData.relationIdList);
param.relationList = relationList.map((e) => Object.assign({}, { relationId: e.menuId, relationName: e.menuName }));
}
if (param.helpDocId) {
await helpDocApi.update(param);
} else {
await helpDocApi.add(param);
}
message.success('保存成功');
emits('reloadList');
onClose();
} catch (err) {
smartSentry.captureError(err);
} finally {
SmartLoading.hide();
}
}
// ----------------------- 上传附件 ----------------------------
// 已上传的附件列表
const defaultFileList = ref([]);
function changeAttachment(fileList) {
defaultFileList.value = fileList;
formData.attachment = _.isEmpty(fileList) ? [] : fileList;
}
// ----------------------- 以下是暴露的方法内容 ------------------------
defineExpose({
showModal,
});
</script>
<style lang="less" scoped>
.visible-list {
display: flex;
flex-wrap: wrap;
.visible-item {
padding-top: 8px;
}
}
</style>

View File

@@ -0,0 +1,268 @@
<!--
* 帮助文档 列表
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-07-21 21:55:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-form class="smart-query-form" v-privilege="'support:helpDoc:query'">
<a-row class="smart-query-form-row">
<a-form-item label="关键字" class="smart-query-form-item">
<a-input style="width: 300px" v-model:value="queryForm.keywords" placeholder="标题、作者" />
</a-form-item>
<a-form-item label="创建时间" class="smart-query-form-item">
<a-range-picker :presets="defaultTimeRanges" v-model:value="createDate" @change="createDateChange" style="width: 220px" />
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="onReload">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-card size="small" :bordered="false">
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button type="primary" @click="addOrUpdate()" v-privilege="'support:helpDoc:add'">
<template #icon>
<PlusOutlined />
</template>
新建
</a-button>
</div>
<div class="smart-table-setting-block">
<TableOperator v-model="tableColumns" :tableId="TABLE_ID_CONST.SUPPORT.HELP_DOC" :refresh="queryHelpDocList" />
</div>
</a-row>
<a-table
rowKey="helpDocId"
:columns="tableColumns"
:scroll="{ x: 1000 }"
:dataSource="tableData"
:pagination="false"
:loading="tableLoading"
size="small"
bordered
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'title'">
<router-link tag="a" target="_blank" :to="{ path: '/help-doc/detail', query: { helpDocId: record.helpDocId } }">{{
record.title
}}</router-link>
</template>
<template v-else-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button type="link" @click="addOrUpdate(record.helpDocId)" v-privilege="'support:helpDoc:update'">编辑</a-button>
<a-button type="link" danger @click="onDelete(record.helpDocId)" v-privilege="'support:helpDoc:delete'">删除</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="queryHelpDocList"
@showSizeChange="queryHelpDocList"
:show-total="(total) => `${total}`"
/>
</div>
</a-card>
<HelpDocFormDrawer ref="helpDocFormDrawerRef" @reloadList="queryHelpDocList" />
</template>
<script setup>
import { message, Modal } from 'ant-design-vue';
import { onMounted, reactive, ref, watch } from 'vue';
import HelpDocFormDrawer from './help-doc-form-drawer.vue';
import { helpDocApi } from '/@/api/support/help-doc-api';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { defaultTimeRanges } from '/@/lib/default-time-ranges';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
const props = defineProps({
// 目录id
helpDocCatalogId: Number,
});
const queryFormState = {
helpDocCatalogId: props.helpDocCatalogId, //目录
keywords: '', //标题、作者
createTimeBegin: null, //创建-开始时间
createTimeEnd: null, //创建-截止时间
pageNum: 1,
pageSize: PAGE_SIZE,
};
const queryForm = reactive({ ...queryFormState });
const tableColumns = ref([
{
title: `标题`,
dataIndex: 'title',
ellipsis: true,
},
{
title: '目录',
dataIndex: 'helpDocCatalogName',
width: 120,
ellipsis: true,
},
{
title: `作者`,
dataIndex: 'author',
width: 110,
ellipsis: true,
},
{
title: '排序',
dataIndex: 'sort',
width: 90,
},
{
title: '页面浏览量',
dataIndex: 'pageViewCount',
width: 90,
},
{
title: '用户浏览量',
dataIndex: 'userViewCount',
width: 90,
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 150,
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 90,
},
]);
// ------------------ 查询相关 ------------------
const tableData = ref([]);
const total = ref(0);
const tableLoading = ref(false);
onMounted(() => {
queryHelpDocList();
});
// 查询列表
async function queryHelpDocList() {
try {
tableLoading.value = true;
const result = await helpDocApi.query(queryForm);
tableData.value = result.data.list;
total.value = result.data.total;
} catch (err) {
smartSentry.captureError(err);
} finally {
tableLoading.value = false;
}
}
// 点击查询
function onSearch() {
queryForm.pageNum = 1;
queryHelpDocList();
}
// 点击重置
function onReload() {
Object.assign(queryForm, queryFormState);
publishDate.value = [];
createDate.value = [];
queryHelpDocList();
}
// 发布日期选择
const publishDate = ref([]);
function publishDateChange(dates, dateStrings) {
queryForm.publishTimeBegin = dateStrings[0];
queryForm.publishTimeEnd = dateStrings[1];
}
// 创建日期选择
const createDate = ref([]);
function createDateChange(dates, dateStrings) {
queryForm.createTimeBegin = dateStrings[0];
queryForm.createTimeEnd = dateStrings[1];
}
// ------------------ 新建、编辑 ------------------
// 新建、编辑
const helpDocFormDrawerRef = ref();
function addOrUpdate(helpDocId) {
helpDocFormDrawerRef.value.showModal(helpDocId);
}
// ------------------ 删除 ------------------
// 删除
function onDelete(helpDocId) {
Modal.confirm({
title: '提示',
content: '确认删除此数据吗?',
onOk() {
deleteHelpDoc(helpDocId);
},
});
}
// 删除API
async function deleteHelpDoc(helpDocId) {
try {
tableLoading.value = true;
await helpDocApi.delete(helpDocId);
message.success('删除成功');
queryHelpDocList();
} catch (err) {
smartSentry.captureError(err);
} finally {
tableLoading.value = false;
}
}
watch(
() => props.helpDocCatalogId,
() => {
queryForm.helpDocCatalogId = props.helpDocCatalogId;
onSearch();
},
{ immediate: true }
);
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,55 @@
<!--
* 帮助文档 管理
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-07-21 21:55:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div class="height100">
<a-row :gutter="16" class="height100">
<a-col :span="6">
<HelpDocCatalogTree ref="helpDocCatalogTreeRef" />
</a-col>
<a-col :span="18" class="height100">
<div class="help-doc-box height100">
<HelpDocList :helpDocCatalogId="selectedHelpDocCatalogId" />
</div>
</a-col>
</a-row>
</div>
</template>
<script setup>
import _ from 'lodash';
import { computed, ref } from 'vue';
import HelpDocCatalogTree from './components/help-doc-catalog-tree.vue';
import HelpDocList from './components/help-doc-list.vue';
const helpDocCatalogTreeRef = ref();
// 当前选中的目录id
const selectedHelpDocCatalogId = computed(() => {
if (helpDocCatalogTreeRef.value) {
let selectedKeys = helpDocCatalogTreeRef.value.selectedKeys;
return _.isEmpty(selectedKeys) ? null : selectedKeys[0];
}
return null;
});
</script>
<style scoped lang="less">
.height100 {
height: 100%;
}
.help-doc-box {
display: flex;
flex-direction: column;
.employee {
flex-grow: 2;
margin-top: 10px;
}
}
</style>

View File

@@ -0,0 +1,11 @@
/*
* 帮助文档 event bus
*
* @Author: 1024创新实验室-主任:卓大
* @Date: 2022-09-12 18:06:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*/
import mitt from 'mitt';
export default mitt();

View File

@@ -0,0 +1,168 @@
<!--
* 查看记录
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-07-21 21:55:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="关键字" class="smart-query-form-item" style="width: 280px">
<a-input v-model:value="queryForm.keywords" placeholder="姓名/IP/设备" />
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-table rowKey="employeeId" :columns="tableColumns" :dataSource="tableData" :pagination="false" :loading="tableLoading" size="small" bordered>
<template #bodyCell="{ column, record, text }">
<template v-if="column.dataIndex === 'firstIp'"> {{ text }} ({{ record.firstDevice }}) </template>
<template v-if="column.dataIndex === 'lastIp'"> {{ text }} ({{ record.lastDevice }}) </template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="queryViewRecord"
@showSizeChange="queryViewRecord"
:show-total="(total) => `${total}`"
/>
</div>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { helpDocApi } from '/@/api/support/help-doc-api';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import uaparser from 'ua-parser-js';
import { smartSentry } from '/@/lib/smart-sentry';
const props = defineProps({
helpDocId: {
type: [Number, String],
},
});
defineExpose({
onSearch,
});
const tableColumns = [
{
title: '用户名',
dataIndex: 'userName',
},
{
title: '查看次数',
dataIndex: 'pageViewCount',
with: 100,
},
{
title: '首次查看设备',
dataIndex: 'firstIp',
},
{
title: '首次查看时间',
dataIndex: 'createTime',
with: 120,
},
{
title: '最后一次查看设备',
dataIndex: 'lastIp',
},
{
title: '最后一次查看时间',
dataIndex: 'updateTime',
with: 120,
},
];
const tableData = ref([]);
const total = ref(0);
const tableLoading = ref(false);
const defaultQueryForm = {
helpDocId: props.helpDocId,
keywords: '',
pageNum: 1,
pageSize: PAGE_SIZE,
};
const queryForm = reactive({ ...defaultQueryForm });
function buildDeviceInfo(userAgent) {
if (!userAgent) {
return '';
}
let ua = uaparser(userAgent);
let browser = ua.browser.name;
let os = ua.os.name;
return browser + '/' + os + '/' + (ua.device.vendor ? ua.device.vendor + ua.device.model : '');
}
async function queryViewRecord() {
try {
tableLoading.value = true;
const result = await helpDocApi.queryViewRecord(queryForm);
for (const e of result.data.list) {
e.firstDevice = buildDeviceInfo(e.firstUserAgent);
e.lastDevice = buildDeviceInfo(e.lastUserAgent);
}
tableData.value = result.data.list;
total.value = result.data.total;
} catch (err) {
smartSentry.captureError(err);
} finally {
tableLoading.value = false;
}
}
// 点击查询
function onSearch() {
queryForm.pageNum = 1;
queryViewRecord();
}
// 点击重置
function resetQuery() {
Object.assign(queryForm, defaultQueryForm);
queryViewRecord();
}
</script>
<style lang="less" scoped>
.ant-table.ant-table-small .ant-table-title,
.ant-table.ant-table-small .ant-table-footer,
.ant-table.ant-table-small .ant-table-tbody > tr > td,
.ant-table.ant-table-small tfoot > tr > th,
.ant-table.ant-table-small tfoot > tr > td {
padding: 0px 3px !important;
line-height: 28px;
}
</style>

View File

@@ -0,0 +1,328 @@
<!--
* 帮助文档详情
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-07-21 21:55:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-card size="small" :bordered="false">
<div v-if="helpDocDetail">
<div class="content-header">
<!--startprint-->
<div class="content-header-title">
{{ helpDocDetail.title }}
</div>
<div class="content-header-info">
<span>阅读量{{ helpDocDetail.pageViewCount }}</span>
<span v-show="helpDocDetail.author">作者{{ helpDocDetail.author }}</span>
<span>发布于{{ helpDocDetail.createTime }}</span>
<span>修改于{{ helpDocDetail.updateTime }}</span>
<span @click="print">打印本页</span>
</div>
</div>
<div class="content-html" v-html="helpDocDetail.contentHtml"></div>
<!--endprint-->
</div>
<a-divider v-if="helpDocDetail.attachment && helpDocDetail.attachment.length > 0" />
<div v-if="helpDocDetail.attachment && helpDocDetail.attachment.length > 0">附件:<FilePreview :fileList="helpDocDetail.attachment" /></div>
</a-card>
<a-card title="阅读记录" size="small" class="smart-margin-top10" :bordered="false">
<HelpDocViewRecordList ref="helpDocViewRecordListRef" :helpDocId="route.query.helpDocId" />
</a-card>
<!-- 预览附件 -->
<FilePreview ref="filePreviewRef" />
</template>
<script setup>
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import HelpDocViewRecordList from './components/help-doc-view-record-list.vue';
import { helpDocApi } from '/@/api/support/help-doc-api';
import { SmartLoading } from '/@/components/framework/smart-loading';
import FilePreview from '/@/components/support/file-preview/index.vue';
import { smartSentry } from '/@/lib/smart-sentry';
const route = useRoute();
const activeKey = ref(1);
const helpDocDetail = ref({});
onMounted(() => {
if (route.query.helpDocId) {
queryHelpDocDetail();
}
});
const helpDocViewRecordListRef = ref();
// 查询详情
async function queryHelpDocDetail() {
try {
SmartLoading.show();
const result = await helpDocApi.view(route.query.helpDocId);
helpDocDetail.value = result.data;
helpDocViewRecordListRef.value.onSearch();
} catch (err) {
smartSentry.captureError(err);
} finally {
SmartLoading.hide();
}
}
// 预览附件
const filePreviewRef = ref();
function onPrevFile(fileItem) {
filePreviewRef.value.showPreview(fileItem);
}
// 打印
function print() {
let bdhtml = window.document.body.innerHTML;
let sprnstr = '<!--startprint-->'; //必须在页面添加<!--startprint-->和<!--endprint-->而且需要打印的内容必须在它们之间
let eprnstr = '<!--endprint-->';
let prnhtml = bdhtml.substr(bdhtml.indexOf(sprnstr));
prnhtml = prnhtml.substring(0, prnhtml.indexOf(eprnstr));
let newWin = window.open(''); //新打开一个空窗口
newWin.document.body.innerHTML = prnhtml;
newWin.document.close(); //在IE浏览器中使用必须添加这一句
newWin.focus(); //在IE浏览器中使用必须添加这一句
newWin.print(); //打印
newWin.close(); //关闭窗口
}
</script>
<style lang="less" scoped>
:deep(.ant-descriptions-item-content) {
flex: 1;
overflow: hidden;
}
.file-list {
width: 100%;
display: flex;
flex-wrap: wrap;
.file-item {
display: block;
margin-right: 10px;
}
}
.visible-list {
display: flex;
flex-wrap: wrap;
.visible-item {
margin-right: 10px;
color: #666;
}
}
.content-header {
.content-header-title {
margin: 10px 0px;
font-size: 20px;
font-weight: bold;
text-align: center;
}
.content-header-info {
margin: 10px 0px;
font-size: 14px;
color: #888;
text-align: center;
span {
margin: 0 10px;
cursor: pointer;
}
}
}
/*样式深入*/
:deep(.content-html) {
margin-top: 30px;
padding: 0 8px;
line-height: 28px;
font-size: 16px;
border: #1e1e1e;
img {
max-width: 100%;
}
body {
margin: 0 auto;
color: #ccd1d8;
line-height: 1.5;
padding: 16px;
background-color: #333842;
font-size: 16px;
}
h1, h2, h3, h4, h5, h6 {
color: #0D366F;
font-weight: bold;
margin-top: 20px;
margin-bottom: 10px;
padding: 0;
}
p {
padding: 0;
margin-bottom: 16px;
}
h1 {
font-size: 26px;
}
h2 {
font-size: 24px;
}
h3 {
font-size: 22px;
}
h4 {
font-size: 20px;
}
h5 {
font-size: 19px;
}
h6 {
font-size: 18px;
}
a {
color: #61afef;
margin: 0;
padding: 0;
vertical-align: baseline;
text-decoration: none;
word-break: break-word;
}
a:hover {
text-decoration: underline;
}
a:visited {
color: #ba68c8;
}
ul, ol {
padding: 0;
padding-left: 24px;
}
li {
line-height: 24px;
}
li ul, li ol {
margin-left: 16px;
}
p, ul, ol {
font-size: 16px;
line-height: 24px;
}
mark {
color: #000000;
background-color: #c4c400;
}
pre {
display: block;
overflow-y: hidden;
overflow-x: auto;
-moz-tab-size: 4;
tab-size: 4;
}
code {
color: #98c379;
word-break: break-word;
}
pre code {
display: block;
padding-left: 0.5em;
padding-right: 0.5em;
color: #98c379;
background-color: #2d323b;
line-height: 1.5;
white-space: pre;
-moz-tab-size: 4;
tab-size: 4;
}
aside {
display: block;
float: right;
width: 390px;
}
blockquote {
color: #abb2bf;
border-left: .5em solid #abb2bf;
padding: 0 1em;
margin-left: 0;
}
blockquote p {
color: #abb2bf;
}
hr {
display: block;
text-align: left;
margin: 1em 0;
border: none;
height: 2px;
background-color: #4c5562;
}
table {
padding: 0;
margin: 1rem 0.5rem;
border-collapse: collapse;
}
table tr {
border-top: 1px solid #4c5562;
margin: 0;
padding: 0;
}
table tr:hover {
background-color: #DBE5F2;
}
table tr th {
font-weight: bold;
background-color: #90BFFF;
border: 1px solid #4c5562;
margin: 0;
padding: 6px 13px;
}
table tr td {
border: 1px solid #4c5562;
margin: 0;
padding: 6px 13px;
}
table tr th :first-child, table tr td :first-child {
margin-top: 0;
}
table tr th :last-child, table tr td :last-child {
margin-bottom: 0;
}
}
</style>

View File

@@ -0,0 +1,248 @@
<!--
* JOB 表单
* @Author: huke
* @Date: 2024/06/29
-->
<template>
<div>
<!-- 编辑 -->
<a-modal :open="updateModalShow" :width="650" title="编辑" ok-text="确认" cancel-text="取消" @cancel="closeUpdateModal" @ok="confirmUpdateJob">
<a-form ref="updateFormRef" :model="updateForm" :rules="updateRules" :label-col="{ span: 4 }">
<a-form-item label="任务名称" name="jobName">
<a-input placeholder="请输入任务名称" v-model:value="updateForm.jobName" :maxlength="100" :showCount="true" />
</a-form-item>
<a-form-item label="任务描述" name="remark">
<a-textarea
:auto-size="{ minRows: 2, maxRows: 4 }"
v-model:value="updateForm.remark"
placeholder="(可选)请输入任务备注描述"
:maxlength="250"
:showCount="true"
/>
</a-form-item>
<a-form-item label="排序" name="sort">
<a-input-number
v-model:value="updateForm.sort"
:min="-99999999"
:max="99999999"
:precision="0"
style="width: 100%"
placeholder="值越小越靠前"
>
</a-input-number>
</a-form-item>
<a-form-item label="执行类" name="jobClass">
<a-textarea
:auto-size="{ minRows: 2, maxRows: 4 }"
v-model:value="updateForm.jobClass"
placeholder="示例net.lab1024.sa.base.module.support.job.sample.SmartJobSample1"
:maxlength="200"
:showCount="true"
/>
</a-form-item>
<a-form-item label="任务参数" name="param">
<a-textarea
:auto-size="{ minRows: 3, maxRows: 6 }"
v-model:value="updateForm.param"
placeholder="(可选)请输入任务执行参数"
:maxlength="1000"
:showCount="true"
/>
</a-form-item>
<a-form-item label="触发类型" name="triggerType">
<a-radio-group v-model:value="updateForm.triggerType">
<a-radio-button :value="TRIGGER_TYPE_ENUM.CRON.value">CRON表达式</a-radio-button>
<a-radio-button :value="TRIGGER_TYPE_ENUM.FIXED_DELAY.value">固定间隔</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="触发时间" name="triggerTime">
<a-input
v-if="updateForm.triggerType === TRIGGER_TYPE_ENUM.CRON.value"
placeholder="示例10 15 0/1 * * *"
v-model:value="updateForm.cron"
:maxlength="100"
:showCount="true"
/>
<a-input-number
v-else-if="updateForm.triggerType === TRIGGER_TYPE_ENUM.FIXED_DELAY.value"
v-model:value="updateForm.fixedDelay"
:min="1"
:max="100000000"
:precision="0"
:defaultValue="100"
>
<template #addonBefore>每隔</template>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
<a-form-item label="是否开启" name="enabledFlag">
<a-switch v-model:checked="updateForm.enabledFlag" />
</a-form-item>
</a-form>
</a-modal>
<!-- 立即执行 -->
<a-modal
:open="executeModalShow"
:width="650"
title="执行任务"
ok-text="执行"
cancel-text="取消"
@cancel="closeExecuteModal"
@ok="confirmExecuteJob"
>
<br />
<a-alert type="info" show-icon style="margin-left: 25px">
<template #message> 点击执行后会按照任务参数无论任务是否开启都会立即执行 </template>
</a-alert>
<br />
<a-form :label-col="{ span: 4 }">
<a-form-item label="任务名称" name="jobName">
<a-input v-model:value="executeForm.jobName" :disabled="true" />
</a-form-item>
<a-form-item label="任务类名" name="jobClass">
<a-textarea :auto-size="{ minRows: 2, maxRows: 4 }" v-model:value="executeForm.jobClass" :disabled="true" />
</a-form-item>
<a-form-item label="任务参数" name="param">
<a-textarea
:auto-size="{ minRows: 3, maxRows: 6 }"
v-model:value="executeForm.param"
placeholder="(可选)请输入任务执行参数"
:maxlength="1000"
:showCount="true"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { message } from 'ant-design-vue';
import { reactive, ref } from 'vue';
import { jobApi } from '/@/api/support/job-api';
import { smartSentry } from '/@/lib/smart-sentry';
import { SmartLoading } from '/@/components/framework/smart-loading/index.js';
import { TRIGGER_TYPE_ENUM } from '/@/constants/support/job-const.js';
// emit
const emit = defineEmits(['reloadList']);
const updateModalShow = ref(false);
const updateFormRef = ref();
const updateFormDefault = {
jobId: null,
jobName: '',
jobClass: '',
triggerType: null,
triggerValue: null,
cron: '',
fixedDelay: null,
param: '',
enabledFlag: false,
remark: '',
sort: null,
};
let updateForm = reactive({ ...updateFormDefault });
const updateRules = {
jobName: [{ required: true, message: '请输入任务名称' }],
jobClass: [{ required: true, message: '请输入执行类' }],
triggerType: [{ required: true, message: '请选择触发类型' }],
sort: [{ required: true, message: '请输入排序' }],
};
// 打开编辑弹框
function openUpdateModal(record) {
Object.assign(updateForm, record);
if (TRIGGER_TYPE_ENUM.CRON.value === record.triggerType) {
updateForm.cron = record.triggerValue;
}
if (TRIGGER_TYPE_ENUM.FIXED_DELAY.value === record.triggerType) {
updateForm.fixedDelay = record.triggerValue;
}
updateModalShow.value = true;
}
// 关闭编辑弹框
function closeUpdateModal() {
Object.assign(updateForm, updateFormDefault);
updateModalShow.value = false;
}
// 确认更新
async function confirmUpdateJob() {
updateFormRef.value
.validate()
.then(async () => {
SmartLoading.show();
if (TRIGGER_TYPE_ENUM.CRON.value === updateForm.triggerType) {
updateForm.triggerValue = updateForm.cron;
}
if (TRIGGER_TYPE_ENUM.FIXED_DELAY.value === updateForm.triggerType) {
updateForm.triggerValue = updateForm.fixedDelay;
}
try {
await jobApi.updateJob(updateForm);
message.success('更新成功');
closeUpdateModal();
emit('reloadList');
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
})
.catch((error) => {
console.log('error', error);
message.error('参数验证错误,请检查表单数据');
});
}
// ------------------------------------ 执行任务 -------------------------------------
const executeModalShow = ref(false);
const executeFormDefault = {
jobId: null,
jobName: '',
jobClass: '',
param: null,
};
let executeForm = reactive({ ...executeFormDefault });
// 打开执行弹框
function openExecuteModal(record) {
Object.assign(executeForm, record);
executeModalShow.value = true;
}
// 关闭执行弹框
function closeExecuteModal() {
Object.assign(executeForm, executeFormDefault);
executeModalShow.value = false;
}
// 确认执行
async function confirmExecuteJob() {
try {
let executeParam = {
jobId: executeForm.jobId,
param: executeForm.param,
};
await jobApi.executeJob(executeParam);
// loading 延迟后再提示刷新
SmartLoading.show();
await new Promise((resolve) => setTimeout(resolve, 2000));
message.success('执行成功');
closeExecuteModal();
emit('reloadList');
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
defineExpose({
openUpdateModal,
openExecuteModal,
});
</script>

View File

@@ -0,0 +1,200 @@
<!--
* job log列表
* @Author: huke
* @Date: 2024/06/25
-->
<template>
<a-drawer v-model:open="showFlag" :width="1000" :title="title" placement="right" :destroyOnClose="true">
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="关键字" class="smart-query-form-item">
<a-input style="width: 200px" v-model:value="queryForm.searchWord" placeholder="请输入关键字" :maxlength="30" />
</a-form-item>
<a-form-item label="执行结果" class="smart-query-form-item">
<a-select style="width: 100px" v-model:value="queryForm.successFlag" placeholder="请选择" allowClear>
<a-select-option :key="1"> 成功 </a-select-option>
<a-select-option :key="0"> 失败 </a-select-option>
</a-select>
</a-form-item>
<a-form-item label="执行时间" class="smart-query-form-item">
<a-space direction="vertical" :size="12">
<a-range-picker v-model:value="searchDate" style="width: 220px" @change="dateChange" />
</a-space>
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<SearchOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-card size="small" :bordered="false" :hoverable="true">
<a-row justify="end">
<TableOperator class="smart-margin-bottom5" v-model="columns" :tableId="TABLE_ID_CONST.SUPPORT.JOB_LOG" :refresh="queryLogList" />
</a-row>
<a-table size="small" :loading="tableLoading" bordered :dataSource="tableData" :columns="columns" rowKey="jobLogId" :pagination="false">
<template #bodyCell="{ record, column }">
<template v-if="column.dataIndex === 'executeStartTime'">
<div><a-tag color="green"></a-tag>{{ record.executeStartTime }}</div>
<div style="margin-top: 5px"><a-tag color="blue"></a-tag>{{ record.executeEndTime }}</div>
</template>
<template v-if="column.dataIndex === 'executeTimeMillis'"> {{ record.executeTimeMillis }} ms </template>
<template v-if="column.dataIndex === 'successFlag'">
<div v-if="record.successFlag" style="color: #39c710"><CheckOutlined />成功</div>
<div v-else style="color: #f50"><WarningOutlined />失败</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="queryLogList"
@showSizeChange="queryLogList"
:show-total="(total) => `${total}`"
/>
</div>
</a-card>
</a-drawer>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { jobApi } from '/@/api/support/job-api';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
const showFlag = ref(false);
const title = ref('');
function show(jobId, name) {
queryForm.jobId = jobId;
queryLogList();
showFlag.value = true;
title.value = name;
}
defineExpose({ show });
const columns = ref([
{
title: '执行人',
dataIndex: 'createName',
width: 100,
ellipsis: true,
},
{
title: '执行参数',
dataIndex: 'param',
width: 80,
ellipsis: true,
},
{
title: '执行时间',
dataIndex: 'executeStartTime',
width: 200,
},
{
title: '执行用时',
dataIndex: 'executeTimeMillis',
width: 100,
ellipsis: true,
},
{
title: '结果',
dataIndex: 'successFlag',
width: 80,
},
{
title: '执行结果',
dataIndex: 'executeResult',
ellipsis: true,
width: 100,
},
{
title: 'ip',
dataIndex: 'ip',
width: 110,
},
{
title: '进程id',
dataIndex: 'processId',
width: 60,
},
{
title: '程序目录',
dataIndex: 'programPath',
ellipsis: true,
},
]);
// ---------------- 查询数据 -----------------------
const queryFormState = {
searchWord: '',
jobId: null,
successFlag: null,
endTime: null,
startTime: null,
pageNum: 1,
pageSize: 10,
};
const queryForm = reactive({ ...queryFormState });
const tableLoading = ref(false);
const tableData = ref([]);
const total = ref(0);
// 日期选择
let searchDate = ref();
function dateChange(dates, dateStrings) {
queryForm.startTime = dateStrings[0];
queryForm.endTime = dateStrings[1];
}
function resetQuery() {
Object.assign(queryForm, queryFormState);
queryLogList();
}
function onSearch() {
queryForm.pageNum = 1;
queryLogList();
}
async function queryLogList() {
try {
tableLoading.value = true;
let responseModel = await jobApi.queryJobLog(queryForm);
const list = responseModel.data.list;
total.value = responseModel.data.total;
tableData.value = list;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
</script>

View File

@@ -0,0 +1,322 @@
<!--
* JOB 列表
* @Author: huke
* @Date: 2024/06/25
-->
<template>
<div>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="关键字" class="smart-query-form-item">
<a-input style="width: 200px" v-model:value="queryForm.searchWord" placeholder="请输入关键字" :maxlength="30" />
</a-form-item>
<a-form-item label="触发类型" class="smart-query-form-item">
<a-select style="width: 155px" v-model:value="queryForm.triggerType" placeholder="请选择触发类型" allowClear>
<a-select-option v-for="item in $smartEnumPlugin.getValueDescList('TRIGGER_TYPE_ENUM')" :key="item.value" :value="item.value">
{{ item.desc }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="状态" class="smart-query-form-item">
<a-select style="width: 150px" v-model:value="queryForm.enabledFlag" placeholder="请选择状态" allowClear>
<a-select-option :key="1"> 开启 </a-select-option>
<a-select-option :key="0"> 停止 </a-select-option>
</a-select>
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch" v-privilege="'support:job:query'">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery" v-privilege="'support:job:query'">
<template #icon>
<SearchOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-card size="small" :bordered="false" :hoverable="true">
<a-row justify="end">
<TableOperator class="smart-margin-bottom5" v-model="columns" :tableId="TABLE_ID_CONST.SUPPORT.JOB" :refresh="queryJobList" />
</a-row>
<a-table
:scroll="{ x: 1800 }"
size="small"
:loading="tableLoading"
bordered
:dataSource="tableData"
:columns="columns"
rowKey="jobId"
:pagination="false"
>
<template #bodyCell="{ record, column }">
<template v-if="column.dataIndex === 'jobClass'">
<a-tooltip>
<template #title>{{ record.jobClass }}</template>
{{ handleJobClass(record.jobClass) }}
</a-tooltip>
</template>
<template v-if="column.dataIndex === 'triggerType'">
<a-tag v-if="record.triggerType === TRIGGER_TYPE_ENUM.CRON.value" color="success">{{ record.triggerTypeDesc }}</a-tag>
<a-tag v-else-if="record.triggerType === TRIGGER_TYPE_ENUM.FIXED_DELAY.value" color="processing">{{ record.triggerTypeDesc }}</a-tag>
<a-tag v-else color="pink">{{ record.triggerTypeDesc }}</a-tag>
</template>
<template v-if="column.dataIndex === 'lastJob'">
<div v-if="record.lastJobLog">
<a-tooltip>
<template #title>{{ handleExecuteResult(record.lastJobLog.executeResult) }}</template>
<CheckOutlined v-if="record.lastJobLog.successFlag" style="color: #39c710" />
<WarningOutlined v-else style="color: #f50" />
{{ record.lastJobLog.executeStartTime }}
</a-tooltip>
</div>
</template>
<template v-if="column.dataIndex === 'nextJob'">
<a-tooltip v-if="record.enabledFlag && record.nextJobExecuteTimeList">
<template #title>
<div>下次执行(预估时间)</div>
<div v-for="item in record.nextJobExecuteTimeList" :key="item">{{ item }}</div>
</template>
{{ record.nextJobExecuteTimeList[0] }}
</a-tooltip>
</template>
<template v-if="column.dataIndex === 'enabledFlag'">
<a-switch
v-model:checked="record.enabledFlag"
@change="(checked) => handleEnabledUpdate(checked, record)"
:loading="record.enabledLoading"
/>
</template>
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button v-privilege="'support:job:update'" @click="openUpdateModal(record)" type="link">编辑</a-button>
<a-button v-privilege="'support:job:execute'" type="link" @click="openExecuteModal(record)">执行</a-button>
<a-button v-privilege="'support:job:log:query'" @click="openJobLogModal(record.jobId, record.jobName)" type="link">记录</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="queryJobList"
@showSizeChange="queryJobList"
:show-total="(total) => `${total}`"
/>
</div>
</a-card>
<!-- 表单操作 -->
<JobFormModal ref="jobFormModal" @reloadList="queryJobList" />
<!-- 记录 -->
<JobLogListModal ref="jobLogModal" />
</div>
</template>
<script setup>
import { message } from 'ant-design-vue';
import { onMounted, reactive, ref } from 'vue';
import { jobApi } from '/@/api/support/job-api';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
import { useRouter } from 'vue-router';
import { TRIGGER_TYPE_ENUM } from '/@/constants/support/job-const.js';
import JobFormModal from './components/job-form-modal.vue';
import JobLogListModal from './components/job-log-list-modal.vue';
const columns = ref([
{
title: 'id',
width: 50,
dataIndex: 'jobId',
},
{
title: '任务名称',
dataIndex: 'jobName',
minWidth: 150,
ellipsis: true,
},
{
title: '执行类',
dataIndex: 'jobClass',
minWidth: 180,
ellipsis: true,
},
{
title: '触发类型',
dataIndex: 'triggerType',
width: 110,
},
{
title: '触发配置',
dataIndex: 'triggerValue',
width: 150,
},
{
title: '上次执行',
width: 180,
dataIndex: 'lastJob',
},
{
title: '下次执行',
width: 150,
dataIndex: 'nextJob',
},
{
title: '状态',
dataIndex: 'enabledFlag',
width: 80,
},
{
title: '执行参数',
dataIndex: 'param',
ellipsis: true,
},
{
title: '任务描述',
dataIndex: 'remark',
ellipsis: true,
},
{
title: '排序',
dataIndex: 'sort',
width: 65,
},
{
title: '更新人',
dataIndex: 'updateName',
width: 90,
},
{
title: '更新时间',
dataIndex: 'updateTime',
width: 150,
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 130,
},
]);
// ---------------- 查询数据 -----------------------
const queryFormState = {
searchWord: '',
enabledFlag: null,
triggerType: null,
pageNum: 1,
pageSize: 10,
};
const queryForm = reactive({ ...queryFormState });
const tableLoading = ref(false);
const tableData = ref([]);
const total = ref(0);
function resetQuery() {
Object.assign(queryForm, queryFormState);
queryJobList();
}
function onSearch() {
queryForm.pageNum = 1;
queryJobList();
}
// 处理执行类展示 默认返回类
function handleJobClass(jobClass) {
return jobClass.split('.').pop();
}
// 上次处理结果展示 最多展示300
function handleExecuteResult(result) {
let num = 400;
return result ? result.substring(0, num) + (result.length > num ? ' ...' : '') : '';
}
async function queryJobList() {
try {
tableLoading.value = true;
let responseModel = await jobApi.queryJob(queryForm);
const list = responseModel.data.list;
total.value = responseModel.data.total;
tableData.value = list;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
onMounted(queryJobList);
// 更新状态
async function handleEnabledUpdate(checked, record) {
record.enabledLoading = true;
try {
let updateForm = {
jobId: record.jobId,
enabledFlag: checked,
};
await jobApi.updateJobEnabled(updateForm);
// 重新查询任务详情
let jobInfo = await queryJobInfo(record.jobId);
Object.assign(record, jobInfo);
message.success('更新成功');
} catch (e) {
record.enabledFlag = !checked;
smartSentry.captureError(e);
} finally {
record.enabledLoading = false;
}
}
// 查询任务详情
async function queryJobInfo(jobId) {
try {
let res = await jobApi.queryJobInfo(jobId);
return res.data;
} catch (e) {
smartSentry.captureError(e);
}
}
// ------------------------------------ 执行记录 -------------------------------------
const jobLogModal = ref();
function openJobLogModal(jobId, name) {
jobLogModal.value.show(jobId, name);
}
// ------------------------------------ 表单操作 -------------------------------------
const jobFormModal = ref();
// 打开更新表单
function openUpdateModal(record) {
jobFormModal.value.openUpdateModal(record);
}
// 打开执行表单
function openExecuteModal(record) {
jobFormModal.value.openExecuteModal(record);
}
</script>

View File

@@ -0,0 +1,137 @@
<!--
* 数据脱敏
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2024-08-02 20:23:08
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-card size="small" :bordered="false" :hoverable="true">
<a-alert>
<template v-slot:message>
<h4>数据脱敏 Data Masking介绍</h4>
</template>
<template v-slot:description>
<pre>
简介信息安全技术 网络安全等级保护基本要求明确规定二级以上保护则需要对敏感数据进行脱敏处理
原理数据脱敏是指对某些敏感信息通过脱敏规则进行数据的变形实现敏感隐私数据的可靠保护
举例在不违反系统规则条件下身份证号手机号卡号客户号等个人信息都需要进行数据脱敏
使用方式
1脱敏注解 @DataMasking 支持数据类型如用户ID手机号密码地址银行卡车牌号等
2脱敏工具类 SmartDataMaskingUtil
</pre
>
</template>
</a-alert>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-table
size="small"
bordered
:scroll="{ x: 1100 }"
:loading="tableLoading"
class="smart-margin-top10"
:dataSource="tableData"
:columns="columns"
:pagination="false"
/>
</a-card>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { heartBeatApi } from '/@/api/support/heart-beat-api';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { defaultTimeRanges } from '/@/lib/default-time-ranges';
import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
import { dataMaskingApi } from '/@/api/support/data-masking-api.js';
//------------------------ 表格渲染 ---------------------
const columns = ref([
{
title: '用户ID',
dataIndex: 'userId',
width: 70,
},
{
title: '默认',
dataIndex: 'other',
width: 100,
},
{
title: '手机号',
dataIndex: 'phone',
width: 100,
},
{
title: '身份证',
dataIndex: 'idCard',
width: 150,
},
{
title: '密码',
dataIndex: 'password',
width: 100,
},
{
title: '邮箱',
dataIndex: 'email',
width: 120,
},
{
title: '车牌号',
dataIndex: 'carLicense',
width: 120,
},
{
title: '银行卡',
dataIndex: 'bankCard',
width: 170,
},
{
title: '地址',
dataIndex: 'address',
width: 210,
},
]);
const tableLoading = ref(false);
const tableData = ref([]);
function onSearch() {
ajaxQuery();
}
async function ajaxQuery() {
try {
tableLoading.value = true;
let responseModel = await dataMaskingApi.query();
tableData.value = responseModel.data;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
onMounted(ajaxQuery);
</script>

View File

@@ -0,0 +1,254 @@
<!--
* 三级等保配置
*
* @Author: 1024创新实验室-主任-卓大
* @Date: 2024-07-31 22:02:37
* @Copyright 1024创新实验室
-->
<template>
<a-alert closable>
<template v-slot:message>
<h4>三级等保</h4>
</template>
<template v-slot:description>
<pre>
1.三级等保是中国国家等级保护认证中的最高级别认证该认证包含了五个等级保护安全技术要求和五个安全管理要求共涉及测评分类73类要求非常严格
2.三级等保是地市级以上国家机关重要企事业单位需要达成的认证在金融行业中可以看作是除了银行机构以外最高级别的信息安全等级保护
3.具体三级等保要求请查看1024创新实验室写的相关文档 <a href="https://smartadmin.vip/views/level3protect/basic.html" target="_blank">三级等保文档</a></pre>
</template>
</a-alert>
<br />
<!---------- 三级等保配置表单 begin ----------->
<a-card title="三级等保配置">
<a-form
:model="form"
:rules="rules"
ref="formRef"
style="width: 800px"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
autocomplete="off"
class="smart-query-form"
>
<a-form-item
label="配置双因子登录模式"
class="smart-query-form-item"
extra="在用户登录时,需要同时提供用户名和密码以及其他形式的身份验证信息,例如短信验证码等"
>
<a-switch v-model:checked="form.twoFactorLoginEnabled" checked-children="开启 " un-checked-children="关闭 " />
</a-form-item>
<a-form-item
label="最大连续登录失败次数"
class="smart-query-form-item"
extra="连续登录失败超过一定次数则需要锁定默认5次0则不锁定"
name="loginFailMaxTimes"
>
<a-input-number :min="0" :max="10" v-model:value="form.loginFailMaxTimes" placeholder="最大连续登录失败次数" addon-after="" />
</a-form-item>
<a-form-item
name="loginFailLockMinutes"
label="连续登录失败锁定分钟"
class="smart-query-form-item"
extra="连续登录失败锁定的时间默认30分钟0则不锁定"
>
<a-input-number :min="0" v-model:value="form.loginFailLockMinutes" placeholder="连续登录失败锁定时分钟" addon-after="分钟" />
</a-form-item>
<a-form-item
name="loginActiveTimeoutMinutes"
label="登录后无操作自动退出的分钟"
class="smart-query-form-item"
extra="如登录1小时没操作自动退出当前登录状态默认30分钟"
>
<a-input-number :min="-1" v-model:value="form.loginActiveTimeoutMinutes" placeholder="登录后无操作自动退出的分钟" addon-after="分钟" />
</a-form-item>
<a-form-item
label="开启密码复杂度"
class="smart-query-form-item"
extra="密码长度为8-20位且必须包含字母、数字、特殊符号@#$%^&*()_+-=)等三种字符"
>
<a-switch v-model:checked="form.passwordComplexityEnabled" checked-children="开启 " un-checked-children="关闭 " />
</a-form-item>
<a-form-item
name="regularChangePasswordMonths"
label="定期修改密码时间间隔"
class="smart-query-form-item"
extra="定期修改密码时间间隔默认3个月"
>
<a-input-number :min="-1" :max="6" v-model:value="form.regularChangePasswordMonths" placeholder="定期修改密码时间间隔" addon-after="" />
</a-form-item>
<a-form-item
name="regularChangePasswordNotAllowRepeatTimes"
label="定期修改密码不允许重复次数"
class="smart-query-form-item"
extra="定期修改密码不允许重复次数默认3次以内密码不能相同"
>
<a-input-number
:min="-1"
:max="6"
v-model:value="form.regularChangePasswordNotAllowRepeatTimes"
placeholder="相同密码不允许重复次数"
addon-after=""
/>
</a-form-item>
<a-form-item
label="文件安全检测"
class="smart-query-form-item"
extra="对文件类型、恶意文件进行检测;(具体请看后端: SecurityFileService 类 checkFile 方法 "
>
<a-switch v-model:checked="form.fileDetectFlag" checked-children="开启 " un-checked-children="关闭 " />
</a-form-item>
<a-form-item
name="maxUploadFileSizeMb"
label="上传文件大小限制"
class="smart-query-form-item"
extra="上传文件大小限制,默认 50 mb ( 0 表示不限制)"
>
<a-input-number :min="0" v-model:value="form.maxUploadFileSizeMb" placeholder="上传文件大小限制" addon-after="mb()" />
</a-form-item>
<br />
<a-form-item :wrapper-col="{ span: 14, offset: 6 }">
<a-button type="primary" style="margin-right: 20px" @click.prevent="onSubmit">保存配置</a-button>
<a-button style="margin-right: 20px" @click="reset">恢复三级等保默认配置</a-button>
<a-button danger @click="clear">清除所有配置</a-button>
</a-form-item>
</a-form>
</a-card>
<!---------- 请求参数加密 end ----------->
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { level3ProtectApi } from '/@/api/support/level3-protect-api.js';
import { SmartLoading } from '/@/components/framework/smart-loading/index.js';
import { smartSentry } from '/@/lib/smart-sentry.js';
import { message, Modal } from 'ant-design-vue';
// 三级等保的默认值
const protectDefaultValues = {
// 连续登录失败次数则锁定
loginFailMaxTimes: 5,
// 连续登录失败锁定分钟
loginFailLockMinutes: 30,
// 最低活跃时间分钟
loginActiveTimeoutMinutes: 30,
// 密码复杂度
passwordComplexityEnabled: true,
// 定期修改密码时间间隔 月份
regularChangePasswordMonths: 3,
// 定期修改密码不允许重复次数默认3次以内密码不能相同
regularChangePasswordNotAllowRepeatTimes: 3,
// 开启双因子登录
twoFactorLoginEnabled: true,
// 文件检测,默认:不开启
fileDetectFlag: true,
// 文件大小限制,单位 mb (默认50 mb)
maxUploadFileSizeMb: 50,
};
// 三级等保的不保护的默认值
const noProtectDefaultValues = {
// 连续登录失败次数则锁定
loginFailMaxTimes: 0,
// 连续登录失败锁定分钟
loginFailLockMinutes: 0,
// 最低活跃时间分钟
loginActiveTimeoutMinutes: 0,
// 密码复杂度
passwordComplexityEnabled: false,
// 定期修改密码时间间隔 月份
regularChangePasswordMonths: 0,
// 定期修改密码不允许重复次数,
regularChangePasswordNotAllowRepeatTimes: 0,
// 开启双因子登录
twoFactorLoginEnabled: false,
// 文件大小限制,单位 mb
maxUploadFileSizeMb: 0,
};
// 三级等保配置表单
const form = reactive({
...protectDefaultValues,
});
const rules = {
loginFailMaxTimes: [{ required: true, message: '请输入 最大连续登录失败次数' }],
loginFailLockMinutes: [{ required: true, message: '请输入 连续登录失败锁定分钟' }],
loginActiveTimeoutMinutes: [{ required: true, message: '请输入 最低活跃时间分钟' }],
regularChangePasswordMonths: [{ required: true, message: '请输入 定期修改密码时间间隔' }],
regularChangePasswordNotAllowRepeatTimes: [{ required: true, message: '请输入 定期修改密码时间间隔' }],
maxUploadFileSizeMb: [{ required: true, message: '请输入 上传文件大小限制' }],
};
//获取配置
async function getConfig() {
SmartLoading.show();
try {
let res = await level3ProtectApi.getConfig();
if (!res.data) {
message.warn('当前未配置三级等保');
return;
}
let json = JSON.parse(res.data);
form.loginFailMaxTimes = json.loginFailMaxTimes;
form.loginFailLockMinutes = json.loginFailLockMinutes;
form.loginActiveTimeoutMinutes = json.loginActiveTimeoutMinutes;
form.passwordComplexityEnabled = json.passwordComplexityEnabled;
form.regularChangePasswordMonths = json.regularChangePasswordMonths;
form.regularChangePasswordNotAllowRepeatTimes = json.regularChangePasswordNotAllowRepeatTimes;
form.twoFactorLoginEnabled = json.twoFactorLoginEnabled;
form.maxUploadFileSizeMb = json.maxUploadFileSizeMb;
form.fileDetectFlag = json.fileDetectFlag;
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
onMounted(getConfig);
const formRef = ref();
// 提交修改
function onSubmit() {
formRef.value
.validate()
.then(save)
.catch((error) => {
message.error('参数验证错误,请仔细填写表单数据!');
});
}
// 提交修改配置
async function save() {
SmartLoading.show();
try {
let res = await level3ProtectApi.updateConfig(form);
message.success(res.msg);
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
// 重置
function reset() {
Object.assign(form, protectDefaultValues);
save();
}
// 清除所有配置
function clear() {
Modal.confirm({
title: '提示',
content: '确定要清除三级等保配置吗?这样系统不安全哦',
okText: '清除三级等保配置',
okType: 'danger',
onOk() {
Object.assign(form, noProtectDefaultValues);
save();
},
cancelText: '取消',
onCancel() {},
});
}
</script>

View File

@@ -0,0 +1,247 @@
<!--
* 登录失败锁定
*
* @Author: 1024创新实验室-主任-卓大
* @Date: 2023-10-17 18:02:37
* @Copyright 1024创新实验室
-->
<template>
<!---------- 查询表单form begin ----------->
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="登录名" class="smart-query-form-item">
<a-input style="width: 300px" v-model:value="queryForm.loginName" placeholder="登录名" />
</a-form-item>
<a-form-item label="快速筛选" class="smart-query-form-item">
<a-radio-group v-model:value="queryForm.lockFlag" @change="onSearch" button-style="solid">
<a-radio-button :value="undefined">全部</a-radio-button>
<a-radio-button :value="true">已锁定</a-radio-button>
<a-radio-button :value="false">未锁定</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="锁定时间" class="smart-query-form-item">
<a-range-picker
v-model:value="queryForm.loginLockBeginTime"
:presets="defaultTimeRanges"
style="width: 220px"
@change="onChangeLoginLockBeginTime"
/>
</a-form-item>
<a-form-item class="smart-query-form-item">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery" class="smart-margin-left10">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<!---------- 查询表单form end ----------->
<a-card size="small" :bordered="false" :hoverable="true">
<!---------- 表格操作行 begin ----------->
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button @click="confirmBatchDelete" danger :disabled="selectedRowKeyList.length === 0">
<template #icon>
<DeleteOutlined />
</template>
解除锁定
</a-button>
</div>
<div class="smart-table-setting-block">
<TableOperator v-model="columns" :tableId="null" :refresh="queryData" />
</div>
</a-row>
<!---------- 表格操作行 end ----------->
<!---------- 表格 begin ----------->
<a-table
size="small"
:dataSource="tableData"
:columns="columns"
rowKey="loginFailId"
bordered
:loading="tableLoading"
:pagination="false"
:row-selection="{ selectedRowKeys: selectedRowKeyList, onChange: onSelectChange }"
>
<template #bodyCell="{ text, column }">
<template v-if="column.dataIndex === 'userType'">
<span>{{ $smartEnumPlugin.getDescByValue('USER_TYPE_ENUM', text) }}</span>
</template>
<template v-if="column.dataIndex === 'lockFlag'">
<template v-if="text">
<a-tag color="error">已锁定</a-tag>
</template>
<template v-if="!text">
<a-tag color="success">未锁定</a-tag>
</template>
</template>
</template>
</a-table>
<!---------- 表格 end ----------->
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="onSearch"
@showSizeChange="onSearch"
:show-total="(total) => `${total}`"
/>
</div>
</a-card>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { loginFailApi } from '/@/api/support/login-fail-api';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { defaultTimeRanges } from '/@/lib/default-time-ranges';
// ---------------------------- 表格列 ----------------------------
const columns = ref([
{
title: '登录名',
dataIndex: 'loginName',
},
{
title: '用户类型',
dataIndex: 'userType',
},
{
title: '登录失败次数',
dataIndex: 'loginFailCount',
},
{
title: '锁定状态',
dataIndex: 'lockFlag',
},
{
title: '锁定开始时间',
dataIndex: 'loginLockBeginTime',
},
{
title: '创建时间',
dataIndex: 'createTime',
},
{
title: '更新时间',
dataIndex: 'updateTime',
},
]);
// ---------------------------- 查询数据表单和方法 ----------------------------
const queryFormState = {
loginName: undefined, //登录名
lockFlag: true, // 锁定状态
loginLockBeginTime: [], //登录失败锁定时间
loginLockBeginTimeBegin: undefined, //登录失败锁定时间 开始
loginLockBeginTimeEnd: undefined, //登录失败锁定时间 结束
pageNum: 1,
pageSize: 10,
};
// 查询表单form
const queryForm = reactive({ ...queryFormState });
// 表格加载loading
const tableLoading = ref(false);
// 表格数据
const tableData = ref([]);
// 总数
const total = ref(0);
// 重置查询条件
function resetQuery() {
let pageSize = queryForm.pageSize;
Object.assign(queryForm, queryFormState);
queryForm.pageSize = pageSize;
queryForm.lockFlag = undefined;
queryData();
}
// 查询数据
function onSearch() {
queryForm.pageNum = 1;
queryData();
}
async function queryData() {
tableLoading.value = true;
try {
let queryResult = await loginFailApi.queryPage(queryForm);
tableData.value = queryResult.data.list;
total.value = queryResult.data.total;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
function onChangeLoginLockBeginTime(dates, dateStrings) {
queryForm.loginLockBeginTimeBegin = dateStrings[0];
queryForm.loginLockBeginTimeEnd = dateStrings[1];
}
onMounted(queryData);
// ---------------------------- 批量解除锁定 ----------------------------
// 选择表格行
const selectedRowKeyList = ref([]);
function onSelectChange(selectedRowKeys) {
selectedRowKeyList.value = selectedRowKeys;
}
// 批量解除锁定
function confirmBatchDelete() {
Modal.confirm({
title: '提示',
content: '确定要批量解除锁定这些数据吗?',
okText: '解锁',
okType: 'danger',
onOk() {
requestBatchDelete();
},
cancelText: '取消',
onCancel() {},
});
}
//请求批量删除
async function requestBatchDelete() {
try {
SmartLoading.show();
await loginFailApi.batchDelete(selectedRowKeyList.value);
message.success('解锁成功');
queryData();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
</script>

View File

@@ -0,0 +1,235 @@
<!--
* 登录登出 日志
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-06-02 20:23:08
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-form class="smart-query-form" v-privilege="'support:loginLog:query'" ref="queryFormRef">
<a-row class="smart-query-form-row">
<a-form-item label="用户名称" class="smart-query-form-item">
<a-input style="width: 300px" v-model:value="queryForm.userName" placeholder="用户名称" />
</a-form-item>
<a-form-item label="用户IP" class="smart-query-form-item">
<a-input style="width: 120px" v-model:value="queryForm.ip" placeholder="IP" />
</a-form-item>
<a-form-item label="时间" class="smart-query-form-item">
<a-range-picker @change="changeCreateDate" v-model:value="createDateRange" :presets="defaultChooseTimeRange" style="width: 240px" />
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-card size="small" :bordered="false" :hoverable="true">
<a-row justify="end" ref="tableOperatorRef">
<TableOperator class="smart-margin-bottom5" v-model="columns" :tableId="TABLE_ID_CONST.SUPPORT.LOGIN_LOG" :refresh="ajaxQuery" />
</a-row>
<a-table
size="small"
:dataSource="tableData"
:columns="columns"
bordered
rowKey="loginLogId"
:pagination="false"
:loading="tableLoading"
:scroll="{ y: scrollY }"
>
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'loginResult'">
<template v-if="text === LOGIN_RESULT_ENUM.LOGIN_SUCCESS.value">
<a-tag color="success">登录成功</a-tag>
</template>
<template v-if="text === LOGIN_RESULT_ENUM.LOGIN_FAIL.value">
<a-tag color="error">登录失败</a-tag>
</template>
<template v-if="text === LOGIN_RESULT_ENUM.LOGIN_OUT.value">
<a-tag color="processing">退出登录</a-tag>
</template>
</template>
<template v-if="column.dataIndex === 'userAgent'">
<div>{{ record.browser }} / {{ record.os }} / {{ record.device }}</div>
</template>
<template v-if="column.dataIndex === 'userType'">
<span>{{ $smartEnumPlugin.getDescByValue('USER_TYPE_ENUM', text) }}</span>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `${total}`"
/>
</div>
</a-card>
</template>
<script setup>
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { defaultTimeRanges } from '/@/lib/default-time-ranges';
import uaparser from 'ua-parser-js';
import { LOGIN_RESULT_ENUM } from '/@/constants/support/login-log-const';
import { loginLogApi } from '/@/api/support/login-log-api';
import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
import { calcTableHeight } from '/@/lib/table-auto-height';
const columns = ref([
{
title: '用户ID',
dataIndex: 'userId',
width: 70,
},
{
title: '用户名',
dataIndex: 'userName',
ellipsis: true,
},
{
title: '类型',
dataIndex: 'userType',
width: 50,
ellipsis: true,
},
{
title: 'IP',
dataIndex: 'loginIp',
ellipsis: true,
},
{
title: 'IP地区',
dataIndex: 'loginIpRegion',
ellipsis: true,
},
{
title: '设备信息',
dataIndex: 'userAgent',
ellipsis: true,
},
{
title: '结果',
dataIndex: 'loginResult',
ellipsis: true,
},
{
title: '备注',
dataIndex: 'remark',
ellipsis: true,
},
{
title: '时间',
dataIndex: 'createTime',
width: 150,
},
]);
const queryFormState = {
userName: '',
ip: '',
startDate: undefined,
endDate: undefined,
pageNum: 1,
pageSize: 10,
};
const queryForm = reactive({ ...queryFormState });
const createDateRange = ref([]);
const defaultChooseTimeRange = defaultTimeRanges;
// 时间变动
function changeCreateDate(dates, dateStrings) {
queryForm.startDate = dateStrings[0];
queryForm.endDate = dateStrings[1];
}
const tableLoading = ref(false);
const tableData = ref([]);
const total = ref(0);
function resetQuery() {
Object.assign(queryForm, queryFormState);
createDateRange.value = [];
ajaxQuery();
}
function onSearch() {
queryForm.pageNum = 1;
ajaxQuery();
}
async function ajaxQuery() {
try {
tableLoading.value = true;
let responseModel = await loginLogApi.queryList(queryForm);
for (const e of responseModel.data.list) {
if (!e.userAgent) {
continue;
}
let ua = uaparser(e.userAgent);
e.browser = ua.browser.name;
e.os = ua.os.name;
e.device = ua.device.vendor ? ua.device.vendor + ua.device.model : '';
}
const list = responseModel.data.list;
total.value = responseModel.data.total;
tableData.value = list;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
// ----------------- 表格自适应高度 --------------------
const scrollY = ref(100);
const tableOperatorRef = ref();
const queryFormRef = ref();
function autoCalcTableHeight() {
calcTableHeight(scrollY, [tableOperatorRef, queryFormRef], 10);
}
window.addEventListener('resize', autoCalcTableHeight);
onMounted(() => {
ajaxQuery();
autoCalcTableHeight();
});
onUnmounted(() => {
window.removeEventListener('resize', autoCalcTableHeight);
});
</script>

View File

@@ -0,0 +1,137 @@
<!--
* 操作记录 详情
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-06-02 20:23:08
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal :open="visible" title="请求详情" width="60%" :footer="null" @cancel="close">
<div class="info-box">
<a-row class="smart-margin-top10">
<a-col :span="16">
<a-row class="detail-info">
<a-col :span="12"> 请求url {{ detail.url }}</a-col>
<a-col :span="12"> 请求日期 {{ detail.createTime }}</a-col>
</a-row>
<a-row class="detail-info">
<a-col :span="12"> 请求IP {{ detail.ip }}</a-col>
<a-col :span="12"> IP地区 {{ detail.ipRegion }}</a-col>
</a-row>
<a-row class="detail-info">
<a-col :span="12"> 用户id{{ detail.operateUserId }}</a-col>
<a-col :span="12"> 用户名称 {{ detail.operateUserName }}</a-col>
</a-row>
</a-col>
<a-col :span="8">
<p class="detail-right-title">请求状态</p>
<a-typography-text class="detail-right" :type="detail.successFlag ? 'success' : 'danger'">
{{ detail.successFlag ? '成功' : '失败' }}
</a-typography-text>
</a-col>
</a-row>
</div>
<div class="info-box">
<h4>请求明细</h4>
<a-col :span="24"> 方法 {{ detail.method }}</a-col>
<a-col :span="24"> 说明 {{ detail.module }} - {{ detail.content }}</a-col>
</div>
<div class="info-box">
<h4>请求参数</h4>
<JsonViewer :value="detail.param ? JSON.parse(detail.param) : ''" theme="jv-dark" copyable boxed sort />
</div>
<div class="info-box" v-if="detail.failReason">
<h4>请求失败原因</h4>
<div>
{{ detail.failReason }}
</div>
</div>
</a-modal>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { JsonViewer } from 'vue3-json-viewer';
import { operateLogApi } from '/@/api/support/operate-log-api';
import { smartSentry } from '/@/lib/smart-sentry';
import { SmartLoading } from '/@/components/framework/smart-loading';
defineExpose({
show,
});
const visible = ref(false);
function show(operateLogId) {
visible.value = true;
clear(detail);
getDetail(operateLogId);
}
const clear = (info) => {
const keys = Object.keys(info);
let obj = {};
keys.forEach((item) => {
obj[item] = '';
});
Object.assign(info, obj);
};
function close() {
visible.value = false;
}
let detail = reactive({
param: '',
url: '',
});
async function getDetail(operateLogId) {
try {
SmartLoading.show();
let res = await operateLogApi.detail(operateLogId);
detail = Object.assign(detail, res.data);
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
</script>
<style scoped lang="less">
.detail-title {
display: flex;
align-items: center;
font-size: 20px;
font-weight: bold;
}
.info-box {
border-bottom: 1px solid #f0f0f0;
padding: 10px 8px;
}
.detail-info {
.ant-col {
line-height: 1.46;
margin-bottom: 12px;
padding-right: 5px;
}
}
.detail-right-title {
text-align: right;
color: grey;
}
:deep(.ant-modal-body) {
padding: 10px !important;
}
.detail-right {
padding-left: 5px;
font-size: 20px;
font-weight: bold;
text-align: right;
float: right;
}
</style>

View File

@@ -0,0 +1,241 @@
<!--
* 操作记录 列表
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-06-02 20:23:08
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-form class="smart-query-form" v-privilege="'support:operateLog:query'">
<a-row class="smart-query-form-row">
<a-form-item label="操作关键字" class="smart-query-form-item">
<a-input style="width: 150px" v-model:value="queryForm.keywords" placeholder="模块/操作内容" />
</a-form-item>
<a-form-item label="请求关键字" class="smart-query-form-item">
<a-input style="width: 220px" v-model:value="queryForm.requestKeywords" placeholder="请求地址/请求方法/请求参数" />
</a-form-item>
<a-form-item label="用户名称" class="smart-query-form-item">
<a-input style="width: 100px" v-model:value="queryForm.userName" placeholder="用户名称" />
</a-form-item>
<a-form-item label="请求时间" class="smart-query-form-item">
<a-range-picker @change="changeCreateDate" v-model:value="createDateRange" :presets="defaultChooseTimeRange" style="width: 240px" />
</a-form-item>
<a-form-item label="快速筛选" class="smart-query-form-item">
<a-radio-group v-model:value="queryForm.successFlag" @change="onSearch">
<a-radio-button :value="undefined">全部</a-radio-button>
<a-radio-button :value="true">成功</a-radio-button>
<a-radio-button :value="false">失败</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="ajaxQuery">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<SearchOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-card size="small" :bordered="false" :hoverable="true" style="height: 100%">
<a-row justify="end">
<TableOperator class="smart-margin-bottom5" v-model="columns" :tableId="TABLE_ID_CONST.SUPPORT.CONFIG" :refresh="ajaxQuery" />
</a-row>
<a-table size="small" :loading="tableLoading" :dataSource="tableData" :columns="columns" bordered rowKey="operateLogId" :pagination="false">
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'successFlag'">
<a-tag :color="text ? 'success' : 'error'">{{ text ? '成功' : '失败' }}</a-tag>
</template>
<template v-if="column.dataIndex === 'userAgent'">
<div>{{ record.browser }} / {{ record.os }} / {{ record.device }}</div>
</template>
<template v-if="column.dataIndex === 'operateUserType'">
<div>{{ $smartEnumPlugin.getDescByValue('USER_TYPE_ENUM', text) }}</div>
</template>
<template v-else-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button @click="showDetail(record.operateLogId)" type="link" v-privilege="'support:operateLog:detail'">详情</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `${total}`"
/>
</div>
<OperateLogDetailModal ref="detailModal" />
</a-card>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import OperateLogDetailModal from './operate-log-detail-modal.vue';
import { operateLogApi } from '/@/api/support/operate-log-api';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { defaultTimeRanges } from '/@/lib/default-time-ranges';
import uaparser from 'ua-parser-js';
import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
const columns = ref([
{
title: '用户',
dataIndex: 'operateUserName',
width: 70,
},
{
title: '类型',
dataIndex: 'operateUserType',
width: 50,
ellipsis: true,
},
{
title: '操作模块',
dataIndex: 'module',
ellipsis: true,
},
{
title: '操作内容',
dataIndex: 'content',
ellipsis: true,
},
{
title: '请求路径',
dataIndex: 'url',
ellipsis: true,
},
{
title: 'IP',
dataIndex: 'ip',
ellipsis: true,
},
{
title: 'IP地区',
dataIndex: 'ipRegion',
ellipsis: true,
},
{
title: '客户端',
dataIndex: 'userAgent',
ellipsis: true,
},
{
title: '请求方法',
dataIndex: 'method',
ellipsis: true,
},
{
title: '请求结果',
dataIndex: 'successFlag',
width: 80,
},
{
title: '时间',
dataIndex: 'createTime',
width: 150,
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 60,
},
]);
const queryFormState = {
userName: '',
requestKeywords: '',
keywords: '',
successFlag: undefined,
startDate: undefined,
endDate: undefined,
pageNum: 1,
pageSize: 10,
};
const queryForm = reactive({ ...queryFormState });
const createDateRange = ref([]);
const defaultChooseTimeRange = defaultTimeRanges;
// 时间变动
function changeCreateDate(dates, dateStrings) {
queryForm.startDate = dateStrings[0];
queryForm.endDate = dateStrings[1];
}
const tableLoading = ref(false);
const tableData = ref([]);
const total = ref(0);
function resetQuery() {
Object.assign(queryForm, queryFormState);
createDateRange.value = [];
ajaxQuery();
}
function onSearch() {
queryForm.pageNum = 1;
ajaxQuery();
}
async function ajaxQuery() {
try {
tableLoading.value = true;
let responseModel = await operateLogApi.queryList(queryForm);
for (const e of responseModel.data.list) {
if (!e.userAgent) {
continue;
}
let ua = uaparser(e.userAgent);
e.browser = ua.browser.name;
e.os = ua.os.name;
e.device = ua.device.vendor ? ua.device.vendor + ua.device.model : '';
}
const list = responseModel.data.list;
total.value = responseModel.data.total;
tableData.value = list;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
onMounted(ajaxQuery);
// ---------------------- 详情 ----------------------
const detailModal = ref();
function showDetail(operateLogId) {
detailModal.value.show(operateLogId);
}
</script>

View File

@@ -0,0 +1,90 @@
<!--
* reload 表单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-07-21 21:55:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal :open="visible" title="执行Reload" ok-text="确认" cancel-text="取消" @ok="onSubmit" @cancel="onClose">
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }">
<a-form-item label="标签">
<a-input v-model:value="form.tag" :disabled="true" />
</a-form-item>
<a-form-item label="运行标识" name="identification">
<a-input v-model:value="form.identification" placeholder="请输入运行标识" />
</a-form-item>
<a-form-item label="参数" name="args">
<a-input v-model:value="form.args" placeholder="请输入参数" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { message } from 'ant-design-vue';
import { reactive, ref } from 'vue';
import { reloadApi } from '/@/api/support/reload-api';
import { smartSentry } from '/@/lib/smart-sentry';
import { SmartLoading } from '/@/components/framework/smart-loading';
// emit
const emit = defineEmits(['refresh']);
defineExpose({
showModal,
});
// ----------------------- 表单 隐藏 与 显示 ------------------------
// 是否展示
const visible = ref(false);
function showModal(tag) {
form.tag = tag;
form.identification = '';
form.args = '';
visible.value = true;
}
function onClose() {
Object.assign(form, formDefault);
visible.value = false;
}
// 组件
const formRef = ref();
const formDefault = {
tag: '',
identification: '',
args: '',
};
let form = reactive({ ...formDefault });
const rules = {
identification: [{ required: true, message: '请输入运行标识' }],
args: [{ required: true, message: '请输入参数值' }],
};
// ----------------------- 提交 ------------------------
function onSubmit() {
formRef.value
.validate()
.then(async () => {
SmartLoading.show();
try {
await reloadApi.reload(form);
message.success('reload成功');
emit('refresh');
onClose();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
})
.catch((error) => {
console.log('error', error);
message.error('参数验证错误,请仔细填写表单数据!');
});
}
</script>

View File

@@ -0,0 +1,133 @@
<!--
* reload
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-07-21 21:55:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-card size="small" :bordered="false" :hoverable="true">
<a-alert>
<template v-slot:message>
<h4>Smart-Reload 心跳服务介绍</h4>
</template>
<template v-slot:description>
<pre>
简介SmartReload是一个可以在不重启进程的情况下动态重新加载配置或者执行某些预先设置的代码
原理
- Java后端会在项目启动的时候开启一个Daemon线程这个Daemon线程会每隔几秒轮询t_smart_item表的状态
- 如果状态标识上次状态标识比较发生变化会将参数传入SmartReload实现类进行自定义操作
用途
· 用于刷新内存中的缓存
· 用于执行某些后门代码
· 用于进行Java热加载前提是类结构不发生变化
· 其他不能重启服务的应用
</pre
>
</template>
</a-alert>
<a-row justify="end">
<TableOperator class="smart-margin-bottom5 smart-margin-top5" v-model="columns" :tableId="TABLE_ID_CONST.SUPPORT.RELOAD" :refresh="ajaxQuery" />
</a-row>
<a-table
size="small"
bordered
class="smart-margin-top10"
:dataSource="tableData"
:loading="tableLoading"
:columns="columns"
rowKey="tag"
:pagination="false"
>
<template #bodyCell="{ record, column }">
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button @click="doReload(record.tag)" v-privilege="'support:reload:execute'" type="link">执行</a-button>
<a-button @click="showResultList(record.tag)" v-privilege="'support:reload:result'" type="link">查看结果</a-button>
</div>
</template>
</template>
</a-table>
<DoReloadForm @refresh="ajaxQuery" ref="doReloadForm" />
<ReloadResultList ref="reloadResultList" />
</a-card>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import DoReloadForm from './do-reload-form-modal.vue';
import ReloadResultList from './reload-result-list.vue';
import { reloadApi } from '/@/api/support/reload-api';
import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
//------------------------ 表格渲染 ---------------------
const columns = ref([
{
title: '标签',
dataIndex: 'tag',
width: 200,
},
{
title: '运行标识',
dataIndex: 'identification',
},
{
title: '参数',
dataIndex: 'args',
},
{
title: '更新时间',
dataIndex: 'updateTime',
width: 150,
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 150,
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 150,
},
]);
const tableLoading = ref(false);
const tableData = ref([]);
async function ajaxQuery() {
try {
tableLoading.value = true;
let res = await reloadApi.queryList();
tableData.value = res.data;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
onMounted(ajaxQuery);
// ------------------------------ 表格操作列: 执行 reload ------------------------------
const doReloadForm = ref();
function doReload(tag) {
doReloadForm.value.showModal(tag);
}
// ------------------------------ 表格操作列: 查看执行结果 ------------------------------
const reloadResultList = ref();
function showResultList(tag) {
reloadResultList.value.showModal(tag);
}
</script>

View File

@@ -0,0 +1,100 @@
<!--
* reload 结果
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-07-21 21:55:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal :open="visible" title="reload结果列表" width="60%" :footer="null" @cancel="onClose">
<a-button type="primary" @click="ajaxQuery" size="small">
<template #icon>
<ReloadOutlined />
</template>
刷新
</a-button>
<a-table :scroll="{ y: 350 }" size="small" bordered rowKey="id" class="smart-margin-top10" :dataSource="tableData" :columns="columns">
<template #bodyCell="{ text, column }">
<template v-if="column.dataIndex === 'result'">
<a-tag :color="text ? 'success' : 'error'">{{ text ? '成功' : '失败' }}</a-tag>
</template>
</template>
<template #expandedRowRender="{ record }">
<pre style="margin: 0; font-size: 12px">
{{ record.exception }}
</pre>
</template>
</a-table>
</a-modal>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { reloadApi } from '/@/api/support/reload-api';
import { smartSentry } from '/@/lib/smart-sentry';
defineExpose({
showModal,
});
// ----------------------- 表单 隐藏 与 显示 ------------------------
// 是否展示
const visible = ref(false);
function showModal(tag) {
queryTag = tag;
ajaxQuery();
visible.value = true;
}
function onClose() {
visible.value = false;
}
//------------------------ 表格查询 ---------------------
let queryTag = '';
const tableLoading = ref(false);
const tableData = ref([]);
async function ajaxQuery() {
try {
tableLoading.value = true;
let res = await reloadApi.queryReloadResult(queryTag);
let count = 1;
for (const item of res.data) {
item.id = count++;
}
tableData.value = res.data;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
//------------------------ 表格列 ---------------------
const columns = reactive([
{
title: '标签',
dataIndex: 'tag',
},
{
title: '参数',
dataIndex: 'args',
},
{
title: '运行结果',
dataIndex: 'result',
},
{
title: '异常',
dataIndex: 'exception',
ellipsis: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
},
]);
</script>

View File

@@ -0,0 +1,107 @@
<!--
* 生成
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-07-21 21:55:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal :open="visible" title="生成单号" ok-text="生成" cancel-text="关闭" @ok="onSubmit" @cancel="onClose">
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }">
<a-form-item label="业务">
<a-input v-model:value="form.businessName" :disabled="true" />
</a-form-item>
<a-form-item label="格式">
<a-input v-model:value="form.format" :disabled="true" />
</a-form-item>
<a-form-item label="循环周期">
<a-input v-model:value="form.ruleType" :disabled="true" />
</a-form-item>
<a-form-item label="上次产生单号">
<a-input v-model:value="form.lastNumber" :disabled="true" />
</a-form-item>
<a-form-item label="生成数量" name="count">
<a-input-number v-model:value="form.count" />
</a-form-item>
<a-form-item label="生成结果">
<a-textarea v-model:value="generateResult" :rows="2" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { message } from 'ant-design-vue';
import { reactive, ref } from 'vue';
import { serialNumberApi } from '/@/api/support/serial-number-api';
import { SmartLoading } from '/@/components/framework/smart-loading';
import _ from 'lodash';
import { smartSentry } from '/@/lib/smart-sentry';
// emit
const emit = defineEmits(['refresh']);
defineExpose({
showModal,
});
// ----------------------- 表单 隐藏 与 显示 ------------------------
// 是否展示
const visible = ref(false);
function showModal(data) {
form.serialNumberId = data.serialNumberId;
form.businessName = data.businessName;
form.format = data.format;
form.ruleType = data.ruleType;
form.lastNumber = data.lastNumber;
form.count = 1;
generateResult.value = '';
visible.value = true;
}
function onClose() {
visible.value = false;
emit('refresh');
}
// ----------------------- 表单 ------------------------
const rules = {
count: [{ required: true, message: '请输入数量' }],
};
//生成结果
const generateResult = ref('');
// 组件
const formRef = ref();
const form = reactive({
serialNumberId: -1,
businessName: '',
format: '',
ruleType: '',
lastNumber: -1,
count: 1,
});
function onSubmit() {
formRef.value
.validate()
.then(async () => {
SmartLoading.show();
try {
let res = await serialNumberApi.generate(form);
message.success('生成成功');
generateResult.value = _.join(res.data, ', ');
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
})
.catch((error) => {
console.log('error', error);
message.error('参数验证错误,请仔细填写表单数据!');
});
}
</script>

View File

@@ -0,0 +1,147 @@
<!--
* 单号
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-07-21 21:55:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-card size="small" :bordered="false" :hoverable="true">
<a-alert>
<template v-slot:message>
<h4>SerialNumber 单号生成器介绍</h4>
</template>
<template v-slot:description>
<pre>
简介SerialNumber是一个可以根据不同的日期规则生成一系列特别单号的功能比如订单号合同号采购单号等等
原理内部有三种实现方式 1) 基于内存锁实现 不支持分布式和集群 2) 基于redis锁实现 3) 基于Mysql 锁for update 实现
- 支持随机生成和查询生成记录
- 支持动态配置
</pre
>
</template>
</a-alert>
<a-row justify="end">
<TableOperator
class="smart-margin-bottom5 smart-margin-top5"
v-model="columns"
:tableId="TABLE_ID_CONST.SUPPORT.SERIAL_NUMBER"
:refresh="ajaxQuery"
/>
</a-row>
<a-table
size="small"
:loading="tableLoading"
bordered
class="smart-margin-top10"
:dataSource="tableData"
:columns="columns"
rowKey="tag"
:pagination="false"
>
<template #bodyCell="{ record, column }">
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button @click="generate(record)" v-privilege="'support:serialNumber:generate'" type="link">生成</a-button>
<a-button @click="showRecord(record.serialNumberId)" v-privilege="'support:serialNumber:record'" type="link">查看记录</a-button>
</div>
</template>
</template>
</a-table>
</a-card>
<!---生成表单--->
<SerialNumberGenerateFormModal ref="generateForm" @refresh="ajaxQuery" />
<!---生成记录--->
<SerialNumberRecordList ref="recordList" />
</template>
<script setup>
import { onMounted, ref } from 'vue';
import SerialNumberGenerateFormModal from './serial-number-generate-form-modal.vue';
import SerialNumberRecordList from './serial-number-record-list.vue';
import { serialNumberApi } from '/@/api/support/serial-number-api';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
import { smartSentry } from '/@/lib/smart-sentry';
//------------------------ 表格渲染 ---------------------
const columns = ref([
{
title: 'ID',
dataIndex: 'serialNumberId',
},
{
title: '业务',
dataIndex: 'businessName',
},
{
title: '格式',
dataIndex: 'format',
},
{
title: '循环周期',
dataIndex: 'ruleType',
},
{
title: '初始值',
dataIndex: 'initNumber',
},
{
title: '随机增量',
dataIndex: 'stepRandomRange',
},
{
title: '备注',
dataIndex: 'remark',
},
{
title: '上次产生单号',
dataIndex: 'lastNumber',
},
{
title: '上次产生时间',
dataIndex: 'lastTime',
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 140,
},
]);
const tableLoading = ref(false);
const tableData = ref([]);
async function ajaxQuery() {
try {
tableLoading.value = true;
let res = await serialNumberApi.getAll();
tableData.value = res.data;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
onMounted(ajaxQuery);
// ------------------------------ 表格操作列: 生成 ------------------------------
const generateForm = ref();
function generate(record) {
generateForm.value.showModal(record);
}
// ------------------------------ 表格操作列: 查看结果 ------------------------------
const recordList = ref();
function showRecord(serialNumberId) {
recordList.value.showModal(serialNumberId);
}
</script>

View File

@@ -0,0 +1,115 @@
<!--
* 单号 记录
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-07-21 21:55:12
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal :open="visible" title="每日生成结果记录" width="60%" :footer="null" @cancel="onClose">
<a-table size="small" :dataSource="tableData" :columns="columns" bordered :pagination="false">
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'successFlag'">
<a-tag :color="text ? 'success' : 'error'">{{ text ? '成功' : '失败' }}</a-tag>
</template>
<template v-else-if="column.dataIndex === 'action'">
<a-button @click="showDetail(record.operateLogId)" type="link">详情</a-button>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `${total}`"
/>
</div>
</a-modal>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { serialNumberApi } from '/@/api/support/serial-number-api';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { smartSentry } from '/@/lib/smart-sentry';
defineExpose({
showModal,
});
// ----------------------- 表单 隐藏 与 显示 ------------------------
// 是否展示
const visible = ref(false);
function showModal(id) {
queryForm.serialNumberId = id;
queryForm.pageNum = 1;
queryForm.pageSize = 10;
ajaxQuery();
visible.value = true;
}
function onClose() {
visible.value = false;
}
// ----------------------- 表格 ------------------------
const columns = reactive([
{
title: '单号ID',
dataIndex: 'serialNumberId',
width: 70,
},
{
title: '日期',
dataIndex: 'recordDate',
},
{
title: '生成数量',
dataIndex: 'count',
},
{
title: '最后更新值',
dataIndex: 'lastNumber',
},
{
title: '上次生成时间',
dataIndex: 'lastTime',
},
]);
const queryForm = reactive({
serialNumberId: -1,
pageNum: 1,
pageSize: 10,
});
const tableLoading = ref(false);
const tableData = ref([]);
const total = ref(0);
async function ajaxQuery() {
try {
tableLoading.value = true;
let responseModel = await serialNumberApi.queryRecord(queryForm);
const list = responseModel.data.list;
total.value = responseModel.data.total;
tableData.value = list;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
</script>

View File

@@ -0,0 +1,25 @@
<!--
* 403 无权限 页面
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-result status="403" title="对不起,您没有权限访问此内容">
<template #extra>
<a-button type="primary" @click="goHome">返回首页</a-button>
</template>
</a-result>
</template>
<script setup>
import { useRouter } from 'vue-router';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
const router = useRouter();
function goHome() {
router.push({ name: HOME_PAGE_NAME });
}
</script>

View File

@@ -0,0 +1,25 @@
<!--
* 404 不存在 页面
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-result status="404" title="对不起,您访问的内容不存在!">
<template #extra>
<a-button type="primary" @click="goHome">返回首页</a-button>
</template>
</a-result>
</template>
<script setup>
import { useRouter } from 'vue-router';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
const router = useRouter();
function goHome() {
router.push({ name: HOME_PAGE_NAME });
}
</script>

View File

@@ -0,0 +1,39 @@
import { defineAsyncComponent, markRaw } from 'vue';
/**
* 菜单展示
* defineAsyncComponent 异步组件
* markRaw 将一个Vue组件对象转换为响应式对象时可能会导致不必要的性能开销。使用markRaw方法将组件对象标记为非响应式对象
*/
export const ACCOUNT_MENU = {
CENTER: {
menuId: 'center',
menuName: '个人中心',
components: markRaw(defineAsyncComponent(() => import('./components/center/index.vue'))),
},
PASSWORD: {
menuId: 'password',
menuName: '修改密码',
components: markRaw(defineAsyncComponent(() => import('./components/password/index.vue'))),
},
MESSAGE: {
menuId: 'message',
menuName: '我的消息',
components: markRaw(defineAsyncComponent(() => import('./components/message/index.vue'))),
},
NOTICE: {
menuId: 'notice',
menuName: '通知公告',
components: markRaw(defineAsyncComponent(() => import('./components/notice/index.vue'))),
},
LOGIN_LOG: {
menuId: 'login-log',
menuName: '登录日志',
components: markRaw(defineAsyncComponent(() => import('./components/login-log/index.vue'))),
},
OPERATE_LOG: {
menuId: 'operate-log',
menuName: '操作日志',
components: markRaw(defineAsyncComponent(() => import('./components/operate-log/index.vue'))),
},
};

View File

@@ -0,0 +1,280 @@
<template>
<div class="center-container">
<!-- 页面标题-->
<div class="header-title">个人中心</div>
<!-- 内容区域-->
<div class="center-form-area">
<a-row>
<a-col flex="350px">
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
<a-form-item label="登录账号" name="loginName">
<a-input class="form-item" v-model:value.trim="form.loginName" placeholder="请输入登录账号" disabled />
</a-form-item>
<a-form-item label="员工名称" name="actualName">
<a-input class="form-item" v-model:value.trim="form.actualName" placeholder="请输入员工名称" />
</a-form-item>
<a-form-item label="性别" name="gender">
<smart-enum-select class="form-item" v-model:value="form.gender" placeholder="请选择性别" enum-name="GENDER_ENUM" />
</a-form-item>
<a-form-item label="手机号码" name="phone">
<a-input class="form-item" v-model:value.trim="form.phone" placeholder="请输入手机号码" />
</a-form-item>
<a-form-item label="部门" name="departmentId">
<DepartmentTreeSelect class="form-item" ref="departmentTreeSelect" width="100%" :init="false" v-model:value="form.departmentId" />
</a-form-item>
<a-form-item label="备注" name="remark">
<a-textarea class="form-item" v-model:value="form.remark" placeholder="请输入备注" :rows="4" />
</a-form-item>
</a-form>
<a-button type="primary" @click="onSubmit">更新个人信息</a-button>
</a-col>
<a-col flex="auto">
<a-form style="padding-left: 80px" layout="vertical">
<a-form-item label="头像" name="avatar">
<br />
<a-upload
name="avatar"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:headers="{ 'x-access-token': useUserStore().getToken }"
:customRequest="customRequest"
:before-upload="beforeUpload"
>
<div v-if="avatarUrl" class="avatar-container">
<img :src="avatarUrl" class="avatar-image" alt="avatar" />
<div class="overlay">
<span>更新头像</span>
</div>
</div>
<div v-else>
<loading-outlined v-if="updateAvatarLoading" />
<plus-outlined v-else />
<div class="ant-upload-text">上传头像</div>
</div>
</a-upload>
</a-form-item>
</a-form>
</a-col>
</a-row>
</div>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { regular } from '/@/constants/regular-const.js';
import DepartmentTreeSelect from '/@/components/system/department-tree-select/index.vue';
import SmartEnumSelect from '/@/components/framework/smart-enum-select/index.vue';
import { loginApi } from '/@/api/system/login-api.js';
import { useUserStore } from '/@/store/modules/system/user.js';
import { message } from 'ant-design-vue';
import { smartSentry } from '/@/lib/smart-sentry.js';
import { employeeApi } from '/@/api/system/employee-api';
import { SmartLoading } from '/@/components/framework/smart-loading/index.js';
import { fileApi } from '/@/api/support/file-api.js';
import { FILE_FOLDER_TYPE_ENUM } from '/@/constants/support/file-const.js';
// 组件ref
const formRef = ref();
const formDefault = {
// 员工ID
employeeId: undefined,
// 头像
avatar: undefined,
// 登录账号
loginName: '',
// 员工名称
actualName: '',
// 性别
gender: undefined,
// 手机号码
phone: '',
// 部门id
departmentId: undefined,
// 是否启用
disabledFlag: undefined,
// 邮箱
email: undefined,
// 备注
remark: '',
};
let form = reactive({ ...formDefault });
const rules = {
actualName: [
{ required: true, message: '姓名不能为空' },
{ max: 30, message: '姓名不能大于30个字符', trigger: 'blur' },
],
phone: [
{ required: true, message: '手机号不能为空' },
{ pattern: regular.phone, message: '请输入正确的手机号码', trigger: 'blur' },
],
gender: [{ required: true, message: '性别不能为空' }],
departmentId: [{ required: true, message: '部门不能为空' }],
};
// 头像地址
let avatarUrl = ref();
// 查询登录信息
async function getLoginInfo() {
try {
//获取登录用户信息
const res = await loginApi.getLoginInfo();
let data = res.data;
//更新用户信息到pinia
useUserStore().setUserLoginInfo(data);
// 当前form展示
form.employeeId = data.employeeId;
form.loginName = data.loginName;
form.actualName = data.actualName;
form.email = data.email;
form.gender = data.gender;
form.phone = data.phone;
form.departmentId = data.departmentId;
form.disabledFlag = data.disabledFlag;
form.remark = data.remark;
// 头像展示
avatarUrl.value = data.avatar;
} catch (e) {
smartSentry.captureError(e);
}
}
// 头像上传
const accept = ref('.jpg,.jpeg,.png,.gif');
const maxSize = ref(10);
const folder = ref(FILE_FOLDER_TYPE_ENUM.COMMON.value);
let updateAvatarLoading = ref(false);
function beforeUpload(file, files) {
const suffixIndex = file.name.lastIndexOf('.');
const fileSuffix = file.name.substring(suffixIndex <= -1 ? 0 : suffixIndex);
if (accept.value.indexOf(fileSuffix) === -1) {
message.error(`只支持上传 ${accept.value.replaceAll(',', ' ')} 格式的文件`);
return false;
}
const isLimitSize = file.size / 1024 / 1024 < maxSize.value;
if (!isLimitSize) {
message.error(`单个文件大小必须小于 ${maxSize.value} Mb`);
return false;
}
return true;
}
async function customRequest(options) {
updateAvatarLoading.value = true;
try {
const formData = new FormData();
formData.append('file', options.file);
let res = await fileApi.uploadFile(formData, folder.value);
let file = res.data;
avatarUrl.value = file.fileUrl;
// 更新头像
let updateAvatarForm = { avatar: file.fileKey };
await employeeApi.updateAvatar(updateAvatarForm);
message.success('更新成功');
// 重新获取详情,刷新整体缓存
await getLoginInfo();
} catch (e) {
smartSentry.captureError(e);
} finally {
updateAvatarLoading.value = false;
}
}
// 更新员工信息
async function updateEmployee() {
SmartLoading.show();
try {
await employeeApi.updateByLogin(form);
message.success('更新成功');
// 重新获取详情,刷新整体缓存
await getLoginInfo();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
}
// 表单提交
function onSubmit() {
formRef.value
.validate()
.then(() => {
updateEmployee();
})
.catch((error) => {
console.log('error', error);
message.error('参数验证错误,请仔细填写表单数据!');
});
}
onMounted(() => {
getLoginInfo();
});
</script>
<style lang="less" scoped>
.center-container {
.header-title {
font-size: 20px;
}
.center-form-area {
margin-top: 20px;
.avatar-container {
position: relative;
border-radius: 50%;
overflow: hidden;
width: 100%;
height: 100%;
.avatar-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
opacity: 0;
transition: opacity 0.3s ease;
display: flex;
justify-content: center;
align-items: center;
color: #ffffff;
font-size: 17px;
}
&:hover .overlay {
opacity: 1; /* 鼠标悬停时显示蒙版 */
}
}
.avatar-uploader {
:deep(.ant-upload) {
border-radius: 50%;
width: 150px;
height: 150px;
}
}
.ant-upload-select-picture-card i {
font-size: 32px;
color: #999;
}
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}
}
}
</style>

View File

@@ -0,0 +1,192 @@
<!--
* 登录登出 日志
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-06-02 20:23:08
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-form class="smart-query-form" ref="queryFormRef">
<a-row class="smart-query-form-row">
<a-form-item label="时间" class="smart-query-form-item">
<a-range-picker @change="changeCreateDate" v-model:value="createDateRange" :presets="defaultChooseTimeRange" style="width: 240px" />
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-table size="small" :dataSource="tableData" :columns="columns" bordered rowKey="loginLogId" :pagination="false" :loading="tableLoading">
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'loginResult'">
<template v-if="text === LOGIN_RESULT_ENUM.LOGIN_SUCCESS.value">
<a-tag color="success">登录成功</a-tag>
</template>
<template v-if="text === LOGIN_RESULT_ENUM.LOGIN_FAIL.value">
<a-tag color="error">登录失败</a-tag>
</template>
<template v-if="text === LOGIN_RESULT_ENUM.LOGIN_OUT.value">
<a-tag color="processing">退出登录</a-tag>
</template>
</template>
<template v-if="column.dataIndex === 'userAgent'">
<div>{{ record.browser }} / {{ record.os }} / {{ record.device }}</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `${total}`"
/>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { defaultTimeRanges } from '/@/lib/default-time-ranges';
import uaparser from 'ua-parser-js';
import { LOGIN_RESULT_ENUM } from '/@/constants/support/login-log-const';
import { loginLogApi } from '/@/api/support/login-log-api';
import { smartSentry } from '/@/lib/smart-sentry';
import { calcTableHeight } from '/@/lib/table-auto-height';
const columns = ref([
{
title: '时间',
dataIndex: 'createTime',
width: 150,
},
{
title: '登录方式',
dataIndex: 'remark',
ellipsis: true,
width: 90,
},
{
title: '登录设备',
dataIndex: 'userAgent',
ellipsis: true,
},
{
title: 'IP地区',
dataIndex: 'loginIpRegion',
ellipsis: true,
},
{
title: 'IP',
dataIndex: 'loginIp',
ellipsis: true,
width: 120,
},
{
title: '结果',
dataIndex: 'loginResult',
ellipsis: true,
width: 90,
},
]);
const queryFormState = {
userName: '',
ip: '',
startDate: undefined,
endDate: undefined,
pageNum: 1,
pageSize: 10,
};
const queryForm = reactive({ ...queryFormState });
const createDateRange = ref([]);
const defaultChooseTimeRange = defaultTimeRanges;
// 时间变动
function changeCreateDate(dates, dateStrings) {
queryForm.startDate = dateStrings[0];
queryForm.endDate = dateStrings[1];
}
const tableLoading = ref(false);
const tableData = ref([]);
const total = ref(0);
function resetQuery() {
Object.assign(queryForm, queryFormState);
createDateRange.value = [];
ajaxQuery();
}
function onSearch() {
queryForm.pageNum = 1;
ajaxQuery();
}
async function ajaxQuery() {
try {
tableLoading.value = true;
let responseModel = await loginLogApi.queryListLogin(queryForm);
for (const e of responseModel.data.list) {
if (!e.userAgent) {
continue;
}
let ua = uaparser(e.userAgent);
e.browser = ua.browser.name;
e.os = ua.os.name;
e.device = ua.device.vendor ? ua.device.vendor + ua.device.model : '';
}
const list = responseModel.data.list;
total.value = responseModel.data.total;
tableData.value = list;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
// ----------------- 表格自适应高度 --------------------
const scrollY = ref(100);
const queryFormRef = ref();
function autoCalcTableHeight() {
calcTableHeight(scrollY, [queryFormRef], 10);
}
window.addEventListener('resize', autoCalcTableHeight);
onMounted(() => {
ajaxQuery();
autoCalcTableHeight();
});
onUnmounted(() => {
window.removeEventListener('resize', autoCalcTableHeight);
});
</script>

View File

@@ -0,0 +1,46 @@
<template>
<a-drawer v-model:open="showFlag" :width="800" title="消息内容" placement="right" :destroyOnClose="true">
<a-descriptions bordered :column="2" size="small">
<a-descriptions-item :labelStyle="{ width: '80px' }" :span="1" label="类型"
>{{ $smartEnumPlugin.getDescByValue('MESSAGE_TYPE_ENUM', messageDetail.messageType) }}
</a-descriptions-item>
<a-descriptions-item :labelStyle="{ width: '120px' }" :span="1" label="发送时间">{{ messageDetail.createTime }}</a-descriptions-item>
<a-descriptions-item :labelStyle="{ width: '80px' }" :span="2" label="标题">{{ messageDetail.title }}</a-descriptions-item>
<a-descriptions-item :labelStyle="{ width: '80px' }" :span="2" label="内容">
<pre>{{ messageDetail.content }}</pre>
</a-descriptions-item>
</a-descriptions>
</a-drawer>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { messageApi } from '/@/api/support/message-api.js';
import { useUserStore } from '/@/store/modules/system/user.js';
const emit = defineEmits(['refresh']);
const messageDetail = reactive({
messageType: '',
title: '',
content: '',
createTime: '',
});
const showFlag = ref(false);
function show(data) {
Object.assign(messageDetail, data);
showFlag.value = true;
read(data);
}
async function read(message) {
if (!message.readFlag) {
await messageApi.updateReadFlag(message.messageId);
await useUserStore().queryUnreadMessageCount();
emit('refresh');
}
}
defineExpose({ show });
</script>

View File

@@ -0,0 +1,171 @@
<template>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="关键字" class="smart-query-form-item">
<a-input style="width: 300px" v-model:value.trim="queryForm.searchWord" placeholder="标题/内容" />
</a-form-item>
<a-form-item label="类型" class="smart-query-form-item">
<smart-enum-select style="width: 150px" v-model:value="queryForm.messageType" placeholder="消息类型" enum-name="MESSAGE_TYPE_ENUM" />
</a-form-item>
<a-form-item label="消息时间" class="smart-query-form-item">
<a-space direction="vertical" :size="12">
<a-range-picker v-model:value="searchDate" @change="dateChange" style="width: 220px" />
</a-space>
</a-form-item>
<a-form-item label="已读" class="smart-query-form-item">
<a-radio-group v-model:value="queryForm.readFlag" @change="quickQuery">
<a-radio-button :value="null">全部</a-radio-button>
<a-radio-button :value="false">未读</a-radio-button>
<a-radio-button :value="true">已读</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="quickQuery">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-table size="small" :dataSource="tableData" :columns="columns" rowKey="messageId" :pagination="false" bordered>
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'messageType'">
<span>{{ $smartEnumPlugin.getDescByValue('MESSAGE_TYPE_ENUM', text) }}</span>
</template>
<template v-if="column.dataIndex === 'readFlag'">
<span v-show="record.readFlag">已读</span>
<span v-show="!record.readFlag" style="color: red">未读</span>
</template>
<template v-if="column.dataIndex === 'title'">
<span v-show="record.readFlag">
<a @click="toDetail(record)" style="color: #8c8c8c"
>{{ $smartEnumPlugin.getDescByValue('MESSAGE_TYPE_ENUM', record.messageType) }}{{ text }}</a
>
</span>
<span v-show="!record.readFlag">
<a @click="toDetail(record)">{{ $smartEnumPlugin.getDescByValue('MESSAGE_TYPE_ENUM', record.messageType) }}{{ text }} </a>
</span>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `${total}`"
/>
</div>
<MessageDetail ref="messageDetailRef" @refresh="ajaxQuery" />
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue';
import { messageApi } from '/@/api/support/message-api';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import SmartEnumSelect from '/@/components/framework/smart-enum-select//index.vue';
import { smartSentry } from '/@/lib/smart-sentry.js';
import MessageDetail from './components/message-detail.vue';
const columns = reactive([
{
title: '消息',
dataIndex: 'title',
},
{
title: '已读',
width: 80,
dataIndex: 'readFlag',
},
{
title: '时间',
dataIndex: 'createTime',
width: 180,
},
]);
const queryFormState = {
searchWord: '',
messageType: null,
dataId: null,
readFlag: null,
endDate: null,
startDate: null,
pageNum: 1,
pageSize: PAGE_SIZE,
searchCount: true,
receiverType: null,
receiverId: null,
};
const queryForm = reactive({ ...queryFormState });
const tableLoading = ref(false);
const tableData = ref([]);
const total = ref(0);
// 日期选择
let searchDate = ref();
function dateChange(dates, dateStrings) {
queryForm.startDate = dateStrings[0];
queryForm.endDate = dateStrings[1];
}
function resetQuery() {
searchDate.value = [];
Object.assign(queryForm, queryFormState);
ajaxQuery();
}
function quickQuery() {
queryForm.pageNum = 1;
ajaxQuery();
}
// 查询
async function ajaxQuery() {
try {
tableLoading.value = true;
let responseModel = await messageApi.queryMessage(queryForm);
const list = responseModel.data.list;
total.value = responseModel.data.total;
tableData.value = list;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
// -------------------- 详情 -----------------------------------
const messageDetailRef = ref();
function toDetail(message) {
messageDetailRef.value.show(message);
}
onMounted(ajaxQuery);
</script>

View File

@@ -0,0 +1,6 @@
<template>
<NoticeEmployeeList />
</template>
<script setup>
import NoticeEmployeeList from '/@/views/business/oa/notice/notice-employee-list.vue';
</script>

View File

@@ -0,0 +1,193 @@
<!--
* 操作记录 列表
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-06-02 20:23:08
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="请求时间" class="smart-query-form-item">
<a-range-picker @change="changeCreateDate" v-model:value="createDateRange" :presets="defaultChooseTimeRange" style="width: 240px" />
</a-form-item>
<a-form-item label="快速筛选" class="smart-query-form-item">
<a-radio-group v-model:value="queryForm.successFlag" @change="onSearch">
<a-radio-button :value="undefined">全部</a-radio-button>
<a-radio-button :value="true">成功</a-radio-button>
<a-radio-button :value="false">失败</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="ajaxQuery">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<SearchOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-table size="small" :loading="tableLoading" :dataSource="tableData" :columns="columns" bordered rowKey="operateLogId" :pagination="false">
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'successFlag'">
<a-tag :color="text ? 'success' : 'error'">{{ text ? '成功' : '失败' }}</a-tag>
</template>
<template v-if="column.dataIndex === 'userAgent'">
<div>{{ record.browser }} / {{ record.os }} / {{ record.device }}</div>
</template>
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button @click="showDetail(record.operateLogId)" type="link">详情</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `${total}`"
/>
</div>
<OperateLogDetailModal ref="detailModal" />
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import OperateLogDetailModal from '/@/views/support/operate-log/operate-log-detail-modal.vue';
import { operateLogApi } from '/@/api/support/operate-log-api';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { defaultTimeRanges } from '/@/lib/default-time-ranges';
import uaparser from 'ua-parser-js';
import { smartSentry } from '/@/lib/smart-sentry';
const columns = ref([
{
title: '操作模块',
dataIndex: 'module',
ellipsis: true,
width: 120,
},
{
title: '操作内容',
dataIndex: 'content',
ellipsis: true,
},
{
title: 'IP地区',
dataIndex: 'ipRegion',
ellipsis: true,
width: 120,
},
{
title: '客户端',
dataIndex: 'userAgent',
ellipsis: true,
width: 140,
},
{
title: '时间',
dataIndex: 'createTime',
width: 150,
},
{
title: '结果',
dataIndex: 'successFlag',
width: 60,
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 60,
},
]);
const queryFormState = {
userName: '',
successFlag: undefined,
startDate: undefined,
endDate: undefined,
pageNum: 1,
pageSize: 10,
};
const queryForm = reactive({ ...queryFormState });
const createDateRange = ref([]);
const defaultChooseTimeRange = defaultTimeRanges;
// 时间变动
function changeCreateDate(dates, dateStrings) {
queryForm.startDate = dateStrings[0];
queryForm.endDate = dateStrings[1];
}
const tableLoading = ref(false);
const tableData = ref([]);
const total = ref(0);
function resetQuery() {
Object.assign(queryForm, queryFormState);
createDateRange.value = [];
ajaxQuery();
}
function onSearch() {
queryForm.pageNum = 1;
ajaxQuery();
}
async function ajaxQuery() {
try {
tableLoading.value = true;
let responseModel = await operateLogApi.queryListLogin(queryForm);
for (const e of responseModel.data.list) {
if (!e.userAgent) {
continue;
}
let ua = uaparser(e.userAgent);
e.browser = ua.browser.name;
e.os = ua.os.name;
e.device = ua.device.vendor ? ua.device.vendor + ua.device.model : '';
}
const list = responseModel.data.list;
total.value = responseModel.data.total;
tableData.value = list;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
onMounted(ajaxQuery);
// ---------------------- 详情 ----------------------
const detailModal = ref();
function showDetail(operateLogId) {
detailModal.value.show(operateLogId);
}
</script>

View File

@@ -0,0 +1,126 @@
<template>
<div class="password-container">
<!-- 内容区域-->
<div class="password-form-area">
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
<a-form-item label="原密码" name="oldPassword">
<a-input-password class="form-item" v-model:value.trim="form.oldPassword" type="password" placeholder="请输入原密码" />
</a-form-item>
<a-form-item label="新密码" name="newPassword" :help="tips">
<a-input-password class="form-item" v-model:value.trim="form.newPassword" type="password" placeholder="请输入新密码" />
</a-form-item>
<a-form-item label="确认密码" name="confirmPwd" :help="tips">
<a-input-password class="form-item" v-model:value.trim="form.confirmPwd" type="password" placeholder="请输入确认密码" />
</a-form-item>
</a-form>
<a-button type="primary" style="margin: 20px 0 0 250px" @click="onSubmit">修改密码</a-button>
</div>
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue';
import { message } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading/index.js';
import { employeeApi } from '/@/api/system/employee-api.js';
import { smartSentry } from '/@/lib/smart-sentry.js';
const emits = defineEmits(['onSuccess']);
const formRef = ref();
const passwordComplexityEnabledTips = '密码长度8-20位必须包含字母、数字、特殊符号@#$%^&*()_+-=)等三种字符'; //校验规则
const passwordTips = '密码长度至少8位';
const tips = ref(passwordTips);
const reg =
/^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z\W_!@#$%^&*`~()-+=]+$)(?![a-z0-9]+$)(?![a-z\W_!@#$%^&*`~()-+=]+$)(?![0-9\W_!@#$%^&*`~()-+=]+$)[a-zA-Z0-9\W_!@#$%^&*`~()-+=]{8,20}$/;
// 获取系统的密码复杂度
const passwordComplexityEnabledFlag = ref(false);
async function getPasswordComplexityEnabled() {
try {
SmartLoading.show();
let res = await employeeApi.getPasswordComplexityEnabled();
passwordComplexityEnabledFlag.value = res.data;
tips.value = passwordComplexityEnabledFlag.value ? passwordComplexityEnabledTips : passwordTips;
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
onMounted(getPasswordComplexityEnabled);
const passwordComplexityEnabledRules = {
oldPassword: [{ required: true, message: '请输入原密码' }],
newPassword: [{ required: true, type: 'string', pattern: reg, message: '密码格式错误' }],
confirmPwd: [{ required: true, type: 'string', pattern: reg, message: '请输入确认密码' }],
};
const commonRules = {
oldPassword: [{ required: true, message: '请输入原密码' }],
newPassword: [
{ required: true, message: '密码格式错误' },
{ min: 8, message: '密码长度至少8位' },
],
confirmPwd: [
{ required: true, message: '密码格式错误' },
{ min: 8, message: '密码长度至少8位' },
],
};
const rules = computed(() => {
return passwordComplexityEnabledFlag.value ? passwordComplexityEnabledRules : commonRules;
});
const formDefault = {
oldPassword: '',
newPassword: '',
};
let form = reactive({
...formDefault,
});
async function onSubmit() {
formRef.value
.validate()
.then(async () => {
if (form.newPassword !== form.confirmPwd) {
message.error('新密码与确认密码不一致');
return;
}
SmartLoading.show();
try {
await employeeApi.updateEmployeePassword(form);
message.success('修改成功');
form.oldPassword = '';
form.newPassword = '';
form.confirmPwd = '';
emits('onSuccess');
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
})
.catch((error) => {
console.log('error', error);
message.error('参数验证错误,请仔细填写表单数据!');
});
}
</script>
<style lang="less" scoped>
.password-container {
.header-title {
font-size: 20px;
}
.password-form-area {
margin-top: 30px;
.form-item {
width: 550px !important;
}
}
}
</style>

View File

@@ -0,0 +1,98 @@
<template>
<div class="account-container">
<!--菜单列-->
<div class="account-menu-list">
<a-menu v-model:selectedKeys="selectedKeys" mode="inline" @click="selectMenu($event.key)">
<a-menu-item v-for="item in menuList" :key="item.menuId">
<span v-if="item.menuId === 'message'">
{{ item.menuName }}
<a-badge :count="unreadMessageCount" style="margin-left: 10px" />
</span>
<span v-if="item.menuId !== 'message'">{{ item.menuName }} </span>
</a-menu-item>
</a-menu>
</div>
<!--内容区域-->
<div class="account-content">
<component :is="selectedMenu.components" />
</div>
</div>
</template>
<script setup>
import { computed, onMounted, ref, watch } from 'vue';
import _ from 'lodash';
import { ACCOUNT_MENU } from '/@/views/system/account/account-menu.js';
import { useRoute } from 'vue-router';
import { useUserStore } from '/@/store/modules/system/user.js';
// 菜单展示
let menuList = computed(() => {
return _.values(ACCOUNT_MENU);
});
// 选中的菜单
let selectedMenu = ref({ menuId: 0 });
let selectedKeys = computed(() => {
return _.isEmpty(selectedMenu.value) ? [] : [selectedMenu.value.menuId];
});
function selectMenu(menuId) {
selectedMenu.value = menuList.value.find((e) => e.menuId === menuId);
}
// ------------------------- 未读消息数量 -------------------------
const unreadMessageCount = computed(() => {
return useUserStore().unreadMessageCount;
});
// ------------------------- 绑定路由参数 -------------------------
const route = useRoute();
onMounted(() => {
if (_.isEmpty(menuList.value)) {
return;
}
let menuId;
if (route.query.menuId) {
menuId = route.query.menuId;
} else {
let firstMenu = menuList.value[0];
menuId = firstMenu.menuId;
}
selectMenu(menuId);
});
watch(
() => route.query,
(newQuery, oldQuery) => {
let menuId;
if (route.query.menuId) {
menuId = route.query.menuId;
} else {
let firstMenu = menuList.value[0];
menuId = firstMenu.menuId;
}
selectMenu(menuId);
}
);
</script>
<style lang="less" scoped>
.account-container {
display: flex;
height: 100%;
background-color: white;
padding: 20px 0;
.account-menu-list {
width: 180px;
height: calc(100% - 100);
border-right: solid 1px #efefef;
}
.account-content {
flex: 1;
margin-left: 10px;
background: #ffffff;
padding: 20px;
border-radius: 8px;
}
}
</style>

View File

@@ -0,0 +1,138 @@
<!--
* 部门表单 弹窗
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal v-model:open="visible" :title="formState.departmentId ? '编辑部门' : '添加部门'" @ok="handleOk" destroyOnClose>
<a-form ref="formRef" :model="formState" :rules="rules" layout="vertical">
<a-form-item label="上级部门" name="parentId" v-if="formState.parentId != 0">
<DepartmentTreeSelect ref="departmentTreeSelect" v-model:value="formState.parentId" :defaultValueFlag="false" width="100%" />
</a-form-item>
<a-form-item label="部门名称" name="name">
<a-input v-model:value.trim="formState.name" placeholder="请输入部门名称" />
</a-form-item>
<a-form-item label="部门负责人" name="managerId">
<EmployeeSelect ref="employeeSelect" placeholder="请选择部门负责人" width="100%" v-model:value="formState.managerId" :leaveFlag="false" />
</a-form-item>
<a-form-item label="部门排序 (值越大越靠前!)" name="sort">
<a-input-number style="width: 100%" v-model:value="formState.sort" :min="0" placeholder="请输入部门名称" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import message from 'ant-design-vue/lib/message';
import { reactive, ref } from 'vue';
import { departmentApi } from '/@/api/system/department-api';
import DepartmentTreeSelect from '/@/components/system/department-tree-select/index.vue';
import EmployeeSelect from '/@/components/system/employee-select/index.vue';
import { smartSentry } from '/@/lib/smart-sentry';
import { SmartLoading } from '/@/components/framework/smart-loading';
// ----------------------- 对外暴漏 ---------------------
defineExpose({
showModal,
});
// ----------------------- modal 的显示与隐藏 ---------------------
const emits = defineEmits(['refresh']);
const visible = ref(false);
function showModal(data) {
visible.value = true;
updateFormData(data);
}
function closeModal() {
visible.value = false;
resetFormData();
}
// ----------------------- form 表单操作 ---------------------
const formRef = ref();
const departmentTreeSelect = ref();
const defaultDepartmentForm = {
id: undefined,
managerId: undefined, //部门负责人
name: undefined,
parentId: undefined,
sort: 0,
};
const employeeSelect = ref();
let formState = reactive({
...defaultDepartmentForm,
});
// 表单校验规则
const rules = {
parentId: [{ required: true, message: '上级部门不能为空' }],
name: [
{ required: true, message: '部门名称不能为空' },
{ max: 50, message: '部门名称不能大于20个字符', trigger: 'blur' },
],
managerId: [{ required: true, message: '部门负责人不能为空' }],
};
// 更新表单数据
function updateFormData(data) {
Object.assign(formState, defaultDepartmentForm);
if (data) {
Object.assign(formState, data);
}
visible.value = true;
}
// 重置表单数据
function resetFormData() {
Object.assign(formState, defaultDepartmentForm);
}
async function handleOk() {
try {
await formRef.value.validate();
if (formState.departmentId) {
updateDepartment();
} else {
addDepartment();
}
} catch (error) {
message.error('参数验证错误,请仔细填写表单数据!');
}
}
// ----------------------- form 表单 ajax 操作 ---------------------
//添加部门ajax请求
async function addDepartment() {
SmartLoading.show();
try {
await departmentApi.addDepartment(formState);
emits('refresh');
closeModal();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
}
//更新部门ajax请求
async function updateDepartment() {
SmartLoading.show();
try {
if (formState.parentId == formState.departmentId) {
message.warning('上级菜单不能为自己');
return;
}
await departmentApi.updateDepartment(formState);
emits('refresh');
closeModal();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
}
</script>

View File

@@ -0,0 +1,258 @@
<template>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="部门名称" class="smart-query-form-item">
<a-input style="width: 300px" v-model:value="keywords" placeholder="请输入部门名称" />
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button v-privilege="'support:department:query'" type="primary" @click="onSearch">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
<a-button v-privilege="'support:department:query'" @click="resetQuery">
<template #icon>
<SearchOutlined />
</template>
重置
</a-button>
</a-button-group>
<a-button v-privilege="'system:department:add'" type="primary" @click="addDepartment" class="smart-margin-left20">
<template #icon>
<PlusOutlined />
</template>
新建
</a-button>
</a-form-item>
</a-row>
</a-form>
<a-card size="small" :bordered="true">
<a-table
size="small"
bordered
:loading="tableLoading"
rowKey="departmentId"
:columns="columns"
:data-source="departmentTreeData"
:defaultExpandAllRows="false"
:defaultExpandedRowKeys="defaultExpandedRowList"
:pagination="false"
>
<template #bodyCell="{ record, column }">
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button @click="addDepartment(record)" v-privilege="'system:department:add'" type="link">添加下级</a-button>
<a-button @click="updateDepartment(record)" v-privilege="'system:department:update'" type="link">编辑</a-button>
<a-button
danger
v-if="record.departmentId !== topDepartmentId"
v-privilege="'system:department:delete'"
@click="deleteDepartment(record.departmentId)"
type="link"
>删除</a-button
>
</div>
</template>
</template>
</a-table>
<!-- 添加编辑部门弹窗 -->
<DepartmentFormModal ref="departmentFormModal" @refresh="queryDepartmentTree" />
</a-card>
</template>
<script setup>
import { onMounted, reactive, ref, watch, createVNode } from 'vue';
import { departmentApi } from '/@/api/system/department-api';
import { Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import _ from 'lodash';
import { SmartLoading } from '/@/components/framework/smart-loading';
import DepartmentFormModal from './components/department-form-modal.vue';
import { smartSentry } from '/@/lib/smart-sentry';
const DEPARTMENT_PARENT_ID = 0;
// ----------------------- 筛选 ---------------------
const keywords = ref('');
// ----------------------- 部门树的展示 ---------------------
const tableLoading = ref(false);
const topDepartmentId = ref();
// 所有部门列表
const departmentList = ref([]);
// 部门树形数据
const departmentTreeData = ref([]);
// 存放部门id和部门用于查找
const idInfoMap = ref(new Map());
// 默认展开的行
const defaultExpandedRowList = reactive([]);
const columns = ref([
{
title: '部门名称',
dataIndex: 'name',
key: 'name',
},
{
title: '负责人',
dataIndex: 'managerName',
key: 'managerName',
width: 100,
},
{
title: '排序',
dataIndex: 'sort',
key: 'sort',
width: 100,
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 150,
},
{
title: '更新时间',
dataIndex: 'updateTime',
width: 150,
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 200,
},
]);
onMounted(() => {
queryDepartmentTree();
});
// 查询部门列表并构建 部门树
async function queryDepartmentTree() {
try {
tableLoading.value = true;
let res = await departmentApi.queryAllDepartment();
let data = res.data;
data.forEach((e) => {
idInfoMap.value.set(e.departmentId, e);
});
departmentList.value = data;
departmentTreeData.value = buildDepartmentTree(data, DEPARTMENT_PARENT_ID);
// 默认显示 最顶级ID为列表中返回的第一条数据的ID
if (!_.isEmpty(departmentTreeData.value) && departmentTreeData.value.length > 0) {
topDepartmentId.value = departmentTreeData.value[0].departmentId;
}
defaultExpandedRowList.value = [];
defaultExpandedRowList.push(topDepartmentId.value);
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
// 构建部门树
function buildDepartmentTree(data, parentId) {
let children = data.filter((e) => e.parentId === parentId) || [];
if (!_.isEmpty(children)) {
children.forEach((e) => {
e.children = buildDepartmentTree(data, e.departmentId);
});
return children;
}
return null;
}
// 重置
function resetQuery() {
keywords.value = '';
onSearch();
}
// 搜索
function onSearch() {
if (!keywords.value) {
departmentTreeData.value = buildDepartmentTree(departmentList.value, DEPARTMENT_PARENT_ID);
return;
}
let originData = departmentList.value.concat();
if (!originData) {
return;
}
// 筛选出名称符合的部门
let filterDepartment = originData.filter((e) => e.name.indexOf(keywords.value) > -1);
let filterDepartmentList = [];
// 循环筛选出的部门 构建部门树
filterDepartment.forEach((e) => {
recursionFilterDepartment(filterDepartmentList, e.departmentId, false);
});
departmentTreeData.value = buildDepartmentTree(filterDepartmentList, DEPARTMENT_PARENT_ID);
}
// 根据ID递归筛选部门
function recursionFilterDepartment(resList, id, unshift) {
let info = idInfoMap.value.get(id);
if (!info || resList.some((e) => e.departmentId === id)) {
return;
}
if (unshift) {
resList.unshift(info);
} else {
resList.push(info);
}
if (info.parentId && info.parentId !== 0) {
recursionFilterDepartment(resList, info.parentId, unshift);
}
}
// ----------------------- 表单操作:添加部门/修改部门/删除部门/上下移动 ---------------------
const departmentFormModal = ref();
// 添加
function addDepartment(e) {
let data = {
departmentId: 0,
name: '',
parentId: e.departmentId || null,
};
departmentFormModal.value.showModal(data);
}
// 编辑
function updateDepartment(e) {
departmentFormModal.value.showModal(e);
}
// 删除
function deleteDepartment(id) {
Modal.confirm({
title: '提醒',
icon: createVNode(ExclamationCircleOutlined),
content: '确定要删除该部门吗?',
okText: '删除',
okType: 'danger',
async onOk() {
SmartLoading.show();
try {
await departmentApi.deleteDepartment(id);
await queryDepartmentTree();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
},
cancelText: '取消',
onCancel() {},
});
}
</script>
<style scoped lang="less"></style>

View File

@@ -0,0 +1,58 @@
<!--
* 当前所选部门的子部门 人员管理右上半部分
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-card class="child-dept-container">
<a-breadcrumb>
<a-breadcrumb-item v-for="(item, index) in props.breadcrumb" :key="index">
{{ item }}
</a-breadcrumb-item>
</a-breadcrumb>
<a-list class="department-list" :data-source="props.selectedDepartmentChildren">
<template #renderItem="{ item }">
<a-list-item>
<div class="department-item" @click="selectTree(item.departmentId)">
{{ item.name }}
<RightOutlined />
</div>
</a-list-item>
</template>
</a-list>
</a-card>
</template>
<script setup>
import emitter from '../../department-mitt';
const props = defineProps({
breadcrumb: Array,
selectedDepartmentChildren: Array,
});
function selectTree(id) {
emitter.emit('selectTree', id);
}
</script>
<style scoped lang="less">
:deep(.ant-list-item) {
padding: 6px 0px;
}
.child-dept-container {
.department-list-box {
margin-top: 20px;
}
.department-list {
height: 170px;
overflow-y: auto;
}
.department-item {
cursor: pointer;
}
}
</style>

View File

@@ -0,0 +1,245 @@
<!--
* 部门树形结构
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-card class="tree-container">
<a-row class="smart-margin-bottom10">
<a-input v-model:value.trim="keywords" placeholder="请输入部门名称" />
</a-row>
<a-tree
v-if="!_.isEmpty(departmentTreeData)"
v-model:selectedKeys="selectedKeys"
v-model:checkedKeys="checkedKeys"
class="tree"
:treeData="departmentTreeData"
:fieldNames="{ title: 'name', key: 'departmentId', value: 'departmentId' }"
style="width: 100%; overflow-x: auto"
:style="[!height ? '' : { height: `${height}px`, overflowY: 'auto' }]"
:checkable="props.checkable"
:checkStrictly="props.checkStrictly"
:selectable="!props.checkable"
:defaultExpandAll="true"
@select="treeSelectChange"
>
<template #title="item">
<div>{{ item.name }}</div>
</template>
</a-tree>
<div class="no-data" v-else>暂无结果</div>
</a-card>
</template>
<script setup>
import { onMounted, onUnmounted, ref, watch } from 'vue';
import _ from 'lodash';
import { departmentApi } from '/@/api/system/department-api';
import departmentEmitter from '../../department-mitt';
import { smartSentry } from '/@/lib/smart-sentry';
const DEPARTMENT_PARENT_ID = 0;
// ----------------------- 组件参数 ---------------------
const props = defineProps({
// 是否可以选中
checkable: {
type: Boolean,
default: false,
},
// 父子节点选中状态不再关联
checkStrictly: {
type: Boolean,
default: false,
},
// 树高度 超出出滚动条
height: Number,
// 显示菜单
showMenu: {
type: Boolean,
default: false,
},
});
// ----------------------- 部门树的展示 ---------------------
const topDepartmentId = ref();
// 所有部门列表
const departmentList = ref([]);
// 部门树形数据
const departmentTreeData = ref([]);
// 存放部门id和部门用于查找
const idInfoMap = ref(new Map());
onMounted(() => {
queryDepartmentTree();
});
// 刷新
async function refresh() {
await queryDepartmentTree();
if (currentSelectedDepartmentId.value) {
selectTree(currentSelectedDepartmentId.value);
}
}
// 查询部门列表并构建 部门树
async function queryDepartmentTree() {
let res = await departmentApi.queryAllDepartment();
let data = res.data;
departmentList.value = data;
departmentTreeData.value = buildDepartmentTree(data, DEPARTMENT_PARENT_ID);
data.forEach((e) => {
idInfoMap.value.set(e.departmentId, e);
});
// 默认显示 最顶级ID为列表中返回的第一条数据的ID
if (!_.isEmpty(departmentTreeData.value) && departmentTreeData.value.length > 0) {
topDepartmentId.value = departmentTreeData.value[0].departmentId;
}
selectTree(departmentTreeData.value[0].departmentId);
}
// 构建部门树
function buildDepartmentTree(data, parentId) {
let children = data.filter((e) => e.parentId === parentId) || [];
children.forEach((e) => {
e.children = buildDepartmentTree(data, e.departmentId);
});
updateDepartmentPreIdAndNextId(children);
return children;
}
// 更新树的前置id和后置id
function updateDepartmentPreIdAndNextId(data) {
for (let index = 0; index < data.length; index++) {
if (index === 0) {
data[index].nextId = data.length > 1 ? data[1].departmentId : undefined;
continue;
}
if (index === data.length - 1) {
data[index].preId = data[index - 1].departmentId;
data[index].nextId = undefined;
continue;
}
data[index].preId = data[index - 1].departmentId;
data[index].nextId = data[index + 1].departmentId;
}
}
// ----------------------- 树的选中 ---------------------
const selectedKeys = ref([]);
const checkedKeys = ref([]);
const breadcrumb = ref([]);
const currentSelectedDepartmentId = ref();
const selectedDepartmentChildren = ref([]);
departmentEmitter.on('selectTree', selectTree);
function selectTree(id) {
selectedKeys.value = [id];
treeSelectChange(selectedKeys.value);
}
function treeSelectChange(idList) {
if (_.isEmpty(idList)) {
breadcrumb.value = [];
selectedDepartmentChildren.value = [];
return;
}
let id = idList[0];
selectedDepartmentChildren.value = departmentList.value.filter((e) => e.parentId == id);
let filterDepartmentList = [];
recursionFilterDepartment(filterDepartmentList, id, true);
breadcrumb.value = filterDepartmentList.map((e) => e.name);
}
// ----------------------- 筛选 ---------------------
const keywords = ref('');
watch(
() => keywords.value,
() => {
onSearch();
}
);
// 筛选
function onSearch() {
if (!keywords.value) {
departmentTreeData.value = buildDepartmentTree(departmentList.value, DEPARTMENT_PARENT_ID);
return;
}
let originData = departmentList.value.concat();
if (!originData) {
return;
}
// 筛选出名称符合的部门
let filterDepartment = originData.filter((e) => e.name.indexOf(keywords.value) > -1);
let filterDepartmentList = [];
// 循环筛选出的部门 构建部门树
filterDepartment.forEach((e) => {
recursionFilterDepartment(filterDepartmentList, e.departmentId, false);
});
departmentTreeData.value = buildDepartmentTree(filterDepartmentList, DEPARTMENT_PARENT_ID);
}
// 根据ID递归筛选部门
function recursionFilterDepartment(resList, id, unshift) {
let info = idInfoMap.value.get(id);
if (!info || resList.some((e) => e.departmentId == id)) {
return;
}
if (unshift) {
resList.unshift(info);
} else {
resList.push(info);
}
if (info.parentId && info.parentId != 0) {
recursionFilterDepartment(resList, info.parentId, unshift);
}
}
onUnmounted(() => {
departmentEmitter.all.clear();
});
// ----------------------- 以下是暴露的方法内容 ----------------------------
defineExpose({
queryDepartmentTree,
selectedDepartmentChildren,
breadcrumb,
selectedKeys,
checkedKeys,
keywords,
});
</script>
<style scoped lang="less">
.tree-container {
height: 100%;
.tree {
height: 618px;
margin-top: 10px;
overflow-x: hidden;
}
.sort-flag-row {
margin-top: 10px;
margin-bottom: 10px;
}
.sort-span {
margin-left: 5px;
}
.no-data {
margin: 10px;
}
}
</style>

View File

@@ -0,0 +1,94 @@
<!--
* 部门 员工 弹窗
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal v-model:open="visible" title="调整部门" :footer="null" destroyOnClose>
<DepartmentTree ref="departmentTree" :height="400" :showMenu="false" />
<div class="footer">
<a-button style="margin-right: 8px" @click="closeModal">取消</a-button>
<a-button type="primary" @click="handleOk">提交</a-button>
</div>
</a-modal>
</template>
<script setup>
import { message } from 'ant-design-vue';
import _ from 'lodash';
import { ref } from 'vue';
import DepartmentTree from '../department-tree/index.vue';
import { employeeApi } from '/@/api/system/employee-api';
import { smartSentry } from '/@/lib/smart-sentry';
import { SmartLoading } from '/@/components/framework/smart-loading';
// ----------------------- 以下是字段定义 emits props ---------------------
const emit = defineEmits(['refresh']);
// ----------------------- 显示/隐藏 ------------------------
const departmentTree = ref();
const visible = ref(false);
const employeeIdList = ref([]);
//显示
async function showModal(selectEmployeeId) {
employeeIdList.value = selectEmployeeId;
visible.value = true;
}
//隐藏
function closeModal() {
visible.value = false;
}
// ----------------------- form操作 ---------------------------------
async function handleOk() {
SmartLoading.show();
try {
if (_.isEmpty(employeeIdList.value)) {
message.warning('请选择要调整的员工');
return;
}
if (_.isEmpty(departmentTree.value.selectedKeys)) {
message.warning('请选择要调整的部门');
return;
}
let departmentId = departmentTree.value.selectedKeys[0];
let params = {
employeeIdList: employeeIdList.value,
departmentId: departmentId,
};
await employeeApi.batchUpdateDepartmentEmployee(params);
message.success('操作成功');
emit('refresh');
closeModal();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
}
// ----------------------- 以下是暴露的方法内容 ----------------------------
defineExpose({
showModal,
});
</script>
<style scoped lang="less">
.footer {
position: absolute;
right: 0;
bottom: 0;
width: 100%;
border-top: 1px solid #e9e9e9;
padding: 10px 16px;
background: #fff;
text-align: right;
z-index: 1;
}
</style>

View File

@@ -0,0 +1,236 @@
<!--
* 员工 表单 弹窗
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-drawer
:title="form.employeeId ? '编辑' : '添加'"
:width="600"
:open="visible"
:body-style="{ paddingBottom: '80px' }"
@close="onClose"
destroyOnClose
>
<a-alert message="超管需要直接在数据库表 t_employee修改哦" type="error" closable />
<br />
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
<a-form-item label="姓名" name="actualName">
<a-input v-model:value.trim="form.actualName" placeholder="请输入姓名" />
</a-form-item>
<a-form-item label="手机号" name="phone">
<a-input v-model:value.trim="form.phone" placeholder="请输入手机号" />
</a-form-item>
<a-form-item label="部门" name="departmentId">
<DepartmentTreeSelect ref="departmentTreeSelect" width="100%" :init="false" v-model:value="form.departmentId" />
</a-form-item>
<a-form-item label="登录名" name="loginName">
<a-input v-model:value.trim="form.loginName" placeholder="请输入登录名" />
<p class="hint">初始密码默认为随机</p>
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input v-model:value.trim="form.email" placeholder="请输入邮箱" />
</a-form-item>
<a-form-item label="性别" name="gender">
<smart-enum-select style="width: 100%" v-model:value="form.gender" placeholder="请选择性别" enum-name="GENDER_ENUM" />
</a-form-item>
<a-form-item label="状态" name="disabledFlag">
<a-select v-model:value="form.disabledFlag" placeholder="请选择状态">
<a-select-option :value="0">启用</a-select-option>
<a-select-option :value="1">禁用</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="职务" name="positionId">
<PositionSelect v-model:value="form.positionId" placeholder="请选择职务" />
</a-form-item>
<a-form-item label="角色" name="roleIdList">
<a-select mode="multiple" v-model:value="form.roleIdList" optionFilterProp="title" placeholder="请选择角色">
<a-select-option v-for="item in roleList" :key="item.roleId" :title="item.roleName">{{ item.roleName }}</a-select-option>
</a-select>
</a-form-item>
</a-form>
<div class="footer">
<a-button style="margin-right: 8px" @click="onClose">取消</a-button>
<a-button type="primary" style="margin-right: 8px" @click="onSubmit(false)">保存</a-button>
<a-button v-if="!form.employeeId" type="primary" @click="onSubmit(true)">保存并继续添加</a-button>
</div>
</a-drawer>
</template>
<script setup>
import { message } from 'ant-design-vue';
import _ from 'lodash';
import { nextTick, reactive, ref } from 'vue';
import { employeeApi } from '/@/api/system/employee-api';
import { roleApi } from '/@/api/system/role-api';
import DepartmentTreeSelect from '/@/components/system/department-tree-select/index.vue';
import SmartEnumSelect from '/@/components/framework/smart-enum-select/index.vue';
import PositionSelect from '/@/components/system/position-select/index.vue';
import { GENDER_ENUM } from '/@/constants/common-const';
import { regular } from '/@/constants/regular-const';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { smartSentry } from '/@/lib/smart-sentry';
// ----------------------- 以下是字段定义 emits props ---------------------
const departmentTreeSelect = ref();
// emit
const emit = defineEmits(['refresh', 'show-account']);
// ----------------------- 显示/隐藏 ---------------------
const visible = ref(false); // 是否展示抽屉
// 隐藏
function onClose() {
reset();
visible.value = false;
}
// 显示
async function showDrawer(rowData) {
Object.assign(form, formDefault);
if (rowData && !_.isEmpty(rowData)) {
Object.assign(form, rowData);
}
visible.value = true;
nextTick(() => {
queryAllRole();
});
}
// ----------------------- 表单显示 ---------------------
const roleList = ref([]); //角色列表
async function queryAllRole() {
let res = await roleApi.queryAll();
roleList.value = res.data;
}
const formRef = ref(); // 组件ref
const formDefault = {
employeeId: undefined,
actualName: undefined,
departmentId: undefined,
disabledFlag: 0,
leaveFlag: 0,
gender: GENDER_ENUM.MAN.value,
loginName: undefined,
phone: undefined,
roleIdList: undefined,
positionId: undefined,
};
let form = reactive(_.cloneDeep(formDefault));
function reset() {
Object.assign(form, formDefault);
formRef.value.resetFields();
}
// ----------------------- 表单提交 ---------------------
// 表单规则
const rules = {
actualName: [
{ required: true, message: '姓名不能为空' },
{ max: 30, message: '姓名不能大于30个字符', trigger: 'blur' },
],
phone: [
{ required: true, message: '手机号不能为空' },
{ pattern: regular.phone, message: '请输入正确的手机号码', trigger: 'blur' },
],
loginName: [
{ required: true, message: '登录账号不能为空' },
{ max: 30, message: '登录账号不能大于30个字符', trigger: 'blur' },
],
gender: [{ required: true, message: '性别不能为空' }],
departmentId: [{ required: true, message: '部门不能为空' }],
disabledFlag: [{ required: true, message: '状态不能为空' }],
leaveFlag: [{ required: true, message: '在职状态不能为空' }],
email: [{ required: true, message: '请输入邮箱' }],
};
// 校验表单
function validateForm(formRef) {
return new Promise((resolve) => {
formRef
.validate()
.then(() => {
resolve(true);
})
.catch(() => {
resolve(false);
});
});
}
// 提交数据
async function onSubmit(keepAdding) {
let validateFormRes = await validateForm(formRef.value);
if (!validateFormRes) {
message.error('参数验证错误,请仔细填写表单数据!');
return;
}
SmartLoading.show();
if (form.employeeId) {
await updateEmployee(keepAdding);
} else {
await addEmployee(keepAdding);
}
}
async function addEmployee(keepAdding) {
try {
let { data } = await employeeApi.addEmployee(form);
message.success('添加成功');
emit('show-account', form.loginName, data);
if (keepAdding) {
reset();
} else {
onClose();
}
emit('refresh');
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
}
async function updateEmployee(keepAdding) {
try {
let result = await employeeApi.updateEmployee(form);
message.success('更新成功');
if (keepAdding) {
reset();
} else {
onClose();
}
emit('refresh');
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
}
// ----------------------- 以下是暴露的方法内容 ----------------------------
defineExpose({
showDrawer,
});
</script>
<style scoped lang="less">
.footer {
position: absolute;
right: 0;
bottom: 0;
width: 100%;
border-top: 1px solid #e9e9e9;
padding: 10px 16px;
background: #fff;
text-align: right;
z-index: 1;
}
.hint {
margin-top: 5px;
color: #bfbfbf;
}
</style>

View File

@@ -0,0 +1,416 @@
<!--
* 员工 列表
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-card class="employee-container">
<div class="header">
<a-typography-title :level="5">部门人员</a-typography-title>
<div class="query-operate">
<a-radio-group v-model:value="params.disabledFlag" style="margin: 8px; flex-shrink: 0" @change="queryEmployeeByKeyword(false)">
<a-radio-button :value="undefined">全部</a-radio-button>
<a-radio-button :value="false">启用</a-radio-button>
<a-radio-button :value="true">禁用</a-radio-button>
</a-radio-group>
<a-input-search v-model:value.trim="params.keyword" placeholder="姓名/手机号/登录账号" @search="queryEmployeeByKeyword(true)">
<template #enterButton>
<a-button type="primary">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
</template>
</a-input-search>
<a-button @click="reset" class="smart-margin-left10">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
</div>
</div>
<div class="btn-group">
<a-button class="btn" type="primary" @click="showDrawer" v-privilege="'system:employee:add'">添加成员</a-button>
<a-button class="btn" @click="updateEmployeeDepartment" v-privilege="'system:employee:department:update'">调整部门</a-button>
<a-button class="btn" @click="batchDelete" v-privilege="'system:employee:delete'">批量删除</a-button>
<span class="smart-table-column-operate">
<TableOperator v-model="columns" :tableId="TABLE_ID_CONST.SYSTEM.EMPLOYEE" :refresh="queryEmployee" />
</span>
</div>
<a-table
:row-selection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
size="small"
:columns="columns"
:data-source="tableData"
:pagination="false"
:loading="tableLoading"
:scroll="{ x: 1500 }"
row-key="employeeId"
bordered
>
<template #bodyCell="{ text, record, index, column }">
<template v-if="column.dataIndex === 'administratorFlag'">
<a-tag color="error" v-if="text">超管</a-tag>
</template>
<template v-if="column.dataIndex === 'disabledFlag'">
<a-tag :color="text ? 'error' : 'processing'">{{ text ? '禁用' : '启用' }}</a-tag>
</template>
<template v-else-if="column.dataIndex === 'gender'">
<span>{{ $smartEnumPlugin.getDescByValue('GENDER_ENUM', text) }}</span>
</template>
<template v-else-if="column.dataIndex === 'operate'">
<div class="smart-table-operate">
<a-button v-privilege="'system:employee:update'" type="link" size="small" @click="showDrawer(record)">编辑</a-button>
<a-button
v-privilege="'system:employee:password:reset'"
type="link"
size="small"
@click="resetPassword(record.employeeId, record.loginName)"
>重置密码</a-button
>
<a-button v-privilege="'system:employee:disabled'" type="link" @click="updateDisabled(record.employeeId, record.disabledFlag)">{{
record.disabledFlag ? '启用' : '禁用'
}}</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="params.pageSize"
v-model:current="params.pageNum"
v-model:pageSize="params.pageSize"
:total="total"
@change="queryEmployee"
@showSizeChange="queryEmployee"
:show-total="showTableTotal"
/>
</div>
<EmployeeFormModal ref="employeeFormModal" @refresh="queryEmployee" @show-account="showAccount" />
<EmployeeDepartmentFormModal ref="employeeDepartmentFormModal" @refresh="queryEmployee" />
<EmployeePasswordDialog ref="employeePasswordDialog" />
</a-card>
</template>
<script setup>
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { message, Modal } from 'ant-design-vue';
import _ from 'lodash';
import { computed, createVNode, reactive, ref, watch } from 'vue';
import { employeeApi } from '/@/api/system/employee-api';
import { PAGE_SIZE } from '/@/constants/common-const';
import { SmartLoading } from '/@/components/framework/smart-loading';
import EmployeeFormModal from '../employee-form-modal/index.vue';
import EmployeeDepartmentFormModal from '../employee-department-form-modal/index.vue';
import EmployeePasswordDialog from '../employee-password-dialog/index.vue';
import { PAGE_SIZE_OPTIONS, showTableTotal } from '/@/constants/common-const';
import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
// ----------------------- 以下是字段定义 emits props ---------------------
const props = defineProps({
departmentId: Number,
breadcrumb: Array,
});
//-------------回显账号密码信息----------
let employeePasswordDialog = ref();
function showAccount(accountName, passWord) {
employeePasswordDialog.value.showModal(accountName, passWord);
}
// ----------------------- 表格/列表/ 搜索 ---------------------
//字段
const columns = ref([
{
title: '姓名',
dataIndex: 'actualName',
width: 85,
},
{
title: '性别',
dataIndex: 'gender',
width: 70,
},
{
title: '登录账号',
dataIndex: 'loginName',
width: 100,
},
{
title: '手机号',
dataIndex: 'phone',
width: 85,
},
{
title: '邮箱',
dataIndex: 'email',
width: 100,
ellipsis: true,
},
{
title: '超管',
dataIndex: 'administratorFlag',
width: 60,
},
{
title: '状态',
dataIndex: 'disabledFlag',
width: 60,
},
{
title: '职务',
dataIndex: 'positionName',
width: 100,
ellipsis: true,
},
{
title: '角色',
dataIndex: 'roleNameList',
width: 100,
},
{
title: '部门',
dataIndex: 'departmentName',
ellipsis: true,
width: 200,
},
{
title: '操作',
dataIndex: 'operate',
width: 140,
},
]);
const tableData = ref();
let defaultParams = {
departmentId: undefined,
disabledFlag: false,
keyword: undefined,
searchCount: undefined,
pageNum: 1,
pageSize: PAGE_SIZE,
sortItemList: undefined,
};
const params = reactive({ ...defaultParams });
const total = ref(0);
// 搜索重置
function reset() {
Object.assign(params, defaultParams);
queryEmployee();
}
const tableLoading = ref(false);
// 查询
async function queryEmployee() {
tableLoading.value = true;
try {
params.departmentId = props.departmentId;
let res = await employeeApi.queryEmployee(params);
for (const item of res.data.list) {
item.roleNameList = _.join(item.roleNameList, ',');
}
tableData.value = res.data.list;
total.value = res.data.total;
// 清除选中
selectedRowKeys.value = [];
selectedRows.value = [];
} catch (error) {
smartSentry.captureError(error);
} finally {
tableLoading.value = false;
}
}
// 根据关键字 查询
async function queryEmployeeByKeyword(allDepartment) {
tableLoading.value = true;
try {
params.pageNum = 1;
params.departmentId = allDepartment ? undefined : props.departmentId;
let res = await employeeApi.queryEmployee(params);
for (const item of res.data.list) {
item.roleNameList = _.join(item.roleNameList, ',');
}
tableData.value = res.data.list;
total.value = res.data.total;
// 清除选中
selectedRowKeys.value = [];
selectedRows.value = [];
} catch (error) {
smartSentry.captureError(error);
} finally {
tableLoading.value = false;
}
}
watch(
() => props.departmentId,
() => {
if (props.departmentId !== params.departmentId) {
params.pageNum = 1;
queryEmployee();
}
},
{ immediate: true }
);
// ----------------------- 多选操作 ---------------------
let selectedRowKeys = ref([]);
let selectedRows = ref([]);
// 是否有选中:用于 批量操作按钮的禁用
const hasSelected = computed(() => selectedRowKeys.value.length > 0);
function onSelectChange(keyArray, selectRows) {
selectedRowKeys.value = keyArray;
selectedRows.value = selectRows;
}
// 批量删除员工
function batchDelete() {
if (!hasSelected.value) {
message.warning('请选择要删除的员工');
return;
}
const actualNameArray = selectedRows.value.map((e) => e.actualName);
const employeeIdArray = selectedRows.value.map((e) => e.employeeId);
Modal.confirm({
title: '确定要删除如下员工吗?',
icon: createVNode(ExclamationCircleOutlined),
content: _.join(actualNameArray, ','),
okText: '删除',
okType: 'danger',
async onOk() {
SmartLoading.show();
try {
await employeeApi.batchDeleteEmployee(employeeIdArray);
message.success('删除成功');
queryEmployee();
selectedRowKeys.value = [];
selectedRows.value = [];
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
},
cancelText: '取消',
onCancel() {},
});
}
// 批量更新员工部门
const employeeDepartmentFormModal = ref();
function updateEmployeeDepartment() {
if (!hasSelected.value) {
message.warning('请选择要调整部门的员工');
return;
}
const employeeIdArray = selectedRows.value.map((e) => e.employeeId);
employeeDepartmentFormModal.value.showModal(employeeIdArray);
}
// ----------------------- 添加、修改、禁用、重置密码 ------------------------------------
const employeeFormModal = ref(); //组件
// 展示编辑弹窗
function showDrawer(rowData) {
let params = {};
if (rowData) {
params = _.cloneDeep(rowData);
params.disabledFlag = params.disabledFlag ? 1 : 0;
} else if (props.departmentId) {
params.departmentId = props.departmentId;
}
employeeFormModal.value.showDrawer(params);
}
// 重置密码
function resetPassword(id, name) {
Modal.confirm({
title: '提醒',
icon: createVNode(ExclamationCircleOutlined),
content: '确定要重置密码吗?',
okText: '确定',
okType: 'danger',
async onOk() {
SmartLoading.show();
try {
let { data: passWord } = await employeeApi.resetPassword(id);
message.success('重置成功');
employeePasswordDialog.value.showModal(name, passWord);
queryEmployee();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
},
cancelText: '取消',
onCancel() {},
});
}
// 禁用 / 启用
function updateDisabled(id, disabledFlag) {
Modal.confirm({
title: '提醒',
icon: createVNode(ExclamationCircleOutlined),
content: `确定要${disabledFlag ? '启用' : '禁用'}吗?`,
okText: '确定',
okType: 'danger',
async onOk() {
SmartLoading.show();
try {
await employeeApi.updateDisabled(id);
message.success(`${disabledFlag ? '启用' : '禁用'}成功`);
queryEmployee();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
},
cancelText: '取消',
onCancel() {},
});
}
</script>
<style scoped lang="less">
.employee-container {
height: 100%;
}
.header {
display: flex;
align-items: center;
}
.query-operate {
margin-left: auto;
display: flex;
align-items: center;
}
.btn-group {
margin: 10px 0;
.btn {
margin-right: 8px;
}
}
</style>

View File

@@ -0,0 +1,79 @@
<!--
* 员工 修改密码的 显示密码弹窗
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal v-model:open="visible" :zIndex="9999" :width="500" title="提示" :closable="false" :maskClosable="false">
<!-- -->
<ul>
<li>登录名: {{ showLoginName }}</li>
<li>密码: {{ showLoginPassword }}</li>
</ul>
<template #footer>
<a-button
type="primary"
class="account-copy"
:data-clipboard-text="`登录名${showLoginName}
密码${showLoginPassword}`"
size="middle"
@click="copy"
>复制密码并关闭</a-button
>
</template>
</a-modal>
</template>
<script setup>
import { message } from 'ant-design-vue';
import Clipboard from 'clipboard';
import { ref } from 'vue';
let visible = ref(false); // 是否展示抽屉
let showLoginName = ref(''); //登录名
let showLoginPassword = ref(''); //登录密码
function copy() {
handleCopy();
visible.value = false;
}
function showModal(loginName, loginPassword) {
visible.value = true;
showLoginName.value = loginName;
showLoginPassword.value = loginPassword;
}
function handleCopy() {
let clipboard = new Clipboard('.account-copy');
clipboard.on('success', (e) => {
message.info('复制成功');
console.log('复制成功');
// 释放内存
clipboard.destroy();
});
clipboard.on('error', (e) => {
// 不支持复制
message.error('浏览器不支持复制,请您手动选择复制');
// 释放内存
clipboard.destroy();
});
}
defineExpose({
showModal,
});
</script>
<style lang="less" scoped>
ul {
margin: 0;
padding: 0;
list-style: none;
padding-left: 32%;
li {
font-weight: bold;
font-size: 16px;
}
}
</style>
>

View File

@@ -0,0 +1,11 @@
/*
* 部门event bus
*
* @Author: 1024创新实验室-主任:卓大
* @Date: 2022-07-12 23:32:48
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*/
import mitt from 'mitt';
export default mitt();

View File

@@ -0,0 +1,70 @@
<!--
* 组织架构
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div class="height100">
<a-row :gutter="16" class="height100">
<a-col :span="6">
<DepartmentTree ref="departmentTree" />
</a-col>
<a-col :span="18" class="height100">
<div class="employee-box height100">
<EmployeeList style="flex-grow: 2.5" class="employee" :departmentId="selectedDepartmentId" />
</div>
</a-col>
</a-row>
</div>
</template>
<script setup>
import _ from 'lodash';
import { computed, ref } from 'vue';
import DepartmentTree from './components/department-tree/index.vue';
import EmployeeList from './components/employee-list/index.vue';
const departmentTree = ref();
// 部门 面包屑
const breadcrumb = computed(() => {
if (departmentTree.value) {
return departmentTree.value.breadcrumb;
}
return [];
});
// 当前选中部门的孩子
const selectedDepartmentChildren = computed(() => {
if (departmentTree.value) {
return departmentTree.value.selectedDepartmentChildren;
}
return [];
});
// 当前选中的部门id
const selectedDepartmentId = computed(() => {
if (departmentTree.value) {
let selectedKeys = departmentTree.value.selectedKeys;
return _.isEmpty(selectedKeys) ? null : selectedKeys[0];
}
return null;
});
</script>
<style scoped lang="less">
.height100 {
height: 100%;
}
.employee-box {
display: flex;
flex-direction: column;
.employee {
flex-grow: 2;
}
}
</style>

View File

@@ -0,0 +1,92 @@
<!--
* 客服人员弹窗
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-06 20:40:16
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal :open="visible" width="600px" :bodyStyle="{height:'480px'}" title="" :closable="false" :maskClosable="true">
<a-row><div style="font-weight:bolder;margin: 0 auto;font-size: 16px">助力卓大抖音1000个粉丝开播写代码🎉🎉</div> </a-row>
<a-row><div style="font-weight:bolder;margin: 20px auto;font-size: 15px">和1024创新实验室一起热爱代码热爱生活永远年轻永远前行🎉🎉</div> </a-row>
<br />
<div class="app-qr-box">
<div class="app-qr">
<a-image
:width="300"
style="border-radius: 15px;"
src="https://img.smartadmin.1024lab.net/wechat/douyin.png"
/>
<span class="qr-desc strong"> 打开抖音APP-点击左上角侧边栏-点击扫一扫-进行关注</span>
</div>
</div>
<template #footer>
<a-button type="primary" @click="hide">知道了</a-button>
</template>
</a-modal>
</template>
<script setup>
import {ref} from 'vue';
defineExpose({
show,
});
const visible = ref(true);
function show() {
visible.value = true;
}
function hide() {
visible.value = false;
}
</script>
<style lang="less" scoped>
.app-qr-box {
display: flex;
align-items: center;
justify-content: space-around;
.app-qr {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
> img {
width: 100%;
height: 100%;
}
.strong {
font-weight: 600;
}
.qr-desc {
display: flex;
margin-top: 20px;
align-items: center;
font-size: 13px;
color: red;
text-align: center;
overflow-x: hidden;
> img {
width: 15px;
height: 18px;
margin-right: 9px;
}
}
}
}
.ant-carousel :deep(.slick-slide) {
text-align: center;
height: 120px;
line-height: 120px;
width: 120px;
background: #364d79;
overflow: hidden;
}
.ant-carousel :deep(.slick-slide h3) {
color: #fff;
}
</style>

View File

@@ -0,0 +1,94 @@
<!--
* 更新日志
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<default-home-card extra="更多" icon="FlagOutlined" title="更新日志" @extraClick="onMore">
<a-empty v-if="$lodash.isEmpty(data)" />
<ul v-else>
<template v-for="(item, index) in data" :key="index">
<li class="un-read">
<a class="content" @click="goDetail(item)">
<a-badge status="geekblue" />
{{ $smartEnumPlugin.getDescByValue('CHANGE_LOG_TYPE_ENUM', item.type) }}{{ item.version }} 版本
</a>
<span class="time"> {{ item.publicDate }}</span>
</li>
</template>
</ul>
</default-home-card>
<ChangeLogForm ref="modalRef" />
</template>
<script setup>
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { smartSentry } from '/@/lib/smart-sentry';
import { changeLogApi } from '/@/api/support/change-log-api';
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
import ChangeLogForm from '/@/views/support/change-log/change-log-modal.vue';
const router = useRouter();
const queryForm = {
pageNum: 1,
pageSize: 8,
searchCount: false,
};
let data = ref([]);
const loading = ref(false);
// 查询列表
async function queryChangeLog() {
loading.value = true;
try {
let queryResult = await changeLogApi.queryPage(queryForm);
data.value = queryResult.data.list;
} catch (e) {
smartSentry.captureError(e);
} finally {
loading.value = false;
}
}
onMounted(queryChangeLog);
// 查看更多
function onMore() {
router.push({
path: '/support/change-log/change-log-list',
});
}
// 进入详情
const modalRef = ref();
function goDetail(data) {
modalRef.value.show(data);
}
</script>
<style lang="less" scoped>
ul li {
margin-bottom: 8px;
display: flex;
justify-content: space-between;
.content {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
}
.time {
flex-shrink: 0;
min-width: 75px;
}
}
</style>

View File

@@ -0,0 +1,68 @@
<!--
* 首页 card 插槽
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<div class="card-container">
<a-card size="small">
<template #title>
<div class="title">
<component :is="$antIcons[props.icon]" v-if="props.icon" :style="{ fontSize: '18px', color: token.colorPrimary }" />
<slot name="title"></slot>
<span v-if="!$slots.title" class="smart-margin-left10">{{ props.title }} </span>
</div>
</template>
<template v-if="props.extra" #extra>
<slot name="extra"></slot>
<a v-if="!$slots.extra" @click="extraClick">{{ props.extra }}</a>
</template>
<slot></slot>
</a-card>
</div>
</template>
<script setup>
import { theme } from 'ant-design-vue';
import { computed } from 'vue';
let props = defineProps({
icon: String,
title: String,
extra: String,
});
let emits = defineEmits(['extraClick']);
function extraClick() {
emits('extraClick');
}
const { useToken } = theme;
const { token } = useToken();
const color = computed(() => {
return token.colorPrimary;
});
</script>
<style lang="less" scoped>
.card-container {
height: 100%;
.title {
display: flex;
align-items: center;
&::before {
content: '';
position: absolute;
top: 3px;
left: 0;
width: 3px;
height: 30px;
background-color: v-bind('token.colorPrimary');
}
}
}
</style>

View File

@@ -0,0 +1,92 @@
<template>
<default-home-card icon="Profile" title="销量统计">
<div class="echarts-box">
<div class="category-main" id="category-main"></div>
</div>
</default-home-card>
</template>
<script setup>
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
import * as echarts from 'echarts';
import { onMounted } from 'vue';
onMounted(() => {
init();
});
function init() {
let option = {
xAxis: {
type: 'category',
data: ['周一', '周二', '周三', '周四', '周五'],
},
yAxis: {
type: 'value',
},
legend: {},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
series: [
{
name: '善逸',
data: [120, 200, 150, 80, 70, 110, 130],
type: 'bar',
backgroundStyle: {
color: 'rgba(180, 180, 180, 0.2)',
},
},
{
name: '胡克',
data: [100, 80, 120, 77, 52, 22, 190],
type: 'bar',
backgroundStyle: {
color: 'rgba(180, 180, 180, 0.2)',
},
},
{
name: '开云',
data: [200, 110, 85, 99, 120, 145, 180],
type: 'bar',
backgroundStyle: {
color: 'rgba(180, 180, 180, 0.2)',
},
},
{
name: '初晓',
data: [80, 70, 90, 110, 200, 44, 80],
type: 'bar',
backgroundStyle: {
color: 'rgba(180, 180, 180, 0.2)',
},
},
],
};
let chartDom = document.getElementById('category-main');
if (chartDom) {
let myChart = echarts.init(chartDom);
option && myChart.setOption(option);
}
}
</script>
<style lang="less" scoped>
.echarts-box {
display: flex;
align-items: center;
justify-content: center;
.category-main {
width: 800px;
height: 280px;
background: #fff;
}
}
</style>

View File

@@ -0,0 +1,118 @@
<!--
* @Author: zhuoda
* @Date: 2021-08-24 16:35:45
* @LastEditTime: 2022-06-11
* @LastEditors: zhuoda
* @Description:
* @FilePath: /smart-admin/@/views/system/home/components/gauge.vue
-->
<template>
<default-home-card icon="Rocket" title="业绩完成度">
<div class="echarts-box">
<div id="gauge-main" class="gauge-main"></div>
</div>
</default-home-card>
</template>
<script setup>
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
import * as echarts from 'echarts';
import { onMounted, watch } from 'vue';
import { reactive } from 'vue';
const props = defineProps({
percent: {
type: Number,
default: 0,
},
});
let option = reactive({});
watch(
() => props.percent,
() => {
init();
}
);
onMounted(() => {
init();
});
function init() {
option = {
series: [
{
type: 'gauge',
startAngle: 90,
endAngle: -270,
pointer: {
show: false,
},
progress: {
show: true,
overlap: false,
roundCap: true,
clip: false,
itemStyle: {
borderWidth: 1,
borderColor: '#464646',
},
},
axisLine: {
lineStyle: {
width: 20,
},
},
splitLine: {
show: false,
distance: 0,
length: 10,
},
axisTick: {
show: false,
},
axisLabel: {
show: false,
distance: 50,
},
data: [
{
value: props.percent,
name: '完成度',
title: {
offsetCenter: ['0%', '-10%'],
},
detail: {
offsetCenter: ['0%', '20%'],
},
},
],
title: {
fontSize: 18,
},
detail: {
fontSize: 16,
color: 'auto',
formatter: '{value}%',
},
},
],
};
let chartDom = document.getElementById('gauge-main');
if (chartDom) {
let myChart = echarts.init(chartDom);
option && myChart.setOption(option);
}
}
</script>
<style lang="less" scoped>
.echarts-box {
display: flex;
align-items: center;
justify-content: center;
.gauge-main {
width: 260px;
height: 260px;
background: #fff;
}
}
</style>

View File

@@ -0,0 +1,210 @@
<template>
<default-home-card icon="BarChartOutlined" title="代码提交量">
<div class="echarts-box">
<div class="gradient-main" id="gradient-main"></div>
</div>
</default-home-card>
</template>
<script setup>
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
import * as echarts from 'echarts';
import { onMounted } from 'vue';
onMounted(() => {
init();
});
function init() {
let option = {
color: ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00'],
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985',
},
},
},
legend: {
data: ['罗伊', '佩弦', '开云', '清野', '飞叶'],
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: [
{
type: 'category',
boundaryGap: false,
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
},
],
yAxis: [
{
type: 'value',
},
],
series: [
{
name: '罗伊',
type: 'line',
stack: 'Total',
smooth: true,
lineStyle: {
width: 0,
},
showSymbol: false,
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgb(128, 255, 165)',
},
{
offset: 1,
color: 'rgb(1, 191, 236)',
},
]),
},
emphasis: {
focus: 'series',
},
data: [140, 232, 101, 264, 90, 340, 250],
},
{
name: '佩弦',
type: 'line',
stack: 'Total',
smooth: true,
lineStyle: {
width: 0,
},
showSymbol: false,
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgb(0, 221, 255)',
},
{
offset: 1,
color: 'rgb(77, 119, 255)',
},
]),
},
emphasis: {
focus: 'series',
},
data: [120, 282, 111, 234, 220, 340, 310],
},
{
name: '开云',
type: 'line',
stack: 'Total',
smooth: true,
lineStyle: {
width: 0,
},
showSymbol: false,
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgb(55, 162, 255)',
},
{
offset: 1,
color: 'rgb(116, 21, 219)',
},
]),
},
emphasis: {
focus: 'series',
},
data: [320, 132, 201, 334, 190, 130, 220],
},
{
name: '清野',
type: 'line',
stack: 'Total',
smooth: true,
lineStyle: {
width: 0,
},
showSymbol: false,
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgb(255, 0, 135)',
},
{
offset: 1,
color: 'rgb(135, 0, 157)',
},
]),
},
emphasis: {
focus: 'series',
},
data: [220, 402, 231, 134, 190, 230, 120],
},
{
name: '飞叶',
type: 'line',
stack: 'Total',
smooth: true,
lineStyle: {
width: 0,
},
showSymbol: false,
label: {
show: true,
position: 'top',
},
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgb(255, 191, 0)',
},
{
offset: 1,
color: 'rgb(224, 62, 76)',
},
]),
},
emphasis: {
focus: 'series',
},
data: [220, 302, 181, 234, 210, 290, 150],
},
],
};
let chartDom = document.getElementById('gradient-main');
if (chartDom) {
let myChart = echarts.init(chartDom);
option && myChart.setOption(option);
}
}
</script>
<style lang="less" scoped>
.echarts-box {
display: flex;
align-items: center;
justify-content: center;
.gradient-main {
width: 1200px;
height: 300px;
background: #fff;
}
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<default-home-card icon="PieChartOutlined" title="加班统计">
<div class="echarts-box">
<div class="pie-main" id="pie-main"></div>
</div>
</default-home-card>
</template>
<script setup>
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
import * as echarts from 'echarts';
import { onMounted } from 'vue';
onMounted(() => {
init();
});
function init() {
let option = {
tooltip: {
trigger: 'item',
},
legend: {
top: '5%',
left: 'center',
},
series: [
{
name: '加班次数',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: '40',
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: [
{ value: 10, name: '初晓' },
{ value: 8, name: '善逸' },
{ value: 3, name: '胡克' },
{ value: 1, name: '罗伊' },
],
},
],
};
let chartDom = document.getElementById('pie-main');
if (chartDom) {
let myChart = echarts.init(chartDom);
option && myChart.setOption(option);
}
}
</script>
<style lang="less" scoped>
.echarts-box {
display: flex;
align-items: center;
justify-content: center;
.pie-main {
width: 260px;
height: 260px;
background: #fff;
}
}
</style>

View File

@@ -0,0 +1,80 @@
<!--
* 官方 二维码
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<default-home-card icon="SmileOutlined" title="联系我们">
<div class="app-qr-box">
<div class="app-qr">
<img :src="zhuoda" />
<span class="qr-desc strong"> 卓大的微信号 </span>
<span class="qr-desc"> 骚扰卓大 :) </span>
</div>
<div class="app-qr">
<img :src="xiaozhen" />
<span class="qr-desc strong"> 六边形工程师 </span>
<span class="qr-desc"> 赚钱代码生活 </span>
</div>
</div>
</default-home-card>
</template>
<script setup>
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
import zhuoda from '/@/assets/images/1024lab/zhuoda-wechat.jpg';
import xiaozhen from '/@/assets/images/1024lab/gzh.jpg';
</script>
<style lang="less" scoped>
.app-qr-box {
display: flex;
height: 150px;
align-items: center;
justify-content: space-around;
.app-qr {
display: flex;
align-items: center;
width: 33%;
justify-content: center;
flex-direction: column;
> img {
width: 100%;
max-width: 120px;
height: 100%;
max-height: 120px;
}
.strong {
font-weight: 600;
}
.qr-desc {
display: flex;
align-items: center;
font-size: 12px;
text-align: center;
overflow-x: hidden;
> img {
width: 15px;
height: 18px;
margin-right: 9px;
}
}
}
}
.ant-carousel :deep(.slick-slide) {
text-align: center;
height: 120px;
line-height: 120px;
width: 120px;
background: #364d79;
overflow: hidden;
}
.ant-carousel :deep(.slick-slide h3) {
color: #fff;
}
</style>

Some files were not shown because too many files have changed in this diff Show More