Commit a065d056 by haojie

新增中控台

parent eec1262b
<svg width="12" height="12" viewBox="0 0 12 12" fill="" xmlns="http://www.w3.org/2000/svg">
<path d="M2.69544 10C2.51747 10 2.33947 9.93208 2.20367 9.79632C1.93211 9.52472 1.93211 9.08441 2.20367 8.81282L8.81282 2.2037C9.08439 1.9321 9.52471 1.9321 9.79633 2.2037C10.0679 2.47527 10.0679 2.91561 9.79633 3.1872L3.18718 9.79632C3.05138 9.93212 2.8734 10 2.69541 10H2.69544Z" fill=""/>
<path d="M9.3046 9.99996C9.12662 9.99996 8.94863 9.93205 8.81283 9.79629L2.20367 3.18719C1.93211 2.91562 1.93211 2.47526 2.20367 2.20369C2.47523 1.93212 2.91559 1.93209 3.18717 2.20369L9.79633 8.81278C10.0679 9.08438 10.0679 9.52469 9.79633 9.79628C9.66056 9.93208 9.48258 10 9.30458 10L9.3046 9.99996Z" fill=""/>
</svg>
\ No newline at end of file
......@@ -7,6 +7,7 @@
class="c-dialog-confirm-default"
:placement="placement"
:closeOnOverlayClick="closeOnOverlayClick"
:zIndex="zIndex"
>
<template #body>
<div class="custom-confirm-dialog-body">
......@@ -40,6 +41,7 @@ const props = withDefaults(
className?: string;
title?: string;
closeOnOverlayClick?: boolean;
zIndex?: number;
}>(),
{
footer: null,
......@@ -48,6 +50,7 @@ const props = withDefaults(
className: '',
title: '',
closeOnOverlayClick: true,
zIndex: 2500,
},
);
const emit = defineEmits(['update:modelValue', 'confirm']);
......
......@@ -9,7 +9,11 @@ export default defineComponent({
setup(props, { emit, slots }) {
let currentTab: { value: string } = inject('currentTab');
return () => {
return <div v-show={currentTab.value === props.name}>{slots.default?.()}</div>;
return (
<div class="custom-tab-panel" v-show={currentTab.value === props.name}>
{slots.default?.()}
</div>
);
};
},
});
......@@ -169,7 +169,7 @@ export default defineComponent({
''
)}
</div>
<div class="c-tabs-content">{slots.default?.()}</div>
<div class={['c-tabs-content', 'narrow-scrollbar']}>{slots.default?.()}</div>
</div>
);
},
......
@import '@/style/variables.less';
.custom-izable-page {
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
.header {
margin: 20px 0;
padding: 20px 0;
.da();
font-size: @size-24;
font-weight: 700;
......@@ -52,6 +56,9 @@
}
}
.izable-page-tabs {
margin-top: 50px;
padding-top: 50px;
flex: 1;
box-sizing: border-box;
overflow: hidden;
}
}
......@@ -6,21 +6,23 @@
class="c-dialog-default"
:destroyOnClose="destroyOnClose"
:placement="placement"
:zIndex="zIndex"
@close="onClose"
>
<slot></slot>
<div class="header-default">
<slot name="header"></slot>
</div>
<slot name="body"></slot>
<template v-if="footer">
<slot name="footer"> </slot>
<template #header>
<div class="header-default">
<slot name="header"></slot>
</div>
</template>
<slot name="body"></slot>
<template #footer>
<div class="footer-default" v-if="footer === null">
<Button @click="visible = false" class="footer-cancel footer-public-btn">取消</Button>
<Button @click="onConfirm" class="footer-confrim footer-public-btn">确定</Button>
</div>
<slot name="footer">
<div class="footer-default" v-if="footer === null">
<Button @click="visible = false" class="footer-cancel footer-public-btn">取消</Button>
<Button @click="onConfirm" class="footer-confrim footer-public-btn">确定</Button>
</div>
</slot>
</template>
</t-dialog>
</template>
......@@ -37,12 +39,14 @@ const props = withDefaults(
placement?: string;
destroyOnClose?: boolean;
className?: string;
zIndex?: number;
}>(),
{
footer: null,
placement: 'center',
destroyOnClose: false,
className: '',
zIndex: 2500,
},
);
const emit = defineEmits(['update:modelValue', 'confirm', 'close']);
......
<template>
<div class="custom-loading">
<div :class="['custom-loading', mask ? 'custom-loading-mask' : '']">
<div class="spinner"></div>
</div>
</template>
<style>
<script lang="ts" setup>
const props = withDefaults(
defineProps<{
mask?: boolean;
}>(),
{
mask: false,
},
);
</script>
<style lang="less">
.custom-loading {
display: flex;
justify-content: center;
align-items: center;
height: 100px;
}
.spinner {
width: 40px;
height: 40px;
border-radius: 50%;
border: 4px solid #ccc;
border-top-color: #888;
animation: spin 1s infinite linear;
.spinner {
width: 40px;
height: 40px;
border-radius: 50%;
border: 4px solid #ccc;
border-top-color: #888;
animation: spin 1s infinite linear;
}
}
@keyframes spin {
......
......@@ -12,7 +12,9 @@
overlayClassName: [className, 'custom-select-popup'],
}"
>
<t-option v-for="item in options" :key="item.value" :value="item.value" :label="item.label"></t-option>
<slot>
<t-option v-for="item in options" :key="item.value" :value="item.value" :label="item.label"></t-option>
</slot>
</TSelect>
</div>
</template>
......
......@@ -111,6 +111,9 @@ watch(
border-top-right-radius: 8px;
}
}
.custom-change-name-box {
margin-top: 6px;
}
.hover {
position: absolute;
top: 0;
......
......@@ -81,13 +81,13 @@ watch(
.custom-textarea-box {
width: 100%;
background: #181818;
// border: @main-border;
border: 1px solid transparent;
color: white;
resize: none;
border-radius: 8px;
transition: border 0.2s;
&:focus-within {
border-color: #00f9f9;
border: 1px solid #00f9f9;
transition: all 0.2s;
}
.custom-t-textarea {
......
<template>
<div class="dialog-confirm-footer">
<template v-if="saveConfirm">
<Button theme="opacity" class="save-confirm-button" @click="onSaveConfirm">{{ saveConfirm }}</Button>
</template>
<template v-if="save">
<Button class="save-button" theme="green" @click="onSave">{{ save }}</Button>
</template>
</div>
</template>
<script lang="ts" setup>
import Button from '@/components/Button.vue';
const props = withDefaults(
defineProps<{
saveConfirm?: string;
save?: string;
}>(),
{
saveConfirm: '',
save: '',
},
);
const emit = defineEmits(['saveConfirm', 'save']);
// 保存并继续
const onSaveConfirm = () => {
emit('saveConfirm');
};
// 保存
const onSave = () => {
emit('save');
};
</script>
<style lang="less">
@import '@/style/variables.less';
.dialog-confirm-footer {
background: #303030;
height: 60px;
width: 100%;
padding: 14px;
display: flex;
justify-content: flex-end;
align-items: flex-start;
.save-confirm-button,
.save-button {
font-size: @size-14;
font-weight: 700;
}
}
</style>
<template>
<div :class="['custom-usiness-form-item', className]">
<div class="label">{{ label }}</div>
<slot />
</div>
</template>
<script lang="ts" setup>
const props = withDefaults(
defineProps<{
label: string;
className?: string;
}>(),
{
className: '',
},
);
</script>
<style lang="less">
@import '@/style/variables.less';
.custom-usiness-form-item {
display: flex;
.label {
margin-right: 6px;
white-space: nowrap;
font-size: @size-15;
color: white;
}
}
.custom-usiness-form-item + .custom-usiness-form-item {
margin-top: 20px;
}
</style>
......@@ -2,7 +2,7 @@
<div class="image-custom-my-person-box">
<Loading v-show="loading"></Loading>
<div class="my-person-items">
<template v-for="item in personList.list" :key="item.id">
<template v-for="item in list" :key="item.id">
<template v-if="item.audit_status == LIVE_AUDIT_STATUS.LIVE_AUDIT_STATUS_FINISH">
<CardOne :id="item.id" :img="item.cover_url" :name="item.name" :edit="true">
<template #hover>
......@@ -53,15 +53,17 @@ import ConfirmDialog from '@/components/ConfirmDialog.vue';
import Button from '@/components/Button.vue';
import Loading from '@/components/loading.vue';
import CardOne from '@/components/cardOne.vue';
import { onMounted, reactive, ref } from 'vue';
import { getDigitalPeopleList } from '@/service/Common';
import { ref } from 'vue';
import { LIVE_AUDIT_STATUS } from '@/service/Live';
import { jumpToCreateLivePage } from '@/router/jump';
const personList = reactive({
list: [],
});
const loading = ref(false);
const props = withDefaults(
defineProps<{
list: any[];
loading: boolean;
}>(),
{},
);
const confirmDialog = ref(false);
const deleteId = ref();
......@@ -76,13 +78,6 @@ const confirm = () => {
//
};
// 获取我的数字人列表
const getList = async () => {
loading.value = true;
let res = await getDigitalPeopleList();
personList.list = res.myList;
loading.value = false;
};
const startLive = (item: any) => {
jumpToCreateLivePage(
{
......@@ -92,20 +87,15 @@ const startLive = (item: any) => {
true,
);
};
onMounted(() => {
getList();
});
</script>
<style lang="less">
@import '@/style/variables';
.image-custom-my-person-box {
padding: 30px 40px;
padding: 30px 30px;
border-radius: 4px 4px 0px 0px;
background: #303030;
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.04);
min-height: 284px;
position: relative;
.my-person-items {
display: flex;
......
<template>
<div class="image-custom-record">
<div class="record-items" v-for="item in recordList.list" :key="item.id">
<div class="record-items" v-for="item in list" :key="item.id">
<div class="left">
<img :src="item.cover_url" alt="" />
</div>
......@@ -20,26 +20,16 @@
</template>
<script lang="tsx" setup>
import { onMounted, reactive, ref } from 'vue';
import { getDigitalPeopleList } from '@/service/Common';
import { ref } from 'vue';
import CustomizationStatus from '@/components/CustomizationStatus';
const recordList = reactive({
list: [],
});
const loading = ref(false);
// 获取我的数字人列表
const getList = async () => {
loading.value = true;
let res = await getDigitalPeopleList();
recordList.list = res.myList;
loading.value = false;
};
onMounted(() => {
getList();
});
const props = withDefaults(
defineProps<{
list: any[];
loading: boolean;
}>(),
{},
);
</script>
<style lang="less">
......
......@@ -8,12 +8,12 @@
:dialogInfo="dialogInfo"
:label="'形象定制'"
>
<CustomTabs v-model="currentTab" theme="dark2">
<CustomTabs v-model="currentTab" theme="dark2" class="custom-tabs-flex">
<CustomTabPanel name="1" label="我的数字人">
<MyDigitalPerson></MyDigitalPerson>
<MyDigitalPerson :list="personList.list" :loading="loading"></MyDigitalPerson>
</CustomTabPanel>
<CustomTabPanel name="2" label="生成记录">
<Record></Record>
<Record :list="personList.list" :loading="loading"></Record>
</CustomTabPanel>
</CustomTabs>
</Customizable>
......@@ -33,17 +33,25 @@ import CustomTabs from '@/components/CustomTabs';
import CustomTabPanel from '@/components/CustomTabPanel';
import Customizable from '@/components/Customizable';
import PersonSvg from '@/assets/svg/custom/person.svg';
import { onMounted, ref } from 'vue';
import { onMounted, ref, reactive } from 'vue';
import { customizedImageSubmission } from '@/utils/api/userApi';
import { show_message } from '@/utils/tool';
import routerConfig from '@/router/tool';
import { useStore } from 'vuex';
import { jumpPageAddNavigation } from '@/router/jump';
import { getDigitalPeopleList } from '@/service/Common';
const { addNavigation } = jumpPageAddNavigation();
const store = useStore();
const currentTab = ref('1');
// 子组件loading
const loading = ref(false);
const personList = reactive({
list: [],
});
const imgs = {
success: new URL('../../assets/svg/upload/success2.svg', import.meta.url).href,
......@@ -67,12 +75,21 @@ const uploadInfo = {
successButtonLabel: '替换视频',
};
// 获取我的数字人列表
const getList = async () => {
loading.value = true;
let res = await getDigitalPeopleList();
personList.list = res.myList;
loading.value = false;
};
const submit = async (params: any) => {
try {
//
let res: any = await customizedImageSubmission(params);
if (res.code == 0) {
show_message('提交成功', 'success');
// 更新记录
getList();
return true;
}
} catch (e) {
......@@ -82,6 +99,8 @@ const submit = async (params: any) => {
onMounted(() => {
addNavigation(routerConfig.ImageCustomization.path);
// 获取数字人列表
getList();
});
</script>
......
<template>
<Dialog v-model="visible" :zIndex="3000" className="add-group-dialog" @confirm="confirm">
<template #body>
<FormItem label="分组名" className="create-group-form-item">
<Input v-model="groupValue" align="left" placeholder="请输入分组名"></Input>
</FormItem>
</template>
<Loading :mask="true" v-show="loading"></Loading>
</Dialog>
</template>
<script lang="ts" setup>
import Dialog from '@/components/Dialog.vue';
import { computed, ref } from 'vue';
import FormItem from '@/componentsUsiness/formItem.vue';
import Input from '@/components/input/index.vue';
import Loading from '@/components/loading.vue';
import { createEplyGroup } from '@/utils/api/userApi';
import { show_message } from '@/utils/tool';
const props = withDefaults(
defineProps<{
modelValue: boolean;
}>(),
{},
);
const emit = defineEmits(['update:modelValue', 'change']);
const loading = ref(false);
const visible = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
},
});
// 分组的值
const groupValue = ref('');
// 提交
const confirm = async () => {
if (!groupValue.value) {
show_message('请输入分组名');
return;
}
try {
loading.value = true;
const res: any = await createEplyGroup({
name: groupValue.value,
});
if (res.code == 0) {
show_message('创建成功', 'success');
visible.value = false;
// 通知更新
emit('change');
}
loading.value = false;
} catch (e) {
loading.value = false;
console.log(e);
}
};
</script>
<style lang="less">
.add-group-dialog {
.t-dialog {
position: relative;
}
.t-dialog__body {
margin: 30px 0 20px 0;
.create-group-form-item {
align-items: center;
.custom-input-global {
flex: 1;
}
}
}
.t-dialog__footer {
.footer-default {
text-align: center;
}
}
}
</style>
<template>
<Dialog v-model="visible" className="add-reply-dialog" :footer="true" :destroyOnClose="false" @close="onClose">
<template #header>
{{ isCreate ? '新增内容' : '编辑内容' }}
</template>
<template #body>
<FormItem label="分组" className="custom-group-form-item">
<div class="add-reply-group">
<Select :options="options" v-model="currentOption" :autoWidth="false">
<t-option v-for="item in options" :key="item.value" :value="item.value" :label="item.label">
<div class="add-reply-select-option">
<span>
{{ item.label }}
</span>
<div class="reply-select-option-icon" @click.stop="deleteGroup(item)">
<DeleteSvg></DeleteSvg>
</div>
</div>
</t-option>
</Select>
<template v-if="isCreate">
<Button theme="dark" @click="addGroup" class="add-reply-group-button">
<img :src="imgs.add" alt="" />
新增</Button
>
</template>
</div>
</FormItem>
<FormItem label="标题">
<Textarea v-model="questionValue" placeholder="请输入标题" class="question-textarea"></Textarea>
</FormItem>
<FormItem label="回复">
<Textarea v-model="eplyValue" placeholder="请输入回复"></Textarea>
</FormItem>
</template>
<template #footer>
<DialogConfirmFooter
:saveConfirm="isCreate ? '保存并继续' : ''"
save="保存"
@save="onSave"
@saveConfirm="saveConfirm"
></DialogConfirmFooter>
</template>
<AddGroupDialog v-model="groupVisible" @change="getGroupList"></AddGroupDialog>
<ConfirmDialog
v-model="confirmDialogVisible"
title="确定要删除掉分组吗?"
:zIndex="6000"
@confirm="confirmDelete"
></ConfirmDialog>
<Loading :mask="true" v-show="loading"></Loading>
</Dialog>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue';
import { Option as TOption } from 'tdesign-vue-next';
import Button from '@/components/Button.vue';
import Dialog from '@/components/Dialog.vue';
import FormItem from '@/componentsUsiness/formItem.vue';
import Textarea from '@/components/textarea.vue';
import Select from '@/components/Select.vue';
import DialogConfirmFooter from '@/componentsUsiness/dialogConfirmFooter.vue';
import { show_message } from '@/utils/tool';
import AddGroupDialog from './addGroupDialog.vue';
import { createEplyContent, deleteEplyGroup, getEplyGroup, updateLiveReply } from '@/utils/api/userApi';
import DeleteSvg from '@/assets/svg/ctrl/delete.svg';
import ConfirmDialog from '@/components/ConfirmDialog.vue';
import Loading from '@/components/loading.vue';
const props = withDefaults(
defineProps<{
modelValue: boolean;
isCreate?: boolean;
groupList: Function;
info: any;
}>(),
{
isCreate: true,
},
);
const emit = defineEmits(['update:modelValue', 'change']);
const imgs = {
add: new URL('@/assets/svg/ctrl/add.svg', import.meta.url).href,
};
const loading = ref(false);
// 弹窗状态
const visible = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
},
});
// 新增分组弹窗
const groupVisible = ref(false);
// 确认删除弹窗
const confirmDialogVisible = ref(false);
// 要删除的行
const deleteGroupRow = ref({});
// 问题
const questionValue = ref('');
// 回复
const eplyValue = ref('');
// 当前选择的分组
const currentOption = ref('');
// 分组列表
const options = ref([]);
// 弹窗关闭事件
const onClose = () => {
//
};
// 打开新增分组弹窗
const addGroup = () => {
groupVisible.value = true;
};
// 确认删除分组
const confirmDelete = async () => {
try {
loading.value = true;
const res: any = await deleteEplyGroup(deleteGroupRow.value.value);
if (res.code == 0) {
show_message('删除成功', 'success');
getGroupList();
}
loading.value = false;
} catch (e) {
console.log(e);
loading.value = false;
}
};
// 删除分组
const deleteGroup = (item: any) => {
// 打开确认弹窗
confirmDialogVisible.value = true;
deleteGroupRow.value = item;
};
// 获取请求参数
const getParams = () => {
return {
title: questionValue.value,
groups_id: currentOption.value,
content: eplyValue.value,
};
};
// 提交后台
const submit = async () => {
try {
loading.value = true;
const res: any = await createEplyContent(getParams());
if (res.code == 0) {
show_message('创建成功', 'success');
emit('change');
}
loading.value = false;
} catch (e) {
console.log(e);
loading.value = false;
}
};
// 更新回复内容
const editSubmit = async () => {
try {
loading.value = true;
const res: any = await updateLiveReply(props.info.id, getParams());
if (res.code == 0) {
show_message('更新成功', 'success');
// 通知更新
emit('change');
}
loading.value = false;
} catch (e) {
loading.value = false;
console.log(e);
}
};
// 提交前校验
const beforeformCheck = () => {
if (!currentOption.value) {
show_message('分组必选');
return;
}
if (!questionValue.value) {
show_message('标题必填');
return;
}
if (!eplyValue.value) {
show_message('内容必填');
return;
}
return true;
};
const clearData = () => {
currentOption.value = '';
questionValue.value = '';
eplyValue.value = '';
};
// 保存并继续
const saveConfirm = async () => {
const status = beforeformCheck();
if (!status) {
return;
}
// 提交内容
await submit();
// 清空输入的内容
clearData();
};
// 保存
const onSave = async () => {
const status = beforeformCheck();
if (!status) {
return;
}
if (props.isCreate) {
// 提交内容
await submit();
} else {
// 编辑
await editSubmit();
}
// 关闭弹窗
visible.value = false;
};
// 获取分组列表
const getGroupList = async () => {
try {
const res: any = await getEplyGroup();
if (res.code == 0 && res.data && res.data.length) {
options.value = res.data.map((item: any) => {
return {
label: item.name,
value: item.id,
};
});
// 提交给父组件
props.groupList(options.value);
} else {
options.value = [];
props.groupList([]);
console.log(res.data, 'getGroupList');
}
} catch (e) {
console.log(e);
}
};
watch(
() => props.info,
(v) => {
if (v && Object.keys(v).length) {
// 设置值
currentOption.value = v.groups_id;
questionValue.value = v.title;
eplyValue.value = v.content;
} else {
clearData();
}
},
);
onMounted(() => {
getGroupList();
});
</script>
<style lang="less">
@import '@/style/variables.less';
@add-reply-padding:0 32px;
.add-reply-select-option {
display: flex;
align-items: center;
justify-content: space-between;
.reply-select-option-icon {
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
svg {
fill: #b5b5b5;
transition: fill 0.2s;
}
&:hover {
svg {
fill: white;
transition: fill 0.2s;
}
}
}
}
.add-reply-dialog {
.t-dialog {
padding: 0;
position: relative;
.t-dialog__header {
padding: 22px 32px 0 32px;
}
.t-dialog__body {
margin: 24px 0;
padding: @add-reply-padding;
.question-textarea {
.t-textarea {
height: 60px;
.t-textarea__inner {
height: 100% !important;
min-height: 100% !important;
}
}
}
.custom-group-form-item {
margin-top: 2px;
}
.add-reply-group {
display: flex;
align-items: center;
width: 100%;
.custom-select-box {
flex: 1;
.t-select__wrap {
height: 32px;
.t-input {
width: 100%;
}
}
}
.add-reply-group-button {
color: #b4b4b4;
font-size: @size-14;
margin-left: 12px;
.t-button__text {
display: flex;
align-items: center;
img {
margin-right: 6px;
}
}
}
}
}
}
}
</style>
......@@ -48,7 +48,7 @@ import Loading from '@/components/loading.vue';
import Button from '@/components/Button.vue';
import Textarea from '@/components/textarea.vue';
import { show_message } from '@/utils/tool';
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue';
import { computed, watch, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
import { getTonesList } from '@/service/Common';
import { liveInteractionReply, closeLiveTask, getHumanReplyCallback } from '@/utils/api/userApi';
import { useRoute, useRouter } from 'vue-router';
......@@ -56,8 +56,17 @@ import routerConfig from '@/router/tool';
import { useStore } from 'vuex';
import { callPyjsInWindow } from '@/utils/pyqt';
import SelectTone from '@/componentsUsiness/selectTone.vue';
const emit = defineEmits(['createAudio']);
const props = withDefaults(
defineProps<{
modelValue: boolean;
sendNum: number;
message: string;
soundColor: string | number;
textTones: string | number;
}>(),
{},
);
const emit = defineEmits(['createAudio', 'update:modelValue', 'update:soundColor', 'update:textTones']);
const store = useStore();
const router = useRouter();
const route = useRoute();
......@@ -69,12 +78,33 @@ const lists = reactive({
soundColor: [],
});
const loading = ref(false);
const loading = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
},
});
const textTonesValue = ref('');
const textTonesValue = computed({
get() {
return props.textTones;
},
set(value) {
emit('update:textTones', value);
},
});
const soundColorVisible = ref(false);
const soundColorValue = ref('');
const soundColorValue = computed({
get() {
return props.soundColor;
},
set(value) {
emit('update:soundColor', value);
},
});
const soundColorInfo = ref({});
const textareaValue = ref('');
......@@ -145,7 +175,7 @@ const closeInterval = () => {
interval = null;
};
const submit = async () => {
const submit = async (content: string = '') => {
if (loading.value) {
show_message('请等待上一条回复完成');
return;
......@@ -158,13 +188,20 @@ const submit = async () => {
show_message('音调必选');
return;
}
let params = {
phonetic_timbres_id: soundColorValue.value,
reply_content: textareaValue.value,
tone_id: textTonesValue.value,
};
if (content) {
params.reply_content = content;
} else if (!textareaValue.value) {
show_message('回复内容必填');
return;
}
try {
loading.value = true;
let params = {
phonetic_timbres_id: soundColorValue.value,
reply_content: textareaValue.value,
tone_id: textTonesValue.value,
};
let res: any = await liveInteractionReply(route.query.id, params);
if (res.code == 0) {
// 开启轮询
......@@ -193,6 +230,17 @@ const onJump = () => {
}
};
watch(
() => props.sendNum,
(v) => {
// 发送消息
if (!props.message) {
return;
}
submit(props.message);
},
);
onMounted(async () => {
let res = await getTonesList(true);
lists.tones = res.tones;
......@@ -246,6 +294,9 @@ onBeforeUnmount(() => {
margin-top: 12px;
.custom-textarea-box {
height: 100%;
&:focus-within {
border-color: transparent;
}
.t-textarea {
height: 100%;
.t-textarea__inner {
......
......@@ -3,27 +3,283 @@
<div class="header">
<div class="label">中控台</div>
<div class="header-buttons">
<Button theme="dark">
<Button theme="dark" @click="setRandomPlay">
<img :src="imgs.andom" alt="" class="quick-reply-button__image" />
随机播放</Button
>
<Button theme="dark">
<Button theme="dark" @click="addEply">
<img :src="imgs.add" alt="" class="quick-reply-button__image" />
添加回复</Button
>
</div>
</div>
<div class="content"></div>
<div class="content narrow-scrollbar" ref="replyListRef">
<div class="quick-reply-select-group">
<Select
:options="options"
@change="groupChange"
v-model="currentOption"
:autoWidth="false"
placeholder="选择分组"
></Select>
</div>
<div class="quick-reply-list">
<div v-for="item in replyList" :key="item.id" class="quick-reply-row">
<div class="title">
{{ item.title }}
</div>
<div class="value">
{{ item.content }}
</div>
<div class="send">
<Button theme="dark" @click="sendMessage(item.content)">发送</Button>
</div>
<Popup v-model="item.popup" className="quick-reply-row-popup">
<div class="dot">
<div></div>
<div></div>
<div></div>
</div>
<template #content>
<div class="quick-reply-row-popup-button" @click="onEdit(item)">
<img :src="imgs.edit" alt="" />
编辑
</div>
<div class="quick-reply-row-popup-button" @click="onDelete(item)">
<img :src="imgs.deleteSvg" alt="" />
删除
</div>
</template>
</Popup>
</div>
<Loading :mask="true" v-show="loading"></Loading>
</div>
</div>
<AddReplyDialog
v-model="eplyDialogVisible"
:isCreate="isCreate"
:groupList="getGroupList"
:info="editInfo"
@change="replyChange"
></AddReplyDialog>
<ConfirmDialog v-model="confirmDialogVisible" title="确定删除吗?" @confirm="confirmDelete"></ConfirmDialog>
<RandomPlayDialog
v-model="randomPlayVisible"
:playStatus="playStatus"
:loading="modelValue"
:textTones="textTones"
:soundColor="soundColor"
@message="sendMessage"
></RandomPlayDialog>
</div>
</template>
<script lang="ts" setup>
import Button from '@/components/Button.vue';
import Loading from '@/components/loading.vue';
import { ref, onMounted, onBeforeUnmount } from 'vue';
import AddReplyDialog from './addReplyDialog.vue';
import Select from '@/components/Select.vue';
import { deleteLiveReply, getLiveReplyList } from '@/utils/api/userApi';
import Popup from '@/components/Popup.vue';
import { throttle, ecursionDeepCopy, show_message } from '@/utils/tool';
import ConfirmDialog from '@/components/ConfirmDialog.vue';
import RandomPlayDialog from './randomPlayDialog.vue';
const props = withDefaults(
defineProps<{
modelValue: boolean;
playStatus: boolean;
textTones: string | number;
soundColor: string | number;
}>(),
{},
);
const emit = defineEmits(['message']);
const imgs = {
andom: new URL('@/assets/svg/ctrl/andom.svg', import.meta.url).href,
add: new URL('@/assets/svg/ctrl/add.svg', import.meta.url).href,
edit: new URL('@/assets/svg/home/edit.svg', import.meta.url).href,
deleteSvg: new URL('@/assets/svg/home/delete.svg', import.meta.url).href,
};
// 滚动元素
const replyListRef = ref<HTMLDivElement>();
// 分组列表
const options = ref([]);
// 当前选择的分组
const currentOption = ref('');
// 回复列表
const replyList = ref([]);
// 是否首次加载
const isFirst = ref(true);
const loading = ref(false);
const editInfo = ref({});
const deleteInfo = ref({});
const isCreate = ref(true);
// 新增回复弹窗状态
const eplyDialogVisible = ref(false);
// 确定删除弹窗
const confirmDialogVisible = ref(false);
// 随机播放弹窗
const randomPlayVisible = ref(false);
// 分页
const pageNum = ref(1);
const pageSize = ref(10);
const total = ref(0);
// 初始化分页的值
const initPage = () => {
pageNum.value = 1;
pageSize.value = 10;
replyList.value = [];
};
// 初始化元素滚动位置
const initElementScrollTop = () => {
if (replyListRef.value) {
replyListRef.value.scrollTo({
top: 0,
behavior: 'smooth',
});
}
};
// 打开随机播放弹窗
const setRandomPlay = () => {
randomPlayVisible.value = true;
};
// 打开回复弹窗
const addEply = () => {
eplyDialogVisible.value = true;
editInfo.value = {};
isCreate.value = true;
};
// 发送内容
const sendMessage = (value: string) => {
if (props.modelValue || props.playStatus) {
show_message('请等待消息发送完毕');
return;
}
emit('message', value);
};
// 编辑
const onEdit = (item: any) => {
editInfo.value = ecursionDeepCopy(item);
eplyDialogVisible.value = true;
item.popup = false;
isCreate.value = false;
};
const getGroupList = (list: any[]) => {
options.value = list;
if (isFirst.value) {
// 默认第一个
if (options.value.length) {
currentOption.value = options.value[0].value;
getList();
}
isFirst.value = false;
}
};
// 打开确定删除弹窗
const onDelete = (item: any) => {
deleteInfo.value = item;
confirmDialogVisible.value = true;
item.popup = false;
};
// 确定删除
const confirmDelete = async () => {
try {
let res: any = await deleteLiveReply(deleteInfo.value.id);
if (res.code == 0) {
show_message('删除成功', 'success');
getList();
}
} catch (e) {
console.log(e);
}
};
// 回复内容变化
const replyChange = () => {
initPage();
getList();
};
const groupChange = (value: any) => {
currentOption.value = value;
initPage();
getList();
};
// 获取分组下的回复列表
const getList = async () => {
try {
loading.value = true;
const res: any = await getLiveReplyList({
groups_id: currentOption.value,
page: pageNum.value,
limit: pageSize.value,
});
if (res.code == 0) {
// 回到顶部
if (pageNum.value == 1) {
initElementScrollTop();
}
res.data.data.forEach((item: any) => {
item.popup = false;
});
replyList.value = replyList.value.concat(res.data.data);
// 总量
total.value = res.data.total;
}
loading.value = false;
} catch (e) {
loading.value = false;
console.log(e);
}
};
// 滚动事件
const scrollEvent = throttle(() => {
if (loading.value || replyList.value.length >= total.value) {
return;
}
const scrollTop = replyListRef.value.scrollTop;
const scrollHeight = replyListRef.value.scrollHeight;
const clientHeight = replyListRef.value.clientHeight;
// 元素剩余可滚动的距离
const scrollDistance = scrollHeight - (scrollTop + clientHeight);
if (scrollDistance <= 50) {
// 下一页
pageNum.value += 1;
getList();
}
}, 200);
onMounted(() => {
if (replyListRef.value) {
// 监听元素滚动
replyListRef.value.onscroll = scrollEvent;
}
});
onBeforeUnmount(() => {
if (replyListRef.value) {
replyListRef.value.onscroll = null;
}
});
</script>
<style lang="less">
......@@ -58,14 +314,101 @@ const imgs = {
}
}
.content {
height: 100%;
flex: 1;
overflow: hidden;
border-radius: 0px 3px 3px 3px;
border: 1px solid #464646;
background: #1e1e1e;
padding: 12px;
display: flex;
position: relative;
flex-direction: column;
overflow-y: auto;
.quick-reply-select-group {
position: sticky;
top: 0;
z-index: 10;
background-color: #1e1e1e;
padding: 12px 16px;
.custom-select-box {
.t-input {
border-radius: 4px;
background-color: #303030;
width: 100%;
}
}
}
.quick-reply-list {
position: relative;
flex: 1;
padding: 0 16px 12px 16px;
.quick-reply-row {
height: 135px;
background-color: #049c78;
border-radius: 4px;
margin-top: 12px;
padding: 12px;
display: flex;
flex-direction: column;
position: relative;
.title,
.value {
color: #fff;
}
.title {
font-size: @size-18;
font-weight: 700;
}
.value {
font-size: @size-14;
padding-top: 6px;
flex: 1;
overflow-y: auto;
}
.send {
text-align: right;
.t-button {
border: 1px solid #464646;
font-size: @size-14;
font-weight: bold;
background-color: #303030;
}
}
.dot {
display: flex;
position: absolute;
right: 16px;
top: 12px;
cursor: pointer;
& > * {
width: 7px;
height: 7px;
border-radius: 50%;
background-color: rgb(217, 217, 217);
margin: 0 2px;
}
}
}
}
}
}
.quick-reply-row-popup {
.t-popup__content {
border: 1px solid #fff;
}
.quick-reply-row-popup-button {
color: #b4b4b4;
font-size: @size-14;
display: flex;
align-items: center;
padding: 6px;
cursor: pointer;
img {
width: 16px;
height: 16px;
margin-right: 6px;
}
}
}
</style>
<template>
<Dialog v-model="visible" :footer="true" className="random-play-dialog">
<template #header> 随机播放设置 </template>
<template #body>
<FormItem label="间隔">
<Input v-model="inputValue" type="number" class="random-play-input" placeholder="请输入间隔时间" align="left">
<template #rightIcon>
<div class="random-play-input__right"></div>
</template>
</Input>
</FormItem>
<FormItem label="状态">
<Switch v-model="switchValue"></Switch>
</FormItem>
</template>
<template #footer>
<DialogConfirmFooter save="确定" @save="onSave"></DialogConfirmFooter>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import Dialog from '@/components/Dialog.vue';
import { computed, onMounted, ref } from 'vue';
import FormItem from '@/componentsUsiness/formItem.vue';
import Input from '@/components/input/index.vue';
import Switch from '@/components/switch';
import DialogConfirmFooter from '@/componentsUsiness/dialogConfirmFooter.vue';
import { show_message } from '@/utils/tool';
import { useRoute } from 'vue-router';
import * as cache from '@/utils/cache';
import { randomLiveReply } from '@/utils/api/userApi';
const props = withDefaults(
defineProps<{
modelValue: boolean;
loading: boolean;
playStatus: boolean;
textTones: string | number;
soundColor: string | number;
}>(),
{},
);
const emit = defineEmits(['update:modelValue', 'message']);
const route = useRoute();
// 本地数据的key
const localKey = 'reply_random_play_';
const visible = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
},
});
let interval = null;
let localInterval = null;
// 间隔
const inputValue = ref('');
const switchValue = ref(false);
// 任务队列
const taskList = ref([]);
const getCacheId = () => {
return localKey + route.query.id;
};
// 获取随机内容
const getRandomValue = async () => {
try {
let res: any = await randomLiveReply();
if (res.code == 0 && res.data.content) {
// 加入队列
taskList.value.push(res.data.content);
}
} catch (e) {
console.log(e);
}
};
// 打开定时器(从后台获取随机内容)
const openInterval = () => {
closeInterval();
interval = window.setInterval(getRandomValue, parseInt(inputValue.value) * 1000);
};
// 关闭定时器
const closeInterval = () => {
window.clearInterval(interval);
clearInterval(interval);
interval = null;
};
// 打开定时器(从本地队列里取任务)
const openLocalInterval = () => {
localInterval = window.setInterval(() => {
// 禁止取任务
if (
!switchValue.value ||
props.loading ||
props.playStatus ||
!taskList.value.length ||
!props.textTones ||
!props.soundColor
) {
return;
}
// 取任务
emit('message', taskList.value[0]);
// 删除这条任务
taskList.value.shift();
}, 5000);
};
const onSave = () => {
if (!route.query.id) {
show_message('禁止访问');
return;
}
if (switchValue.value && !inputValue.value) {
show_message('间隔时间必填,或关闭随机播放');
return;
}
if (!switchValue.value) {
// 关闭定时任务
closeInterval();
// 清空队列
taskList.value = [];
} else {
openInterval();
}
// 数据存本地,根据id
cache.setCache(getCacheId(), {
interval: inputValue.value,
status: switchValue.value,
});
visible.value = false;
};
const init = () => {
const data = cache.getCache(getCacheId());
if (data) {
inputValue.value = data.interval;
switchValue.value = data.status;
}
// 打开随机播放
if (inputValue.value && switchValue.value) {
openInterval();
}
};
onMounted(() => {
init();
openLocalInterval();
});
</script>
<style lang="less">
@import '@/style/variables.less';
.random-play-dialog {
.t-dialog__body {
margin-top: 20px;
.custom-usiness-form-item {
align-items: center;
.random-play-input {
flex: 1;
.random-play-input__right {
background: #5a5a5a;
border-radius: 0px 4px 4px 0px;
color: #fff;
font-size: @size-12;
font-weight: 600;
width: 45px;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
}
}
}
.dialog-confirm-footer {
padding: 0;
align-items: flex-end;
}
}
</style>
<template>
<div class="custom-interactive-response-page">
<QuickReply></QuickReply>
<Human @createAudio="createAudio">
<QuickReply
v-model="loading"
:soundColor="soundColorValue"
:textTones="textTonesValue"
:playStatus="playStatus"
@message="onMessage"
></QuickReply>
<Human
v-model="loading"
v-model:soundColor="soundColorValue"
v-model:textTones="textTonesValue"
:message="message"
:sendNum="sendNum"
@createAudio="createAudio"
>
<div v-show="false">
<audio
:volume="liveDefaultVolume"
......@@ -22,9 +35,25 @@ import Human from './components/human.vue';
import { callPyjsInWindow } from '@/utils/pyqt';
import QuickReply from './components/quickReply.vue';
// 音色id
const soundColorValue = ref('');
// 音调id
const textTonesValue = ref('');
// 音频文件
const audioRef = ref<HTMLAudioElement>();
const audioFile = ref('');
const loading = ref(false);
const message = ref('');
const sendNum = ref(1);
// 当前是否正在播放
const playStatus = ref(false);
// 接收回复内容
const onMessage = (value: string) => {
message.value = value;
sendNum.value += 1;
};
const createAudio = (url: string) => {
audioFile.value = url;
......@@ -32,12 +61,14 @@ const createAudio = (url: string) => {
const audioCanplay = () => {
audioRef.value.play();
playStatus.value = true;
// 通知python将直播页面的音量减小
callPyjsInWindow('lowerVideoVolume');
};
// 音频播放结束
const audioEnded = () => {
playStatus.value = false;
// 通知python将直播页面的音量恢复
callPyjsInWindow('videoVolumeRestoration');
};
......
......@@ -244,17 +244,13 @@ export default function () {
}
};
// 找到所有已经播放完毕的主视频
let playEndVideos = realVideoList.value.map((row: any, index: number) => {
let playEndVideos = [];
realVideoList.value.forEach((row: any, index: number) => {
if (row.remove && row.result && row.status && typeof index === 'number') {
return index;
}
});
// 再次过滤
playEndVideos = playEndVideos.map((row: any, index: number) => {
if (typeof index === 'number') {
return index;
playEndVideos.push(index);
}
});
console.log(playEndVideos, '随机下标列表');
if (playEndVideos.length) {
if (playEndVideos.length === 1) {
console.log('只有一条视频播放完毕,重新播放该视频');
......@@ -263,7 +259,22 @@ export default function () {
// 多条视频
// 随机下标
let num = randomIntFormList(playEndVideos);
// 防止出错
try {
if (typeof num !== 'number' || !realVideoList.value[num]) {
console.log('num格式错误,初始化第1个视频', num);
num = 0;
}
} catch (e) {
num = 0;
console.log(e, '初始化旧视频出错了,将num改为0');
writeLog({
name: '初始化旧视频出错了,将num改为0',
value: e,
});
}
console.log(`初始化第${num}个视频`);
console.log(realVideoList.value[num]);
// 链接是否一致
if (realVideoList.value[num].result === item.url) {
// 循环播放
......@@ -612,11 +623,11 @@ export default function () {
url: url,
type: 1,
},
{
url: 'http://yunyi-live.oss-cn-hangzhou.aliyuncs.com/upload/2/2023-08-217a51d89c-1a9f-476b-950c-f81d0423b816.mp4',
type: 3,
play_time: 0,
},
// {
// url: 'http://yunyi-live.oss-cn-hangzhou.aliyuncs.com/upload/2/2023-08-217a51d89c-1a9f-476b-950c-f81d0423b816.mp4',
// type: 3,
// play_time: 0,
// },
];
res.data.url = list;
......
......@@ -77,7 +77,7 @@ const confirm = () => {
<style lang="less">
@import '@/style/variables';
.image-custom-my-person-box {
padding: 30px 40px;
padding: 30px 20px;
border-radius: 4px 4px 0px 0px;
background: #303030;
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.04);
......
......@@ -9,7 +9,7 @@
version="v1"
:label="'声音定制'"
>
<CustomTabs v-model="currentTab" theme="dark2">
<CustomTabs v-model="currentTab" theme="dark2" class="custom-tabs-flex">
<CustomTabPanel name="1" label="我的音色">
<MyDigitalPerson :list="personList.list" :loading="loading" @playAudio="myAudioPlay"></MyDigitalPerson>
</CustomTabPanel>
......@@ -94,8 +94,10 @@ const getList = async () => {
item.play_status = false;
});
personList.list = res.soundColor;
// 复制一份给记录列表
personList.record = dimensionalConvert(res.soundColor);
loading.value = false;
} catch (e) {
loading.value = false;
......
@import '@/style/variables';
.action-create-success-tab {
padding: 30px 40px;
padding: 30px 30px;
border-radius: 4px 4px 0px 0px;
background: #303030;
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.04);
......
@import '@/style/variables.less';
.custom-action-page {
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
.header {
margin: 20px 0;
padding: 20px 0;
.da();
font-size: @size-24;
font-weight: 700;
......@@ -79,6 +83,8 @@
}
}
.izable-page-tabs {
margin-top: 50px;
padding-top: 50px;
flex: 1;
box-sizing: border-box;
}
}
......@@ -237,7 +237,7 @@ export default defineComponent({
</div>
</div>
<div class="izable-page-tabs">
<CustomTabs v-model={currentTab.value} theme="dark2" onChange={tabsChange}>
<CustomTabs v-model={currentTab.value} theme="dark2" class="custom-tabs-flex" onChange={tabsChange}>
<CustomTabPanel
name="1"
label="已创建"
......
......@@ -18,9 +18,7 @@
>
</template>
<template v-else>
<Button class="digtal-people-start-end" theme="danger" height="40px" @click="startLive(item)"
>开播中</Button
>
<Button class="digtal-people-start-end" theme="danger" height="40px">开播中</Button>
<Button class="digtal-people-ctrl" theme="danger" height="40px" @click="openInteractiveResponse(item)"
>控制台</Button
>
......
......@@ -70,3 +70,14 @@ html {
background: #b4b4b4;
border-radius: 8px;
}
.custom-tabs-flex {
height: 100%;
display: flex;
flex-direction: column;
.c-tabs-content {
flex: 1;
overflow-y: auto;
background-color: #303030;
}
}
......@@ -430,3 +430,91 @@ export const getLiveMovement = () => {
},
});
};
/**
* 中控台快捷回复模块
*/
// 创建分组
export const createEplyGroup = (data: any) => {
const header = getHeader();
return request.post(`/api/live/groups/interaction`, data, {
headers: {
...header,
},
});
};
// 获取分组列表
export const getEplyGroup = () => {
const header = getHeader();
return request.get(`/api/live/groups/interaction`, {
params: {},
headers: {
...header,
},
});
};
// 删除分组
export const deleteEplyGroup = (id: number | string) => {
const header = getHeader();
return request.delete(`/api/live/groups/interaction/${id}`, {
params: {},
headers: {
...header,
},
});
};
// 创建回复内容
export const createEplyContent = (data: any) => {
const header = getHeader();
return request.post(`/api/live/reply`, data, {
headers: {
...header,
},
});
};
// 获取回复内容列表
export const getLiveReplyList = (data: any) => {
const header = getHeader();
return request.get(`/api/live/reply`, {
params: data,
headers: {
...header,
},
});
};
// 更新回复内容
export const updateLiveReply = (id: number | string, data: any) => {
const header = getHeader();
return request.post(`/api/live/reply/${id}`, data, {
headers: {
...header,
},
});
};
// 删除回复内容
export const deleteLiveReply = (id: number | string) => {
const header = getHeader();
return request.delete(`/api/live/reply/${id}`, {
params: {},
headers: {
...header,
},
});
};
// 随机获取回复内容
export const randomLiveReply = () => {
const header = getHeader();
return request.get(`/api/live/reply/rand`, {
params: {},
headers: {
...header,
},
});
};
// set
export const setCache = (key: string, value: any, type: 'local' | 'session' = 'local') => {
if (type === 'local') {
window.localStorage.setItem(key, JSON.stringify(value));
} else {
window.sessionStorage.setItem(key, JSON.stringify(value));
}
};
// get
export const getCache = (key: string, type: 'local' | 'session' = 'local') => {
let data = null;
if (type === 'local') {
data = window.localStorage.getItem(key);
} else {
data = window.sessionStorage.getItem(key);
}
if (data) {
return JSON.parse(data);
}
return data;
};
......@@ -29,8 +29,8 @@ const getBaseUrl = async () => {
const instance = axios.create({
baseURL: '',
// withCredentials: false,
withCredentials: isDev() ? true : false,
withCredentials: false,
// withCredentials: isDev() ? true : false,
});
// 请求拦截
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment