Merge remote-tracking branch 'yudao-ui-admin-vue3/dev' into dev
# Conflicts: # src/api/ai/writer/index.ts
@ -13,6 +13,12 @@ export interface WriteVO {
|
|||||||
format: number // 格式
|
format: number // 格式
|
||||||
tone: number // 语气
|
tone: number // 语气
|
||||||
language: number // 语言
|
language: number // 语言
|
||||||
|
userId?: number // 用户编号
|
||||||
|
platform?: string // 平台
|
||||||
|
model?: string // 模型
|
||||||
|
generatedContent?: string // 生成的内容
|
||||||
|
errorMessage: string // 错误信息
|
||||||
|
createTime?: Date // 创建时间
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AiWritePageReqVO extends PageParam {
|
export interface AiWritePageReqVO extends PageParam {
|
||||||
|
@ -1,28 +1,36 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="contentRef" class="markdown-view" v-html="contentHtml"></div>
|
<div ref="contentRef" class="markdown-view" v-html="contentHtml"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useClipboard} from "@vueuse/core";
|
import { useClipboard } from '@vueuse/core'
|
||||||
|
import { marked } from 'marked'
|
||||||
import {marked} from 'marked'
|
|
||||||
import 'highlight.js/styles/vs2015.min.css'
|
import 'highlight.js/styles/vs2015.min.css'
|
||||||
import hljs from 'highlight.js'
|
import hljs from 'highlight.js'
|
||||||
import {ref} from "vue";
|
|
||||||
|
|
||||||
const {copy} = useClipboard() // 初始化 copy 到粘贴板
|
// 定义组件属性
|
||||||
|
const props = defineProps({
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const message = useMessage() // 消息弹窗
|
||||||
|
const { copy } = useClipboard() // 初始化 copy 到粘贴板
|
||||||
const contentRef = ref()
|
const contentRef = ref()
|
||||||
|
const contentHtml = ref<any>() // 渲染的html内容
|
||||||
|
const { content } = toRefs(props) // 将 props 变为引用类型
|
||||||
|
|
||||||
// 代码高亮:https://highlightjs.org/
|
// 代码高亮:https://highlightjs.org/
|
||||||
// 转换 markdown:marked
|
// 转换 markdown:marked
|
||||||
|
|
||||||
// marked 渲染器
|
/** marked 渲染器 */
|
||||||
const renderer = {
|
const renderer = {
|
||||||
code(code, language, c) {
|
code(code, language, c) {
|
||||||
let highlightHtml
|
let highlightHtml
|
||||||
try {
|
try {
|
||||||
highlightHtml = hljs.highlight(code, {language: language, ignoreIllegals: true}).value
|
highlightHtml = hljs.highlight(code, { language: language, ignoreIllegals: true }).value
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// skip
|
// skip
|
||||||
}
|
}
|
||||||
@ -36,50 +44,30 @@ marked.use({
|
|||||||
renderer: renderer
|
renderer: renderer
|
||||||
})
|
})
|
||||||
|
|
||||||
// 渲染的html内容
|
/** 监听 content 变化 */
|
||||||
const contentHtml = ref<any>()
|
|
||||||
|
|
||||||
// 定义组件属性
|
|
||||||
const props = defineProps({
|
|
||||||
content: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 将 props 变为引用类型
|
|
||||||
const { content } = toRefs(props)
|
|
||||||
|
|
||||||
// 监听 content 变化
|
|
||||||
watch(content, async (newValue, oldValue) => {
|
watch(content, async (newValue, oldValue) => {
|
||||||
await renderMarkdown(newValue);
|
await renderMarkdown(newValue)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 渲染 markdown
|
/** 渲染 markdown */
|
||||||
const renderMarkdown = async (content: string) => {
|
const renderMarkdown = async (content: string) => {
|
||||||
contentHtml.value = await marked(content)
|
contentHtml.value = await marked(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件挂在时
|
/** 初始化 **/
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 解析转换 markdown
|
// 解析转换 markdown
|
||||||
await renderMarkdown(props.content as string);
|
await renderMarkdown(props.content as string)
|
||||||
//
|
|
||||||
// 添加 copy 监听
|
// 添加 copy 监听
|
||||||
contentRef.value.addEventListener('click', (e: any) => {
|
contentRef.value.addEventListener('click', (e: any) => {
|
||||||
console.log(e)
|
|
||||||
if (e.target.id === 'copy') {
|
if (e.target.id === 'copy') {
|
||||||
copy(e.target?.dataset?.copy)
|
copy(e.target?.dataset?.copy)
|
||||||
ElMessage({
|
message.success('复制成功!')
|
||||||
message: '复制成功!',
|
|
||||||
type: 'success'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.markdown-view {
|
.markdown-view {
|
||||||
font-family: PingFang SC;
|
font-family: PingFang SC;
|
||||||
|
@ -38,24 +38,24 @@ provide('reload', reload)
|
|||||||
:class="[
|
:class="[
|
||||||
'p-[var(--app-content-padding)] w-[calc(100%-var(--app-content-padding)-var(--app-content-padding))] bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]',
|
'p-[var(--app-content-padding)] w-[calc(100%-var(--app-content-padding)-var(--app-content-padding))] bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]',
|
||||||
{
|
{
|
||||||
'!h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
|
'!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
|
||||||
(fixedHeader &&
|
(fixedHeader &&
|
||||||
(layout === 'classic' || layout === 'topLeft' || layout === 'top') &&
|
(layout === 'classic' || layout === 'topLeft' || layout === 'top') &&
|
||||||
footer) ||
|
footer) ||
|
||||||
(!tagsView && layout === 'top' && footer),
|
(!tagsView && layout === 'top' && footer),
|
||||||
'!h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height))]':
|
'!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height))]':
|
||||||
tagsView && layout === 'top' && footer,
|
tagsView && layout === 'top' && footer,
|
||||||
|
|
||||||
'!h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--top-tool-height)-var(--app-footer-height))]':
|
'!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--top-tool-height)-var(--app-footer-height))]':
|
||||||
!fixedHeader && layout === 'classic' && footer,
|
!fixedHeader && layout === 'classic' && footer,
|
||||||
|
|
||||||
'!h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
|
'!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
|
||||||
!fixedHeader && layout === 'topLeft' && footer,
|
!fixedHeader && layout === 'topLeft' && footer,
|
||||||
|
|
||||||
'!h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding))]':
|
'!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding))]':
|
||||||
fixedHeader && layout === 'cutMenu' && footer,
|
fixedHeader && layout === 'cutMenu' && footer,
|
||||||
|
|
||||||
'!h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding)-var(--tags-view-height))]':
|
'!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding)-var(--tags-view-height))]':
|
||||||
!fixedHeader && layout === 'cutMenu' && footer
|
!fixedHeader && layout === 'cutMenu' && footer
|
||||||
}
|
}
|
||||||
]"
|
]"
|
||||||
|
@ -70,26 +70,26 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
path: '/ai/music',
|
// path: '/ai/music',
|
||||||
component: Layout,
|
// component: Layout,
|
||||||
redirect: '/index',
|
// redirect: '/index',
|
||||||
name: 'AIMusic',
|
// name: 'AIMusic',
|
||||||
meta: {},
|
// meta: {},
|
||||||
children: [
|
// children: [
|
||||||
{
|
// {
|
||||||
path: 'index',
|
// path: 'index',
|
||||||
component: () => import('@/views/ai/music/components/index.vue'),
|
// component: () => import('@/views/ai/music/components/index.vue'),
|
||||||
name: 'AIMusicIndex',
|
// name: 'AIMusicIndex',
|
||||||
meta: {
|
// meta: {
|
||||||
title: 'AI 音乐',
|
// title: 'AI 音乐',
|
||||||
icon: 'ep:home-filled',
|
// icon: 'ep:home-filled',
|
||||||
noCache: false,
|
// noCache: false,
|
||||||
affix: true
|
// affix: true
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
]
|
// ]
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
path: '/user',
|
path: '/user',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
|
@ -222,5 +222,10 @@ export enum DICT_TYPE {
|
|||||||
AI_PLATFORM = 'ai_platform', // AI 平台
|
AI_PLATFORM = 'ai_platform', // AI 平台
|
||||||
AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态
|
AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态
|
||||||
AI_MUSIC_STATUS = 'ai_music_status', // AI 音乐状态
|
AI_MUSIC_STATUS = 'ai_music_status', // AI 音乐状态
|
||||||
AI_GENERATE_MODE = 'ai_generate_mode' // AI 生成模式
|
AI_GENERATE_MODE = 'ai_generate_mode', // AI 生成模式
|
||||||
|
AI_WRITE_TYPE = 'ai_write_type', // AI 写作类型
|
||||||
|
AI_WRITE_LENGTH = 'ai_write_length', // AI 写作长度
|
||||||
|
AI_WRITE_FORMAT = 'ai_write_format', // AI 写作格式
|
||||||
|
AI_WRITE_TONE = 'ai_write_tone', // AI 写作语气
|
||||||
|
AI_WRITE_LANGUAGE = 'ai_write_language' // AI 写作语言
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,13 @@
|
|||||||
<el-text tag="b">平台</el-text>
|
<el-text tag="b">平台</el-text>
|
||||||
</div>
|
</div>
|
||||||
<el-space wrap class="group-item-body">
|
<el-space wrap class="group-item-body">
|
||||||
<el-select v-model="otherPlatform" placeholder="Select" size="large" class="!w-350px" @change="handlerPlatformChange">
|
<el-select
|
||||||
|
v-model="otherPlatform"
|
||||||
|
placeholder="Select"
|
||||||
|
size="large"
|
||||||
|
class="!w-350px"
|
||||||
|
@change="handlerPlatformChange"
|
||||||
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in OtherPlatformEnum"
|
v-for="item in OtherPlatformEnum"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
@ -52,12 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<el-space wrap class="group-item-body">
|
<el-space wrap class="group-item-body">
|
||||||
<el-select v-model="model" placeholder="Select" size="large" class="!w-350px">
|
<el-select v-model="model" placeholder="Select" size="large" class="!w-350px">
|
||||||
<el-option
|
<el-option v-for="item in models" :key="item.key" :label="item.name" :value="item.key" />
|
||||||
v-for="item in models"
|
|
||||||
:key="item.key"
|
|
||||||
:label="item.name"
|
|
||||||
:value="item.key"
|
|
||||||
/>
|
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-space>
|
</el-space>
|
||||||
</div>
|
</div>
|
||||||
@ -77,12 +78,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ImageApi, ImageDrawReqVO, ImageVO} from '@/api/ai/image'
|
import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image'
|
||||||
import {
|
import {
|
||||||
AiPlatformEnum,
|
AiPlatformEnum,
|
||||||
|
ChatGlmModels,
|
||||||
ImageHotWords,
|
ImageHotWords,
|
||||||
ImageModelVO,
|
ImageModelVO,
|
||||||
OtherPlatformEnum,
|
OtherPlatformEnum,
|
||||||
|
QianFanModels,
|
||||||
TongYiWanXiangModels
|
TongYiWanXiangModels
|
||||||
} from '@/views/ai/utils/constants'
|
} from '@/views/ai/utils/constants'
|
||||||
|
|
||||||
@ -96,10 +99,9 @@ const prompt = ref<string>('') // 提示词
|
|||||||
const width = ref<number>(512) // 图片宽度
|
const width = ref<number>(512) // 图片宽度
|
||||||
const height = ref<number>(512) // 图片高度
|
const height = ref<number>(512) // 图片高度
|
||||||
const otherPlatform = ref<string>(AiPlatformEnum.TONG_YI) // 平台
|
const otherPlatform = ref<string>(AiPlatformEnum.TONG_YI) // 平台
|
||||||
const models = ref<ImageModelVO[]>(TongYiWanXiangModels) // 模型
|
const models = ref<ImageModelVO[]>(TongYiWanXiangModels) // 模型 TongYiWanXiangModels、QianFanModels
|
||||||
const model = ref<string>(models.value[0].key) // 模型
|
const model = ref<string>(models.value[0].key) // 模型
|
||||||
|
|
||||||
|
|
||||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
|
const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
|
||||||
|
|
||||||
/** 选择热词 */
|
/** 选择热词 */
|
||||||
@ -131,9 +133,8 @@ const handleGenerateImage = async () => {
|
|||||||
prompt: prompt.value, // 提示词
|
prompt: prompt.value, // 提示词
|
||||||
width: width.value, // 图片宽度
|
width: width.value, // 图片宽度
|
||||||
height: height.value, // 图片高度
|
height: height.value, // 图片高度
|
||||||
options: {
|
options: {}
|
||||||
}
|
} as unknown as ImageDrawReqVO
|
||||||
} as ImageDrawReqVO
|
|
||||||
await ImageApi.drawImage(form)
|
await ImageApi.drawImage(form)
|
||||||
} finally {
|
} finally {
|
||||||
// 回调
|
// 回调
|
||||||
@ -148,21 +149,24 @@ const settingValues = async (detail: ImageVO) => {
|
|||||||
prompt.value = detail.prompt
|
prompt.value = detail.prompt
|
||||||
width.value = detail.width
|
width.value = detail.width
|
||||||
height.value = detail.height
|
height.value = detail.height
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 平台切换 */
|
/** 平台切换 */
|
||||||
const handlerPlatformChange = async (platform) => {
|
const handlerPlatformChange = async (platform: string) => {
|
||||||
// 切换平台,切换模型、风格
|
// 切换平台,切换模型、风格
|
||||||
if (AiPlatformEnum.YI_YAN === platform) {
|
if (AiPlatformEnum.TONG_YI === platform) {
|
||||||
models.value = TongYiWanXiangModels
|
models.value = TongYiWanXiangModels
|
||||||
|
} else if (AiPlatformEnum.YI_YAN === platform) {
|
||||||
|
models.value = QianFanModels
|
||||||
|
} else if (AiPlatformEnum.ZHI_PU === platform) {
|
||||||
|
models.value = ChatGlmModels
|
||||||
} else {
|
} else {
|
||||||
models.value = []
|
models.value = []
|
||||||
}
|
}
|
||||||
// 切换平台,默认选择一个风格
|
// 切换平台,默认选择一个风格
|
||||||
if (models.value.length > 0) {
|
if (models.value.length > 0) {
|
||||||
model.value = models.value[0].key
|
model.value = models.value[0].key
|
||||||
} else {
|
} else {
|
||||||
model.value = ''
|
model.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@
|
|||||||
ref="otherRef"
|
ref="otherRef"
|
||||||
@on-draw-complete="handleDrawComplete"
|
@on-draw-complete="handleDrawComplete"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
@ -63,7 +62,7 @@ const platformOptions = [
|
|||||||
value: AiPlatformEnum.STABLE_DIFFUSION
|
value: AiPlatformEnum.STABLE_DIFFUSION
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '其他',
|
label: '其它',
|
||||||
value: 'other'
|
value: 'other'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -89,6 +88,7 @@ const handleRegeneration = async (image: ImageVO) => {
|
|||||||
} else if (image.platform === AiPlatformEnum.STABLE_DIFFUSION) {
|
} else if (image.platform === AiPlatformEnum.STABLE_DIFFUSION) {
|
||||||
stableDiffusionRef.value.settingValues(image)
|
stableDiffusionRef.value.settingValues(image)
|
||||||
}
|
}
|
||||||
|
// TODO @fan:貌似 other 重新设置不行?
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -9,13 +9,19 @@
|
|||||||
label-width="68px"
|
label-width="68px"
|
||||||
>
|
>
|
||||||
<el-form-item label="用户编号" prop="userId">
|
<el-form-item label="用户编号" prop="userId">
|
||||||
<el-input
|
<el-select
|
||||||
v-model="queryParams.userId"
|
v-model="queryParams.userId"
|
||||||
placeholder="请输入用户编号"
|
|
||||||
clearable
|
clearable
|
||||||
@keyup.enter="handleQuery"
|
placeholder="请输入用户编号"
|
||||||
class="!w-240px"
|
class="!w-240px"
|
||||||
/>
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in userList"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.nickname"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="音乐名称" prop="title">
|
<el-form-item label="音乐名称" prop="title">
|
||||||
<el-input
|
<el-input
|
||||||
|
@ -20,21 +20,24 @@ export const AiPlatformEnum = {
|
|||||||
Ollama: 'Ollama',
|
Ollama: 'Ollama',
|
||||||
STABLE_DIFFUSION: 'StableDiffusion', // Stability AI
|
STABLE_DIFFUSION: 'StableDiffusion', // Stability AI
|
||||||
MIDJOURNEY: 'Midjourney', // Midjourney
|
MIDJOURNEY: 'Midjourney', // Midjourney
|
||||||
SUNO: 'Suno', // Suno AI
|
SUNO: 'Suno' // Suno AI
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OtherPlatformEnum:ImageModelVO [] = [
|
export const OtherPlatformEnum: ImageModelVO[] = [
|
||||||
{
|
{
|
||||||
key: AiPlatformEnum.TONG_YI,
|
key: AiPlatformEnum.TONG_YI,
|
||||||
name: '通义万相'
|
name: '通义万相'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: AiPlatformEnum.YI_YAN,
|
key: AiPlatformEnum.YI_YAN,
|
||||||
name: '百度图片'
|
name: '百度千帆'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: AiPlatformEnum.ZHI_PU,
|
||||||
|
name: '智谱 AI'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI 图像生成状态的枚举
|
* AI 图像生成状态的枚举
|
||||||
*/
|
*/
|
||||||
@ -207,54 +210,6 @@ export const StableDiffusionStylePresets: ImageModelVO[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
// todo @芋艿 这些是通义的风格,看要不要删除
|
|
||||||
export const TongYiWanXiangStylePresets: ImageModelVO[] = [
|
|
||||||
{
|
|
||||||
key: '-1',
|
|
||||||
name: '上传图像风格'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '0',
|
|
||||||
name: '复古漫画'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '1',
|
|
||||||
name: '3D童话'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '2',
|
|
||||||
name: '二次元'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '3',
|
|
||||||
name: '小清新'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '4',
|
|
||||||
name: '未来科技'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '5',
|
|
||||||
name: '国画古风'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '6',
|
|
||||||
name: '将军百战'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '7',
|
|
||||||
name: '炫彩卡通'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '8',
|
|
||||||
name: '清雅国风'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '9',
|
|
||||||
name: '喜迎新年'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
export const TongYiWanXiangModels: ImageModelVO[] = [
|
export const TongYiWanXiangModels: ImageModelVO[] = [
|
||||||
{
|
{
|
||||||
key: 'wanx-v1',
|
key: 'wanx-v1',
|
||||||
@ -266,6 +221,20 @@ export const TongYiWanXiangModels: ImageModelVO[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const QianFanModels: ImageModelVO[] = [
|
||||||
|
{
|
||||||
|
key: 'sd_xl',
|
||||||
|
name: 'sd_xl'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const ChatGlmModels: ImageModelVO[] = [
|
||||||
|
{
|
||||||
|
key: 'cogview-3',
|
||||||
|
name: 'cogview-3'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
export const StableDiffusionClipGuidancePresets: ImageModelVO[] = [
|
export const StableDiffusionClipGuidancePresets: ImageModelVO[] = [
|
||||||
{
|
{
|
||||||
key: 'NONE',
|
key: 'NONE',
|
||||||
@ -325,7 +294,7 @@ export const Dall3StyleList: ImageModelVO[] = [
|
|||||||
|
|
||||||
export interface ImageSizeVO {
|
export interface ImageSizeVO {
|
||||||
key: string
|
key: string
|
||||||
name: string
|
name?: string
|
||||||
style: string
|
style: string
|
||||||
width: string
|
width: string
|
||||||
height: string
|
height: string
|
||||||
|
238
src/views/ai/write/manager/index.vue
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
<template>
|
||||||
|
<ContentWrap>
|
||||||
|
<!-- 搜索工作栏 -->
|
||||||
|
<el-form
|
||||||
|
class="-mb-15px"
|
||||||
|
:model="queryParams"
|
||||||
|
ref="queryFormRef"
|
||||||
|
:inline="true"
|
||||||
|
label-width="68px"
|
||||||
|
>
|
||||||
|
<el-form-item label="用户编号" prop="userId">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.userId"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入用户编号"
|
||||||
|
class="!w-240px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in userList"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.nickname"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="写作类型" prop="type">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.type"
|
||||||
|
placeholder="请选择写作类型"
|
||||||
|
clearable
|
||||||
|
class="!w-240px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="dict in getIntDictOptions(DICT_TYPE.AI_WRITE_TYPE)"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.label"
|
||||||
|
:value="dict.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="平台" prop="platform">
|
||||||
|
<el-select v-model="queryParams.status" placeholder="请选择平台" clearable class="!w-240px">
|
||||||
|
<el-option
|
||||||
|
v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.label"
|
||||||
|
:value="dict.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建时间" prop="createTime">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="queryParams.createTime"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
type="daterange"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||||
|
class="!w-240px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||||
|
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
@click="openForm('create')"
|
||||||
|
v-hasPermi="['ai:write:create']"
|
||||||
|
>
|
||||||
|
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
plain
|
||||||
|
@click="handleExport"
|
||||||
|
:loading="exportLoading"
|
||||||
|
v-hasPermi="['ai:write:export']"
|
||||||
|
>
|
||||||
|
<Icon icon="ep:download" class="mr-5px" /> 导出
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<ContentWrap>
|
||||||
|
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||||
|
<el-table-column label="编号" align="center" prop="id" width="120" fixed="left" />
|
||||||
|
<el-table-column label="用户" align="center" prop="userId" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="写作类型" align="center" prop="type">
|
||||||
|
<template #default="scope">
|
||||||
|
<dict-tag :type="DICT_TYPE.AI_WRITE_TYPE" :value="scope.row.type" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="平台" align="center" prop="platform" width="120">
|
||||||
|
<template #default="scope">
|
||||||
|
<dict-tag :type="DICT_TYPE.AI_PLATFORM" :value="scope.row.platform" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="模型" align="center" prop="model" width="180" />
|
||||||
|
<el-table-column label="生成内容提示" align="center" prop="prompt" width="180" />
|
||||||
|
<el-table-column label="生成的内容" align="center" prop="generatedContent" width="180" />
|
||||||
|
<el-table-column label="原文" align="center" prop="originalContent" width="180" />
|
||||||
|
<el-table-column label="长度" align="center" prop="length">
|
||||||
|
<template #default="scope">
|
||||||
|
<dict-tag :type="DICT_TYPE.AI_WRITE_LENGTH" :value="scope.row.length" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="格式" align="center" prop="format">
|
||||||
|
<template #default="scope">
|
||||||
|
<dict-tag :type="DICT_TYPE.AI_WRITE_FORMAT" :value="scope.row.format" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="语气" align="center" prop="tone">
|
||||||
|
<template #default="scope">
|
||||||
|
<dict-tag :type="DICT_TYPE.AI_WRITE_TONE" :value="scope.row.tone" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="语言" align="center" prop="language">
|
||||||
|
<template #default="scope">
|
||||||
|
<dict-tag :type="DICT_TYPE.AI_WRITE_LANGUAGE" :value="scope.row.language" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
label="创建时间"
|
||||||
|
align="center"
|
||||||
|
prop="createTime"
|
||||||
|
:formatter="dateFormatter"
|
||||||
|
width="180px"
|
||||||
|
/>
|
||||||
|
<el-table-column label="错误信息" align="center" prop="errorMessage" />
|
||||||
|
<el-table-column label="操作" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
@click="openForm('update', scope.row.id)"
|
||||||
|
v-hasPermi="['ai:write:update']"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
@click="handleDelete(scope.row.id)"
|
||||||
|
v-hasPermi="['ai:write:delete']"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<!-- 分页 -->
|
||||||
|
<Pagination
|
||||||
|
:total="total"
|
||||||
|
v-model:page="queryParams.pageNo"
|
||||||
|
v-model:limit="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</ContentWrap>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
|
||||||
|
import { dateFormatter } from '@/utils/formatTime'
|
||||||
|
// TODO 芋艿:这里应该是 write
|
||||||
|
import { WriteApi, WriteVO } from '@/api/ai/writer'
|
||||||
|
import * as UserApi from '@/api/system/user'
|
||||||
|
|
||||||
|
/** AI 写作列表 */
|
||||||
|
defineOptions({ name: 'AiWriteManager' })
|
||||||
|
|
||||||
|
const message = useMessage() // 消息弹窗
|
||||||
|
const { t } = useI18n() // 国际化
|
||||||
|
|
||||||
|
const loading = ref(true) // 列表的加载中
|
||||||
|
const list = ref<WriteVO[]>([]) // 列表的数据
|
||||||
|
const total = ref(0) // 列表的总页数
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
userId: undefined,
|
||||||
|
type: undefined,
|
||||||
|
platform: undefined,
|
||||||
|
createTime: []
|
||||||
|
})
|
||||||
|
const queryFormRef = ref() // 搜索的表单
|
||||||
|
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
|
||||||
|
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data = await WriteApi.getWritePage(queryParams)
|
||||||
|
list.value = data.list
|
||||||
|
total.value = data.total
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNo = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value.resetFields()
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除按钮操作 */
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
try {
|
||||||
|
// 删除的二次确认
|
||||||
|
await message.delConfirm()
|
||||||
|
// 发起删除
|
||||||
|
await WriteApi.deleteWrite(id)
|
||||||
|
message.success(t('common.delSuccess'))
|
||||||
|
// 刷新列表
|
||||||
|
await getList()
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 **/
|
||||||
|
onMounted(async () => {
|
||||||
|
getList()
|
||||||
|
// 获得用户列表
|
||||||
|
userList.value = await UserApi.getSimpleUserList()
|
||||||
|
})
|
||||||
|
</script>
|
@ -83,13 +83,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<ReuseLabel label="长度" />
|
<ReuseLabel label="长度" />
|
||||||
<Tag v-model="formData.length" :tags="getIntDictOptions('ai_write_length')" />
|
<Tag v-model="formData.length" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_LENGTH)" />
|
||||||
<ReuseLabel label="格式" />
|
<ReuseLabel label="格式" />
|
||||||
<Tag v-model="formData.format" :tags="getIntDictOptions('ai_write_format')" />
|
<Tag v-model="formData.format" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_FORMAT)" />
|
||||||
<ReuseLabel label="语气" />
|
<ReuseLabel label="语气" />
|
||||||
<Tag v-model="formData.tone" :tags="getIntDictOptions('ai_write_tone')" />
|
<Tag v-model="formData.tone" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_TONE)" />
|
||||||
<ReuseLabel label="语言" />
|
<ReuseLabel label="语言" />
|
||||||
<Tag v-model="formData.language" :tags="getIntDictOptions('ai_write_language')" />
|
<Tag v-model="formData.language" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_LANGUAGE)" />
|
||||||
|
|
||||||
<div class="flex items-center justify-center mt-3">
|
<div class="flex items-center justify-center mt-3">
|
||||||
<el-button :disabled="isWriting" @click="reset">重置</el-button>
|
<el-button :disabled="isWriting" @click="reset">重置</el-button>
|
||||||
@ -106,7 +106,7 @@ import { ref } from 'vue'
|
|||||||
import Tag from './Tag.vue'
|
import Tag from './Tag.vue'
|
||||||
import { WriteVO } from '@/api/ai/writer'
|
import { WriteVO } from '@/api/ai/writer'
|
||||||
import { omit } from 'lodash-es'
|
import { omit } from 'lodash-es'
|
||||||
import { getIntDictOptions } from '@/utils/dict'
|
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||||
import { AiWriteTypeEnum, WriteExample } from '@/views/ai/utils/constants'
|
import { AiWriteTypeEnum, WriteExample } from '@/views/ai/utils/constants'
|
||||||
|
|
||||||
type TabType = WriteVO['type']
|
type TabType = WriteVO['type']
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="kefu">
|
<div class="kefu">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in conversationList"
|
v-for="item in conversationList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:class="{ active: index === activeConversationIndex, pinned: item.adminPinned }"
|
:class="{ active: item.id === activeConversationId, pinned: item.adminPinned }"
|
||||||
class="kefu-conversation flex items-center"
|
class="kefu-conversation flex items-center"
|
||||||
@click="openRightMessage(item, index)"
|
@click="openRightMessage(item)"
|
||||||
@contextmenu.prevent="rightClick($event as PointerEvent, item)"
|
@contextmenu.prevent="rightClick($event as PointerEvent, item)"
|
||||||
>
|
>
|
||||||
<div class="flex justify-center items-center w-100%">
|
<div class="flex justify-center items-center w-100%">
|
||||||
<!-- TODO style 换成 unocss -->
|
<div class="flex justify-center items-center w-50px h-50px">
|
||||||
<div class="flex justify-center items-center" style="width: 50px; height: 50px">
|
|
||||||
<!-- 头像 + 未读 -->
|
<!-- 头像 + 未读 -->
|
||||||
<el-badge
|
<el-badge
|
||||||
:hidden="item.adminUnreadMessageCount === 0"
|
:hidden="item.adminUnreadMessageCount === 0"
|
||||||
@ -27,19 +26,13 @@
|
|||||||
{{ formatDate(item.lastMessageTime) }}
|
{{ formatDate(item.lastMessageTime) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 文本消息 -->
|
<!-- 最后聊天内容 -->
|
||||||
<template v-if="KeFuMessageContentTypeEnum.TEXT === item.lastMessageContentType">
|
<div
|
||||||
<div
|
v-dompurify-html="
|
||||||
v-dompurify-html="replaceEmoji(item.lastMessageContent)"
|
getConversationDisplayText(item.lastMessageContentType, item.lastMessageContent)
|
||||||
class="last-message flex items-center color-[#989EA6]"
|
"
|
||||||
></div>
|
class="last-message flex items-center color-[#989EA6]"
|
||||||
</template>
|
></div>
|
||||||
<!-- 图片消息 -->
|
|
||||||
<template v-else>
|
|
||||||
<div class="last-message flex items-center color-[#989EA6]">
|
|
||||||
{{ getContentType(item.lastMessageContentType) }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -47,7 +40,7 @@
|
|||||||
<!-- 右键,进行操作(类似微信) -->
|
<!-- 右键,进行操作(类似微信) -->
|
||||||
<ul v-show="showRightMenu" :style="rightMenuStyle" class="right-menu-ul">
|
<ul v-show="showRightMenu" :style="rightMenuStyle" class="right-menu-ul">
|
||||||
<li
|
<li
|
||||||
v-show="!selectedConversation.adminPinned"
|
v-show="!rightClickConversation.adminPinned"
|
||||||
class="flex items-center"
|
class="flex items-center"
|
||||||
@click.stop="updateConversationPinned(true)"
|
@click.stop="updateConversationPinned(true)"
|
||||||
>
|
>
|
||||||
@ -55,7 +48,7 @@
|
|||||||
置顶会话
|
置顶会话
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-show="selectedConversation.adminPinned"
|
v-show="rightClickConversation.adminPinned"
|
||||||
class="flex items-center"
|
class="flex items-center"
|
||||||
@click.stop="updateConversationPinned(false)"
|
@click.stop="updateConversationPinned(false)"
|
||||||
>
|
>
|
||||||
@ -79,18 +72,22 @@ import { KeFuConversationApi, KeFuConversationRespVO } from '@/api/mall/promotio
|
|||||||
import { useEmoji } from './tools/emoji'
|
import { useEmoji } from './tools/emoji'
|
||||||
import { formatDate } from '@/utils/formatTime'
|
import { formatDate } from '@/utils/formatTime'
|
||||||
import { KeFuMessageContentTypeEnum } from './tools/constants'
|
import { KeFuMessageContentTypeEnum } from './tools/constants'
|
||||||
|
import { useAppStore } from '@/store/modules/app'
|
||||||
|
|
||||||
defineOptions({ name: 'KeFuConversationBox' })
|
defineOptions({ name: 'KeFuConversationList' })
|
||||||
|
|
||||||
const message = useMessage() // 消息弹窗
|
const message = useMessage() // 消息弹窗
|
||||||
|
const appStore = useAppStore()
|
||||||
const { replaceEmoji } = useEmoji()
|
const { replaceEmoji } = useEmoji()
|
||||||
const conversationList = ref<KeFuConversationRespVO[]>([]) // 会话列表
|
const conversationList = ref<KeFuConversationRespVO[]>([]) // 会话列表
|
||||||
const activeConversationIndex = ref(-1) // 选中的会话 index 位置 TODO @puhui999:这个可以改成 activeConversationId 么?因为一般是选中的对话编号
|
const activeConversationId = ref(-1) // 选中的会话
|
||||||
|
const collapse = computed(() => appStore.getCollapse) // 折叠菜单
|
||||||
|
|
||||||
/** 加载会话列表 */
|
/** 加载会话列表 */
|
||||||
const getConversationList = async () => {
|
const getConversationList = async () => {
|
||||||
conversationList.value = await KeFuConversationApi.getConversationList()
|
const list = await KeFuConversationApi.getConversationList()
|
||||||
|
list.sort((a: KeFuConversationRespVO, _) => (a.adminPinned ? -1 : 1))
|
||||||
|
conversationList.value = list
|
||||||
}
|
}
|
||||||
defineExpose({ getConversationList })
|
defineExpose({ getConversationList })
|
||||||
|
|
||||||
@ -98,45 +95,48 @@ defineExpose({ getConversationList })
|
|||||||
const emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
(e: 'change', v: KeFuConversationRespVO): void
|
(e: 'change', v: KeFuConversationRespVO): void
|
||||||
}>()
|
}>()
|
||||||
const openRightMessage = (item: KeFuConversationRespVO, index: number) => {
|
const openRightMessage = (item: KeFuConversationRespVO) => {
|
||||||
activeConversationIndex.value = index
|
activeConversationId.value = item.id
|
||||||
emits('change', item)
|
emits('change', item)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO @puhui999:这个,是不是改成 getConversationDisplayText,获取会话的展示文本。然后,把文本消息类型,也统一处理(包括上面的 replaceEmoji)。这样,更统一。
|
|
||||||
/** 获得消息类型 */
|
/** 获得消息类型 */
|
||||||
const getContentType = computed(() => (lastMessageContentType: number) => {
|
const getConversationDisplayText = computed(
|
||||||
switch (lastMessageContentType) {
|
() => (lastMessageContentType: number, lastMessageContent: string) => {
|
||||||
case KeFuMessageContentTypeEnum.SYSTEM:
|
switch (lastMessageContentType) {
|
||||||
return '[系统消息]'
|
case KeFuMessageContentTypeEnum.SYSTEM:
|
||||||
case KeFuMessageContentTypeEnum.VIDEO:
|
return '[系统消息]'
|
||||||
return '[视频消息]'
|
case KeFuMessageContentTypeEnum.VIDEO:
|
||||||
case KeFuMessageContentTypeEnum.IMAGE:
|
return '[视频消息]'
|
||||||
return '[图片消息]'
|
case KeFuMessageContentTypeEnum.IMAGE:
|
||||||
case KeFuMessageContentTypeEnum.PRODUCT:
|
return '[图片消息]'
|
||||||
return '[商品消息]'
|
case KeFuMessageContentTypeEnum.PRODUCT:
|
||||||
case KeFuMessageContentTypeEnum.ORDER:
|
return '[商品消息]'
|
||||||
return '[订单消息]'
|
case KeFuMessageContentTypeEnum.ORDER:
|
||||||
case KeFuMessageContentTypeEnum.VOICE:
|
return '[订单消息]'
|
||||||
return '[语音消息]'
|
case KeFuMessageContentTypeEnum.VOICE:
|
||||||
default:
|
return '[语音消息]'
|
||||||
return ''
|
case KeFuMessageContentTypeEnum.TEXT:
|
||||||
|
return replaceEmoji(lastMessageContent)
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
//======================= 右键菜单 =======================
|
//======================= 右键菜单 =======================
|
||||||
const showRightMenu = ref(false) // 显示右键菜单
|
const showRightMenu = ref(false) // 显示右键菜单
|
||||||
const rightMenuStyle = ref<any>({}) // 右键菜单 Style
|
const rightMenuStyle = ref<any>({}) // 右键菜单 Style
|
||||||
const selectedConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 右键选中的会话对象 TODO puhui999:这个是不是叫 rightClickConversation 会好点。因为 selected 容易和选中的对话,定义上有点重叠
|
const rightClickConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 右键选中的会话对象
|
||||||
|
|
||||||
/** 打开右键菜单 */
|
/** 打开右键菜单 */
|
||||||
const rightClick = (mouseEvent: PointerEvent, item: KeFuConversationRespVO) => {
|
const rightClick = (mouseEvent: PointerEvent, item: KeFuConversationRespVO) => {
|
||||||
selectedConversation.value = item
|
rightClickConversation.value = item
|
||||||
// 显示右键菜单
|
// 显示右键菜单
|
||||||
showRightMenu.value = true
|
showRightMenu.value = true
|
||||||
rightMenuStyle.value = {
|
rightMenuStyle.value = {
|
||||||
top: mouseEvent.clientY - 110 + 'px',
|
top: mouseEvent.clientY - 110 + 'px',
|
||||||
left: mouseEvent.clientX - 80 + 'px'
|
left: collapse.value ? mouseEvent.clientX - 80 + 'px' : mouseEvent.clientX - 210 + 'px'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/** 关闭右键菜单 */
|
/** 关闭右键菜单 */
|
||||||
@ -148,7 +148,7 @@ const closeRightMenu = () => {
|
|||||||
const updateConversationPinned = async (adminPinned: boolean) => {
|
const updateConversationPinned = async (adminPinned: boolean) => {
|
||||||
// 1. 会话置顶/取消置顶
|
// 1. 会话置顶/取消置顶
|
||||||
await KeFuConversationApi.updateConversationPinned({
|
await KeFuConversationApi.updateConversationPinned({
|
||||||
id: selectedConversation.value.id,
|
id: rightClickConversation.value.id,
|
||||||
adminPinned
|
adminPinned
|
||||||
})
|
})
|
||||||
message.notifySuccess(adminPinned ? '置顶成功' : '取消置顶成功')
|
message.notifySuccess(adminPinned ? '置顶成功' : '取消置顶成功')
|
||||||
@ -161,7 +161,7 @@ const updateConversationPinned = async (adminPinned: boolean) => {
|
|||||||
const deleteConversation = async () => {
|
const deleteConversation = async () => {
|
||||||
// 1. 删除会话
|
// 1. 删除会话
|
||||||
await message.confirm('您确定要删除该会话吗?')
|
await message.confirm('您确定要删除该会话吗?')
|
||||||
await KeFuConversationApi.deleteConversation(selectedConversation.value.id)
|
await KeFuConversationApi.deleteConversation(rightClickConversation.value.id)
|
||||||
// 2. 关闭右键菜单,更新会话列表
|
// 2. 关闭右键菜单,更新会话列表
|
||||||
closeRightMenu()
|
closeRightMenu()
|
||||||
await getConversationList()
|
await getConversationList()
|
@ -1,21 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container v-if="showChatBox" class="kefu">
|
<el-container v-if="showKeFuMessageList" class="kefu">
|
||||||
<el-header>
|
<el-header>
|
||||||
<!-- TODO @puhui999:keFuConversation => conversation -->
|
<div class="kefu-title">{{ conversation.userNickname }}</div>
|
||||||
<div class="kefu-title">{{ keFuConversation.userNickname }}</div>
|
|
||||||
</el-header>
|
</el-header>
|
||||||
<!-- TODO @puhui999:unocss -->
|
<el-main class="kefu-content overflow-visible">
|
||||||
<el-main class="kefu-content" style="overflow: visible">
|
|
||||||
<!-- 加载历史消息 -->
|
|
||||||
<div
|
|
||||||
v-show="loadingMore"
|
|
||||||
class="loadingMore flex justify-center items-center cursor-pointer"
|
|
||||||
@click="handleOldMessage"
|
|
||||||
>
|
|
||||||
加载更多
|
|
||||||
</div>
|
|
||||||
<el-scrollbar ref="scrollbarRef" always height="calc(100vh - 495px)" @scroll="handleScroll">
|
<el-scrollbar ref="scrollbarRef" always height="calc(100vh - 495px)" @scroll="handleScroll">
|
||||||
<div ref="innerRef" class="w-[100%] pb-3px">
|
<div v-if="refreshContent" ref="innerRef" class="w-[100%] pb-3px">
|
||||||
<!-- 消息列表 -->
|
<!-- 消息列表 -->
|
||||||
<div v-for="(item, index) in getMessageList0" :key="item.id" class="w-[100%]">
|
<div v-for="(item, index) in getMessageList0" :key="item.id" class="w-[100%]">
|
||||||
<div class="flex justify-center items-center mb-20px">
|
<div class="flex justify-center items-center mb-20px">
|
||||||
@ -48,7 +38,7 @@
|
|||||||
>
|
>
|
||||||
<el-avatar
|
<el-avatar
|
||||||
v-if="item.senderType === UserTypeEnum.MEMBER"
|
v-if="item.senderType === UserTypeEnum.MEMBER"
|
||||||
:src="keFuConversation.userAvatar"
|
:src="conversation.userAvatar"
|
||||||
alt="avatar"
|
alt="avatar"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@ -121,36 +111,48 @@ import relativeTime from 'dayjs/plugin/relativeTime'
|
|||||||
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
defineOptions({ name: 'KeFuMessageBox' })
|
defineOptions({ name: 'KeFuMessageList' })
|
||||||
|
|
||||||
const message = ref('') // 消息弹窗
|
const message = ref('') // 消息弹窗
|
||||||
|
|
||||||
const messageTool = useMessage()
|
const messageTool = useMessage()
|
||||||
const messageList = ref<KeFuMessageRespVO[]>([]) // 消息列表
|
const messageList = ref<KeFuMessageRespVO[]>([]) // 消息列表
|
||||||
const keFuConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话
|
const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话
|
||||||
const showNewMessageTip = ref(false) // 显示有新消息提示
|
const showNewMessageTip = ref(false) // 显示有新消息提示
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
pageNo: 1,
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
conversationId: 0
|
conversationId: 0
|
||||||
})
|
})
|
||||||
const total = ref(0) // 消息总条数
|
const total = ref(0) // 消息总条数
|
||||||
|
const refreshContent = ref(false) // 内容刷新,主要解决会话消息页面高度不一致导致的滚动功能精度失效
|
||||||
/** 获得消息列表 */
|
/** 获得消息列表 */
|
||||||
const getMessageList = async (conversation: KeFuConversationRespVO) => {
|
const getMessageList = async (val: KeFuConversationRespVO, conversationChange: boolean) => {
|
||||||
keFuConversation.value = conversation
|
// 会话切换,重置相关参数
|
||||||
queryParams.conversationId = conversation.id
|
if (conversationChange) {
|
||||||
const messageTotal = messageList.value.length
|
queryParams.pageNo = 1
|
||||||
if (total.value > 0 && messageTotal > 0 && messageTotal === total.value) {
|
messageList.value = []
|
||||||
return
|
total.value = 0
|
||||||
|
loadHistory.value = false
|
||||||
|
refreshContent.value = false
|
||||||
}
|
}
|
||||||
|
conversation.value = val
|
||||||
|
queryParams.conversationId = val.id
|
||||||
const res = await KeFuMessageApi.getKeFuMessagePage(queryParams)
|
const res = await KeFuMessageApi.getKeFuMessagePage(queryParams)
|
||||||
total.value = res.total
|
total.value = res.total
|
||||||
for (const item of res.list) {
|
// 情况一:加载最新消息
|
||||||
if (messageList.value.some((val) => val.id === item.id)) {
|
if (queryParams.pageNo === 1) {
|
||||||
continue
|
messageList.value = res.list
|
||||||
|
} else {
|
||||||
|
// 情况二:加载历史消息
|
||||||
|
for (const item of res.list) {
|
||||||
|
if (messageList.value.some((val) => val.id === item.id)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
messageList.value.push(item)
|
||||||
}
|
}
|
||||||
messageList.value.push(item)
|
|
||||||
}
|
}
|
||||||
|
refreshContent.value = true
|
||||||
await scrollToBottom()
|
await scrollToBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,20 +164,24 @@ const getMessageList0 = computed(() => {
|
|||||||
|
|
||||||
/** 刷新消息列表 */
|
/** 刷新消息列表 */
|
||||||
const refreshMessageList = async () => {
|
const refreshMessageList = async () => {
|
||||||
if (!keFuConversation.value) {
|
if (!conversation.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
queryParams.pageNo = 1
|
queryParams.pageNo = 1
|
||||||
await getMessageList(keFuConversation.value)
|
await getMessageList(conversation.value, false)
|
||||||
if (loadHistory.value) {
|
if (loadHistory.value) {
|
||||||
// 有下角显示有新消息提示
|
// 右下角显示有新消息提示
|
||||||
showNewMessageTip.value = true
|
showNewMessageTip.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ getMessageList, refreshMessageList })
|
defineExpose({ getMessageList, refreshMessageList })
|
||||||
const showChatBox = computed(() => !isEmpty(keFuConversation.value)) // 是否显示聊天区域
|
const showKeFuMessageList = computed(() => !isEmpty(conversation.value)) // 是否显示聊天区域
|
||||||
|
const skipGetMessageList = computed(() => {
|
||||||
|
// 已加载到最后一页的话则不触发新的消息获取
|
||||||
|
return total.value > 0 && Math.ceil(total.value / queryParams.pageSize) === queryParams.pageNo
|
||||||
|
}) // 跳过消息获取
|
||||||
|
|
||||||
/** 处理表情选择 */
|
/** 处理表情选择 */
|
||||||
const handleEmojiSelect = (item: Emoji) => {
|
const handleEmojiSelect = (item: Emoji) => {
|
||||||
@ -186,7 +192,7 @@ const handleEmojiSelect = (item: Emoji) => {
|
|||||||
const handleSendPicture = async (picUrl: string) => {
|
const handleSendPicture = async (picUrl: string) => {
|
||||||
// 组织发送消息
|
// 组织发送消息
|
||||||
const msg = {
|
const msg = {
|
||||||
conversationId: keFuConversation.value.id,
|
conversationId: conversation.value.id,
|
||||||
contentType: KeFuMessageContentTypeEnum.IMAGE,
|
contentType: KeFuMessageContentTypeEnum.IMAGE,
|
||||||
content: picUrl
|
content: picUrl
|
||||||
}
|
}
|
||||||
@ -202,7 +208,7 @@ const handleSendMessage = async () => {
|
|||||||
}
|
}
|
||||||
// 2. 组织发送消息
|
// 2. 组织发送消息
|
||||||
const msg = {
|
const msg = {
|
||||||
conversationId: keFuConversation.value.id,
|
conversationId: conversation.value.id,
|
||||||
contentType: KeFuMessageContentTypeEnum.TEXT,
|
contentType: KeFuMessageContentTypeEnum.TEXT,
|
||||||
content: message.value
|
content: message.value
|
||||||
}
|
}
|
||||||
@ -215,7 +221,7 @@ const sendMessage = async (msg: any) => {
|
|||||||
await KeFuMessageApi.sendKeFuMessage(msg)
|
await KeFuMessageApi.sendKeFuMessage(msg)
|
||||||
message.value = ''
|
message.value = ''
|
||||||
// 加载消息列表
|
// 加载消息列表
|
||||||
await getMessageList(keFuConversation.value)
|
await getMessageList(conversation.value, false)
|
||||||
// 滚动到最新消息处
|
// 滚动到最新消息处
|
||||||
await scrollToBottom()
|
await scrollToBottom()
|
||||||
}
|
}
|
||||||
@ -233,7 +239,7 @@ const scrollToBottom = async () => {
|
|||||||
scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight)
|
scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight)
|
||||||
showNewMessageTip.value = false
|
showNewMessageTip.value = false
|
||||||
// 2.2 消息已读
|
// 2.2 消息已读
|
||||||
await KeFuMessageApi.updateKeFuMessageReadStatus(keFuConversation.value.id)
|
await KeFuMessageApi.updateKeFuMessageReadStatus(conversation.value.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查看新消息 */
|
/** 查看新消息 */
|
||||||
@ -243,23 +249,28 @@ const handleToNewMessage = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 加载历史消息 */
|
/** 加载历史消息 */
|
||||||
const loadingMore = ref(false) // 滚动到顶部加载更多
|
|
||||||
const loadHistory = ref(false) // 加载历史消息
|
const loadHistory = ref(false) // 加载历史消息
|
||||||
const handleScroll = async ({ scrollTop }) => {
|
const handleScroll = async ({ scrollTop }) => {
|
||||||
const messageTotal = messageList.value.length
|
if (skipGetMessageList.value) {
|
||||||
if (total.value > 0 && messageTotal > 0 && messageTotal === total.value) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 距顶 20 加载下一页数据
|
// 触顶自动加载下一页数据
|
||||||
loadingMore.value = scrollTop < 20
|
if (scrollTop === 0) {
|
||||||
|
await handleOldMessage()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const handleOldMessage = async () => {
|
const handleOldMessage = async () => {
|
||||||
|
// 记录已有页面高度
|
||||||
|
const oldPageHeight = innerRef.value?.clientHeight
|
||||||
|
if (!oldPageHeight) {
|
||||||
|
return
|
||||||
|
}
|
||||||
loadHistory.value = true
|
loadHistory.value = true
|
||||||
// 加载消息列表
|
// 加载消息列表
|
||||||
queryParams.pageNo += 1
|
queryParams.pageNo += 1
|
||||||
await getMessageList(keFuConversation.value)
|
await getMessageList(conversation.value, false)
|
||||||
loadingMore.value = false
|
// 等页面加载完后,获得上一页最后一条消息的位置,控制滚动到它所在位置
|
||||||
// TODO puhui999: 等页面加载完后,获得上一页最后一条消息的位置,控制滚动到它所在位置
|
scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight - oldPageHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -288,20 +299,6 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
|
|||||||
&-content {
|
&-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.loadingMore {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 50px;
|
|
||||||
background-color: #eee;
|
|
||||||
color: #666;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 50px;
|
|
||||||
transform: translateY(-100%);
|
|
||||||
transition: transform 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.newMessageTip {
|
.newMessageTip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 35px;
|
bottom: 35px;
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
@ -1,4 +1,4 @@
|
|||||||
import KeFuConversationBox from './KeFuConversationBox.vue'
|
import KeFuConversationList from './KeFuConversationList.vue'
|
||||||
import KeFuChatBox from './KeFuChatBox.vue'
|
import KeFuMessageList from './KeFuMessageList.vue'
|
||||||
|
|
||||||
export { KeFuConversationBox, KeFuChatBox }
|
export { KeFuConversationList, KeFuMessageList }
|
||||||
|
@ -10,12 +10,13 @@
|
|||||||
: ''
|
: ''
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<!-- TODO @puhui999:unocss -->
|
|
||||||
<el-image
|
<el-image
|
||||||
|
:initial-index="0"
|
||||||
|
:preview-src-list="[message.content]"
|
||||||
:src="message.content"
|
:src="message.content"
|
||||||
|
class="w-200px"
|
||||||
fit="contain"
|
fit="contain"
|
||||||
style="width: 200px"
|
preview-teleported
|
||||||
@click="imagePreview(message.content)"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -25,17 +26,9 @@
|
|||||||
import { KeFuMessageContentTypeEnum } from '../tools/constants'
|
import { KeFuMessageContentTypeEnum } from '../tools/constants'
|
||||||
import { UserTypeEnum } from '@/utils/constants'
|
import { UserTypeEnum } from '@/utils/constants'
|
||||||
import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
|
import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
|
||||||
import { createImageViewer } from '@/components/ImageViewer'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ImageMessageItem' })
|
defineOptions({ name: 'ImageMessageItem' })
|
||||||
defineProps<{
|
defineProps<{
|
||||||
message: KeFuMessageRespVO
|
message: KeFuMessageRespVO
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
/** 图预览 */
|
|
||||||
const imagePreview = (imgUrl: string) => {
|
|
||||||
createImageViewer({
|
|
||||||
urlList: [imgUrl]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -18,10 +18,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="item in getMessageContent.items" :key="item.id" class="border-bottom">
|
<div v-for="item in getMessageContent.items" :key="item.id" class="border-bottom">
|
||||||
<!-- TODO @puhui999:要不把 img => picUrl 类似这种,搞的更匹配一点 -->
|
|
||||||
<ProductItem
|
<ProductItem
|
||||||
:img="item.picUrl"
|
|
||||||
:num="item.count"
|
:num="item.count"
|
||||||
|
:picUrl="item.picUrl"
|
||||||
:price="item.price"
|
:price="item.price"
|
||||||
:skuText="item.properties.map((property: any) => property.valueName).join(' ')"
|
:skuText="item.properties.map((property: any) => property.valueName).join(' ')"
|
||||||
:title="item.spuName"
|
:title="item.spuName"
|
||||||
@ -61,7 +60,7 @@ const getMessageContent = computed(() => JSON.parse(props.message.content))
|
|||||||
* @param order 订单
|
* @param order 订单
|
||||||
* @return {string} 颜色的 class 名称
|
* @return {string} 颜色的 class 名称
|
||||||
*/
|
*/
|
||||||
function formatOrderColor(order) {
|
function formatOrderColor(order: any) {
|
||||||
if (order.status === 0) {
|
if (order.status === 0) {
|
||||||
return 'info-color'
|
return 'info-color'
|
||||||
}
|
}
|
||||||
@ -79,7 +78,7 @@ function formatOrderColor(order) {
|
|||||||
*
|
*
|
||||||
* @param order 订单
|
* @param order 订单
|
||||||
*/
|
*/
|
||||||
function formatOrderStatus(order) {
|
function formatOrderStatus(order: any) {
|
||||||
if (order.status === 0) {
|
if (order.status === 0) {
|
||||||
return '待付款'
|
return '待付款'
|
||||||
}
|
}
|
||||||
@ -109,23 +108,23 @@ function formatOrderStatus(order) {
|
|||||||
background-color: #e2e2e2;
|
background-color: #e2e2e2;
|
||||||
|
|
||||||
.order-card-header {
|
.order-card-header {
|
||||||
height: 80rpx;
|
height: 80px;
|
||||||
|
|
||||||
.order-no {
|
.order-no {
|
||||||
font-size: 26rpx;
|
font-size: 26px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pay-box {
|
.pay-box {
|
||||||
.discounts-title {
|
.discounts-title {
|
||||||
font-size: 24rpx;
|
font-size: 24px;
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
color: #999999;
|
color: #999999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.discounts-money {
|
.discounts-money {
|
||||||
font-size: 24rpx;
|
font-size: 24px;
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
color: #999;
|
color: #999;
|
||||||
font-family: OPPOSANS;
|
font-family: OPPOSANS;
|
||||||
@ -137,29 +136,29 @@ function formatOrderStatus(order) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.order-card-footer {
|
.order-card-footer {
|
||||||
height: 100rpx;
|
height: 100px;
|
||||||
|
|
||||||
.more-item-box {
|
.more-item-box {
|
||||||
padding: 20rpx;
|
padding: 20px;
|
||||||
|
|
||||||
.more-item {
|
.more-item {
|
||||||
height: 60rpx;
|
height: 60px;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 26rpx;
|
font-size: 26px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.more-btn {
|
.more-btn {
|
||||||
color: #999999;
|
color: #999999;
|
||||||
font-size: 24rpx;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
width: 154rpx;
|
width: 154px;
|
||||||
color: #333333;
|
color: #333333;
|
||||||
font-size: 26rpx;
|
font-size: 26px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,14 @@
|
|||||||
class="ss-order-card-warp flex items-stretch justify-between bg-white"
|
class="ss-order-card-warp flex items-stretch justify-between bg-white"
|
||||||
>
|
>
|
||||||
<div class="img-box mr-24px">
|
<div class="img-box mr-24px">
|
||||||
<el-image :src="img" class="order-img" fit="contain" @click="imagePrediv(img)" />
|
<el-image
|
||||||
|
:initial-index="0"
|
||||||
|
:preview-src-list="[picUrl]"
|
||||||
|
:src="picUrl"
|
||||||
|
class="order-img"
|
||||||
|
fit="contain"
|
||||||
|
preview-teleported
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
:style="[{ width: titleWidth ? titleWidth + 'px' : '' }]"
|
:style="[{ width: titleWidth ? titleWidth + 'px' : '' }]"
|
||||||
@ -44,12 +51,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { createImageViewer } from '@/components/ImageViewer'
|
|
||||||
import { fenToYuan } from '@/utils'
|
import { fenToYuan } from '@/utils'
|
||||||
|
|
||||||
defineOptions({ name: 'ProductItem' })
|
defineOptions({ name: 'ProductItem' })
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
img: {
|
picUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'https://img1.baidu.com/it/u=1601695551,235775011&fm=26&fmt=auto'
|
default: 'https://img1.baidu.com/it/u=1601695551,235775011&fm=26&fmt=auto'
|
||||||
},
|
},
|
||||||
@ -101,14 +107,6 @@ const skuString = computed(() => {
|
|||||||
}
|
}
|
||||||
return props.skuText
|
return props.skuText
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO @puhui999:可以使用 preview-teleported
|
|
||||||
/** 图预览 */
|
|
||||||
const imagePrediv = (imgUrl: string) => {
|
|
||||||
createImageViewer({
|
|
||||||
urlList: [imgUrl]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<ProductItem
|
<ProductItem
|
||||||
:img="getMessageContent.picUrl"
|
:picUrl="getMessageContent.picUrl"
|
||||||
:price="getMessageContent.price"
|
:price="getMessageContent.price"
|
||||||
:skuText="getMessageContent.introduction"
|
:skuText="getMessageContent.introduction"
|
||||||
:title="getMessageContent.spuName"
|
:title="getMessageContent.spuName"
|
||||||
|
@ -17,8 +17,7 @@
|
|||||||
class="icon-item mr-2 mt-1 w-1/10 flex cursor-pointer items-center justify-center border border-solid p-2"
|
class="icon-item mr-2 mt-1 w-1/10 flex cursor-pointer items-center justify-center border border-solid p-2"
|
||||||
@click="handleSelect(item)"
|
@click="handleSelect(item)"
|
||||||
>
|
>
|
||||||
<!-- TODO @puhui999:换成 unocss -->
|
<img :src="item.url" class="w-24px h-24px" />
|
||||||
<img :src="item.url" style="width: 24px; height: 24px" />
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</ElScrollbar>
|
</ElScrollbar>
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
<!-- 图片选择 -->
|
<!-- 图片选择 -->
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- TODO @puhui999:unocss -->
|
<img :src="Picture" class="w-35px h-35px" @click="selectAndUpload" />
|
||||||
<img :src="Picture" style="width: 35px; height: 35px" @click="selectAndUpload" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// TODO @puhui999:images 换成 asserts
|
import Picture from '@/views/mall/promotion/kefu/components/asserts/picture.svg'
|
||||||
import Picture from '@/views/mall/promotion/kefu/components/images/picture.svg'
|
|
||||||
import * as FileApi from '@/api/infra/file'
|
import * as FileApi from '@/api/infra/file'
|
||||||
|
|
||||||
defineOptions({ name: 'PictureSelectUpload' })
|
defineOptions({ name: 'PictureSelectUpload' })
|
||||||
|
@ -59,12 +59,10 @@ export interface Emoji {
|
|||||||
export const useEmoji = () => {
|
export const useEmoji = () => {
|
||||||
const emojiPathList = ref<any[]>([])
|
const emojiPathList = ref<any[]>([])
|
||||||
|
|
||||||
// TODO @puhui999:initStaticEmoji 会不会更好
|
|
||||||
/** 加载本地图片 */
|
/** 加载本地图片 */
|
||||||
const getStaticEmojiPath = async () => {
|
const initStaticEmoji = async () => {
|
||||||
// TODO @puhui999:images 改成 asserts 更合适哈。
|
|
||||||
const pathList = import.meta.glob(
|
const pathList = import.meta.glob(
|
||||||
'@/views/mall/promotion/kefu/components/images/*.{png,jpg,jpeg,svg}'
|
'@/views/mall/promotion/kefu/components/asserts/*.{png,jpg,jpeg,svg}'
|
||||||
)
|
)
|
||||||
for (const path in pathList) {
|
for (const path in pathList) {
|
||||||
const imageModule: any = await pathList[path]()
|
const imageModule: any = await pathList[path]()
|
||||||
@ -75,26 +73,24 @@ export const useEmoji = () => {
|
|||||||
/** 初始化 */
|
/** 初始化 */
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (isEmpty(emojiPathList.value)) {
|
if (isEmpty(emojiPathList.value)) {
|
||||||
await getStaticEmojiPath()
|
await initStaticEmoji()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO @puhui999:建议 function 都改成 const 这种来定义哈。保持统一风格
|
|
||||||
/**
|
/**
|
||||||
* 将文本中的表情替换成图片
|
* 将文本中的表情替换成图片
|
||||||
*
|
*
|
||||||
* @param data 文本 TODO @puhui999:data => content
|
* @param data 文本
|
||||||
* @return 替换后的文本
|
* @return 替换后的文本
|
||||||
*/
|
*/
|
||||||
function replaceEmoji(data: string) {
|
const replaceEmoji = (content: string) => {
|
||||||
let newData = data
|
let newData = content
|
||||||
if (typeof newData !== 'object') {
|
if (typeof newData !== 'object') {
|
||||||
// TODO @puhui999: \] 是不是可以简化成 ]。我看 idea 提示了哈
|
const reg = /\[(.+?)]/g // [] 中括号
|
||||||
const reg = /\[(.+?)\]/g // [] 中括号
|
|
||||||
const zhEmojiName = newData.match(reg)
|
const zhEmojiName = newData.match(reg)
|
||||||
if (zhEmojiName) {
|
if (zhEmojiName) {
|
||||||
zhEmojiName.forEach((item) => {
|
zhEmojiName.forEach((item) => {
|
||||||
const emojiFile = selEmojiFile(item)
|
const emojiFile = getEmojiFileByName(item)
|
||||||
newData = newData.replace(
|
newData = newData.replace(
|
||||||
item,
|
item,
|
||||||
`<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${emojiFile}"/>`
|
`<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${emojiFile}"/>`
|
||||||
@ -112,13 +108,12 @@ export const useEmoji = () => {
|
|||||||
*/
|
*/
|
||||||
function getEmojiList(): Emoji[] {
|
function getEmojiList(): Emoji[] {
|
||||||
return emojiList.map((item) => ({
|
return emojiList.map((item) => ({
|
||||||
url: selEmojiFile(item.name),
|
url: getEmojiFileByName(item.name),
|
||||||
name: item.name
|
name: item.name
|
||||||
})) as Emoji[]
|
})) as Emoji[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO @puhui999:getEmojiFileByName 会不会更容易理解哈
|
function getEmojiFileByName(name: string) {
|
||||||
function selEmojiFile(name: string) {
|
|
||||||
for (const emoji of emojiList) {
|
for (const emoji of emojiList) {
|
||||||
if (emoji.name === name) {
|
if (emoji.name === name) {
|
||||||
return emojiPathList.value.find((item: string) => item.indexOf(emoji.file) > -1)
|
return emojiPathList.value.find((item: string) => item.indexOf(emoji.file) > -1)
|
||||||
|
@ -1,23 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-row :gutter="10">
|
<el-row :gutter="10">
|
||||||
<!-- TODO @puhui999:KeFuConversationBox => KeFuConversationList ;KeFuChatBox => KeFuMessageList -->
|
|
||||||
<!-- 会话列表 -->
|
<!-- 会话列表 -->
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<KeFuConversationBox ref="keFuConversationRef" @change="handleChange" />
|
<KeFuConversationList ref="keFuConversationRef" @change="handleChange" />
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
</el-col>
|
</el-col>
|
||||||
<!-- 会话详情(选中会话的消息列表) -->
|
<!-- 会话详情(选中会话的消息列表) -->
|
||||||
<el-col :span="16">
|
<el-col :span="16">
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<KeFuChatBox ref="keFuChatBoxRef" @change="getConversationList" />
|
<KeFuMessageList ref="keFuChatBoxRef" @change="getConversationList" />
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { KeFuChatBox, KeFuConversationBox } from './components'
|
import { KeFuConversationList, KeFuMessageList } from './components'
|
||||||
import { WebSocketMessageTypeConstants } from './components/tools/constants'
|
import { WebSocketMessageTypeConstants } from './components/tools/constants'
|
||||||
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
|
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
|
||||||
import { getAccessToken } from '@/utils/auth'
|
import { getAccessToken } from '@/utils/auth'
|
||||||
@ -29,14 +28,12 @@ const message = useMessage() // 消息弹窗
|
|||||||
|
|
||||||
// ======================= WebSocket start =======================
|
// ======================= WebSocket start =======================
|
||||||
const server = ref(
|
const server = ref(
|
||||||
(import.meta.env.VITE_BASE_URL + '/infra/ws/').replace('http', 'ws') +
|
(import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') + '?token=' + getAccessToken()
|
||||||
'?token=' +
|
|
||||||
getAccessToken()
|
|
||||||
) // WebSocket 服务地址
|
) // WebSocket 服务地址
|
||||||
|
|
||||||
/** 发起 WebSocket 连接 */
|
/** 发起 WebSocket 连接 */
|
||||||
const { data, close, open } = useWebSocket(server.value, {
|
const { data, close, open } = useWebSocket(server.value, {
|
||||||
autoReconnect: false, // TODO @puhui999:重连要加下
|
autoReconnect: true,
|
||||||
heartbeat: true
|
heartbeat: true
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -76,17 +73,16 @@ watchEffect(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
// ======================= WebSocket end =======================
|
// ======================= WebSocket end =======================
|
||||||
|
|
||||||
/** 加载会话列表 */
|
/** 加载会话列表 */
|
||||||
const keFuConversationRef = ref<InstanceType<typeof KeFuConversationBox>>()
|
const keFuConversationRef = ref<InstanceType<typeof KeFuConversationList>>()
|
||||||
const getConversationList = () => {
|
const getConversationList = () => {
|
||||||
keFuConversationRef.value?.getConversationList()
|
keFuConversationRef.value?.getConversationList()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 加载指定会话的消息列表 */
|
/** 加载指定会话的消息列表 */
|
||||||
const keFuChatBoxRef = ref<InstanceType<typeof KeFuChatBox>>()
|
const keFuChatBoxRef = ref<InstanceType<typeof KeFuMessageList>>()
|
||||||
const handleChange = (conversation: KeFuConversationRespVO) => {
|
const handleChange = (conversation: KeFuConversationRespVO) => {
|
||||||
keFuChatBoxRef.value?.getMessageList(conversation)
|
keFuChatBoxRef.value?.getMessageList(conversation, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 初始化 */
|
/** 初始化 */
|
||||||
|