Commit ad43b680 authored by TerryLan's avatar TerryLan

chore: 编辑器内容优化弹窗完善

parent d28e0366
......@@ -8,7 +8,7 @@
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link rel="stylesheet" href="//at.alicdn.com/t/c/font_4711453_usul8q5bwqp.css" />
<link rel="stylesheet" href="//at.alicdn.com/t/c/font_4711453_lz4oxe9et7.css" />
<link
rel="preload"
href="https://gsst-poe-sit.gz.bcebos.com/front/SourceHanSansCN-Medium.otf"
......
<script setup lang="ts">
import { ref } from 'vue'
import { ref, watchEffect } from 'vue'
import fetchEventStreamSource from './utils/fetch-event-stream-source'
import type { Editor } from 'tinymce'
interface Props {
containerTop: number
editor: Editor | null
isSetBottom: boolean
location: number
}
defineProps<Props>()
const questionContent = ref()
const props = defineProps<Props>()
const isShowModal = defineModel<boolean>('isShowModal', { required: true })
const containerWrapperStyle = ref<{ top?: string; bottom?: string }>({})
const questionContent = ref('')
const isShowResponseContentEditModal = ref(false)
const isShowReplaceToolbar = ref(false)
const editResponseContent = ref('')
const editResponseLoading = ref(false)
watchEffect(() => {
if (props.isSetBottom) {
containerWrapperStyle.value = { bottom: `${props.location}px`, top: 'unset' }
} else {
containerWrapperStyle.value = { top: `${props.location}px` }
}
})
function handleQuestionSubmitEnter(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
}
}
function editResponseStatusReset() {
isShowModal.value = false
editResponseContent.value = ''
isShowResponseContentEditModal.value = true
editResponseLoading.value = true
isShowReplaceToolbar.value = false
}
function handleRetouching() {
const selectContent = props.editor?.selection.getContent()
editResponseStatusReset()
fetchEventStreamSource(
'/aiGcRest/articlePolish.json',
{
articleContent: selectContent || '',
editorRequired: questionContent.value || '',
},
{
onResponse: (res) => {
editResponseContent.value += res.message
},
onend: () => {
editResponseLoading.value = false
isShowReplaceToolbar.value = true
},
},
)
}
</script>
<template>
<div class="absolute w-full px-[16px]" :style="{ top: `${containerTop || 0}px` }">
<div class="rounded-[6px] bg-white py-[6px]">
<n-input
v-model:value="questionContent"
size="large"
type="textarea"
placeholder="请输入优化文本的指令"
:autosize="{ minRows: 1, maxRows: 3 }"
/>
<Transition mode="out-in">
<div v-if="isShowModal" class="absolute top-0 w-full px-[16px]" :style="containerWrapperStyle">
<div class="relative rounded-[6px] bg-white py-[10px]">
<n-input
v-model:value="questionContent"
size="large"
type="textarea"
placeholder="请输入优化文本的指令"
:autosize="{ minRows: 1, maxRows: 3 }"
@keydown="handleQuestionSubmitEnter"
>
<template #prefix>
<i class="iconfont icon-association text-theme-color"></i>
</template>
<template #suffix>
<i class="iconfont icon-fasong text-[#d0ceff]"></i>
</template>
</n-input>
<ul
class="absolute w-[142px] select-none overflow-hidden rounded-[5px] border border-[#9EA3FF] bg-white"
:class="isSetBottom ? 'top-[6px] -translate-y-full' : 'bottom-[6px] translate-y-full'"
>
<li
class="flex h-[34px] cursor-pointer items-center px-[10px] transition-[background] duration-300 ease-linear hover:bg-[#eeeffe]"
@click="handleRetouching"
>
<i class="iconfont icon-retouching text-[16px] text-[#9ea3ff]"></i>
<span class="pl-[10px] text-[14px]">润色</span>
</li>
<li
class="flex h-[34px] cursor-pointer items-center px-[10px] transition-[background] duration-300 ease-linear hover:bg-[#eeeffe]"
>
<i class="iconfont icon-expansion text-[16px] text-[#9ea3ff]"></i>
<span class="pl-[10px] text-[14px]">扩写</span>
</li>
<li
class="flex h-[34px] cursor-pointer items-center px-[10px] transition-[background] duration-300 ease-linear hover:bg-[#eeeffe]"
>
<i class="iconfont icon-suoxie text-[16px] text-[#9ea3ff]"></i>
<span class="pl-[10px] text-[14px]">缩写</span>
</li>
<li
class="flex h-[34px] cursor-pointer items-center px-[10px] transition-[background] duration-300 ease-linear hover:bg-[#eeeffe]"
>
<i class="iconfont icon-adjust-tone text-[16px] text-[#9ea3ff]"></i>
<span class="pl-[10px] text-[14px]">调整语气</span>
</li>
</ul>
</div>
</div>
<div v-else-if="isShowResponseContentEditModal" class="absolute top-1/2 w-full -translate-y-1/2 px-[16px]">
<div class="rounded-[5px] border border-[#9EA3FF] bg-white px-[10px] py-[14px]">
<div class="text-[14px] leading-[18px]">
{{ editResponseContent }}
</div>
<Transition>
<div v-show="editResponseLoading" class="flex select-none items-center justify-between">
<span v-show="!editResponseContent">生成中...</span>
<i class="iconfont icon-zanting cursor-pointer text-[18px]"></i>
</div>
</Transition>
<Transition>
<div v-show="isShowReplaceToolbar" class="mt-[9px]">
<n-button type="primary" size="small"
>替换
<template #icon>
<i class="iconfont icon-tihuan"></i>
</template>
</n-button>
<n-button class="!ml-[17px]" size="small"
>插入
<template #icon>
<i class="iconfont icon-charu"></i>
</template>
</n-button>
</div>
</Transition>
</div>
</div>
</div>
</Transition>
</template>
<style lang="scss" scoped>
:deep(.n-input .n-input__state-border) {
border-color: #9ea3ff !important;
}
.v-enter-active,
.v-leave-active {
transition-timing-function: ease-in-out;
transition-duration: 0.2s;
transition-property: opacity;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>
<script setup lang="ts">
import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue'
import { computed, nextTick, onMounted, ref, shallowRef, useTemplateRef, watch } from 'vue'
import { createEditorConfig } from './config/editor-config'
import EditorToolbar from './editor-toolbar.vue'
import { markdownTransformHtml } from '@/utils/markdown-parse'
......@@ -7,6 +7,8 @@ import { downloadFile } from '@/utils/download-file'
import { useI18n } from 'vue-i18n'
import { fetchExportFile } from '@/apis/file'
import ContentOptimizationEdit from './content-optimization-edit.vue'
import { useElementSize } from '@vueuse/core'
import type { Editor } from 'tinymce'
interface Props {
content: string
......@@ -31,25 +33,24 @@ defineExpose({
const editorToolbarRef = useTemplateRef<InstanceType<typeof EditorToolbar>>('editorToolbarRef')
const { t } = useI18n()
const editorWrapperRef = useTemplateRef('editorWrapperRef')
const { height: editorWrapperRefHeight } = useElementSize(editorWrapperRef)
let editor: any = {}
let controller: AbortController | null = null
const editorConfig = createEditorConfig()
const editor = shallowRef<Editor | null>(null)
const isShowEditor = ref(false)
const editorWrapperRef = ref<HTMLDivElement | null>(null)
const modifyInputContainerStyle = ref({
top: '0px',
})
const articleModifyInputContent = ref('')
const articleContentModifyContainerShow = ref(false)
const articleContentModifyState = ref<'selection' | 'loading' | 'generating' | 'done'>('selection')
const articleContentModifyResponseText = ref('')
const articleContentModifyResponseTextResource = ref('')
const articleContentModifyContainerClientY = ref(0)
const contentOptimizationEditContainerTop = ref(300)
// const contentOptimizationEditContainerTop = ref(300)
const contentOptimizationEditContainerIsSetBottom = ref(false)
const contentOptimizationEditContainerLocation = ref(0)
const editorContent = computed({
get() {
......@@ -63,17 +64,23 @@ const editorContent = computed({
// const editorTitleText = ref('')
watch(articleContentModifyResponseTextResource, (newVal) => {
if (newVal)
if (newVal && editor.value)
articleContentModifyResponseText.value = markdownTransformHtml(
editor.dom ? editor.dom.decode(newVal) : newVal,
editor.value.dom ? editor.value.dom.decode(newVal) : newVal,
) as string
})
watch(
() => props.content,
(val: string, prevVal: string) => {
if (editor && editor.getContent && typeof val === 'string' && val !== prevVal && val !== editor.getContent())
editor.setContent(val)
if (
editor.value &&
editor.value.getContent &&
typeof val === 'string' &&
val !== prevVal &&
val !== editor.value.getContent()
)
editor.value.setContent(val)
},
)
......@@ -82,7 +89,7 @@ onMounted(() => {
...editorConfig,
init_instance_callback: (editorInstance) => {
isShowEditor.value = true
editor = editorInstance
editor.value = editorInstance
nextTick(() => {
props.content && editorInstance.setContent(props.content)
......@@ -113,8 +120,6 @@ onMounted(() => {
editorInstance.on('mousedown', (e: MouseEvent) => {
if (e.button !== 0) return
articleContentModifyContainerClientY.value = e.clientY
if (articleContentModifyContainerShow.value) {
if (controller) {
controller.abort()
......@@ -125,7 +130,6 @@ onMounted(() => {
articleContentModifyResponseText.value = ''
articleContentModifyResponseTextResource.value = ''
articleContentModifyState.value = 'selection'
articleModifyInputContent.value = ''
}
editorInstance.selection.collapse()
......@@ -146,25 +150,24 @@ onMounted(() => {
if (lastRect) {
/* 其中 46 是当前编辑器文档HTMl距离 外部挂载容器之间产生的高度 */
contentOptimizationEditContainerTop.value = lastRect.bottom + 46 + 10
// contentOptimizationEditContainerTop.value = locationEl.offsetTop + locationEl.offsetHeight
const locationHeight = lastRect.bottom + 46 + 10
if (editorWrapperRefHeight.value - 260 < locationHeight) {
contentOptimizationEditContainerIsSetBottom.value = true
contentOptimizationEditContainerLocation.value =
editorWrapperRefHeight.value - (rects[0].bottom + 46 + 10) + rects[0].height + 16
} else {
contentOptimizationEditContainerIsSetBottom.value = false
contentOptimizationEditContainerLocation.value = locationHeight
}
}
const top =
(e.clientY > articleContentModifyContainerClientY.value
? e.clientY
: articleContentModifyContainerClientY.value) + 130
const bodyHeight = document.body.offsetHeight
modifyInputContainerStyle.value.top = `${top > bodyHeight - 400 ? bodyHeight - 400 : top}px`
articleContentModifyContainerShow.value = true
}
})
editorInstance.on('keydown', () => {
if (articleContentModifyContainerShow.value) articleContentModifyContainerShow.value = false
// if (articleContentModifyContainerShow.value) articleContentModifyContainerShow.value = false
})
},
})
......@@ -185,7 +188,7 @@ onMounted(() => {
function getContent() {
if (!isShowEditor.value) return ''
const content = editor.getContent()
const content = editor.value?.getContent() || ''
const parser = window.tinymce.html.DomParser({ validate: false })
parser.addNodeFilter('mf-confirmation-box', (nodes) => {
......@@ -229,7 +232,12 @@ function onDownloadFile() {
</div>
</div>
<ContentOptimizationEdit :container-top="contentOptimizationEditContainerTop" />
<ContentOptimizationEdit
v-model:is-show-modal="articleContentModifyContainerShow"
:editor="editor"
:is-set-bottom="contentOptimizationEditContainerIsSetBottom"
:location="contentOptimizationEditContainerLocation"
/>
</div>
<div v-show="!isShowEditor" class="flex h-full w-full items-center justify-center">
<n-spin size="large" />
......
import { BASE_URLS } from '@/config/base-url'
import { useSystemLanguageStore } from '@/store/modules/system-language'
import { useUserStore } from '@/store/modules/user'
import { languageKeyTransform } from '@/utils/language-key-transform'
import { fetchEventSource } from '@microsoft/fetch-event-source'
interface ResponseData {
message: string
reasoningContent: string
function: { name: string }
}
interface Options {
onResponse?: (data: ResponseData) => void
onend?: () => void
onclose?: () => void
onerror?: (err: Error) => void
}
export default function fetchEventStreamSource(
url: string,
payload: object = {},
options: Options = {
onResponse: (_data: ResponseData) => {},
onend: () => {},
onclose: () => {},
onerror: (_err: Error) => {},
},
) {
const ENV = import.meta.env.VITE_APP_ENV
const userStore = useUserStore()
const systemLanguageStore = useSystemLanguageStore()
const controller = new AbortController()
fetchEventSource(`${BASE_URLS[ENV || 'DEV']}/api/rest${url}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-request-token': userStore.token,
'x-lang': languageKeyTransform(systemLanguageStore.currentLanguageInfo.key),
},
body: JSON.stringify(payload),
signal: controller?.signal,
onmessage: (e: { data: string }) => {
if (e.data === '[DONE]') {
options.onend && options.onend()
return
}
try {
const data = JSON.parse(e.data)
if (data.code === 0 || data.code === '0') {
data && options.onResponse && options.onResponse(data)
} else {
options.onerror && options.onerror(new Error(data.message))
controller.abort()
options.onclose && options.onclose()
}
} catch (error) {
options.onerror && options.onerror(error as Error)
}
},
onclose: () => {
options.onclose && options.onclose()
},
onerror: (err) => {
options.onerror && options.onerror(err)
throw err
},
})
return controller
}
......@@ -4,7 +4,7 @@ import CustomEditor from '@/components/custom-editor/custom-editor.vue'
const contentEdit = defineModel<string>('contentEdit', { required: true })
const isShowEditorDrawerDraft = true
const contentEditDraft = `<h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h4>济南的冬天,是一幅独特的画卷。它不同于北方的严寒,也不同于南方的温润。这里的冬天,有着独特的魅力和风情。济南的冬日,天空湛蓝,阳光明媚,尽管寒风凛冽,但总能带给人一份宁静和温馨。济南的泉水在冬天依然潺潺流淌,为这座城市增添了一份生机和活力。济南的冬日,不仅是季节的更迭,更是一种生活的体验,一种对大自然的敬畏和感慨。</h4>`
const contentEditDraft = `<h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h4>济南的冬天,是一幅独特的画卷。它不同于北方的严寒,也不同于南方的温润。这里的冬天,有着独特的魅力和风情。济南的冬日,天空湛蓝,阳光明媚,尽管寒风凛冽,但总能带给人一份宁静和温馨。济南的泉水在冬天依然潺潺流淌,为这座城市增添了一份生机和活力。济南的冬日,不仅是季节的更迭,更是一种生活的体验,一种对大自然的敬畏和感慨。</h4><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h1>标题</h1><h4>济南的冬天,是一幅独特的画卷。它不同于北方的严寒,也不同于南方的温润。这里的冬天,有着独特的魅力和风情。济南的冬日,天空湛蓝,阳光明媚,尽管寒风凛冽,但总能带给人一份宁静和温馨。济南的泉水在冬天依然潺潺流淌,为这座城市增添了一份生机和活力。济南的冬日,不仅是季节的更迭,更是一种生活的体验,一种对大自然的敬畏和感慨。</h4><h1>标题</h1><h1>标题</h1>`
const isShowEditorDrawer = defineModel<boolean>('isShowEditorDrawer', { required: true })
......
export const BASE_URLS: Record<'DEV' | 'PROD', string> = {
DEV: 'https://poc-sit.gsstcloud.com',
// DEV: 'https://poc-sit.gsstcloud.com',
DEV: 'http://localhost:8848',
PROD: 'https://model-link.gsstcloud.com',
}
......
......@@ -27,6 +27,15 @@ export default defineConfig(({ command, mode }) => {
server: {
host: true,
port: envConf.VITE_PORT,
proxy: {
'/api/rest': {
target: 'http://192.168.13.93:5000',
changeOrigin: true,
rewrite: (path) => {
return path
},
},
},
},
css: {
preprocessorOptions: {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment