769 lines
19 KiB
Vue
769 lines
19 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="title-box">
|
|
<input type="text" v-model="newTitle" class="title-input" placeholder="请输入文章标题" />
|
|
</div>
|
|
<Editor class="editor" style="height: calc(100% - 190px); overflow-y: hidden;" v-model="valueHtml"
|
|
:defaultConfig="editorConfig" :mode="mode" @onCreated="handleCreated" />
|
|
|
|
<!-- 添加字数统计显示 -->
|
|
|
|
|
|
<div class="editor-btn-box">
|
|
<div class="character-count">
|
|
字数: {{ characterCount }}
|
|
</div>
|
|
<el-button type="primary" @click="handleSaveArticle(1)">保存</el-button>
|
|
<el-button type="primary" @click="handleSaveArticle(2)">存草稿</el-button>
|
|
<el-button type="primary" @click="handlePreviewArticle">预览</el-button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<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 { title, inspiration, configList, writeOutline, models, generateArticle, analysisStyle, saveStyle, saveArticle, editArticle, customerTool } from '@/api/AICreation'
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
|
|
|
|
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: import.meta.env.VITE_API_BASE_URL + '/api/upload', // 使用环境变量中的API基础URL
|
|
// 上传图片的最大体积限制,默认为 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) {
|
|
// res 即服务端的返回结果
|
|
if (res.code === 200 && res.data) {
|
|
// 从响应结果中获取图片URL
|
|
const url = res.data.url || res.data
|
|
// 调用插入图片的函数
|
|
insertFn(url)
|
|
} else {
|
|
ElNotification.error('图片插入失败')
|
|
}
|
|
}
|
|
}
|
|
|
|
editorConfig.MENU_CONF['uploadVideo'] = {
|
|
server: import.meta.env.VITE_API_BASE_URL + '/api/upload', // 使用环境变量中的API基础URL
|
|
maxFileSize: 5 * 1024 * 1024, // 5M
|
|
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('视频上传失败,请重试')
|
|
},
|
|
}
|
|
|
|
|
|
// 组件销毁时,也及时销毁编辑器
|
|
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 newTitle = ref('')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
})
|
|
|
|
// 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;
|
|
}
|
|
|
|
.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;
|
|
padding-right: 60px;
|
|
position: relative;
|
|
|
|
&::after {
|
|
content: '';
|
|
width: 1px;
|
|
height: 100%;
|
|
background-color: var(--el-border-color);
|
|
position: absolute;
|
|
right: 30px;
|
|
top: 0;
|
|
}
|
|
|
|
.editor {
|
|
height: calc(100% - 300px);
|
|
}
|
|
|
|
.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> |