2025-07-03 18:15:16 +08:00

843 lines
20 KiB
Vue

<template>
<div class="container">
<div class="ai-box">
<div class="editor-box">
<div class="toolbar-box">
<Toolbar style="" :editor="editorRef" :defaultConfig="toolbarConfig" :mode="mode" />
</div>
<div class="editor-box-content">
<div class="title-box">
<input type="text" v-model="newTitle" class="title-input" placeholder="请输入文章标题" />
</div>
<Editor class="editor" style="height: calc(100% - 60px); overflow-y: hidden;" v-model="valueHtml"
:defaultConfig="editorConfig" :mode="mode" @onCreated="handleCreated" />
</div>
<!-- 添加字数统计显示 -->
<div class="editor-btn-box">
<div class="character-count">
字数: {{ characterCount }}
</div>
<el-button type="primary" @click="handleSaveArticle">保存</el-button>
<!-- <el-button type="primary" @click="handleSaveArticle(2)">存草稿</el-button> -->
<el-button type="primary" @click="handlePreviewArticle">预览</el-button>
</div>
</div>
</div>
<input type="file" @change="handleUpload" />
<el-dialog v-model="dialogPreview" title="文章预览" width="1000px">
<div class="dialog-preview" style="padding: 20px;">
<div class="preview-title">{{ newTitle }}</div>
<div class="preview-content" v-html="valueHtml">
</div>
</div>
<template #footer>
<el-button type="primary" @click="dialogPreview = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import '@wangeditor/editor/dist/css/style.css' // 引入 css
import { Loading } from '@element-plus/icons-vue'
import { onBeforeUnmount, onUnmounted, ref, reactive, shallowRef, onMounted, computed, getCurrentInstance, nextTick } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { addNews, editNews } from '@/api/news'
import { getCode } from '@/api/upload'
import { useRoute, useRouter } from 'vue-router'
import { DomEditor } from '@wangeditor/editor'
import OSS from 'ali-oss'
const ossurl = 'https://image-fudan.oss-cn-beijing.aliyuncs.com'
const route = useRoute()
const router = useRouter()
const generating = ref(1)
// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef()
// 内容 HTML
const valueHtml = ref('')
const mode = ref('default')
// 纯文本
const valueText = ref('')
// 添加字数统计
const characterCount = ref(0)
const toolbarConfig = {
}
const editorConfig = {
placeholder: '请输入内容...',
MENU_CONF: {}, // Initialize MENU_CONF as an empty object
}
editorConfig.MENU_CONF['uploadImage'] = {
// 上传图片的配置
// server: '', // 使用环境变量中的API基础URL
// fieldName: 'your-custom-name',
// // 上传图片的最大体积限制,默认为 10M
// maxFileSize: 5 * 1024 * 1024, // 5M
// // 上传图片的类型限制
// allowedFileTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
// // 自定义上传参数
// meta: {
// token: localStorage.getItem('token') || ''
// },
// // 自定义添加 http 请求头
// headers: {
// Authorization: 'Bearer ' + (localStorage.getItem('token') || '')
// },
// // 上传之前触发
// onBeforeUpload(file) {
// console.log('准备上传图片', file)
// return file // 返回file继续上传
// },
// // 上传成功触发
// onSuccess(file, res) {
// console.log('图片上传成功', file, res)
// },
// // 上传失败触发
// onError(file, err, res) {
// console.log('图片上传失败', file, err, res)
// // 可以自定义错误提示
// ElNotification.error('图片上传失败,请重试')
// },
// 自定义插入图片
// customInsert(res, insertFn) {
// console.log('自定义插入图片', res, insertFn)
// // res 即服务端的返回结果
// if (res.code === 200 && res.data) {
// // 从响应结果中获取图片URL
// const url = res.data.url || res.data
// // 调用插入图片的函数
// insertFn(url)
// } else {
// ElNotification.error('图片插入失败')
// }
// }
// 自定义上传
async customUpload(file, insertFn) {
const url = await uploadFile(file)
insertFn(url, file.name, url);
},
}
editorConfig.MENU_CONF['uploadVideo'] = {
async customUpload(file, insertFn) {
const url = await uploadFile(file)
insertFn(url, file.name, url);
},
}
const uploadFile = (file) => {
return new Promise(async (resolve, reject) => {
const code = await getCode()
const res = JSON.parse(code).token
const formData = new FormData()
formData.append('policy', res.policy);
formData.append('OSSAccessKeyId', res.ossAccessKeyId);
formData.append('success_action_status', '200');
formData.append('signature', res.signature);
formData.append('key', res.dir + file.name);
formData.append('file', file);
fetch('http://image-fudan.oss-cn-beijing.aliyuncs.com', { method: 'POST', body: formData },).then((res) => {
resolve('http://image-fudan.oss-cn-beijing.aliyuncs.com/ddbs-admin/' + file.name)
}).catch((err) => {
reject('上传失败', err)
});
})
}
const handleUpload = (e) => {
console.log(e.target.files[0])
}
const handleSaveArticle = () => {
if (route.query.id) {
editNews({
title: newTitle.value,
content: valueHtml.value
}, route.query.id).then(res => {
ElNotification.success('编辑成功')
router.back()
})
} else {
addNews({
title: newTitle.value,
content: valueHtml.value
}).then(res => {
ElNotification.success('添加成功')
router.back()
})
}
}
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
const handleCreated = (editor) => {
editorRef.value = editor // 记录 editor 实例,重要!
if (route.query.id) {
editor.setHtml(sessionStorage.getItem('works'))
// valueHtml.value = sessionStorage.getItem('works')
}
// 监听编辑器内容变化,可以在这里添加额外逻辑
editor.on('change', (e) => {
// 获取纯文本
valueText.value = editor.getText()
// 计算字数
characterCount.value = valueText.value.length
})
// const toolbar = DomEditor.getToolbar(editor)
// const curToolbarConfig = toolbar.getConfig()
// console.log(curToolbarConfig.toolbarKeys) // 当前菜单排序和分组
}
const newTitle = ref('')
const dialogPreview = ref(false)
const handlePreviewArticle = () => {
dialogPreview.value = true
}
onMounted(() => {
if (route.query.id) {
newTitle.value = sessionStorage.getItem('title')
}
})
// Auto-resize directive
const vAutoResize = {
mounted: (el) => {
const resize = () => {
el.style.height = 'auto'
el.style.height = `${el.scrollHeight}px`
}
// Initial resize
resize()
// Add event listeners
el.addEventListener('input', resize)
window.addEventListener('resize', resize)
// Cleanup
onBeforeUnmount(() => {
el.removeEventListener('input', resize)
window.removeEventListener('resize', resize)
})
}
}
// Register directive
const app = getCurrentInstance()
app.appContext.app.directive('auto-resize', vAutoResize)
</script>
<style lang="scss" scoped>
.container {
width: 100%;
height: 100%;
.toolbar-box {
height: 80px;
}
:deep(.el-radio__label) {
white-space: normal;
}
.editor-box-content {
padding: 0 16px;
height: calc(100% - 130px);
}
.title-input {
width: 100%;
height: 50px;
border: none;
// margin: 20px 0;
font-size: 20px;
}
.ai-box {
width: 100%;
height: 100%;
display: flex;
.editor-box {
height: 100%;
flex: 1;
position: relative;
.editor {}
.editor-btn-box {
width: 100%;
text-align: center;
margin-top: 15px;
position: relative;
}
.character-count {
position: absolute;
right: 0;
bottom: 15px;
text-align: right;
color: #909399;
font-size: 12px;
margin-top: 5px;
padding-right: 10px;
}
.title-box {
display: flex;
border-bottom: 1px solid var(--el-border-color);
margin-bottom: 10px;
.title-input {
flex: 1;
}
.title-btn {
flex: none;
width: 100px;
}
}
}
.active-box {
width: 350px;
flex: none;
overflow: hidden;
.el-scrollbar {
overflow-x: hidden;
}
}
}
.tab-item {
margin-top: 10px;
margin-bottom: 15px;
.tab-item-label {
margin-top: 15px;
font-size: 14px;
margin-bottom: 15px;
}
}
.tab-item-btn {
// margin-top: 15px;
}
.tab-item-radio-box {
padding: 20px;
border-radius: 4px;
background-color: var(--el-fill-color-blank);
margin-bottom: 15px;
display: flex;
height: 160px;
overflow-y: auto;
:deep(.el-radio-group) {
display: block;
overflow: auto;
}
:deep(.el-radio) {
display: flex;
height: auto;
line-height: 18px;
margin: 5px 0;
}
}
.tab-item-label {
display: block;
margin-top: 15px;
font-size: 14px;
font-weight: 600;
margin-bottom: 15px;
}
.model-label {
display: flex;
justify-content: space-between;
margin-top: 15px;
font-size: 14px;
font-weight: 600;
margin-bottom: 15px;
}
.model-box {
margin-top: 15px;
border-radius: 4px;
background-color: var(--el-fill-color-blank);
padding: 15px 20px;
div {
height: 30px;
line-height: 30px;
// border: 1px solid var(--el-border-color);
cursor: pointer;
// border-radius: 4px;
font-size: 14px;
display: flex;
align-items: center;
// margin-bottom: 10px;
span {
color: rgb(239, 53, 53);
font-size: 14px;
margin-left: 15px;
font-style: italic;
vertical-align: middle;
.el-icon {
font-size: 28px;
position: relative;
top: 6px;
}
}
&:hover {
color: var(--el-color-warning);
}
}
.active {
color: var(--el-color-warning);
}
}
.model-item {
padding: 15px;
border-radius: 4px;
background-color: var(--el-fill-color-blank);
cursor: pointer;
label {
font-size: 15px;
font-weight: 600;
line-height: 24px;
}
p {
font-size: 13px;
line-height: 24px;
}
&.active {
background-color: var(--el-color-primary);
label {
color: #fff;
}
p {
color: #fff;
}
}
}
.tab-item-select-box {
.el-select {
margin-bottom: 15px;
}
}
.tab-item-text {
margin: 15px 0;
}
.dialog-content {
margin: 20px;
}
.dialog-title {
margin: 20px 0;
}
.dialog-content-title {
// height: 200px;
min-height: 200px;
border-radius: 6px;
background-color: var(--el-fill-color-blank);
margin-bottom: 20px;
padding: 20px 15px;
p {
line-height: 20px;
padding-right: 50px;
position: relative;
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.dialog-content-title-span {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
}
}
.drawer-title {
display: flex;
justify-content: center;
.serial-number {
height: 30px;
width: 30px;
border-radius: 30px;
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary-light-5);
display: flex;
justify-content: center;
align-items: center;
margin-right: 15px;
}
.drawer-title-text {
line-height: 30px;
font-size: 14px;
margin-right: 15px;
color: var(--el-color-primary-light-5);
}
.steps-item {
&::after {
border-radius: 50%;
box-shadow: -10px -8px 0 -2px var(--el-color-primary), 0 -8px 0 -1px var(--el-color-primary-light-5), 10px -8px 0 -2px var(--el-color-primary-light-5);
content: " ";
display: inline-block;
height: 8px;
margin: 0 10px;
position: relative;
transform: translateY(calc(100% - 1px)) translateX(8px);
width: 8px;
}
}
.active {
color: var(--el-color-primary);
}
}
:deep(.el-drawer__header) {
margin-bottom: 0;
}
:deep(.el-drawer__body) {
overflow: hidden;
padding: 0;
}
.drawer-content,
.drawer-content2 {
padding: 20px;
padding-top: 0;
margin-top: 20px;
height: calc(100vh - 142px);
overflow-y: auto;
.drawer-content-item {
margin-bottom: 20px;
transition: all 0.3s;
.drawer-content-item-button {
text-align: right;
margin-top: 5px;
}
.expand-enter-active,
.expand-leave-active {
transition: all 0.3s ease-in-out;
max-height: 50px;
/* 根据实际内容调整最大高度 */
overflow: hidden;
}
.expand-enter-from,
.expand-leave-to {
max-height: 0;
opacity: 0;
}
.drawer-content-item-box {
border-radius: 6px;
background-color: #f9f9fb;
padding: 18px;
padding-top: 10px;
padding-bottom: 15px;
position: relative;
&.streaming {
background-color: rgba(var(--el-color-primary-rgb), 0.05);
border: 1px solid rgba(var(--el-color-primary-rgb), 0.1);
}
.streaming-cursor {
position: absolute;
right: 18px;
bottom: 15px;
width: 8px;
height: 16px;
background-color: var(--el-color-primary);
animation: blink 1s infinite;
}
}
.EaFdC {
// list-style: cjk-ideographic;
margin-bottom: 4px;
}
.drawer-content-title {
font-size: 16px;
line-height: 24px;
font-weight: 500;
line-height: 24px;
// list-style: cjk-ideographic;
margin: 0;
// margin-left: 35px;
height: auto;
min-height: 24px;
&::marker {
unicode-bidi: isolate;
font-variant-numeric: tabular-nums;
text-transform: none;
text-indent: 0px !important;
text-align: start !important;
text-align-last: start !important;
transform: translateY(-3px);
}
textarea {
width: 100%;
background-color: rgba($color: #000000, $alpha: 0);
border: 0;
font-style: normal;
resize: none;
min-height: 24px;
overflow: hidden;
position: relative;
top: 7px;
padding: 0;
}
}
.drawer-content-text {
font-size: 14px;
line-height: 20px;
width: 100%;
background-color: rgba($color: #000000, $alpha: 0);
border: 0;
font-style: normal;
resize: none;
height: auto;
min-height: 24px;
overflow: hidden;
padding: 0;
word-break: break-all;
}
}
}
.generating-indicator {
position: absolute;
top: 70px;
left: 30px;
color: var(--el-color-primary);
font-size: 28px;
font-weight: bold;
}
.loading-dots {
animation: loadingDots 1.5s infinite;
}
@keyframes loadingDots {
0% {
opacity: 0.2;
}
20% {
opacity: 1;
}
100% {
opacity: 0.2;
}
}
.article-preview {
padding: 10px;
height: 100%;
.article-preview-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
color: var(--el-color-primary);
}
.article-preview-content {
font-size: 14px;
line-height: 1.6;
// max-height: calc(100vh - 200px);
overflow-y: auto;
padding: 15px;
background-color: #f9f9fb;
border-radius: 6px;
h1,
h2,
h3 {
margin-top: 16px;
margin-bottom: 8px;
font-weight: 600;
}
h1 {
font-size: 20px;
}
h2 {
font-size: 18px;
}
h3 {
font-size: 16px;
}
p {
margin-bottom: 10px;
}
}
}
.extract-style {
font-size: 14px;
background-color: var(--el-fill-color-blank);
border-radius: 6px;
padding: 15px;
// min-height: 100px;
// max-height: 200px;
overflow-y: auto;
height: 200px;
display: flex;
pre {
line-height: 18px;
white-space: pre-wrap;
}
}
.no-click {
pointer-events: none;
}
.article-preview-title {
font-size: 18px;
margin-bottom: 15px;
}
.dialog-preview {
max-height: calc(100vh - 200px);
overflow: auto;
.preview-title {
font-size: 24px;
color: var(--el-text-color-primary);
margin-bottom: 40px;
}
.preview-content {
color: var(--el-text-color-primary);
font-size: 16px;
line-height: 1;
// p{
// line-height: 26px;
// }
}
}
.drawer-footer {
display: flex;
justify-content: space-between;
}
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
</style>