vue3的js和ts代码上传

This commit is contained in:
zhuoda
2022-10-24 20:11:58 +08:00
parent 0996e90df0
commit 207b949484
562 changed files with 103071 additions and 0 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 :visible="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,163 @@
<!--
* 目录 表格
*
* @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" size="small" 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:edit`">编辑</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 { reactive, ref, onMounted, computed } from 'vue';
import { Modal, message } 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: 'goods'
}
});
// ------------------------------ 查询 ------------------------------
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);
const list = responseModel.data;
tableData.value = list;
} 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,150 @@
<!--
* 商品表单
*
* @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" :visible="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 key-code="GODOS_PLACE" v-model:value="form.place" />
</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: undefined,
//商品价格
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);
}
console.log(form);
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,309 @@
<!--
* 商品列表
*
* @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="queryData">
<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 type="primary" @click="queryData" v-privilege="'goods:query'">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery" class="smart-margin-left10" v-privilege="'goods:query'">
<template #icon>
<SearchOutlined />
</template>
重置
</a-button>
</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" size="small" v-privilege="'goods:add'">
<template #icon>
<PlusOutlined />
</template>
新建
</a-button>
<a-button @click="confirmBatchDelete" type="danger" size="small" :disabled="selectedRowKeyList.length == 0" v-privilege="'goods:batchDelete'">
<template #icon>
<DeleteOutlined />
</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"
:row-selection="{ selectedRowKeys: selectedRowKeyList, onChange: onSelectChange }"
>
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'place'">
<span>{{ text && text.length > 0 ? text[0].valueName : '' }}</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-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';
// ---------------------------- 表格列 ----------------------------
const columns = ref([
{
title: '商品分类',
dataIndex: 'categoryName',
},
{
title: '商品名称',
dataIndex: 'goodsName',
},
{
title: '商品状态',
dataIndex: 'goodsStatus',
},
{
title: '产地',
dataIndex: 'place',
},
{
title: '商品价格',
dataIndex: 'price',
},
{
title: '上架状态',
dataIndex: 'shelvesFlag',
},
{
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,
};
// 查询表单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();
}
// 查询数据
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();
}
}
</script>

View File

@@ -0,0 +1,239 @@
<!--
* 企业 银行列表
*
* @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 type="primary" @click="ajaxQuery">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
<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();
}
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,133 @@
<!--
* 企业 银行 表单
*
* @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 :visible="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) => {
debugger
console.log('error', error);
message.error('参数验证错误,请仔细填写表单数据!');
});
}
defineExpose({
showModal,
});
</script>

View File

