@zuopngfei 62d2ead615 dsdq
2025-05-19 16:44:51 +08:00

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>