Merge remote-tracking branch 'origin/main' into feature/0707_产品权限

This commit is contained in:
rd
2025-07-09 09:55:47 +08:00
10 changed files with 129 additions and 78 deletions

View File

@ -80,7 +80,7 @@
> >
<icon-arrow-up v-if="detailData[field.dataIndex] > 0" size="16" /> <icon-arrow-up v-if="detailData[field.dataIndex] > 0" size="16" />
<icon-arrow-down v-else size="16" /> <icon-arrow-down v-else size="16" />
{{ `${(detailData[field.dataIndex] * 100).toFixed(2)}%` }} {{ `${detailData[field.dataIndex]}%` }}
</div> </div>
</template> </template>
<template v-else> <template v-else>

View File

@ -40,8 +40,12 @@ export const getAccountInfoFields = (dateType: string, showMore: boolean) => {
], ],
[ [
{ title: 'AI评价', dataIndex: 'ai_evaluation' }, { title: 'AI评价', dataIndex: 'ai_evaluation' },
{ title: '粉丝量', dataIndex: 'fans_number', tooltip: '粉丝量' }, { title: '粉丝量', dataIndex: 'fans_number', tooltip: '账号的当前粉丝总数。' },
{ title: '总赞藏数', dataIndex: 'like_collect_number', tooltip: '总赞藏数' }, {
title: '总赞藏数',
dataIndex: 'like_collect_number',
tooltip: '账号所有内容获得的点赞数与收藏数总和,用于衡量历史内容的整体吸引力与认可度。',
},
], ],
]; ];
const customFields = groupArrayBySize(CUSTOM_FIELDS, 4, dateType); const customFields = groupArrayBySize(CUSTOM_FIELDS, 4, dateType);

View File

@ -107,8 +107,8 @@
</div> </div>
</div> </div>
<PauseAccountPatchModal ref="pauseAccountPatchModalRef" @success="emits('update')" /> <PauseAccountPatchModal ref="pauseAccountPatchModalRef" @success="emits('update')" />
<ReauthorizeAccountModal ref="reauthorizeAccountModalRef" @success="emits('update')" /> <ReauthorizeAccountModal ref="reauthorizeAccountModalRef" @update="emits('update')" />
<AuthorizedAccountModal ref="authorizedAccountModalRef" @success="emits('update')" /> <AuthorizedAccountModal ref="authorizedAccountModalRef" @update="emits('update')" />
</div> </div>
</template> </template>
@ -166,11 +166,13 @@ const openDelete = (item) => {
}; };
const handleReauthorize = (item) => { const handleReauthorize = (item) => {
const isUnauthorized = isUnauthorizedStatus(item.status); console.log({ item });
const { id, platform, status } = item;
const isUnauthorized = isUnauthorizedStatus(status);
if (isUnauthorized) { if (isUnauthorized) {
authorizedAccountModalRef.value?.open(item.id); authorizedAccountModalRef.value?.open(id, platform);
} else { } else {
reauthorizeAccountModalRef.value?.open(item.id); reauthorizeAccountModalRef.value?.open(id, platform);
} }
}; };

View File