@@ -0,0 +1,275 @@
<!--
* 企业 员工
*
* @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.keywords" placeholder="姓名/手机号/登录账号" />
<a-button class="button-style" type="primary" @click="queryEmployee">搜索</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="'enterprise:addEmployee'"> 添加员工 </a-button>
<a-button class="button-style" type="primary" danger @click="batchDelete" v-privilege="'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="'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,
keywords: undefined,
};
// 查询表单
const queryForm = reactive({ ...defaultQueryForm });
const total = ref(0);
const tableData = ref([]);
const tableLoading = ref(false);
function resetQueryEmployee() {
queryForm.keywords = '';
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,243 @@
<!--
* 企业 发票信息
*
* @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 type="primary" @click="ajaxQuery">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
<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();
}
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 :visible="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 :visible="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 :default-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,264 @@
<!--
* 公司列表
*
* @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="'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" @change="dateChange" />
</a-space>
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button type="primary" @click="ajaxQuery">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
</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="'enterprise:add'" type="primary" size="small">
<template #icon>
<PlusOutlined />
</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)" v-privilege="'enterprise:edit'" type="link">编辑</a-button>
<a-button @click="confirmDelete(record.enterpriseId)" danger v-privilege="'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 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 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;
}
}
// --------------------------- 删除 ---------------------------
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 ? '编辑' : '新建'"
:visible="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 lodash 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 (!lodash.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 = lodash.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:visible="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 lodash from 'lodash';
import { NOTICE_VISIBLE_RANGE_DATA_TYPE_ENUM } from '/@/constants/business/oa/notice-const';
import { departmentApi } from '/@/api/system/department/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 (!lodash.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 { reactive, ref, onMounted, watch, computed, nextTick } from 'vue';
import lodash from 'lodash';
import { NOTICE_VISIBLE_RANGE_DATA_TYPE_ENUM } from '/@/constants/business/oa/notice-const';
import { departmentApi } from '/@/api/system/department/department-api';
import { employeeApi } from '/@/api/system/employee/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 (!lodash.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" :columns="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.attachmentFile)" label="附件">
<div class="file-list">
<a class="file-item" v-for="item in noticeDetail.attachmentFile" :key="item.fileId" @click="onPrevFile(item)">{{ item.fileName }}</a>
</div>
</a-descriptions-item>
<a-descriptions-item label="可见范围" :span="2">
<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,153 @@
<!--
* 通知 详情 员工
*
* @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 :fileList="noticeDetail.attachment" /></div>
</a-card>
<a-card title="记录" size="small" class="smart-margin-top10">
<NoticeViewRecordList ref="noticeViewRecordList" :noticeId="route.query.noticeId" />
</a-card>
<!-- 预览附件 -->
<FilePreviewModal ref="filePreviewRef" />
</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 FilePreviewModal from '/@/components/support/file-preview-modal/index.vue';
import FilePreview from '/@/components/support/file-preview/index.vue';
import { smartSentry } from '/@/lib/smart-sentry';
const route = useRoute();
const activeKey = ref(1);
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);
}
// 预览附件
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: 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,239 @@
<!--
* 通知 详情 员工列表
*
* @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-select v-model:value="queryForm.noticeTypeId" style="width: 100px" :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="关键字" 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 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-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"
:scroll="{ x: 1500 }"
: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: #666">{{ 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 { 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 { smartSentry } from '/@/lib/smart-sentry';
const tableColumns = reactive([
{
title: `标题`,
dataIndex: 'title',
width: 300,
ellipsis: true,
},
{
title: `文号`,
dataIndex: 'documentNumber',
width: 100,
ellipsis: true,
},
{
title: `作者`,
dataIndex: 'author',
width: 40,
ellipsis: true,
},
{
title: `来源`,
dataIndex: 'source',
width: 90,
ellipsis: true,
},
{
title: '发布时间',
dataIndex: 'publishTime',
width: 140,
},
{
title: '用户/页面浏览量',
dataIndex: 'pageViewCount',
width: 90,
},
]);
// ------------------ 通知分类 ------------------
// 查询分类列表
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="'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" @change="publishDateChange" style="width: 220px" />
</a-form-item>
<a-form-item label="创建时间" class="smart-query-form-item">
<a-range-picker 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" size="small" @click="addOrUpdate()" v-privilege="'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="'notice:edit'">编辑</a-button>
<a-button type="link" @click="onDelete(record.noticeId)" v-privilege="'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 { 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,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/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"
:visible="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/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,317 @@
<!--
* 系统更新日志
*
* @Author: 卓大
* @Date: 2022-09-26 14:53:50
* @Copyright 1024创新实验室
-->
<template>
<!---------- 查询表单form begin ----------->
<a-form class="smart-query-form" v-privilege="'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" :ranges="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 type="primary" @click="queryData">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery" class="smart-margin-left10">
<template #icon>
<SearchOutlined />
</template>
重置
</a-button>
</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" size="small" v-privilege="'changeLog:add'">
<template #icon>
<PlusOutlined />
</template>
新建
</a-button>
<a-button @click="confirmBatchDelete" type="danger" size="small" :disabled="selectedRowKeyList.length == 0" v-privilege="'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="'changeLog:update'">编辑</a-button>
<a-button @click="onDelete(record)" danger type="link" v-privilege="'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/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();
}
// 查询数据
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 {
let deleteForm = {
goodsIdList: selectedRowKeyList.value,
};
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" :visible="visibleFlag" @close="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,178 @@
<!--
* 代码生成 列表
*
* @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 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" :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" />
<CodeGeneratorPreviewModal ref="codeGeneratorPreviewModalRef" />
</div>
</template>
<script setup>
import { onMounted, reactive, ref, nextTick } from 'vue';
import { codeGeneratorApi } from '/@/api/support/code-generator/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();
}
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,171 @@
import { convertUpperCamel } from '/@/utils/str-util';
// -------------------------------- java 类型 --------------------------------
export const JavaTypeMap = new Map();
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('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 = [
'Number', //
'String', //
'Date', //
'Boolean', //
'String', //
];
export function getJsType(dataType) {
return JsTypeMap.get(dataType);
}
// -------------------------------- 前端组件 --------------------------------
export const FrontComponentMap = new Map();
FrontComponentMap.set('int', 'InputNumber');
FrontComponentMap.set('tinyint', 'BooleanSelect');
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', 'Upload');
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,
];
// -------------------------------- 枚举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,275 @@
<!--
* 代码生成 配置信息
*
* @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 type="flex">
<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="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 lodash 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 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 (lodash.startsWith(tableInfo.tableName, 't_')) {
removePrefixTableName = lodash.trim(removePrefixTableName, '_t');
}
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;
}
// 获取表单数据
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('基础命名表单 参数验证错误!');
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,128 @@
<!--
* 代码生成 删除
*
* @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 lodash 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;
}
//表单
let deleteInfo = config.delete;
formData.isSupportDelete = deleteInfo && deleteInfo.isSupportDelete ? deleteInfo.isSupportDelete : true;
formData.isPhysicallyDeleted = deleteInfo && deleteInfo.isPhysicallyDeleted ? deleteInfo.isPhysicallyDeleted : deletedFlagColumn ? false : true;
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) => lodash.startsWith(e.columnName, 'deleted_flag' || lodash.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('删除表单 参数验证错误!');
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,222 @@
<!--
* 代码生成 配置信息
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-22 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<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 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 lodash from 'lodash';
//------------------------ 全局数据 ---------------------
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 (lodash.startsWith(tableInfo.tableName, 't_')) {
removePrefixTableName = lodash.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' ? true : false,
primaryKeyFlag: column.columnKey === 'PRI' ? true : false,
autoIncreaseFlag: column.extra === 'auto_increment' ? true : false,
//表单
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,293 @@
<!--
* 代码生成 新增和更新
*
* @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 { inject, ref, reactive, computed } from 'vue';
import { checkExistEnum, getFrontComponent } from '../../code-generator-util';
import SmartEnumRadio from '/@/components/framework/smart-enum-radio/index.vue';
import { 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';
import { CODE_FRONT_COMPONENT_ENUM } from '/@/constants/support/code-generator-const';
//------------------------ 全局数据 ---------------------
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' ? true : false,
primaryKeyFlag: column.columnKey === 'PRI' ? true : false,
autoIncreaseFlag: column.extra === 'auto_increment' ? true : false,
};
//表单
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('基础命名表单 参数验证错误!');
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,257 @@
<!--
* 代码生成 查询条件
*
* @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="{ text, 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 lodash from 'lodash';
import Sortable from 'sortablejs';
import { inject, nextTick, ref } from 'vue';
import SmartEnumSelect from '/@/components/framework/smart-enum-select/index.vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
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: 'columnNameList',
},
{
title: '查询类型',
dataIndex: 'queryTypeEnum',
width: 130,
},
{
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([]);
//初始化设置数据
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' + (index + 1);
rowKeyCounter++;
}
tableData.value = data;
tableColumns.value = tableColumnInfos;
nextTick(() => {
initDrag();
});
}
// ------------------- 增加、删除 -------------------
let rowKeyCounter = 1;
function addQuery() {
tableData.value.push({
rowKey: 'rowKey' + rowKeyCounter,
label: '',
fieldName: '',
queryTypeEnum: '',
columnNameList: null,
width: '',
});
rowKeyCounter++;
}
function onDelete(index) {
lodash.pullAt(tableData.value, index);
}
//初始化拖拽
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',
});
}
// ------------------- 监听数据变化 -------------------
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);
//表单
if (!record.fieldName) {
record.fieldName = column && column.columnName ? convertLowerCamel(column.columnName) : '';
}
if (!record.label) {
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 = [];
let trList = document.querySelectorAll('#smartCodeQueryFieldsTable tbody .column-row');
if (trList && trList.length === 0) {
return result;
}
for (let tr of trList) {
let rowKey = tr.getAttribute('data-row-key');
if (!rowKey) {
continue;
}
if (rowKey && rowKey.indexOf('rowKey') === -1) {
continue;
}
let index = parseInt(rowKey.substring(6));
let tableItem = tableData.value[index - 1];
let obj = {
queryTypeEnum: tableItem.queryTypeEnum,
label: tableItem.label,
fieldName: tableItem.fieldName,
columnNameList: tableItem.columnNameList,
width: tableItem.width,
};
// 字符串转为数组
if (obj.columnNameList && typeof obj.columnNameList === 'string') {
obj.columnNameList = [obj.columnNameList];
}
result.push(obj);
}
return result;
}
defineExpose({
setData,
getFieldsForm,
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,150 @@
<!--
* 代码生成 列表
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-22 21:50:41
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-table size="small" bordered :scroll="{ x: 1000 }" 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 === '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,225 @@
<!--
* 代码生成 配置信息
*
* @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=""
:visible="visibleFlag"
:width="1000"
: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/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() {
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);
}
}
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="代码预览"
:visible="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, watch } from 'vue';
import { codeGeneratorApi } from '/@/api/support/code-generator/code-generator-api';
import { JAVA_FILE_LIST, LANGUAGE_LIST, JS_FILE_LIST,TS_FILE_LIST, JAVA_DOMAIN_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 :visible="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/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,170 @@
<!--
* 系统设置 列表
*
* @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 type="primary" @click="ajaxQuery" 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 @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/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();
}
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 :visible="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/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" :visible="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="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/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 :visible="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/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,237 @@
<!--
* 数据 字典
*
* @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 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" :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" size="small">
<template #icon>
<PlusOutlined />
</template>
新建
</a-button>
<a-button @click="confirmBatchDelete" v-privilege="'support:dict:batch:delete'" type="danger" size="small" :disabled="selectedRowKeyList.length == 0">
<template #icon>
<DeleteOutlined />
</template>
批量删除
</a-button>
<a-button @click="cacheRefresh" v-privilege="'support:dict:refresh'" type="primary" size="small">
<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: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>
<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/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();
}
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"
:ranges="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, record, column }">
<template v-if="column.dataIndex === 'feedbackAttachment'">
<FilePreview :fileList="text" />
</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/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,280 @@
<!--
* 文件
*
* @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" :ranges="defaultTimeRanges" style="width: 220px" @change="onChangeCreateTime" />
</a-form-item>
<a-form-item class="smart-query-form-item">
<a-button type="primary" @click="queryData">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery" class="smart-margin-left10">
<template #icon>
<SearchOutlined />
</template>
重置
</a-button>
</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" size="small">
<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" :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:visible="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/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',
ellipsis: true,
width: 200,
},
{
title: '文件大小',
dataIndex: 'fileSize',
ellipsis: true,
width: 100,
},
{
title: '文件key',
dataIndex: 'fileKey',
ellipsis: true,
},
{
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,
},
]);
// ---------------------------- 查询数据表单和方法 ----------------------------
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();
}
// 查询数据
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.fileName, 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,166 @@
<!--
* 心跳记录
*
* @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" :ranges="defaultChooseTimeRange" style="width: 240px" />
</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-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/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();
}
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:visible="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 lang="ts">
import message from 'ant-design-vue/lib/message';
import { reactive, ref } from 'vue';
import { helpDocCatalogApi } from '/@/api/support/help-doc/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/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,354 @@
<!--
* 目录树
*
* @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="'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="'helpDocCatalog:addCategory'">添加下级</a-button>
<a-button type="text" @click="updateHelpDocCatalog(item.dataRef)" v-privilege="'helpDocCatalog:edit'">修改</a-button>
<a-button
type="text"
v-if="item.helpDocCatalogId != topHelpDocCatalogId"
@click="deleteHelpDocCatalog(item.helpDocCatalogId)"
v-privilege="'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 lang="ts">
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/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;
color: @success-color;
}
.no-data {
margin: 10px;
}
}
</style>

View File

@@ -0,0 +1,204 @@
<!--
* 帮助文档表单
*
* @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 ? '编辑' : '新建'"
:visible="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="关联菜单">
<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 { reactive, ref, nextTick } from 'vue';
import { message, Modal } from 'ant-design-vue';
import lodash 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/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 _ from 'lodash';
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 noticeFormVisibleModal = ref();
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 (!lodash.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) : [];
} 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
const menuTreeSelect = ref();
async function save() {
try {
SmartLoading.show();
let param = _.cloneDeep(formData);
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 = lodash.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,267 @@
<!--
* 帮助文档 列表
*
* @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="'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 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" size="small" @click="addOrUpdate()" v-privilege="'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="'helpDoc:update'">编辑</a-button>
<a-button type="link" danger @click="onDelete(record.helpDocId)" v-privilege="'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/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 { 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/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,150 @@
<!--
* 帮助文档详情
*
* @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/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;
}
}
}
.content-html {
margin-top: 30px;
padding: 0 8px;
line-height: 28px;
font-size: 14px;
img {
max-width: 100%;
}
}
</style>

View File

@@ -0,0 +1,194 @@
<!--
* 登录登出 日志
*
* @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="'loginLog: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.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" :ranges="defaultChooseTimeRange" style="width: 240px" />
</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" :hoverable="true">
<a-row justify="end">
<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">
<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, 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/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';
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: '设备信息',
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();
}
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;
}
}
onMounted(ajaxQuery);
</script>

View File

@@ -0,0 +1,139 @@
<!--
* 操作记录 详情
*
* @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 :visible="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"> 请求地址 {{ detail.url }}</a-col>
<a-col :span="12"> 请求日期 {{ detail.createTime }}</a-col>
</a-row>
<a-row class="detail-info">
<a-col> 请求方法 {{ detail.method }}</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-col :span="12"> 请求内容 {{ detail.module }} - {{ detail.content }}</a-col>
</a-row>
</a-col>
<a-col :span="8">
<p class="detail-right-title">请求状态</p>
<p :class="['detail-right', detail.successFlag ? 'success' : 'error']">
{{ detail.successFlag ? '成功' : '失败' }}
</p>
</a-col>
</a-row>
</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">
<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/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;
}
.success {
color: @success-color;
}
.error {
color: @error-color;
}
</style>

View File

@@ -0,0 +1,220 @@
<!--
* 操作记录 列表
*
* @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="'operateLog: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.userName" placeholder="用户名称" />
</a-form-item>
<a-form-item label="请求时间" class="smart-query-form-item">
<a-range-picker @change="changeCreateDate" v-model:value="createDateRange" :ranges="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="ajaxQuery">
<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 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" :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="'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/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: '客户端',
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: '',
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();
}
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 :visible="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/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="{ text, record, index, column }">
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button @click="doReload(record.tag)" v-privilege="'reload:execute'" type="link">执行</a-button>
<a-button @click="showResultList(record.tag)" v-privilege="'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, reactive, ref } from 'vue';
import DoReloadForm from './do-reload-form-modal.vue';
import ReloadResultList from './reload-result-list.vue';
import { reloadApi } from '/@/api/support/reload/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 :visible="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/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 :visible="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/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,143 @@
<!--
* 单号
*
* @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:serial:number:generate'" type="link">生成</a-button>
<a-button @click="showRecord(record.serialNumberId)" v-privilege="'support:serial:number: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, reactive, 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/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 :visible="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/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,24 @@
<!--
* 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="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';
function goHome() {
useRouter().push({ name: HOME_PAGE_NAME });
}
</script>

View File

@@ -0,0 +1,24 @@
<!--
* 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';
function goHome() {
useRouter().push({ name: HOME_PAGE_NAME });
}
</script>

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 '/@/views/system/employee/department/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,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:visible="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 lang="ts">
import message from 'ant-design-vue/lib/message';
import { reactive, ref } from 'vue';
import { departmentApi } from '/@/api/system/department/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,339 @@
<!--
* 部门树形结构
*
* @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>
<a-input v-model:value.trim="keywords" placeholder="请输入部门名称" />
</a-row>
<a-row class="sort-flag-row" v-if="props.showMenu">
显示排序字段
<template v-if="showSortFlag"> 值越大越靠前 </template>
<a-switch v-model:checked="showSortFlag" />
</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' }]"
: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="addDepartment(item.dataRef)" v-privilege="'system:department:add'">添加下级</a-button>
<a-button type="text" @click="updateDepartment(item.dataRef)" v-privilege="'system:department:update'">修改</a-button>
<a-button
type="text"
v-if="item.departmentId != topDepartmentId"
@click="deleteDepartment(item.departmentId)"
v-privilege="'system:department: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>
<!-- 添加编辑部门弹窗 -->
<DepartmentFormModal ref="departmentFormModal" @refresh="refresh" />
</a-card>
</template>
<script setup>
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { onUnmounted, ref, watch } from 'vue';
import { Modal } from 'ant-design-vue';
import _ from 'lodash';
import { createVNode, onMounted } from 'vue';
import DepartmentFormModal from '../department-form-modal/index.vue';
import { departmentApi } from '/@/api/system/department/department-api';
import { SmartLoading } from '/@/components/framework/smart-loading';
import departmentEmitter from '/@/views/system/employee/department/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: true,
},
});
// ----------------------- 部门树的展示 ---------------------
const topDepartmentId = ref();
// 所有部门列表
const departmentList = ref([]);
// 部门树形数据
const departmentTreeData = ref([]);
// 存放部门id和部门用于查找
const idInfoMap = ref(new Map());
// 是否显示排序字段
const showSortFlag = ref(false);
onMounted(() => {
queryDepartmentTree();
});
// 刷新
async function refresh() {
await queryDepartmentTree();
if (currentSelectedDpartmentId.value) {
selectTree(currentSelectedDpartmentId.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 currentSelectedDpartmentId = 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 filterDepartmenet = originData.filter((e) => e.name.indexOf(keywords.value) > -1);
let filterDepartmentList = [];
// 循环筛选出的部门 构建部门树
filterDepartmenet.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,
};
currentSelectedDpartmentId.value = e.departmentId;
departmentFormModal.value.showModal(data);
}
// 编辑
function updateDepartment(e) {
currentSelectedDpartmentId.value = e.departmentId;
departmentFormModal.value.showModal(e);
}
// 删除
function deleteDepartment(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 = departmentList.value.find((e) => e.departmentId == id);
if (selectInfo && selectInfo.parentId) {
selectedKey = selectInfo.parentId;
}
}
}
await departmentApi.deleteDepartment(id);
await queryDepartmentTree();
// 刷新选中部门
if (selectedKey) {
selectTree(selectedKey);
}
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
},
cancelText: '取消',
onCancel() {},
});
}
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;
color: @success-color;
}
.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:visible="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 lang="ts">
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/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,224 @@
<!--
* 员工 表单 弹窗
*
* @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"
:visible="visible"
:body-style="{ paddingBottom: '80px' }"
@close="onClose"
destroyOnClose
>
<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="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="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/employee-api';
import { roleApi } from '/@/api/system/role/role-api';
import DepartmentTreeSelect from '/@/components/system/department-tree-select/index.vue';
import SmartEnumSelect from '/@/components/framework/smart-enum-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 = {
id: undefined,
actualName: undefined,
departmentId: undefined,
disabledFlag: 0,
leaveFlag: 0,
gender: GENDER_ENUM.MAN.value,
loginName: undefined,
phone: undefined,
roleIdList: 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: '在职状态不能为空' }],
};
// 校验表单
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,390 @@
<!--
* 员工 列表
*
* @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 style="margin-left: 8px" type="primary">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
</template>
</a-input-search>
<a-button @click="reset">
<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'" size="small">添加成员</a-button>
<a-button class="btn" size="small" @click="updateEmployeeDepartment" v-privilege="'system:employee:department:update'">调整部门</a-button>
<a-button class="btn" size="small" @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: 1200 }"
row-key="employeeId"
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-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 lang="ts">
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/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: 'phone',
width: 80,
},
{
title: '性别',
dataIndex: 'gender',
width: 40,
},
{
title: '登录账号',
dataIndex: 'loginName',
width: 100,
},
{
title: '状态',
dataIndex: 'disabledFlag',
width: 60,
},
{
title: '角色',
dataIndex: 'roleNameList',
width: 100,
},
{
title: '部门',
dataIndex: 'departmentName',
ellipsis: true,
width: 200,
},
{
title: '操作',
dataIndex: 'operate',
width: 120,
},
]);
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);
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);
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:visible="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,72 @@
<!--
* 组织架构
*
* @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">
<!-- <DepartmentChildren style="flex-grow: 1" :breadcrumb="breadcrumb" :selectedDepartmentChildren="selectedDepartmentChildren" /> -->
<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 DepartmentChildren from './components/department-children/index.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,157 @@
<!--
* 角色 数据范围
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<div>
<div class="btn-group">
<a-button class="button-style" type="primary" @click="updateDataScope" v-privilege="'system:role:dataScope:update'"> 保存 </a-button>
<a-button class="button-style" @click="getDataScope" v-privilege="'role:query'"> 刷新 </a-button>
</div>
<a-row class="header">
<a-col class="tab-margin" :span="4">业务单据</a-col>
<a-col class="tab-data" :span="8">查看数据范围</a-col>
<a-col class="tab-margin" :span="12" />
</a-row>
<div class="data-container">
<a-row class="data" align="middle" justify="center" v-for="(item, index) in dataScopeList" :key="item.dataScopeType">
<a-col class="tab-margin" :span="4">
{{ item.dataScopeTypeName }}
</a-col>
<a-col class="tab-data" :span="8">
<a-radio-group v-model:value="selectedDataScopeList[index].viewType">
<a-radio
v-for="scope in item.viewTypeList"
:key="`${item.dataScopeType}-${scope.viewType}`"
class="radio-style"
:value="scope.viewType"
>{{ scope.viewTypeName }}</a-radio
>
</a-radio-group>
</a-col>
<a-col class="tab-margin tab-desc" :span="12">
<p>{{ item.dataScopeTypeDesc }}</p>
</a-col>
</a-row>
</div>
</div>
</template>
<script setup>
import { message } from 'ant-design-vue';
import _ from 'lodash';
import { inject, onMounted, ref, watch } from 'vue';
import { roleApi } from '/@/api/system/role/role-api';
import { smartSentry } from '/@/lib/smart-sentry';
const props = defineProps({
value: Number,
});
defineEmits(['update:value']);
// ----------------------- 显示 ---------------------------------
let selectRoleId = inject('selectRoleId');
let dataScopeList = ref([]);
let selectedDataScopeList = ref([]);
watch(
() => selectRoleId.value,
() => getRoleDataScope()
);
onMounted(getDataScope);
// 获取系统支持的所有种类的数据范围
async function getDataScope() {
let result = await roleApi.getDataScopeList();
dataScopeList.value = result.data;
selectedDataScopeList.value = [];
dataScopeList.value.forEach((item) => {
selectedDataScopeList.value.push({
viewType: undefined,
dataScopeType: item.dataScopeType,
});
});
getRoleDataScope();
}
// 获取数据范围根据角色id并赋予选中状态
async function getRoleDataScope() {
let result = await roleApi.getDataScopeByRoleId(selectRoleId.value);
let data = result.data;
selectedDataScopeList.value = [];
dataScopeList.value.forEach((item) => {
let find = data.find((e) => e.dataScopeType == item.dataScopeType);
selectedDataScopeList.value.push({
viewType: find ? find.viewType : undefined,
dataScopeType: item.dataScopeType,
});
});
}
// ----------------------- 数据范围更新 ---------------------------------
// 更新
async function updateDataScope() {
try {
let data = {
roleId: selectRoleId.value,
dataScopeItemList: selectedDataScopeList.value.filter((e) => !_.isUndefined(e.viewType)),
};
await roleApi.updateRoleDataScopeList(data);
message.success('保存成功');
getDataScope();
} catch (e) {
smartSentry.captureError(e);
}
}
</script>
<style scoped lang="less">
.btn-group {
text-align: right;
}
.button-style {
margin: 0 10px;
}
.header {
border-bottom: 1px solid #f2f2f2;
font-weight: 600;
margin: 10px 0px;
}
.tab-data {
margin: 10px 0px;
}
.data-container {
height: 680px;
overflow-y: scroll;
}
.data {
border-bottom: 1px solid #f2f2f2;
margin: 10px 0px;
}
.radio-style {
display: block;
height: 30px;
line-height: 30px;
}
.tab-margin {
text-align: center;
margin: 10px 0px;
}
.tab-desc {
line-height: 30px;
font-size: 16px;
text-align: left;
}
</style>

