Merge remote-tracking branch 'yudao-ui-admin-vue3/dev' into dev

# Conflicts:
#	src/views/ai/utils/constants.ts
This commit is contained in:
hhhero 2024-07-10 00:15:07 +08:00
commit a68ae29f3a
30 changed files with 1813 additions and 1237 deletions

View File

@ -12,16 +12,11 @@ export interface ImageVO {
publicStatus: boolean // 公开状态
picUrl: string // 任务地址
errorMessage: string // 错误信息
options: object // 配置 Map<string, string>
options: any // 配置 Map<string, string>
taskId: number // 任务编号
buttons: ImageMjButtonsVO[] // mj 操作按钮
createTime: string // 创建时间
finishTime: string // 完成时间
}
export interface ImagePageReqVO {
pageNo: number // 分页编号
pageSize: number // 分页大小
buttons: ImageMidjourneyButtonsVO[] // mj 操作按钮
createTime: Date // 创建时间
finishTime: Date // 完成时间
}
export interface ImageDrawReqVO {
@ -43,22 +38,22 @@ export interface ImageMidjourneyImagineReqVO {
version: string // 版本
}
export interface ImageMjActionVO {
export interface ImageMidjourneyActionVO {
id: number // 图片编号
customId: string // MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 动作标识
}
export interface ImageMjButtonsVO {
export interface ImageMidjourneyButtonsVO {
customId: string // MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 动作标识
emoji: string // 图标 emoji
label: string // Make Variations 文本
style: number // 样式: 2Primary、3Green
}
// AI API 密钥 API
// AI 图片 API
export const ImageApi = {
// 获取【我的】绘图分页
getImagePageMy: async (params: ImagePageReqVO) => {
getImagePageMy: async (params: PageParam) => {
return await request.get({ url: `/ai/image/my-page`, params })
},
// 获取【我的】绘图记录
@ -85,7 +80,7 @@ export const ImageApi = {
return await request.post({ url: `/ai/image/midjourney/imagine`, data })
},
// 【Midjourney】Action 操作(二次生成图片)
midjourneyAction: async (data: ImageMjActionVO) => {
midjourneyAction: async (data: ImageMidjourneyActionVO) => {
return await request.post({ url: `/ai/image/midjourney/action`, data })
},

View File

@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1716342375293" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2604" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M899.1 869.6l-53-305.6H864c14.4 0 26-11.6 26-26V346c0-14.4-11.6-26-26-26H618V138c0-14.4-11.6-26-26-26H432c-14.4 0-26 11.6-26 26v182H160c-14.4 0-26 11.6-26 26v192c0 14.4 11.6 26 26 26h17.9l-53 305.6c-0.3 1.5-0.4 3-0.4 4.4 0 14.4 11.6 26 26 26h723c1.5 0 3-0.1 4.4-0.4 14.2-2.4 23.7-15.9 21.2-30zM204 390h272V182h72v208h272v104H204V390z m468 440V674c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v156H416V674c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v156H202.8l45.1-260H776l45.1 260H672z" p-id="2605" fill="#8a8a8a"></path></svg>

Before

Width:  |  Height:  |  Size: 844 B

View File

@ -32,26 +32,25 @@ const download = {
// 下载 Markdown 方法
markdown: (data: Blob, fileName: string) => {
download0(data, fileName, 'text/markdown')
},
// 下载图片(允许跨域)
image: (url: string) => {
const image = new Image()
image.setAttribute('crossOrigin', 'anonymous')
image.src = url
image.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = image.width
canvas.height = image.height
const ctx = canvas.getContext('2d') as CanvasDrawImage
ctx.drawImage(image, 0, 0, image.width, image.height)
const url = canvas.toDataURL('image/png')
const a = document.createElement('a')
a.href = url
a.download = 'image.png'
a.click()
}
}
}
export default download
/** 图片下载(通过浏览器图片下载) */
export const downloadImage = async (imageUrl) => {
const image = new Image()
image.setAttribute('crossOrigin', 'anonymous')
image.src = imageUrl
image.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = image.width
canvas.height = image.height
const ctx = canvas.getContext('2d') as CanvasDrawImage
ctx.drawImage(image, 0, 0, image.width, image.height)
const url = canvas.toDataURL('image/png')
const a = document.createElement('a')
a.href = url
a.download = 'image.png'
a.click()
}
}

View File

@ -1,5 +1,5 @@
<template>
<div ref="messageContainer" class="h-100% overflow-y relative">
<div ref="messageContainer" class="h-100% overflow-y-auto relative">
<div class="chat-list" v-for="(item, index) in list" :key="index">
<!-- 靠左 messagesystemassistant 类型 -->
<div class="left-message message-item" v-if="item.type !== 'user'">
@ -101,13 +101,12 @@ const emits = defineEmits(['onDeleteSuccess', 'onRefresh', 'onEdit']) // 定义
/** 滚动到底部 */
const scrollToBottom = async (isIgnore?: boolean) => {
// 使 nextTick dom
await nextTick(() => {
if (isIgnore || !isScrolling.value) {
messageContainer.value.scrollTop =
messageContainer.value.scrollHeight - messageContainer.value.offsetHeight
}
})
// 使 nextTick dom
await nextTick()
if (isIgnore || !isScrolling.value) {
messageContainer.value.scrollTop =
messageContainer.value.scrollHeight - messageContainer.value.offsetHeight
}
}
function handleScroll() {

View File

@ -10,15 +10,13 @@
<el-icon><More /></el-icon>
</el-button>
</span>
<!-- TODO @fan下面两个 icon可以使用类似 <Icon icon="ep:question-filled" /> 替代哈 -->
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="['edit', role]">
<el-icon><EditPen /></el-icon>
<Icon icon="ep:edit" color="#787878" />
</el-dropdown-item>
<el-dropdown-item :command="['delete', role]" style="color: red">
<el-icon><Delete /></el-icon>
<span>删除</span>
<Icon icon="ep:delete" color="red" />删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
@ -43,9 +41,9 @@
</template>
<script setup lang="ts">
import { ChatRoleVO } from '@/api/ai/model/chatRole'
import { PropType, ref } from 'vue'
import { Delete, EditPen, More } from '@element-plus/icons-vue'
import {ChatRoleVO} from '@/api/ai/model/chatRole'
import {PropType, ref} from 'vue'
import {More} from '@element-plus/icons-vue'
const tabsRef = ref<any>() // tabs ref

View File

@ -23,10 +23,7 @@
@click="handlerAddRole"
class="ml-20px"
>
<!-- TODO @fan下面两个 icon可以使用类似 <Icon icon="ep:question-filled" /> 替代哈 -->
<el-icon>
<User />
</el-icon>
<Icon icon="ep:user" style="margin-right: 5px;" />
添加角色
</el-button>
</div>
@ -67,15 +64,15 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import {ref} from 'vue'
import RoleHeader from './RoleHeader.vue'
import RoleList from './RoleList.vue'
import ChatRoleForm from '@/views/ai/model/chatRole/ChatRoleForm.vue'
import RoleCategoryList from './RoleCategoryList.vue'
import { ChatRoleApi, ChatRolePageReqVO, ChatRoleVO } from '@/api/ai/model/chatRole'
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
import { Search, User } from '@element-plus/icons-vue'
import { TabsPaneContext } from 'element-plus'
import {ChatRoleApi, ChatRolePageReqVO, ChatRoleVO} from '@/api/ai/model/chatRole'
import {ChatConversationApi, ChatConversationVO} from '@/api/ai/chat/conversation'
import {Search} from '@element-plus/icons-vue'
import {TabsPaneContext} from 'element-plus'
const router = useRouter() //
@ -222,15 +219,14 @@ onMounted(async () => {
// role
await getActiveTabsRole()
})
// TODO @fancss scss
</script>
<style lang="css">
<!-- 覆盖 element ui css -->
<style lang="scss">
.el-tabs__content {
position: relative;
height: 100%;
overflow: hidden;
}
.el-tabs__nav-scroll {
margin: 10px 20px;
}

View File

@ -22,11 +22,14 @@
<Icon icon="ep:setting" class="ml-10px" />
</el-button>
<el-button size="small" class="btn" @click="handlerMessageClear">
<img src="@/assets/ai/clear.svg" class="h-14px" />
<Icon icon="heroicons-outline:archive-box-x-mark" color="#787878" />
</el-button>
<el-button size="small" class="btn">
<Icon icon="ep:download" color="#787878" />
</el-button>
<el-button size="small" class="btn" @click="handleGoTopMessage" >
<Icon icon="ep:top" color="#787878" />
</el-button>
<!-- TODO @fan下面两个 icon可以使用类似 <Icon icon="ep:question-filled" /> 替代哈 -->
<el-button size="small" :icon="Download" class="btn" />
<el-button size="small" :icon="Top" class="btn" @click="handleGoTopMessage" />
</div>
</el-header>
@ -180,11 +183,6 @@ const handleConversationClick = async (conversation: ChatConversationVO) => {
// id
activeConversationId.value = conversation.id
activeConversation.value = conversation
//
// TODO @fan
if (conversationInProgress.value) {
await stopStream()
}
// message
await getMessageList()
//
@ -203,7 +201,11 @@ const handlerConversationDelete = async (delConversation: ChatConversationVO) =>
}
/** 清空选中的对话 */
const handleConversationClear = async () => {
// TODO @fan
//
if (conversationInProgress.value) {
message.alert('对话中,不允许切换!')
return false
}
activeConversationId.value = null
activeConversation.value = null
activeMessageList.value = []
@ -363,7 +365,7 @@ const handlePromptInput = (event) => {
isComposing.value = false
}, 400)
}
// TODO @fan @keydown.enter@keydown.shift.enter shift+ isComposing
// TODO @ @keydown.enter@keydown.shift.enter shift+ isComposing
const onCompositionstart = () => {
isComposing.value = true
}
@ -394,7 +396,6 @@ const doSendMessage = async (content: string) => {
} as ChatMessageVO)
}
// TODO @fan= =
/** 真正执行【发送】消息操作 */
const doSendMessageStream = async (userMessage: ChatMessageVO) => {
// AbortController 便
@ -421,9 +422,8 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
createTime: new Date()
} as ChatMessageVO)
// 1.2
nextTick(async () => {
await scrollToBottom() //
})
await nextTick()
await scrollToBottom() //
// 1.3
textRoll()
@ -573,7 +573,6 @@ onMounted(async () => {
<style lang="scss" scoped>
.ai-layout {
// TODO @ height 100% TODO @fan
position: absolute;
flex: 1;
top: 0;

View File

@ -1,140 +0,0 @@
<template>
<el-drawer
v-model="showDrawer"
title="图片详细"
@close="handleDrawerClose"
custom-class="drawer-class"
>
<!-- 图片 -->
<div class="item">
<!-- <div class="header">-->
<!-- <div>图片</div>-->
<!-- <div>-->
<!-- </div>-->
<!-- </div>-->
<div class="body">
<!-- TODO @fan: 要不这里只展示图片不用 ImageTaskCard -->
<ImageTaskCard :image-detail="imageDetail" />
</div>
</div>
<!-- 时间 -->
<div class="item">
<div class="tip">时间</div>
<div class="body">
<div>提交时间{{ imageDetail.createTime }}</div>
<div>生成时间{{ imageDetail.finishTime }}</div>
</div>
</div>
<!-- 模型 -->
<div class="item">
<div class="tip">模型</div>
<div class="body">
{{ imageDetail.model }}({{ imageDetail.height }}x{{ imageDetail.width }})
</div>
</div>
<!-- 提示词 -->
<div class="item">
<div class="tip">提示词</div>
<div class="body">
{{ imageDetail.prompt }}
</div>
</div>
<!-- 地址 -->
<div class="item">
<div class="tip">图片地址</div>
<div class="body">
{{ imageDetail.picUrl }}
</div>
</div>
<!-- 风格 -->
<div class="item" v-if="imageDetail?.options?.style">
<div class="tip">风格</div>
<div class="body">
<!-- TODO @fan貌似需要把 imageStyleList 搞到 api/image/index.ts 枚举起来 -->
<!-- TODO @fan这里的展示可能需要按照平台做区分 -->
{{ imageDetail?.options?.style }}
</div>
</div>
</el-drawer>
</template>
<script setup lang="ts">
import { ImageApi, ImageVO } from '@/api/ai/image'
import ImageTaskCard from './ImageTaskCard.vue'
const showDrawer = ref<boolean>(false) //
const imageDetail = ref<ImageVO>({} as ImageVO) //
const props = defineProps({
show: {
type: Boolean,
require: true,
default: false
},
id: {
type: Number,
required: true
}
})
/** 抽屉 - close */
const handleDrawerClose = async () => {
emits('handleDrawerClose')
}
/** 获取 - 图片 detail */
const getImageDetail = async (id) => {
//
imageDetail.value = await ImageApi.getImageMy(id)
}
/** 任务 - detail */
const handleTaskDetail = async () => {
showDrawer.value = true
}
// watch show
const { show } = toRefs(props)
watch(show, async (newValue, oldValue) => {
showDrawer.value = newValue as boolean
})
// watch id
const { id } = toRefs(props)
watch(id, async (newVal, oldVal) => {
if (newVal) {
await getImageDetail(newVal)
}
})
//
const emits = defineEmits(['handleDrawerClose'])
//
onMounted(async () => {})
</script>
<style scoped lang="scss">
.item {
margin-bottom: 20px;
width: 100%;
overflow: hidden;
word-wrap: break-word;
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.tip {
font-weight: bold;
font-size: 16px;
}
.body {
margin-top: 10px;
color: #616161;
.taskImage {
border-radius: 10px;
}
}
}
</style>

View File

@ -2,58 +2,53 @@
<el-card body-class="" class="image-card">
<div class="image-operation">
<div>
<el-button
type="primary"
text
bg
v-if="imageDetail?.status === AiImageStatusEnum.IN_PROGRESS"
>
<el-button type="primary" text bg v-if="detail?.status === AiImageStatusEnum.IN_PROGRESS">
生成中
</el-button>
<el-button text bg v-else-if="imageDetail?.status === AiImageStatusEnum.SUCCESS">
<el-button text bg v-else-if="detail?.status === AiImageStatusEnum.SUCCESS">
已完成
</el-button>
<el-button type="danger" text bg v-else-if="imageDetail?.status === AiImageStatusEnum.FAIL">
<el-button type="danger" text bg v-else-if="detail?.status === AiImageStatusEnum.FAIL">
异常
</el-button>
</div>
<!-- 操作区 -->
<div>
<el-button
class="btn"
text
:icon="Download"
@click="handleBtnClick('download', imageDetail)"
@click="handleButtonClick('download', detail)"
/>
<el-button
class="btn"
text
:icon="RefreshRight"
@click="handleBtnClick('regeneration', imageDetail)"
@click="handleButtonClick('regeneration', detail)"
/>
<el-button
class="btn"
text
:icon="Delete"
@click="handleBtnClick('delete', imageDetail)"
/>
<el-button class="btn" text :icon="More" @click="handleBtnClick('more', imageDetail)" />
<el-button class="btn" text :icon="Delete" @click="handleButtonClick('delete', detail)" />
<el-button class="btn" text :icon="More" @click="handleButtonClick('more', detail)" />
</div>
</div>
<div class="image-wrapper" ref="cardImageRef">
<!-- TODO @fan要不加个点击大图预览 -->
<img class="image" :src="imageDetail?.picUrl" />
<div v-if="imageDetail?.status === AiImageStatusEnum.FAIL">
{{ imageDetail?.errorMessage }}
<el-image
class="image"
:src="detail?.picUrl"
:preview-src-list="[detail.picUrl]"
preview-teleported
/>
<div v-if="detail?.status === AiImageStatusEnum.FAIL">
{{ detail?.errorMessage }}
</div>
</div>
<!-- TODO @fanstyle 使用 unocss 替代下 -->
<!-- Midjourney 专属操作 -->
<div class="image-mj-btns">
<el-button
size="small"
v-for="button in imageDetail?.buttons"
v-for="button in detail?.buttons"
:key="button"
style="min-width: 40px; margin-left: 0; margin-right: 10px; margin-top: 5px"
@click="handleMjBtnClick(button)"
class="min-w-40px ml-0 mr-10px mt-5px"
@click="handleMidjourneyBtnClick(button)"
>
{{ button.label }}{{ button.emoji }}
</el-button>
@ -61,34 +56,53 @@
</el-card>
</template>
<script setup lang="ts">
import {Delete, Download, More, RefreshRight} from '@element-plus/icons-vue'
import { ImageVO, ImageMjButtonsVO } from '@/api/ai/image'
import { Delete, Download, More, RefreshRight } from '@element-plus/icons-vue'
import { ImageVO, ImageMidjourneyButtonsVO } from '@/api/ai/image'
import { PropType } from 'vue'
import {ElLoading, LoadingOptionsResolved} from 'element-plus'
import { ElLoading, LoadingOptionsResolved } from 'element-plus'
import { AiImageStatusEnum } from '@/views/ai/utils/constants'
const cardImageRef = ref<any>() // image ref
const cardImageLoadingInstance = ref<any>() // image ref
const message = useMessage()
const message = useMessage() //
const props = defineProps({
imageDetail: {
detail: {
type: Object as PropType<ImageVO>,
require: true
}
})
/** 按钮 - 点击事件 */
const handleBtnClick = async (type, imageDetail: ImageVO) => {
emits('onBtnClick', type, imageDetail)
const cardImageRef = ref<any>() // image ref
const cardImageLoadingInstance = ref<any>() // image ref
/** 处理点击事件 */
const handleButtonClick = async (type, detail: ImageVO) => {
emits('onBtnClick', type, detail)
}
/** 处理 Midjourney 按钮点击事件 */
const handleMidjourneyBtnClick = async (button: ImageMidjourneyButtonsVO) => {
//
await message.confirm(`确认操作 "${button.label} ${button.emoji}" ?`)
emits('onMjBtnClick', button, props.detail)
}
const emits = defineEmits(['onBtnClick', 'onMjBtnClick']) // emits
/** 监听详情 */
const { detail } = toRefs(props)
watch(detail, async (newVal, oldVal) => {
await handleLoading(newVal.status as string)
})
/** 处理加载状态 */
const handleLoading = async (status: number) => {
// TODO @ Loading
// loading
if (status === AiImageStatusEnum.IN_PROGRESS) {
cardImageLoadingInstance.value = ElLoading.service({
target: cardImageRef.value,
text: '生成中...'
} as LoadingOptionsResolved)
// loading
} else {
if (cardImageLoadingInstance.value) {
cardImageLoadingInstance.value.close()
@ -97,25 +111,9 @@ const handleLoading = async (status: number) => {
}
}
/** mj 按钮 click */
const handleMjBtnClick = async (button: ImageMjButtonsVO) => {
//
await message.confirm(`确认操作 "${button.label} ${button.emoji}" ?`)
emits('onMjBtnClick', button, props.imageDetail)
}
// watch
const { imageDetail } = toRefs(props)
watch(imageDetail, async (newVal, oldVal) => {
await handleLoading(newVal.status as string)
})
// emits
const emits = defineEmits(['onBtnClick', 'onMjBtnClick'])
//
/** 初始化 */
onMounted(async () => {
await handleLoading(props.imageDetail.status as string)
await handleLoading(props.detail.status as string)
})
</script>

View File

@ -0,0 +1,224 @@
<template>
<el-drawer
v-model="showDrawer"
title="图片详细"
@close="handleDrawerClose"
custom-class="drawer-class"
>
<!-- 图片 -->
<div class="item">
<div class="body">
<el-image
class="image"
:src="detail?.picUrl"
:preview-src-list="[detail.picUrl]"
preview-teleported
/>
</div>
</div>
<!-- 时间 -->
<div class="item">
<div class="tip">时间</div>
<div class="body">
<div>提交时间{{ formatTime(detail.createTime, 'yyyy-MM-dd HH:mm:ss') }}</div>
<div>生成时间{{ formatTime(detail.finishTime, 'yyyy-MM-dd HH:mm:ss') }}</div>
</div>
</div>
<!-- 模型 -->
<div class="item">
<div class="tip">模型</div>
<div class="body"> {{ detail.model }}({{ detail.height }}x{{ detail.width }}) </div>
</div>
<!-- 提示词 -->
<div class="item">
<div class="tip">提示词</div>
<div class="body">
{{ detail.prompt }}
</div>
</div>
<!-- 地址 -->
<div class="item">
<div class="tip">图片地址</div>
<div class="body">
{{ detail.picUrl }}
</div>
</div>
<!-- StableDiffusion 专属区域 -->
<div
class="item"
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.sampler"
>
<div class="tip">采样方法</div>
<div class="body">
{{
StableDiffusionSamplers.find(
(item: ImageModelVO) => item.key === detail?.options?.sampler
)?.name
}}
</div>
</div>
<div
class="item"
v-if="
detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.clipGuidancePreset
"
>
<div class="tip">CLIP</div>
<div class="body">
{{
StableDiffusionClipGuidancePresets.find(
(item: ImageModelVO) => item.key === detail?.options?.clipGuidancePreset
)?.name
}}
</div>
</div>
<div
class="item"
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.stylePreset"
>
<div class="tip">风格</div>
<div class="body">
{{
StableDiffusionStylePresets.find(
(item: ImageModelVO) => item.key === detail?.options?.stylePreset
)?.name
}}
</div>
</div>
<div
class="item"
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.steps"
>
<div class="tip">迭代步数</div>
<div class="body">
{{ detail?.options?.steps }}
</div>
</div>
<div
class="item"
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.scale"
>
<div class="tip">引导系数</div>
<div class="body">
{{ detail?.options?.scale }}
</div>
</div>
<div
class="item"
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.seed"
>
<div class="tip">随机因子</div>
<div class="body">
{{ detail?.options?.seed }}
</div>
</div>
<!-- Dall3 专属区域 -->
<div class="item" v-if="detail.platform === AiPlatformEnum.OPENAI && detail?.options?.style">
<div class="tip">风格选择</div>
<div class="body">
{{ Dall3StyleList.find((item: ImageModelVO) => item.key === detail?.options?.style)?.name }}
</div>
</div>
<!-- Midjourney 专属区域 -->
<div
class="item"
v-if="detail.platform === AiPlatformEnum.MIDJOURNEY && detail?.options?.version"
>
<div class="tip">模型版本</div>
<div class="body">
{{ detail?.options?.version }}
</div>
</div>
<div
class="item"
v-if="detail.platform === AiPlatformEnum.MIDJOURNEY && detail?.options?.referImageUrl"
>
<div class="tip">参考图</div>
<div class="body">
<el-image :src="detail.options.referImageUrl" />
</div>
</div>
</el-drawer>
</template>
<script setup lang="ts">
import { ImageApi, ImageVO } from '@/api/ai/image'
import {
AiPlatformEnum,
Dall3StyleList,
ImageModelVO,
StableDiffusionClipGuidancePresets,
StableDiffusionSamplers,
StableDiffusionStylePresets
} from '@/views/ai/utils/constants'
import { formatTime } from '@/utils'
const showDrawer = ref<boolean>(false) //
const detail = ref<ImageVO>({} as ImageVO) //
const props = defineProps({
show: {
type: Boolean,
require: true,
default: false
},
id: {
type: Number,
required: true
}
})
/** 关闭抽屉 */
const handleDrawerClose = async () => {
emits('handleDrawerClose')
}
/** 监听 drawer 是否打开 */
const { show } = toRefs(props)
watch(show, async (newValue, oldValue) => {
showDrawer.value = newValue as boolean
})
/** 获取图片详情 */
const getImageDetail = async (id: number) => {
detail.value = await ImageApi.getImageMy(id)
}
/** 监听 id 变化,加载最新图片详情 */
const { id } = toRefs(props)
watch(id, async (newVal, oldVal) => {
if (newVal) {
await getImageDetail(newVal)
}
})
const emits = defineEmits(['handleDrawerClose'])
</script>
<style scoped lang="scss">
.item {
margin-bottom: 20px;
width: 100%;
overflow: hidden;
word-wrap: break-word;
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.tip {
font-weight: bold;
font-size: 16px;
}
.body {
margin-top: 10px;
color: #616161;
.taskImage {
border-radius: 10px;
}
}
}
</style>

View File

@ -1,85 +1,87 @@
<template>
<el-card class="dr-task" body-class="task-card" shadow="never">
<template #header>绘画任务</template>
<div class="task-image-list" ref="imageTaskRef">
<ImageTaskCard
<!-- 图片列表 -->
<div class="task-image-list" ref="imageListRef">
<ImageCard
v-for="image in imageList"
:key="image"
:image-detail="image"
@on-btn-click="handleImageBtnClick"
@on-mj-btn-click="handleImageMjBtnClick"
:key="image.id"
:detail="image"
@on-btn-click="handleImageButtonClick"
@on-mj-btn-click="handleImageMidjourneyButtonClick"
/>
</div>
<div class="task-image-pagination">
<el-pagination
background
layout="prev, pager, next"
:default-page-size="pageSize"
<Pagination
:total="pageTotal"
@change="handlePageChange"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getImageList"
/>
</div>
</el-card>
<!-- 图片 detail 抽屉 -->
<ImageDetailDrawer
<!-- 图片详情 -->
<ImageDetail
:show="isShowImageDetail"
:id="showImageDetailId"
@handle-drawer-close="handleDrawerClose"
@handle-drawer-close="handleDetailClose"
/>
</template>
<script setup lang="ts">
import { ImageApi, ImageVO, ImageMjActionVO, ImageMjButtonsVO } from '@/api/ai/image'
import ImageDetailDrawer from './ImageDetailDrawer.vue'
import ImageTaskCard from './ImageTaskCard.vue'
import {
ImageApi,
ImageVO,
ImageMidjourneyActionVO,
ImageMidjourneyButtonsVO
} from '@/api/ai/image'
import ImageDetail from './ImageDetail.vue'
import ImageCard from './ImageCard.vue'
import { ElLoading, LoadingOptionsResolved } from 'element-plus'
import { AiImageStatusEnum } from '@/views/ai/utils/constants'
import { downloadImage } from '@/utils/download'
import download from '@/utils/download'
const message = useMessage() //
const imageList = ref<ImageVO[]>([]) // image
const inProgressImageMap = ref<{}>({}) // image key image value image
const imageListInterval = ref<any>() // image
const isShowImageDetail = ref<boolean>(false) // task
const showImageDetailId = ref<number>(0) // task
const imageTaskRef = ref<any>() // ref
const imageTaskLoadingInstance = ref<any>() // loading
const imageTaskLoading = ref<boolean>(false) // loading
const pageNo = ref<number>(1) // page no
const pageSize = ref<number>(10) // page size
//
const queryParams = reactive({
pageNo: 1,
pageSize: 10
})
const pageTotal = ref<number>(0) // page size
const imageList = ref<ImageVO[]>([]) // image
const imageListLoadingInstance = ref<any>() // image
const imageListRef = ref<any>() // ref
//
const inProgressImageMap = ref<{}>({}) // image key image value image
const inProgressTimer = ref<any>() // image
//
const isShowImageDetail = ref<boolean>(false) //
const showImageDetailId = ref<number>(0) //
/** 抽屉 - close */
const handleDrawerClose = async () => {
isShowImageDetail.value = false
}
/** 任务 - detail */
const handleDrawerOpen = async () => {
/** 查看图片的详情 */
const handleDetailOpen = async () => {
isShowImageDetail.value = true
}
/**
* 获取 - image 列表
*/
const getImageList = async (apply: boolean = false) => {
imageTaskLoading.value = true
/** 关闭图片的详情 */
const handleDetailClose = async () => {
isShowImageDetail.value = false
}
/** 获得 image 图片列表 */
const getImageList = async () => {
try {
imageTaskLoadingInstance.value = ElLoading.service({
target: imageTaskRef.value,
// 1.
imageListLoadingInstance.value = ElLoading.service({
target: imageListRef.value,
text: '加载中...'
} as LoadingOptionsResolved)
const { list, total } = await ImageApi.getImagePageMy({
pageNo: pageNo.value,
pageSize: pageSize.value
})
if (apply) {
imageList.value = [...imageList.value, ...list]
} else {
imageList.value = list
}
const { list, total } = await ImageApi.getImagePageMy(queryParams)
imageList.value = list
pageTotal.value = total
// watch
// 2.
const newWatImages = {}
imageList.value.forEach((item) => {
if (item.status === AiImageStatusEnum.IN_PROGRESS) {
@ -88,9 +90,10 @@ const getImageList = async (apply: boolean = false) => {
})
inProgressImageMap.value = newWatImages
} finally {
if (imageTaskLoadingInstance.value) {
imageTaskLoadingInstance.value.close()
imageTaskLoadingInstance.value = null
// Loading
if (imageListLoadingInstance.value) {
imageListLoadingInstance.value.close()
imageListLoadingInstance.value = null
}
}
}
@ -117,50 +120,52 @@ const refreshWatchImages = async () => {
inProgressImageMap.value = newWatchImages
}
/** 图片 - btn click */
const handleImageBtnClick = async (type: string, imageDetail: ImageVO) => {
// image detail id
showImageDetailId.value = imageDetail.id
// btn
/** 图片的点击事件 */
const handleImageButtonClick = async (type: string, imageDetail: ImageVO) => {
//
if (type === 'more') {
await handleDrawerOpen()
} else if (type === 'delete') {
showImageDetailId.value = imageDetail.id
await handleDetailOpen()
return
}
//
if (type === 'delete') {
await message.confirm(`是否删除照片?`)
await ImageApi.deleteImageMy(imageDetail.id)
await getImageList()
message.success('删除成功!')
} else if (type === 'download') {
await downloadImage(imageDetail.picUrl)
} else if (type === 'regeneration') {
// Midjourney
console.log('regeneration', imageDetail.id)
return
}
//
if (type === 'download') {
await download.image(imageDetail.picUrl)
return
}
//
if (type === 'regeneration') {
await emits('onRegeneration', imageDetail)
return
}
}
/** 图片 - mj btn click */
const handleImageMjBtnClick = async (button: ImageMjButtonsVO, imageDetail: ImageVO) => {
// 1 params
/** 处理 Midjourney 按钮点击事件 */
const handleImageMidjourneyButtonClick = async (
button: ImageMidjourneyButtonsVO,
imageDetail: ImageVO
) => {
// 1. params
const data = {
id: imageDetail.id,
customId: button.customId
} as ImageMjActionVO
// 2 action
} as ImageMidjourneyActionVO
// 2. action
await ImageApi.midjourneyAction(data)
// 3
// 3.
await getImageList()
}
// page change
const handlePageChange = async (page) => {
pageNo.value = page
await getImageList(false)
}
defineExpose({ getImageList }) //
/** 暴露组件方法 */
defineExpose({ getImageList })
// emits
const emits = defineEmits(['onRegeneration'])
/** 组件挂在的时候 */
@ -168,19 +173,20 @@ onMounted(async () => {
// image
await getImageList()
// image
imageListInterval.value = setInterval(async () => {
inProgressTimer.value = setInterval(async () => {
await refreshWatchImages()
}, 1000 * 3)
})
/** 组件取消挂在的时候 */
onUnmounted(async () => {
if (imageListInterval.value) {
clearInterval(imageListInterval.value)
if (inProgressTimer.value) {
clearInterval(inProgressTimer.value)
}
})
</script>
<!-- TODO fan 2 scss 可以合并么 -->
<style lang="scss">
.task-card {
margin: 0;
@ -197,8 +203,7 @@ onUnmounted(async () => {
align-content: flex-start;
height: 100%;
overflow: auto;
padding: 20px;
padding-bottom: 140px;
padding: 20px 20px 140px;
box-sizing: border-box; /* 确保内边距不会增加高度 */
> div {
@ -224,7 +229,6 @@ onUnmounted(async () => {
align-items: center;
}
</style>
<style scoped lang="scss">
.dr-task {
width: 100%;

View File

@ -3,12 +3,11 @@
<div class="prompt">
<el-text tag="b">画面描述</el-text>
<el-text tag="p">建议使用形容词+动词+风格的格式使用隔开</el-text>
<!-- TODO @fanstyle 看看能不能哟 unocss 替代 -->
<el-input
v-model="prompt"
maxlength="1024"
rows="5"
style="width: 100%; margin-top: 15px;"
class="w-100% mt-15px"
input-style="border-radius: 7px;"
placeholder="例如:童话里的小屋应该是什么样子?"
show-word-limit
@ -20,12 +19,13 @@
<el-text tag="b">随机热词</el-text>
</div>
<el-space wrap class="word-list">
<el-button round
class="btn"
:type="(selectHotWord === hotWord ? 'primary' : 'default')"
v-for="hotWord in hotWords"
:key="hotWord"
@click="handleHotWordClick(hotWord)"
<el-button
round
class="btn"
:type="selectHotWord === hotWord ? 'primary' : 'default'"
v-for="hotWord in ImageHotWords"
:key="hotWord"
@click="handleHotWordClick(hotWord)"
>
{{ hotWord }}
</el-button>
@ -38,16 +38,11 @@
<el-space wrap class="model-list">
<div
:class="selectModel === model.key ? 'modal-item selectModel' : 'modal-item'"
v-for="model in models"
v-for="model in Dall3Models"
:key="model.key"
>
<el-image
:src="model.image"
fit="contain"
@click="handleModelClick(model)"
/>
<div class="model-font">{{model.name}}</div>
<el-image :src="model.image" fit="contain" @click="handleModelClick(model)" />
<div class="model-font">{{ model.name }}</div>
</div>
</el-space>
</div>
@ -57,16 +52,12 @@
</div>
<el-space wrap class="image-style-list">
<div
:class="selectImageStyle === imageStyle.key ? 'image-style-item selectImageStyle' : 'image-style-item'"
v-for="imageStyle in imageStyleList"
:class="style === imageStyle.key ? 'image-style-item selectImageStyle' : 'image-style-item'"
v-for="imageStyle in Dall3StyleList"
:key="imageStyle.key"
>
<el-image
:src="imageStyle.image"
fit="contain"
@click="handleStyleClick(imageStyle)"
/>
<div class="style-font">{{imageStyle.name}}</div>
<el-image :src="imageStyle.image" fit="contain" @click="handleStyleClick(imageStyle)" />
<div class="style-font">{{ imageStyle.name }}</div>
</div>
</el-space>
</div>
@ -75,11 +66,15 @@
<el-text tag="b">画面比例</el-text>
</div>
<el-space wrap class="size-list">
<div class="size-item"
v-for="imageSize in imageSizeList"
:key="imageSize.key"
@click="handleSizeClick(imageSize)">
<div :class="selectImageSize === imageSize.key ? 'size-wrapper selectImageSize' : 'size-wrapper'">
<div
class="size-item"
v-for="imageSize in Dall3SizeList"
:key="imageSize.key"
@click="handleSizeClick(imageSize)"
>
<div
:class="selectSize === imageSize.key ? 'size-wrapper selectImageSize' : 'size-wrapper'"
>
<div :style="imageSize.style"></div>
</div>
<div class="size-font">{{ imageSize.name }}</div>
@ -87,126 +82,60 @@
</el-space>
</div>
<div class="btns">
<el-button type="primary"
size="large"
round
:loading="drawIn"
@click="handleGenerateImage">
{{drawIn ? '生成中' : '生成内容'}}
<el-button type="primary" size="large" round :loading="drawIn" @click="handleGenerateImage">
{{ drawIn ? '生成中' : '生成内容' }}
</el-button>
</div>
</template>
<script setup lang="ts">
import {ImageApi, ImageDrawReqVO, ImageVO} from '@/api/ai/image';
import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image'
import {
Dall3Models,
Dall3StyleList,
ImageHotWords,
Dall3SizeList,
ImageModelVO,
AiPlatformEnum
} from '@/views/ai/utils/constants'
// image
interface ImageModelVO {
key: string
name: string
image: string
}
// image
interface ImageSizeVO {
key: string
name: string,
style: string,
width: string,
height: string,
}
const message = useMessage() //
//
const prompt = ref<string>('') //
const drawIn = ref<boolean>(false) //
const prompt = ref<string>('') //
const drawIn = ref<boolean>(false) //
const selectHotWord = ref<string>('') //
const hotWords = ref<string[]>(['中国旗袍', '古装美女', '卡通头像', '机甲战士', '童话小屋', '中国长城']) //
const selectModel = ref<string>('dall-e-3') //
// message
const message = useMessage()
const models = ref<ImageModelVO[]>([
{
key: 'dall-e-3',
name: 'DALL·E 3',
image: `/src/assets/ai/dall2.jpg`,
},
{
key: 'dall-e-2',
name: 'DALL·E 2',
image: `/src/assets/ai/dall3.jpg`,
},
]) //
const selectSize = ref<string>('1024x1024') // size
const style = ref<string>('vivid') // style
const selectImageStyle = ref<string>('vivid') // style
const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // emits
const imageStyleList = ref<ImageModelVO[]>([
{
key: 'vivid',
name: '清晰',
image: `/src/assets/ai/qingxi.jpg`,
},
{
key: 'natural',
name: '自然',
image: `/src/assets/ai/ziran.jpg`,
},
]) // style
const selectImageSize = ref<string>('1024x1024') // size
const imageSizeList = ref<ImageSizeVO[]>([
{
key: '1024x1024',
name: '1:1',
width: '1024',
height: '1024',
style: 'width: 30px; height: 30px;background-color: #dcdcdc;',
},
{
key: '1024x1792',
name: '3:5',
width: '1024',
height: '1792',
style: 'width: 30px; height: 50px;background-color: #dcdcdc;',
},
{
key: '1792x1024',
name: '5:3',
width: '1792',
height: '1024',
style: 'width: 50px; height: 30px;background-color: #dcdcdc;',
}
]) // size
// Props
const props = defineProps({})
// emits
const emits = defineEmits(['onDrawStart', 'onDrawComplete'])
/** 热词 - click */
/** 选择热词 */
const handleHotWordClick = async (hotWord: string) => {
//
//
if (selectHotWord.value == hotWord) {
selectHotWord.value = ''
return
}
//
//
selectHotWord.value = hotWord
//
prompt.value = hotWord
}
/** 模型 - click */
/** 选择 model 模型 */
const handleModelClick = async (model: ImageModelVO) => {
selectModel.value = model.key
}
/** 样式 - click */
/** 选择 style 样式 */
const handleStyleClick = async (imageStyle: ImageModelVO) => {
selectImageStyle.value = imageStyle.key
style.value = imageStyle.key
}
/** size - click */
/** 选择 size 大小 */
const handleSizeClick = async (imageSize: ImageSizeVO) => {
selectImageSize.value = imageSize.key
selectSize.value = imageSize.key
}
/** 图片生产 */
@ -217,42 +146,41 @@ const handleGenerateImage = async () => {
//
drawIn.value = true
//
emits('onDrawStart', selectModel.value)
const imageSize = imageSizeList.value.find(item => item.key === selectImageSize.value) as ImageSizeVO
emits('onDrawStart', AiPlatformEnum.OPENAI)
const imageSize = Dall3SizeList.find((item) => item.key === selectSize.value) as ImageSizeVO
const form = {
platform: 'OpenAI',
platform: AiPlatformEnum.OPENAI,
prompt: prompt.value, //
model: selectModel.value, //
width: imageSize.width, // size
height: imageSize.height, // size
options: {
style: selectImageStyle.value, //
style: style.value //
}
} as ImageDrawReqVO
//
await ImageApi.drawImage(form)
} finally {
//
emits('onDrawComplete', selectModel.value)
emits('onDrawComplete', AiPlatformEnum.OPENAI)
//
drawIn.value = false
}
}
/** 填充值 */
const settingValues = async (imageDetail: ImageVO) => {
prompt.value = imageDetail.prompt
selectModel.value = imageDetail.model
//
selectImageStyle.value = imageDetail.options?.style
//
const imageSize = imageSizeList.value.find(item => item.key === `${imageDetail.width}x${imageDetail.height}`) as ImageSizeVO
const settingValues = async (detail: ImageVO) => {
prompt.value = detail.prompt
selectModel.value = detail.model
style.value = detail.options?.style
const imageSize = Dall3SizeList.find(
(item) => item.key === `${detail.width}x${detail.height}`
) as ImageSizeVO
await handleSizeClick(imageSize)
}
/** 暴露组件方法 */
defineExpose({ settingValues })
</script>
<style scoped lang="scss">
//
@ -309,7 +237,6 @@ defineExpose({ settingValues })
}
}
// style
.image-style {
margin-top: 30px;

View File

@ -7,7 +7,7 @@
v-model="prompt"
maxlength="1024"
rows="5"
style="width: 100%; margin-top: 15px;"
class="w-100% mt-15px"
input-style="border-radius: 7px;"
placeholder="例如:童话里的小屋应该是什么样子?"
show-word-limit
@ -19,12 +19,13 @@
<el-text tag="b">随机热词</el-text>
</div>
<el-space wrap class="word-list">
<el-button round
class="btn"
:type="(selectHotWord === hotWord ? 'primary' : 'default')"
v-for="hotWord in hotWords"
:key="hotWord"
@click="handleHotWordClick(hotWord)"
<el-button
round
class="btn"
:type="selectHotWord === hotWord ? 'primary' : 'default'"
v-for="hotWord in ImageHotWords"
:key="hotWord"
@click="handleHotWordClick(hotWord)"
>
{{ hotWord }}
</el-button>
@ -35,17 +36,36 @@
<el-text tag="b">尺寸</el-text>
</div>
<el-space wrap class="size-list">
<div class="size-item"
v-for="imageSize in imageSizeList"
:key="imageSize.key"
@click="handleSizeClick(imageSize)">
<div :class="selectImageSize === imageSize.key ? 'size-wrapper selectImageSize' : 'size-wrapper'">
<div
class="size-item"
v-for="imageSize in MidjourneySizeList"
:key="imageSize.key"
@click="handleSizeClick(imageSize)"
>
<div
:class="selectSize === imageSize.key ? 'size-wrapper selectImageSize' : 'size-wrapper'"
>
<div :style="imageSize.style"></div>
</div>
<div class="size-font">{{ imageSize.key }}</div>
</div>
</el-space>
</div>
<div class="model">
<div>
<el-text tag="b">模型</el-text>
</div>
<el-space wrap class="model-list">
<div
:class="selectModel === model.key ? 'modal-item selectModel' : 'modal-item'"
v-for="model in MidjourneyModels"
:key="model.key"
>
<el-image :src="model.image" fit="contain" @click="handleModelClick(model)" />
<div class="model-font">{{ model.name }}</div>
</div>
</el-space>
</div>
<div class="version">
<div>
<el-text tag="b">版本</el-text>
@ -53,11 +73,9 @@
<el-space wrap class="version-list">
<el-select
v-model="selectVersion"
class="version-select"
class="version-select !w-350px"
clearable
placeholder="请选择版本"
style="width: 350px"
@change="handleChangeVersion"
>
<el-option
v-for="item in versionList"
@ -68,228 +86,130 @@
</el-select>
</el-space>
</div>
<div class="model">
<div>
<el-text tag="b">模型</el-text>
</div>
<el-space wrap class="model-list">
<div
:class="selectModel === model.key ? 'modal-item selectModel' : 'modal-item'"
v-for="model in models"
:key="model.key"
>
<el-image
:src="model.image"
fit="contain"
@click="handleModelClick(model)"
/>
<div class="model-font">{{model.name}}</div>
</div>
</el-space>
</div>
<div class="model">
<div>
<el-text tag="b">参考图</el-text>
</div>
<el-space wrap class="model-list">
<UploadImg v-model="referImage" height="80px" width="80px" />
<UploadImg v-model="referImageUrl" height="120px" width="120px" />
</el-space>
</div>
<div class="btns">
<!-- <el-button size="large" round>重置内容</el-button>-->
<el-button type="primary" size="large" round @click="handleGenerateImage">生成内容</el-button>
<el-button type="primary" size="large" round @click="handleGenerateImage">
{{ drawIn ? '生成中' : '生成内容' }}
</el-button>
</div>
</template>
<script setup lang="ts">
import { ImageApi, ImageMidjourneyImagineReqVO, ImageVO } from '@/api/ai/image'
import {
AiPlatformEnum,
ImageHotWords,
ImageSizeVO,
ImageModelVO,
MidjourneyModels,
MidjourneySizeList,
MidjourneyVersions,
NijiVersionList
} from '@/views/ai/utils/constants'
// image
import {ImageApi, ImageMidjourneyImagineReqVO, ImageVO} from "@/api/ai/image";
// message
const message = useMessage()
// emits
const emits = defineEmits(['onDrawStart', 'onDrawComplete'])
interface ImageModelVO {
key: string
name: string
image: string
}
// image
interface ImageSizeVO {
key: string
style: string,
width: string,
height: string,
}
const message = useMessage() //
//
const prompt = ref<string>('') //
const referImage = ref<any>() //
const drawIn = ref<boolean>(false) //
const selectHotWord = ref<string>('') //
const hotWords = ref<string[]>(['中国旗袍', '古装美女', '卡通头像', '机甲战士', '童话小屋', '中国长城']) //
const selectModel = ref<string>('midjourney') //
const models = ref<ImageModelVO[]>([
{
key: 'midjourney',
name: 'MJ',
image: 'https://bigpt8.com/pc/_nuxt/mj.34a61377.png',
},
{
key: 'niji',
name: 'NIJI',
image: 'https://bigpt8.com/pc/_nuxt/nj.ca79b143.png',
},
]) //
const selectImageSize = ref<string>('1:1') // size
const imageSizeList = ref<ImageSizeVO[]>([
{
key: '1:1',
width: "1",
height: "1",
style: 'width: 30px; height: 30px;background-color: #dcdcdc;',
},
{
key: '3:4',
width: "3",
height: "4",
style: 'width: 30px; height: 40px;background-color: #dcdcdc;',
},
{
key: '4:3',
width: "4",
height: "3",
style: 'width: 40px; height: 30px;background-color: #dcdcdc;',
},
{
key: '9:16',
width: "9",
height: "16",
style: 'width: 30px; height: 50px;background-color: #dcdcdc;',
},
{
key: '16:9',
width: "16",
height: "9",
style: 'width: 50px; height: 30px;background-color: #dcdcdc;',
},
]) // size
// version
const midjourneyVersionList = ref<any>([
{
value: '6.0',
label: 'v6.0',
},
{
value: '5.2',
label: 'v5.2',
},
{
value: '5.1',
label: 'v5.1',
},
{
value: '5.0',
label: 'v5.0',
},
{
value: '4.0',
label: 'v4.0',
},
])
const nijiVersionList = ref<any>([
{
value: '5',
label: 'v5',
},
])
//
const prompt = ref<string>('') //
const referImageUrl = ref<any>() //
const selectModel = ref<string>('midjourney') //
const selectSize = ref<string>('1:1') // size
const selectVersion = ref<any>('6.0') // version
let versionList = ref<any>([]) // version
versionList.value = midjourneyVersionList.value // midjourney
const versionList = ref<any>(MidjourneyVersions) // version
const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // emits
/** 热词 - click */
/** 选择热词 */
const handleHotWordClick = async (hotWord: string) => {
//
//
if (selectHotWord.value == hotWord) {
selectHotWord.value = ''
return
}
//
selectHotWord.value = hotWord
//
prompt.value = hotWord
//
selectHotWord.value = hotWord //
prompt.value = hotWord //
}
/** size - click */
/** 点击 size 尺寸 */
const handleSizeClick = async (imageSize: ImageSizeVO) => {
selectImageSize.value = imageSize.key
selectSize.value = imageSize.key
}
/** 模型 - click */
/** 点击 model 模型 */
const handleModelClick = async (model: ImageModelVO) => {
selectModel.value = model.key
if (model.key === 'niji') {
versionList.value = nijiVersionList.value // niji
versionList.value = NijiVersionList // niji
} else {
versionList.value = midjourneyVersionList.value // midjourney
versionList.value = MidjourneyVersions // midjourney
}
selectVersion.value = versionList.value[0].value
}
/** version - click */
const handleChangeVersion = async (version) => {
console.log('version', version)
}
/** 图片生产 */
/** 图片生成 */
const handleGenerateImage = async () => {
//
await message.confirm(`确认生成内容?`)
// todo @
try {
//
drawIn.value = true
//
emits('onDrawStart', selectModel.value)
emits('onDrawStart', AiPlatformEnum.MIDJOURNEY)
//
const imageSize = imageSizeList.value.find(item => selectImageSize.value === item.key) as ImageSizeVO
const imageSize = MidjourneySizeList.find(
(item) => selectSize.value === item.key
) as ImageSizeVO
const req = {
prompt: prompt.value,
model: selectModel.value,
width: imageSize.width,
height: imageSize.height,
version: selectVersion.value,
referImageUrl: referImage.value,
referImageUrl: referImageUrl.value
} as ImageMidjourneyImagineReqVO
await ImageApi.midjourneyImagine(req)
} finally {
//
emits('onDrawComplete', selectModel.value)
emits('onDrawComplete', AiPlatformEnum.MIDJOURNEY)
//
drawIn.value = false
}
}
/** 填充值 */
const settingValues = async (imageDetail: ImageVO) => {
const settingValues = async (detail: ImageVO) => {
//
prompt.value = imageDetail.prompt
prompt.value = detail.prompt
// image size
const imageSize = imageSizeList.value.find(item => item.key === `${imageDetail.width}:${imageDetail.height}`) as ImageSizeVO
selectImageSize.value = imageSize.key
const imageSize = MidjourneySizeList.find(
(item) => item.key === `${detail.width}:${detail.height}`
) as ImageSizeVO
selectSize.value = imageSize.key
//
const model = models.value.find(item => item.key === imageDetail.options?.model) as ImageModelVO
const model = MidjourneyModels.find((item) => item.key === detail.options?.model) as ImageModelVO
await handleModelClick(model)
//
selectVersion.value = versionList.value.find(item => item.value === imageDetail.options?.version).value
selectVersion.value = versionList.value.find(
(item) => item.value === detail.options?.version
).value
// image
referImage.value = imageDetail.options.referImageUrl
referImageUrl.value = detail.options.referImageUrl
}
/** 暴露组件方法 */
defineExpose({ settingValues })
</script>
<style scoped lang="scss">
//
.prompt {
}
@ -354,7 +274,6 @@ defineExpose({ settingValues })
}
}
//
.image-size {
width: 100%;

View File

@ -0,0 +1,272 @@
<!-- dall3 -->
<template>
<div class="prompt">
<el-text tag="b">画面描述</el-text>
<el-text tag="p">建议使用形容词+动词+风格的格式使用隔开</el-text>
<el-input
v-model="prompt"
maxlength="1024"
rows="5"
class="w-100% mt-15px"
input-style="border-radius: 7px;"
placeholder="例如:童话里的小屋应该是什么样子?"
show-word-limit
type="textarea"
/>
</div>
<div class="hot-words">
<div>
<el-text tag="b">随机热词</el-text>
</div>
<el-space wrap class="word-list">
<el-button
round
class="btn"
:type="selectHotWord === hotWord ? 'primary' : 'default'"
v-for="hotWord in ImageHotEnglishWords"
:key="hotWord"
@click="handleHotWordClick(hotWord)"
>
{{ hotWord }}
</el-button>
</el-space>
</div>
<div class="group-item">
<div>
<el-text tag="b">采样方法</el-text>
</div>
<el-space wrap class="group-item-body">
<el-select v-model="sampler" placeholder="Select" size="large" class="!w-350px">
<el-option
v-for="item in StableDiffusionSamplers"
:key="item.key"
:label="item.name"
:value="item.key"
/>
</el-select>
</el-space>
</div>
<div class="group-item">
<div>
<el-text tag="b">CLIP</el-text>
</div>
<el-space wrap class="group-item-body">
<el-select v-model="clipGuidancePreset" placeholder="Select" size="large" class="!w-350px">
<el-option
v-for="item in StableDiffusionClipGuidancePresets"
:key="item.key"
:label="item.name"
:value="item.key"
/>
</el-select>
</el-space>
</div>
<div class="group-item">
<div>
<el-text tag="b">风格</el-text>
</div>
<el-space wrap class="group-item-body">
<el-select v-model="stylePreset" placeholder="Select" size="large" class="!w-350px">
<el-option
v-for="item in StableDiffusionStylePresets"
:key="item.key"
:label="item.name"
:value="item.key"
/>
</el-select>
</el-space>
</div>
<div class="group-item">
<div>
<el-text tag="b">图片尺寸</el-text>
</div>
<el-space wrap class="group-item-body">
<el-input v-model="width" class="w-170px" placeholder="图片宽度" />
<el-input v-model="height" class="w-170px" placeholder="图片高度" />
</el-space>
</div>
<div class="group-item">
<div>
<el-text tag="b">迭代步数</el-text>
</div>
<el-space wrap class="group-item-body">
<el-input
v-model="steps"
type="number"
size="large"
class="!w-350px"
placeholder="Please input"
/>
</el-space>
</div>
<div class="group-item">
<div>
<el-text tag="b">引导系数</el-text>
</div>
<el-space wrap class="group-item-body">
<el-input
v-model="scale"
type="number"
size="large"
class="!w-350px"
placeholder="Please input"
/>
</el-space>
</div>
<div class="group-item">
<div>
<el-text tag="b">随机因子</el-text>
</div>
<el-space wrap class="group-item-body">
<el-input
v-model="seed"
type="number"
size="large"
class="!w-350px"
placeholder="Please input"
/>
</el-space>
</div>
<div class="btns">
<el-button type="primary" size="large" round :loading="drawIn" @click="handleGenerateImage">
{{ drawIn ? '生成中' : '生成内容' }}
</el-button>
</div>
</template>
<script setup lang="ts">
import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image'
import { hasChinese } from '@/views/ai/utils/utils'
import {
AiPlatformEnum,
ImageHotEnglishWords,
StableDiffusionClipGuidancePresets,
StableDiffusionSamplers,
StableDiffusionStylePresets
} from '@/views/ai/utils/constants'
const message = useMessage() //
//
const drawIn = ref<boolean>(false) //
const selectHotWord = ref<string>('') //
//
const prompt = ref<string>('') //
const width = ref<number>(512) //
const height = ref<number>(512) //
const sampler = ref<string>('DDIM') //
const steps = ref<number>(20) //
const seed = ref<number>(42) //
const scale = ref<number>(7.5) //
const clipGuidancePreset = ref<string>('NONE') // (clip_guidance_preset) CLIP
const stylePreset = ref<string>('3d-model') //
const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // emits
/** 选择热词 */
const handleHotWordClick = async (hotWord: string) => {
//
if (selectHotWord.value == hotWord) {
selectHotWord.value = ''
return
}
//
selectHotWord.value = hotWord //
prompt.value = hotWord //
}
/** 图片生成 */
const handleGenerateImage = async () => {
//
if (hasChinese(prompt.value)) {
message.alert('暂不支持中文!')
return
}
await message.confirm(`确认生成内容?`)
try {
//
drawIn.value = true
//
emits('onDrawStart', AiPlatformEnum.STABLE_DIFFUSION)
//
const form = {
platform: AiPlatformEnum.STABLE_DIFFUSION,
model: 'stable-diffusion-v1-6',
prompt: prompt.value, //
width: width.value, //
height: height.value, //
options: {
seed: seed.value, //
steps: steps.value, //
scale: scale.value, //
sampler: sampler.value, //
clipGuidancePreset: clipGuidancePreset.value, // CLIP
stylePreset: stylePreset.value //
}
} as ImageDrawReqVO
await ImageApi.drawImage(form)
} finally {
//
emits('onDrawComplete', AiPlatformEnum.STABLE_DIFFUSION)
//
drawIn.value = false
}
}
/** 填充值 */
const settingValues = async (detail: ImageVO) => {
prompt.value = detail.prompt
width.value = detail.width
height.value = detail.height
seed.value = detail.options?.seed
steps.value = detail.options?.steps
scale.value = detail.options?.scale
sampler.value = detail.options?.sampler
clipGuidancePreset.value = detail.options?.clipGuidancePreset
stylePreset.value = detail.options?.stylePreset
}
/** 暴露组件方法 */
defineExpose({ settingValues })
</script>
<style scoped lang="scss">
//
.prompt {
}
//
.hot-words {
display: flex;
flex-direction: column;
margin-top: 30px;
.word-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: start;
margin-top: 15px;
.btn {
margin: 0;
}
}
}
//
.group-item {
margin-top: 30px;
.group-item-body {
margin-top: 15px;
width: 100%;
}
}
.btns {
display: flex;
justify-content: center;
margin-top: 50px;
}
</style>

View File

@ -12,10 +12,7 @@
@on-draw-start="handleDrawStart"
@on-draw-complete="handleDrawComplete"
/>
<Midjourney
v-if="selectPlatform === AiPlatformEnum.MIDJOURNEY"
ref="midjourneyRef"
/>
<Midjourney v-if="selectPlatform === AiPlatformEnum.MIDJOURNEY" ref="midjourneyRef" />
<StableDiffusion
v-if="selectPlatform === AiPlatformEnum.STABLE_DIFFUSION"
ref="stableDiffusionRef"
@ -24,28 +21,26 @@
</div>
</div>
<div class="main">
<ImageTask ref="imageTaskRef" @on-regeneration="handleRegeneration" />
<ImageList ref="imageListRef" @on-regeneration="handleRegeneration" />
</div>
</div>
</template>
<script setup lang="ts">
// TODO @fan /views/ai/image/index /views/ai/image/manager
import Dall3 from './dall3/index.vue'
import Midjourney from './midjourney/index.vue'
import StableDiffusion from './stable-diffusion/index.vue'
import ImageTask from './ImageTask.vue'
import ImageList from './components/ImageList.vue'
import { AiPlatformEnum } from '@/views/ai/utils/constants'
import {ImageVO} from "@/api/ai/image";
import { ImageVO } from '@/api/ai/image'
import Dall3 from './components/dall3/index.vue'
import Midjourney from './components/midjourney/index.vue'
import StableDiffusion from './components/stableDiffusion/index.vue'
const imageTaskRef = ref<any>() // image task ref
const dall3Ref = ref<any>() // openai ref
const imageListRef = ref<any>() // image ref
const dall3Ref = ref<any>() // dall3(openai) ref
const midjourneyRef = ref<any>() // midjourney ref
const stableDiffusionRef = ref<any>() // stable diffusion ref
//
const selectPlatform = ref('StableDiffusion')
const selectPlatform = ref(AiPlatformEnum.MIDJOURNEY)
const platformOptions = [
{
label: 'DALL3 绘画',
@ -61,35 +56,27 @@ const platformOptions = [
}
]
/** 绘画 - start */
const handleDrawStart = async (type) => {
/** 绘画 start */
const handleDrawStart = async (platform: string) => {}
/** 绘画 complete */
const handleDrawComplete = async (platform: string) => {
await imageListRef.value.getImageList()
}
/** 绘画 - complete */
const handleDrawComplete = async (type) => {
await imageTaskRef.value.getImageList()
}
/** 绘画 - 重新生成 */
const handleRegeneration = async (imageDetail: ImageVO) => {
/** 重新生成:将画图详情填充到对应平台 */
const handleRegeneration = async (image: ImageVO) => {
//
selectPlatform.value = imageDetail.platform
console.log('切换平台', imageDetail.platform)
// imageDetail
if (imageDetail.platform === AiPlatformEnum.MIDJOURNEY) {
await nextTick(async () => {
midjourneyRef.value.settingValues(imageDetail)
})
} else if (imageDetail.platform === AiPlatformEnum.OPENAI) {
await nextTick(async () => {
dall3Ref.value.settingValues(imageDetail)
})
} else if (imageDetail.platform === AiPlatformEnum.STABLE_DIFFUSION) {
await nextTick(async () => {
stableDiffusionRef.value.settingValues(imageDetail)
})
selectPlatform.value = image.platform
// image
await nextTick()
if (image.platform === AiPlatformEnum.MIDJOURNEY) {
midjourneyRef.value.settingValues(image)
} else if (image.platform === AiPlatformEnum.OPENAI) {
dall3Ref.value.settingValues(image)
} else if (image.platform === AiPlatformEnum.STABLE_DIFFUSION) {
stableDiffusionRef.value.settingValues(image)
}
}
</script>

View File

@ -1,437 +0,0 @@
<!-- dall3 -->
<template>
<div class="prompt">
<el-text tag="b">画面描述</el-text>
<el-text tag="p">建议使用形容词+动词+风格的格式使用隔开</el-text>
<!-- TODO @fanstyle 看看能不能哟 unocss 替代 -->
<el-input
v-model="prompt"
maxlength="1024"
rows="5"
style="width: 100%; margin-top: 15px"
input-style="border-radius: 7px;"
placeholder="例如:童话里的小屋应该是什么样子?"
show-word-limit
type="textarea"
/>
</div>
<div class="hot-words">
<div>
<el-text tag="b">随机热词</el-text>
</div>
<el-space wrap class="word-list">
<el-button
round
class="btn"
:type="selectHotWord === hotWord ? 'primary' : 'default'"
v-for="hotWord in hotWords"
:key="hotWord"
@click="handleHotWordClick(hotWord)"
>
{{ hotWord }}
</el-button>
</el-space>
</div>
<div class="group-item">
<div>
<el-text tag="b">采样方法</el-text>
</div>
<el-space wrap class="group-item-body">
<el-select v-model="selectSampler" placeholder="Select" size="large" style="width: 350px">
<el-option v-for="item in sampler" :key="item.key" :label="item.name" :value="item.key" />
</el-select>
</el-space>
</div>
<div class="group-item">
<div>
<el-text tag="b">CLIP</el-text>
</div>
<el-space wrap class="group-item-body">
<el-select
v-model="selectClipGuidancePreset"
placeholder="Select"
size="large"
style="width: 350px"
>
<el-option
v-for="item in clipGuidancePresets"
:key="item.key"
:label="item.name"
:value="item.key"
/>
</el-select>
</el-space>
</div>
<div class="group-item">
<div>
<el-text tag="b">风格</el-text>
</div>
<el-space wrap class="group-item-body">
<el-select v-model="selectStylePreset" placeholder="Select" size="large" style="width: 350px">
<el-option
v-for="item in stylePresets"
:key="item.key"
:label="item.name"
:value="item.key"
/>
</el-select>
</el-space>
</div>
<div class="group-item">
<div>
<el-text tag="b">图片尺寸</el-text>
</div>
<el-space wrap class="group-item-body">
<el-input v-model="imageWidth" style="width: 170px" placeholder="图片宽度" />
<el-input v-model="imageHeight" style="width: 170px" placeholder="图片高度" />
</el-space>
</div>
<div class="group-item">
<div>
<el-text tag="b">迭代步数</el-text>
</div>
<el-space wrap class="group-item-body">
<el-input
v-model="steps"
type="number"
size="large"
style="width: 350px"
placeholder="Please input"
/>
</el-space>
</div>
<div class="group-item">
<div>
<el-text tag="b">引导系数</el-text>
</div>
<el-space wrap class="group-item-body">
<el-input
v-model="scale"
type="number"
size="large"
style="width: 350px"
placeholder="Please input"
/>
</el-space>
</div>
<div class="group-item">
<div>
<el-text tag="b">随机因子</el-text>
</div>
<el-space wrap class="group-item-body">
<el-input
v-model="seed"
type="number"
size="large"
style="width: 350px"
placeholder="Please input"
/>
</el-space>
</div>
<div class="btns">
<el-button type="primary" size="large" round :loading="drawIn" @click="handleGenerateImage">
{{ drawIn ? '生成中' : '生成内容' }}
</el-button>
</div>
</template>
<script setup lang="ts">
import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image'
import { hasChinese } from '@/views/ai/utils/utils'
// image
interface ImageModelVO {
key: string
name: string
}
//
const prompt = ref<string>('') //
const drawIn = ref<boolean>(false) //
const selectHotWord = ref<string>('') //
const imageWidth = ref<number>(512) //
const imageHeight = ref<number>(512) //
const hotWords = ref<string[]>([
'中国旗袍',
'古装美女',
'卡通头像',
'机甲战士',
'童话小屋',
'中国长城'
]) //
// message
const message = useMessage()
//
const selectSampler = ref<string>('DDIM') //
// DDIM DDPM K_DPMPP_2M K_DPMPP_2S_ANCESTRAL K_DPM_2 K_DPM_2_ANCESTRAL K_EULER K_EULER_ANCESTRAL K_HEUN K_LMS
const sampler = ref<ImageModelVO[]>([
{
key: 'DDIM',
name: 'DDIM'
},
{
key: 'DDPM',
name: 'DDPM'
},
{
key: 'K_DPMPP_2M',
name: 'K_DPMPP_2M'
},
{
key: 'K_DPMPP_2S_ANCESTRAL',
name: 'K_DPMPP_2S_ANCESTRAL'
},
{
key: 'K_DPM_2',
name: 'K_DPM_2'
},
{
key: 'K_DPM_2_ANCESTRAL',
name: 'K_DPM_2_ANCESTRAL'
},
{
key: 'K_EULER',
name: 'K_EULER'
},
{
key: 'K_EULER_ANCESTRAL',
name: 'K_EULER_ANCESTRAL'
},
{
key: 'K_HEUN',
name: 'K_HEUN'
},
{
key: 'K_LMS',
name: 'K_LMS'
}
])
//
// 3d-model analog-film anime cinematic comic-book digital-art enhance fantasy-art isometric
// line-art low-poly modeling-compound neon-punk origami photographic pixel-art tile-texture
const selectStylePreset = ref<string>('3d-model') //
const stylePresets = ref<ImageModelVO[]>([
{
key: '3d-model',
name: '3d-model'
},
{
key: 'analog-film',
name: 'analog-film'
},
{
key: 'anime',
name: 'anime'
},
{
key: 'cinematic',
name: 'cinematic'
},
{
key: 'comic-book',
name: 'comic-book'
},
{
key: 'digital-art',
name: 'digital-art'
},
{
key: 'enhance',
name: 'enhance'
},
{
key: 'fantasy-art',
name: 'fantasy-art'
},
{
key: 'isometric',
name: 'isometric'
},
{
key: 'line-art',
name: 'line-art'
},
{
key: 'low-poly',
name: 'low-poly'
},
{
key: 'modeling-compound',
name: 'modeling-compound'
},
// neon-punk origami photographic pixel-art tile-texture
{
key: 'neon-punk',
name: 'neon-punk'
},
{
key: 'origami',
name: 'origami'
},
{
key: 'photographic',
name: 'photographic'
},
{
key: 'pixel-art',
name: 'pixel-art'
},
{
key: 'tile-texture',
name: 'tile-texture'
}
])
// (clip_guidance_preset) CLIP
// https://platform.stability.ai/docs/api-reference#tag/SDXL-and-SD1.6/operation/textToImage
// FAST_BLUE FAST_GREEN NONE SIMPLE SLOW SLOWER SLOWEST
const selectClipGuidancePreset = ref<string>('NONE') //
const clipGuidancePresets = ref<ImageModelVO[]>([
{
key: 'NONE',
name: 'NONE'
},
{
key: 'FAST_BLUE',
name: 'FAST_BLUE'
},
{
key: 'FAST_GREEN',
name: 'FAST_GREEN'
},
{
key: 'SIMPLE',
name: 'SIMPLE'
},
{
key: 'SLOW',
name: 'SLOW'
},
{
key: 'SLOWER',
name: 'SLOWER'
},
{
key: 'SLOWEST',
name: 'SLOWEST'
}
])
const steps = ref<number>(20) //
const seed = ref<number>(42) //
const scale = ref<number>(7.5) //
// Props
const props = defineProps({})
// emits
const emits = defineEmits(['onDrawStart', 'onDrawComplete'])
/** 热词 - click */
const handleHotWordClick = async (hotWord: string) => {
//
if (selectHotWord.value == hotWord) {
selectHotWord.value = ''
return
}
//
selectHotWord.value = hotWord
//
prompt.value = hotWord
}
/** 图片生产 */
const handleGenerateImage = async () => {
//
await message.confirm(`确认生成内容?`)
if (await hasChinese(prompt.value)) {
message.alert('暂不支持中文!')
return
}
try {
//
drawIn.value = true
//
emits('onDrawStart', 'StableDiffusion')
//
const form = {
platform: 'StableDiffusion',
model: 'stable-diffusion-v1-6',
prompt: prompt.value, //
width: imageWidth.value, //
height: imageHeight.value, //
options: {
seed: seed.value, //
steps: steps.value, //
scale: scale.value, //
sampler: selectSampler.value, //
clipGuidancePreset: selectClipGuidancePreset.value, // CLIP
stylePreset: selectStylePreset.value //
}
} as ImageDrawReqVO
await ImageApi.drawImage(form)
} finally {
//
emits('onDrawComplete', 'StableDiffusion')
//
drawIn.value = false
}
}
/** 填充值 */
const settingValues = async (imageDetail: ImageVO) => {
prompt.value = imageDetail.prompt
imageWidth.value = imageDetail.width
imageHeight.value = imageDetail.height
seed.value = imageDetail.options?.seed
steps.value = imageDetail.options?.steps
scale.value = imageDetail.options?.scale
selectSampler.value = imageDetail.options?.sampler
selectClipGuidancePreset.value = imageDetail.options?.clipGuidancePreset
selectStylePreset.value = imageDetail.options?.stylePreset
}
/** 暴露组件方法 */
defineExpose({ settingValues })
</script>
<style scoped lang="scss">
//
.prompt {
}
//
.hot-words {
display: flex;
flex-direction: column;
margin-top: 30px;
.word-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: start;
margin-top: 15px;
.btn {
margin: 0;
}
}
}
//
.group-item {
margin-top: 30px;
.group-item-body {
margin-top: 15px;
width: 100%;
}
}
.btns {
display: flex;
justify-content: center;
margin-top: 50px;
}
</style>

View File

@ -13,7 +13,7 @@
<el-form-item label="角色头像" prop="avatar">
<UploadImg v-model="formData.avatar" height="60px" width="60px" />
</el-form-item>
<el-form-item label="绑定模型" prop="modelId" v-if="!isUser(formType)">
<el-form-item label="绑定模型" prop="modelId" v-if="!isUser">
<el-select v-model="formData.modelId" placeholder="请选择模型" clearable>
<el-option
v-for="chatModel in chatModelList"
@ -23,7 +23,7 @@
/>
</el-select>
</el-form-item>
<el-form-item label="角色类别" prop="category" v-if="!isUser(formType)">
<el-form-item label="角色类别" prop="category" v-if="!isUser">
<el-input v-model="formData.category" placeholder="请输入角色类别" />
</el-form-item>
<el-form-item label="角色描述" prop="description">
@ -32,7 +32,7 @@
<el-form-item label="角色设定" prop="systemMessage">
<el-input type="textarea" v-model="formData.systemMessage" placeholder="请输入角色设定" />
</el-form-item>
<el-form-item label="是否公开" prop="publicStatus" v-if="!isUser(formType)">
<el-form-item label="是否公开" prop="publicStatus" v-if="!isUser">
<el-radio-group v-model="formData.publicStatus">
<el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
@ -43,10 +43,10 @@
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="角色排序" prop="sort" v-if="!isUser(formType)">
<el-form-item label="角色排序" prop="sort" v-if="!isUser">
<el-input-number v-model="formData.sort" placeholder="请输入角色排序" class="!w-1/1" />
</el-form-item>
<el-form-item label="开启状态" prop="status" v-if="!isUser(formType)">
<el-form-item label="开启状态" prop="status" v-if="!isUser">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
@ -97,10 +97,9 @@ const formRef = ref() // 表单 Ref
const chatModelList = ref([] as ChatModelVO[]) //
/** 是否【我】自己创建,私有角色 */
// TODO @fan computed
const isUser = (type: string) => {
return type === 'my-create' || type === 'my-update'
}
const isUser = computed(() => {
return formType.value === 'my-create' || formType.value === 'my-update'
})
// TODO @fan使 formRules
const getFormRules = async (type: string) => {

View File

@ -48,3 +48,308 @@ export enum AiWriteTypeEnum {
WRITING = 1, // 撰写
REPLY // 回复
}
// ========== 【图片 UI】相关的枚举 ==========
export const ImageHotWords = [
'中国旗袍',
'古装美女',
'卡通头像',
'机甲战士',
'童话小屋',
'中国长城'
] // 图片热词
export const ImageHotEnglishWords = [
'Chinese Cheongsam',
'Ancient Beauty',
'Cartoon Avatar',
'Mech Warrior',
'Fairy Tale Cottage',
'The Great Wall of China'
] // 图片热词(英文)
export interface ImageModelVO {
key: string
name: string
image?: string
}
export const StableDiffusionSamplers: ImageModelVO[] = [
{
key: 'DDIM',
name: 'DDIM'
},
{
key: 'DDPM',
name: 'DDPM'
},
{
key: 'K_DPMPP_2M',
name: 'K_DPMPP_2M'
},
{
key: 'K_DPMPP_2S_ANCESTRAL',
name: 'K_DPMPP_2S_ANCESTRAL'
},
{
key: 'K_DPM_2',
name: 'K_DPM_2'
},
{
key: 'K_DPM_2_ANCESTRAL',
name: 'K_DPM_2_ANCESTRAL'
},
{
key: 'K_EULER',
name: 'K_EULER'
},
{
key: 'K_EULER_ANCESTRAL',
name: 'K_EULER_ANCESTRAL'
},
{
key: 'K_HEUN',
name: 'K_HEUN'
},
{
key: 'K_LMS',
name: 'K_LMS'
}
]
export const StableDiffusionStylePresets: ImageModelVO[] = [
{
key: '3d-model',
name: '3d-model'
},
{
key: 'analog-film',
name: 'analog-film'
},
{
key: 'anime',
name: 'anime'
},
{
key: 'cinematic',
name: 'cinematic'
},
{
key: 'comic-book',
name: 'comic-book'
},
{
key: 'digital-art',
name: 'digital-art'
},
{
key: 'enhance',
name: 'enhance'
},
{
key: 'fantasy-art',
name: 'fantasy-art'
},
{
key: 'isometric',
name: 'isometric'
},
{
key: 'line-art',
name: 'line-art'
},
{
key: 'low-poly',
name: 'low-poly'
},
{
key: 'modeling-compound',
name: 'modeling-compound'
},
// neon-punk origami photographic pixel-art tile-texture
{
key: 'neon-punk',
name: 'neon-punk'
},
{
key: 'origami',
name: 'origami'
},
{
key: 'photographic',
name: 'photographic'
},
{
key: 'pixel-art',
name: 'pixel-art'
},
{
key: 'tile-texture',
name: 'tile-texture'
}
]
export const StableDiffusionClipGuidancePresets: ImageModelVO[] = [
{
key: 'NONE',
name: 'NONE'
},
{
key: 'FAST_BLUE',
name: 'FAST_BLUE'
},
{
key: 'FAST_GREEN',
name: 'FAST_GREEN'
},
{
key: 'SIMPLE',
name: 'SIMPLE'
},
{
key: 'SLOW',
name: 'SLOW'
},
{
key: 'SLOWER',
name: 'SLOWER'
},
{
key: 'SLOWEST',
name: 'SLOWEST'
}
]
export const Dall3Models: ImageModelVO[] = [
{
key: 'dall-e-3',
name: 'DALL·E 3',
image: `/src/assets/ai/dall2.jpg`
},
{
key: 'dall-e-2',
name: 'DALL·E 2',
image: `/src/assets/ai/dall3.jpg`
}
]
export const Dall3StyleList: ImageModelVO[] = [
{
key: 'vivid',
name: '清晰',
image: `/src/assets/ai/qingxi.jpg`
},
{
key: 'natural',
name: '自然',
image: `/src/assets/ai/ziran.jpg`
}
]
export interface ImageSizeVO {
key: string
name: string
style: string
width: string
height: string
}
export const Dall3SizeList: ImageSizeVO[] = [
{
key: '1024x1024',
name: '1:1',
width: '1024',
height: '1024',
style: 'width: 30px; height: 30px;background-color: #dcdcdc;'
},
{
key: '1024x1792',
name: '3:5',
width: '1024',
height: '1792',
style: 'width: 30px; height: 50px;background-color: #dcdcdc;'
},
{
key: '1792x1024',
name: '5:3',
width: '1792',
height: '1024',
style: 'width: 50px; height: 30px;background-color: #dcdcdc;'
}
]
export const MidjourneyModels: ImageModelVO[] = [
{
key: 'midjourney',
name: 'MJ',
image: 'https://bigpt8.com/pc/_nuxt/mj.34a61377.png'
},
{
key: 'niji',
name: 'NIJI',
image: 'https://bigpt8.com/pc/_nuxt/nj.ca79b143.png'
}
]
export const MidjourneySizeList: ImageSizeVO[] = [
{
key: '1:1',
width: '1',
height: '1',
style: 'width: 30px; height: 30px;background-color: #dcdcdc;'
},
{
key: '3:4',
width: '3',
height: '4',
style: 'width: 30px; height: 40px;background-color: #dcdcdc;'
},
{
key: '4:3',
width: '4',
height: '3',
style: 'width: 40px; height: 30px;background-color: #dcdcdc;'
},
{
key: '9:16',
width: '9',
height: '16',
style: 'width: 30px; height: 50px;background-color: #dcdcdc;'
},
{
key: '16:9',
width: '16',
height: '9',
style: 'width: 50px; height: 30px;background-color: #dcdcdc;'
}
]
export const MidjourneyVersions = [
{
value: '6.0',
label: 'v6.0'
},
{
value: '5.2',
label: 'v5.2'
},
{
value: '5.1',
label: 'v5.1'
},
{
value: '5.0',
label: 'v5.0'
},
{
value: '4.0',
label: 'v4.0'
}
]
export const NijiVersionList = [
{
value: '5',
label: 'v5'
}
]

View File

@ -8,7 +8,7 @@
*/
/** 判断字符串是否包含中文 */
export const hasChinese = async (str) => {
export const hasChinese = (str: string) => {
return /[\u4e00-\u9fa5]/.test(str)
}

View File

@ -126,7 +126,6 @@
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { CategoryApi, CategoryVO } from '@/api/bpm/category'
import CategoryForm from './CategoryForm.vue'

View File

@ -292,6 +292,7 @@ import { createImageViewer } from '@/components/ImageViewer'
import { RuleConfig } from '@/views/mall/product/spu/components/index'
import { PropertyAndValues } from './index'
import { ElTable } from 'element-plus'
import { isEmpty } from '@/utils/is'
defineOptions({ name: 'SkuList' })
const message = useMessage() //
@ -340,11 +341,22 @@ const imagePreview = (imgUrl: string) => {
/** 批量添加 */
const batchAdd = () => {
validateProperty()
formData.value!.skus!.forEach((item) => {
copyValueToTarget(item, skuList.value[0])
})
}
/** 校验商品属性属性值 */
const validateProperty = () => {
//
const warningInfo = '存在属性属性值为空,请先检查完善属性值后重试!!!'
for (const item of props.propertyList) {
if (!item.values || isEmpty(item.values)) {
message.warning(warningInfo)
throw new Error(warningInfo)
}
}
}
/** 删除 sku */
const deleteSku = (row) => {
const index = formData.value!.skus!.findIndex(
@ -358,6 +370,7 @@ const tableHeaders = ref<{ prop: string; label: string }[]>([]) // 多属性表
* 保存时每个商品规格的表单要校验下例如说销售金额最低是 0.01 这种
*/
const validateSku = () => {
validateProperty()
let warningInfo = '请检查商品各行相关属性配置,'
let validate = true //
for (const sku of formData.value!.skus!) {
@ -421,7 +434,7 @@ watch(
const generateTableData = (propertyList: any[]) => {
//
const propertyValues = propertyList.map((item) =>
item.values.map((v) => ({
item.values.map((v: any) => ({
propertyId: item.id,
propertyName: item.name,
valueId: v.id,
@ -464,15 +477,14 @@ const generateTableData = (propertyList: any[]) => {
*/
const validateData = (propertyList: any[]) => {
const skuPropertyIds: number[] = []
formData.value!.skus!.forEach(
(sku) =>
sku.properties
?.map((property) => property.propertyId)
?.forEach((propertyId) => {
if (skuPropertyIds.indexOf(propertyId!) === -1) {
skuPropertyIds.push(propertyId!)
}
})
formData.value!.skus!.forEach((sku) =>
sku.properties
?.map((property) => property.propertyId)
?.forEach((propertyId) => {
if (skuPropertyIds.indexOf(propertyId!) === -1) {
skuPropertyIds.push(propertyId!)
}
})
)
const propertyIds = propertyList.map((item) => item.id)
return skuPropertyIds.length === propertyIds.length
@ -543,7 +555,7 @@ watch(
return
}
//
if (propertyList.some((item) => item.values!.length === 0)) {
if (propertyList.some((item) => !item.values || isEmpty(item.values))) {
return
}
// table sku

View File

@ -3,7 +3,7 @@
<el-col v-for="(item, index) in attributeList" :key="index">
<div>
<el-text class="mx-1">属性名</el-text>
<el-tag class="mx-1" :closable="!isDetail" type="success" @close="handleCloseProperty(index)">
<el-tag :closable="!isDetail" class="mx-1" type="success" @close="handleCloseProperty(index)">
{{ item.name }}
</el-tag>
</div>
@ -12,8 +12,8 @@
<el-tag
v-for="(value, valueIndex) in item.values"
:key="value.id"
class="mx-1"
:closable="!isDetail"
class="mx-1"
@close="handleCloseValue(index, valueIndex)"
>
{{ value.name }}
@ -44,7 +44,6 @@
<script lang="ts" setup>
import { ElInput } from 'element-plus'
import * as PropertyApi from '@/api/mall/product/property'
import { PropertyVO } from '@/api/mall/product/property'
import { PropertyAndValues } from '@/views/mall/product/spu/components'
import { propTypes } from '@/utils/propTypes'
@ -59,9 +58,9 @@ const inputVisible = computed(() => (index: number) => {
if (attributeIndex.value === null) return false
if (attributeIndex.value === index) return true
})
const inputRef = ref([]) //Ref
const inputRef = ref<any[]>([]) //Ref
/** 解决 ref 在 v-for 中的获取问题*/
const setInputRef = (el) => {
const setInputRef = (el: any) => {
if (el === null || typeof el === 'undefined') return
// id
if (!inputRef.value.some((item) => item.input?.attributes.id === el.input?.attributes.id)) {
@ -81,7 +80,7 @@ watch(
() => props.propertyList,
(data) => {
if (!data) return
attributeList.value = data
attributeList.value = data as any
},
{
deep: true,
@ -97,6 +96,7 @@ const handleCloseValue = (index: number, valueIndex: number) => {
/** 删除属性*/
const handleCloseProperty = (index: number) => {
attributeList.value?.splice(index, 1)
emit('success', attributeList.value)
}
/** 显示输入框并获取焦点 */

View File

@ -1,18 +1,18 @@
<!-- 商品发布 - 库存价格 -->
<template>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" :disabled="isDetail">
<el-form ref="formRef" :disabled="isDetail" :model="formData" :rules="rules" label-width="120px">
<el-form-item label="分销类型" props="subCommissionType">
<el-radio-group
v-model="formData.subCommissionType"
@change="changeSubCommissionType"
class="w-80"
@change="changeSubCommissionType"
>
<el-radio :label="false">默认设置</el-radio>
<el-radio :label="true" class="radio">单独设置</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="商品规格" props="specType">
<el-radio-group v-model="formData.specType" @change="onChangeSpec" class="w-80">
<el-radio-group v-model="formData.specType" class="w-80" @change="onChangeSpec">
<el-radio :label="false" class="radio">单规格</el-radio>
<el-radio :label="true">多规格</el-radio>
</el-radio-group>
@ -29,22 +29,22 @@
<el-form-item v-if="formData.specType" label="商品属性">
<el-button class="mb-10px mr-15px" @click="attributesAddFormRef.open">添加属性</el-button>
<ProductAttributes
:is-detail="isDetail"
:property-list="propertyList"
@success="generateSkus"
:is-detail="isDetail"
/>
</el-form-item>
<template v-if="formData.specType && propertyList.length > 0">
<el-form-item label="批量设置" v-if="!isDetail">
<el-form-item v-if="!isDetail" label="批量设置">
<SkuList :is-batch="true" :prop-form-data="formData" :property-list="propertyList" />
</el-form-item>
<el-form-item label="规格列表">
<SkuList
ref="skuListRef"
:is-detail="isDetail"
:prop-form-data="formData"
:property-list="propertyList"
:rule-config="ruleConfig"
:is-detail="isDetail"
/>
</el-form-item>
</template>
@ -181,7 +181,7 @@ const onChangeSpec = () => {
}
/** 调用 SkuList generateTableData 方法*/
const generateSkus = (propertyList) => {
const generateSkus = (propertyList: any[]) => {
skuListRef.value.generateTableData(propertyList)
}
</script>

View File

@ -4,9 +4,16 @@
<div class="kefu-title">{{ keFuConversation.userNickname }}</div>
</el-header>
<el-main class="kefu-content" style="overflow: visible">
<el-scrollbar ref="scrollbarRef" always height="calc(100vh - 495px)">
<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">
<div ref="innerRef" class="w-[100%] pb-3px">
<div v-for="(item, index) in messageList" :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
@ -48,6 +55,10 @@
<TextMessageItem :message="item" />
<!-- 图片消息 -->
<ImageMessageItem :message="item" />
<!-- 商品消息 -->
<ProductMessageItem :message="item" />
<!-- 订单消息 -->
<OrderMessageItem :message="item" />
</div>
<el-avatar
v-if="item.senderType === UserTypeEnum.ADMIN"
@ -58,6 +69,14 @@
</div>
</div>
</el-scrollbar>
<div
v-show="showNewMessageTip"
class="newMessageTip flex items-center cursor-pointer"
@click="handleToNewMessage"
>
<span>有新消息</span>
<Icon class="ml-5px" icon="ep:bottom" />
</div>
</el-main>
<el-footer height="230px">
<div class="h-[100%]">
@ -86,6 +105,8 @@ import EmojiSelectPopover from './tools/EmojiSelectPopover.vue'
import PictureSelectUpload from './tools/PictureSelectUpload.vue'
import TextMessageItem from './message/TextMessageItem.vue'
import ImageMessageItem from './message/ImageMessageItem.vue'
import ProductMessageItem from './message/ProductMessageItem.vue'
import OrderMessageItem from './message/OrderMessageItem.vue'
import { Emoji } from './tools/emoji'
import { KeFuMessageContentTypeEnum } from './tools/constants'
import { isEmpty } from '@/utils/is'
@ -101,23 +122,47 @@ const messageTool = useMessage()
const message = ref('') //
const messageList = ref<KeFuMessageRespVO[]>([]) //
const keFuConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) //
// TODO puhui999:
const showNewMessageTip = ref(false) //
const queryParams = reactive({
pageNo: 1,
conversationId: 0
})
const total = ref(0) //
//
const getMessageList = async (conversation: KeFuConversationRespVO) => {
keFuConversation.value = conversation
const { list } = await KeFuMessageApi.getKeFuMessagePage({
pageNo: 1,
conversationId: conversation.id
})
messageList.value = list.reverse()
// TODO puhui999:
queryParams.conversationId = conversation.id
const messageTotal = messageList.value.length
if (total.value > 0 && messageTotal > 0 && messageTotal === total.value) {
return
}
const res = await KeFuMessageApi.getKeFuMessagePage(queryParams)
total.value = res.total
for (const item of res.list) {
if (messageList.value.some((val) => val.id === item.id)) {
continue
}
messageList.value.push(item)
}
await scrollToBottom()
}
const getMessageList0 = computed(() => {
messageList.value.sort((a: any, b: any) => a.createTime - b.createTime)
return messageList.value
})
//
const refreshMessageList = () => {
const refreshMessageList = async () => {
if (!keFuConversation.value) {
return
}
getMessageList(keFuConversation.value)
queryParams.pageNo = 1
await getMessageList(keFuConversation.value)
if (loadHistory.value) {
//
showNewMessageTip.value = true
}
}
defineExpose({ getMessageList, refreshMessageList })
//
@ -140,7 +185,7 @@ const handleSendPicture = async (picUrl: string) => {
const handleSendMessage = async () => {
// 1.
if (isEmpty(unref(message.value))) {
messageTool.warning('请输入消息后再发送哦!')
messageTool.notifyWarning('请输入消息后再发送哦!')
return
}
// 2.
@ -167,12 +212,41 @@ const innerRef = ref<HTMLDivElement>()
const scrollbarRef = ref<InstanceType<typeof ElScrollbarType>>()
//
const scrollToBottom = async () => {
// 1.
// 1.
if (loadHistory.value) {
return
}
// 2.1
await nextTick()
scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight)
// 2.
showNewMessageTip.value = false
// 2.2
await KeFuMessageApi.updateKeFuMessageReadStatus(keFuConversation.value.id)
}
//
const handleToNewMessage = async () => {
loadHistory.value = false
await scrollToBottom()
}
const loadingMore = ref(false) //
const loadHistory = ref(false) //
const handleScroll = async ({ scrollTop }) => {
const messageTotal = messageList.value.length
if (total.value > 0 && messageTotal > 0 && messageTotal === total.value) {
return
}
// 20
loadingMore.value = scrollTop < 20
}
const handleOldMessage = async () => {
loadHistory.value = true
//
queryParams.pageNo += 1
await getMessageList(keFuConversation.value)
loadingMore.value = false
// TODO puhui999:
}
/**
* 是否显示时间
* @param {*} item - 数据
@ -196,6 +270,32 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
}
&-content {
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 {
position: absolute;
bottom: 35px;
right: 35px;
background-color: #fff;
padding: 10px;
border-radius: 30px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 阴影效果 */
}
.ss-row-left {
justify-content: flex-start;

View File

@ -1,5 +1,6 @@
<template>
<div class="kefu">
<!-- TODO @puhui999item => conversation 会不会更容易理解 -->
<div
v-for="(item, index) in conversationList"
:key="item.id"
@ -9,7 +10,9 @@
@contextmenu.prevent="rightClick($event as PointerEvent, item)"
>
<div class="flex justify-center items-center w-100%">
<!-- TODO style 换成 unocss -->
<div class="flex justify-center items-center" style="width: 50px; height: 50px">
<!-- 头像 + 未读 -->
<el-badge
:hidden="item.adminUnreadMessageCount === 0"
:max="99"
@ -41,7 +44,8 @@
</div>
</div>
</div>
<!-- 通过右击获取到的坐标定位 -->
<!-- 右键进行操作类似微信 -->
<ul v-show="showRightMenu" :style="rightMenuStyle" class="right-menu-ul">
<li
v-show="!selectedConversation.adminPinned"
@ -74,45 +78,34 @@
<script lang="ts" setup>
import { KeFuConversationApi, KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
import { useEmoji } from './tools/emoji'
import { formatDate, getNowDateTime } from '@/utils/formatTime'
import { formatDate } from '@/utils/formatTime'
import { KeFuMessageContentTypeEnum } from './tools/constants'
defineOptions({ name: 'KeFuConversationBox' })
const message = useMessage()
const message = useMessage() //
const { replaceEmoji } = useEmoji()
const activeConversationIndex = ref(-1) //
const conversationList = ref<KeFuConversationRespVO[]>([]) //
const activeConversationIndex = ref(-1) // index TODO @puhui999 activeConversationId
/** 加载会话列表 */
const getConversationList = async () => {
conversationList.value = await KeFuConversationApi.getConversationList()
//
for (let i = 0; i < 5; i++) {
conversationList.value.push({
id: 1,
userId: 283,
userAvatar:
'https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKMezSxtOImrC9lbhwHiazYwck3xwrEcO7VJfG6WQo260whaeVNoByE5RreiaGsGfOMlIiaDhSaA991w/132',
userNickname: '辉辉鸭' + i,
lastMessageTime: getNowDateTime(),
lastMessageContent:
'[爱心][爱心]你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇',
lastMessageContentType: 1,
adminPinned: false,
userDeleted: false,
adminDeleted: false,
adminUnreadMessageCount: i
})
}
}
defineExpose({ getConversationList })
/** 打开右侧的消息列表 */
const emits = defineEmits<{
(e: 'change', v: KeFuConversationRespVO): void
}>()
//
const openRightMessage = (item: KeFuConversationRespVO, index: number) => {
activeConversationIndex.value = index
emits('change', item)
}
//
// TODO @puhui999 getConversationDisplayText replaceEmoji
/** 获得消息类型 */
const getContentType = computed(() => (lastMessageContentType: number) => {
switch (lastMessageContentType) {
case KeFuMessageContentTypeEnum.SYSTEM:
@ -135,8 +128,9 @@ const getContentType = computed(() => (lastMessageContentType: number) => {
//======================= =======================
const showRightMenu = ref(false) //
const rightMenuStyle = ref<any>({}) // Style
const selectedConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) //
//
const selectedConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // TODO puhui999 rightClickConversation selected
/** 打开右键菜单 */
const rightClick = (mouseEvent: PointerEvent, item: KeFuConversationRespVO) => {
selectedConversation.value = item
//
@ -146,24 +140,25 @@ const rightClick = (mouseEvent: PointerEvent, item: KeFuConversationRespVO) => {
left: mouseEvent.clientX - 80 + 'px'
}
}
//
/** 关闭右键菜单 */
const closeRightMenu = () => {
showRightMenu.value = false
}
//
/** 置顶会话 */
const updateConversationPinned = async (adminPinned: boolean) => {
// 1. /
await KeFuConversationApi.updateConversationPinned({
id: selectedConversation.value.id,
adminPinned
})
// TODO puhui999:
message.success(adminPinned ? '置顶成功' : '取消置顶成功')
message.notifySuccess(adminPinned ? '置顶成功' : '取消置顶成功')
// 2.
closeRightMenu()
await getConversationList()
}
//
/** 删除会话 */
const deleteConversation = async () => {
// 1.
await message.confirm('您确定要删除该会话吗?')
@ -172,6 +167,8 @@ const deleteConversation = async () => {
closeRightMenu()
await getConversationList()
}
/** 监听右键菜单的显示状态,添加点击事件监听器 */
watch(showRightMenu, (val) => {
if (val) {
document.body.addEventListener('click', closeRightMenu)

View File

@ -0,0 +1,182 @@
<template>
<!-- 图片消息 -->
<template v-if="KeFuMessageContentTypeEnum.ORDER === message.contentType">
<div
:class="[
message.senderType === UserTypeEnum.MEMBER
? `ml-10px`
: message.senderType === UserTypeEnum.ADMIN
? `mr-10px`
: ''
]"
>
<div :key="getMessageContent.id" class="order-list-card-box mt-14px">
<div class="order-card-header flex items-center justify-between p-x-20px">
<div class="order-no">订单号{{ getMessageContent.no }}</div>
<div :class="formatOrderColor(getMessageContent)" class="order-state font-26">
{{ formatOrderStatus(getMessageContent) }}
</div>
</div>
<div v-for="item in getMessageContent.items" :key="item.id" class="border-bottom">
<ProductItem
:img="item.picUrl"
:num="item.count"
:price="item.price"
:skuText="item.properties.map((property: any) => property.valueName).join(' ')"
:title="item.spuName"
/>
</div>
<div class="pay-box mt-30px flex justify-end pr-20px">
<div class="flex items-center">
<div class="discounts-title pay-color"
> {{ getMessageContent.productCount }} 件商品,总金额:
</div>
<div class="discounts-money pay-color">
{{ fenToYuan(getMessageContent.payPrice) }}
</div>
</div>
</div>
</div>
</div>
</template>
</template>
<script lang="ts" setup>
import { KeFuMessageContentTypeEnum } from '../tools/constants'
import ProductItem from './ProductItem.vue'
import { UserTypeEnum } from '@/utils/constants'
import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
import { fenToYuan } from '@/utils'
defineOptions({ name: 'OrderMessageItem' })
const props = defineProps<{
message: KeFuMessageRespVO
}>()
const getMessageContent = computed(() => JSON.parse(props.message.content))
/**
* 格式化订单状态的颜色
*
* @param order 订单
* @return {string} 颜色的 class 名称
*/
function formatOrderColor(order) {
if (order.status === 0) {
return 'info-color'
}
if (order.status === 10 || order.status === 20 || (order.status === 30 && !order.commentStatus)) {
return 'warning-color'
}
if (order.status === 30 && order.commentStatus) {
return 'success-color'
}
return 'danger-color'
}
/**
* 格式化订单状态
*
* @param order 订单
*/
function formatOrderStatus(order) {
if (order.status === 0) {
return '待付款'
}
if (order.status === 10 && order.deliveryType === 1) {
return '待发货'
}
if (order.status === 10 && order.deliveryType === 2) {
return '待核销'
}
if (order.status === 20) {
return '待收货'
}
if (order.status === 30 && !order.commentStatus) {
return '待评价'
}
if (order.status === 30 && order.commentStatus) {
return '已完成'
}
return '已关闭'
}
</script>
<style lang="scss" scoped>
.order-list-card-box {
border-radius: 10px;
padding: 10px;
background-color: #e2e2e2;
.order-card-header {
height: 80rpx;
.order-no {
font-size: 26rpx;
font-weight: 500;
}
}
.pay-box {
.discounts-title {
font-size: 24rpx;
line-height: normal;
color: #999999;
}
.discounts-money {
font-size: 24rpx;
line-height: normal;
color: #999;
font-family: OPPOSANS;
}
.pay-color {
color: #333;
}
}
.order-card-footer {
height: 100rpx;
.more-item-box {
padding: 20rpx;
.more-item {
height: 60rpx;
.title {
font-size: 26rpx;
}
}
}
.more-btn {
color: #999999;
font-size: 24rpx;
}
.content {
width: 154rpx;
color: #333333;
font-size: 26rpx;
font-weight: 500;
}
}
}
.warning-color {
color: #faad14;
}
.danger-color {
color: #ff3000;
}
.success-color {
color: #52c41a;
}
.info-color {
color: #999999;
}
</style>

View File

@ -0,0 +1,195 @@
<template>
<div>
<div>
<slot name="top"></slot>
</div>
<div
:style="[{ borderRadius: radius + 'px', marginBottom: marginBottom + 'px' }]"
class="ss-order-card-warp flex items-stretch justify-between bg-white"
>
<div class="img-box mr-24px">
<el-image :src="img" class="order-img" fit="contain" @click="imagePrediv(img)" />
</div>
<div
:style="[{ width: titleWidth ? titleWidth + 'px' : '' }]"
class="box-right flex flex-col justify-between"
>
<div v-if="title" class="title-text ss-line-2">{{ title }}</div>
<div v-if="skuString" class="spec-text mt-8px mb-12px">{{ skuString }}</div>
<div class="groupon-box">
<slot name="groupon"></slot>
</div>
<div class="flex">
<div class="flex items-center">
<div
v-if="price && Number(price) > 0"
:style="[{ color: priceColor }]"
class="price-text flex items-center"
>
{{ fenToYuan(price) }}
</div>
<div v-if="num" class="total-text flex items-center">x {{ num }}</div>
<slot name="priceSuffix"></slot>
</div>
</div>
<div class="tool-box">
<slot name="tool"></slot>
</div>
<div>
<slot name="rightBottom"></slot>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { createImageViewer } from '@/components/ImageViewer'
import { fenToYuan } from '@/utils'
defineOptions({ name: 'ProductItem' })
const props = defineProps({
img: {
type: String,
default: 'https://img1.baidu.com/it/u=1601695551,235775011&fm=26&fmt=auto'
},
title: {
type: String,
default: ''
},
titleWidth: {
type: Number,
default: 0
},
skuText: {
type: [String, Array],
default: ''
},
price: {
type: [String, Number],
default: ''
},
priceColor: {
type: [String],
default: ''
},
num: {
type: [String, Number],
default: 0
},
score: {
type: [String, Number],
default: ''
},
radius: {
type: [String],
default: ''
},
marginBottom: {
type: [String],
default: ''
}
})
const skuString = computed(() => {
if (!props.skuText) {
return ''
}
if (typeof props.skuText === 'object') {
return props.skuText.join(',')
}
return props.skuText
})
/** 图预览 */
const imagePrediv = (imgUrl: string) => {
createImageViewer({
urlList: [imgUrl]
})
}
</script>
<style lang="scss" scoped>
.score-img {
width: 36px;
height: 36px;
margin: 0 4px;
}
.ss-order-card-warp {
padding: 20px;
border-radius: 10px;
background-color: #e2e2e2;
.img-box {
width: 164px;
height: 164px;
border-radius: 10px;
overflow: hidden;
.order-img {
width: 164px;
height: 164px;
}
}
.box-right {
flex: 1;
// width: 500px;
// height: 164px;
position: relative;
.tool-box {
position: absolute;
right: 0px;
bottom: -10px;
}
}
.title-text {
font-size: 28px;
font-weight: 500;
line-height: 40px;
}
.spec-text {
font-size: 24px;
font-weight: 400;
color: #999999;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.price-text {
font-size: 24px;
font-weight: 500;
font-family: OPPOSANS;
}
.total-text {
font-size: 24px;
font-weight: 400;
line-height: 24px;
color: #999999;
margin-left: 8px;
}
}
.ss-line {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
&-1 {
-webkit-line-clamp: 1;
}
&-2 {
-webkit-line-clamp: 2;
}
}
</style>

View File

@ -0,0 +1,36 @@
<template>
<!-- 图片消息 -->
<template v-if="KeFuMessageContentTypeEnum.PRODUCT === message.contentType">
<div
:class="[
message.senderType === UserTypeEnum.MEMBER
? `ml-10px`
: message.senderType === UserTypeEnum.ADMIN
? `mr-10px`
: ''
]"
>
<ProductItem
:img="getMessageContent.picUrl"
:price="getMessageContent.price"
:skuText="getMessageContent.introduction"
:title="getMessageContent.spuName"
:titleWidth="400"
priceColor="#FF3000"
/>
</div>
</template>
</template>
<script lang="ts" setup>
import { KeFuMessageContentTypeEnum } from '../tools/constants'
import ProductItem from './ProductItem.vue'
import { UserTypeEnum } from '@/utils/constants'
import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
defineOptions({ name: 'ProductMessageItem' })
const props = defineProps<{
message: KeFuMessageRespVO
}>()
const getMessageContent = computed(() => JSON.parse(props.message.content))
</script>

View File

@ -9,6 +9,7 @@ export const KeFuMessageContentTypeEnum = {
PRODUCT: 10, // 商品消息
ORDER: 11 // 订单消息"
}
// Promotion 的 WebSocket 消息类型枚举类
export const WebSocketMessageTypeConstants = {
KEFU_MESSAGE_TYPE: 'kefu_message_type', // 客服消息类型

View File

@ -1,10 +1,13 @@
<template>
<el-row :gutter="10">
<!-- TODO @puhui999KeFuConversationBox => KeFuConversationList KeFuChatBox => KeFuMessageList -->
<!-- 会话列表 -->
<el-col :span="8">
<ContentWrap>
<KeFuConversationBox ref="keFuConversationRef" @change="handleChange" />
</ContentWrap>
</el-col>
<!-- 会话详情选中会话的消息列表 -->
<el-col :span="16">
<ContentWrap>
<KeFuChatBox ref="keFuChatBoxRef" @change="getConversationList" />
@ -21,15 +24,10 @@ import { getAccessToken } from '@/utils/auth'
import { useWebSocket } from '@vueuse/core'
defineOptions({ name: 'KeFu' })
const message = useMessage()
//
const keFuChatBoxRef = ref<InstanceType<typeof KeFuChatBox>>()
const handleChange = (conversation: KeFuConversationRespVO) => {
keFuChatBoxRef.value?.getMessageList(conversation)
}
const message = useMessage() //
//======================= websocket start=======================
// ======================= WebSocket start =======================
const server = ref(
(import.meta.env.VITE_BASE_URL + '/infra/ws/').replace('http', 'ws') +
'?token=' +
@ -38,9 +36,11 @@ const server = ref(
/** 发起 WebSocket 连接 */
const { data, close, open } = useWebSocket(server.value, {
autoReconnect: false,
autoReconnect: false, // TODO @puhui999
heartbeat: true
})
/** 监听 WebSocket 数据 */
watchEffect(() => {
if (!data.value) {
return
@ -75,17 +75,28 @@ watchEffect(() => {
console.error(error)
}
})
//======================= websocket end=======================
//
// ======================= WebSocket end =======================
/** 加载会话列表 */
const keFuConversationRef = ref<InstanceType<typeof KeFuConversationBox>>()
const getConversationList = () => {
keFuConversationRef.value?.getConversationList()
}
/** 加载指定会话的消息列表 */
const keFuChatBoxRef = ref<InstanceType<typeof KeFuChatBox>>()
const handleChange = (conversation: KeFuConversationRespVO) => {
keFuChatBoxRef.value?.getMessageList(conversation)
}
/** 初始化 */
onMounted(() => {
getConversationList()
// websocket
open()
})
/** 销毁 */
onBeforeUnmount(() => {
// websocket
close()
@ -104,17 +115,17 @@ onBeforeUnmount(() => {
height: 6px;
}
/*定义滚动条轨道 内阴影+圆角*/
/* 定义滚动条轨道 内阴影+圆角 */
::-webkit-scrollbar-track {
box-shadow: inset 0 0 0px rgba(240, 240, 240, 0.5);
box-shadow: inset 0 0 0 rgba(240, 240, 240, 0.5);
border-radius: 10px;
background-color: #fff;
}
/*定义滑块 内阴影+圆角*/
/* 定义滑块 内阴影+圆角 */
::-webkit-scrollbar-thumb {
border-radius: 10px;
box-shadow: inset 0 0 0px rgba(240, 240, 240, 0.5);
box-shadow: inset 0 0 0 rgba(240, 240, 240, 0.5);
background-color: rgba(240, 240, 240, 0.5);
}
</style>