Skip to content

Commit 96a499a

Browse files
YunaiVgitee-org
authored andcommitted
!473 [新增]AI写作
Merge pull request !473 from hhhero/dev
2 parents 44f322b + f72e4f4 commit 96a499a

File tree

6 files changed

+449
-0
lines changed

6 files changed

+449
-0
lines changed

src/api/ai/writer/index.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { fetchEventSource } from '@microsoft/fetch-event-source'
2+
3+
import { getAccessToken } from '@/utils/auth'
4+
import { config } from '@/config/axios/config'
5+
6+
export interface WriteParams {
7+
/**
8+
* 1:撰写 2:回复
9+
*/
10+
type: 1 | 2
11+
/**
12+
* 写作内容提示 1。撰写 2回复
13+
*/
14+
prompt: string
15+
/**
16+
* 原文
17+
*/
18+
originalContent: string
19+
/**
20+
* 长度
21+
*/
22+
length: number
23+
/**
24+
* 格式
25+
*/
26+
format: number
27+
/**
28+
* 语气
29+
*/
30+
tone: number
31+
/**
32+
* 语言
33+
*/
34+
language: number
35+
}
36+
export const writeStream = ({
37+
data,
38+
onClose,
39+
onMessage,
40+
onError,
41+
ctrl
42+
}: {
43+
data: WriteParams
44+
onMessage?: (res: any) => void
45+
onError?: (...args: any[]) => void
46+
onClose?: (...args: any[]) => void
47+
ctrl: AbortController
48+
}) => {
49+
// return request.post({ url: '/ai/write/generate-stream', data })
50+
const token = getAccessToken()
51+
return fetchEventSource(`${config.base_url}/ai/write/generate-stream`, {
52+
method: 'post',
53+
headers: {
54+
'Content-Type': 'application/json',
55+
Authorization: `Bearer ${token}`
56+
},
57+
openWhenHidden: true,
58+
body: JSON.stringify(data),
59+
onmessage: onMessage,
60+
onerror: onError,
61+
onclose: onClose,
62+
signal: ctrl.signal
63+
})
64+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
<template>
2+
<!-- 定义tab组件 -->
3+
<DefineTab v-slot="{ active, text, itemClick }">
4+
<span
5+
class="inline-block w-1/2 rounded-full cursor-pointer text-center leading-[30px] relative z-1 text-[5C6370] hover:text-black"
6+
:class="active ? 'text-black shadow-md' : 'hover:bg-[#DDDFE3]'"
7+
@click="itemClick"
8+
>
9+
{{ text }}
10+
</span>
11+
</DefineTab>
12+
<!-- 定义label组件 -->
13+
<DefineLabel v-slot="{ label, hint, hintClick }">
14+
<h3 class="mt-5 mb-3 flex items-center justify-between text-[14px]">
15+
<span>{{ label }}</span>
16+
<span
17+
@click="hintClick"
18+
v-if="hint"
19+
class="flex items-center text-[12px] text-[#846af7] cursor-pointer select-none"
20+
>
21+
<Icon icon="ep:question-filled" />
22+
{{ hint }}
23+
</span>
24+
</h3>
25+
</DefineLabel>
26+
<!-- TODO 小屏幕的时候是定位在左边的,大屏是分开的 -->
27+
<div class="relative" v-bind="$attrs">
28+
<!-- tab -->
29+
<div
30+
class="absolute left-1/2 top-2 -translate-x-1/2 w-[303px] rounded-full bg-[#DDDFE3] p-1 z-10"
31+
>
32+
<div
33+
class="flex items-center relative after:content-[''] after:block after:bg-white after:h-[30px] after:w-1/2 after:absolute after:top-0 after:left-0 after:transition-transform after:rounded-full"
34+
:class="selectedTab === 2 && 'after:transform after:translate-x-[100%]'"
35+
>
36+
<ReuseTab
37+
v-for="tab in tabs"
38+
:key="tab.value"
39+
:text="tab.text"
40+
:active="tab.value === selectedTab"
41+
:itemClick="() => switchTab(tab.value)"
42+
/>
43+
</div>
44+
</div>
45+
<div
46+
class="px-7 pb-2 pt-[46px] overflow-y-auto lg:block w-[380px] box-border bg-[#ECEDEF] h-full"
47+
>
48+
<div>
49+
<template v-if="selectedTab === 1">
50+
<ReuseLabel label="写作内容" hint="示例" :hint-click="() => example('write')" />
51+
<el-input
52+
type="textarea"
53+
:rows="5"
54+
:maxlength="500"
55+
v-model="writeForm.prompt"
56+
placeholder="请输入写作内容"
57+
showWordLimit
58+
/>
59+
</template>
60+
61+
<template v-else>
62+
<ReuseLabel label="原文" hint="示例" :hint-click="() => example('reply')" />
63+
<el-input
64+
type="textarea"
65+
:rows="5"
66+
:maxlength="500"
67+
v-model="writeForm.originalContent"
68+
placeholder="请输入原文"
69+
showWordLimit
70+
/>
71+
72+
<ReuseLabel label="回复内容" />
73+
<el-input
74+
type="textarea"
75+
:rows="5"
76+
:maxlength="500"
77+
v-model="writeForm.prompt"
78+
placeholder="请输入回复内容"
79+
showWordLimit
80+
/>
81+
</template>
82+
83+
<ReuseLabel label="长度" />
84+
<Tag v-model="writeForm.length" :tags="writeTags.lenTags" />
85+
<ReuseLabel label="格式" />
86+
<Tag v-model="writeForm.format" :tags="writeTags.formatTags" />
87+
<ReuseLabel label="语气" />
88+
<Tag v-model="writeForm.tone" :tags="writeTags.toneTags" />
89+
<ReuseLabel label="语言" />
90+
<Tag v-model="writeForm.language" :tags="writeTags.langTags" />
91+
92+
<div class="flex items-center justify-center mt-3">
93+
<el-button :disabled="isWriting">重置</el-button>
94+
<el-button :loading="isWriting" @click="submit" color="#846af7">生成</el-button>
95+
</div>
96+
</div>
97+
</div>
98+
</div>
99+
</template>
100+
101+
<script setup lang="ts">
102+
import { createReusableTemplate } from '@vueuse/core'
103+
import { ref } from 'vue'
104+
import Tag from './Tag.vue'
105+
import { WriteParams } from '@/api/ai/writer'
106+
import { omit } from 'lodash-es'
107+
import { getIntDictOptions } from '@/utils/dict'
108+
import dataJson from '../data.json'
109+
110+
type TabType = WriteParams['type']
111+
112+
const message = useMessage()
113+
114+
defineProps<{
115+
isWriting: boolean
116+
}>()
117+
118+
const emits = defineEmits<{
119+
(e: 'submit', params: Partial<WriteParams>)
120+
(e: 'example', param: 'write' | 'reply')
121+
}>()
122+
123+
const example = (type: 'write' | 'reply') => {
124+
writeForm.value = {
125+
...initData,
126+
...omit(dataJson[type], ['data'])
127+
}
128+
emits('example', type)
129+
}
130+
131+
const selectedTab = ref<TabType>(1)
132+
const tabs: {
133+
text: string
134+
value: TabType
135+
}[] = [
136+
{ text: '撰写', value: 1 },
137+
{ text: '回复', value: 2 }
138+
]
139+
const [DefineTab, ReuseTab] = createReusableTemplate<{
140+
active?: boolean
141+
text: string
142+
itemClick: () => void
143+
}>()
144+
145+
const initData: WriteParams = {
146+
type: 1,
147+
prompt: '',
148+
originalContent: '',
149+
tone: 1,
150+
language: 1,
151+
length: 1,
152+
format: 1
153+
}
154+
const writeForm = ref<WriteParams>({ ...initData })
155+
156+
const writeTags = {
157+
// 长度
158+
lenTags: getIntDictOptions('ai_write_length'),
159+
// 格式
160+
161+
formatTags: getIntDictOptions('ai_write_format'),
162+
// 语气
163+
164+
toneTags: getIntDictOptions('ai_write_tone'),
165+
// 语言
166+
langTags: getIntDictOptions('ai_write_language')
167+
//
168+
}
169+
170+
const [DefineLabel, ReuseLabel] = createReusableTemplate<{
171+
label: string
172+
class?: string
173+
hint?: string
174+
hintClick?: () => void
175+
}>()
176+
177+
const switchTab = (value: TabType) => {
178+
selectedTab.value = value
179+
writeForm.value = { ...initData }
180+
}
181+
182+
const submit = () => {
183+
if (selectedTab.value === 2 && !writeForm.value.originalContent) {
184+
message.warning('请输入原文')
185+
return
186+
} else if (!writeForm.value.prompt) {
187+
message.warning(`请输入${selectedTab.value === 1 ? '写作' : '回复'}内容`)
188+
return
189+
}
190+
emits('submit', {
191+
...(selectedTab.value === 1 ? omit(writeForm.value, ['originalContent']) : writeForm.value),
192+
type: selectedTab.value
193+
})
194+
}
195+
</script>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<template>
2+
<div class="h-full box-border py-6 px-7">
3+
<div class="w-full h-full relative bg-white box-border p-3 sm:p-16 pr-0">
4+
<!-- 展示在右上角 -->
5+
<el-button
6+
color="#846af7"
7+
v-show="showCopy"
8+
@click="copyMsg"
9+
class="absolute top-2 right-2 copy-btn"
10+
:data-clipboard-target="inputId"
11+
>
12+
复制
13+
</el-button>
14+
<!-- 展示在下面中间的位置 -->
15+
<el-button
16+
v-show="isWriting"
17+
class="absolute bottom-2 left-1/2 -translate-x-1/2"
18+
@click="emits('stopStream')"
19+
>
20+
终止生成
21+
</el-button>
22+
<div ref="contentRef" class="w-full h-full pr-3 sm:pr-16 overflow-y-auto">
23+
<el-input
24+
id="inputId"
25+
type="textarea"
26+
v-model="compMsg"
27+
autosize
28+
:input-style="{ boxShadow: 'none' }"
29+
resize="none"
30+
placeholder="生成的内容……"
31+
/>
32+
</div>
33+
</div>
34+
</div>
35+
</template>
36+
37+
<script setup lang="ts">
38+
import { useClipboard } from '@vueuse/core'
39+
const message = useMessage()
40+
const props = defineProps({
41+
msg: {
42+
type: String,
43+
default: ''
44+
},
45+
isWriting: {
46+
type: Boolean,
47+
default: false
48+
}
49+
})
50+
51+
const emits = defineEmits(['update:msg', 'stopStream'])
52+
53+
const { copied, copy } = useClipboard()
54+
55+
const compMsg = computed({
56+
get() {
57+
return props.msg
58+
},
59+
set(val) {
60+
emits('update:msg', val)
61+
}
62+
})
63+
64+
const showCopy = computed(() => props.msg && !props.isWriting)
65+
66+
const inputId = computed(() => getCurrentInstance()?.uid)
67+
68+
const contentRef = ref<HTMLDivElement>()
69+
defineExpose({
70+
scrollToBottom() {
71+
contentRef.value?.scrollTo(0, contentRef.value?.scrollHeight)
72+
}
73+
})
74+
75+
// 点击复制的时候复制msg
76+
const copyMsg = () => {
77+
copy(props.msg)
78+
}
79+
80+
watch(copied, (val) => {
81+
console.log({ copied: val })
82+
if (val) {
83+
message.success('复制成功')
84+
}
85+
})
86+
</script>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<template>
2+
<div class="flex flex-wrap gap-[8px]">
3+
<span
4+
v-for="tag in props.tags"
5+
:key="tag.value"
6+
class="tag mb-2 border-[2px] border-solid border-[#DDDFE3] px-2 leading-6 text-[12px] bg-[#DDDFE3] rounded-[4px] cursor-pointer"
7+
:class="modelValue === tag.value && '!border-[#846af7] text-[#846af7]'"
8+
@click="emits('update:modelValue', tag.value)"
9+
>
10+
{{ tag.label }}
11+
</span>
12+
</div>
13+
</template>
14+
15+
<script setup lang="ts">
16+
const props = withDefaults(
17+
defineProps<{
18+
tags: { label: string; value: string }[]
19+
modelValue: string
20+
[k: string]: any
21+
}>(),
22+
{
23+
tags: () => []
24+
}
25+
)
26+
27+
const emits = defineEmits<{
28+
(e: 'update:modelValue', value: string): void
29+
}>()
30+
</script>
31+
32+
<style scoped></style>

src/views/ai/writer/data.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"write": {
3+
"prompt": "vue",
4+
"data": "Vue.js 是一种用于构建用户界面的渐进式 JavaScript 框架。它的核心库只关注视图层,易于上手,同时也便于与其他库或已有项目整合。\n\nVue.js 的特点包括:\n- 响应式的数据绑定:Vue.js 会自动将数据与 DOM 同步,使得状态管理变得更加简单。\n- 组件化:Vue.js 允许开发者通过小型、独立和通常可复用的组件构建大型应用。\n- 虚拟 DOM:Vue.js 使用虚拟 DOM 实现快速渲染,提高了性能。\n\n在 Vue.js 中,一个典型的应用结构可能包括:\n1. 根实例:每个 Vue 应用都需要一个根实例作为入口点。\n2. 组件系统:可以创建自定义的可复用组件。\n3. 指令:特殊的带有前缀 v- 的属性,为 DOM 元素提供特殊的行为。\n4. 插值:用于文本内容,将数据动态地插入到 HTML。\n5. 计算属性和侦听器:用于处理数据的复杂逻辑和响应数据变化。\n6. 条件渲染:根据条件决定元素的渲染。\n7. 列表渲染:用于显示列表数据。\n8. 事件处理:响应用户交互。\n9. 表单输入绑定:处理表单输入和验证。\n10. 组件生命周期钩子:在组件的不同阶段执行特定的函数。\n\nVue.js 还提供了官方的路由器 Vue Router 和状态管理库 Vuex,以支持构建复杂的单页应用(SPA)。\n\n在开发过程中,开发者通常会使用 Vue CLI,这是一个强大的命令行工具,用于快速生成 Vue 项目脚手架,集成了诸如 Babel、Webpack 等现代前端工具,以及热重载、代码检测等开发体验优化功能。\n\nVue.js 的生态系统还包括大量的第三方库和插件,如 Vuetify(UI 组件库)、Vue Test Utils(测试工具)等,这些都极大地丰富了 Vue.js 的开发生态。\n\n总的来说,Vue.js 是一个灵活、高效的前端框架,适合从小型项目到大型企业级应用的开发。它的易用性、灵活性和强大的社区支持使其成为许多开发者的首选框架之一。"
5+
},
6+
"reply": {
7+
"originalContent": "领导,我想请假",
8+
"prompt": "不批",
9+
"data": "您的请假申请已收悉,经核实和考虑,暂时无法批准您的请假申请。\n\n如有特殊情况或紧急事务,请及时与我联系。\n\n祝工作顺利。\n\n谢谢。"
10+
}
11+
}

0 commit comments

Comments
 (0)