View File

@@ -0,0 +1,263 @@
<!--
* 角色 员工 列表
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @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.keywords" placeholder="姓名/手机号/登录账号" />
<a-button class="button-style" v-if="selectRoleId" type="primary" @click="queryRoleEmployee">搜索</a-button>
<a-button class="button-style" v-if="selectRoleId" type="default" @click="resetQueryRoleEmployee">重置</a-button>
</div>
<div>
<a-button class="button-style" v-if="selectRoleId" type="primary" @click="addRoleEmployee" v-privilege="'system:role:employee:add'"
>添加员工</a-button
>
<a-button class="button-style" v-if="selectRoleId" type="primary" danger @click="batchDelete" v-privilege="'system:role:employee:batch:delete'"
>批量移除</a-button
>
</div>
</div>
<a-table
:loading="tableLoading"
:dataSource="tableData"
:columns="columns"
:pagination="false"
:scroll="{ y: 400 }"
rowKey="employeeId"
:row-selection="{ selectedRowKeys: selectedRowKeyList, onChange: onSelectChange }"
size="small"
bordered
>
<template #bodyCell="{ text, record, 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="deleteEmployeeRole(record.employeeId)" v-privilege="'system:role:employee:delete'">移除</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="queryRoleEmployee"
@showSizeChange="queryRoleEmployee"
:show-total="showTableTotal"
/>
</div>
<EmployeeTableSelectModal ref="selectEmployeeModal" @selectData="selectData" />
</div>
</template>
<script setup>
import { message, Modal } from 'ant-design-vue';
import _ from 'lodash';
import { computed, inject, onMounted, reactive, ref, watch } from 'vue';
import { roleApi } from '/@/api/system/role/role-api';
import { PAGE_SIZE, showTableTotal, PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { SmartLoading } from '/@/components/framework/smart-loading';
import EmployeeTableSelectModal from '/@/components/system/employee-table-select-modal/index.vue';
import { smartSentry } from '/@/lib/smart-sentry';
// ----------------------- 以下是字段定义 emits props ---------------------
let selectRoleId = inject('selectRoleId');
// ----------------------- 员工列表:显示和搜索 ------------------------
watch(
() => selectRoleId.value,
() => queryRoleEmployee()
);
onMounted(queryRoleEmployee);
const defaultQueryForm = {
pageNum: 1,
pageSize: PAGE_SIZE,
roleId: undefined,
keywords: undefined,
};
// 查询表单
const queryForm = reactive({ ...defaultQueryForm });
// 总数
const total = ref(0);
// 表格数据
const tableData = ref([]);
// 表格loading效果
const tableLoading = ref(false);
function resetQueryRoleEmployee() {
queryForm.keywords = '';
queryRoleEmployee();
}
async function queryRoleEmployee() {
try {
tableLoading.value = true;
queryForm.roleId = selectRoleId.value;
let res = await roleApi.queryRoleEmployee(queryForm);
tableData.value = res.data.list;
total.value = res.data.total;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
const columns = reactive([
{
title: '姓名',
dataIndex: 'actualName',
},
{
title: '手机号',
dataIndex: 'phone',
},
{
title: '登录账号',
dataIndex: 'loginName',
},
{
title: '部门',
dataIndex: 'departmentName',
},
{
title: '状态',
dataIndex: 'disabledFlag',
},
{
title: '操作',
dataIndex: 'operate',
width: 60,
},
]);
// ----------------------- 添加成员 ---------------------------------
const selectEmployeeModal = ref();
async function addRoleEmployee() {
let res = await roleApi.getRoleAllEmployee(selectRoleId.value);
let selectedIdList = res.data.map((e) => e.roleId) || [];
selectEmployeeModal.value.showModal(selectedIdList);
}
async function selectData(list) {
if (_.isEmpty(list)) {
message.warning('请选择角色人员');
return;
}
SmartLoading.show();
try {
let params = {
employeeIdList: list,
roleId: selectRoleId.value,
};
await roleApi.batchAddRoleEmployee(params);
message.success('添加成功');
await queryRoleEmployee();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
// ----------------------- 移除成员 ---------------------------------
// 删除角色成员方法
async function deleteEmployeeRole(employeeId) {
Modal.confirm({
title: '提示',
content: '确定要删除该角色成员么?',
okText: '确定',
okType: 'danger',
async onOk() {
SmartLoading.show();
try {
await roleApi.deleteEmployeeRole(employeeId, selectRoleId.value);
message.success('移除成功');
await queryRoleEmployee();
} 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,
roleId: selectRoleId.value,
};
await roleApi.batchRemoveRoleEmployee(params);
message.success('移除成功');
selectedRowKeyList.value = [];
await queryRoleEmployee();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
},
cancelText: '取消',
onCancel() {},
});
}
</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,111 @@
<!--
* 角色 表单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<a-modal :title="form.roleId ? '编辑角色' : '添加角色'" :width="600" :visible="modalVisible" @cancel="onClose" :footer="null">
<a-form ref="formRef" :model="form" :rules="rules" :labelCol="{ span: 4 }">
<a-form-item label="角色名称" name="roleName">
<a-input style="width: 100%" placeholder="请输入角色名称" v-model:value="form.roleName" />
</a-form-item>
<a-form-item label="角色备注">
<a-input style="width: 100%" placeholder="请输入角色备注" v-model:value="form.remark" />
</a-form-item>
</a-form>
<div class="footer">
<a-button style="margin-right: 8px" @click="onClose">取消</a-button>
<a-button type="primary" @click="submitForm">提交</a-button>
</div>
</a-modal>
</template>
<script setup>
import { message } from 'ant-design-vue';
import { reactive, ref } from 'vue';
import { roleApi } from '/@/api/system/role/role-api';
import { smartSentry } from '/@/lib/smart-sentry';
import { SmartLoading } from '/@/components/framework/smart-loading';
// ----------------------- 以下是字段定义 emits props ---------------------
let emits = defineEmits(['refresh']);
defineExpose({
showModal,
});
// ----------------------- modal 显示与隐藏 ---------------------
const modalVisible = ref(false);
function showModal(role) {
Object.assign(form, formDefault);
if (role) {
Object.assign(form, role);
}
modalVisible.value = true;
}
function onClose() {
Object.assign(form, formDefault);
modalVisible.value = false;
}
// ----------------------- 表单 ---------------------
const formRef = ref();
const formDefault = {
id: undefined,
remark: undefined,
roleName: undefined,
};
let form = reactive({ ...formDefault });
// 表单规则
const rules = {
roleName: [{ required: true, message: '请输入角色名称' }],
};
// 提交表单
async function submitForm() {
formRef.value
.validate()
.then(async () => {
SmartLoading.show();
try {
if (form.roleId) {
await roleApi.updateRole(form);
} else {
await roleApi.addRole(form);
}
message.info(`${form.roleId ? '编辑' : '添加'}成功`);
emits('refresh');
onClose();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
})
.catch((error) => {
message.error('参数验证错误,请仔细填写表单数据!');
});
}
</script>
<style scoped lang="less">
.footer {
width: 100%;
border-top: 1px solid #e9e9e9;
padding: 10px 16px;
background: #fff;
text-align: right;
z-index: 1;
}
</style>

View File

@@ -0,0 +1,116 @@
<!--
* 角色 列表
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<a-card title="角色列表" class="role-container" style="padding: 0">
<template #extra>
<a-button type="primary" size="small" @click="showRoleFormModal" v-privilege="'system:role:add'">添加</a-button>
</template>
<a-menu mode="vertical" v-model:selectedKeys="selectedKeys">
<a-menu-item v-for="item in roleList" :key="item.roleId">
<a-popover placement="right">
<template #content>
<div style="display: flex; flex-direction: column">
<a-button type="text" @click="deleteRole(item.roleId)" v-privilege="'system:role:delete'">删除</a-button>
<a-button type="text" @click="showRoleFormModal(item)" v-privilege="'system:role:update'">编辑</a-button>
</div>
</template>
{{ item.roleName }}
</a-popover>
</a-menu-item>
</a-menu>
</a-card>
<RoleFormModal ref="roleFormModal" @refresh="queryAllRole" />
</template>
<script setup>
import { message, Modal } from 'ant-design-vue';
import _ from 'lodash';
import { computed, onMounted, ref } from 'vue';
import { roleApi } from '/@/api/system/role/role-api';
import { SmartLoading } from '/@/components/framework/smart-loading';
import RoleFormModal from '../role-form-modal/index.vue';
import { smartSentry } from '/@/lib/smart-sentry';
// ----------------------- 角色列表显示 ---------------------
const roleList = ref([]);
onMounted(queryAllRole);
// 查询列表
async function queryAllRole() {
let res = await roleApi.queryAll();
roleList.value = res.data;
if (!_.isEmpty(res.data) && res.data[0].roleId) {
selectedKeys.value = [res.data[0].roleId];
}
}
let selectedKeys = ref([]);
const selectRoleId = computed(() => {
if (!selectedKeys.value && _.isEmpty(selectedKeys.value)) {
return null;
}
return selectedKeys.value[0];
});
// ----------------------- 添加、修改、删除 ---------------------------------
const roleFormModal = ref();
// 显示表单框
function showRoleFormModal(role) {
roleFormModal.value.showModal(role);
}
// 删除角色
function deleteRole(roleId) {
if (!roleId) {
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除该角色么?',
okText: '确定',
okType: 'danger',
async onOk() {
SmartLoading.show();
try {
await roleApi.deleteRole(roleId);
message.info('删除成功');
queryAllRole();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
},
cancelText: '取消',
onCancel() {},
});
}
// ----------------------- 以下是暴露的方法内容 ----------------------------
defineExpose({
selectRoleId,
});
</script>
<style scoped lang="less">
.role-container {
height: 100%;
overflow-y: auto;
:deep(.ant-card-body) {
padding: 5px;
}
}
.ant-menu-inline,
.ant-menu-vertical,
.ant-menu-vertical-left {
border-right: none;
}
</style>

View File

@@ -0,0 +1,44 @@
<!--
* 角色 设置
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<a-card class="role-container">
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="1" tab="角色-功能权限">
<RoleTree />
</a-tab-pane>
<a-tab-pane key="2" tab="角色-数据范围">
<RoleDataScope />
</a-tab-pane>
<a-tab-pane key="3" tab="角色-员工列表">
<RoleEmployeeList />
</a-tab-pane>
</a-tabs>
</a-card>
</template>
<script setup>
import { ref } from 'vue';
import RoleDataScope from '../role-data-scope/index.vue';
import RoleEmployeeList from '../role-employee-list/index.vue';
import RoleTree from '../role-tree/index.vue';
defineProps({
value: Number,
});
defineEmits(['update:value']);
let activeKey = ref();
</script>
<style scoped lang="less">
.role-container {
height: 100%;
}
</style>

View File

@@ -0,0 +1,78 @@
:deep(.ant-checkbox-group) {
width: 100%;
}
.tree-header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 20px 0;
}
.col-desc {
margin: 20px 0;
font-size: 15px;
color: #95a5a6;
padding: 0 20px;
}
.button-style {
margin: 20px 0 20px 0;
padding-left: 20px;
text-align: right;
}
.check-right {
margin-right: 20px;
}
.row-border {
border: 1px solid #f0f0f0;
}
.col-border {
line-height: 50px;
padding-left: 20px;
border-right: 1px solid #f0f0f0;
}
.col-left {
line-height: 50px;
padding-left: 40px;
border-right: 1px solid #f0f0f0;
}
.col-right {
padding-left: 20px;
border-right: 1px solid #f0f0f0;
}
.checked-box {
padding: 0 15px;
:deep(ul li::marker) {
content: '';
}
:deep(ul) {
padding: 0;
margin: 0;
li {
list-style: none;
padding: 0;
margin: 10px 0;
.menu {
border-bottom: 1px solid rgb(240, 240, 240);
display: flex;
align-items: center;
line-height: 25px;
}
.point {
display: flex;
align-items: center;
.point-label {
flex: 1;
padding-left: 40px;
border-left: 1px rgb(240, 240, 240) solid;
}
}
.checked-box-label {
min-width: 150px;
}
}
}
}

View File

@@ -0,0 +1,74 @@
<!--
* 角色 树形结构
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<div>
<div class="tree-header">
<p>设置角色对应的功能操作后台管理权限</p>
<a-button v-if="selectRoleId" type="primary" @click="saveChange" v-privilege="'system:role:menu:update'"> 保存 </a-button>
</div>
<!-- 功能权限勾选部分 -->
<RoleTreeCheckbox :tree="tree" />
</div>
</template>
<script setup>
import { inject, ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import _ from 'lodash';
import RoleTreeCheckbox from './role-tree-checkbox.vue';
import { roleMenuApi } from '/@/api/system/role-menu/role-menu-api';
import { useRoleStore } from '/@/store/modules/system/role';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { smartSentry } from '/@/lib/smart-sentry';
let roleStore = useRoleStore();
let tree = ref();
let selectRoleId = inject('selectRoleId');
watch(selectRoleId, () => getRoleSelectedMenu(), {
immediate: true,
});
async function getRoleSelectedMenu() {
if (!selectRoleId.value) {
return;
}
let res = await roleMenuApi.getRoleSelectedMenu(selectRoleId.value);
let data = res.data;
if (_.isEmpty(roleStore.treeMap)) {
roleStore.initTreeMap(data.menuTreeList || []);
}
roleStore.initCheckedData(data.selectedMenuId || []);
tree.value = data.menuTreeList;
}
async function saveChange() {
let checkedData = roleStore.checkedData;
if (_.isEmpty(checkedData)) {
message.error('还未选择任何权限');
return;
}
let params = {
roleId: selectRoleId.value,
menuIdList: checkedData,
};
SmartLoading.show();
try {
await roleMenuApi.updateRoleMenu(params);
message.success('保存成功');
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
}
</script>
<style scoped lang="less">
@import './index.less';
</style>

View File

@@ -0,0 +1,49 @@
<!--
* 角色
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<div style="height: 542px; overflow: auto">
<a-checkbox-group v-model:value="checkedData">
<div class="checked-box">
<ul>
<!--li 菜单模块 start-->
<RoleTreeMenu :tree="props.tree" :index="0" />
<!--li 菜单模块 end-->
</ul>
</div>
</a-checkbox-group>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
import { useRoleStore } from '/@/store/modules/system/role';
import RoleTreeMenu from '../role-tree/role-tree-menu.vue';
let props = defineProps({
tree: {
type: Array,
default: [],
},
});
defineEmits('update:value');
let roleStore = useRoleStore();
let checkedData = ref();
watch(
() => roleStore.checkedData,
(e) => (checkedData.value = e),
{
deep: true,
}
);
</script>
<style scoped lang="less">
@import './index.less';
</style>

View File

@@ -0,0 +1,64 @@
<!--
* 角色 菜单
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<li v-for="module in props.tree" :key="module.menuId">
<div class="menu" :style="{ marginLeft: `${props.index * 4}%` }">
<a-checkbox @change="selectCheckbox(module)" class="checked-box-label" :value="module.menuId">{{ module.menuName }} </a-checkbox>
<div v-if="module.children && module.children.some((e) => e.menuType == MENU_TYPE_ENUM.POINTS.value)">
<RoleTreePoint :tree="module.children" @selectCheckbox="selectCheckbox" />
</div>
</div>
<template v-if="module.children && !module.children.some((e) => e.menuType == MENU_TYPE_ENUM.POINTS.value)">
<RoleTreeMenu :tree="module.children" :index="props.index + 1" />
</template>
</li>
</template>
<script setup>
import { MENU_TYPE_ENUM } from '/@/constants/system/menu-const';
import { useRoleStore } from '/@/store/modules/system/role';
import RoleTreePoint from './role-tree-point.vue';
import RoleTreeMenu from '../role-tree/role-tree-menu.vue';
const props = defineProps({
tree: {
type: Array,
default: [],
},
index: {
type: Number,
default: 0,
},
});
defineEmits('update:value');
let roleStore = useRoleStore();
function selectCheckbox(module) {
if (!module.menuId) {
return;
}
// 是否勾选
let checkedData = roleStore.checkedData;
let findIndex = checkedData.indexOf(module.menuId);
// 选中
if (findIndex == -1) {
// 选中本级以及子级
roleStore.addCheckedDataAndChildren(module);
// 选中上级
roleStore.selectUpperLevel(module);
// 是否有关联菜单 有则选中
if (module.contextMenuId) {
roleStore.addCheckedData(module.contextMenuId);
}
} else {
// 取消选中本级以及子级
roleStore.deleteCheckedDataAndChildren(module);
}
}
</script>

View File

@@ -0,0 +1,33 @@
<!--
* 角色 功能点
*
* @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="point">
<div class="point-label">
<template v-for="module in props.tree" :key="module.menuId">
<a-checkbox @change="emits('selectCheckbox', module)" :value="module.menuId">{{ module.menuName }} </a-checkbox>
</template>
</div>
</div>
</template>
<script setup>
const props = defineProps({
tree: {
type: Array,
default: [],
},
index: {
type: Number,
default: 0,
},
});
let emits = defineEmits('selectCheckbox');
</script>
<style scoped lang="less"></style>

View File

@@ -0,0 +1,45 @@
<!--
* 角色 管理
*
* @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="height100">
<a-row :gutter="10" type="flex" class="height100">
<a-col flex="200px">
<RoleList ref="roleList" />
</a-col>
<a-col flex="1">
<RoleSetting />
</a-col>
</a-row>
</div>
</template>
<script setup>
import { computed, provide, ref } from 'vue';
import RoleList from './components/role-list/index.vue';
import RoleSetting from './components/role-setting/index.vue';
defineProps({
value: Object,
});
defineEmits('update:value');
let roleList = ref();
const selectRoleId = computed(() => {
if (!roleList.value) {
return null;
}
return roleList.value.selectRoleId;
});
provide('selectRoleId', selectRoleId);
</script>
<style scoped lang="less">
.height100 {
height: 100%;
}
</style>

View File

@@ -0,0 +1,107 @@
<!--
* 更新日志
*
* @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="FireTwoTone" 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/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;
color: @text-color-secondary;
min-width: 75px;
}
}
ul li :hover {
color: @primary-color;
}
.un-read a {
color: @text-color;
}
.read a {
color: @text-color-secondary;
}
</style>

View File

@@ -0,0 +1,60 @@
<!--
* 首页 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' }" />
<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>
let props = defineProps({
icon: String,
title: String,
extra: String,
});
let emits = defineEmits(['extraClick']);
function extraClick() {
emits('extraClick');
}
</script>
<style lang="less" scoped>
.card-container {
background-color: #fff;
height: 100%;
.title {
display: flex;
align-items: center;
&::before {
content: '';
position: absolute;
top: 3px;
left: 0;
width: 3px;
height: 30px;
background-color: @primary-color;
}
}
}
</style>

View File

@@ -0,0 +1,92 @@
<template>
<default-home-card icon="ProfileTwoTone" title="【1024创新实验室】人员饭量">
<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,119 @@
<!--
* @Author: zhuoda
* @Date: 2021-08-24 16:35:45
* @LastEditTime: 2022-06-11
* @LastEditors: zhuoda
* @Description:
* @FilePath: /smart-admin/src/views/system/home/components/gauge.vue
-->
<template>
<default-home-card icon="RocketTwoTone" 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="FundTwoTone" title="【1024创新实验室】代码提交量">
<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="PieChartTwoTone" title="【1024创新实验室】上班摸鱼次数">
<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,86 @@
<!--
* 官方 二维码
*
* @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="SmileTwoTone" title="添加微信关注【小镇程序员】、【1024创新实验室】">
<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 class="app-qr">
<img :src="lab1024" />
<span class="qr-desc strong"> 1024创新实验室 </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 lab1024 from '/@/assets/images/1024lab/1024lab-gzh.jpg';
import zhuoda from '/@/assets/images/1024lab/zhuoda-wechat.jpg';
import xiaozhen from '/@/assets/images/1024lab/xiaozhen-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>

View File

@@ -0,0 +1,81 @@
<template>
<a-modal v-model:visible="visible" title="新建快捷入口" @close="onClose">
<a-form ref="formRef" :model="form" :rules="rules">
<a-form-item label="图标" name="icon">
<IconSelect @updateIcon="selectIcon">
<template #iconSelect>
<a-input v-model:value="form.icon" placeholder="请输入菜单图标" style="width: 200px"/>
<component :is="$antIcons[form.icon]" class="smart-margin-left15" style="font-size: 20px"/>
</template>
</IconSelect>
</a-form-item>
<a-form-item label="标题" name="title">
<a-input v-model:value="form.title" placeholder="请输入标题"/>
</a-form-item>
<a-form-item label="路径" name="path">
<a-input v-model:value="form.path" placeholder="请输入路径"/>
</a-form-item>
</a-form>
<template #footer>
<a-button @click="onClose">取消</a-button>
<a-button type="primary" @click="onSubmit">提交</a-button>
</template>
</a-modal>
</template>
<script setup>
import {reactive, ref} from "vue";
import {message} from "ant-design-vue";
import IconSelect from '/@/components/framework/icon-select/index.vue';
import _ from "lodash";
defineExpose({
showModal
})
const emit = defineEmits("addQuickEntry");
// 组件ref
const formRef = ref();
const formDefault = {
icon: undefined,
title: "",
path: "",
};
let form = reactive({...formDefault});
const rules = {
icon: [{required: true, message: "请选择图标"}],
title: [{required: true, message: "标题不能为空"}],
path: [{required: true, message: "路径不能为空"}],
};
const visible = ref(false);
function showModal() {
visible.value = true;
}
function selectIcon(icon) {
form.icon = icon;
}
function onClose() {
Object.assign(form, formDefault);
visible.value = false;
}
function onSubmit() {
formRef.value
.validate()
.then(() => {
emit("addQuickEntry", _.cloneDeep(form));
onClose();
})
.catch((error) => {
console.log("error", error);
message.error("参数验证错误,请仔细填写表单数据!");
});
}
</script>
<style lang='less' scoped></style>

View File

@@ -0,0 +1,149 @@
<template>
<default-home-card
:extra="`${editFlag ? '完成' : '编辑'}`"
icon="ThunderboltTwoTone"
title="快捷入口"
@extraClick="editFlag = !editFlag"
>
<div class="quick-entry-list">
<a-row>
<a-col v-for="(item,index) in quickEntry" :key="index" span="4">
<div class="quick-entry" @click="turnToPage(item.path)">
<div class="icon">
<component :is='$antIcons[item.icon]' :style="{ fontSize:'30px'}"/>
<close-circle-outlined v-if="editFlag" class="delete-icon" @click="deleteQuickEntry(index)"/>
</div>
<span class="entry-title">{{ item.title }}</span>
</div>
</a-col>
<a-col v-if="editFlag && quickEntry.length < maxCount" span="4">
<div class="add-quick-entry" @click="addHomeQuickEntry">
<div class="add-icon">
<plus-outlined :style="{ fontSize:'30px'}"/>
</div>
</div>
</a-col>
</a-row>
</div>
</default-home-card>
<HomeQuickEntryModal ref="homeQuickEntryModal" @addQuickEntry="addQuickEntry"/>
</template>
<script setup>
import {onMounted, ref} from "vue";
import {router} from "/@/router";
import HomeQuickEntryModal from './home-quick-entry-modal.vue'
import localKey from '/@/constants/local-storage-key-const';
import {localRead, localSave} from '/@/utils/local-util';
import _ from "lodash";
import InitQuickEntryList from './init-quick-entry-list';
import DefaultHomeCard from "/@/views/system/home/components/default-home-card.vue";
//---------------- 初始化展示 --------------------
onMounted(() => {
initQuickEntry();
})
let quickEntry = ref([])
function initQuickEntry() {
let quickEntryJson = localRead(localKey.HOME_QUICK_ENTRY);
if (!quickEntryJson) {
quickEntry.value = _.cloneDeep(InitQuickEntryList);
return;
}
let quickEntryList = JSON.parse(quickEntryJson);
if (_.isEmpty(quickEntryList)) {
quickEntry.value = _.cloneDeep(InitQuickEntryList);
return;
}
quickEntry.value = quickEntryList;
}
// 页面跳转
function turnToPage(path) {
if (editFlag.value) {
return;
}
router.push({path});
}
//---------------- 编辑快捷入口 --------------------
let editFlag = ref(false);
let maxCount = ref(6);
// 快捷入口删除
function deleteQuickEntry(index) {
quickEntry.value.splice(index, 1)
localSave(localKey.HOME_QUICK_ENTRY, JSON.stringify(quickEntry.value));
}
// 添加快捷入口
let homeQuickEntryModal = ref();
function addHomeQuickEntry() {
homeQuickEntryModal.value.showModal();
}
function addQuickEntry(row) {
quickEntry.value.push(row);
localSave(localKey.HOME_QUICK_ENTRY, JSON.stringify(quickEntry.value));
}
</script>
<style lang='less' scoped>
.quick-entry-list {
height: 100%;
.quick-entry {
padding: 10px 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 4px;
.entry-title {
margin-top: 5px;
}
.icon {
position: relative;
}
&:hover {
background-color: #F0FFFF;
}
.delete-icon {
position: absolute;
color: #F08080;
top: -5px;
right: -5px;
}
}
.add-quick-entry {
display: flex;
align-items: center;
justify-content: center;
.add-icon {
width: 70px;
height: 70px;
background-color: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 2px;
cursor: pointer;
transition: border-color .3s;
display: flex;
align-items: center;
justify-content: center;
color: #A9A9A9;
&:hover {
border-color: @primary-color;
color: @primary-color;
}
}
}
}
</style>

View File

@@ -0,0 +1,27 @@
export default [
{
icon: 'CopyrightTwoTone',
title: '菜单',
path: '/menu/list'
},
{
icon: 'ExperimentTwoTone',
title: '请求',
path: '/log/operate-log/list'
},
{
icon: 'FireTwoTone',
title: '缓存',
path: '/support/cache/cache-list'
},
{
icon: 'HourglassTwoTone',
title: '字典',
path: '/setting/dict'
},
{
icon: 'MessageTwoTone',
title: '单号',
path: '/support/serial-number/serial-number-list'
}
]

View File

@@ -0,0 +1,158 @@
<!--
* 已办/代办
*
* @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="StarTwoTone" title="已办待办">
<div style="height: 280px;">
<div class="center column">
<a-space direction="vertical" style="width: 100%">
<div v-for="(item, index) in toDoList" :key="index" :class="['to-do', { done: item.doneFlag }]">
<a-checkbox v-model:checked="item.doneFlag">
<span class="task">{{ item.title }}</span>
</a-checkbox>
<div class="star-icon" @click="itemStar(item)">
<StarFilled v-if="item.starFlag" style="color: #ff8c00" />
<StarOutlined v-else style="color: #c0c0c0" />
</div>
</div>
<div v-for="(item, index) in doneList" :key="index" :class="['to-do', { done: item.doneFlag }]">
<a-checkbox v-model:checked="item.doneFlag">
<span class="task">{{ item.title }}</span>
</a-checkbox>
<div class="star-icon" @click="itemStar(item)">
<StarFilled v-if="item.starFlag" style="color: #ff8c00" />
<StarOutlined v-else style="color: #c0c0c0" />
</div>
</div>
</a-space>
</div>
</div>
</default-home-card>
</template>
<script setup>
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
import { computed, ref } from 'vue';
import dayjs from 'dayjs';
import { message } from 'ant-design-vue';
let taskList = ref([
{
title: '周五下班前需要提交周报',
doneFlag: true,
starFlag: true,
starTime: 0,
},
{
title: '为SmartAdmin前端小组分配任务',
doneFlag: false,
starFlag: false,
starTime: 0,
},
{
title: '跟进团建内容事宜',
doneFlag: false,
starFlag: true,
starTime: 0,
},
{
title: '跟进客户定制一个软件平台',
doneFlag: false,
starFlag: false,
starTime: 0,
},
{
title: '下个版本的需求确认',
doneFlag: false,
starFlag: false,
starTime: 0,
},
{
title: '线上版本发布',
doneFlag: true,
starFlag: true,
starTime: dayjs().unix(),
},
{
title: '周一财务报销',
doneFlag: true,
starFlag: false,
starTime: 0,
},
]);
let toDoList = computed(() => {
return taskList.value.filter((e) => !e.doneFlag).sort((a, b) => b.starTime - a.starTime);
});
let doneList = computed(() => {
return taskList.value.filter((e) => e.doneFlag);
});
function itemStar(item) {
item.starFlag = !item.starFlag;
if (item.starFlag) {
item.starTime = dayjs().unix();
}
}
//-------------------------任务新建-----------------------
let taskTitle = ref('');
function addTask() {
if (!taskTitle.value) {
message.warn('请输入任务标题');
return;
}
let data = {
title: taskTitle.value,
doneFlag: false,
starFlag: false,
starTime: 0,
};
taskList.value.unshift(data);
taskTitle.value = '';
}
</script>
<style lang="less" scoped>
.center {
display: flex;
justify-content: center;
height: 100%;
&.column {
flex-direction: column;
width: 100%;
padding: 0 10px;
justify-content: flex-start;
}
}
.to-do {
width: 100%;
border: 1px solid #d3d3d3;
border-radius: 4px;
padding: 4px;
display: flex;
align-items: center;
.star-icon {
margin-left: auto;
cursor: pointer;
}
&.done {
text-decoration: line-through;
color: #8c8c8c;
.task {
color: #8c8c8c;
}
}
}
</style>

View File

@@ -0,0 +1,159 @@
<!--
* 首页 用户头部信息
*
* @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="user-header">
<a-page-header :title="welcomeSentence" :sub-title="departmentName" >
<template #tags>
<a-tag color="blue">努力工作</a-tag>
<a-tag color="success">主动 / 皮实 / 可靠 </a-tag>
<a-tag color="error">自省 / 精进 / 创新</a-tag>
</template>
<template #extra>
<p>{{ dayInfo }}</p>
</template>
<a-row class="content">
<span class="heart-sentence">
<h3>{{ heartSentence }}</h3>
<p class="last-login-info">{{ lastLoginInfo }}</p>
<div></div>
</span>
<div class="weather">
<iframe
width="100%"
scrolling="no"
height="60"
frameborder="0"
allowtransparency="true"
src="//i.tianqi.com/index.php?c=code&id=12&icon=1&num=5&site=12"
></iframe>
</div>
</a-row>
</a-page-header>
</div>
</template>
<script setup>
import { computed } from 'vue-demi';
import { useUserStore } from '/@/store/modules/system/user';
import uaparser from 'ua-parser-js';
import { Solar, Lunar } from 'lunar-javascript';
import _ from 'lodash';
const userStore = useUserStore();
const departmentName = computed(() => useUserStore.departmentName);
// 欢迎语
const welcomeSentence = computed(() => {
let sentence = '';
let now = new Date().getHours();
if (now > 0 && now <= 6) {
sentence = '午夜好,';
} else if (now > 6 && now <= 11) {
sentence = '早上好,';
} else if (now > 11 && now <= 14) {
sentence = '中午好,';
} else if (now > 14 && now <= 18) {
sentence = '下午好,';
} else {
sentence = '晚上好,';
}
return sentence + userStore.$state.actualName;
});
//上次登录信息
const lastLoginInfo = computed(() => {
let info = '';
if (userStore.$state.lastLoginTime) {
info = info + '上次登录:' + userStore.$state.lastLoginTime;
}
if (userStore.$state.lastLoginIp) {
info = info + '; IP:' + userStore.$state.lastLoginIp;
}
if (userStore.$state.lastLoginUserAgent) {
let ua = uaparser(userStore.$state.lastLoginUserAgent);
info = info + '; 设备:';
if (ua.browser.name) {
info = info + ' ' + ua.browser.name;
}
if (ua.os.name) {
info = info + ' ' + ua.os.name;
}
let device = ua.device.vendor ? ua.device.vendor + ua.device.model : null;
if (device) {
info = info + ' ' + device;
}
}
return info;
});
//日期、节日、节气
const dayInfo = computed(() => {
//阳历
let solar = Solar.fromDate(new Date());
let day = solar.toString();
let week = solar.getWeekInChinese();
//阴历
let lunar = Lunar.fromDate(new Date());
let lunarMonth = lunar.getMonthInChinese();
let lunarDay = lunar.getDayInChinese();
//节气
let jieqi = lunar.getPrevJieQi().getName();
let next = lunar.getNextJieQi();
let nextJieqi = next.getName() + ' ' + next.getSolar().toYmd();
return `${day} 星期${week},农历${lunarMonth}${lunarDay}(当前${jieqi}${nextJieqi} `;
});
// 毒鸡汤
const heartSentenceArray = [
'每个人的一生好比一根蜡烛,看似不经意间散发的光和热,都可能照亮和温暖他人。这是生活赋予我们的智慧,也让我们在寻常的日子成为一个温暖善良的人。',
'立规矩的目的,不是禁锢、限制,而是教育;孩子犯了错,父母不能帮孩子逃避,而应该让孩子学会承担责任。让孩子有面对错误的诚实和勇气,这才是立规矩的意义所在。',
'人这一辈子,格局大了、善良有了,成功自然也就近了。格局越大,人生越宽。你的人生会是什么样,与你在为人处世时的表现有很大关系。世间美好都是环环相扣的,善良的人总不会被亏待。',
'平日里的千锤百炼,才能托举出光彩时刻;逆境中的亮剑、失败后的奋起,才能让梦想成真。哪有什么一战成名,其实都是百炼成钢。“天才”都是汗水浇灌出来的,天赋或许可以决定起点,但唯有坚持和努力才能达到终点。',
'家,不在于奢华,而在于温馨;家,不在于大小,而在于珍惜。在家里,有父母的呵护,有爱人的陪伴,有子女的欢笑。一家人整整齐齐、和和睦睦,就是人生最大的幸福!',
'每一个不向命运低头、努力生活的人,都值得被尊重。',
'青年的肩上,从不只有清风明月,更有责任担当。岁月因青春慨然以赴而更加美好,世间因少年挺身向前而更加瑰丽。请相信,不会有人永远年轻,但永远有人年轻。',
'人生路上,总有人走得比你快,但不必介意,也不必着急。一味羡慕别人的成绩,只会给自己平添压力、徒增烦恼。不盲从别人的脚步,坚定目标,才能找到自己的节奏,进而逢山开路、遇水搭桥。',
'如果你真的在乎一个人,首先要学会的就是感恩对方的好。这样,对方才会在和你的相处中找到价值感,相处起来也会更加舒适愉悦。',
'一个人只有心里装得下别人,有换位思考的品质,有为他人谋幸福的信念,才能真正做到慷慨施予。同样,也只有赠人玫瑰而无所求时,你才会手有余香、真有所得。',
];
const heartSentence = computed(() => {
return heartSentenceArray[_.random(0, heartSentenceArray.length - 1)];
});
</script>
<style scoped lang="less">
.user-header {
width: 100%;
background-color: #fff;
margin-bottom: 10px;
.heart-sentence {
width: calc(100% - 500px);
h3 {
color: rgba(0, 0, 0, 0.75);
}
}
.content {
display: flex;
justify-content: space-between;
.weather {
width: 440px;
}
}
.last-login-info {
font-size: 13px;
color: rgba(0, 0, 0, 0.45);
overflow-wrap: break-word;
}
}
</style>

View File

@@ -0,0 +1,129 @@
<!--
* 首页的 通知公告
*
* @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="SoundTwoTone" title="通知公告" @extraClick="onMore">
<a-spin :spinning="loading">
<div class="content-wrapper">
<a-empty v-if="$lodash.isEmpty(data)" />
<ul v-else>
<li v-for="(item, index) in data" :key="index" :class="[item.viewFlag ? 'read' : 'un-read']">
<a-tooltip placement="top">
<template #title>
<span>{{ item.title }}</span>
</template>
<a class="content" @click="toDetail(item.noticeId)">
<a-badge :status="item.viewFlag ? 'default' : 'error'" />
{{ item.title }}
</a>
</a-tooltip>
<span class="time"> {{ item.publishDate }}</span>
</li>
</ul>
</div>
</a-spin>
</default-home-card>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { noticeApi } from '/@/api/business/oa/notice-api';
import { smartSentry } from '/@/lib/smart-sentry';
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
const props = defineProps({
noticeTypeId: {
type: Number,
default: 1,
},
});
const queryForm = {
noticeTypeId: props.noticeTypeId,
pageNum: 1,
pageSize: 6,
searchCount: false,
};
let data = ref([]);
const loading = ref(false);
// 查询列表
async function queryNoticeList() {
try {
loading.value = true;
const result = await noticeApi.queryEmployeeNotice(queryForm);
data.value = result.data.list;
} catch (err) {
smartSentry.captureError(err);
} finally {
loading.value = false;
}
}
onMounted(() => {
queryNoticeList();
});
// 查看更多
function onMore() {
router.push({
path: '/oa/notice/notice-employee-list',
});
}
// 进入详情
const router = useRouter();
function toDetail(noticeId) {
router.push({
path: '/oa/notice/notice-employee-detail',
query: { noticeId },
});
}
</script>
<style lang="less" scoped>
@read-color: #666;
.content-wrapper{
height: 150px;
overflow-y: hidden;
overflow-x: hidden;
}
ul li {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
.content {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
margin-right: 5px;
}
.time {
flex-shrink: 0;
color: @read-color;
min-width: 75px;
}
}
ul li :hover {
color: @primary-color;
}
.un-read a {
color: @text-color;
}
.read a {
color: @read-color;
}
</style>

View File

@@ -0,0 +1,65 @@
.no-footer {
:deep(.ant-card-body) {
padding-bottom: 0;
}
}
.content {
height: 150px;
&.large {
height: 360px;
}
&.statistice {
display: flex;
flex-direction: column;
justify-content: space-between;
}
&.app {
display: flex;
align-items: center;
padding-bottom: 24px;
.app-qr {
display: flex;
align-items: center;
flex-direction: column;
margin-right: 40px;
> img {
height: 120px;
}
> span {
font-size: 14px;
}
}
}
&.gauge {
display: flex;
align-items: center;
}
&.wait-handle {
padding-bottom: 24px;
overflow-y: auto;
> p {
font-size: 18px;
}
:deep(.ant-tag) {
padding: 1px 8px;
font-size: 15px;
}
}
.count {
font-size: 30px;
font-weight: 700;
margin-bottom: 10px;
}
}
.footer {
width: 100%;
border-top: 1px solid #e9e9e9;
padding: 10px 0;
background: #fff;
z-index: 1;
}

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