@ -124,7 +124,7 @@
</a-button> </a-button>
</template> </template>
<AuthorizedAccountModal ref="authorizedAccountModalRef" /> <AuthorizedAccountModal ref="authorizedAccountModalRef" @update="emits('update')" />
<ImportPromptModal ref="importPromptModalRef" /> <ImportPromptModal ref="importPromptModalRef" />
</a-modal> </a-modal>
</template> </template>
@ -322,15 +322,15 @@ async function onSubmit() {
if (isEdit.value) { if (isEdit.value) {
onClose(); onClose();
} else { } else {
startAuthorized(data?.id); startAuthorized(data?.id, data?.platform);
} }
} }
} }
}); });
} }
const startAuthorized = (id) => { const startAuthorized = (id, platform) => {
authorizedAccountModalRef.value.open(id); authorizedAccountModalRef.value.open(id, platform);
}; };
const handleDownloadTemplate = async () => { const handleDownloadTemplate = async () => {

View File

@ -34,7 +34,7 @@
<div class="img-box"> <div class="img-box">
<template v-if="qrCodeLoading || isFailLoadQrCode"> <template v-if="qrCodeLoading || isFailLoadQrCode">
<div class="relative w-160px h-160px"> <div class="relative w-160px h-160px">
<a-image :src="icon1" width="160" height="160" /> <a-image v-if="qrCodeLoading" :src="icon1" width="160" height="160" />
<div class="absolute top-0 left-0 z-2 w-full h-full flex flex-col items-center justify-center"> <div class="absolute top-0 left-0 z-2 w-full h-full flex flex-col items-center justify-center">
<img v-if="isFailLoadQrCode" :src="icon4" width="24" height="24" class="mb-13px" /> <img v-if="isFailLoadQrCode" :src="icon4" width="24" height="24" class="mb-13px" />
<icon-loading v-else size="24" class="color-#6D4CFE mb-13px" /> <icon-loading v-else size="24" class="color-#6D4CFE mb-13px" />
@ -55,7 +55,7 @@
<p class="s1">请点击刷新</p> <p class="s1">请点击刷新</p>
</div> </div>
</div> </div>
<span class="mt-16px"> 请使用抖音扫码将公司账号绑定至灵机平台 </span> <span class="mt-16px"> 请使用{{ platform }}扫码将公司账号绑定至灵机平台 </span>
</template> </template>
</template> </template>
</div> </div>
@ -88,7 +88,7 @@ const isSuccess = ref(false);
const failReason = ref(''); const failReason = ref('');
const progress = ref(0); const progress = ref(0);
const id = ref(''); const id = ref('');
const platform = ref('');
const isFailLoadQrCode = ref(false); const isFailLoadQrCode = ref(false);
const qrCodeUrl = ref(''); const qrCodeUrl = ref('');
const qrCodeLoading = ref(false); const qrCodeLoading = ref(false);
@ -103,8 +103,9 @@ const confirmBtnText = computed(() => {
return isSuccess.value ? '继续添加' : '重新扫码'; return isSuccess.value ? '继续添加' : '重新扫码';
}); });
const open = (accountId) => { const open = (accountId, platformCode) => {
id.value = accountId; id.value = accountId;
platform.value = platformCode === 0 ? '抖音' : '小红书';
getAuthorizedQrCode(); getAuthorizedQrCode();
visible.value = true; visible.value = true;
}; };
@ -114,6 +115,7 @@ const resetTaskFields = () => {
isCompleted.value = false; isCompleted.value = false;
isSuccess.value = false; isSuccess.value = false;
failReason.value = ''; failReason.value = '';
platform.value = '';
progress.value = 0; progress.value = 0;
qrCodeUrl.value = ''; qrCodeUrl.value = '';
qrCodeLoading.value = false; qrCodeLoading.value = false;
@ -168,6 +170,7 @@ const startStatusPolling = () => {
clearFakeProgressTimer(); clearFakeProgressTimer();
clearStatusPollingTimer(); clearStatusPollingTimer();
isLoading.value = false; isLoading.value = false;
emits('update');
} }
} }
}, 2000); }, 2000);

View File

