Files
lingji-work-fe/src/views/property-marketing/assignment-management/components/draw-popup.vue
2025-09-26 16:49:17 +08:00

738 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<a-drawer
title="创建任务"
placement="right"
v-model:visible="showDriwer"
@ok="handleCreateTask"
width="480px"
class="task-drawer"
style="z-index: 999"
>
<div class="drawer-content" :style="isActive == 'ai' ? 'height: 376px;' : 'height:216px;'">
<div class="flex flex-col justify-center">
<CommonSelect
v-model="localQuery.accounts"
:options="accountList || []"
:multiple="false"
@change="(val) => handleChange('accounts', val)"
class="!w-432px select-with-tags"
placeholder="请选择账号名称"
:allowSearch="true"
:maxTagCount="999"
popup-container=".filter-popup-content"
/>
<div class="ai-content-generator">
<div class="flex mt-16px">
<Button
class="w-194px h-38px mr-8px"
:class="isActive == 'ai' ? 'active-chose' : ''"
@click="handleSelect('ai')"
>
<template #icon>
<img :src="aiIcon" class="w-16 h-16 mr-8px" />
</template>
AI生成
</Button>
<Button
class="w-194px h-38px"
:class="isActive == 'chose' ? 'active-chose' : ''"
@click="handleSelect('chose')"
>
从成品库选择</Button
>
</div>
<div v-show="isActive == 'ai'">
<!-- 任务描述区域 -->
<div class="form-section">
<div class="flex items-center w-400px mt-16px mb-8px">
<div class="section-title">任务描述</div>
<div class="font-size-12px text-[#999999]">非必填</div>
</div>
<div class="w-400px h-126px border-rounded-8px mb-8px" style="background: #fff">
<a-textarea
placeholder="描述你想让AI帮你生成的内容。未填写时AI 会参考账号历史内容的题材与行业方向,结合当下话题,自动生成文案和图片后发布"
class="task-description font-size-12px"
:rows="5"
v-model="taskDescription"
/>
</div>
</div>
<!-- 素材添加区域 -->
<div class="form-section material-section">
<div class="flex items-center"></div>
<Button class="add-material-btn" @click="handleAddMaterial">
<template #icon>
<icon-plus size="16" class="mr-8px" />
</template>
从原料库添加
</Button>
<div v-if="hasChoseMaterial" class="flex flex-col items-center w-full">
<div
v-for="item in selectedMaterials.texts"
:key="item.id"
class="flex items-center bg-#F7F8FA border-rounded-8px w-full justify-items-center pt-8px pb-8px pl-12px pr-12px mb-16px"
>
{{ item }}
</div>
<div class="w-full">
<div class="grid grid-cols-4 gap-8px w-full">
<div v-for="(item, index) in selectedMaterials.images" :key="item.id" class="w-88px h-88px">
<img :src="item.cover" class="w-full h-full rounded-8px" />
</div>
</div>
</div>
</div>
<div v-else class="flex flex-col items-center">
<p class="material-hint">AI会参考添加的文本图片视频等素材完成符合需求的创作</p>
</div>
</div>
</div>
<div v-show="isActive == 'chose'">
<!-- 任务描述区域 -->
<div class="form-section">
<div class="flex items-center w-400px mt-16px mb-8px">
<div class="section-title">发布内容</div>
<div class="font-size-12px text-[#999999]">必填</div>
</div>
<div class="form-section material-section" v-if="hasChoseFinishedProducts == false">
<Button @click="handleAddContent" class="add-material-btn">
<template #icon>
<icon-plus size="16" class="mr-8px" />
</template>
添加内容
</Button>
<p class="material-hint">前往成品库选择要发布的内容</p>
</div>
<div v-else class="flex flex-col items-start w-full content-center">
<div class="opt-btn">
<SwapOutlined class="bg-#00000060 p-4px rounded-4px cursor-pointer" @click="handleAddContent" />
<DeleteOutlined
style="margin-left: 16px"
class="bg-#00000060 p-4px rounded-4px cursor-pointer"
@click="handleDelte"
/>
</div>
<div class="mb-12px" v-if="selectedProducts.data.length > 0">{{ selectedProducts.data[0].title }}</div>
<div class="mb-12px color-#939499 line-clamp-2" v-if="selectedProducts.data.length > 0">
{{ selectedProducts.data[0].content }}
</div>
<div v-for="item in selectedProducts.images" :key="item.id">
<img v-if="item.cover" :src="item.cover" class="w-88 h-88 mr-8px border-rounded-8px mb-12px" />
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 发布计划区域 -->
<div class="publish-section">
<div class="flex items-center justify-between w-384px">
<div class="section-title">发布计划</div>
<CommonSelect
v-model="publishType"
:options="publishOptions"
@change="handlePublishTypeChange"
class="w-180px background-#fff publish-type-select"
:allowSearch="false"
:multiple="false"
popup-container=".filter-popup-content"
/>
</div>
<div v-if="publishType === 'timing'">
<div class="line"></div>
<div class="flex items-center justify-between mt-16px w-384px">
<div class="section-title">日期</div>
<a-date-picker
class="w-180px h-40px background-#fff"
@change="handleDateChange"
v-model="currentDate"
format="YYYY年MM月DD日周dd"
value-format="YYYY-MM-DD"
/>
</div>
<div class="line"></div>
<div class="flex items-center justify-between mt-16px w-384px">
<div class="section-title">时间</div>
<a-time-picker v-model="strValue" format="HH:mm" class="w-180px h-40px background-#fff" />
</div>
</div>
</div>
</div>
<!-- 原料库子组件使用:visible和@update:visible替代v-model -->
<RawMaterialDrawer
:visible="showDrawer2"
:query="materialQuery"
@after-visible-change="handleMaterialDrawerVisibleChange"
@confirm="handleMaterialConfirm"
@cancel="handleMaterialCancel"
/>
<!-- 成品库子组件使用:visible和@update:visible替代v-model -->
<FinishedProductDrawer
:visible="showDrawer3"
@update:visible="(val) => (showDrawer3 = val)"
:query="productQuery"
@after-visible-change="handleProductDrawerVisibleChange"
@confirm="handleProductConfirm"
@cancel="handleProductCancel"
/>
<!-- 底部操作栏 -->
<template #footer>
<div class="flex items-center justify-between">
<div class="flex items-center"></div>
<div class="flex justify-end">
<Button @click="handleCancel">取消</Button>
<Button class="ml-16px" type="primary" @click="handleCreateTask">创建任务</Button>
</div>
</div>
</template>
</a-drawer>
</template>
<script lang="ts" setup>
import { getWorksPage } from '@/api/all/generationWorkshop.ts';
import { ref, reactive, watch, nextTick } from 'vue';
import aiIcon from '@/assets/img/media-account/icon-AI.png';
import { SwapOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import { Button, DatePicker, TimePicker } from 'ant-design-vue';
import { TABS_LIST, ORIGIN_LIST, RawMaterialType } from '@/views/material-center/components/raw-material/constants';
import dayjs from 'dayjs';
import { getRawMaterialsPage } from '@/api/all/generationWorkshop';
import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination';
import { EnumManuscriptType } from '@/views/material-center/components/finished-products/manuscript/list/constants';
import icon2 from '@/assets/img/creative-generation-workshop/icon-photo.png';
import icon3 from '@/assets/img/creative-generation-workshop/icon-video.png';
import icon4 from '@/assets/img/error-img.png';
// 引入子组件
import RawMaterialDrawer from './raw-material-drawer.vue';
import FinishedProductDrawer from './finished-product-drawer.vue';
import { message } from 'ant-design-vue';
import { getMediaAccountList } from '@/api/all/propertyMarketing';
// 导入任务详情API
import { getWorkDetail } from '@/api/all/assignment-management';
// 平台图标
import iconDy from '@/assets/img/platform/icon-dy.png';
import iconXhs from '@/assets/img/platform/icon-xhs.png';
import iconBilibili from '@/assets/img/platform/icon-bilibili.png';
import iconKs from '@/assets/img/platform/icon-ks.png';
import iconSph from '@/assets/img/platform/icon-sph.png';
import iconWb from '@/assets/img/platform/icon-wb.png';
import iconGzh from '@/assets/img/platform/icon-gzh.png';
import iconWarn from '@/assets/img/media-account/icon-warn.png';
// 状态管理
const choseText = ref('');
const taskDescription = ref('');
const hasChoseMaterial = ref(false);
const hasChoseFinishedProducts = ref(false);
const isActive = ref('ai');
const showDriwer = ref(false);
const showDrawer2 = ref(false);
const showDrawer3 = ref(false);
const accountList = ref([]);
onMounted(() => {
getData();
});
// 平台配置
const platformConfig = {
icons: { 0: iconDy, 1: iconXhs, 2: iconBilibili, 3: iconKs, 4: iconSph, 5: iconWb, 6: iconGzh },
names: { 0: '抖音', 1: '小红书', 2: 'B站', 3: '快手', 4: '视频号', 5: '微博', 6: '公众号' },
options: [
{ id: 0, name: '抖音', icon: iconDy },
{ id: 1, name: '小红书', icon: iconXhs },
{ id: 2, name: 'B站', icon: iconBilibili },
{ id: 3, name: '快手', icon: iconKs },
{ id: 4, name: '视频号', icon: iconSph },
{ id: 5, name: '微博', icon: iconWb },
{ id: 6, name: '公众号', icon: iconGzh },
],
};
const platformOptions = ref(platformConfig.options);
// 工具函数
const getPlatformIcon = (platform: number) => platformConfig.icons[platform] || iconWarn;
const getPlatformName = (platform: number) => platformConfig.names[platform] || '未知平台';
const getData = async () => {
try {
const { code, data: accountData } = await getMediaAccountList();
if (code === 200) {
accountList.value = accountData.map((account: any) => ({
value: account.id,
name: `${account.name}(${getPlatformName(account.platform)})`,
platform: account.platform,
icon: getPlatformIcon(account.platform),
}));
}
} catch (error) {
console.error('获取账号列表失败:', error);
}
};
// 发布类型选项
const publishOptions = ref([
{ value: 'immediate', label: '立即发布' },
{ value: 'timing', label: '定时发布' },
]);
// 发布类型,默认为立即发布
const publishType = ref('immediate');
// 时间选择相关
const currentDate = ref(new Date());
const strValue = ref();
// 定义props和emit
const props = defineProps({
operators: Array,
platformOptions: Array,
accountList: Array,
query: Object,
});
// 本地筛选状态(保持上次选择)
const localQuery = ref({
accounts: props.query?.name || [],
ids: props.query?.ids || [],
});
// 原料库查询参数
const materialQuery = reactive({
page: 1,
page_size: 10,
platforms: undefined,
operator_ids: undefined,
ids: [],
top_execution_time: undefined,
});
// 成品库查询参数
const productQuery = reactive({
page: 1,
page_size: 10,
platforms: undefined,
operator_ids: undefined,
ids: [],
});
// 选中的素材数据
const selectedMaterials = ref({
keys: [],
data: [],
text: '',
images: [],
texts: [],
});
const selectedProducts = ref({
keys: [],
data: [],
text: '',
images: [],
});
// 处理AI/成品库选择切换
const handleSelect = (value) => {
isActive.value = value;
if (value === 'ai') {
showDrawer3.value = false;
} else {
showDrawer2.value = false;
}
};
// 打开原料库抽屉
const handleAddMaterial = () => {
materialQuery.page = 1;
materialQuery.page_size = 10;
showDrawer2.value = true;
};
// 打开成品库抽屉
const handleAddContent = () => {
productQuery.page = 1;
productQuery.page_size = 10;
showDrawer3.value = true;
};
const handleDelte = () => {
hasChoseFinishedProducts.value = false;
selectedProducts.value = {
keys: [],
data: [],
text: '',
images: [],
};
};
// 处理原料库选择确认
const handleMaterialConfirm = (result) => {
console.log('handleMaterialConfirm', result);
selectedMaterials.value = {
keys: result.selectedKeys,
data: result.selectedData,
text: result.choseText,
images: result.choseImgArray,
texts: result.selectedTexts || [],
};
hasChoseMaterial.value = result.selectedKeys.length > 0;
};
// 处理原料库取消
const handleMaterialCancel = () => {
// 取消逻辑
showDrawer2.value = false;
};
// 处理成品库选择确认
const handleProductConfirm = async (result) => {
selectedProducts.value = {
keys: result.selectedKeys,
data: result.selectedData,
text: result.choseText,
images: result.choseImgArray,
};
// 如果是单选模式,确保只选择一个项目
if (result.selectedRows && result.selectedRows.length > 0) {
hasChoseFinishedProducts.value = true;
// 取第一个选中的项目
const selectedProduct = result.selectedRows[0];
// 获取成品详情
try {
const res = await getWorkDetail(selectedProduct.id);
if (res && res.code === 200) {
const workDetail = res.data;
selectedProducts.value = {
keys: [workDetail.id],
data: [workDetail],
text: workDetail.title || '1个稿件',
images: workDetail.files ? workDetail.files.filter((f) => f.type === 0) : [], // 图片文件
};
}
} catch (error) {
console.error('获取成品详情失败:', error);
// 如果获取详情失败,使用原始数据
selectedProducts.value = {
keys: [selectedProduct.id],
data: [selectedProduct],
text: '1个稿件',
images: [selectedProduct],
};
}
}
};
// 处理成品库取消
const handleProductCancel = () => {
// 取消逻辑
showDrawer3.value = false;
};
// 处理发布类型变化
const handlePublishTypeChange = (value) => {
publishType.value = value;
};
// 处理日期变化
const handleDateChange = (date) => {
// 日期处理逻辑
};
// 处理筛选条件变化
const handleChange = (field, value) => {
localQuery.value[field] = value;
localQuery.value.ids = [value];
emit('filter-change', {
accounts: localQuery.value.accounts,
});
};
const handleCancel = () => {
showDriwer.value = false;
};
// 点击创建任务按钮时触发
const handleCreateTask = () => {
// 验证表单
if (localQuery.value.ids.length == 0) {
// 可以添加错误提示:请选择发布内容
message.error('请选择发布账号');
return;
}
if (isActive.value === 'chose' && selectedProducts.value.keys.length === 0) {
// 可以添加错误提示:请选择发布内容
message.error('请选择发布内容');
return;
}
// 准备提交的数据
const taskData = {
media_account_id: localQuery.value.ids[0],
is_ai_generate: isActive.value == 'chose' ? 0 : 1,
ai_prompt: taskDescription.value,
raw_material_ids: selectedMaterials.value.keys,
products: selectedProducts.value.keys,
publish_type: publishType.value == 'immediate' ? 0 : 1,
execution_time:
publishType.value === 'timing' ? `${dayjs(currentDate.value).format('YYYY-MM-DD')} ${strValue.value}` : undefined,
work_id: selectedProducts.value.keys[0],
};
// 发射创建任务事件
emit('create-task', taskData);
// 关闭抽屉
showDriwer.value = false;
};
// 暴露方法给父组件
const showDrawer = (accountInfo = null, selectedDate = null) => {
showDriwer.value = true;
if (accountInfo && accountInfo.id) {
nextTick(() => {
// 查找账号列表中匹配的账号,包含平台信息和图标
const matchedAccount = accountList.value.find((account) => account.value === accountInfo.id);
if (matchedAccount) {
localQuery.value.accounts = [matchedAccount.name];
localQuery.value.ids = [accountInfo.id];
} else {
// 如果没有找到匹配项,检查账号列表是否已加载
if (accountList.value.length > 0) {
// 账号列表已加载但未找到匹配项,回退到原来的方式
localQuery.value.accounts = [`${accountInfo.name}(${getPlatformName(accountInfo.platform)})`];
localQuery.value.ids = [accountInfo.id];
} else {
// 账号列表尚未加载,等待加载完成后再设置
const unwatch = watch(
accountList,
(newAccountList) => {
if (newAccountList.length > 0) {
const matched = newAccountList.find((account) => account.value === accountInfo.id);
if (matched) {
localQuery.value.accounts = [matched.name];
localQuery.value.ids = [accountInfo.id];
} else {
localQuery.value.accounts = [`${accountInfo.name}(${getPlatformName(accountInfo.platform)})`];
localQuery.value.ids = [accountInfo.id];
}
// 取消监听
unwatch();
}
},
{ immediate: true },
);
}
}
});
}
// 如果传入了日期,则设置为默认日期
if (selectedDate) {
currentDate.value = selectedDate;
console.log('currentDate', currentDate.value);
publishType.value = 'timing';
}
};
// 新增:编辑任务时的数据回填方法
const fillTaskData = async (taskData) => {
// 如果传入的数据包含完整信息,则直接使用,否则获取详情
let fullTaskData = taskData;
// 如果没有work或raw_materials等详细信息则需要获取完整详情
if ((!taskData.work && !taskData.raw_materials) || taskData.id) {
try {
const res = await getTaskSchedulesDetail(taskData.id);
if (res && res.code === 200) {
fullTaskData = res.data;
}
} catch (error) {
console.error('获取任务详情失败:', error);
}
}
// 设置账号信息
if (fullTaskData.media_account) {
nextTick(() => {
localQuery.value.accounts = [fullTaskData.media_account.name];
localQuery.value.ids = [fullTaskData.media_account.id];
});
}
// 设置AI生成或成品库选择
isActive.value = fullTaskData.is_ai_generate ? 'ai' : 'chose';
// 设置任务描述AI生成时
if (fullTaskData.is_ai_generate && fullTaskData.ai_prompt) {
taskDescription.value = fullTaskData.ai_prompt;
}
// 设置发布时间
if (fullTaskData.publish_type === 1 && fullTaskData.execution_time) {
// 定时发布
publishType.value = 'timing';
const execTime = new Date(fullTaskData.execution_time * 1000);
currentDate.value = execTime;
strValue.value = dayjs(execTime).format('HH:mm');
} else {
// 立即发布
publishType.value = 'immediate';
}
// 设置选中的素材AI生成时
if (fullTaskData.is_ai_generate && fullTaskData.raw_materials && fullTaskData.raw_materials.length > 0) {
const materials = fullTaskData.raw_materials;
selectedMaterials.value = {
keys: materials.map((m) => m.id),
data: materials,
text: '',
images: materials.filter((m) => m.type === 0), // 图片
texts: materials.filter((m) => m.type === 2), // 文本
};
hasChoseMaterial.value = materials.length > 0;
}
// 设置选中的成品(成品库选择时)
if (!fullTaskData.is_ai_generate && fullTaskData.work) {
const work = fullTaskData.work;
selectedProducts.value = {
keys: [work.id],
data: [work],
text: work.title || '1个稿件',
images: work.files ? work.files.filter((f) => f.type === 0) : [], // 图片文件
};
hasChoseFinishedProducts.value = true;
}
};
// 定义事件发射器
const emit = defineEmits(['filter-change', 'create-task']);
// 暴露方法
defineExpose({
showDrawer,
fillTaskData, // 暴露新的数据回填方法
});
</script>
<style scoped>
.drawer-content {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.arco-drawer {
border-top-left-radius: 8px !important;
border-bottom-left-radius: 8px !important;
}
.ai-content-generator {
display: flex;
flex-direction: column;
align-items: center;
justify-content: start;
margin-top: 24px;
width: 432px;
background: linear-gradient(to right, #f0f5ff, #fff6f5) !important;
border-radius: 8px;
}
.active-chose {
border: #722ed1 1px solid !important;
}
.form-section {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 400px;
border-radius: 8px;
padding: 8px;
}
.publish-section {
width: 432px;
padding: 16px 24px;
background-color: #f7f8fa;
display: flex;
flex-direction: column;
border-radius: 8px;
align-items: start;
justify-content: space-between;
margin-top: 24px;
}
.section-title {
font-size: 14px;
color: #333;
font-weight: 500;
}
.task-description {
width: 100%;
font-size: 14px;
height: 126px;
border: none !important;
box-shadow: none !important;
outline: none !important;
}
.task-description::placeholder {
color: #999;
}
.material-section {
border-radius: 8px;
text-align: center;
background-color: #fff;
margin-bottom: 8px;
padding: 16px;
}
.add-material-btn {
width: 240px;
height: 38px;
border-radius: 8px;
font-size: 14px;
border: 1px dashed #e0e0e0 !important;
margin-bottom: 8px;
}
.material-hint {
color: #939499;
font-size: 14px;
margin: 0 40px;
}
.content-center {
position: relative;
}
.opt-btn {
display: flex;
position: absolute; /* 设置为绝对定位 */
top: 0; /* 紧贴顶部 */
right: 0; /* 紧贴右侧 */
}
.publish-type-select :deep(.ant-select-selection-item) {
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.select-with-tags :deep(.ant-select-selection-item) {
text-align: start;
display: flex;
align-items: center;
justify-content: start;
}
.line {
background-color: #e6e6e8;
width: 382px;
height: 1px;
margin-top: 16px;
}
</style>