提交 636c5107 authored 作者: 王鹏飞's avatar 王鹏飞

chore: update

上级 63ad166f
import { ref, computed, onUnmounted, type Ref } from 'vue'
// 定义参数类型
interface CountdownOptions {
initialTime: number
autoStart?: boolean
onEnd?: () => void
}
// 定义返回类型接口
interface UseCountdown {
timeLeft: Ref<number>
formattedTime: Ref<string>
isRunning: Ref<boolean>
start: () => void
stop: () => void
reset: (newTime?: number) => void
}
export function useCountdown({ initialTime = 0, autoStart = false, onEnd }: CountdownOptions): UseCountdown {
const timeLeft = ref(initialTime) // 剩余时间(秒)
const isRunning = ref(false) // 是否在倒计时
let timer: number | null = null
// 格式化为 分钟:秒
const formattedTime = computed(() => {
const minutes = Math.floor(timeLeft.value / 60)
.toString()
.padStart(2, '0')
const seconds = (timeLeft.value % 60).toString().padStart(2, '0')
return `${minutes}:${seconds}`
})
// 开始倒计时
const start = () => {
if (isRunning.value || timeLeft.value <= 0) return
isRunning.value = true
timer = window.setInterval(() => {
if (timeLeft.value > 0) {
timeLeft.value--
} else {
stop()
if (onEnd) onEnd() // 倒计时结束后调用回调函数
}
}, 1000)
}
// 停止倒计时
const stop = () => {
isRunning.value = false
if (timer !== null) {
clearInterval(timer)
timer = null
}
}
// 重置倒计时
const reset = (newTime: number = initialTime) => {
stop()
timeLeft.value = newTime
if (autoStart) start()
}
// 组件卸载时清除定时器
onUnmounted(() => {
stop()
})
if (autoStart) start() // 自动开始倒计时
return {
timeLeft,
formattedTime,
isRunning,
start,
stop,
reset,
}
}
......@@ -54,7 +54,8 @@ const listOptions = computed(() => {
label: '状态',
prop: 'status',
computed({ row }) {
return getNameByValue(row.status, statusList)
const color = row.status === '1' ? 'var(--main-success-color)' : 'var(--main-color)'
return `<span style="color: ${color}">${getNameByValue(row.status, statusList)}</span>`
},
},
{ label: '更新时间', prop: 'updated_time' },
......
......@@ -51,7 +51,7 @@ async function handleUpdate() {
<el-dialog :title="title" :close-on-click-modal="false" @closed="$emit('update:modelValue', false)">
<el-row justify="center">
<el-col :sm="24" :md="16">
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" @submit.prevent="handleSubmit">
<el-form-item label="类别名称" prop="name">
<el-input v-model="form.name" placeholder="请输入" />
</el-form-item>
......
......@@ -30,7 +30,8 @@ const listOptions = {
label: '状态',
prop: 'status',
computed({ row }) {
return getNameByValue(row.status, statusList)
const color = row.status === '1' ? 'var(--main-success-color)' : 'var(--main-color)'
return `<span style="color: ${color}">${getNameByValue(row.status, statusList)}</span>`
},
},
{ label: '操作', slots: 'table-x', width: 160 },
......@@ -65,8 +66,8 @@ const handleRemove = async (row) => {
<el-button type="primary" @click="handleAdd()">新增类别</el-button>
</template>
<template #table-x="{ row }">
<el-button text type="primary" @click="handleUpdate(row)">编辑</el-button>
<el-button text type="primary" @click="handleAdd({ pid: row.id })">新增</el-button>
<el-button text type="primary" @click="handleUpdate(row)">编辑</el-button>
<el-button text type="primary" @click="handleRemove(row)">删除</el-button>
</template>
</AppList>
......
......@@ -30,6 +30,16 @@ export function getCategoryList(params?: { name?: string }) {
return httpRequest.get('/api/lab/v1/experiment/live-commodity-type/trees', { params })
}
// 获取最近添加的直播商品品类
export function getCategoryTopicList() {
return httpRequest.get('/api/lab/v1/experiment/live-commodity/recent-create-live-commodity-types')
}
// 搜索直播商品品类
export function searchCategory(params: { name: string }) {
return httpRequest.get('/api/lab/v1/experiment/live-commodity-type/search', { params })
}
// 获取直播商品品类下的所有属性
export function getAttrList(params: { live_commodity_type_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/live-commodity-attr/search', { params })
......
......@@ -33,19 +33,27 @@ function handleChange(value, item, attr) {
<template>
<div>
<el-form-item label="商品标题" prop="title" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]">
<div class="form-tips">标题不规范会有可能引起商品下架,影响您的正常销售,请点击学习商品发布规范认真填写</div>
<el-input placeholder="至少输入8个字(16个字符)以上,30个字(60个字符)以下" size="large" v-model="form.title" />
</el-form-item>
<el-form-item label="导购短标题" prop="shopping_guide_short_title">
<el-input
placeholder="建议填写简明准确的标题内容,避免重复表达"
size="large"
v-model="form.shopping_guide_short_title" />
<div class="form-tips">短标题可用于商品搜索、首页推荐、物流单等场景,请提炼商品关键信息,客观准确填写</div>
<div style="display: flex; width: 100%; align-items: center; gap: 20px">
<el-input
placeholder="建议填写简明准确的标题内容,避免重复表达"
size="large"
v-model="form.shopping_guide_short_title"
style="flex: 1" />
<el-button type="primary" plain>一键智能推荐</el-button>
</div>
</el-form-item>
<el-form-item label="重要属性" prop="live_commodity_attrs.importance_attrs">
<el-form-item :label="`重要属性 0/${attrs.importance_attrs.total}`" prop="live_commodity_attrs.importance_attrs">
<div class="form-tips">错误填写属性,会引起商品下架,请认真准确填写。</div>
<el-form-item
v-for="item in attrs.importance_attrs.items"
:key="item.id"
:label="item.name"
:required="item.is_importance == 1"
style="width: 200px; margin-right: 20px">
<el-input
placeholder="请输入"
......@@ -53,11 +61,12 @@ function handleChange(value, item, attr) {
@input="(value) => handleChange(value, item, 'importance_attrs')"></el-input>
</el-form-item>
</el-form-item>
<el-form-item label="非重要属性" prop="live_commodity_attrs.unimportance">
<el-form-item :label="`非重要属性 0/${attrs.unimportance_attrs.total}`" prop="live_commodity_attrs.unimportance">
<el-form-item
v-for="item in attrs.unimportance_attrs.items"
:key="item.id"
:label="item.name"
:required="item.is_importance == 1"
style="width: 200px; margin-right: 20px">
<el-input
placeholder="请输入"
......
<script setup>
import { Search } from '@element-plus/icons-vue'
const form = inject('form')
import { getCategoryList } from '../api'
import { getCategoryTopicList, getCategoryList, searchCategory } from '../api'
const options = ref([])
async function fetchList() {
const res = await getCategoryList()
options.value = res.data.trees
}
watchEffect(() => {
const topicList = ref([])
async function fetchTopicList() {
const res = await getCategoryTopicList()
topicList.value = res.data.items
}
onMounted(() => {
fetchList()
fetchTopicList()
})
const keyword = ref('')
const querySearchAsync = async () => {
const res = await searchCategory({ name: keyword.value })
return res.data.items
}
const handleSelect = (item) => {
form.live_commodity_type_id = item.id
}
</script>
<template>
<div>
<el-form-item label="选择商品类目" prop="live_commodity_type_id" :rules="[{ required: true, message: '请选择' }]">
<!-- <el-input placeholder="请输入关键词搜索商品分类" size="large" /> -->
<div class="form-tips">请按照商品类别谨慎选择对应类目,若是错放类目将可能会导致商品封禁。</div>
<el-autocomplete
clearable
placeholder="请输入关键词搜索商品分类"
size="large"
value-key="name"
v-model="keyword"
:prefix-icon="Search"
:fetch-suggestions="querySearchAsync"
@select="handleSelect">
<template #default="{ item }">
<div class="value">{{ item.name }}</div>
</template></el-autocomplete
>
<p class="topic-category">
最近创建:
<template v-for="(item, index) in topicList" :key="item.id">
<i v-if="index !== 0"></i>
<span @click="form.live_commodity_type_id = item.id">{{ item.name }}</span>
</template>
</p>
<el-cascader-panel
v-model="form.live_commodity_type_id"
:options="options"
......@@ -24,7 +64,21 @@ watchEffect(() => {
size="large"
filterable
clearable
style="width: 100%; margin-top: 20px" />
style="width: 100%" />
</el-form-item>
</div>
</template>
<style lang="scss">
.topic-category {
line-height: 40px;
color: #92939a;
span {
margin: 0 10px;
cursor: pointer;
&:hover {
color: var(--main-color);
}
}
}
</style>
<script setup>
import AppUpload from '@/components/base/AppUpload.vue'
import Upload from './Upload.vue'
const form = inject('form')
const handleCopy = () => {
form.picture_34_addreses = form.picture_addreses
}
</script>
<template>
<div>
<el-form-item label="主图" required>
<AppUpload v-model="form.picture_addreses"></AppUpload>
<el-form-item label="主图" prop="picture_addreses" :rules="[{ required: true, message: '该项为必填项' }]">
<div class="form-tips">影响商品曝光引流效果</div>
<div>
<Upload
v-model="form.picture_addreses"
:limit="5"
accept="image/*"
first-title="商品正面图"
first-tips="上传主图"
tips="上传辅助图"></Upload>
<div class="upload-tips">
<p>1、仅支持png/jpg/jpeg格式,宽高至少600*600px,大小2M内</p>
<p>2、低质图无法获得平台免费推荐机会,更多图片上传规范要求可点击</p>
</div>
</div>
</el-form-item>
<el-form-item label="主图3:4">
<AppUpload v-model="form.picture_34_addreses"></AppUpload>
<el-form-item label="">
<template #label>
<span>主图3:4</span>
<el-button type="primary" plain size="small" style="margin-left: 430px" @click="handleCopy"
>从主图一键填入</el-button
>
</template>
<div class="form-tips">有机会在搜索、推荐等场景展示,提升转化</div>
<div>
<Upload
v-model="form.picture_34_addreses"
:limit="5"
accept="image/*"
itemHeight="134px"
first-title="商品正面图"
first-tips="上传主图"
tips="上传辅助图"></Upload>
<div class="upload-tips">
<p>1、仅支持png/jpg/jpeg格式,图片宽高不低于375*500(750*1000最佳),大小2M内</p>
<p>
2、可优先上传3:4或1:1图,系统将有概率生成对应比例图填充至上传入口(如1:1图满足智能生成条件,系统可自动生成3:4图)
点击发布后方可生效
</p>
</div>
</div>
</el-form-item>
<el-form-item label="主图视频">
<AppUpload v-model="form.video_url"></AppUpload>
<div class="form-tips">有机会在搜索、推荐等场景展示,提升转化</div>
<Upload v-model="form.video_url" accept="video/*" tips="主图视频" isVideo></Upload>
<div class="upload-tips" style="margin-left: 20px">
<p>1、仅支持mp4格式上传,大小50M内,比例支持1:1、3:4 (分辨率不低于720P)</p>
<p>2、5s&lt;=时长&lt;=60s(30秒内最佳),画面整洁声音流畅,出镜商品与实际商品为同款且主体突出</p>
</div>
</el-form-item>
</div>
</template>
<style lang="scss">
.upload-tips {
padding: 10px 0;
color: #999;
line-height: 24px;
}
</style>
<script setup>
import { Plus, QuestionFilled } from '@element-plus/icons-vue'
import { deliveryMode, deliveryTime, orderStockCount } from '@/utils/dictionary'
const form = inject('form')
</script>
......@@ -6,12 +7,21 @@ const form = inject('form')
<template>
<div>
<el-form-item label="发货模式" prop="info.delivery_mode" :rules="[{ required: true, message: '请选择' }]">
<el-radio-group v-model="form.info.delivery_mode">
<el-radio v-for="item in deliveryMode" :key="item.value" v-bind="item"></el-radio>
<div class="form-tips">
注:现货商品切勿虚设为预售商品,请合理配置发货模式,以免影响成交转化。若切换发货模式,系统将不会回增原发货模式下被取消订单库存。
</div>
<el-radio-group v-model="form.info.delivery_mode" style="display: block">
<el-radio v-for="item in deliveryMode" :key="item.value" v-bind="item" style="display: block"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="商品规格"> </el-form-item>
<el-form-item label="商品规格">
<div class="form-tips">准确填写规格信息,有助于商品在搜索场景获取更多流量</div>
<el-button size="large" :icon="Plus">添加规格(1/3)</el-button>
</el-form-item>
<el-form-item label="发货时效" prop="info.delivery_time">
<div class="form-tips">
发货时效计算:买家支付后开始计算发货时效,请合理配置发货时效(注!现货库存切勿虚设为预售时效)
</div>
<el-radio-group v-model="form.info.delivery_time">
<el-radio v-for="item in deliveryTime" :key="item.value" v-bind="item"></el-radio>
</el-radio-group>
......@@ -31,7 +41,16 @@ const form = inject('form')
</el-table>
</el-form-item>
<el-form-item label="参考价" prop="info.reference_price">
<el-input v-model="form.info.reference_price" placeholder="请输入" />
<template #label>
<span>参考价</span>
<el-popover :width="200" trigger="hover" content="this is content, this is content, this is content">
<img src="/live/product.png" style="width: 100%" />
<template #reference>
<QuestionFilled style="width: 20px"></QuestionFilled>
</template>
</el-popover>
</template>
¥<el-input v-model="form.info.reference_price" placeholder="请输入" style="width: 300px; margin: 0 10px" />
</el-form-item>
<el-form-item label="订单库存计数" prop="info.order_stock_count">
<el-radio-group v-model="form.info.order_stock_count">
......
......@@ -6,12 +6,12 @@ const form = inject('form')
<template>
<div>
<el-form-item label="运费模板" prop="info.shipping_template" :rules="[{ required: true, message: '请选择' }]">
<el-select v-model="form.info.shipping_template">
<el-select v-model="form.info.shipping_template" style="width: 300px">
<el-option v-for="item in shippingTemplate" :key="item.value" v-bind="item"></el-option>
</el-select>
</el-form-item>
<el-form-item label="售后政策" prop="info.after_sales_policy" :rules="[{ required: true, message: '请选择' }]">
<el-select v-model="form.info.after_sales_policy">
<el-select v-model="form.info.after_sales_policy" style="width: 300px">
<el-option v-for="item in afterSalesPolicy" :key="item.value" v-bind="item"></el-option>
</el-select>
</el-form-item>
......
<script setup>
import { Plus, UploadFilled, Picture } from '@element-plus/icons-vue'
import { useFileDialog } from '@vueuse/core'
import { upload } from '@/utils/upload'
import dayjs from 'dayjs'
const props = defineProps({
modelValue: { type: [Array, String], default: () => [] },
multiple: { type: Boolean, default: false },
accept: { type: String, default: '' },
limit: { type: Number, default: 1 },
title: { type: String },
firstTitle: { type: String },
tips: { type: String, default: '' },
firstTips: { type: String },
itemHeight: { type: String, default: '100px' },
isVideo: { type: Boolean, default: false },
})
const emit = defineEmits(['update:modelValue'])
const index = ref(0)
const { open, onChange } = useFileDialog({
accept: props.accept,
multiple: props.multiple,
})
onChange(async (files) => {
const [file] = files
const res = await upload(file)
const result = {
name: file.name,
size: file.size,
type: file.type,
url: res,
upload_time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
}
if (typeof props.modelValue === 'string') {
emit('update:modelValue', result.url)
} else {
const updatedValue = [...props.modelValue]
updatedValue[index.value] = result
emit('update:modelValue', updatedValue)
}
})
const handleOpen = (i) => {
index.value = i
open()
}
const getTitle = (i) => (i === 0 && props.firstTitle) || props.title
const getTips = (i) => (i === 0 && props.firstTips) || props.tips
</script>
<template>
<div class="upload-wrapper">
<template v-if="Array.isArray(props.modelValue)">
<div v-for="(item, i) in props.limit" :key="i">
<el-popover placement="top" width="160px" trigger="hover">
<ul class="upload-popover">
<li @click="handleOpen(i)">
<i class="el-icon"><UploadFilled /></i>本地上传
</li>
<li v-if="!isVideo">
<i class="el-icon"><Picture /></i>图库选择
</li>
</ul>
<template #reference>
<div class="upload-item">
<template v-if="props.modelValue[i]">
<a :href="props.modelValue[i]?.url" target="_blank">
<img :src="props.modelValue[i]?.url" />
</a>
</template>
<template v-else>
<div class="upload-item__title">{{ getTitle(i) }}</div>
<i class="el-icon"><Plus /></i>
<div class="upload-item__tips">{{ getTips(i) }}</div>
</template>
</div>
</template>
</el-popover>
</div>
</template>
<template v-else>
<el-popover placement="top" width="160px" trigger="hover">
<ul class="upload-popover">
<li @click="handleOpen(0)">
<i class="el-icon"><UploadFilled /></i>本地上传
</li>
<li v-if="!isVideo">
<i class="el-icon"><Picture /></i>图库选择
</li>
</ul>
<template #reference>
<div class="upload-item" :style="{ height: props.itemHeight, width: props.itemHeight }">
<template v-if="props.modelValue">
<a :href="props.modelValue" target="_blank">
<video :src="props.modelValue" v-if="isVideo"></video>
<img :src="props.modelValue" v-else />
</a>
</template>
<template v-else>
<div class="upload-item__title">{{ props.firstTitle || props.title }}</div>
<i class="el-icon"><Plus /></i>
<div class="upload-item__tips">{{ props.firstTips || props.tips }}</div>
</template>
</div>
</template>
</el-popover>
</template>
</div>
</template>
<style lang="scss">
.upload-wrapper {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.upload-popover {
display: flex;
justify-content: space-around;
li {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
.el-icon {
font-size: 20px;
}
}
}
.upload-item {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100px;
height: v-bind(itemHeight);
border: 1px dashed var(--el-border-color-darker);
border-radius: 6px;
cursor: pointer;
overflow: hidden;
a,
video,
img {
width: 100%;
height: 100%;
object-fit: cover;
}
&__title {
position: absolute;
top: 0;
width: 100%;
background-color: var(--main-color);
font-size: 12px;
color: #fff;
text-align: center;
line-height: 20px;
}
&__tips {
font-size: 12px;
}
}
</style>
......@@ -26,7 +26,7 @@ const listOptions = computed(() => {
const list = data.list?.map((item) => {
item.picture_addreses = JSON.parse(item.picture_addreses)
item.picture_url = item.picture_addreses[0]?.url
item.picture_url_list = item.picture_addreses?.map((i) => i.url)
item.picture_url_list = item.picture_addreses?.filter((i) => i).map((i) => i.url)
return item
})
return { ...data, list }
......@@ -68,17 +68,18 @@ const handleRemove = async (row) => {
</template>
<template #table-picture="{ row }">
<el-image
fit="cover"
preview-teleported
:src="row.picture_url"
style="width: 100px; height: 100px"
:previewSrcList="row.picture_url_list"
preview-teleported></el-image>
style="width: 100px; height: 100px"></el-image>
</template>
<template #table-x="{ row }">
<el-button text type="primary">
<router-link :to="{ path: 'management/view', query: { id: row.id } }">查看</router-link>
</el-button>
<el-button text type="primary">
<router-link :to="{ path: 'management/view', query: { id: row.id } }">编辑</router-link>
<router-link :to="{ path: 'management/update', query: { id: row.id } }">编辑</router-link>
</el-button>
<el-button text type="primary" @click="handleRemove(row)">删除</el-button>
</template>
......
......@@ -39,16 +39,26 @@ const form = reactive({
})
provide('form', form)
const array2json = (arr) => {
const result = {}
arr.forEach((item) => {
result[item.id] = { ...item }
})
return result
}
async function fetchInfo() {
const res = await getProduct({ id: route.query.id })
Object.assign(form, res.data.detail, {
// live_commodity_attrs: {
// importance_attrs: Object.values(form.live_commodity_attrs.importance_attrs),
// unimportance_attrs: Object.values(form.live_commodity_attrs.unimportance_attrs),
// },
picture_addreses: JSON.parse(form.picture_addreses),
picture_34_addreses: JSON.parse(form.picture_34_addreses),
info: JSON.parse(form.info),
const detail = res.data.detail
const attrs = JSON.parse(detail.live_commodity_attrs)
Object.assign(form, detail, {
live_commodity_attrs: {
importance_attrs: array2json(attrs.importance_attrs),
unimportance_attrs: array2json(attrs.unimportance_attrs),
},
picture_addreses: JSON.parse(detail.picture_addreses),
picture_34_addreses: JSON.parse(detail.picture_34_addreses),
info: JSON.parse(detail.info),
})
}
watchEffect(() => {
......@@ -107,9 +117,9 @@ async function handleUpdate(params) {
<el-tab-pane label="服务与履约" :name="5"></el-tab-pane>
</el-tabs>
<el-row justify="center" style="min-height: 400px">
<el-col :sm="24" :md="16">
<el-col :xl="16">
<!-- <el-card shadow="never"> -->
<el-form label-position="top" :model="form" ref="formRef">
<el-form label-position="top" :model="form" :disabled="action == 'view'" ref="formRef" @submit.prevent>
<!-- 商品类目 -->
<FormCategory v-if="activeName === 1"></FormCategory>
<!-- 基础信息 -->
......@@ -121,11 +131,11 @@ async function handleUpdate(params) {
<!-- 服务与履约 -->
<FormService v-if="activeName === 5"></FormService>
</el-form>
<el-row justify="center">
<el-row justify="center" style="margin: 100px 0 50px">
<el-button type="primary" plain v-if="activeName === 1" @click="$router.back()">取消</el-button>
<el-button type="primary" plain @click="handlePrev" v-if="activeName > 1">上一步</el-button>
<el-button type="primary" @click="handleNext" v-if="activeName < 5">下一步</el-button>
<el-button type="primary" @click="handleSubmit" v-if="activeName === 5">保存</el-button>
<el-button type="primary" @click="handleSubmit" v-if="activeName === 5 && action !== 'view'">保存</el-button>
</el-row>
<!-- </el-card> -->
</el-col>
......@@ -147,5 +157,14 @@ async function handleUpdate(params) {
.el-card {
background-color: rgba(242, 242, 242, 0.49);
}
.form-tips {
position: absolute;
right: 0;
top: -30px;
z-index: 100;
line-height: 22px;
color: #92939a;
}
}
</style>
import httpRequest from '@/utils/axios'
// 获取实验直播话术的列表
export function getTalkList(params?: { name?: string }) {
export function getTalkList(params?: {
name?: string
live_commodity_id?: string
live_commodity_type_id?: string
live_commodity_title?: string
}) {
return httpRequest.get('/api/lab/v1/experiment/live-speeches/list', { params })
}
......
......@@ -19,7 +19,7 @@ const form = reactive({
live_commodity_id: '',
selling_point: '',
marketing_campaign: '',
duration: '10分钟',
duration: '10',
content: '',
})
watchEffect(() => {
......@@ -63,10 +63,12 @@ async function handleUpdate() {
emit('update')
emit('update:modelValue', false)
}
const show = ref(false)
</script>
<template>
<el-dialog :title="title" :close-on-click-modal="false" @closed="$emit('update:modelValue', false)">
<el-dialog :title="title" :close-on-click-modal="false" @closed="$emit('update:modelValue', false)" width="1000px">
<el-row justify="center" :gutter="40">
<el-col :sm="24" :md="12">
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" :disabled="action === 'view'">
......@@ -77,13 +79,18 @@ async function handleUpdate() {
<LiveProductSelect v-model="form.live_commodity_id"></LiveProductSelect>
</el-form-item>
<el-form-item label="商品卖点" prop="selling_point">
<el-input type="textarea" v-model="form.selling_point" placeholder="多个卖点请使用“;”分割。"></el-input>
<el-input
type="textarea"
v-model="form.selling_point"
placeholder="多个卖点请使用“;”分割。"
:rows="4"></el-input>
</el-form-item>
<el-form-item label="营销活动" prop="marketing_campaign">
<el-input
type="textarea"
v-model="form.marketing_campaign"
placeholder="多个营销活动请使用“;”分割。"></el-input>
placeholder="多个营销活动请使用“;”分割。"
:rows="4"></el-input>
</el-form-item>
<el-form-item label="话术时长" prop="duration">
<el-radio-group v-model="form.duration">
......@@ -95,7 +102,52 @@ async function handleUpdate() {
<el-col :sm="24" :md="12" style="border-left: 1px solid #dcdfe6">
<div style="text-align: center">
<h2 style="margin-bottom: 20px">直播话术</h2>
<el-button type="primary" size="large">AI生成直播话术</el-button>
<el-button type="primary" size="large" @click="show = true">
{{ show ? '再次生成直播话术' : 'AI生成直播话术' }}
</el-button>
</div>
<div class="live-talk-content" v-if="show">
<p class="t1">商品引入</p>
<p>
<span>开场互动</span>
亲爱的家人们,欢迎来到默认主播的直播间呀!今天可是有超棒的宝贝要给大家分享哦,大家有没有对电脑硬件感兴趣的呀?快来跟主播互动一下吧!
</p>
<p>
<span>引出话题</span>
说到电脑,那可是我们生活和工作中不可或缺的一部分呢。大家平时使用电脑的时候有没有遇到过卡顿、运行不流畅的情况呀?今天我带来的这款
1DIY 兼容机,就能完美解决这些问题哦!
</p>
<p>
<span>引出商品</span>
这款 1DIY
兼容机,它可是经过精心设计和配置的呢。它能够满足大家各种不同的使用需求,无论是日常办公、学习,还是玩大型游戏,都能轻松应对。
</p>
<p>
<span>目标人群场景锁定</span>
对于那些对电脑性能有较高要求的朋友们,这款兼容机绝对是你们的最佳选择。比如那些经常需要处理大量数据、进行图形设计的专业人士,或者是喜欢玩高画质游戏的游戏爱好者们,它都能给你们带来超棒的体验哦!
</p>
<p class="t1">商品讲解</p>
<p>
<span>产品卖点讲解</span>
咱们这款兼容机的第一个卖点就是它的强大性能。它配备了最新的处理器和高性能显卡,运行速度那叫一个快,让你在使用过程中感受不到丝毫卡顿。而且内存和硬盘的容量也非常大,能够存储大量的文件和数据。第二个卖点就是它的兼容性非常好,能够兼容各种软件和硬件,不用担心出现不兼容的情况。
</p>
<p>
<span>观众互动</span>
家人们,你们觉得什么样的电脑配置才是最适合自己的呢?快在评论区留言告诉主播哦,主播会根据大家的需求给大家更详细的介绍。
</p>
<p class="t1">引导转化</p>
<p>
<span>催单话术</span>
这么好的一款兼容机,大家是不是已经心动啦?别再犹豫啦,赶紧下单把它带回家吧!现在下单还有特别的优惠活动哦,错过可就太可惜啦!
</p>
<p>
<span>强调购买方式</span>
购买的方式非常简单哦,大家只需要点击屏幕下方的购买按钮,按照提示填写好收货信息就可以啦。我们会尽快安排发货,让大家尽快收到心仪的电脑。
</p>
<p>
<span>结尾互动</span>
好啦,今天的直播就到这里啦,感谢家人们的陪伴和支持。如果大家还有什么问题或者建议,都可以随时在直播间里留言哦。下次直播我们还会有更多惊喜好物等着大家,再见啦,家人们!
</p>
</div>
</el-col>
</el-row>
......@@ -109,3 +161,25 @@ async function handleUpdate() {
</template>
</el-dialog>
</template>
<style lang="scss">
.live-talk-content {
font-size: 12px;
p {
line-height: 22px;
}
.t1 {
padding: 0 5px;
display: inline-block;
background-color: rgba(25, 102, 255, 0.08);
color: rgb(25, 102, 255);
border: 1px solid transparent;
}
span {
padding: 0 5px;
color: rgb(25, 102, 255);
border: 1px solid rgba(25, 102, 255, 0.12);
}
}
</style>
import httpRequest from '@/utils/axios'
// 获取实验直播练习的列表
export function getTestList(params?: { name?: string }) {
export function getTestList(params?: {
live_commodity_id?: string
live_commodity_type_id?: string
live_commodity_title?: string
}) {
return httpRequest.get('/api/lab/v1/experiment/live-practice/list', { params })
}
......@@ -24,3 +28,13 @@ export function updateTest(data: { id: string; name: string; status: string }) {
export function deleteTest(data: { id: string }) {
return httpRequest.post('/api/lab/v1/experiment/live-practice/delete', data)
}
// 获取实验直播话术的列表
export function getTalkList(params?: {
name?: string
live_commodity_id?: string
live_commodity_type_id?: string
live_commodity_title?: string
}) {
return httpRequest.get('/api/lab/v1/experiment/live-speeches/list', { params })
}
......@@ -2,7 +2,7 @@
import { ElMessage } from 'element-plus'
import { liveTestDuration, liveTestUploadWay } from '@/utils/dictionary'
import LiveProductSelect from '@/components/LiveProductSelect.vue'
import { createTest, updateTest } from '../api'
import { createTest, updateTest, getTalkList } from '../api'
const props = defineProps(['data'])
......@@ -15,7 +15,7 @@ const formRef = ref()
const form = reactive({
live_commodity_id: '',
live_speech_id: '',
duration: '10分钟',
duration: '10',
upload_way: '1',
})
......@@ -26,6 +26,16 @@ const rules = ref({
upload_way: [{ required: true, message: '请选择' }],
})
const options = ref([])
watch(
() => form.live_commodity_id,
() => {
form.live_speech_id = ''
getTalkList({ live_commodity_id: form.live_commodity_id }).then((res) => {
options.value = res.data.list
})
}
)
// 提交
async function handleSubmit() {
await formRef.value?.validate()
......@@ -57,7 +67,9 @@ async function handleUpdate() {
<LiveProductSelect v-model="form.live_commodity_id"></LiveProductSelect>
</el-form-item>
<el-form-item label="选择直播话术" prop="live_speech_id">
<el-select v-model="form.live_speech_id" style="width: 100%" placeholder="请选择"></el-select>
<el-select v-model="form.live_speech_id" style="width: 100%" placeholder="请选择">
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="选择练习时长" prop="duration">
<el-radio-group v-model="form.duration">
......
......@@ -3,45 +3,55 @@ import { useUserStore } from '@/stores/user'
import LiveCover from './LiveCover.vue'
import LiveStream from './LiveStream.vue'
import LivePlayback from './LivePlayback.vue'
import { ref, watch, defineExpose, defineProps } from 'vue'
defineProps({
const props = defineProps({
isView: { type: Boolean, default: false },
onStart: { type: Function, default: () => {} },
onStop: { type: Function, default: () => {} },
})
const userStore = useUserStore()
const live = ref(null)
const enabled = ref(false)
function start() {
if (live.value) {
enabled.value = true
live.value.start()
const toggleLive = () => {
if (enabled.value) {
live.value?.start()
props.onStart?.()
} else {
live.value?.stop()
props.onStop?.()
}
}
watch(enabled, toggleLive)
function start() {
if (live.value) enabled.value = true
}
function stop() {
if (live.value) {
enabled.value = false
live.value.stop()
}
if (live.value) enabled.value = false
}
defineExpose({ enabled, start, stop })
</script>
<template>
<div class="live">
<div class="live-hd">
<p>主播:{{ userStore.user.name }}</p>
<img :src="enabled ? '/live/live2.png' : '/live/live1.png'" style="height: 20px" v-if="!isView" />
<img :src="enabled ? '/live/live2.png' : '/live/live1.png'" v-if="!isView" alt="直播状态" class="live-icon" />
</div>
<div class="live-bd">
<LiveStream ref="live" v-if="isView"></LiveStream>
<LivePlayback ref="live" v-else></LivePlayback>
<component :is="isView ? LivePlayback : LiveStream" ref="live" />
<LiveCover v-if="enabled" />
</div>
<div class="live-ft" v-if="!isView">
<el-button type="primary" size="large" round style="width: 80%" @click="start" v-if="!enabled"
>开始练习</el-button
>
<el-button type="primary" size="large" round style="width: 80%" @click="stop" v-else>结束直播练习</el-button>
<el-button type="primary" size="large" round style="width: 80%" @click="enabled ? stop() : start()">
{{ enabled ? '结束直播练习' : '开始练习' }}
</el-button>
</div>
</div>
</template>
......@@ -49,20 +59,27 @@ function stop() {
<style lang="scss">
.live {
width: 375px;
}
.live-hd {
display: flex;
justify-content: space-between;
}
.live-bd {
position: relative;
margin: 10px 0;
height: 667px;
border-radius: 8px;
overflow: hidden;
background-color: #000;
}
.live-ft {
text-align: center;
&-hd {
display: flex;
justify-content: space-between;
.live-icon {
height: 20px;
}
}
&-bd {
position: relative;
margin: 10px 0;
height: 667px;
border-radius: 8px;
overflow: hidden;
background-color: #000;
}
&-ft {
text-align: center;
}
}
</style>
<script setup>
import Live from '../components/Live.vue'
import { getTest } from '../api'
import { useCountdown } from '@/composables/useCountdown'
defineProps({
isView: { type: Boolean, default: false },
})
const route = useRoute()
const hotList = ref(['一键开启', '高效便捷', '一键开启', '高效便捷', '一键开启', '高效便捷', '一键开启', '高效便捷'])
const actList = ref(['7天无理由退货', '7天无理由退货', '7天无理由退货', '7天无理由退货', '7天无理由退货'])
const { timeLeft, formattedTime, start, stop } = useCountdown({
// 倒计时结束
onEnd: () => {
live.value?.stop()
},
})
const live = ref(null)
const detail = ref(null)
async function fetchInfo() {
const res = await getTest({ id: route.query.id })
detail.value = res.data.detail
timeLeft.value = parseInt(detail.value?.duration) || 0
}
watchEffect(() => {
fetchInfo()
})
// 商品卖点
const hotList = computed(() => {
return detail.value?.live_speech.selling_point.split(';')
})
// 营销活动
const actList = computed(() => {
return detail.value?.live_speech.marketing_campaign.split(';')
})
</script>
<template>
<AppCard title="直播练习" full>
<div class="live-row">
<div class="live-col"><Live isView /></div>
<div class="live-col"><Live ref="live" :isView="isView" :onStart="start" :onStop="stop" /></div>
<div class="live-col" style="flex: 1">
<h2 class="h2-title">直播话术</h2>
<div class="live-talk-content">
......@@ -62,30 +89,32 @@ const actList = ref(['7天无理由退货', '7天无理由退货', '7天无理
<div class="live-col" style="width: 350px">
<div class="live-col-box" v-if="isView">
<h2 class="h2-title">直播数据</h2>
<dl>
<dt>观众总人数:</dt>
<dd>100</dd>
</dl>
<dl>
<dt>最高峰人数:</dt>
<dd>100</dd>
</dl>
<dl>
<dt>点赞数:</dt>
<dd>9778</dd>
</dl>
<dl>
<dt>刷礼物人数:</dt>
<dd>40</dd>
</dl>
<dl>
<dt>刷礼物总数:</dt>
<dd>1200</dd>
</dl>
<div class="live-data">
<dl>
<dt>观众总人数:</dt>
<dd>100</dd>
</dl>
<dl>
<dt>最高峰人数:</dt>
<dd>100</dd>
</dl>
<dl>
<dt>点赞数:</dt>
<dd>9778</dd>
</dl>
<dl>
<dt>刷礼物人数:</dt>
<dd>40</dd>
</dl>
<dl>
<dt>刷礼物总数:</dt>
<dd>1200</dd>
</dl>
</div>
</div>
<div class="live-col-box" v-else>
<h2 class="h2-title">倒计时</h2>
<h3 class="live-time">14 : 55</h3>
<h3 class="live-time">{{ formattedTime }}</h3>
</div>
<div class="live-col-box">
<h2 class="h2-title">商品卖点</h2>
......@@ -131,6 +160,16 @@ const actList = ref(['7天无理由退货', '7天无理由退货', '7天无理
border: 1px solid rgba(105, 113, 140, 0.12);
}
}
.live-data {
padding: 0 0 20px 10px;
dl {
margin: 10px 0;
display: flex;
}
dt {
font-weight: bold;
}
}
.live-time {
height: 140px;
font-size: 72px;
......
<script setup>
import { ElMessage } from 'element-plus'
import LiveProductCategory from '@/components/LiveProductCategory.vue'
import { getTestList } from '../api'
import { useFileDialog } from '@vueuse/core'
import { upload } from '@/utils/upload'
const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
......@@ -10,7 +13,7 @@ const handleRefresh = () => {
appList.value?.refetch()
}
const listParams = reactive({ name: '', live_commodity_type_id: '' })
const listParams = reactive({ live_commodity_title: '', live_commodity_type_id: '' })
// 列表配置
const listOptions = computed(() => {
......@@ -26,49 +29,36 @@ const listOptions = computed(() => {
},
filters: [
{ label: '商品品类', prop: 'live_commodity_type_id', slots: 'filter-category' },
{ label: '商品标题', prop: 'product_name', type: 'input' },
{ label: '商品标题', prop: 'live_commodity_title', type: 'input' },
],
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '直播ID', prop: 'id' },
{ label: '商品标题', prop: 'product_name' },
{ label: '直播话术', prop: 'name' },
{ label: '所属商品品类', prop: 'product_category' },
{ label: '商品标题', prop: 'live_commodity_title' },
{ label: '直播话术', prop: 'live_commodity_speeches_name' },
{ label: '所属商品品类', prop: 'live_commodity_type_full_name' },
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 200 },
],
data: [
{
id: 1,
product_name: '手机',
name: '手机销售',
product_category: '智能手机',
updated_time: '2021-07-30 14:59:56',
},
{
id: 1,
product_name: '手机',
name: '手机销售',
product_category: '智能手机',
updated_time: '2021-07-30 14:59:56',
},
{
id: 1,
product_name: '手机',
name: '手机销售',
product_category: '智能手机',
updated_time: '2021-07-30 14:59:56',
},
],
}
})
const formVisible = ref(false)
const handleUpload = () => {
const dialog = useFileDialog()
dialog.open({ accept: 'video/mp4' })
dialog.onChange(async ([file]) => {
const res = await upload(file)
console.log(file, res)
ElMessage.success('上传成功')
})
}
</script>
<template>
<AppCard title="直播练习">
<AppList v-bind="listOptions" re="appList">
<AppList v-bind="listOptions" ref="appList">
<template #header-buttons>
<el-button type="primary" @click="formVisible = true">添加直播练习</el-button>
</template>
......@@ -76,7 +66,7 @@ const formVisible = ref(false)
<LiveProductCategory v-model="listParams.live_commodity_type_id" @change="handleRefresh"></LiveProductCategory>
</template>
<template #table-x="{ row }">
<el-button text type="primary">上传</el-button>
<el-button text type="primary" @click="handleUpload">上传</el-button>
<el-button text type="primary">
<router-link :to="{ path: 'test/demo', query: { id: row.id } }">练习</router-link>
</el-button>
......
......@@ -158,9 +158,9 @@ export const textPurposeList = [
// 直播练习时长
export const liveTestDuration = [
{ label: '10分钟', value: '10分钟' },
{ label: '15分钟 ', value: '15分钟' },
{ label: '20分钟 ', value: '20分钟' },
{ label: '10分钟', value: '10' },
{ label: '15分钟 ', value: '15' },
{ label: '20分钟 ', value: '20' },
]
// 直播练习时长
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论