@ -34,7 +34,7 @@
<div class="img-box"> <div class="img-box">
<template v-if="qrCodeLoading || isFailLoadQrCode"> <template v-if="qrCodeLoading || isFailLoadQrCode">
<div class="relative w-160px h-160px"> <div class="relative w-160px h-160px">
<a-image :src="icon1" width="160" height="160" /> <a-image v-if="qrCodeLoading" :src="icon1" width="160" height="160" />
<div class="absolute top-0 left-0 z-2 w-full h-full flex flex-col items-center justify-center"> <div class="absolute top-0 left-0 z-2 w-full h-full flex flex-col items-center justify-center">
<img v-if="isFailLoadQrCode" :src="icon4" width="24" height="24" class="mb-13px" /> <img v-if="isFailLoadQrCode" :src="icon4" width="24" height="24" class="mb-13px" />
<icon-loading v-else size="24" class="color-#6D4CFE mb-13px" /> <icon-loading v-else size="24" class="color-#6D4CFE mb-13px" />
@ -55,7 +55,7 @@
<p class="s1">请点击刷新</p> <p class="s1">请点击刷新</p>
</div> </div>
</div> </div>
<span class="mt-16px"> 请使用抖音扫码将公司账号绑定至灵机平台 </span> <span class="mt-16px"> 请使用{{ platform }}扫码将公司账号绑定至灵机平台 </span>
</template> </template>
<!-- </template> --> <!-- </template> -->
<template v-else-if="taskStep === TASK_STEP.loading"> <template v-else-if="taskStep === TASK_STEP.loading">
@ -106,24 +106,25 @@
import { defineExpose, ref, onUnmounted } from 'vue'; import { defineExpose, ref, onUnmounted } from 'vue';
import { getMediaAccountsAuthorizedStatus, getAuthorizedImage } from '@/api/all/propertyMarketing'; import { getMediaAccountsAuthorizedStatus, getAuthorizedImage } from '@/api/all/propertyMarketing';
import icon1 from '@/assets/img/media-account/icon-warn.png'; import icon1 from '@/assets/img/media-account/icon-default-qrcode.png';
import icon2 from '@/assets/img/media-account/icon-feedback-success.png'; import icon2 from '@/assets/img/media-account/icon-feedback-success.png';
import icon3 from '@/assets/img/media-account/icon-feedback-fail.png'; import icon3 from '@/assets/img/media-account/icon-feedback-fail.png';
import icon4 from '@/assets/img/media-account/icon-warn-1.png'; import icon4 from '@/assets/img/media-account/icon-warn-1.png';
const emits = defineEmits(['update']);
const OVERDUE_TIME = 30000; // 失效时间 const OVERDUE_TIME = 30000; // 失效时间
const TASK_STEP = { const TASK_STEP = {
default: 1, default: 1,
loading: 2, loading: 2,
end: 3, end: 3,
}; };
const visible = ref(false); const visible = ref(false);
const isOverdue = ref(false); // 二维码失效 const isOverdue = ref(false); // 二维码失效
const isSuccess = ref(false); const isSuccess = ref(false);
const failReason = ref(''); const failReason = ref('');
const progress = ref(0); const progress = ref(0);
const id = ref(''); const id = ref('');
const platform = ref('');
const isFailLoadQrCode = ref(false); const isFailLoadQrCode = ref(false);
const qrCodeUrl = ref(''); const qrCodeUrl = ref('');
@ -153,8 +154,9 @@ const confirmBtnText = computed(() => {
return ''; return '';
}); });
const open = (accountId) => { const open = (accountId, platformCode) => {
id.value = accountId; id.value = accountId;
platform.value = platformCode === 0 ? '抖音' : '小红书';
getAuthorizedQrCode(); getAuthorizedQrCode();
visible.value = true; visible.value = true;
}; };
@ -162,6 +164,7 @@ const resetTaskFields = () => {
isOverdue.value = false; isOverdue.value = false;
isSuccess.value = false; isSuccess.value = false;
failReason.value = ''; failReason.value = '';
platform.value = '';
progress.value = 0; progress.value = 0;
qrCodeUrl.value = ''; qrCodeUrl.value = '';
qrCodeLoading.value = false; qrCodeLoading.value = false;
@ -200,6 +203,7 @@ const startStatusPolling = () => {
progress.value = 1; progress.value = 1;
clearFakeProgressTimer(); clearFakeProgressTimer();
clearStatusPollingTimer(); clearStatusPollingTimer();
emits('update');
} }
} }
}, 2000); }, 2000);

View File

@ -11,20 +11,11 @@
</div> </div>
<div class="month-data-div"> <div class="month-data-div">
<div style="align-self: stretch"> <div style="align-self: stretch">
<a-space direction="vertical" v-for="(item, index) in overview"> <a-space direction="vertical">
<span :style="{ color: getColor(item?.highlight) }">{{ item.text }}</span> <span v-for="(line, index) in formattedText" :key="index" :class="getCss(line.highlight)" >
<br /> {{ line.text }}
</span>
</a-space> </a-space>
<!-- <span class="month-text-black">总消耗</span>-->
<!-- <span class="month-text-blue">52,382.16</span>-->
<!-- <span class="month-normal-span">较上周期</span><span class="month-text-red">12.6%</span>-->
<!-- <span class="month-normal-span"><br />整体ROI</span><span class="month-text-blue">2.84</span>-->
<!-- <span class="month-normal-span">属于</span><span class="month-text-red">中等偏高水平</span>-->
<!-- <span class="month-text-black">较上周期 </span><span class="month-text-red">+0.45</span>-->
<!-- <span class="month-normal-span"><br />主要转化来源</span><span class="month-text-blue">抖音 46.3%</span>-->
<!-- <span class="month-normal-span">CTR 2.91%<br />优质素材表现</span>-->
<!-- <span class="month-text-blue">美团酒店爆款横版1号</span>-->
<!-- <span class="month-normal-spanw">CTR 3.47%CVR 5.92%</span>-->
</div> </div>
</div> </div>
</div> </div>
@ -34,20 +25,48 @@
import { IconQuestionCircle } from '@arco-design/web-vue/es/icon'; import { IconQuestionCircle } from '@arco-design/web-vue/es/icon';
import { defineProps } from 'vue'; import { defineProps } from 'vue';
const colorMap = {
purple: 'purple',
orange: 'orange',
};
const props = defineProps({ const props = defineProps({
overview: { overview: {
type: Array, type: Object,
default: () => [], default: () => ({
text: '',
parts: [],
}),
}, },
}); });
const classMap = {
const getColor = (highlight?: string) => { purple: 'month-text-blue',
return highlight ? colorMap[highlight] : undefined; black: 'month-text-black',
orange: 'orange',
red: 'month-text-red',
}; };
const formattedText = computed(() => {
console.log(props.overview, 'props.overview');
const { text, parts } = props.overview;
if (!text || !parts) return [];
// 替换占位符
let resultText = text;
parts.forEach((part) => {
const key = Object.keys(part)[0];
resultText = resultText.replace(`{${key}}`, part[key]);
});
// 按分号拆分并保留颜色信息
return resultText.split('').map((line) => {
const matchedPart = parts.find((part) => line.includes(part[Object.keys(part)[0]]));
return {
text: line.trim(),
highlight: matchedPart?.highlight || 'black',
};
});
});
const getCss = (highlight?: string) => {
return highlight ? classMap[highlight] : undefined;
};
console.log(props.overview, 'overvie333'); console.log(props.overview, 'overvie333');
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -33,14 +33,7 @@
<div class="filter-row-item flex items-center"> <div class="filter-row-item flex items-center">
<span class="label">平台</span> <span class="label">平台</span>
<a-select <a-select v-model="query.platform" class="w-150" size="medium" placeholder="全部" allow-clear>
v-model="query.platform"
class="w-150"
size="medium"
placeholder="全部"
allow-clear
@change="handleSearch"
>
<a-option v-for="(item, index) in PLATFORM_LIST" :key="index" :value="item.value" :label="item.label" <a-option v-for="(item, index) in PLATFORM_LIST" :key="index" :value="item.value" :label="item.label"
>{{ item.label }} >{{ item.label }}
</a-option> </a-option>
@ -83,6 +76,10 @@ const props = defineProps({
type: Object, type: Object,
required: true, required: true,
}, },
disabled: {
type: Boolean,
default: false,
},
}); });
const emits = defineEmits('onSearch', 'onReset', 'update:query'); const emits = defineEmits('onSearch', 'onReset', 'update:query');
// 获取最近7天的日期 // 获取最近7天的日期
@ -100,6 +97,10 @@ const localQuery = computed({
get: () => props.query, get: () => props.query,
set: (value) => emits('update:query', value), set: (value) => emits('update:query', value),
}); });
const handleReset = () => {
emits('onReset');
};
onMounted(() => { onMounted(() => {
localQuery.value.data_time = getLast7Days(); localQuery.value.data_time = getLast7Days();
}); });

