Merge remote-tracking branch 'yudao-ui-admin-vue3/dev' into dev
# Conflicts: # package.jsonpull/484/head
commit
a4a376b18d
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
After Width: | Height: | Size: 348 KiB |
14
README.md
14
README.md
|
@ -191,26 +191,24 @@ ps:核心功能已经实现,正在对接微信小程序中...
|
||||||
|
|
||||||
### 商城系统
|
### 商城系统
|
||||||
|
|
||||||
|
演示地址:<https://doc.iocoder.cn/mall-preview/>
|
||||||
|
|
||||||
![功能图](/.image/common/mall-feature.png)
|
![功能图](/.image/common/mall-feature.png)
|
||||||
|
|
||||||
![功能图](/.image/common/mall-preview.png)
|
![功能图](/.image/common/mall-preview.png)
|
||||||
|
|
||||||
_前端基于 crmeb uniapp 经过授权重构,优化代码实现,接入芋道快速开发平台_
|
|
||||||
|
|
||||||
演示地址:<https://doc.iocoder.cn/mall-preview/>
|
|
||||||
|
|
||||||
### ERP 系统
|
### ERP 系统
|
||||||
|
|
||||||
![功能图](/.image/common/erp-feature.png)
|
|
||||||
|
|
||||||
演示地址:<https://doc.iocoder.cn/erp-preview/>
|
演示地址:<https://doc.iocoder.cn/erp-preview/>
|
||||||
|
|
||||||
|
![功能图](/.image/common/erp-feature.png)
|
||||||
|
|
||||||
### CRM 系统
|
### CRM 系统
|
||||||
|
|
||||||
![功能图](/.image/common/crm-feature.png)
|
|
||||||
|
|
||||||
演示地址:<https://doc.iocoder.cn/crm-preview/>
|
演示地址:<https://doc.iocoder.cn/crm-preview/>
|
||||||
|
|
||||||
|
![功能图](/.image/common/crm-feature.png)
|
||||||
|
|
||||||
## 🐷 演示图
|
## 🐷 演示图
|
||||||
|
|
||||||
### 系统功能
|
### 系统功能
|
||||||
|
|
|
@ -56,6 +56,10 @@ export const ImageApi = {
|
||||||
getImagePageMy: async (params: PageParam) => {
|
getImagePageMy: async (params: PageParam) => {
|
||||||
return await request.get({ url: `/ai/image/my-page`, params })
|
return await request.get({ url: `/ai/image/my-page`, params })
|
||||||
},
|
},
|
||||||
|
// 获取公开的绘图记录
|
||||||
|
getImagePagePublic: async (params) => {
|
||||||
|
return await request.get({ url: `/ai/image/public-page`, params })
|
||||||
|
},
|
||||||
// 获取【我的】绘图记录
|
// 获取【我的】绘图记录
|
||||||
getImageMy: async (id: number) => {
|
getImageMy: async (id: number) => {
|
||||||
return await request.get({ url: `/ai/image/get-my?id=${id}` })
|
return await request.get({ url: `/ai/image/get-my?id=${id}` })
|
||||||
|
|
|
@ -17,7 +17,7 @@ export interface WriteVO {
|
||||||
platform?: string // 平台
|
platform?: string // 平台
|
||||||
model?: string // 模型
|
model?: string // 模型
|
||||||
generatedContent?: string // 生成的内容
|
generatedContent?: string // 生成的内容
|
||||||
errorMessage: string // 错误信息
|
errorMessage?: string // 错误信息
|
||||||
createTime?: Date // 创建时间
|
createTime?: Date // 创建时间
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得商品浏览记录分页
|
||||||
|
*
|
||||||
|
* @param params 请求参数
|
||||||
|
*/
|
||||||
|
export const getBrowseHistoryPage = (params: any) => {
|
||||||
|
return request.get({ url: '/product/browse-history/page', params })
|
||||||
|
}
|
Binary file not shown.
|
@ -1,10 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="contentRef" class="markdown-view" v-html="contentHtml"></div>
|
<!-- <div ref="contentRef" class="markdown-view" v-html="contentHtml"></div>-->
|
||||||
|
<div ref="contentRef" class="markdown-view" v-html="renderedMarkdown"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useClipboard } from '@vueuse/core'
|
import {useClipboard} from '@vueuse/core'
|
||||||
import { marked } from 'marked'
|
import MarkdownIt from 'markdown-it'
|
||||||
import 'highlight.js/styles/vs2015.min.css'
|
import 'highlight.js/styles/vs2015.min.css'
|
||||||
import hljs from 'highlight.js'
|
import hljs from 'highlight.js'
|
||||||
|
|
||||||
|
@ -19,45 +20,26 @@ const props = defineProps({
|
||||||
const message = useMessage() // 消息弹窗
|
const message = useMessage() // 消息弹窗
|
||||||
const { copy } = useClipboard() // 初始化 copy 到粘贴板
|
const { copy } = useClipboard() // 初始化 copy 到粘贴板
|
||||||
const contentRef = ref()
|
const contentRef = ref()
|
||||||
const contentHtml = ref<any>() // 渲染的html内容
|
|
||||||
const { content } = toRefs(props) // 将 props 变为引用类型
|
const { content } = toRefs(props) // 将 props 变为引用类型
|
||||||
|
|
||||||
// 代码高亮:https://highlightjs.org/
|
const md = new MarkdownIt({
|
||||||
// 转换 markdown:marked
|
highlight: function (str, lang) {
|
||||||
|
if (lang && hljs.getLanguage(lang)) {
|
||||||
/** marked 渲染器 */
|
try {
|
||||||
const renderer = {
|
const copyHtml = `<div id="copy" data-copy='${str}' style="position: absolute; right: 10px; top: 5px; color: #fff;cursor: pointer;">复制</div>`
|
||||||
code(code, language, c) {
|
return `<pre style="position: relative;">${copyHtml}<code class="hljs">${hljs.highlight(lang, str, true).value}</code></pre>`
|
||||||
let highlightHtml
|
} catch (__) {}
|
||||||
try {
|
|
||||||
highlightHtml = hljs.highlight(code, { language: language, ignoreIllegals: true }).value
|
|
||||||
} catch (e) {
|
|
||||||
// skip
|
|
||||||
}
|
}
|
||||||
const copyHtml = `<div id="copy" data-copy='${code}' style="position: absolute; right: 10px; top: 5px; color: #fff;cursor: pointer;">复制</div>`
|
return ``
|
||||||
return `<pre style="position: relative;">${copyHtml}<code class="hljs">${highlightHtml}</code></pre>`
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
// 配置 marked
|
const renderedMarkdown = computed(() => {
|
||||||
marked.use({
|
return md.render(props.content);
|
||||||
renderer: renderer
|
});
|
||||||
})
|
|
||||||
|
|
||||||
/** 监听 content 变化 */
|
|
||||||
watch(content, async (newValue, oldValue) => {
|
|
||||||
await renderMarkdown(newValue)
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 渲染 markdown */
|
|
||||||
const renderMarkdown = async (content: string) => {
|
|
||||||
contentHtml.value = await marked(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 初始化 **/
|
/** 初始化 **/
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 解析转换 markdown
|
|
||||||
await renderMarkdown(props.content as string)
|
|
||||||
// 添加 copy 监听
|
// 添加 copy 监听
|
||||||
contentRef.value.addEventListener('click', (e: any) => {
|
contentRef.value.addEventListener('click', (e: any) => {
|
||||||
if (e.target.id === 'copy') {
|
if (e.target.id === 'copy') {
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
<template>
|
||||||
|
<div class="square-container">
|
||||||
|
<el-input
|
||||||
|
v-model="searchText"
|
||||||
|
style="width: 100%; margin-bottom: 20px"
|
||||||
|
size="large"
|
||||||
|
placeholder="请输入要搜索的内容"
|
||||||
|
:suffix-icon="Search"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
<div class="gallery">
|
||||||
|
<div v-for="item in publicList" :key="item.id" class="gallery-item">
|
||||||
|
<img :src="item.picUrl" class="img" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ImageApi, ImageVO } from '@/api/ai/image'
|
||||||
|
import { Search } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
/** 属性 */
|
||||||
|
// TODO @fan:queryParams 里面搞分页哈。
|
||||||
|
const pageNo = ref<number>(1)
|
||||||
|
const pageSize = ref<number>(20)
|
||||||
|
const publicList = ref<ImageVO[]>([])
|
||||||
|
const searchText = ref<string>('')
|
||||||
|
|
||||||
|
/** 获取数据 */
|
||||||
|
const getListData = async () => {
|
||||||
|
const res = await ImageApi.getImagePagePublic({
|
||||||
|
pageNo: pageNo.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
prompt: searchText.value
|
||||||
|
})
|
||||||
|
publicList.value = res.list as ImageVO[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索 */
|
||||||
|
const handleSearch = async () => {
|
||||||
|
await getListData()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await getListData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.square-container {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.gallery {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
//max-width: 1000px;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f0f0f0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover img {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-1/1">
|
<div class="flex h-full items-stretch">
|
||||||
<!-- 模式 -->
|
<!-- 模式 -->
|
||||||
<Mode class="flex-none" @generate-music="generateMusic"/>
|
<Mode class="flex-none" @generate-music="generateMusic"/>
|
||||||
<!-- 音频列表 -->
|
<!-- 音频列表 -->
|
||||||
|
@ -13,8 +13,13 @@ import List from './list/index.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'Index' })
|
defineOptions({ name: 'Index' })
|
||||||
|
|
||||||
const listRef = ref<{generateMusic: (...args) => void} | null>(null)
|
const listRef = ref<Nullable<{generateMusic: (...args) => void}>>(null)
|
||||||
|
|
||||||
|
/*
|
||||||
|
*@Description: 拿到左侧配置信息调用右侧音乐生成的方法
|
||||||
|
*@MethodAuthor: xiaohong
|
||||||
|
*@Date: 2024-07-19 11:13:38
|
||||||
|
*/
|
||||||
function generateMusic (args: {formData: Recordable}) {
|
function generateMusic (args: {formData: Recordable}) {
|
||||||
unref(listRef)?.generateMusic(args.formData)
|
unref(listRef)?.generateMusic(args.formData)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,70 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="h-72px bg-[var(--el-bg-color-overlay)] b-solid b-1 b-[var(--el-border-color)] b-l-none">播放器</div>
|
<div class="flex items-center justify-between px-2 h-72px bg-[var(--el-bg-color-overlay)] b-solid b-1 b-[var(--el-border-color)] b-l-none">
|
||||||
|
<!-- 歌曲信息 -->
|
||||||
|
<div class="flex gap-[10px]">
|
||||||
|
<el-image src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png" class="w-[45px]"/>
|
||||||
|
<div>
|
||||||
|
<div>{{currentSong.name}}</div>
|
||||||
|
<div class="text-[12px] text-gray-400">{{currentSong.singer}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 音频controls -->
|
||||||
|
<div class="flex gap-[12px] items-center">
|
||||||
|
<Icon icon="majesticons:back-circle" :size="20" class="text-gray-300 cursor-pointer"/>
|
||||||
|
<Icon :icon="audioProps.paused ? 'mdi:arrow-right-drop-circle' : 'solar:pause-circle-bold'" :size="30" class=" cursor-pointer" @click="toggleStatus('paused')"/>
|
||||||
|
<Icon icon="majesticons:next-circle" :size="20" class="text-gray-300 cursor-pointer"/>
|
||||||
|
<div class="flex gap-[16px] items-center">
|
||||||
|
<span>{{audioProps.currentTime}}</span>
|
||||||
|
<el-slider v-model="audioProps.duration" color="#409eff" class="w-[160px!important] "/>
|
||||||
|
<span>{{ audioProps.duration }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- 音频 -->
|
||||||
|
<audio v-bind="audioProps" ref="audioRef" controls v-show="!audioProps" @timeupdate="audioTimeUpdate">
|
||||||
|
<source :src="audioUrl"/>
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 音量控制器 -->
|
||||||
|
<div class="flex gap-[16px] items-center">
|
||||||
|
<Icon :icon="audioProps.muted ? 'tabler:volume-off' : 'tabler:volume'" :size="20" class="cursor-pointer" @click="toggleStatus('muted')"/>
|
||||||
|
<el-slider v-model="audioProps.volume" color="#409eff" class="w-[160px!important] "/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { formatPast } from '@/utils/formatTime'
|
||||||
|
import audioUrl from '@/assets/audio/response.mp3'
|
||||||
|
|
||||||
defineOptions({ name: 'Index' })
|
defineOptions({ name: 'Index' })
|
||||||
|
|
||||||
|
const currentSong = inject('currentSong', {})
|
||||||
|
|
||||||
|
const audioRef = ref<Nullable<HTMLElement>>(null)
|
||||||
|
// 音频相关属性https://www.runoob.com/tags/ref-av-dom.html
|
||||||
|
const audioProps = reactive({
|
||||||
|
autoplay: true,
|
||||||
|
paused: false,
|
||||||
|
currentTime: '00:00',
|
||||||
|
duration: '00:00',
|
||||||
|
muted: false,
|
||||||
|
volume: 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
function toggleStatus (type: string) {
|
||||||
|
audioProps[type] = !audioProps[type]
|
||||||
|
if (type === 'paused' && audioRef.value) {
|
||||||
|
if (audioProps[type]) {
|
||||||
|
audioRef.value.pause()
|
||||||
|
} else {
|
||||||
|
audioRef.value.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新播放位置
|
||||||
|
function audioTimeUpdate (args) {
|
||||||
|
audioProps.currentTime = formatPast(new Date(args.timeStamp), 'mm:ss')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,29 +1,29 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col">
|
||||||
<div class="flex-auto flex overflow-hidden">
|
<div class="flex-auto flex overflow-hidden">
|
||||||
<el-tabs v-model="currentType" class="flex-auto px-[var(--app-content-padding)]">
|
<el-tabs v-model="currentType" class="flex-auto px-[var(--app-content-padding)]">
|
||||||
<!-- 我的创作 -->
|
<!-- 我的创作 -->
|
||||||
<el-tab-pane label="我的创作" v-loading="loading" name="mine">
|
<el-tab-pane v-loading="loading" label="我的创作" name="mine">
|
||||||
<el-row v-if="mySongList.length" :gutter="12">
|
<el-row v-if="mySongList.length" :gutter="12">
|
||||||
<el-col v-for="song in mySongList" :key="song.id" :span="24">
|
<el-col v-for="song in mySongList" :key="song.id" :span="24">
|
||||||
<songCard v-bind="song"/>
|
<songCard :songInfo="song" @play="setCurrentSong(song)"/>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-empty v-else description="暂无音乐"/>
|
<el-empty v-else description="暂无音乐"/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<!-- 试听广场 -->
|
<!-- 试听广场 -->
|
||||||
<el-tab-pane label="试听广场" v-loading="loading" name="square">
|
<el-tab-pane v-loading="loading" label="试听广场" name="square">
|
||||||
<el-row v-if="squareSongList.length" v-loading="loading" :gutter="12">
|
<el-row v-if="squareSongList.length" v-loading="loading" :gutter="12">
|
||||||
<el-col v-for="song in squareSongList" :key="song.id" :span="24">
|
<el-col v-for="song in squareSongList" :key="song.id" :span="24">
|
||||||
<songCard v-bind="song"/>
|
<songCard :songInfo="song" @play="setCurrentSong(song)"/>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-empty v-else description="暂无音乐"/>
|
<el-empty v-else description="暂无音乐"/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
<!-- songInfo -->
|
<!-- songInfo -->
|
||||||
<songInfo v-bind="squareSongList[0]" class="flex-none"/>
|
<songInfo class="flex-none"/>
|
||||||
</div>
|
</div>
|
||||||
<audioBar class="flex-none"/>
|
<audioBar class="flex-none"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,13 +36,18 @@ import audioBar from './audioBar/index.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'Index' })
|
defineOptions({ name: 'Index' })
|
||||||
|
|
||||||
|
|
||||||
const currentType = ref('mine')
|
const currentType = ref('mine')
|
||||||
// loading 状态
|
// loading 状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
// 当前音乐
|
||||||
|
const currentSong = ref({})
|
||||||
|
|
||||||
const mySongList = ref<Recordable[]>([])
|
const mySongList = ref<Recordable[]>([])
|
||||||
const squareSongList = ref<Recordable[]>([])
|
const squareSongList = ref<Recordable[]>([])
|
||||||
|
|
||||||
|
provide('currentSong', currentSong)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
*@Description: 调接口生成音乐列表
|
*@Description: 调接口生成音乐列表
|
||||||
*@MethodAuthor: xiaohong
|
*@MethodAuthor: xiaohong
|
||||||
|
@ -57,7 +62,7 @@ function generateMusic (formData: Recordable) {
|
||||||
id: index,
|
id: index,
|
||||||
audioUrl: '',
|
audioUrl: '',
|
||||||
videoUrl: '',
|
videoUrl: '',
|
||||||
title: '我走后',
|
title: '我走后' + index,
|
||||||
imageUrl: 'https://www.carsmp3.com/data/attachment/forum/201909/19/091020q5kgre20fidreqyt.jpg',
|
imageUrl: 'https://www.carsmp3.com/data/attachment/forum/201909/19/091020q5kgre20fidreqyt.jpg',
|
||||||
desc: 'Metal, symphony, film soundtrack, grand, majesticMetal, dtrack, grand, majestic',
|
desc: 'Metal, symphony, film soundtrack, grand, majesticMetal, dtrack, grand, majestic',
|
||||||
date: '2024年04月30日 14:02:57',
|
date: '2024年04月30日 14:02:57',
|
||||||
|
@ -76,6 +81,15 @@ function generateMusic (formData: Recordable) {
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
*@Description: 设置当前播放的音乐
|
||||||
|
*@MethodAuthor: xiaohong
|
||||||
|
*@Date: 2024-07-19 11:22:33
|
||||||
|
*/
|
||||||
|
function setCurrentSong (music: Recordable) {
|
||||||
|
currentSong.value = music
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
generateMusic
|
generateMusic
|
||||||
})
|
})
|
||||||
|
@ -86,9 +100,9 @@ defineExpose({
|
||||||
:deep(.el-tabs) {
|
:deep(.el-tabs) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
.el-tabs__content{
|
.el-tabs__content {
|
||||||
padding: 0 7px;
|
padding: 0 7px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex bg-[var(--el-bg-color-overlay)] p-12px mb-12px rounded-1">
|
<div class="flex bg-[var(--el-bg-color-overlay)] p-12px mb-12px rounded-1">
|
||||||
<el-image :src="imageUrl" class="flex-none w-80px"/>
|
<div class="relative" @click="playSong">
|
||||||
|
<el-image :src="songInfo.imageUrl" class="flex-none w-80px"/>
|
||||||
|
<div class="bg-black bg-op-40 absolute top-0 left-0 w-full h-full flex items-center justify-center cursor-pointer">
|
||||||
|
<Icon :icon="currentSong.id === songInfo.id ? 'solar:pause-circle-bold':'mdi:arrow-right-drop-circle'" :size="30" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="ml-8px">
|
<div class="ml-8px">
|
||||||
<div>{{ title }}</div>
|
<div>{{ songInfo.title }}</div>
|
||||||
<div class="mt-8px text-12px text-[var(--el-text-color-secondary)] line-clamp-2">
|
<div class="mt-8px text-12px text-[var(--el-text-color-secondary)] line-clamp-2">
|
||||||
{{ desc }}
|
{{ songInfo.desc }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,15 +20,17 @@
|
||||||
defineOptions({ name: 'Index' })
|
defineOptions({ name: 'Index' })
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
imageUrl: {
|
songInfo: {
|
||||||
type: String
|
type: Object,
|
||||||
},
|
default: () => ({})
|
||||||
title: {
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
desc: {
|
|
||||||
type: String
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits(['play'])
|
||||||
|
|
||||||
|
const currentSong = inject('currentSong', {})
|
||||||
|
|
||||||
|
function playSong () {
|
||||||
|
emits('play')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<ContentWrap class="w-300px mb-[0!important] line-height-24px">
|
<ContentWrap class="w-300px mb-[0!important] line-height-24px">
|
||||||
<el-image :src="imageUrl"/>
|
<el-image :src="currentSong.imageUrl"/>
|
||||||
<div class="">{{ title }}</div>
|
<div class="">{{ currentSong.title }}</div>
|
||||||
<div class="text-[var(--el-text-color-secondary)] text-12px line-clamp-1">{{ desc }}</div>
|
<div class="text-[var(--el-text-color-secondary)] text-12px line-clamp-1">
|
||||||
<div class="text-[var(--el-text-color-secondary)] text-12px">{{ date }}</div>
|
{{ currentSong.desc }}
|
||||||
|
</div>
|
||||||
|
<div class="text-[var(--el-text-color-secondary)] text-12px">
|
||||||
|
{{ currentSong.date }}
|
||||||
|
</div>
|
||||||
<el-button size="small" round class="my-6px">信息复用</el-button>
|
<el-button size="small" round class="my-6px">信息复用</el-button>
|
||||||
<div class="text-[var(--el-text-color-secondary)] text-12px" v-html="lyric"></div>
|
<div class="text-[var(--el-text-color-secondary)] text-12px" v-html="currentSong.lyric"></div>
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -13,21 +17,6 @@
|
||||||
|
|
||||||
defineOptions({ name: 'Index' })
|
defineOptions({ name: 'Index' })
|
||||||
|
|
||||||
defineProps({
|
const currentSong = inject('currentSong', {})
|
||||||
imageUrl: {
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
desc: {
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
date: {
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
lyric: {
|
|
||||||
type: String
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<ContentWrap class="w-300px h-full">
|
<ContentWrap class="w-300px h-full mb-[0!important]">
|
||||||
<el-radio-group v-model="generateMode" class="mb-15px">
|
<el-radio-group v-model="generateMode" class="mb-15px">
|
||||||
<el-radio-button label="desc">
|
<el-radio-button label="desc">
|
||||||
描述模式
|
描述模式
|
||||||
|
@ -28,10 +28,7 @@ const emits = defineEmits(['generate-music'])
|
||||||
|
|
||||||
const generateMode = ref('lyric')
|
const generateMode = ref('lyric')
|
||||||
|
|
||||||
interface ModeRef {
|
const modeRef = ref<Nullable<{ formData: Recordable }>>(null)
|
||||||
formData: Recordable
|
|
||||||
}
|
|
||||||
const modeRef = ref<ModeRef | null>(null)
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
*@Description: 根据信息生成音乐
|
*@Description: 根据信息生成音乐
|
||||||
|
@ -39,6 +36,6 @@ const modeRef = ref<ModeRef | null>(null)
|
||||||
*@Date: 2024-06-27 16:40:16
|
*@Date: 2024-06-27 16:40:16
|
||||||
*/
|
*/
|
||||||
function generateMusic () {
|
function generateMusic () {
|
||||||
emits('generate-music', {formData: unref(modeRef)?.formData.value})
|
emits('generate-music', {formData: unref(modeRef)?.formData})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<el-card class="my-card h-full">
|
<el-card class="my-card h-full">
|
||||||
<template #header
|
<template #header>
|
||||||
><h3 class="m-0 px-7 shrink-0 flex items-center justify-between">
|
<h3 class="m-0 px-7 shrink-0 flex items-center justify-between">
|
||||||
<span>预览</span>
|
<span>预览</span>
|
||||||
<!-- 展示在右上角 -->
|
<!-- 展示在右上角 -->
|
||||||
<el-button color="#846af7" v-show="showCopy" @click="copyContent" size="small">
|
<el-button color="#846af7" v-show="showCopy" @click="copyContent" size="small">
|
||||||
|
@ -10,8 +10,8 @@
|
||||||
</template>
|
</template>
|
||||||
复制
|
复制
|
||||||
</el-button>
|
</el-button>
|
||||||
</h3></template
|
</h3>
|
||||||
>
|
</template>
|
||||||
|
|
||||||
<div ref="contentRef" class="hide-scroll-bar h-full box-border overflow-y-auto">
|
<div ref="contentRef" class="hide-scroll-bar h-full box-border overflow-y-auto">
|
||||||
<div class="w-full min-h-full relative flex-grow bg-white box-border p-3 sm:p-7">
|
<div class="w-full min-h-full relative flex-grow bg-white box-border p-3 sm:p-7">
|
||||||
|
@ -105,7 +105,7 @@ watch(copied, (val) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.my-card{
|
.my-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
<!-- 标签选项 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap gap-[8px]">
|
<div class="flex flex-wrap gap-[8px]">
|
||||||
<span
|
<span
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Left from './components/Left.vue'
|
import Left from './components/Left.vue'
|
||||||
import Right from './components/Right.vue'
|
import Right from './components/Right.vue'
|
||||||
import { WriteApi } from '@/api/ai/write'
|
import { WriteApi, WriteVO } from '@/api/ai/write'
|
||||||
import { WriteExample } from '@/views/ai/utils/constants'
|
import { WriteExample } from '@/views/ai/utils/constants'
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
@ -37,7 +37,7 @@ const stopStream = () => {
|
||||||
|
|
||||||
/** 执行写作 */
|
/** 执行写作 */
|
||||||
const rightRef = ref<InstanceType<typeof Right>>()
|
const rightRef = ref<InstanceType<typeof Right>>()
|
||||||
const submit = (data) => {
|
const submit = (data: WriteVO) => {
|
||||||
abortController.value = new AbortController()
|
abortController.value = new AbortController()
|
||||||
writeResult.value = ''
|
writeResult.value = ''
|
||||||
isWriting.value = true
|
isWriting.value = true
|
||||||
|
|
|
@ -21,9 +21,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-10px w-100%">
|
<div class="ml-10px w-100%">
|
||||||
<div class="flex justify-between items-center w-100%">
|
<div class="flex justify-between items-center w-100%">
|
||||||
<span>{{ item.userNickname }}</span>
|
<span class="username">{{ item.userNickname }}</span>
|
||||||
<span class="color-[#989EA6]">
|
<span class="color-[#989EA6]">
|
||||||
{{ formatDate(item.lastMessageTime) }}
|
{{ formatPast(item.lastMessageTime, 'YYYY-mm-dd') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 最后聊天内容 -->
|
<!-- 最后聊天内容 -->
|
||||||
|
@ -70,7 +70,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { KeFuConversationApi, KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
|
import { KeFuConversationApi, KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
|
||||||
import { useEmoji } from './tools/emoji'
|
import { useEmoji } from './tools/emoji'
|
||||||
import { formatDate } from '@/utils/formatTime'
|
import { formatPast } from '@/utils/formatTime'
|
||||||
import { KeFuMessageContentTypeEnum } from './tools/constants'
|
import { KeFuMessageContentTypeEnum } from './tools/constants'
|
||||||
import { useAppStore } from '@/store/modules/app'
|
import { useAppStore } from '@/store/modules/app'
|
||||||
|
|
||||||
|
@ -185,6 +185,16 @@ watch(showRightMenu, (val) => {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
transition: border-left 0.05s ease-in-out; /* 设置过渡效果 */
|
transition: border-left 0.05s ease-in-out; /* 设置过渡效果 */
|
||||||
|
|
||||||
|
.username {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 60%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.last-message {
|
.last-message {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
overflow: hidden; // 隐藏超出的文本
|
overflow: hidden; // 隐藏超出的文本
|
||||||
|
|
|
@ -40,19 +40,54 @@
|
||||||
v-if="item.senderType === UserTypeEnum.MEMBER"
|
v-if="item.senderType === UserTypeEnum.MEMBER"
|
||||||
:src="conversation.userAvatar"
|
:src="conversation.userAvatar"
|
||||||
alt="avatar"
|
alt="avatar"
|
||||||
|
class="w-60px h-60px"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
:class="{ 'kefu-message': KeFuMessageContentTypeEnum.TEXT === item.contentType }"
|
:class="{ 'kefu-message': KeFuMessageContentTypeEnum.TEXT === item.contentType }"
|
||||||
class="p-10px"
|
class="p-10px"
|
||||||
>
|
>
|
||||||
<!-- 文本消息 -->
|
<!-- 文本消息 -->
|
||||||
<TextMessageItem :message="item" />
|
<MessageItem :message="item">
|
||||||
|
<template v-if="KeFuMessageContentTypeEnum.TEXT === item.contentType">
|
||||||
|
<div
|
||||||
|
v-dompurify-html="replaceEmoji(item.content)"
|
||||||
|
class="flex items-center"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
</MessageItem>
|
||||||
<!-- 图片消息 -->
|
<!-- 图片消息 -->
|
||||||
<ImageMessageItem :message="item" />
|
<MessageItem :message="item">
|
||||||
|
<el-image
|
||||||
|
v-if="KeFuMessageContentTypeEnum.IMAGE === item.contentType"
|
||||||
|
:initial-index="0"
|
||||||
|
:preview-src-list="[item.content]"
|
||||||
|
:src="item.content"
|
||||||
|
class="w-200px"
|
||||||
|
fit="contain"
|
||||||
|
preview-teleported
|
||||||
|
/>
|
||||||
|
</MessageItem>
|
||||||
<!-- 商品消息 -->
|
<!-- 商品消息 -->
|
||||||
<ProductMessageItem :message="item" />
|
<MessageItem :message="item">
|
||||||
|
<ProductItem
|
||||||
|
v-if="KeFuMessageContentTypeEnum.PRODUCT === item.contentType"
|
||||||
|
:picUrl="getMessageContent(item).picUrl"
|
||||||
|
:price="getMessageContent(item).price"
|
||||||
|
:skuText="getMessageContent(item).introduction"
|
||||||
|
:title="getMessageContent(item).spuName"
|
||||||
|
:titleWidth="400"
|
||||||
|
class="max-w-70%"
|
||||||
|
priceColor="#FF3000"
|
||||||
|
/>
|
||||||
|
</MessageItem>
|
||||||
<!-- 订单消息 -->
|
<!-- 订单消息 -->
|
||||||
<OrderMessageItem :message="item" />
|
<MessageItem :message="item">
|
||||||
|
<OrderItem
|
||||||
|
v-if="KeFuMessageContentTypeEnum.ORDER === item.contentType"
|
||||||
|
:message="item"
|
||||||
|
class="max-w-70%"
|
||||||
|
/>
|
||||||
|
</MessageItem>
|
||||||
</div>
|
</div>
|
||||||
<el-avatar
|
<el-avatar
|
||||||
v-if="item.senderType === UserTypeEnum.ADMIN"
|
v-if="item.senderType === UserTypeEnum.ADMIN"
|
||||||
|
@ -97,24 +132,24 @@ import { KeFuMessageApi, KeFuMessageRespVO } from '@/api/mall/promotion/kefu/mes
|
||||||
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
|
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
|
||||||
import EmojiSelectPopover from './tools/EmojiSelectPopover.vue'
|
import EmojiSelectPopover from './tools/EmojiSelectPopover.vue'
|
||||||
import PictureSelectUpload from './tools/PictureSelectUpload.vue'
|
import PictureSelectUpload from './tools/PictureSelectUpload.vue'
|
||||||
import TextMessageItem from './message/TextMessageItem.vue'
|
import ProductItem from './message/ProductItem.vue'
|
||||||
import ImageMessageItem from './message/ImageMessageItem.vue'
|
import OrderItem from './message/OrderItem.vue'
|
||||||
import ProductMessageItem from './message/ProductMessageItem.vue'
|
import { Emoji, useEmoji } from './tools/emoji'
|
||||||
import OrderMessageItem from './message/OrderMessageItem.vue'
|
|
||||||
import { Emoji } from './tools/emoji'
|
|
||||||
import { KeFuMessageContentTypeEnum } from './tools/constants'
|
import { KeFuMessageContentTypeEnum } from './tools/constants'
|
||||||
import { isEmpty } from '@/utils/is'
|
import { isEmpty } from '@/utils/is'
|
||||||
import { UserTypeEnum } from '@/utils/constants'
|
import { UserTypeEnum } from '@/utils/constants'
|
||||||
import { formatDate } from '@/utils/formatTime'
|
import { formatDate } from '@/utils/formatTime'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
|
import { debounce } from 'lodash-es'
|
||||||
|
import { jsonParse } from '@/utils'
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
defineOptions({ name: 'KeFuMessageList' })
|
defineOptions({ name: 'KeFuMessageList' })
|
||||||
|
|
||||||
const message = ref('') // 消息弹窗
|
const message = ref('') // 消息弹窗
|
||||||
|
const { replaceEmoji } = useEmoji()
|
||||||
const messageTool = useMessage()
|
const messageTool = useMessage()
|
||||||
const messageList = ref<KeFuMessageRespVO[]>([]) // 消息列表
|
const messageList = ref<KeFuMessageRespVO[]>([]) // 消息列表
|
||||||
const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话
|
const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话
|
||||||
|
@ -126,18 +161,11 @@ const queryParams = reactive({
|
||||||
})
|
})
|
||||||
const total = ref(0) // 消息总条数
|
const total = ref(0) // 消息总条数
|
||||||
const refreshContent = ref(false) // 内容刷新,主要解决会话消息页面高度不一致导致的滚动功能精度失效
|
const refreshContent = ref(false) // 内容刷新,主要解决会话消息页面高度不一致导致的滚动功能精度失效
|
||||||
|
|
||||||
|
/** 获悉消息内容 */
|
||||||
|
const getMessageContent = computed(() => (item: any) => jsonParse(item.content))
|
||||||
/** 获得消息列表 */
|
/** 获得消息列表 */
|
||||||
const getMessageList = async (val: KeFuConversationRespVO, conversationChange: boolean) => {
|
const getMessageList = async () => {
|
||||||
// 会话切换,重置相关参数
|
|
||||||
if (conversationChange) {
|
|
||||||
queryParams.pageNo = 1
|
|
||||||
messageList.value = []
|
|
||||||
total.value = 0
|
|
||||||
loadHistory.value = false
|
|
||||||
refreshContent.value = false
|
|
||||||
}
|
|
||||||
conversation.value = val
|
|
||||||
queryParams.conversationId = val.id
|
|
||||||
const res = await KeFuMessageApi.getKeFuMessagePage(queryParams)
|
const res = await KeFuMessageApi.getKeFuMessagePage(queryParams)
|
||||||
total.value = res.total
|
total.value = res.total
|
||||||
// 情况一:加载最新消息
|
// 情况一:加载最新消息
|
||||||
|
@ -146,14 +174,18 @@ const getMessageList = async (val: KeFuConversationRespVO, conversationChange: b
|
||||||
} else {
|
} else {
|
||||||
// 情况二:加载历史消息
|
// 情况二:加载历史消息
|
||||||
for (const item of res.list) {
|
for (const item of res.list) {
|
||||||
if (messageList.value.some((val) => val.id === item.id)) {
|
pushMessage(item)
|
||||||
continue
|
|
||||||
}
|
|
||||||
messageList.value.push(item)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
refreshContent.value = true
|
refreshContent.value = true
|
||||||
await scrollToBottom()
|
}
|
||||||
|
|
||||||
|
/** 添加消息 */
|
||||||
|
const pushMessage = (message: any) => {
|
||||||
|
if (messageList.value.some((val) => val.id === message.id)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
messageList.value.push(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按照时间倒序,获取消息列表 */
|
/** 按照时间倒序,获取消息列表 */
|
||||||
|
@ -163,20 +195,49 @@ const getMessageList0 = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 刷新消息列表 */
|
/** 刷新消息列表 */
|
||||||
const refreshMessageList = async () => {
|
const refreshMessageList = async (message?: any) => {
|
||||||
if (!conversation.value) {
|
if (!conversation.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
queryParams.pageNo = 1
|
if (typeof message !== 'undefined') {
|
||||||
await getMessageList(conversation.value, false)
|
// 当前查询会话与消息所属会话不一致则不做处理
|
||||||
|
if (message.conversationId !== conversation.value.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pushMessage(message)
|
||||||
|
} else {
|
||||||
|
// TODO @puhui999:不基于 page 做。而是流式分页;通过 createTime 排序查询;
|
||||||
|
queryParams.pageNo = 1
|
||||||
|
await getMessageList()
|
||||||
|
}
|
||||||
|
|
||||||
if (loadHistory.value) {
|
if (loadHistory.value) {
|
||||||
// 右下角显示有新消息提示
|
// 右下角显示有新消息提示
|
||||||
showNewMessageTip.value = true
|
showNewMessageTip.value = true
|
||||||
|
} else {
|
||||||
|
// 滚动到最新消息处
|
||||||
|
await handleToNewMessage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ getMessageList, refreshMessageList })
|
/** 获得新会话的消息列表 */
|
||||||
|
// TODO @puhui999:可优化:可以考虑本地做每个会话的消息 list 缓存;然后点击切换时,读取缓存;然后异步获取新消息,merge 下;
|
||||||
|
const getNewMessageList = async (val: KeFuConversationRespVO) => {
|
||||||
|
// 会话切换,重置相关参数
|
||||||
|
queryParams.pageNo = 1
|
||||||
|
messageList.value = []
|
||||||
|
total.value = 0
|
||||||
|
loadHistory.value = false
|
||||||
|
refreshContent.value = false
|
||||||
|
// 设置会话相关属性
|
||||||
|
conversation.value = val
|
||||||
|
queryParams.conversationId = val.id
|
||||||
|
// 获取消息
|
||||||
|
await refreshMessageList()
|
||||||
|
}
|
||||||
|
defineExpose({ getNewMessageList, refreshMessageList })
|
||||||
|
|
||||||
const showKeFuMessageList = computed(() => !isEmpty(conversation.value)) // 是否显示聊天区域
|
const showKeFuMessageList = computed(() => !isEmpty(conversation.value)) // 是否显示聊天区域
|
||||||
const skipGetMessageList = computed(() => {
|
const skipGetMessageList = computed(() => {
|
||||||
// 已加载到最后一页的话则不触发新的消息获取
|
// 已加载到最后一页的话则不触发新的消息获取
|
||||||
|
@ -221,9 +282,7 @@ const sendMessage = async (msg: any) => {
|
||||||
await KeFuMessageApi.sendKeFuMessage(msg)
|
await KeFuMessageApi.sendKeFuMessage(msg)
|
||||||
message.value = ''
|
message.value = ''
|
||||||
// 加载消息列表
|
// 加载消息列表
|
||||||
await getMessageList(conversation.value, false)
|
await refreshMessageList()
|
||||||
// 滚动到最新消息处
|
|
||||||
await scrollToBottom()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 滚动到底部 */
|
/** 滚动到底部 */
|
||||||
|
@ -248,17 +307,24 @@ const handleToNewMessage = async () => {
|
||||||
await scrollToBottom()
|
await scrollToBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 加载历史消息 */
|
|
||||||
const loadHistory = ref(false) // 加载历史消息
|
const loadHistory = ref(false) // 加载历史消息
|
||||||
const handleScroll = async ({ scrollTop }) => {
|
/** 处理消息列表滚动事件(debounce 限流) */
|
||||||
|
const handleScroll = debounce(({ scrollTop }) => {
|
||||||
if (skipGetMessageList.value) {
|
if (skipGetMessageList.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 触顶自动加载下一页数据
|
// 触顶自动加载下一页数据
|
||||||
if (scrollTop === 0) {
|
if (Math.floor(scrollTop) === 0) {
|
||||||
await handleOldMessage()
|
handleOldMessage()
|
||||||
}
|
}
|
||||||
}
|
const wrap = scrollbarRef.value?.wrapRef
|
||||||
|
// 触底重置
|
||||||
|
if (Math.abs(wrap!.scrollHeight - wrap!.clientHeight - wrap!.scrollTop) < 1) {
|
||||||
|
loadHistory.value = false
|
||||||
|
refreshMessageList()
|
||||||
|
}
|
||||||
|
}, 200)
|
||||||
|
/** 加载历史消息 */
|
||||||
const handleOldMessage = async () => {
|
const handleOldMessage = async () => {
|
||||||
// 记录已有页面高度
|
// 记录已有页面高度
|
||||||
const oldPageHeight = innerRef.value?.clientHeight
|
const oldPageHeight = innerRef.value?.clientHeight
|
||||||
|
@ -268,7 +334,7 @@ const handleOldMessage = async () => {
|
||||||
loadHistory.value = true
|
loadHistory.value = true
|
||||||
// 加载消息列表
|
// 加载消息列表
|
||||||
queryParams.pageNo += 1
|
queryParams.pageNo += 1
|
||||||
await getMessageList(conversation.value, false)
|
await getMessageList()
|
||||||
// 等页面加载完后,获得上一页最后一条消息的位置,控制滚动到它所在位置
|
// 等页面加载完后,获得上一页最后一条消息的位置,控制滚动到它所在位置
|
||||||
scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight - oldPageHeight)
|
scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight - oldPageHeight)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
<!-- 目录是不是叫 member 好点。然后这个组件是 MemberInfo,里面有浏览足迹 -->
|
||||||
|
<template>
|
||||||
|
<div v-show="!isEmpty(conversation)" class="kefu">
|
||||||
|
<div class="header-title h-60px flex justify-center items-center">他的足迹</div>
|
||||||
|
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
|
||||||
|
<el-tab-pane label="最近浏览" name="a" />
|
||||||
|
<el-tab-pane label="订单列表" name="b" />
|
||||||
|
</el-tabs>
|
||||||
|
<div>
|
||||||
|
<el-scrollbar ref="scrollbarRef" always height="calc(100vh - 400px)" @scroll="handleScroll">
|
||||||
|
<!-- 最近浏览 -->
|
||||||
|
<ProductBrowsingHistory v-if="activeName === 'a'" ref="productBrowsingHistoryRef" />
|
||||||
|
<!-- 订单列表 -->
|
||||||
|
<OrderBrowsingHistory v-if="activeName === 'b'" ref="orderBrowsingHistoryRef" />
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-show="isEmpty(conversation)" description="请选择左侧的一个会话后开始" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { TabsPaneContext } from 'element-plus'
|
||||||
|
import ProductBrowsingHistory from './ProductBrowsingHistory.vue'
|
||||||
|
import OrderBrowsingHistory from './OrderBrowsingHistory.vue'
|
||||||
|
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
|
||||||
|
import { isEmpty } from '@/utils/is'
|
||||||
|
import { debounce } from 'lodash-es'
|
||||||
|
import { ElScrollbar as ElScrollbarType } from 'element-plus/es/components/scrollbar'
|
||||||
|
|
||||||
|
defineOptions({ name: 'MemberBrowsingHistory' })
|
||||||
|
|
||||||
|
const activeName = ref('a')
|
||||||
|
|
||||||
|
/** tab 切换 */
|
||||||
|
const productBrowsingHistoryRef = ref<InstanceType<typeof ProductBrowsingHistory>>()
|
||||||
|
const orderBrowsingHistoryRef = ref<InstanceType<typeof OrderBrowsingHistory>>()
|
||||||
|
const handleClick = async (tab: TabsPaneContext) => {
|
||||||
|
activeName.value = tab.paneName as string
|
||||||
|
await nextTick()
|
||||||
|
await getHistoryList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得历史数据 */
|
||||||
|
// TODO @puhui:不要用 a、b 哈。就订单列表、浏览列表这种噶
|
||||||
|
const getHistoryList = async () => {
|
||||||
|
switch (activeName.value) {
|
||||||
|
case 'a':
|
||||||
|
await productBrowsingHistoryRef.value?.getHistoryList(conversation.value)
|
||||||
|
break
|
||||||
|
case 'b':
|
||||||
|
await orderBrowsingHistoryRef.value?.getHistoryList(conversation.value)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载下一页数据 */
|
||||||
|
const loadMore = async () => {
|
||||||
|
switch (activeName.value) {
|
||||||
|
case 'a':
|
||||||
|
await productBrowsingHistoryRef.value?.loadMore()
|
||||||
|
break
|
||||||
|
case 'b':
|
||||||
|
await orderBrowsingHistoryRef.value?.loadMore()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 浏览历史初始化 */
|
||||||
|
const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话
|
||||||
|
const initHistory = async (val: KeFuConversationRespVO) => {
|
||||||
|
activeName.value = 'a'
|
||||||
|
conversation.value = val
|
||||||
|
await nextTick()
|
||||||
|
await getHistoryList()
|
||||||
|
}
|
||||||
|
defineExpose({ initHistory })
|
||||||
|
|
||||||
|
/** 处理消息列表滚动事件(debounce 限流) */
|
||||||
|
const scrollbarRef = ref<InstanceType<typeof ElScrollbarType>>()
|
||||||
|
const handleScroll = debounce(() => {
|
||||||
|
const wrap = scrollbarRef.value?.wrapRef
|
||||||
|
// 触底重置
|
||||||
|
if (Math.abs(wrap!.scrollHeight - wrap!.clientHeight - wrap!.scrollTop) < 1) {
|
||||||
|
loadMore()
|
||||||
|
}
|
||||||
|
}, 200)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.header-title {
|
||||||
|
border-bottom: #e4e0e0 solid 1px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,44 @@
|
||||||
|
<template>
|
||||||
|
<OrderItem v-for="item in list" :key="item.id" :order="item" class="mb-10px" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import OrderItem from '@/views/mall/promotion/kefu/components/message/OrderItem.vue'
|
||||||
|
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
|
||||||
|
import { getOrderPage } from '@/api/mall/trade/order'
|
||||||
|
import { concat } from 'lodash-es'
|
||||||
|
|
||||||
|
defineOptions({ name: 'OrderBrowsingHistory' })
|
||||||
|
|
||||||
|
const list = ref<any>([]) // 列表
|
||||||
|
const total = ref(0) // 总数
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
userId: 0
|
||||||
|
})
|
||||||
|
const skipGetMessageList = computed(() => {
|
||||||
|
// 已加载到最后一页的话则不触发新的消息获取
|
||||||
|
return total.value > 0 && Math.ceil(total.value / queryParams.pageSize) === queryParams.pageNo
|
||||||
|
}) // 跳过消息获取
|
||||||
|
|
||||||
|
/** 获得浏览记录 */
|
||||||
|
const getHistoryList = async (val: KeFuConversationRespVO) => {
|
||||||
|
queryParams.userId = val.userId
|
||||||
|
const res = await getOrderPage(queryParams)
|
||||||
|
total.value = res.total
|
||||||
|
list.value = res.list
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载下一页数据 */
|
||||||
|
const loadMore = async () => {
|
||||||
|
if (skipGetMessageList.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
queryParams.pageNo += 1
|
||||||
|
const res = await getOrderPage(queryParams)
|
||||||
|
total.value = res.total
|
||||||
|
concat(list.value, res.list)
|
||||||
|
}
|
||||||
|
defineExpose({ getHistoryList, loadMore })
|
||||||
|
</script>
|
|
@ -0,0 +1,57 @@
|
||||||
|
<template>
|
||||||
|
<ProductItem
|
||||||
|
v-for="item in list"
|
||||||
|
:key="item.id"
|
||||||
|
:picUrl="item.picUrl"
|
||||||
|
:price="item.price"
|
||||||
|
:skuText="item.introduction"
|
||||||
|
:title="item.spuName"
|
||||||
|
:titleWidth="400"
|
||||||
|
class="mb-10px"
|
||||||
|
priceColor="#FF3000"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { getBrowseHistoryPage } from '@/api/mall/product/history'
|
||||||
|
import ProductItem from '@/views/mall/promotion/kefu/components/message/ProductItem.vue'
|
||||||
|
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
|
||||||
|
import { concat } from 'lodash-es'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ProductBrowsingHistory' })
|
||||||
|
|
||||||
|
const list = ref<any>([]) // 列表
|
||||||
|
const total = ref(0) // 总数
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
userId: 0,
|
||||||
|
userDeleted: false
|
||||||
|
})
|
||||||
|
const skipGetMessageList = computed(() => {
|
||||||
|
// 已加载到最后一页的话则不触发新的消息获取
|
||||||
|
return total.value > 0 && Math.ceil(total.value / queryParams.pageSize) === queryParams.pageNo
|
||||||
|
}) // 跳过消息获取
|
||||||
|
|
||||||
|
/** 获得浏览记录 */
|
||||||
|
const getHistoryList = async (val: KeFuConversationRespVO) => {
|
||||||
|
queryParams.userId = val.userId
|
||||||
|
const res = await getBrowseHistoryPage(queryParams)
|
||||||
|
total.value = res.total
|
||||||
|
list.value = res.list
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载下一页数据 */
|
||||||
|
const loadMore = async () => {
|
||||||
|
if (skipGetMessageList.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
queryParams.pageNo += 1
|
||||||
|
const res = await getBrowseHistoryPage(queryParams)
|
||||||
|
total.value = res.total
|
||||||
|
concat(list.value, res.list)
|
||||||
|
}
|
||||||
|
defineExpose({ getHistoryList, loadMore })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -1,4 +1,5 @@
|
||||||
import KeFuConversationList from './KeFuConversationList.vue'
|
import KeFuConversationList from './KeFuConversationList.vue'
|
||||||
import KeFuMessageList from './KeFuMessageList.vue'
|
import KeFuMessageList from './KeFuMessageList.vue'
|
||||||
|
import MemberBrowsingHistory from './history/MemberBrowsingHistory.vue'
|
||||||
|
|
||||||
export { KeFuConversationList, KeFuMessageList }
|
export { KeFuConversationList, KeFuMessageList, MemberBrowsingHistory }
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
<template>
|
|
||||||
<!-- 图片消息 -->
|
|
||||||
<template v-if="KeFuMessageContentTypeEnum.IMAGE === message.contentType">
|
|
||||||
<div
|
|
||||||
:class="[
|
|
||||||
message.senderType === UserTypeEnum.MEMBER
|
|
||||||
? `ml-10px`
|
|
||||||
: message.senderType === UserTypeEnum.ADMIN
|
|
||||||
? `mr-10px`
|
|
||||||
: ''
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<el-image
|
|
||||||
:initial-index="0"
|
|
||||||
:preview-src-list="[message.content]"
|
|
||||||
:src="message.content"
|
|
||||||
class="w-200px"
|
|
||||||
fit="contain"
|
|
||||||
preview-teleported
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { KeFuMessageContentTypeEnum } from '../tools/constants'
|
|
||||||
import { UserTypeEnum } from '@/utils/constants'
|
|
||||||
import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ImageMessageItem' })
|
|
||||||
defineProps<{
|
|
||||||
message: KeFuMessageRespVO
|
|
||||||
}>()
|
|
||||||
</script>
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
<template>
|
||||||
|
<!-- 消息组件 -->
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
message.senderType === UserTypeEnum.MEMBER
|
||||||
|
? `ml-10px`
|
||||||
|
: message.senderType === UserTypeEnum.ADMIN
|
||||||
|
? `mr-10px`
|
||||||
|
: ''
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { UserTypeEnum } from '@/utils/constants'
|
||||||
|
import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
|
||||||
|
|
||||||
|
defineOptions({ name: 'MessageItem' })
|
||||||
|
defineProps<{
|
||||||
|
message: KeFuMessageRespVO
|
||||||
|
}>()
|
||||||
|
</script>
|
|
@ -0,0 +1,146 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="isObject(getMessageContent)">
|
||||||
|
<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-16">
|
||||||
|
{{ formatOrderStatus(getMessageContent) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-for="item in getMessageContent.items" :key="item.id" class="border-bottom">
|
||||||
|
<ProductItem
|
||||||
|
:num="item.count"
|
||||||
|
:picUrl="item.picUrl"
|
||||||
|
:price="item.price"
|
||||||
|
:skuText="item.properties.map((property: any) => property.valueName).join(' ')"
|
||||||
|
:title="item.spuName"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="pay-box 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>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { fenToYuan, jsonParse } from '@/utils'
|
||||||
|
import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
|
||||||
|
import { isObject } from '@/utils/is'
|
||||||
|
import ProductItem from '@/views/mall/promotion/kefu/components/message/ProductItem.vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'OrderItem' })
|
||||||
|
const props = defineProps<{
|
||||||
|
message?: KeFuMessageRespVO
|
||||||
|
order?: any
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const getMessageContent = computed(() =>
|
||||||
|
typeof props.message !== 'undefined' ? jsonParse(props!.message!.content) : props.order
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化订单状态的颜色
|
||||||
|
*
|
||||||
|
* @param order 订单
|
||||||
|
* @return {string} 颜色的 class 名称
|
||||||
|
*/
|
||||||
|
function formatOrderColor(order: any) {
|
||||||
|
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: any) {
|
||||||
|
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: 28px;
|
||||||
|
|
||||||
|
.order-no {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-box {
|
||||||
|
.discounts-title {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: normal;
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discounts-money {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: normal;
|
||||||
|
color: #999;
|
||||||
|
font-family: OPPOSANS;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-color {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-color {
|
||||||
|
color: #faad14;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-color {
|
||||||
|
color: #ff3000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-color {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-color {
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,182 +0,0 @@
|
||||||
<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
|
|
||||||
:num="item.count"
|
|
||||||
:picUrl="item.picUrl"
|
|
||||||
: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: any) {
|
|
||||||
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: any) {
|
|
||||||
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: 80px;
|
|
||||||
|
|
||||||
.order-no {
|
|
||||||
font-size: 26px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-box {
|
|
||||||
.discounts-title {
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: normal;
|
|
||||||
color: #999999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discounts-money {
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: normal;
|
|
||||||
color: #999;
|
|
||||||
font-family: OPPOSANS;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-color {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-card-footer {
|
|
||||||
height: 100px;
|
|
||||||
|
|
||||||
.more-item-box {
|
|
||||||
padding: 20px;
|
|
||||||
|
|
||||||
.more-item {
|
|
||||||
height: 60px;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 26px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.more-btn {
|
|
||||||
color: #999999;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
width: 154px;
|
|
||||||
color: #333333;
|
|
||||||
font-size: 26px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning-color {
|
|
||||||
color: #faad14;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danger-color {
|
|
||||||
color: #ff3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-color {
|
|
||||||
color: #52c41a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-color {
|
|
||||||
color: #999999;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -110,33 +110,25 @@ const skuString = computed(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.score-img {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
margin: 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ss-order-card-warp {
|
.ss-order-card-warp {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background-color: #e2e2e2;
|
background-color: #e2e2e2;
|
||||||
|
|
||||||
.img-box {
|
.img-box {
|
||||||
width: 164px;
|
width: 80px;
|
||||||
height: 164px;
|
height: 80px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.order-img {
|
.order-img {
|
||||||
width: 164px;
|
width: 80px;
|
||||||
height: 164px;
|
height: 80px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.box-right {
|
.box-right {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
// width: 500px;
|
|
||||||
// height: 164px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.tool-box {
|
.tool-box {
|
||||||
|
@ -147,13 +139,13 @@ const skuString = computed(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-text {
|
.title-text {
|
||||||
font-size: 28px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 40px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spec-text {
|
.spec-text {
|
||||||
font-size: 24px;
|
font-size: 16px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #999999;
|
color: #999999;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
@ -165,15 +157,15 @@ const skuString = computed(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-text {
|
.price-text {
|
||||||
font-size: 24px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-family: OPPOSANS;
|
font-family: OPPOSANS;
|
||||||
}
|
}
|
||||||
|
|
||||||
.total-text {
|
.total-text {
|
||||||
font-size: 24px;
|
font-size: 16px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 24px;
|
line-height: 16px;
|
||||||
color: #999999;
|
color: #999999;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
<template>
|
|
||||||
<!-- 图片消息 -->
|
|
||||||
<template v-if="KeFuMessageContentTypeEnum.PRODUCT === message.contentType">
|
|
||||||
<div
|
|
||||||
:class="[
|
|
||||||
message.senderType === UserTypeEnum.MEMBER
|
|
||||||
? `ml-10px`
|
|
||||||
: message.senderType === UserTypeEnum.ADMIN
|
|
||||||
? `mr-10px`
|
|
||||||
: ''
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<ProductItem
|
|
||||||
:picUrl="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>
|
|
|
@ -1,29 +0,0 @@
|
||||||
<template>
|
|
||||||
<!-- 文本消息 -->
|
|
||||||
<template v-if="KeFuMessageContentTypeEnum.TEXT === message.contentType">
|
|
||||||
<div
|
|
||||||
v-dompurify-html="replaceEmoji(message.content)"
|
|
||||||
:class="[
|
|
||||||
message.senderType === UserTypeEnum.MEMBER
|
|
||||||
? `ml-10px`
|
|
||||||
: message.senderType === UserTypeEnum.ADMIN
|
|
||||||
? `mr-10px`
|
|
||||||
: ''
|
|
||||||
]"
|
|
||||||
class="flex items-center"
|
|
||||||
></div>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { KeFuMessageContentTypeEnum } from '../tools/constants'
|
|
||||||
import { UserTypeEnum } from '@/utils/constants'
|
|
||||||
import { useEmoji } from '../tools/emoji'
|
|
||||||
import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
|
|
||||||
|
|
||||||
defineOptions({ name: 'TextMessageItem' })
|
|
||||||
defineProps<{
|
|
||||||
message: KeFuMessageRespVO
|
|
||||||
}>()
|
|
||||||
const { replaceEmoji } = useEmoji()
|
|
||||||
</script>
|
|
|
@ -1,22 +1,28 @@
|
||||||
<template>
|
<template>
|
||||||
<el-row :gutter="10">
|
<el-row :gutter="10">
|
||||||
<!-- 会话列表 -->
|
<!-- 会话列表 -->
|
||||||
<el-col :span="8">
|
<el-col :span="6">
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<KeFuConversationList ref="keFuConversationRef" @change="handleChange" />
|
<KeFuConversationList ref="keFuConversationRef" @change="handleChange" />
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
</el-col>
|
</el-col>
|
||||||
<!-- 会话详情(选中会话的消息列表) -->
|
<!-- 会话详情(选中会话的消息列表) -->
|
||||||
<el-col :span="16">
|
<el-col :span="12">
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<KeFuMessageList ref="keFuChatBoxRef" @change="getConversationList" />
|
<KeFuMessageList ref="keFuChatBoxRef" @change="getConversationList" />
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<!-- 会员足迹(选中会话的会员足迹) -->
|
||||||
|
<el-col :span="6">
|
||||||
|
<ContentWrap>
|
||||||
|
<MemberBrowsingHistory ref="memberBrowsingHistoryRef" />
|
||||||
|
</ContentWrap>
|
||||||
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { KeFuConversationList, KeFuMessageList } from './components'
|
import { KeFuConversationList, KeFuMessageList, MemberBrowsingHistory } from './components'
|
||||||
import { WebSocketMessageTypeConstants } from './components/tools/constants'
|
import { WebSocketMessageTypeConstants } from './components/tools/constants'
|
||||||
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
|
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
|
||||||
import { getAccessToken } from '@/utils/auth'
|
import { getAccessToken } from '@/utils/auth'
|
||||||
|
@ -58,14 +64,16 @@ watchEffect(() => {
|
||||||
// 2.2 消息类型:KEFU_MESSAGE_TYPE
|
// 2.2 消息类型:KEFU_MESSAGE_TYPE
|
||||||
if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) {
|
if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) {
|
||||||
// 刷新会话列表
|
// 刷新会话列表
|
||||||
|
// TODO @puhui999:不应该刷新列表,而是根据消息,本地 update 列表的数据;
|
||||||
getConversationList()
|
getConversationList()
|
||||||
// 刷新消息列表
|
// 刷新消息列表
|
||||||
keFuChatBoxRef.value?.refreshMessageList()
|
keFuChatBoxRef.value?.refreshMessageList(JSON.parse(jsonMessage.content))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 2.3 消息类型:KEFU_MESSAGE_ADMIN_READ
|
// 2.3 消息类型:KEFU_MESSAGE_ADMIN_READ
|
||||||
if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_ADMIN_READ) {
|
if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_ADMIN_READ) {
|
||||||
// 刷新会话列表
|
// 刷新会话列表
|
||||||
|
// TODO @puhui999:不应该刷新列表,而是根据消息,本地 update 列表的数据;
|
||||||
getConversationList()
|
getConversationList()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -81,8 +89,10 @@ const getConversationList = () => {
|
||||||
|
|
||||||
/** 加载指定会话的消息列表 */
|
/** 加载指定会话的消息列表 */
|
||||||
const keFuChatBoxRef = ref<InstanceType<typeof KeFuMessageList>>()
|
const keFuChatBoxRef = ref<InstanceType<typeof KeFuMessageList>>()
|
||||||
|
const memberBrowsingHistoryRef = ref<InstanceType<typeof MemberBrowsingHistory>>()
|
||||||
const handleChange = (conversation: KeFuConversationRespVO) => {
|
const handleChange = (conversation: KeFuConversationRespVO) => {
|
||||||
keFuChatBoxRef.value?.getMessageList(conversation, true)
|
keFuChatBoxRef.value?.getNewMessageList(conversation)
|
||||||
|
memberBrowsingHistoryRef.value?.initHistory(conversation)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 初始化 */
|
/** 初始化 */
|
||||||
|
|
Loading…
Reference in New Issue