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