View File

@ -61,15 +61,15 @@
<template #weekConsumptionRate="{ record }"> <template #weekConsumptionRate="{ record }">
<a-statistic <a-statistic
:value="record.week_consumption_rate * 100" :value="record.pre_total_use_amount_chain * 100"
:value-style="{ :value-style="{
color: record.week_consumption_rate > 0 ? '#F64B31' : '#25C883', color: record.pre_total_use_amount_chain > 0 ? '#F64B31' : '#25C883',
fontStyle: 'normal', fontStyle: 'normal',
fontSize: '14px', fontSize: '14px',
}" }"
> >
<template #prefix> <template #prefix>
<icon-arrow-rise v-if="record.week_consumption_rate > 0" /> <icon-arrow-rise v-if="record.pre_total_use_amount_chain > 0" />
<icon-arrow-down v-else /> <icon-arrow-down v-else />
</template> </template>
<template #suffix>%</template> <template #suffix>%</template>
@ -91,7 +91,7 @@ const props = defineProps({
default: () => { default: () => {
data: []; data: [];
}, },
}, }
}); });
const emit = defineEmits(['updateQuery']); const emit = defineEmits(['updateQuery']);
@ -99,13 +99,7 @@ const emit = defineEmits(['updateQuery']);
const handleSorterChange = (column, order) => { const handleSorterChange = (column, order) => {
emit('updateQuery', { column, order }); emit('updateQuery', { column, order });
}; };
const listQuery = reactive({
project_id: ref(''),
platform: ref(''),
page: ref(1),
name: ref(''),
page_size: ref('10'),
});
const columns = [ const columns = [
{ {
title: '账户名称', title: '账户名称',
@ -139,7 +133,7 @@ const columns = [
{ {
title: '本周总消耗环比', title: '本周总消耗环比',
titleSlotName: 'weekConsumptionRateTitle', titleSlotName: 'weekConsumptionRateTitle',
dataIndex: 'week_consumption_rate', dataIndex: 'pre_total_use_amount_chain',
width: 120, width: 120,
minWidth: 120, minWidth: 120,
slotName: 'weekConsumptionRate', slotName: 'weekConsumptionRate',

View File

@ -15,7 +15,12 @@
</a-tabs> </a-tabs>
</div> </div>
<!--表单组件搜索--> <!--表单组件搜索-->
<listSearchForm v-model:query="query" @onSearch="onSearch"></listSearchForm> <listSearchForm
@onReset="handleReset"
v-model:query="query"
@onSearch="onSearch"
:disabled="loading"
></listSearchForm>
<component <component
:is="currentComponent" :is="currentComponent"
@ -38,18 +43,17 @@
/> />
</div> </div>
</div> </div>
<a-spin v-if="tabData === 'placement_guide'" :loading="loading" tip="AI分析中...." wrapperClassName="custom-spin-wrapper">
<div >
<div v-if="tabData === 'placement_guide'">
<a-spin :loading="loading" tip="AI分析中">
<!-- 本月摘要-->
<MonthData :overview="aiResult.overview"></MonthData> <MonthData :overview="aiResult.overview"></MonthData>
<!-- 投放建议--> <!-- 投放建议-->
<PlacementSuggestions :optimization="aiResult.optimization"></PlacementSuggestions> <PlacementSuggestions :optimization="aiResult.optimization"></PlacementSuggestions>
<!-- 投放行动指南--> <!-- 投放行动指南-->
<ActionGuideDistribution :action_guide="aiResult.action_guide"></ActionGuideDistribution> <ActionGuideDistribution :action_guide="aiResult.action_guide"></ActionGuideDistribution>
</a-spin> </div>
</div>
</a-spin>
<div v-if="tabData == 'placement_guide'"> <div v-if="tabData == 'placement_guide'">
<a-space class="down-btn"> <a-space class="down-btn">
<a-button type="outline" @click="downPage"> <a-button type="outline" @click="downPage">
@ -91,7 +95,9 @@ const tabData = ref('placement_guide');
const query = reactive({ const query = reactive({
platform: '', platform: '',
date_time: '', account_name: '',
plan: '',
date_time: [],
sort_column: '', sort_column: '',
sort_order: '', sort_order: '',
page_size: 20, page_size: 20,
@ -109,11 +115,12 @@ const onPageSizeChange = (pageSize) => {
query.page_size = pageSize; query.page_size = pageSize;
onSearch(); onSearch();
}; };
const isGetAi = ref(true); // 是否获取AI数据
const handleUpdateQuery = (payload) => { const handleUpdateQuery = (payload) => {
payload.order = payload.order === 'ascend' ? 'asc' : 'desc'; payload.order = payload.order === 'ascend' ? 'asc' : 'desc';
query.sort_column = payload.column; query.sort_column = payload.column;
query.sort_order = payload.order; query.sort_order = payload.order;
isGetAi.value = false;
onSearch(); onSearch();
}; };
@ -138,8 +145,11 @@ const onSearch = async () => {
listData.total = result.data.total; listData.total = result.data.total;
if (placementGuideList.value.length > 0) { if (placementGuideList.value.length > 0) {
loading.value = true; loading.value = true;
startTask(); if (isGetAi.value) {
startTask();
}
} }
isGetAi.value = true;
}; };
const aiResult = reactive({ const aiResult = reactive({
optimization: [], // 投放建议优化 optimization: [], // 投放建议优化
@ -148,7 +158,6 @@ const aiResult = reactive({
}); });
// 下载当前页面 // 下载当前页面
const downPage = async () => { const downPage = async () => {
await nextTick(); // 确保 DOM 更新完成 await nextTick(); // 确保 DOM 更新完成
html2canvas(document.querySelector('.guidelines-data-wrap')).then((canvas) => { html2canvas(document.querySelector('.guidelines-data-wrap')).then((canvas) => {
@ -168,10 +177,7 @@ const saveForm = reactive({
code: '', code: '',
}); });
const timerRef = ref<number | null>(null); const timerRef = ref<number | null>(null);
const startTask = () => { const startTask = () => {
//todo 暂时注释
return
if (timerRef.value !== null) return; if (timerRef.value !== null) return;
timerRef.value = setInterval(async () => { timerRef.value = setInterval(async () => {
try { try {
@ -185,7 +191,7 @@ const startTask = () => {
loading.value = false; loading.value = false;
aiResult.optimization = data.result?.optimization?.modules || []; aiResult.optimization = data.result?.optimization?.modules || [];
aiResult.action_guide = data.result?.action_guide?.modules || []; aiResult.action_guide = data.result?.action_guide?.modules || [];
aiResult.overview = data.result?.overview || []; aiResult.overview = data.result?.overview?.content_blocks[0] || [];
} }
saveForm.code = data?.code; saveForm.code = data?.code;
console.log(aiResult, 'aiResult'); console.log(aiResult, 'aiResult');
@ -195,7 +201,21 @@ const startTask = () => {
} }
}, 5000); }, 5000);
}; };
const handleReset = () => {
console.log('handleReset');
query.page = 1;
query.page_size = 20;
query.account_name = '';
query.platform = '';
query.sort_column = '';
query.sort_order = '';
query.platform = '';
query.date_time = [];
onSearch();
};
const stopTask = () => { const stopTask = () => {
loading.value = false;
if (timerRef.value !== null) { if (timerRef.value !== null) {
clearInterval(timerRef.value); // 清除定时器 clearInterval(timerRef.value); // 清除定时器
timerRef.value = null; // 重置引用 timerRef.value = null; // 重置引用
@ -222,4 +242,8 @@ onMounted(() => {
<style lang="scss"> <style lang="scss">
@import './style.scss'; @import './style.scss';
.custom-spin-wrapper {
display: block;
width: 100%;
}
</style> </style>