Initial commit

This commit is contained in:
南宁网宿科技
2024-04-24 16:36:46 +08:00
commit 121348e011
991 changed files with 158700 additions and 0 deletions

View File

@@ -0,0 +1,184 @@
<!-- 文档编辑弹窗 -->
<template>
<ele-modal
:width="400"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '修改文档' : '添加文档'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
:label-col="{ md: { span: 24 }, sm: { span: 24 } }"
:wrapper-col="{ md: { span: 24 }, sm: { span: 24 } }"
layout="vertical"
>
<a-form-item label="上级目录" v-bind="validateInfos.parentId">
<docs-select
:data="docs"
placeholder="请选择上级目录"
v-model:value="form.parentId"
/>
</a-form-item>
<a-form-item label="文档标题" v-bind="validateInfos.title">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入文档标题"
v-model:value="form.title"
@pressEnter="save"
/>
</a-form-item>
<a-form-item label="排序号" v-bind="validateInfos.sortNumber">
<a-input-number
:min="0"
:max="99999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
</a-form>
<!-- <a-alert-->
<!-- message="文档内容应该放在最后一级"-->
<!-- banner-->
<!-- style="margin-bottom: 12px"-->
<!-- />-->
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import DocsSelect from './docs-select.vue';
import { addDocs, updateDocs } from '@/api/cms/docs';
import type { Docs } from '@/api/cms/docs/model';
const useForm = Form.useForm;
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Docs | null;
// 文档id
docsId?: number;
// 全部文档
docs: Docs[];
}>();
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 表单数据
const form = reactive<Docs>({
// 文档id
docsId: 0,
// 文档标题
title: '',
// 文档类型 0目录 1文档
type: 0,
// 上级文档
parentId: 0,
// 封面图
avatar: '',
// 用户ID
userId: '',
// 所属门店ID
shopId: '',
// 排序
sortNumber: 100,
// 备注
comments: '',
// 内容
content: ''
});
// 表单验证规则
const rules = reactive({
title: [
{
required: true,
message: '请输入文档标题',
type: 'string',
trigger: 'blur'
}
],
sortNumber: [
{
required: true,
message: '请输入排序号',
type: 'number',
trigger: 'blur'
}
]
});
/* type选择改变 */
const onTypeChange = (e) => {
if (e.target.value === 1) {
form.type = 0;
} else {
form.type = 1;
}
};
const { resetFields, validate, validateInfos } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
validate()
.then(() => {
loading.value = true;
const docsData = {
...form,
parentId: form.parentId || 0
};
const saveOrUpdate = isUpdate.value ? updateDocs : addDocs;
saveOrUpdate(docsData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
} else {
form.parentId = props.docsId;
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>

View File

@@ -0,0 +1,39 @@
<!-- 目录选择下拉框 -->
<template>
<a-tree-select
allow-clear
tree-default-expand-all
:placeholder="placeholder"
:value="value || undefined"
:tree-data="data"
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
@update:value="updateValue"
/>
</template>
<script lang="ts" setup>
import type { Docs } from '@/api/cms/docs/model';
const emit = defineEmits<{
(e: 'update:value', value?: number): void;
}>();
withDefaults(
defineProps<{
// 选中的数据(v-modal)
value?: number;
// 提示信息
placeholder?: string;
// 目录数据
data: Docs[];
}>(),
{
placeholder: '请选择目录'
}
);
/* 更新选中数据 */
const updateValue = (value?: number) => {
emit('update:value', value);
};
</script>

View File

@@ -0,0 +1,169 @@
<template>
<div class="page">
<a-card v-if="current" :bordered="false" :title="`${current.title}`">
<template #extra>
<a-space>
<a @click="onEdit">{{ isUpdate ? '预览' : '编辑' }}</a>
<a-divider v-if="isUpdate" type="vertical" />
<a v-if="isUpdate" type="primary" @click="save">保存</a>
</a-space>
</template>
<div class="content">
<!-- 编辑器 -->
<byte-md-editor
v-if="isUpdate"
v-model:value="content"
placeholder="请输入您的内容,图片请直接粘贴"
:locale="zh_Hans"
mode="split"
:plugins="plugins"
:editorConfig="{ lineNumbers: true }"
@paste="onPaste"
/>
<byte-md-viewer v-else :value="content" :plugins="plugins" />
</div>
</a-card>
<div class="docs-sumbit" v-if="isUpdate">
<a-button type="primary" @click="save">保存</a-button>
</div>
</div>
</template>
<script lang="ts" setup>
import 'bytemd/dist/index.min.css';
import { reactive, ref, watch } from 'vue';
import 'github-markdown-css/github-markdown-light.css';
import type { Docs } from '@/api/cms/docs/model';
import { message } from 'ant-design-vue';
import ByteMdEditor from '@/components/ByteMdEditor/index.vue';
import highlight from '@bytemd/plugin-highlight';
// 中文语言文件
import zh_Hans from 'bytemd/locales/zh_Hans.json';
// // 链接、删除线、复选框、表格等的插件
import gfm from '@bytemd/plugin-gfm';
// // 插件的中文语言文件
import zh_HansGfm from '@bytemd/plugin-gfm/locales/zh_Hans.json';
// // 预览界面的样式,这里用的 github 的 markdown 主题
import 'github-markdown-css/github-markdown-light.css';
import { addDocs, updateDocs } from '@/api/cms/docs';
import {ItemType} from "ele-admin-pro/es/ele-image-upload/types";
import {uploadFile} from "@/api/system/file";
const props = defineProps<{
// 文档 id
current?: Docs | null;
}>();
// 插件
const plugins = ref([
gfm({
locale: zh_HansGfm
}),
highlight()
]);
// 提交状态
const loading = ref(false);
// 编辑器内容,双向绑定
const content = ref('');
const editStatus = ref(false);
// const disabled = ref(false);
// 是否是修改
const isUpdate = ref(false);
// const docsContentId = ref(0);
const form = reactive<Docs>({
docsId: 0,
content: ''
});
const onEdit = () => {
isUpdate.value = !isUpdate.value;
}
/* 粘贴图片上传服务器并插入编辑器 */
const onPaste = (e) => {
console.log(e);
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
console.log(items);
let hasFile = false;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
let file = items[i].getAsFile();
const item: ItemType = {
file,
uid: (file as any).lastModified,
name: file.name
};
uploadFile(<File>item.file)
.then((result) => {
const addPath = '!['+result.name+']('+ result.url+')\n\r';
content.value = content.value + addPath
})
.catch((e) => {
message.error(e.message);
});
hasFile = true;
}
}
if (hasFile) {
e.preventDefault();
}
}
// 保存文档
const save = () => {
loading.value = true;
const docsData = {
...form,
docsId: props.current?.docsId,
content: content.value
};
const saveOrUpdate = isUpdate.value ? updateDocs : addDocs;
saveOrUpdate(docsData)
.then((msg) => {
loading.value = false;
isUpdate.value = false;
message.success(msg);
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
};
// 监听文档 id 变化
watch(
() => props.current?.docsId,
() => {
content.value = props.current?.content + '';
}
);
</script>
<script lang="ts">
export default {
name: 'Markdown'
};
</script>
<style lang="less" scoped>
/deep/.markdown-body{
background-color: transparent; /* 设置背景透明 */
}
.sys-category-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.sys-category-table :deep(.ant-table-pagination.ant-pagination) {
padding: 0 4px;
margin-bottom: 0;
}
.content *{
max-width: 80%;
}
.docs-sumbit {
margin: 20px 0;
}
</style>

View File

@@ -0,0 +1,258 @@
<template>
<div class="ele-body">
<a-card
:bordered="false"
class="transparent-bg"
:body-style="{ padding: '16px' }"
>
<ele-split-layout
width="266px"
allow-collapse
:right-style="{ overflow: 'hidden' }"
:style="{ minHeight: 'calc(100vh - 152px)' }"
>
<a-card :body-style="{ padding: '0' }">
<ele-toolbar theme="default">
<a-space :size="10">
<span>目录</span>
<a-button
v-permission="'cms:docs:save'"
type="primary"
:size="`small`"
class="ele-btn-icon"
@click="openEdit()"
>
<template #icon>
<plus-outlined />
</template>
</a-button>
<a-button
v-permission="'cms:docs:update'"
type="primary"
:size="`small`"
:disabled="!current"
class="ele-btn-icon"
@click="openEdit(current)"
>
<template #icon>
<edit-outlined />
</template>
</a-button>
<a-button
v-permission="'cms:docs:remove'"
danger
type="primary"
:size="`small`"
:disabled="!current"
class="ele-btn-icon"
@click="remove"
>
<template #icon>
<delete-outlined />
</template>
</a-button>
</a-space>
</ele-toolbar>
<div class="ele-border-split sys-docs-list">
<a-tree
:tree-data="data"
v-model:expanded-keys="expandedRowKeys"
v-model:selected-keys="selectedRowKeys"
@select="onTreeSelect"
>
<template #title="{ key: treeKey, title }">
<a-dropdown :trigger="['contextmenu']">
<span>{{ title }}</span>
<template #overlay>
<a-menu
@click="
({ key: menuKey }) =>
onContextMenuClick(treeKey, menuKey)
"
>
<a-menu-item key="1">添加</a-menu-item>
<a-menu-item key="2">修改</a-menu-item>
<a-menu-item key="3">删除</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</a-tree>
</div>
</a-card>
<template #content>
<content :data="data" :current="current" @done="query"></content>
</template>
</ele-split-layout>
</a-card>
<!-- 编辑弹窗 -->
<docs-edit
v-model:visible="showEdit"
:data="editData"
:docs="data"
:docs-id="current?.docsId"
@done="query"
/>
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue';
import { toTreeData, eachTreeData } from 'ele-admin-pro';
import Content from './content.vue';
import DocsEdit from './components/docs-edit.vue';
import { listDocs, getDocs, removeDocs } from '@/api/cms/docs';
import type { Docs, DocsParam } from '@/api/cms/docs/model';
import useSearch from '@/utils/use-search';
// 加载状态
const loading = ref(true);
// 树形数据
const data = ref<any[]>([]);
// 树展开的key
const expandedRowKeys = ref<number[]>([]);
// 树选中的key
const selectedRowKeys = ref<number[]>([]);
// 选中数据
const current = ref<Docs | null>();
// 是否显示表单弹窗
const showEdit = ref(false);
// 编辑回显数据
const editData = ref<Docs | null>(null);
// 表单数据
const { where } = useSearch<DocsParam>({
title: ''
});
/* 查询 */
const query = () => {
loading.value = true;
listDocs()
.then((list) => {
loading.value = false;
const eks: number[] = [];
list.forEach((d, i) => {
d.key = d.docsId;
d.value = d.docsId;
if (typeof d.docsId === 'number') {
eks.push(d.docsId);
}
});
expandedRowKeys.value = eks;
data.value = toTreeData({
data: list,
idField: 'docsId',
parentIdField: 'parentId'
});
if (list.length) {
if (typeof list[0].docsId === 'number') {
selectedRowKeys.value = [list[0].docsId];
}
queryDocs(list[0].docsId);
}
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
};
/* 选择数据 */
const onTreeSelect = () => {
eachTreeData(data.value, (d) => {
if (
typeof d.docsId === 'number' &&
selectedRowKeys.value.includes(d.docsId)
) {
queryDocs(d.docsId);
return false;
}
});
};
// 查询文档
const queryDocs = (docsId) => {
getDocs(docsId).then((detail) => {
current.value = detail;
});
};
/* 打开编辑弹窗 */
const openEdit = (item?: Docs | null) => {
editData.value = item ?? null;
showEdit.value = true;
};
/* 删除 */
const remove = () => {
Modal.confirm({
title: '提示',
content: '确定要删除选中的文档吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeDocs(current.value?.docsId)
.then((msg) => {
hide();
message.success(msg);
query();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
const onContextMenuClick = (treeKey: string, menuKey: string | number) => {
// 添加操作
if (menuKey == 1) {
openEdit();
}
// 编辑操作
if (menuKey == 2) {
openEdit(current.value);
}
// 删除操作
if (menuKey == 3) {
remove();
}
console.log(`treeKey: ${treeKey}, menuKey: ${menuKey}`);
};
query();
</script>
<script lang="ts">
export default {
name: 'Docs'
};
</script>
<style lang="less" scoped>
.transparent-bg {
background-color: transparent; /* 设置背景透明 */
}
.sys-docs-list {
padding: 12px 6px;
height: calc(100vh - 242px);
border-width: 1px;
border-style: solid;
overflow: auto;
}
</style>

View File

@@ -0,0 +1,162 @@
<template>
<div class="page">
<a-card v-if="current" :bordered="false" :title="`${current.title}`">
<!-- 编辑器 -->
<tinymce-editor
v-model:value="content"
:disabled="disabled"
:init="config"
/>
<div class="ele-text-info" style="margin-top: 10px">
<a-space :size="100">
<div>文档ID: {{ current.docsId }}</div>
<div>预览地址: {{ `/docs?id=${current.docsId}` }}</div>
<div>发布时间: {{ current.createTime }}</div>
</a-space>
</div>
<div class="docs-sumbit" v-permission="'cms:docs:update'">
<a-space>
<a-button type="primary" @click="save">保存</a-button>
<!-- <router-link :to="'/content/docs?id=' + current.docsId" @click.stop="">-->
<!-- <a-button>预览</a-button>-->
<!-- </router-link>-->
</a-space>
</div>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, watch } from 'vue';
import 'github-markdown-css/github-markdown-light.css';
import type { Docs } from '@/api/cms/docs/model';
import { message } from 'ant-design-vue';
import TinymceEditor from '@/components/TinymceEditor/index.vue';
import {
addDocsContent,
getDocsContent,
updateDocsContent
} from '@/api/cms/docs-content';
import { DocsContent } from '@/api/cms/docs-content/model';
const props = defineProps<{
// 文档 id
current?: Docs | null;
}>();
// 提交状态
const loading = ref(false);
// 编辑器内容,双向绑定
const content = ref('');
const disabled = ref(false);
// 是否是修改
const isUpdate = ref(false);
const docsContentId = ref(0);
const form = reactive<DocsContent>({
docsContentId: 0,
docsId: 0,
content: ''
});
const editorRef = ref<InstanceType<typeof TinymceEditor> | null>(null);
const config = ref({
height: 690,
// 自定义文件上传(这里使用把选择的文件转成 blob 演示)
file_picker_callback: (callback: any, _value: any, meta: any) => {
const input = document.createElement('input');
input.setAttribute('type', 'file');
// 设定文件可选类型
if (meta.filetype === 'image') {
input.setAttribute('accept', 'image/*');
} else if (meta.filetype === 'media') {
input.setAttribute('accept', 'video/*');
}
input.onchange = () => {
const file = input.files?.[0];
if (!file) {
return;
}
if (meta.filetype === 'media') {
if (!file.type.startsWith('video/')) {
editorRef.value?.alert({ content: '只能选择视频文件' });
return;
}
}
if (file.size / 1024 / 1024 > 20) {
editorRef.value?.alert({ content: '大小不能超过 20MB' });
return;
}
const reader = new FileReader();
reader.onload = (e) => {
if (e.target?.result != null) {
const blob = new Blob([e.target.result], { type: file.type });
callback(URL.createObjectURL(blob));
}
};
reader.readAsArrayBuffer(file);
};
input.click();
}
});
/* 搜索 */
const reload = () => {
getDocsContent(Number(props.current?.docsId))
.then((res) => {
content.value = String(res.content);
docsContentId.value = Number(res.docsContentId);
isUpdate.value = true;
})
.catch(() => {
loading.value = false;
content.value = '';
isUpdate.value = false;
});
};
// 保存文档
const save = () => {
loading.value = true;
const docsData = {
...form,
docsContentId: docsContentId.value,
docsId: props.current?.docsId,
content: content.value
};
const saveOrUpdate = isUpdate.value ? updateDocsContent : addDocsContent;
saveOrUpdate(docsData)
.then((msg) => {
loading.value = false;
message.success(msg);
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
};
// 监听文档 id 变化
watch(
() => props.current?.docsId,
() => {
reload();
}
);
</script>
<style lang="less" scoped>
.sys-category-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.sys-category-table :deep(.ant-table-pagination.ant-pagination) {
padding: 0 4px;
margin-bottom: 0;
}
.docs-sumbit {
margin: 10px 0;
}
</style>

View File

@@ -0,0 +1,162 @@
<template>
<div class="page">
<a-card v-if="current" :bordered="false" :title="`${current.title}`">
<!-- 编辑器 -->
<tinymce-editor
v-model:value="content"
:disabled="disabled"
:init="config"
/>
<div class="ele-text-info" style="margin-top: 10px">
<a-space :size="100">
<div>文档ID: {{ current.docsId }}</div>
<div>预览地址: {{ `/docs?id=${current.docsId}` }}</div>
<div>发布时间: {{ current.createTime }}</div>
</a-space>
</div>
<div class="docs-sumbit" v-permission="'cms:docs:update'">
<a-space>
<a-button type="primary" @click="save">保存</a-button>
<!-- <router-link :to="'/content/docs?id=' + current.docsId" @click.stop="">-->
<!-- <a-button>预览</a-button>-->
<!-- </router-link>-->
</a-space>
</div>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, watch } from 'vue';
import 'github-markdown-css/github-markdown-light.css';
import type { Docs } from '@/api/cms/docs/model';
import { message } from 'ant-design-vue';
import TinymceEditor from '@/components/TinymceEditor/index.vue';
import {
addDocsContent,
getDocsContent,
updateDocsContent
} from '@/api/cms/docs-content';
import { DocsContent } from '@/api/cms/docs-content/model';
const props = defineProps<{
// 文档 id
current?: Docs | null;
}>();
// 提交状态
const loading = ref(false);
// 编辑器内容,双向绑定
const content = ref('');
const disabled = ref(false);
// 是否是修改
const isUpdate = ref(false);
const docsContentId = ref(0);
const form = reactive<DocsContent>({
docsContentId: 0,
docsId: 0,
content: ''
});
const editorRef = ref<InstanceType<typeof TinymceEditor> | null>(null);
const config = ref({
height: 690,
// 自定义文件上传(这里使用把选择的文件转成 blob 演示)
file_picker_callback: (callback: any, _value: any, meta: any) => {
const input = document.createElement('input');
input.setAttribute('type', 'file');
// 设定文件可选类型
if (meta.filetype === 'image') {
input.setAttribute('accept', 'image/*');
} else if (meta.filetype === 'media') {
input.setAttribute('accept', 'video/*');
}
input.onchange = () => {
const file = input.files?.[0];
if (!file) {
return;
}
if (meta.filetype === 'media') {
if (!file.type.startsWith('video/')) {
editorRef.value?.alert({ content: '只能选择视频文件' });
return;
}
}
if (file.size / 1024 / 1024 > 20) {
editorRef.value?.alert({ content: '大小不能超过 20MB' });
return;
}
const reader = new FileReader();
reader.onload = (e) => {
if (e.target?.result != null) {
const blob = new Blob([e.target.result], { type: file.type });
callback(URL.createObjectURL(blob));
}
};
reader.readAsArrayBuffer(file);
};
input.click();
}
});
/* 搜索 */
const reload = () => {
getDocsContent(Number(props.current?.docsId))
.then((res) => {
content.value = String(res.content);
docsContentId.value = Number(res.docsContentId);
isUpdate.value = true;
})
.catch(() => {
loading.value = false;
content.value = '';
isUpdate.value = false;
});
};
// 保存文档
const save = () => {
loading.value = true;
const docsData = {
...form,
docsContentId: docsContentId.value,
docsId: props.current?.docsId,
content: content.value
};
const saveOrUpdate = isUpdate.value ? updateDocsContent : addDocsContent;
saveOrUpdate(docsData)
.then((msg) => {
loading.value = false;
message.success(msg);
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
};
// 监听文档 id 变化
watch(
() => props.current?.docsId,
() => {
reload();
}
);
</script>
<style lang="less" scoped>
.sys-category-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.sys-category-table :deep(.ant-table-pagination.ant-pagination) {
padding: 0 4px;
margin-bottom: 0;
}
.docs-sumbit {
margin: 10px 0;
}
</style>