6d19cb5a05
生成的 markdown 正文现在能一键转成带格式的富文本: - 正文区改为「Markdown 预览 / 富文本排版」两个 tab,一键排版后自动切到编辑器 - 转换统一走 utils/manuscript-content 的 markdownToHtml,新增 plugins 参数, 传 gfm 插件还原表格、删除线(不挂 gfm 时表格会漏成一堆 | 管道符) - 生成标题不拼进正文:标题有独立字段,导出 Word 时才拼进导出件 - 编辑器已有内容且文本不一致时才二次确认覆盖,同一份内容重复排版不打扰 - 富文本下方新增「保存为稿件」,入库写 contentFormat: 'html'; 存过之后按钮置灰,避免连点存出重复草稿 - oa/项目资料 里指向外部排版站的同名按钮改成「公众号排版」,避开同名不同义
489 lines
14 KiB
Vue
489 lines
14 KiB
Vue
<!-- 用户编辑弹窗 -->
|
|
<template>
|
|
<ele-modal
|
|
width="75%"
|
|
:visible="visible"
|
|
:maskClosable="false"
|
|
:confirm-loading="loading"
|
|
:title="isUpdate ? '编辑项目' : '添加项目'"
|
|
:body-style="{ paddingBottom: '8px' }"
|
|
@update:visible="updateVisible"
|
|
@ok="save"
|
|
>
|
|
<a-tabs type="card" v-model:activeKey="activeKey">
|
|
<a-tab-pane key="1" tab="基本信息">
|
|
<a-form
|
|
:label-col="{ md: { span: 2 }, sm: { span: 2 }, xs: { span: 24 } }"
|
|
:wrapper-col="{
|
|
md: { span: 20 },
|
|
sm: { span: 20 },
|
|
xs: { span: 24 }
|
|
}"
|
|
>
|
|
<a-form-item label="项目名称" v-bind="validateInfos.projectName">
|
|
<a-input
|
|
allow-clear
|
|
placeholder="请输入项目名称"
|
|
v-model:value="form.projectName"
|
|
@blur="validate('projectName', { trigger: 'blur' }).catch(() => {})"
|
|
/>
|
|
</a-form-item>
|
|
<a-form-item label="项目编号" v-bind="validateInfos.projectCode">
|
|
<a-input
|
|
allow-clear
|
|
placeholder="请输入用户账号"
|
|
v-model:value="form.projectCode"
|
|
/>
|
|
</a-form-item>
|
|
<a-form-item label="项目分类" v-bind="validateInfos.projectCategory">
|
|
<category-select
|
|
v-model:value="form.projectCategory"
|
|
@blur="
|
|
validate('projectCategory', { trigger: 'blur' }).catch(() => {})
|
|
"
|
|
/>
|
|
</a-form-item>
|
|
<a-form-item label="所属客户" v-bind="validateInfos.customerId">
|
|
<customer-select
|
|
v-model:value="form.customerId"
|
|
@blur="
|
|
validate('customerId', { trigger: 'blur' }).catch(() => {})
|
|
"
|
|
/>
|
|
</a-form-item>
|
|
<a-form-item label="图标">
|
|
<ele-image-upload
|
|
v-model:value="avatar"
|
|
:item-style="{ width: '90px', height: '90px' }"
|
|
:limit="1"
|
|
@upload="onUploadAvatar"
|
|
/>
|
|
</a-form-item>
|
|
<a-form-item label="开发环境" v-bind="validateInfos.urlDev">
|
|
<a-input
|
|
allow-clear
|
|
placeholder="请输入开发环境地址"
|
|
v-model:value="form.urlDev"
|
|
@blur="validate('urlDev', { trigger: 'blur' }).catch(() => {})"
|
|
/>
|
|
</a-form-item>
|
|
<a-form-item label="账号密码" v-bind="validateInfos.account">
|
|
<a-input
|
|
allow-clear
|
|
placeholder="请输入开发环境账号密码"
|
|
v-model:value="form.account"
|
|
@blur="validate('account', { trigger: 'blur' }).catch(() => {})"
|
|
/>
|
|
</a-form-item>
|
|
<a-form-item label="生产环境" v-bind="validateInfos.url">
|
|
<a-input
|
|
allow-clear
|
|
placeholder="请输入生产环境地址"
|
|
v-model:value="form.url"
|
|
@blur="validate('url', { trigger: 'blur' }).catch(() => {})"
|
|
/>
|
|
</a-form-item>
|
|
<a-form-item label="状态" v-bind="validateInfos.status">
|
|
<status-select
|
|
v-model:value="form.status"
|
|
@blur="validate('status', { trigger: 'blur' }).catch(() => {})"
|
|
/>
|
|
</a-form-item>
|
|
<a-form-item label="排序" v-bind="validateInfos.sortNumber">
|
|
<a-input
|
|
allow-clear
|
|
:maxlength="20"
|
|
placeholder="排序"
|
|
v-model:value="form.sortNumber"
|
|
/>
|
|
</a-form-item>
|
|
<a-form-item label="项目简介" v-bind="validateInfos.comments">
|
|
<a-textarea
|
|
:rows="4"
|
|
:maxlength="200"
|
|
placeholder="请输入项目简介"
|
|
v-model:value="form.comments"
|
|
/>
|
|
</a-form-item>
|
|
</a-form>
|
|
</a-tab-pane>
|
|
<a-tab-pane key="2" tab="项目资料">
|
|
<!-- 按钮 -->
|
|
<div style="margin-bottom: 16px">
|
|
<a-space>
|
|
<a-button type="dashed" class="ele-btn-icon" @click="setContent">
|
|
载入模板
|
|
</a-button>
|
|
<!-- <a-button type="dashed" class="ele-btn-icon" @click="showHtml">-->
|
|
<!-- 获取html-->
|
|
<!-- </a-button>-->
|
|
<!-- <a-button type="dashed" class="ele-btn-icon" @click="showText">-->
|
|
<!-- 获取文本-->
|
|
<!-- </a-button>-->
|
|
<!-- <a-button-->
|
|
<!-- type="dashed"-->
|
|
<!-- :danger="!disabled"-->
|
|
<!-- class="ele-btn-icon"-->
|
|
<!-- @click="toggleDisabled"-->
|
|
<!-- >-->
|
|
<!-- {{ disabled ? '启用' : '禁用' }}-->
|
|
<!-- </a-button>-->
|
|
<a-button type="dashed" @click="openMarkdown">转换工具</a-button>
|
|
<a-button
|
|
type="dashed"
|
|
class="ele-btn-icon"
|
|
@click="openUrl('https://p.haoii123.com/')"
|
|
>
|
|
公众号排版
|
|
</a-button>
|
|
</a-space>
|
|
</div>
|
|
<!-- 编辑器 -->
|
|
<tinymce-editor
|
|
v-model:value="content"
|
|
:disabled="disabled"
|
|
:init="config"
|
|
/>
|
|
</a-tab-pane>
|
|
<a-tab-pane key="3" tab="项目附件">
|
|
<ele-image-upload
|
|
v-model:value="files"
|
|
:limit="10"
|
|
:drag="true"
|
|
:multiple="true"
|
|
@upload="onUploadFile"
|
|
/>
|
|
</a-tab-pane>
|
|
</a-tabs>
|
|
<!-- 转换工具 -->
|
|
<markdown v-model:visible="showMarkdown" />
|
|
</ele-modal>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { ref, reactive, watch } from 'vue';
|
|
import { Form, message, Modal } from 'ant-design-vue';
|
|
import { assignObject, htmlToText } from 'ele-admin-pro';
|
|
import CategorySelect from './category-select.vue';
|
|
import StatusSelect from './status-select.vue';
|
|
import CustomerSelect from './customer-select.vue';
|
|
import Markdown from './markdown.vue';
|
|
import { addProject, updateProject } from '@/api/oa/project';
|
|
import type { Project } from '@/api/oa/project/model';
|
|
import { createCode, openUrl } from '@/utils/common';
|
|
import TinymceEditor from '@/components/TinymceEditor/index.vue';
|
|
import type { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
|
|
import { uploadFile } from '@/api/system/file';
|
|
import { FILE_SERVER } from '@/config/setting';
|
|
|
|
const useForm = Form.useForm;
|
|
// 是否是修改
|
|
const isUpdate = ref(false);
|
|
const props = defineProps<{
|
|
// 弹窗是否打开
|
|
visible: boolean;
|
|
// 修改回显的数据
|
|
data?: Project | null;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'done'): void;
|
|
(e: 'update:visible', visible: boolean): void;
|
|
}>();
|
|
|
|
// 提交状态
|
|
const loading = ref(false);
|
|
const activeKey = ref('1');
|
|
const content = ref('');
|
|
const disabled = ref(false);
|
|
const showMarkdown = ref(false);
|
|
const editorRef = ref<InstanceType<typeof TinymceEditor> | null>(null);
|
|
|
|
// 用户信息
|
|
const form = reactive<Project>({
|
|
projectCode: `X` + createCode(),
|
|
projectName: '',
|
|
projectAvatar: '',
|
|
projectCategory: undefined,
|
|
urlDev: '',
|
|
urlAdmin: '',
|
|
account: '',
|
|
comments: '',
|
|
content: '',
|
|
sortNumber: 100,
|
|
customerName: '',
|
|
customerId: undefined,
|
|
status: '0',
|
|
projectId: 0
|
|
});
|
|
|
|
/* 更新visible */
|
|
const updateVisible = (value: boolean) => {
|
|
emit('update:visible', value);
|
|
};
|
|
|
|
// 表单验证规则
|
|
const rules = reactive({
|
|
projectName: [
|
|
{
|
|
required: true,
|
|
type: 'string',
|
|
message: '请输入项目名称',
|
|
trigger: 'blur'
|
|
}
|
|
],
|
|
projectCode: [
|
|
{
|
|
required: true,
|
|
type: 'string',
|
|
message: '请输入项目编号',
|
|
trigger: 'blur'
|
|
}
|
|
],
|
|
// projectAvatar: [
|
|
// {
|
|
// required: true,
|
|
// type: 'string',
|
|
// message: '请上传项目图标',
|
|
// trigger: 'blur'
|
|
// }
|
|
// ],
|
|
projectCategory: [
|
|
{
|
|
required: true,
|
|
type: 'string',
|
|
message: '请选择项目分类',
|
|
trigger: 'blur'
|
|
}
|
|
],
|
|
customerId: [
|
|
{
|
|
required: true,
|
|
type: 'number',
|
|
message: '请选择所属客户',
|
|
trigger: 'blur'
|
|
}
|
|
],
|
|
status: [
|
|
{
|
|
required: true,
|
|
type: 'string',
|
|
message: '项目状态',
|
|
trigger: 'blur'
|
|
}
|
|
]
|
|
});
|
|
|
|
const { resetFields, validate, validateInfos } = useForm(form, rules);
|
|
|
|
// 项目图标
|
|
const avatar = ref([]);
|
|
|
|
const onUploadAvatar = (d: ItemType) => {
|
|
uploadFile(<File>d.file)
|
|
.then((result) => {
|
|
console.log(result);
|
|
form.projectAvatar = result.path;
|
|
message.success('上传成功');
|
|
})
|
|
.catch((e) => {
|
|
message.error(e.message);
|
|
});
|
|
};
|
|
|
|
// 已上传数据
|
|
const files = ref([]);
|
|
|
|
const onUploadFile = (d: ItemType) => {
|
|
uploadFile(<File>d.file)
|
|
.then((result) => {
|
|
console.log(result);
|
|
form.projectAvatar = result.path;
|
|
message.success('上传成功');
|
|
})
|
|
.catch((e) => {
|
|
message.error(e.message);
|
|
});
|
|
};
|
|
|
|
/* 修改编辑器内容 */
|
|
const setContent = () => {
|
|
content.value = [
|
|
'<div style="text-align: center;color: #fff;background-image: linear-gradient(-90deg, rgb(62,119,255), rgb(159,98,212), rgb(255,78,170));padding: 32px 0;">',
|
|
' <div style="font-size: 28px;margin-bottom: 16px;">项目资料模板</div>',
|
|
' <div style="font-size:18px">通过设置常用模板可以加快工作效率哦!</div>',
|
|
'</div><br/>'
|
|
].join('');
|
|
};
|
|
|
|
/* 获取编辑器内容 */
|
|
const showHtml = () => {
|
|
Modal.info({
|
|
maskClosable: true,
|
|
content: content.value
|
|
});
|
|
};
|
|
|
|
/* 获取编辑器纯文本内容 */
|
|
const showText = () => {
|
|
Modal.info({
|
|
maskClosable: true,
|
|
content: htmlToText(content.value)
|
|
});
|
|
};
|
|
|
|
/* 禁用启用切换 */
|
|
const toggleDisabled = () => {
|
|
disabled.value = !disabled.value;
|
|
};
|
|
|
|
/* 打开用户详情弹窗 */
|
|
const openMarkdown = () => {
|
|
showMarkdown.value = true;
|
|
};
|
|
|
|
const config = ref({
|
|
height: 520,
|
|
// 自定义文件上传(这里使用把选择的文件转成 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 uploadHandler = (file: File) => {
|
|
const item: ItemType = {
|
|
file,
|
|
uid: (file as any).uid,
|
|
name: file.name,
|
|
progress: 0,
|
|
status: undefined
|
|
};
|
|
console.log(item);
|
|
if (!file.type.startsWith('image')) {
|
|
message.error('只能选择图片');
|
|
return;
|
|
}
|
|
if (file.size / 1024 / 1024 > 2) {
|
|
message.error('大小不能超过 2MB');
|
|
return;
|
|
}
|
|
item.url = window.URL.createObjectURL(file);
|
|
images.value.push(item);
|
|
};
|
|
|
|
|
|
/* 上传 item */
|
|
const onUpload = ({ file }) => {
|
|
// const item = images.value.find((t) => t.uid === d.uid) ?? d;
|
|
// console.log('item:', item);
|
|
// 模拟上传
|
|
// item.status = 'uploading';
|
|
// item.progress = 20;
|
|
uploadFile(file).then((res) => {
|
|
message.success('上传成功');
|
|
console.log(res);
|
|
});
|
|
// const timer = setInterval(() => {
|
|
// if (item.progress == null) {
|
|
// item.progress = 20;
|
|
// } else {
|
|
// item.progress += 20;
|
|
// }
|
|
// if (item.progress === 100) {
|
|
// item.status = 'done';
|
|
// clearInterval(timer);
|
|
// }
|
|
// }, 1000);
|
|
};
|
|
|
|
/* 保存编辑 */
|
|
const save = () => {
|
|
validate()
|
|
.then(() => {
|
|
loading.value = true;
|
|
// 处理文章内容
|
|
form.content = content.value;
|
|
const data = {
|
|
...form
|
|
};
|
|
// 转字符串
|
|
const saveOrUpdate = isUpdate.value ? updateProject : addProject;
|
|
saveOrUpdate(data)
|
|
.then((msg) => {
|
|
loading.value = false;
|
|
message.success(msg);
|
|
updateVisible(false);
|
|
emit('done');
|
|
})
|
|
.catch((e) => {
|
|
loading.value = false;
|
|
message.error(e.message);
|
|
});
|
|
})
|
|
.catch(() => {});
|
|
};
|
|
|
|
watch(
|
|
() => props.visible,
|
|
(visible) => {
|
|
if (visible) {
|
|
form.projectCode = `X` + createCode();
|
|
if (props.data) {
|
|
loading.value = false;
|
|
assignObject(form, props.data);
|
|
if (props.data?.content != null) {
|
|
content.value = props.data?.content;
|
|
}
|
|
if (form.projectCode == '') {
|
|
form.projectCode = `X` + createCode();
|
|
}
|
|
// 项目图标赋值
|
|
avatar.value = [];
|
|
if(props.data.projectAvatar){
|
|
avatar.value.push({ uid:1, url: FILE_SERVER + props.data.projectAvatar, status: 'done' });
|
|
}
|
|
isUpdate.value = true;
|
|
} else {
|
|
isUpdate.value = false;
|
|
}
|
|
} else {
|
|
resetFields();
|
|
}
|
|
}
|
|
);
|
|
</script>
|
|
<style lang="less"></style>
|