Merge branch 'test' of ssh://gta.lvfunai.com:42001/ai-team/lingji-work-fe into test

This commit is contained in:
rd
2025-09-24 17:35:28 +08:00
17 changed files with 2733 additions and 926 deletions

View File

@ -22,3 +22,18 @@ export const getTaskSchedulesDetail = (id: string) => {
console.log('id', id); console.log('id', id);
return Http.get(`/v1/task-schedules/${id}`); return Http.get(`/v1/task-schedules/${id}`);
}; };
//任务管理-手动添加
export const createTask = (params = {}) => {
return Http.post(`/v1/task-schedules/manual`, params);
};
//任务管理-手动添加
export const generateContent = (id: string) => {
return Http.post(`/v1/task-schedules/${id}/generate-content`);
};
// 任务管理-修改
export const editTaskSchedulesTime = (id: string, params = {}) => {
console.log('id', id);
return Http.patch(`/v1/task-schedules/${id}/execution-time`, params);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -11,23 +11,35 @@
:allowClear="allClear" :allowClear="allClear"
:showSearch="allowSearch" :showSearch="allowSearch"
showArrow showArrow
:maxTagCount="maxTagCount" :maxTagCount="maxTagCount !== undefined ? maxTagCount : multiple ? 3 : undefined"
@change="handleChange" @change="handleChange"
@dropdownVisibleChange="onDropdownVisibleChange" @dropdownVisibleChange="onDropdownVisibleChange"
:filterOption="allowSearch ? filterOption : undefined"
> >
<Option v-for="(item, index) in options" :key="index" :value="item.id" :label="item.name"> <Option v-for="(item, index) in validOptions" :key="index" :value="item.value" :label="item.label">
<div class="flex items-center"> <div class="flex items-center">
<img v-if="item.icon" :src="item.icon" class="w-16px h-16px mr-8px rounded-4px" /> <img v-if="item.icon" :src="item.icon" class="w-16px h-16px mr-4px rounded-4px" />
{{ item.name }} <span>{{ item.label }}</span>
</div> </div>
</Option> </Option>
<template #tag="{ label, icon }">
<div class="flex items-center">
<img v-if="icon" :src="icon" class="w-16px h-16px mr-4px rounded-4px" />
<span>{{ label }}</span>
</div>
</template>
<template v-if="!allowSearch" #empty>
<div v-if="validOptions.length === 0" class="empty-placeholder">
{{ placeholder || '暂无数据' }}
</div>
</template>
</Select> </Select>
</template> </template>
<script setup> <script setup>
import { Select } from 'ant-design-vue'; import { Select } from 'ant-design-vue';
const { Option } = Select; const { Option } = Select;
import { ref, watch } from 'vue'; import { ref, watch, computed } from 'vue';
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -48,7 +60,7 @@ const props = defineProps({
}, },
maxTagCount: { maxTagCount: {
type: Number, type: Number,
default: 3, default: undefined,
}, },
allClear: { allClear: {
type: Boolean, type: Boolean,
@ -64,6 +76,26 @@ const emits = defineEmits(['update:modelValue', 'change', 'dropdownVisibleChange
const selectedValues = ref(props.multiple ? [] : ''); const selectedValues = ref(props.multiple ? [] : '');
// 计算有效选项,兼容不同的数据格式
const validOptions = computed(() => {
if (!props.options || !Array.isArray(props.options)) {
return [];
}
return props.options
.filter(
(item) =>
item &&
(item.id !== undefined || item.value !== undefined) &&
(item.name !== undefined || item.label !== undefined),
)
.map((item) => ({
...item,
value: item.id !== undefined ? item.id : item.value,
label: item.name !== undefined ? item.name : item.label,
}));
});
watch( watch(
() => props.modelValue, () => props.modelValue,
(newVal) => { (newVal) => {
@ -84,3 +116,11 @@ const onDropdownVisibleChange = (visible) => {
emits('dropdownVisibleChange', visible); emits('dropdownVisibleChange', visible);
}; };
</script> </script>
<style scoped>
.empty-placeholder {
padding: 8px 12px;
color: #8f959e;
text-align: center;
}
</style>

View File

@ -0,0 +1,115 @@
<!-- // 新增内容: -->
<template>
<a-popover trigger="click" :overlayStyle="{ width: '300px' }" overlayClassName="filter-popup-popover">
<template #content>
<div>
<!-- 运营人员 -->
<div class="flex items-center mb-6">
<div class="w-70px">运营人员</div>
<a-space class="w-200px">
<CommonSelect
placeholder="请选择运营人员"
:options="operators"
v-model="localQuery.operator"
@change="(val) => handleChange('operator_id', val)"
class="!w-200px"
:allowSearch="true"
/>
</a-space>
</div>
<!-- 发布平台 -->
<div class="flex items-center mb-6">
<div class="w-70px">发布平台</div>
<a-space class="w-200px">
<CommonSelect
:options="platformOptions"
v-model="localQuery.platform"
@change="(val) => handleChange('platform', val)"
class="!w-200px"
placeholder="请选择发布平台"
:allowSearch="true"
/>
</a-space>
</div>
<!-- 账号名称 -->
<div class="flex items-center">
<div class="w-70px">账号名称</div>
<a-space class="w-200px">
<CommonSelect
v-model="localQuery.accounts"
:options="accountList"
:multiple="true"
@change="(val) => handleChange('account_id', val)"
class="!w-200px"
placeholder="请选择账号名称"
:allowSearch="true"
/>
</a-space>
</div>
</div>
</template>
<a-button size="small">
<template #icon>
<FilterOutlined class="color-#55585F" />
</template>
<template #default>筛选</template>
</a-button>
</a-popover>
</template>
<script setup>
import { ref, watch } from 'vue';
import { FilterOutlined } from '@ant-design/icons-vue';
const props = defineProps({
operators: Array,
platformOptions: Array,
accountList: Array,
query: Object,
triggerStyle: Object,
});
const emit = defineEmits(['filter-change']);
// 初始化本地查询对象,只设置非空值
const localQuery = ref({});
// 监听外部 query 变化并同步到本地状态
watch(
() => props.query,
(newQuery) => {
if (newQuery) {
// 只有当值不为空时才设置到localQuery中
const filteredQuery = {};
if (newQuery.operator) {
filteredQuery.operator = newQuery.operator;
}
if (newQuery.platform) {
filteredQuery.platform = newQuery.platform;
}
if (newQuery.accounts && newQuery.accounts.length > 0) {
filteredQuery.accounts = newQuery.accounts;
}
localQuery.value = { ...filteredQuery };
}
},
{ deep: true, immediate: true },
);
// 处理筛选变化
const handleChange = (field, value) => {
localQuery.value[field] = value;
console.log(localQuery.value);
emit('filter-change', { ...localQuery.value });
};
</script>
<style scoped>
.filter-popup-popover :deep(.ant-popover-inner-content) {
background-color: #ffffff;
border: 1px solid #d9d9d9;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
padding: 16px;
}
</style>

View File

@ -1,7 +1,10 @@
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { merge } from 'lodash-es';
import { cloneDeep } from 'lodash-es';
interface UseTableSelectionWithPaginationOptions { interface UseTableSelectionWithPaginationOptions {
rowKey?: string; // 主键字段名,默认 'id' rowKey?: string; // 主键字段名,默认 'id'
type?: 'checkbox' | 'radio'; // 选择类型,默认 'checkbox'
pageInfo?: { pageInfo?: {
page?: number; page?: number;
page_size?: number; page_size?: number;
@ -20,6 +23,7 @@ const DEFAULT_PAGE_INFO = {
export function useTableSelectionWithPagination(options: UseTableSelectionWithPaginationOptions = {}) { export function useTableSelectionWithPagination(options: UseTableSelectionWithPaginationOptions = {}) {
const rowKey = options.rowKey || 'id'; const rowKey = options.rowKey || 'id';
const type = options.type || 'checkbox'; // 默认为复选框
const selectedRowKeys = ref<Array<string | number>>([]); const selectedRowKeys = ref<Array<string | number>>([]);
const selectedRows = ref<Array<any>>([]); const selectedRows = ref<Array<any>>([]);
@ -31,6 +35,17 @@ export function useTableSelectionWithPagination(options: UseTableSelectionWithPa
// 单行选择 // 单行选择
const handleSelect = (record: any, select: boolean) => { const handleSelect = (record: any, select: boolean) => {
const _targetKey = record[rowKey]; const _targetKey = record[rowKey];
if (type === 'radio') {
// 单选模式
if (select) {
selectedRows.value = [record];
selectedRowKeys.value = [_targetKey];
} else {
selectedRows.value = [];
selectedRowKeys.value = [];
}
} else {
// 多选模式(默认)
if (select) { if (select) {
selectedRows.value.push(record); selectedRows.value.push(record);
selectedRowKeys.value.push(_targetKey); selectedRowKeys.value.push(_targetKey);
@ -38,12 +53,16 @@ export function useTableSelectionWithPagination(options: UseTableSelectionWithPa
selectedRows.value = selectedRows.value.filter((v) => v[rowKey] !== _targetKey); selectedRows.value = selectedRows.value.filter((v) => v[rowKey] !== _targetKey);
selectedRowKeys.value = selectedRowKeys.value.filter((key) => key !== _targetKey); selectedRowKeys.value = selectedRowKeys.value.filter((key) => key !== _targetKey);
} }
}
options.onSelectChange?.(); options.onSelectChange?.();
}; };
// 全选/取消全选 // 全选/取消全选
const handleSelectAll = (checked: boolean) => { const handleSelectAll = (checked: boolean) => {
// 单选模式下不支持全选
if (type === 'radio') return;
const currentPageRows = dataSource.value; const currentPageRows = dataSource.value;
const currentPageKeys = currentPageRows.map((v) => v[rowKey]); const currentPageKeys = currentPageRows.map((v) => v[rowKey]);
@ -60,26 +79,44 @@ export function useTableSelectionWithPagination(options: UseTableSelectionWithPa
options.onSelectChange?.(); options.onSelectChange?.();
}; };
// 选择变更处理
const handleSelectChange = (keys: Array<string | number>, rows: Array<any>) => {
if (type === 'radio') {
// 单选模式下只保留最后一个选择
selectedRowKeys.value = keys.length > 0 ? [keys[keys.length - 1]] : [];
selectedRows.value = rows.length > 0 ? [rows[rows.length - 1]] : [];
} else {
// 多选模式
selectedRowKeys.value = keys;
selectedRows.value = rows;
}
options.onSelectChange?.();
};
const onPageChange = (page: number, pageSize: number) => { const onPageChange = (page: number, pageSize: number) => {
// console.log('onPageChange', page, pageSize); // console.log('onPageChange', page, pageSize);
pageInfo.value.page = page; pageInfo.value.page = page;
pageInfo.value.page_size = pageSize; pageInfo.value.page_size = pageSize;
options.onPageChange?.(page); options.onPageChange?.(page);
}; };
const onPageSizeChange = (current: number, size: number) => { const onPageSizeChange = (current: number, size: number) => {
// console.log('onPageSizeChange', current, size); // console.log('onPageSizeChange', current, size);
// pageInfo.value.page_size = size; // pageInfo.value.page_size = size;
// pageInfo.value.page = 1; // pageInfo.value.page = 1;
// options.onPageSizeChange?.(size); // options.onPageSizeChange?.(size);
}; };
const resetPageInfo = () => { const resetPageInfo = () => {
pageInfo.value = cloneDeep(DEFAULT_PAGE_INFO); pageInfo.value = cloneDeep(DEFAULT_PAGE_INFO);
}; };
const rowSelection = computed(() => ({ const rowSelection = computed(() => ({
type: 'checkbox', type: type,
showCheckedAll: true, showCheckedAll: type === 'checkbox', // 只有复选框模式才显示全选
width: 48, width: 48,
selectedRowKeys: selectedRowKeys.value,
onChange: handleSelectChange
})); }));
return { return {
@ -92,6 +129,7 @@ export function useTableSelectionWithPagination(options: UseTableSelectionWithPa
rowSelection, rowSelection,
handleSelect, handleSelect,
handleSelectAll, handleSelectAll,
handleSelectChange,
resetPageInfo, resetPageInfo,
DEFAULT_PAGE_INFO, DEFAULT_PAGE_INFO,
}; };

View File

@ -13,7 +13,7 @@ import SvgIcon from '@/components/svg-icon/index.vue';
import '@/api/index'; import '@/api/index';
import './core'; import './core';
import '@arco-design/web-vue/dist/arco.css'; // 已移除 Arco 样式 // import '@arco-design/web-vue/dist/arco.css'; // 已移除 Arco 样式
import 'normalize.css'; import 'normalize.css';
import 'uno.css'; import 'uno.css';

View File

@ -43,14 +43,6 @@ export const TABLE_COLUMNS = [
dataIndex: 'origin', dataIndex: 'origin',
width: 120, width: 120,
}, },
{
title: '上传时间',
dataIndex: 'created_at',
width: 180,
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
{ {
title: '上传人员', title: '上传人员',
dataIndex: 'uploader', dataIndex: 'uploader',

View File

@ -1,105 +0,0 @@
<template>
<div class="flex flex-col items-start cell-detail">
<div class="flex items-center" @click="gotoDetail">
<img
:src="getPlatformIcon(record.platform)"
style="border-radius: 8px; width: 16px; height: 16px; margin-right: 8px; font-size: 14px"
/>
{{ record.name || '-' }}
</div>
<div class="size-12px color-#939499 mt-2px" @click="gotoDetail">
{{ timestampToTime1(task.execution_time) }}
</div>
<div class="size-14px color-#211F24 mt-12px color-#6D4CFE" @click="gotoDetail">{{ task.name || '未命名' }}</div>
<div class="flex items-center mt-12px">
<div class="custom-time-picker">
<a-time-picker
format="HH:mm"
size="small"
:visible="isPanelVisible"
@visible-change="handleVisibleChange"
@change="handleTimeChange"
placeholder="修改发布时间"
class="hide-input-container opt-btn mr-12px"
/>
</div>
<div class="opt-btn" @click.stop="handleDelete" style="margin-right: 0px">删除</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits } from 'vue';
import { ref, computed } from 'vue';
const props = defineProps({
task: {
type: Object,
required: true,
},
record: {
type: Object,
required: true,
},
getPlatformIcon: {
type: Function,
required: true,
},
});
const isPanelVisible = ref(false);
const toggleTimePanel = () => {
isPanelVisible.value = !isPanelVisible.value;
};
const handleVisibleChange = (visible: boolean) => {
isPanelVisible.value = visible;
};
const handleTimeChange = (time: string) => {
if (time) {
emit('edit-time', props.task, timestampToTime1() + ' ' + time + ':00');
}
};
const timestampToTime1 = (): string => {
const timestamp = Date.now();
const date = new Date(timestamp);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // 补零
const day = String(date.getDate()).padStart(2, '0'); // 补零
return `${year}-${month}-${day}`;
};
const emit = defineEmits(['edit-time', 'delete', 'gotoDetail']);
const handleDelete = () => {
emit('delete', props.task);
};
const gotoDetail = () => {
console.log('跳转详情');
emit('gotoDetail', props.task);
};
</script>
<style scoped>
.cell-detail {
background-color: #fff;
border: 1px solid #d9d9d9;
border-radius: 4px;
box-shadow: #00000010 0px 2px 8px;
padding: 8px 16px;
margin-top: 8px;
}
.opt-btn {
font-size: 12px;
width: 138px;
height: 28px;
background-color: #f2f3f5;
text-align: center;
line-height: 28px;
border-radius: 4px;
margin-right: 12px;
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<a-popover trigger="click" overlayClassName="color-tip-popover">
<template #content>
<div class="flex items-center mt-8px w-104px mr-8px">
<div style="background-color: #ffae00; width: 16px; height: 16px; margin-right: 5px; border-radius: 4px"></div>
<div>待生成</div>
</div>
<div class="flex items-center mt-8px w-104px mr-8px">
<div style="background-color: #6d4cfe; width: 16px; height: 16px; margin-right: 5px; border-radius: 4px"></div>
<div>待发布</div>
</div>
<div class="flex items-center mt-8px w-104px mr-8px">
<div style="background-color: #939499; width: 16px; height: 16px; margin-right: 5px; border-radius: 4px"></div>
<div>已发布</div>
</div>
<div class="flex items-center mt-8px w-104px mr-8px">
<div style="background-color: #f64b31; width: 16px; height: 16px; margin-right: 5px; border-radius: 4px"></div>
<div>发布失败</div>
</div>
</template>
<Button class="w-112px mr-8px" size="middle">
<template #icon>
<InfoCircleOutlined class="color-#55585F" />
</template>
<template #default>颜色示意</template>
</Button>
</a-popover>
</template>
<script setup>
import { InfoCircleOutlined } from '@ant-design/icons-vue';
import { Checkbox, Button, Space, Pagination, notification } from 'ant-design-vue';
</script>
<style scoped>
.color-tip-popover :deep(.ant-popover-inner-content) {
background-color: #ffffff;
border: 1px solid #d9d9d9;
border-radius: 4px;
box-shadow: #00000010 0px 2px 8px;
padding: 8px;
margin-top: 8px;
}
.color-tip-popover :deep(.ant-popover-arrow) {
display: none;
}
</style>

View File

@ -0,0 +1,280 @@
<template>
<div class="flex items-center">
<!-- 日选择器 -->
<a-date-picker
class="w-188px size-14px"
v-if="choseType === '日'"
@change="handleDateChange"
v-model="currentDate"
format="YYYY年MM月DD日周dd"
value-format="YYYY-MM-DD"
/>
<!-- 周选择器 -->
<a-week-picker
class="w-188px size-14px"
v-else-if="choseType === '周'"
@change="handleDateChange"
v-model="currentDate"
format="YYYY年MM月DD日"
value-format="YYYY-MM-DD"
/>
<!-- 月选择器 -->
<a-month-picker
class="w-188px size-14px"
v-else
@change="handleDateChange"
v-model="currentDate"
format="YYYY年MM月"
value-format="YYYY-MM"
/>
<!-- 日期导航按钮 -->
<div class="flex items-center ml-12px">
<a-button class="mr-4px prv-btn" @click="navigate(-1)" type="text">
<template #icon><icon-left /></template>
</a-button>
<a-button
@click="navigateToToday"
type="text"
style="background-color: #f7f8fa !important; color: #211f24 !important; height: 28px"
>今天</a-button
>
<a-button class="ml-4px prv-btn" @click="navigate(1)" type="text">
<template #icon><icon-right /></template>
</a-button>
</div>
<!-- 维度切换下拉框 -->
<div class="flex items-center">
<a-dropdown
position="bottom"
@select="handleTypeChange"
class="w-80px"
:popupVisible="dropdownVisible"
@popupVisibleChange="handleDropdownVisibleChange"
>
<a-button type="text" class="prv-today"> {{ choseType }}<icon-down class="ml-4px" /> </a-button>
<template #content>
<a-doption value="日" class="doption"></a-doption>
<a-doption value="周" class="doption"></a-doption>
<a-doption value="月" class="doption"></a-doption>
</template>
</a-dropdown>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, watch, defineProps, defineEmits, withDefaults, computed } from 'vue';
import DateUtils from '@/utils/DateUtils';
// 1. 定义Props接收父组件初始日期配置
interface Props {
modelValue?: {
choseType: '日' | '周' | '月';
dayModel?: Date;
weekModel?: Date;
monthModel?: Date;
};
}
const props = withDefaults(defineProps<Props>(), {
modelValue: () => ({ choseType: '周', dayModel: new Date(), weekModel: new Date(), monthModel: new Date() }),
});
// 2. 定义Emits向父组件传递事件
const emit = defineEmits([
'update:modelValue', // v-model双向绑定
'date-change', // 日期/维度切换统一事件(父组件只需要监听这个)
]);
// 3. 内部状态管理:根据当前维度同步日期
const choseType = ref<Props['modelValue']['choseType']>(props.modelValue.choseType);
const dayModel = ref(props.modelValue.dayModel || new Date());
const weekModel = ref(props.modelValue.weekModel || new Date());
const monthModel = ref(props.modelValue.monthModel || new Date());
// 下拉菜单显示状态
const dropdownVisible = ref(false);
// 添加一个标志位,用于避免初始化时触发事件
let isInitializing = true;
// 计算属性根据当前维度返回对应日期简化选择器v-model绑定
const currentDate = computed({
get() {
switch (choseType.value) {
case '日':
return dayModel.value;
case '周':
return weekModel.value;
case '月':
return monthModel.value;
}
},
set(val: Date) {
switch (choseType.value) {
case '日':
dayModel.value = val;
break;
case '周':
weekModel.value = val;
break;
case '月':
monthModel.value = val;
break;
}
},
});
// 4. 核心工具函数:获取当前维度的日期范围(给父组件用)
const getDateRange = (): { start: string; end: string } => {
if (choseType.value === '周') {
const range = DateUtils.getWeekRangeByDate(weekModel.value);
return { start: range.startFormatted, end: range.endFormatted };
}
if (choseType.value === '月') {
const date = monthModel.value;
const range = DateUtils.getMonthRangeByYearMonth(date.getFullYear(), date.getMonth() + 1);
return { start: range.startFormatted, end: range.endFormatted };
}
// 日维度
const dateStr = DateUtils.formatDate(dayModel.value);
return { start: dateStr, end: dateStr };
};
// 5. 维度切换处理(日/周/月)
const handleTypeChange = (val: '日' | '周' | '月') => {
choseType.value = val;
// 切换维度时默认选中当天对应的维度日期
const today = new Date();
currentDate.value = today;
emitChange();
// 选择后隐藏下拉菜单
setTimeout(() => {
dropdownVisible.value = false;
}, 100);
};
// 6. 日期选择器变更处理
const handleDateChange = (val: Date | string | undefined) => {
if (!val) return;
let selectedDate: Date;
if (val instanceof Date) {
selectedDate = val;
} else {
// 处理字符串格式的日期
if (choseType.value === '月') {
// 月份选择器返回 YYYY-MM 格式
const [year, month] = val.split('-').map(Number);
selectedDate = new Date(year, month - 1, 1);
} else {
// 日和周选择器返回 YYYY-MM-DD 格式
selectedDate = new Date(val);
}
}
currentDate.value = selectedDate;
emitChange();
};
// 7. 日期导航(上一个/下一个维度)
const navigate = (step: 1 | -1) => {
const current = currentDate.value;
const newDate = new Date(current);
switch (choseType.value) {
case '日':
newDate.setDate(current.getDate() + step);
break;
case '周':
newDate.setDate(current.getDate() + step * 7);
break;
case '月':
newDate.setMonth(current.getMonth() + step);
break;
}
currentDate.value = newDate;
emitChange();
};
// 8. 跳转今天
const navigateToToday = () => {
currentDate.value = new Date();
emitChange();
};
// 9. 下拉菜单显示状态变化处理
const handleDropdownVisibleChange = (visible: boolean) => {
dropdownVisible.value = visible;
};
// 10. 统一事件发射:向父组件传递完整信息
const emitChange = () => {
const result = {
choseType: choseType.value,
dayModel: dayModel.value,
weekModel: weekModel.value,
monthModel: monthModel.value,
dateRange: getDateRange(), // 父组件直接可用的日期范围
};
// 只有在初始化完成后才触发 update:modelValue 和 date-change 事件
if (!isInitializing) {
emit('update:modelValue', result);
console.log('emitChange', result);
emit('date-change', result); // 父组件监听此事件做后续处理
}
};
// 11. 监听父组件Props变化同步外部修改
watch(
() => props.modelValue,
(newVal) => {
if (newVal && !isInitializing) {
// 只有在初始化完成后才响应外部props变化
choseType.value = newVal.choseType;
dayModel.value = newVal.dayModel || new Date();
weekModel.value = newVal.weekModel || new Date();
monthModel.value = newVal.monthModel || new Date();
emitChange();
}
},
{ deep: true },
);
// 初始化完成,设置标志位,并在之后触发一次事件
setTimeout(() => {
isInitializing = false;
emitChange();
}, 0);
</script>
<style scoped>
.arco-dropdown-open .arco-icon-down {
transform: rotate(180deg);
transition: transform 0.2s ease;
}
.prv-btn {
background-color: #f7f8fa !important; /* 使用正确的6位十六进制颜色值 */
width: 28px;
height: 28px;
border-radius: 4px;
color: #211f24 !important;
}
.doption {
width: 78px !important;
}
.prv-today {
color: #211f24 !important;
width: 80px !important;
background-color: #f7f8fa !important;
height: 28px;
margin-left: 4px;
display: flex;
justify-content: space-between;
}
</style>

View File

@ -0,0 +1,625 @@
<template>
<a-drawer
title="创建任务"
cancel-text="取消"
ok-text="创建任务"
placement="right"
v-model:visible="showDriwer"
@after-visible-change="showDriwerChange"
@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="flex items-center w-full">
<img
v-for="item in selectedMaterials.images"
:key="item.id"
:src="item.cover"
class="w-88 h-88 mr-8px border-rounded-8px"
/>
</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">{{ selectedProducts.data[0].title }}</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"
/>
</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';
// 平台图标
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 = (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];
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 showDriwerChange = (visible: boolean) => {
console.log('Main Drawer visible: ', visible);
};
// 原料库抽屉显示状态变化
const handleMaterialDrawerVisibleChange = (visible: boolean) => {
console.log('Raw Material Drawer visible: ', visible);
};
// 成品库抽屉显示状态变化
const handleProductDrawerVisibleChange = (visible: boolean) => {
console.log('Finished Product Drawer visible: ', visible);
};
// 处理筛选条件变化
const handleChange = (field, value) => {
localQuery.value[field] = value;
localQuery.value.ids = [value];
emit('filter-change', {
accounts: localQuery.value.accounts,
});
};
// 点击创建任务按钮时触发
const handleCreateTask = () => {
// 验证表单
if (localQuery.value.ids.length == 0) {
// 可以添加错误提示:请选择发布内容
message.error('请选择发布账号');
return;
}
if (isActive.value === 'chose' && selectedProducts.value.keys.length === 0) {
// 可以添加错误提示:请选择发布内容
message.error('请选择发布内容');
return;
}
if (isActive.value === 'ai' && selectedMaterials.value.keys.length === 0) {
// 可以添加错误提示:请选择发布内容
message.error('请选择发布内容');
return;
}
console.log('有问题已返回');
// 准备提交的数据
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(() => {
localQuery.value.accounts = [accountInfo.name];
localQuery.value.ids = [accountInfo.id];
});
}
// 如果传入了日期,则设置为默认日期
if (selectedDate) {
currentDate.value = selectedDate;
console.log('currentDate', currentDate.value);
publishType.value = 'timing';
}
};
// 定义事件发射器
const emit = defineEmits(['filter-change', 'create-task']);
// 暴露方法
defineExpose({
showDrawer,
});
</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>

View File

@ -0,0 +1,181 @@
<template>
<div class="filter-popup-wrapper">
<!-- 关键修复添加手动控制弹窗显示/隐藏的逻辑 -->
<a-popover
trigger="click"
:visible="visible"
:overlayStyle="{ width: '360px' }"
overlayClassName="filter-popup-popover"
@clickoutside="handleClickOutside"
>
<template #content>
<div class="filter-popup-content">
<!-- 运营人员 -->
<div class="flex items-center mb-3 select-container">
<div class="w-70px pt-6px">运营人员</div>
<a-space class="flex-1">
<CommonSelect
placeholder="请选择运营人员"
:options="operators || []"
v-model="localQuery.operator"
@change="(val) => handleChange('operator', val)"
class="!w-240px select-with-tags"
:allowSearch="true"
:maxTagCount="999"
popup-container=".filter-popup-content"
/>
</a-space>
</div>
<!-- 发布平台 -->
<div class="flex items-center mb-3 select-container">
<div class="w-70px pt-6px">发布平台</div>
<a-space class="flex-1">
<CommonSelect
:options="platformOptions || []"
v-model="localQuery.platform"
@change="(val) => handleChange('platform', val)"
class="!w-240px select-with-tags"
placeholder="请选择发布平台"
:allowSearch="true"
:maxTagCount="999"
popup-container=".filter-popup-content"
/>
</a-space>
</div>
<!-- 账号名称 -->
<div class="flex items-center mb-3 select-container">
<div class="w-70px pt-6px">账号名称</div>
<a-space class="flex-1">
<CommonSelect
v-model="localQuery.accounts"
:options="accountList || []"
:multiple="true"
@change="(val) => handleChange('accounts', val)"
class="!w-240px select-with-tags"
placeholder="请选择账号名称"
:allowSearch="true"
:maxTagCount="999"
popup-container=".filter-popup-content"
/>
</a-space>
</div>
</div>
</template>
<!-- 触发按钮添加点击事件控制弹窗显示 -->
<Button class="w-112px mr-8px" size="middle" @click="visible = !visible">
<template #icon>
<FilterOutlined class="color-#55585F" />
</template>
<template #default>筛选</template>
</Button>
</a-popover>
</div>
</template>
<script setup>
import { ref, watch, defineProps, defineEmits } from 'vue';
import { FilterOutlined } from '@ant-design/icons-vue';
import { Button } from 'ant-design-vue';
// 定义props和emit
const props = defineProps({
operators: Array,
platformOptions: Array,
accountList: Array,
query: Object,
});
const emit = defineEmits(['filter-change']);
// 弹窗显示状态(关键:控制弹窗显示的核心变量)
const visible = ref(false);
// 本地筛选状态(保持上次选择)
const localQuery = ref({
operator: props.query?.operator_ids,
platform: props.query?.platforms,
accounts: props.query?.ids || [],
});
// 监听父组件query变化同步到本地
watch(
() => props.query,
(newQuery) => {
if (newQuery) {
localQuery.value = {
operator: newQuery.operator_ids,
platform: newQuery.platforms,
accounts: newQuery.ids || [],
};
}
},
{ deep: true, immediate: true },
);
// 处理筛选条件变化(不关闭弹窗,直接触发筛选)
const handleChange = (field, value) => {
localQuery.value[field] = value;
// 直接触发筛选变更,不需要确认按钮
emit('filter-change', {
operator: localQuery.value.operator,
platform: localQuery.value.platform,
accounts: localQuery.value.accounts,
});
// 选择后自动隐藏下拉菜单
setTimeout(() => {
visible.value = false;
}, 100);
};
// 点击外部关闭弹窗
const handleClickOutside = () => {
visible.value = false;
};
</script>
<style scoped>
.filter-popup-popover :deep(.ant-popover-inner-content) {
background-color: #ffffff;
border: 1px solid #d9d9d9;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 24px;
margin-top: 8px;
margin-right: 50px;
max-height: 400px;
overflow-y: auto;
}
.select-container {
align-items: flex-start;
}
.select-with-tags :deep(.ant-select-selector) {
height: auto !important;
min-height: 32px !important;
align-items: flex-start !important;
}
.select-with-tags :deep(.ant-select-selection-item) {
max-width: none !important;
overflow: visible !important;
text-overflow: unset !important;
line-height: 1.5;
}
/* 确保选择器容器能够适应内容高度 */
.select-with-tags :deep(.ant-select) {
height: auto !important;
}
.select-with-tags :deep(.ant-select-multiple .ant-select-selection-item) {
height: auto !important;
align-items: flex-start !important;
}
.select-with-tags :deep(.ant-select-selection-overflow) {
flex-wrap: wrap !important;
}
</style>

View File

@ -0,0 +1,270 @@
<template>
<a-drawer
title="成品库"
cancel-text="取消"
ok-text="确定"
placement="right"
:visible="visible"
@after-visible-change="onAfterVisibleChange"
@cancel="handleCancel"
width="904px"
class="task-drawer"
style="right: 481px"
>
<!-- 成品库表格 -->
<Table
:data-source="materialData"
bordered
:columns="columns"
:pagination="false"
row-key="id"
:row-selection="rowSelection"
>
<template #cover="{ record }">
<div class="flex items-center">
<img
:src="record.cover ? record.cover : icon4"
alt="内容预览"
class="w-44px h-44px border-rounded-8px bg-#F0EDFF"
/>
</div>
</template>
<template #name="{ record }">
<div class="flex items-center">{{ record.title }}</div>
</template>
<template #type="{ record }">
<div class="flex items-center">
<img :src="record.type === EnumManuscriptType.Image ? icon2 : icon3" width="16" height="16" class="mr-4px" />
<span class="cts" :class="record.type === EnumManuscriptType.Image ? '!color-#25C883' : '!color-#6D4CFE'">{{
record.type === EnumManuscriptType.Image ? '图文' : '视频'
}}</span>
</div>
</template>
<template #uploader="{ record }">
<div class="flex items-center">{{ record.last_modifier?.mobile || '-' }}</div>
</template>
<template #audit_status="{ record }">
<div
class="flex items-center justify-center font-size-14px p-4px border-rounded-4px"
:style="getAuditStatusStyle(record.audit_status)"
>
{{ getStatus(record.audit_status) }}
</div>
</template>
<template #last_modified_at="{ record }">
<div class="flex items-center">
{{ record.last_modified_at ? dayjs(record.last_modified_at * 1000).format('YYYY-MM-DD HH:mm:ss') : '-' }}
</div>
</template>
</Table>
<!-- 分页控件 -->
<div class="pagination-box">
<a-pagination
:total="pageInfo.total"
size="mini"
show-total
show-jumper
show-page-size
:current="pageInfo.page"
:page-size="pageInfo.page_size"
@change="onPageChange"
@page-size-change="onPageSizeChange"
/>
</div>
<!-- 底部操作栏 -->
<template #footer>
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="color-#737478 font-size-14px">已选择</div>
<div class="color-#737478 font-size-14px">{{ choseText }}</div>
</div>
<div class="flex justify-end">
<Button @click="handleCancel">取消</Button>
<Button class="ml-16px" type="primary" @click="handleOk">确定</Button>
</div>
</div>
</template>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, watch, defineProps, defineEmits, watchEffect } from 'vue';
import { Table, Button, Pagination } from 'ant-design-vue';
import dayjs from 'dayjs';
import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination';
import { EnumManuscriptType } from '@/views/material-center/components/finished-products/manuscript/list/constants';
import { getWorksPage } from '@/api/all/generationWorkshop.ts';
// 引入图片资源
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';
// 定义Props
const props = defineProps({
visible: {
type: Boolean,
required: true,
default: false,
},
query: {
type: Object,
required: true,
default: () => ({
page: 1,
page_size: 10,
platforms: undefined,
operator_ids: undefined,
ids: [],
keyword: '',
audit_status: undefined,
type: undefined,
}),
},
});
// 定义Emits
const emit = defineEmits(['update:visible', 'after-visible-change', 'confirm', 'cancel']);
// 内部状态管理
const materialData = ref([]);
const choseText = ref('');
const choseImgArray = ref([]);
// 表格列配置
const columns = ref([
{ title: '图片/视频', dataIndex: 'cover', width: 200, slots: { customRender: 'cover' } },
{ title: '文件名称', dataIndex: 'name', width: 200, slots: { customRender: 'name' } },
{ title: '稿件类型', dataIndex: 'type', width: 100, slots: { customRender: 'type' } },
{ title: '审核状态', dataIndex: 'audit_status', width: 100, slots: { customRender: 'audit_status' } },
{ title: '最后修改时间', dataIndex: 'last_modified_at', width: 200, slots: { customRender: 'last_modified_at' } },
{ title: '上传人员', dataIndex: 'uploader', width: 200, slots: { customRender: 'uploader' } },
]);
// 1. 先定义数据获取函数确保使用最新的query参数
const fetchProductData = async () => {
try {
// 使用主组件传递的最新查询参数
const params = { ...props.query };
console.log('成品库请求参数:', params); // 调试用
const res = await getWorksPage(params);
materialData.value = [];
pageInfo.value.total = res.data.total;
if (pageInfo.value.page === 1) {
materialData.value = res.data.data;
} else {
materialData.value = [...materialData.value, ...res.data.data];
}
} catch (error) {
console.error('获取成品库数据失败:', error);
}
};
// 2. 初始化表格选择逻辑
const { pageInfo, onPageChange, onPageSizeChange, rowSelection, selectedRowKeys, selectedRows } =
useTableSelectionWithPagination({
rowKey: 'id',
type: 'radio', // 改为单选模式
onPageChange: fetchProductData,
onPageSizeChange: fetchProductData,
});
// 监听query参数变化当主组件传递的参数变化时重新请求数据
watchEffect(() => {
if (props.visible) {
console.log('成品库查询参数变化,重新加载数据');
fetchProductData();
}
});
// 监听选中项变化
watch(selectedRows, (newRows) => {
if (rowSelection.value.type === 'radio') {
// 单选模式
choseText.value = newRows.length > 0 ? '1个稿件' : '0个稿件';
// 如果类型是文本则choseImgArray为空数组
if (newRows.length > 0 && newRows[0].type !== 'text') {
choseImgArray.value = [newRows[0]];
} else {
choseImgArray.value = [];
}
} else {
// 多选模式
choseText.value = newRows.length + '个稿件';
// 过滤掉文本类型,只保留非文本类型的项目
choseImgArray.value = newRows.filter((item) => item.type !== 'text');
}
});
// 格式化审核状态显示
const getStatus = (status: number) => {
switch (status) {
case 1:
return '待审核';
case 2:
return '审核中';
case 3:
return '已通过';
default:
return '未知';
}
};
const getAuditStatusStyle = (status: number) => {
switch (status) {
case 1:
return { backgroundColor: '#f0f0f0', color: '#666' };
case 2:
return { backgroundColor: '#fff7ed', color: '#ff9c00' };
case 3:
return { backgroundColor: '#e6ffe6', color: '#008000' };
default:
return { backgroundColor: '#f0f0f0', color: '#666' };
}
};
// 抽屉显示状态变化处理
const onAfterVisibleChange = (visible: boolean) => {
emit('after-visible-change', visible);
if (visible) {
// 当抽屉显示时,使用最新参数请求数据
fetchProductData();
} else {
// 关闭时清空数据
materialData.value = [];
selectedRowKeys.value = [];
choseText.value = '';
choseImgArray.value = [];
}
};
// 取消按钮处理
const handleCancel = () => {
emit('cancel');
};
// 确定按钮处理
const handleOk = () => {
emit('confirm', {
selectedKeys: selectedRowKeys.value,
selectedData: selectedRows.value,
selectedRows: selectedRows.value,
choseText: choseText.value,
choseImgArray: choseImgArray.value,
});
emit('update:visible', false);
};
</script>
<style scoped>
.pagination-box {
display: flex;
width: 100%;
padding: 16px 0;
justify-content: flex-end;
align-items: center;
}
</style>

View File

@ -0,0 +1,295 @@
<template>
<a-drawer
title="原料库"
cancel-text="取消"
ok-text="确定"
placement="right"
:visible="visible"
@after-visible-change="onAfterVisibleChange"
@cancel="handleCancel"
width="904px"
class="task-drawer"
style="right: 481px"
>
<!-- 成品库表格 -->
<Table
:data-source="materialData"
bordered
:columns="columns"
:pagination="false"
row-key="id"
:row-selection="rowSelection"
>
<template #name="{ record }">
<div class="name-cell flex items-center">
<img
:src="record.type == 2 ? icon5 : record.cover"
alt="类型图标"
class="w-44px h-44px border-rounded-8px bg-#F0EDFF"
/>
<div class="flex flex-col ml-8px">
<span class="material-name">{{ record.name }}</span>
<span class="material-type">{{ record.uid }}</span>
</div>
</div>
</template>
<template #type="{ record }">
<div class="flex items-center">
<span>{{ getType(record.type) }}</span>
</div>
</template>
<template #origin="{ record }">
<span>{{ getOrigin(record.origin) }}</span>
</template>
<template #created_at="{ record }">
<div class="flex items-center">
{{ record.created_at ? dayjs(record.created_at * 1000).format('YYYY-MM-DD HH:mm:ss') : '-' }}
</div>
</template>
</Table>
<!-- 分页控件 -->
<div class="pagination-box">
<a-pagination
:total="pageInfo.total"
size="mini"
show-total
show-jumper
show-page-size
:current="pageInfo.page"
:page-size="pageInfo.page_size"
@change="onPageChange"
@page-size-change="onPageSizeChange"
/>
</div>
<!-- 底部操作栏 -->
<template #footer>
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="color-#737478 font-size-14px">已选择</div>
<div class="color-#737478 font-size-14px">{{ choseText }}</div>
<div v-for="item in choseImgArray" :key="item.id" class="ml-16px">
<img
:src="item.cover ? item.cover : icon4"
alt="选中的内容"
class="w-44px h-44px border-rounded-8px bg-#F0EDFF"
/>
</div>
<div
v-for="item in choseTextArray"
:key="item.id"
class="ml-16px bg-#F7F8FA h-44px overflow-hidden w-75px border-rounded-8px flex items-center"
>
<div class="whitespace-nowrap overflow-hidden text-ellipsis w-full px-2" :title="item.name">
{{ item.name }}
</div>
</div>
</div>
<div class="flex justify-end">
<Button @click="handleCancel">取消</Button>
<Button class="ml-16px" type="primary" @click="handleOk">确定</Button>
</div>
</div>
</template>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, watch, defineProps, defineEmits, watchEffect } from 'vue';
import { Table, Button, Pagination } from 'ant-design-vue';
import dayjs from 'dayjs';
import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination';
import { EnumManuscriptType } from '@/views/material-center/components/finished-products/manuscript/list/constants';
import { getRawMaterialsPage } from '@/api/all/generationWorkshop';
// 引入图片资源
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 { RawMaterialType } from '@/views/material-center/components/raw-material/constants';
import icon5 from '../../../../views/material-center/components/raw-material/img/icon-no-text.png';
// 定义Props
const props = defineProps({
visible: {
type: Boolean,
required: true,
default: false,
},
query: {
type: Object,
required: true,
default: () => ({
page: 1,
page_size: 10,
platforms: undefined,
operator_ids: undefined,
ids: [],
keyword: '',
audit_status: undefined,
type: undefined,
}),
},
});
// 辅助函数
const getType = (type: number): string => {
const typeMap = {
[RawMaterialType.Image]: '图片',
[RawMaterialType.Video]: '视频',
[RawMaterialType.Text]: '文本',
};
return typeMap[type] || '未知';
};
const getOrigin = (origin: number): string => {
const fromMap = {
0: '本地上传',
1: 'AI生成',
};
return fromMap[origin] || '未知';
};
// 定义Emits类型
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
(
e: 'confirm',
data: {
selectedKeys: string[];
selectedData: any[];
choseText: string;
choseImgArray: any[];
selectedTexts: string[];
},
): void;
(e: 'cancel'): void;
}>();
// 内部状态管理
const materialData = ref([]);
const choseText = ref('');
const choseImgArray = ref([]);
const choseTextArray = ref([]);
// 表格列配置
const columns = ref([
{ title: '文件名称', dataIndex: 'name', width: 200, slots: { customRender: 'name' } },
{ title: '类型', dataIndex: 'type', width: 100, slots: { customRender: 'type' } },
{ title: '来源', dataIndex: 'origin', width: 100, slots: { customRender: 'origin' } },
{ title: '上传时间', dataIndex: 'created_at', width: 200, slots: { customRender: 'created_at' } },
]);
// 1. 先定义数据获取函数确保使用最新的query参数
const fetchProductData = async () => {
try {
// 使用主组件传递的最新查询参数
const params = { ...props.query };
console.log('成品库请求参数:', params); // 调试用
const res = await getRawMaterialsPage(params);
materialData.value = [];
pageInfo.value.total = res.data.total;
if (pageInfo.value.page === 1) {
materialData.value = res.data.data;
} else {
materialData.value = [...materialData.value, ...res.data.data];
}
} catch (error) {
console.error('获取成品库数据失败:', error);
}
};
// 2. 初始化表格选择逻辑
const { pageInfo, onPageChange, onPageSizeChange, rowSelection, selectedRowKeys } = useTableSelectionWithPagination({
rowKey: 'id',
onPageChange: fetchProductData,
onPageSizeChange: fetchProductData,
});
// 监听query参数变化当主组件传递的参数变化时重新请求数据
watchEffect(() => {
if (props.visible) {
console.log('成品库查询参数变化,重新加载数据');
fetchProductData();
}
});
// 监听选中项变化
watch(selectedRowKeys, (newKeys) => {
const filteredData = materialData.value.filter((item) => newKeys.includes(item.id));
const typeCount: Record<string, number> = {};
filteredData.forEach((item) => {
typeCount[item.type] = (typeCount[item.type] || 0) + 1;
});
choseText.value = Object.entries(typeCount)
.map(([type, count]) => {
const typeName = getType(Number(type));
return `${typeName}: ${count}`;
})
.join(' ');
choseImgArray.value = filteredData.filter((item) => [0, 1].includes(item.type));
choseTextArray.value = filteredData.filter((item) => [2].includes(item.type));
});
// 格式化审核状态显示
const getStatus = (status: number) => {
switch (status) {
case 0:
return '待审核';
case 1:
return '审核中';
case 2:
return '已通过';
case 3:
return '已拒绝';
default:
return '未知';
}
};
// 抽屉显示状态变化处理
const onAfterVisibleChange = (visible: boolean) => {
emit('after-visible-change', visible);
if (visible) {
// 当抽屉显示时,使用最新参数请求数据
fetchProductData();
} else {
// 关闭时清空数据
materialData.value = [];
selectedRowKeys.value = [];
choseText.value = '';
choseImgArray.value = [];
}
};
// 取消按钮处理
const handleCancel = () => {
console.log('取消');
emit('cancel');
};
// 确定按钮处理
const handleOk = () => {
const selectedData = materialData.value.filter((item) => selectedRowKeys.value.includes(item.id));
const selectedTexts = selectedData
.filter((item) => item.type === RawMaterialType.Text)
.map((item) => item.content || item.name);
emit('confirm', {
selectedKeys: selectedRowKeys.value,
selectedData,
choseText: choseText.value,
choseImgArray: choseImgArray.value,
selectedTexts: selectedTexts,
});
emit('update:visible', false);
};
</script>
<style scoped>
.pagination-box {
display: flex;
width: 100%;
padding: 16px 0;
justify-content: flex-end;
align-items: center;
}
</style>

View File

@ -0,0 +1,372 @@
<template>
<a-trigger trigger="click" position="br" @click.stop @popup-visible-change="onPopupVisibleChange">
<div class="task-item">
<div class="color-indicator" :style="{ background: getTaskColor() }"></div>
<div>{{ timestampToTime(task.execution_time) }}</div>
<div class="task-name" :title="task?.name || '-'">{{ task?.name || 'AI生成内容' }}</div>
</div>
<template #content>
<div class="flex flex-col items-start popup-content" @click="gotoDetail">
<div class="flex flex-col items-start p-16px w-full" @click="gotoDetail">
<div class="flex justify-between w-full items-center">
<div class="flex items-center title-container">
<img
:src="getPlatformIcon(record.platform)"
style="border-radius: 8px; width: 16px; height: 16px; margin-right: 8px; font-size: 14px"
/>
<div class="task-title" :title="record.name || 'AI生成内容'">{{ record.name || 'AI生成内容' }}</div>
</div>
<div
class="status-tag"
:style="{
color: getTaskColor(),
backgroundColor: getTaskColor() + '50',
}"
>
{{ getTaskStatusText(task.status) }}
</div>
</div>
<div class="text-12px color-#939499 mt-4px h-22px" @click="gotoDetail">
{{ timestampToTime1(task.execution_time) }}
</div>
</div>
<div class="font-size-14px color-#211F24 color-#211F24 task-description" :title="task.name || 'AI生成内容'">
{{ task.name || 'AI生成内容' }}
</div>
<div v-if="task.ai_generate_status == 0" class="AILoding">
<ASpin />
<div style="color: #9a56ba">内容生成中...</div>
<div style="color: #9a56ba">完成后将自动展示您可先返回其他操作</div>
</div>
<div v-else-if="task.ai_generate_status == 1 && taskDetail" class="w-full pl-16px">
<div
v-for="item in choseTextArray"
: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="flex items-center w-full">
<img
v-for="item in choseImgArray"
:key="item.id"
:src="item.cover || item.url"
class="w-88 h-88 mr-8px border-rounded-8px"
/>
</div>
<div class="flex items-center w-full">
<video
v-for="item in choseVideoArray"
:key="item.id"
:src="item.cover || item.url"
class="w-44 h-44 mr-8px border-rounded-8px object-fit-contain"
/>
</div>
</div>
<div class="flex items-center mt-12px w-full h-1px bg-#E6E6E8"></div>
<div class="flex items-center mt-12px mb-16px action-buttons w-full px-16px">
<div class="flex items-center mr-12px" @click.stop="handleDelete" v-if="task.status != 1">
<icon-delete style="font-size: 20px; margin-left: 0" />
</div>
<div class="flex w-full" :class="{ 'justify-between': task.ai_generate_status == 0 }">
<a-date-picker
v-model="datePickerValue"
placeholder="修改发布时间"
:show-time="{ format: 'HH:mm' }"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm"
@change="onChange"
@ok="onOk"
@click.stop
@mousedown.stop
@focus.stop
/>
<button v-if="task.ai_generate_status == 0" class="opt-btn ml-12px opt-right" @click.stop="handleAICreate">
AI立即生成
</button>
</div>
</div>
</div>
</template>
</a-trigger>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { Spin as ASpin } from 'ant-design-vue';
import dayjs from 'dayjs';
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';
import { getTaskSchedulesDetail } from '@/api/all/assignment-management';
import { DatePicker } from '@arco-design/web-vue';
// 定义props和emit
const props = defineProps({
task: Object,
record: Object,
});
const taskDetail = ref(null);
const choseImgArray = ref([]);
const choseTextArray = ref([]);
const choseVideoArray = ref([]);
const getTaskDetail = async () => {
if (!props.task || !props.task.id) return;
try {
const res = await getTaskSchedulesDetail(props.task.id);
if (res && res.data) {
datePickerValue.value = dayjs(res.data.execution_time * 1000);
console.log('任务详情:', datePickerValue.value);
if ('work' in res.data && res.data.work) {
taskDetail.value = res.data.work;
choseImgArray.value = res.data.work.files.filter((item) => [0].includes(item.type));
choseTextArray.value = res.data.work.files.filter((item) => [2].includes(item.type));
choseVideoArray.value = res.data.work.files.filter((item) => [1].includes(item.type));
} else {
taskDetail.value = res.data.raw_materials;
choseImgArray.value = taskDetail.value.filter((item) => [0].includes(item.type));
choseTextArray.value = taskDetail.value.filter((item) => [2].includes(item.type));
choseVideoArray.value = taskDetail.value.filter((item) => [1].includes(item.type));
}
}
} catch (error) {
console.error('获取任务详情失败:', error);
}
};
const onChange = (date, dateString) => {
console.log('Selected Date: ', date, dateString);
if (date) {
emit('handle-task', 'edit-time', props.task, dateString);
}
};
const onOk = (value) => {
console.log('DatePicker OK: ', value);
if (value) {
emit('handle-task', 'edit-time', props.task, value);
}
};
const onPopupVisibleChange = (visible) => {
if (visible) {
getTaskDetail();
}
};
const handleDelete = () => {
emit('handle-task', 'delete', props.task, props.record);
};
const gotoDetail = () => {
console.log('跳转详情');
emit('handle-task', 'goto-detail', props.task, props.record);
};
const handleTimeChange = (time: string) => {
// if (time) {
// emit('handle-task', 'edit-time', props.task, timestampToTime1() + ' ' + time + ':00');
// }
};
const handleAICreate = () => {
emit('handle-task', 'ai-create', props.task, props.record);
};
const timestampToTime = (timestamp: number): string => {
// 如果没有传入时间戳,则返回空字符串
if (!timestamp) return '';
// 将时间戳转换为毫秒(如果时间戳是秒单位的话)
const date = new Date(timestamp * 1000);
// 格式化为 HH:mm 格式
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${hours}:${minutes}`;
};
const timestampToTime1 = (timestamp: number): string => {
// 如果没有传入时间戳,则使用当前时间
if (!timestamp) {
timestamp = Date.now() / 1000; // 使用秒级时间戳保持一致性
}
const date = new Date(timestamp * 1000);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // 补零
const day = String(date.getDate()).padStart(2, '0'); // 补零
return `${month}${day}`;
};
const emit = defineEmits(['filter-change', 'handle-task']);
// 日期选择器的值
const datePickerValue = ref(null);
// 平台配置
const platformConfig = {
icons: {
0: iconDy,
1: iconXhs,
2: iconBilibili,
3: iconKs,
4: iconSph,
5: iconWb,
6: iconGzh,
},
};
// 获取平台图标
const getPlatformIcon = (platform: number) => platformConfig.icons[platform] || iconWarn;
// 根据任务类型获取颜色
const getTaskColor = () => {
if (!props.task) return '#000';
// 根据colorTip.vue中的颜色定义设置不同状态的颜色
switch (props.task.status) {
case 0: // 待生成
return '#ffae00';
case 1: // 待发布
return '#6d4cfe';
case 2: // 已发布
return '#939499';
case 3: // 发布失败
return '#f64b31';
default:
return props.task.color || '#939499';
}
};
const getTaskStatusText = (status) => {
console.log('任务状态:', status);
switch (status) {
case 0:
return '待生成';
case 1:
return '待发布';
case 2:
return '已发布';
case 3:
return '发布失败';
default:
return '未知状态';
}
};
</script>
<style lang="less" scoped>
.task-item {
width: 100%;
font-size: 12px;
height: 19px;
display: flex;
align-items: center;
gap: 4px;
min-width: 0;
&:last-child {
margin-bottom: 0;
}
}
.color-indicator {
width: 4px;
height: 18px;
border-radius: 2px;
flex-shrink: 0;
}
.task-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
width: 0;
}
.popup-content {
width: 388px;
background-color: #fff;
box-shadow: #e6e6e8 0px 2px 8px;
border-radius: 4px;
}
.title-container {
flex: 1;
min-width: 0;
}
.task-title {
font-size: 16px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
line-height: 1.2;
}
.task-description {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.2;
flex: 1;
min-width: 0;
width: 365px;
margin-top: -10px;
margin-bottom: 12px;
padding: 0 16px;
}
.status-tag {
padding: 2px 8px;
border-radius: 4px;
flex-shrink: 0;
}
.opt-btn {
width: 154px;
height: 32px;
font-size: 14px;
background: #f2f3f5;
border: none;
border-radius: 4px;
}
.opt-right {
background: linear-gradient(90deg, #fbf9ff 0%, #f3fafe 100%);
color: #9a56ba;
}
.AILoding {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 356px;
height: 108px;
border-radius: 8px;
background: linear-gradient(90deg, #fbf9ff 0%, #f3fafe 100%);
color: linear-gradient(90deg, #9a56ba 0%, #576fd1 100%);
margin-left: 16px;
font-weight: 400;
font-style: Regular;
font-size: 12px;
leading-trim: NONE;
line-height: 20px;
letter-spacing: 0%;
text-align: center;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -8936,6 +8936,11 @@ vue-eslint-parser@^9.0.0, vue-eslint-parser@^9.0.1, vue-eslint-parser@^9.1.0, vu
lodash "^4.17.21" lodash "^4.17.21"
semver "^7.3.6" semver "^7.3.6"
vue-lazyload@^3.0.0:
version "3.0.0"
resolved "https://registry.npmmirror.com/vue-lazyload/-/vue-lazyload-3.0.0.tgz"
integrity sha512-h2keL/Rj550dLgesgOtXJS9qOiSMmuJNeVlfNAYV1/IYwOQYaWk5mFJlwRxmZDK9YC5gECcFLYYj7z1lKSf9ug==
vue-router@^4.4.0: vue-router@^4.4.0:
version "4.5.1" version "4.5.1"
resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz" resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz"