Merge remote-tracking branch 'origin/feature/0918_账号管理新增视图_rxd' into test

# Conflicts:
#	src/components/common-select/index.vue
#	src/router/routes/modules/propertyMarketing.ts
This commit is contained in:
rd
2025-09-24 16:37:16 +08:00
34 changed files with 998 additions and 258 deletions

View File

@ -26,7 +26,9 @@ const enterpriseStore = useEnterpriseStore();
const redTheme = { const redTheme = {
token: { token: {
colorPrimary: '#6d4cfe', // 主色 colorPrimary: '#6d4cfe', // 主色
colorError: '#f64b31', // 危险色
colorLink: '#f5222d', // 链接色 colorLink: '#f5222d', // 链接色
colorSuccess: '#25c883', // 成功色
}, },
}; };

View File

@ -402,3 +402,8 @@ export const putProject = (params = {}) => {
export const getProjectDetail = (id: string) => { export const getProjectDetail = (id: string) => {
return Http.get(`/v1/projects/${id}`); return Http.get(`/v1/projects/${id}`);
}; };
// 账号笔记-详情
export const getMediaAccountWorkDetail = (id: string) => {
return Http.get(`/v1/media-account-works/${id}`);
};

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M6.70215 8.47705C7.20637 8.52829 7.59961 8.95443 7.59961 9.47217V13.7173L7.59473 13.8198C7.54671 14.2902 7.17257 14.6636 6.70215 14.7114L6.59961 14.7173H2.35449L2.25195 14.7114C1.78167 14.6635 1.40736 14.2901 1.35938 13.8198L1.35449 13.7173V9.47217C1.35449 8.95454 1.74788 8.52843 2.25195 8.47705L2.35449 8.47217H6.59961L6.70215 8.47705ZM13.7471 10.3101C14.2513 10.3613 14.6455 10.7875 14.6455 11.3052V13.7173L14.6396 13.8188C14.5919 14.2895 14.2177 14.6636 13.7471 14.7114L13.6455 14.7173H9.42676C8.87474 14.7171 8.42689 14.2693 8.42676 13.7173V11.3052C8.42676 10.753 8.87466 10.3054 9.42676 10.3052H13.6455L13.7471 10.3101ZM2.55469 13.5171H6.40039V9.67139H2.55469V13.5171ZM9.62695 13.5171H13.4453V11.5044H9.62695V13.5171ZM13.7471 1.2876C14.2512 1.33887 14.6455 1.76503 14.6455 2.28271V8.35791L14.6396 8.46045C14.5917 8.93092 14.2176 9.3052 13.7471 9.35303L13.6455 9.35791H9.42676L9.3252 9.35303C8.85463 9.30527 8.48054 8.93097 8.43262 8.46045L8.42676 8.35791V2.28271C8.4268 1.7306 8.87469 1.28293 9.42676 1.28271H13.6455L13.7471 1.2876ZM9.62695 8.15771H13.4453V2.48291H9.62695V8.15771ZM6.70215 1.2876C7.20635 1.33883 7.59957 1.765 7.59961 2.28271V6.52783L7.59473 6.63037C7.54691 7.10097 7.17274 7.47514 6.70215 7.52295L6.59961 7.52783H2.35449L2.25195 7.52295C1.78151 7.475 1.40718 7.10086 1.35938 6.63037L1.35449 6.52783V2.28271C1.35453 1.7651 1.74788 1.33895 2.25195 1.2876L2.35449 1.28271H6.59961L6.70215 1.2876ZM2.55469 6.32861H6.40039V2.48291H2.55469V6.32861Z"
fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M4.19526 1.86198C4.45561 1.60163 4.87762 1.60163 5.13797 1.86198C5.39832 2.12233 5.39832 2.54434 5.13797 2.80469L3.13797 4.80469C2.87762 5.06504 2.45561 5.06504 2.19526 4.80469L1.19526 3.80469C0.934913 3.54434 0.934913 3.12233 1.19526 2.86198C1.43934 2.6179 1.82534 2.60284 2.08719 2.81641L2.13797 2.86198L2.66662 3.39063L4.19526 1.86198Z"
fill="currentColor"/>
<path
d="M4.19526 6.52865C4.45561 6.2683 4.87762 6.2683 5.13797 6.52865C5.39832 6.789 5.39832 7.21101 5.13797 7.47135L3.13797 9.47135C2.87762 9.7317 2.45561 9.7317 2.19526 9.47135L1.19526 8.47135C0.934913 8.21101 0.934913 7.789 1.19526 7.52865C1.43934 7.28457 1.82534 7.26951 2.08719 7.48307L2.13797 7.52865L2.66662 8.05729L4.19526 6.52865Z"
fill="currentColor"/>
<path
d="M4.19526 11.1953C4.45561 10.935 4.87762 10.935 5.13797 11.1953C5.39832 11.4557 5.39832 11.8777 5.13797 12.138L3.13797 14.138C2.87762 14.3984 2.45561 14.3984 2.19526 14.138L1.19526 13.138C0.934913 12.8777 0.934913 12.4557 1.19526 12.1953C1.43934 11.9512 1.82534 11.9362 2.08719 12.1497L2.13797 12.1953L2.66662 12.724L4.19526 11.1953Z"
fill="currentColor"/>
<path
d="M14.3333 7.33333C14.7015 7.33333 14.9999 7.63181 14.9999 8C14.9999 8.36819 14.7015 8.66667 14.3333 8.66667H6.99995C6.63176 8.66667 6.33328 8.36819 6.33328 8C6.33328 7.63181 6.63176 7.33333 6.99995 7.33333H14.3333Z"
fill="currentColor"/>
<path
d="M14.3333 12C14.7015 12 14.9999 12.2985 14.9999 12.6667C14.9999 13.0349 14.7015 13.3333 14.3333 13.3333H6.99995C6.63176 13.3333 6.33328 13.0349 6.33328 12.6667C6.33328 12.2985 6.63176 12 6.99995 12H14.3333Z"
fill="currentColor"/>
<path
d="M14.3333 2.66667C14.7015 2.66667 14.9999 2.96514 14.9999 3.33333C14.9999 3.70152 14.7015 4 14.3333 4H6.99995C6.63176 4 6.33328 3.70152 6.33328 3.33333C6.33328 2.96514 6.63176 2.66667 6.99995 2.66667H14.3333Z"
fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -13,6 +13,7 @@
showArrow showArrow
:maxTagCount="maxTagCount" :maxTagCount="maxTagCount"
@change="handleChange" @change="handleChange"
@dropdownVisibleChange="onDropdownVisibleChange"
> >
<Option v-for="(item, index) in options" :key="index" :value="item.id" :label="item.name"> <Option v-for="(item, index) in options" :key="index" :value="item.id" :label="item.name">
<div class="flex items-center"> <div class="flex items-center">
@ -56,10 +57,10 @@ const props = defineProps({
allowSearch: { allowSearch: {
type: Boolean, type: Boolean,
default: false, default: false,
} },
}); });
const emits = defineEmits(['update:modelValue', 'change']); const emits = defineEmits(['update:modelValue', 'change', 'dropdownVisibleChange']);
const selectedValues = ref(props.multiple ? [] : ''); const selectedValues = ref(props.multiple ? [] : '');
@ -79,4 +80,7 @@ const handleChange = (value) => {
selectedValues.value = value; selectedValues.value = value;
emits('change', value); emits('change', value);
}; };
const onDropdownVisibleChange = (visible) => {
emits('dropdownVisibleChange', visible);
};
</script> </script>

View File

@ -99,7 +99,7 @@ export default defineComponent({
watch([updateCount, scrollReachEnd, listRef], () => { watch([updateCount, scrollReachEnd, listRef], () => {
if (props.autoScroll && unref(listRef) && unref(scrollReachEnd)) { if (props.autoScroll && unref(listRef) && unref(scrollReachEnd)) {
nextTick(() => { nextTick(() => {
console.log('自然滚动') console.log('自然滚动');
unref(listRef)!.scrollTo({ top: unref(listRef)!.scrollHeight }); unref(listRef)!.scrollTo({ top: unref(listRef)!.scrollHeight });
}); });
} }
@ -111,7 +111,7 @@ export default defineComponent({
if (!props.autoScroll) return; if (!props.autoScroll) return;
// 首次渲染:当有内容时滚到底部一次 // 首次渲染:当有内容时滚到底部一次
if (!didInitialAutoScroll.value && newLen > 0) { if (!didInitialAutoScroll.value && newLen > 0) {
console.log('首次渲染滚动到底部-----') console.log('首次渲染滚动到底部-----');
scrollToBottom('auto'); scrollToBottom('auto');
didInitialAutoScroll.value = true; didInitialAutoScroll.value = true;
return; return;
@ -142,7 +142,7 @@ export default defineComponent({
el.scrollTo({ top: el.scrollHeight, behavior }); el.scrollTo({ top: el.scrollHeight, behavior });
setScrollReachEnd(true); setScrollReachEnd(true);
} }
}) });
}); });
}; };
// 对外暴露能力 // 对外暴露能力

View File

@ -21,7 +21,7 @@
<!-- 头像设置 --> <!-- 头像设置 -->
<Dropdown trigger="click" overlayClassName="layout-avatar-dropdown"> <Dropdown trigger="click" overlayClassName="layout-avatar-dropdown">
<div <div
class="w-126px h-32px flex items-center justify-between px-4px pl-4px pr-8px rounded-30px bg-white bg-opacity-60 group" class="max-w-126px h-32px flex items-center justify-between px-4px pl-4px pr-8px rounded-30px bg-white bg-opacity-60 group"
> >
<div class="flex mr-4px overflow-hidden h-24px lh-24px items-center"> <div class="flex mr-4px overflow-hidden h-24px lh-24px items-center">
<div class="mr-4px"> <div class="mr-4px">

View File

@ -12,7 +12,7 @@ import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPa
import { downloadByUrl } from '@/utils/tools'; import { downloadByUrl } from '@/utils/tools';
import DeleteTaskModal from './delete-task-modal.vue'; import DeleteTaskModal from './delete-task-modal.vue';
import icon1 from '@/assets/img/media-account/icon-delete.png'; import icon1 from '@/assets/img/media-account/icon-delete.png';
import { showExportNotification, showFailExportNotification } from '@/utils/arcoD'; import { showExportNotification, showFailExportNotification } from '@/utils/notification';
export default { export default {
setup(props, { emit, expose }) { setup(props, { emit, expose }) {

View File

@ -11,7 +11,7 @@ import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPa
import { downloadByUrl } from '@/utils/tools'; import { downloadByUrl } from '@/utils/tools';
import DeleteTaskModal from './delete-task-modal.vue'; import DeleteTaskModal from './delete-task-modal.vue';
import icon1 from '@/assets/img/media-account/icon-delete.png'; import icon1 from '@/assets/img/media-account/icon-delete.png';
// import { showExportNotification } from '@/utils/arcoD'; // import { showExportNotification } from '@/utils/notification';
export default { export default {
setup(props, { emit, expose }) { setup(props, { emit, expose }) {

View File

@ -100,6 +100,19 @@ const COMPONENTS: AppRouteRecordRaw[] = [
}, },
component: () => import('@/views/property-marketing/media-account/account-detail/index.vue'), component: () => import('@/views/property-marketing/media-account/account-detail/index.vue'),
}, },
{
path: 'note-detail/:id',
name: 'MediaAccountNoteDetails',
meta: {
locale: '作品详情',
requiresAuth: true,
requireLogin: true,
roles: ['*'],
hideInMenu: true,
activeMenu: 'MediaAccountAccountDashboard',
},
component: () => import('@/views/property-marketing/media-account/node-detail/index.vue'),
},
], ],
}, },
// { // {

View File

@ -50,6 +50,12 @@
padding: 11px 16px; padding: 11px 16px;
@include table-cell-text; @include table-cell-text;
} }
&-selected {
> td {
background-color: #fff;
}
}
} }
} }
} }

View File

@ -1 +1 @@
@import "./ellipsis.scss" @import './ellipsis.scss';

View File

@ -71,6 +71,7 @@ export default {
}; };
const onSubmit = async (action) => { const onSubmit = async (action) => {
try {
uploadLoading.value = true; uploadLoading.value = true;
const filteredWorks = map(works.value, (work) => omit(work, 'videoInfo')); const filteredWorks = map(works.value, (work) => omit(work, 'videoInfo'));
const { code, data } = await postWorksBatch({ works: filteredWorks }); const { code, data } = await postWorksBatch({ works: filteredWorks });
@ -88,6 +89,9 @@ export default {
} }
} }
} }
} finally {
uploadLoading.value = false;
}
}; };
// 本地存储同步 // 本地存储同步

View File

@ -64,7 +64,7 @@ import { getAccountBoardOverview, getAccountBoardList, postAccountBoardExport }
import { formatNumberShow } from '@/utils/tools'; import { formatNumberShow } from '@/utils/tools';
import { INITIAL_QUERY, CARD_FIELDS } from './constants'; import { INITIAL_QUERY, CARD_FIELDS } from './constants';
// import { downloadByUrl } from '@/utils/tools'; // import { downloadByUrl } from '@/utils/tools';
import { showExportNotification } from '@/utils/arcoD'; import { showExportNotification } from '@/utils/notification';
import icon1 from '@/assets/img/icon-question.png'; import icon1 from '@/assets/img/icon-question.png';

View File

@ -6,15 +6,15 @@
<div class="note-table-wrap bg-#fff rounded-8px px-24px flex-1 flex flex-col"> <div class="note-table-wrap bg-#fff rounded-8px px-24px flex-1 flex flex-col">
<div class="title-row"> <div class="title-row">
<div class="flex items-center"> <div class="flex items-center">
<span class="cts !text-18px !lh-26px mr-4px title">笔记详情</span> <span class="cts !text-18px !lh-26px mr-4px title">作品列表</span>
<Tooltip title="展示笔记层级的详细数据,如曝光、互动等,是内容精细分析入口。"> <Tooltip title="展示笔记层级的详细数据,如曝光、互动等,是内容精细分析入口。">
<icon-question-circle size="16" class="color-#737478" /> <icon-question-circle class="color-#737478" size="14" />
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
<div class="filter-row flex my-16px"> <div class="filter-row flex my-16px">
<div class="filter-row-item flex items-center"> <div class="filter-row-item flex items-center">
<span class="label">笔记标题</span> <span class="label">作品标题</span>
<Input v-model:value="query.title" class="!w-240px" placeholder="请搜索..." allowClear @change="handleSearch"> <Input v-model:value="query.title" class="!w-240px" placeholder="请搜索..." allowClear @change="handleSearch">
<template #prefix> <template #prefix>
<icon-search /> <icon-search />
@ -68,18 +68,20 @@
<template #title> <template #title>
<span class="cts mr-4px">{{ column.title }}</span> <span class="cts mr-4px">{{ column.title }}</span>
<Tooltip v-if="column.tooltip" :title="column.tooltip" placement="top"> <Tooltip v-if="column.tooltip" :title="column.tooltip" placement="top">
<icon-question-circle class="tooltip-icon color-#737478" size="16" /> <icon-question-circle class="tooltip-icon color-#737478" size="14" />
</Tooltip> </Tooltip>
</template> </template>
<template #customRender="{ record }"> <template #customRender="{ record }">
<template v-if="column.dataIndex === 'published_at'"> <template v-if="column.dataIndex === 'published_at'">
{{ exactFormatTime(record.published_at) }} {{ exactFormatTime(record.published_at) }}
</template> </template>
<template v-else-if="column.dataIndex === 'exposure_number'">
{{ formatNumberShow({ value: record.view_number * 10, showExactValue: true }) }}
</template>
<template v-else-if="column.dataIndex === 'title'"> <template v-else-if="column.dataIndex === 'title'">
<TextoverTips :context="record.title" /> <Tooltip placement="top" title="查看作品详情">
<p class="cursor-pointer hover:color-#6D4CFE title" @click="() => goNoteDetail(record)">
{{ record.title }}
</p>
</Tooltip>
<!-- <TextoverTips :context="record.title" />-->
</template> </template>
<template v-else> <template v-else>
{{ formatTableField(column, record, true) }} {{ formatTableField(column, record, true) }}
@ -108,12 +110,13 @@
<script setup> <script setup>
import { Button, Input, Tooltip, Table, Pagination, DatePicker } from 'ant-design-vue'; import { Button, Input, Tooltip, Table, Pagination, DatePicker } from 'ant-design-vue';
import { TABLE_COLUMNS, INITIAL_QUERY, INITIAL_PAGE_INFO } from './constants'; import { TABLE_COLUMNS, INITIAL_QUERY, INITIAL_PAGE_INFO } from './constants';
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { formatTableField, exactFormatTime, formatNumberShow } from '@/utils/tools'; import { formatTableField, exactFormatTime, formatNumberShow } from '@/utils/tools';
import { getMediaAccountBoardWorks } from '@/api/all/propertyMarketing'; import { getMediaAccountBoardWorks } from '@/api/all/propertyMarketing';
import TextoverTips from '@/components/text-over-tips/index.vue'; // import TextoverTips from '@/components/text-over-tips/index.vue';
const route = useRoute(); const route = useRoute();
const router = useRouter();
const id = route.params.id; const id = route.params.id;
const dataSource = ref([]); const dataSource = ref([]);
const published_at = ref([]); const published_at = ref([]);
@ -171,6 +174,12 @@ const getData = async () => {
} }
}; };
const goNoteDetail = (record) => {
router.push({
name: 'MediaAccountNoteDetails',
});
};
onMounted(() => { onMounted(() => {
getData(); getData();
}); });

View File

@ -19,4 +19,8 @@
} }
} }
} }
.title {
@include ellipsis;
}
} }

View File

@ -34,9 +34,10 @@ export const getAccountInfoFields = (dateType: string, showMore: boolean) => {
{ title: '账号ID', dataIndex: 'account_id', notDifferentiateDateType: true }, { title: '账号ID', dataIndex: 'account_id', notDifferentiateDateType: true },
{ title: '手机号码', dataIndex: 'mobile', notDifferentiateDateType: true }, { title: '手机号码', dataIndex: 'mobile', notDifferentiateDateType: true },
{ title: '运营人员', dataIndex: 'operator.name', notDifferentiateDateType: true }, { title: '运营人员', dataIndex: 'operator.name', notDifferentiateDateType: true },
{ title: '所属项目', dataIndex: 'group.name', notDifferentiateDateType: true }, // { title: '所属项目', dataIndex: 'group.name', notDifferentiateDateType: true },
{ title: '分组', dataIndex: 'group.name', notDifferentiateDateType: true }, { title: '分组', dataIndex: 'group.name', notDifferentiateDateType: true },
{ title: '标签', dataIndex: 'tags', notDifferentiateDateType: true }, { title: '标签', dataIndex: 'tags', notDifferentiateDateType: true },
{ title: 'AI评价', dataIndex: 'ai_evaluation', notDifferentiateDateType: true },
{ title: '粉丝量', dataIndex: 'fans_number', tooltip: '账号的当前粉丝总数。', notDifferentiateDateType: true }, { title: '粉丝量', dataIndex: 'fans_number', tooltip: '账号的当前粉丝总数。', notDifferentiateDateType: true },
{ {
title: '总赞藏数', title: '总赞藏数',
@ -47,7 +48,6 @@ export const getAccountInfoFields = (dateType: string, showMore: boolean) => {
]; ];
const customFieldsWithAiEvaluation = [ const customFieldsWithAiEvaluation = [
CUSTOM_FIELDS[0], CUSTOM_FIELDS[0],
{ title: 'AI评价', dataIndex: 'ai_evaluation', notDifferentiateDateType: true },
...CUSTOM_FIELDS.slice(1), ...CUSTOM_FIELDS.slice(1),
]; ];
const allFields = showMore ? [...baseFields, ...customFieldsWithAiEvaluation] : baseFields.slice(0, 8); const allFields = showMore ? [...baseFields, ...customFieldsWithAiEvaluation] : baseFields.slice(0, 8);

View File

@ -6,7 +6,7 @@
<div class="account-detail-wrap"> <div class="account-detail-wrap">
<div class="flex items-center mb-16px cursor-pointer" @click="handleBack"> <div class="flex items-center mb-16px cursor-pointer" @click="handleBack">
<icon-left size="16" /> <icon-left size="16" />
<spa class="cts title ml-8px">账号详情</spa> <span class="cts title ml-8px">账号列表</span>
</div> </div>
<AccountInfo /> <AccountInfo />
<NoteTable /> <NoteTable />

View File

@ -3,7 +3,8 @@
* @Date: 2025-06-25 15:31:15 * @Date: 2025-06-25 15:31:15
--> -->
<template> <template>
<div class="card-container"> <NoData v-if="!dataSource.length" />
<div v-else class="card-container">
<Spin <Spin
v-for="(item, index) in dataSource" v-for="(item, index) in dataSource"
:key="index" :key="index"
@ -14,7 +15,12 @@
<template #icon> <template #icon>
<icon-sync size="24" /> <icon-sync size="24" />
</template> </template>
<Checkbox :checked="isSelected(item)" :value="item.id" @change="toggleSelect(item)" class="relative top--2px"></Checkbox> <Checkbox
:checked="isSelected(item)"
:value="item.id"
class="relative top--2px"
@change="toggleSelect(item)"
></Checkbox>
<div class="ml-8px flex-1"> <div class="ml-8px flex-1">
<Tooltip title="点击查看账号详情"> <Tooltip title="点击查看账号详情">
<p class="name cursor-pointer hover:!color-#6d4cfe" @click="goDetail(item)">{{ item.name || '-' }}</p> <p class="name cursor-pointer hover:!color-#6d4cfe" @click="goDetail(item)">{{ item.name || '-' }}</p>
@ -67,30 +73,30 @@
<span class="label">运营人员</span> <span class="label">运营人员</span>
<span class="cts">{{ item.operator?.name || '-' }}</span> <span class="cts">{{ item.operator?.name || '-' }}</span>
</div> </div>
<div class="field-row"> <!-- <div class="field-row">-->
<span class="label">所属项目</span> <!-- <span class="label">所属项目</span>-->
<span v-if="!item.projects.length" class="cts">-</span> <!-- <span v-if="!item.projects.length" class="cts">-</span>-->
<div v-else class="flex items-center"> <!-- <div v-else class="flex items-center">-->
<Tooltip <!-- <Tooltip-->
v-if="item.projects.length > 2" <!-- v-if="item.projects.length > 2"-->
placement="bottom" <!-- placement="bottom"-->
:title=" <!-- :title="-->
item.projects <!-- item.projects-->
.slice(2) <!-- .slice(2)-->
.map((v) => v.name) <!-- .map((v) => v.name)-->
.join(',') <!-- .join(',')-->
" <!-- "-->
> <!-- >-->
<div class="tag-box"> <!-- <div class="tag-box">-->
<span class="text">{{ `+${item.projects.length - 2}` }}</span> <!-- <span class="text">{{ `+${item.projects.length - 2}` }}</span>-->
</div> <!-- </div>-->
</Tooltip> <!-- </Tooltip>-->
<div v-for="(project, index) in item.projects.slice(0, 2)" :key="index" class="tag-box"> <!-- <div v-for="(project, index) in item.projects.slice(0, 2)" :key="index" class="tag-box">-->
<span class="text">{{ project.name }}</span> <!-- <span class="text">{{ project.name }}</span>-->
</div> <!-- </div>-->
</div> <!-- </div>-->
</div> <!-- </div>-->
<div class="field-row"> <div class="field-row">
<span class="label">分组</span> <span class="label">分组</span>
<span class="cts">{{ item.group?.name || '-' }}</span> <span class="cts">{{ item.group?.name || '-' }}</span>
@ -143,14 +149,11 @@
</div> </div>
</div> </div>
</Spin> </Spin>
<PauseAccountPatchModal ref="pauseAccountPatchModalRef" @success="emits('update')" />
<ReauthorizeAccountModal ref="reauthorizeAccountModalRef" @update="emits('update')" />
<AuthorizedAccountModal ref="authorizedAccountModalRef" @update="emits('update')" />
</div> </div>
</template> </template>
<script setup> <script setup>
import { defineProps, ref, computed, inject } from 'vue'; import { defineProps, inject } from 'vue';
import { Checkbox, Button, Tooltip, Spin } from 'ant-design-vue'; import { Checkbox, Button, Tooltip, Spin } from 'ant-design-vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { deleteSyncStatus } from '@/api/all/propertyMarketing'; import { deleteSyncStatus } from '@/api/all/propertyMarketing';
@ -161,10 +164,7 @@ import {
EnumStatus, EnumStatus,
} from '@/views/property-marketing/media-account/components/status-select/status-box'; } from '@/views/property-marketing/media-account/components/status-select/status-box';
import PauseAccountPatchModal from './pause-account-patch';
import StatusBox from '@/views/property-marketing/media-account/components/status-select/status-box.tsx'; import StatusBox from '@/views/property-marketing/media-account/components/status-select/status-box.tsx';
import ReauthorizeAccountModal from '../reauthorize-account-modal';
import AuthorizedAccountModal from '../authorized-account-modal';
import FooterBtn from './footer-btn'; import FooterBtn from './footer-btn';
import icon1 from '@/assets/img/platform/icon-dy.png'; import icon1 from '@/assets/img/platform/icon-dy.png';
@ -184,7 +184,7 @@ const props = defineProps({
type: Array, type: Array,
default: () => [], default: () => [],
}, },
selectedItems: { selectedRows: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
@ -194,28 +194,26 @@ const props = defineProps({
}, },
}); });
const emits = defineEmits(['openEdit', 'update', 'selectionChange', 'delete', 'updateSyncStatus']); const emits = defineEmits([
'openEdit',
'update',
'delete',
'updateSyncStatus',
'pause',
'reauthorize',
'select',
'selectAll',
]);
const syncData = inject('handleSyncData'); const syncData = inject('handleSyncData');
const router = useRouter(); const router = useRouter();
const pauseAccountPatchModalRef = ref(null);
const reauthorizeAccountModalRef = ref(null);
const authorizedAccountModalRef = ref(null);
// item
const isSelected = (item) => { const isSelected = (item) => {
return props.selectedItems.some((i) => i.id === item.id); return props.selectedRows.some((i) => i.id === item.id);
}; };
const toggleSelect = (item) => { const toggleSelect = (item) => {
let newSelected; emits('select', item, !isSelected(item));
if (isSelected(item)) {
newSelected = props.selectedItems.filter((i) => i.id !== item.id);
} else {
newSelected = [...props.selectedItems, item];
}
emits('selectionChange', newSelected);
}; };
const openEdit = (item) => { const openEdit = (item) => {
@ -227,21 +225,11 @@ const openDelete = (item) => {
}; };
const handleReauthorize = (item) => { const handleReauthorize = (item) => {
const { id, platform, error_status } = item; emits('reauthorize', item);
const isUnauthorized = isUnauthorizedStatus(error_status);
if (isUnauthorized) {
authorizedAccountModalRef.value?.open(id, platform);
} else {
reauthorizeAccountModalRef.value?.open(id, platform);
}
}; };
const handlePause = (item) => { const handlePause = (item) => {
pauseAccountPatchModalRef.value?.open(item); emits('pause', item);
};
const isUnauthorizedStatus = (error_status) => {
return [EnumErrorStatus.UNAUTHORIZED].includes(error_status);
}; };
const goDetail = (item) => { const goDetail = (item) => {
@ -293,6 +281,7 @@ const handleConfirm = (item) => {
syncData(item); syncData(item);
} }
if (error_status === EnumErrorStatus.LOGIN) { if (error_status === EnumErrorStatus.LOGIN) {
emits('');
handleReauthorize(item); handleReauthorize(item);
} }
}; };

View File

@ -3,9 +3,8 @@ import {
EnumErrorStatus, EnumErrorStatus,
getStatusInfo, getStatusInfo,
} from '@/views/property-marketing/media-account/components/status-select/status-box'; } from '@/views/property-marketing/media-account/components/status-select/status-box';
import { Dropdown, Menu } from 'ant-design-vue'; import { Dropdown, Menu, Tooltip, Button } from 'ant-design-vue';
const { Item: MenuItem } = Menu; const { Item: MenuItem } = Menu;
import { Tooltip, Button } from 'ant-design-vue';
export default defineComponent({ export default defineComponent({
name: 'FooterBtn', name: 'FooterBtn',
props: { props: {
@ -44,7 +43,7 @@ export default defineComponent({
}; };
const renderUpdateBtn = () => { const renderUpdateBtn = () => {
return ( return (
<Button type="primary" ghost size="small" onClick={() => emit('syncData', props.item)}> <Button type="primary" class="!h-24px !px-12px" ghost size="small" onClick={() => emit('syncData', props.item)}>
</Button> </Button>
); );
@ -63,7 +62,7 @@ export default defineComponent({
trigger="hover" trigger="hover"
v-slots={{ v-slots={{
default: () => ( default: () => (
<Button type="primary" ghost class="mr-8px" size="small"> <Button type="primary" ghost class="!h-24px !px-12px mr-8px" size="small">
</Button> </Button>
), ),
@ -88,7 +87,7 @@ export default defineComponent({
trigger="hover" trigger="hover"
v-slots={{ v-slots={{
default: () => ( default: () => (
<Button type="primary" ghost class="mr-8px" size="small"> <Button type="primary" ghost class="mr-8px !h-24px !px-12px" size="small">
</Button> </Button>
), ),
@ -100,7 +99,13 @@ export default defineComponent({
), ),
}} }}
></Dropdown> ></Dropdown>
<Button type="primary" ghost size="small" onClick={() => emit('handleReauthorize', props.item)}> <Button
type="primary"
ghost
size="small"
class="!h-24px !px-12px"
onClick={() => emit('handleReauthorize', props.item)}
>
</Button> </Button>
</> </>
@ -118,14 +123,20 @@ export default defineComponent({
} else if ([EnumErrorStatus.REQUEST, EnumErrorStatus.FREEZE].includes(error_status)) { } else if ([EnumErrorStatus.REQUEST, EnumErrorStatus.FREEZE].includes(error_status)) {
return ( return (
<Tooltip title={statusInfo.value.disabledBtnTooltip}> <Tooltip title={statusInfo.value.disabledBtnTooltip}>
<Button type="primary" ghost size="small" disabled> <Button type="primary" ghost size="small" disabled class="!h-24px !px-12px">
</Button> </Button>
</Tooltip> </Tooltip>
); );
} else { } else {
return ( return (
<Button type="primary" ghost size="small" onClick={() => emit('handleReauthorize', props.item)}> <Button
type="primary"
ghost
class="!h-24px !px-12px"
size="small"
onClick={() => emit('handleReauthorize', props.item)}
>
{isUnauthorized ? '去授权' : '重新授权'} {isUnauthorized ? '去授权' : '重新授权'}
</Button> </Button>
); );
@ -137,7 +148,7 @@ export default defineComponent({
trigger="hover" trigger="hover"
v-slots={{ v-slots={{
default: () => ( default: () => (
<Button type="primary" ghost class="mr-8px" size="small"> <Button type="primary" ghost size="small" class="mr-8px !h-24px !px-12px">
</Button> </Button>
), ),

View File

@ -10,6 +10,15 @@
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
} }
:deep(.status-box) {
.label {
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
}
.card-item { .card-item {
border-radius: 8px; border-radius: 8px;
border: 1px solid transparent; border: 1px solid transparent;

View File

@ -0,0 +1,293 @@
<template>
<Table
ref="tableRef"
:dataSource="dataSource"
:pagination="false"
:rowSelection="rowSelection"
:scroll="{ x: '100%' }"
:showSorterTooltip="false"
bordered
class="flex-1 w-100%"
rowKey="id"
:rowClassName="(record) => (isSyncing(record) || isSyncFailed(record) ? 'sync' : '')"
>
<template #emptyText>
<NoData />
</template>
<Column
v-for="column in tableColumns"
:key="column.dataIndex"
:align="column.align"
:dataIndex="column.dataIndex"
:ellipsis="true"
:fixed="column.fixed"
:minWidth="column.minWidth"
:sorter="column.sortable"
:width="column.width"
:className="column.class"
>
<template #title>
<span class="cts mr-4px">{{ column.title }}</span>
<Tooltip v-if="column.tooltip" :title="column.tooltip" placement="top">
<icon-question-circle class="tooltip-icon color-#737478" size="16" />
</Tooltip>
</template>
<template v-if="column.dataIndex === 'name'" #customRender="{ record }">
<TextOverTips
:context="record.name || '-'"
:line="1"
class="name cursor-pointer hover:!color-#6d4cfe"
@click="goDetail(record)"
/>
</template>
<template v-else-if="column.dataIndex === 'tags'" #customRender="{ record }">
<div v-if="record.tags.length > 0" class="flex flex-wrap gap-4px">
<Tag v-for="tag in record.tags" :key="tag.id" class="mr-0 rounded-4px bg-#F2F3F5 px-8px">
<Tooltip v-if="tag.name.length > 5" :title="tag.name">
<span class="cts !color-#55585F !lh-24px !text-14px"> {{ `${tag.name.slice(0, 5)}...` }} </span>
</Tooltip>
<span v-else class="cts !color-#55585F !lh-24px !text-14px"> {{ tag.name }} </span>
</Tag>
</div>
<template v-else> -</template>
</template>
<template v-else-if="column.dataIndex === 'status'" #customRender="{ record }">
<StatusBox :item="record" />
</template>
<template v-else-if="column.dataIndex === 'platform'" #customRender="{ record }">
<img :src="record.platform === 0 ? icon2 : icon3" alt="" height="16" width="16" />
</template>
<template v-else-if="column.dataIndex === 'last_synced_at'" #customRender="{ record }">
<span class="cts num">{{ getLastSyncedAt(record) }}</span>
</template>
<template v-else-if="column.dataIndex === 'last_authorized_at'" #customRender="{ record }">
<span class="cts num">{{ formatTime(record.last_authorized_at) }}</span>
</template>
<template v-else-if="column.dataIndex === 'sync'" #customRender="{ record }">
<div v-if="isSyncing(record)" class="sync-col">
<Spin tip="更新数据中..." />
</div>
<div v-else-if="isSyncFailed(record)" class="flex items-center">
<div class="flex items-center box mr-16px">
<img :src="icon4" alt="" class="mr-8px" height="16" width="16" />
<span class="name !mb-0">{{ getErrorStatusText(record) }}</span>
</div>
<div class="flex items-center">
<Button class="mr-8px !h-24px !px-12px" ghost size="small" type="primary" @click="handleCancel(record)">
取消
</Button>
<Button v-if="showConfirmBtn(record)" ghost size="small" type="primary" @click="handleConfirm(record)"
>{{ getConfirmBtnText(record) }}
</Button>
</div>
</div>
</template>
<template v-else-if="column.dataIndex === 'operation'" #customRender="{ record }">
<div class="flex items-center">
<FooterBtn
:item="record"
@handlePause="handlePause"
@handleReauthorize="handleReauthorize"
@openDelete="openDelete"
@openEdit="openEdit"
@syncData="syncData"
/>
</div>
</template>
<template v-else #customRender="{ record }">
{{ formatTableField(column, record, true) }}
</template>
</Column>
</Table>
</template>
<script setup>
import { ref, computed, inject } from 'vue';
import { Tooltip, Table, Tag, Spin, Button } from 'ant-design-vue';
import StatusBox, {
EnumErrorStatus,
EnumStatus,
errorStatusMap,
} from '@/views/property-marketing/media-account/components/status-select/status-box.tsx';
const { Column } = Table;
import { formatTableField, exactFormatTime } from '@/utils/tools';
import { deleteSyncStatus } from '@/api/all/propertyMarketing';
import TextOverTips from '@/components/text-over-tips';
import FooterBtn from './footer-btn';
// import icon1 from '@/assets/img/media-account/icon-delete.png';
import icon2 from '@/assets/img/platform/icon-dy.png';
import icon3 from '@/assets/img/platform/icon-xhs.png';
import icon4 from '@/assets/img/media-account/icon-warn.png';
const emits = defineEmits([
'openEdit',
'update',
'selectionChange',
'delete',
'updateSyncStatus',
'pause',
'reauthorize',
'select',
'selectAll',
]);
const syncData = inject('handleSyncData');
const router = useRouter();
const props = defineProps({
dataSource: {
type: Array,
default: () => [],
},
tableColumns: {
type: Array,
default: () => [],
},
selectedRowKeys: {
type: Array,
default: () => [],
},
syncMediaAccounts: {
type: Array,
default: () => [],
},
isLoadingTaskStatus: {
type: Boolean,
default: () => false,
},
});
const tableRef = ref(null);
const goDetail = (item) => {
router.push(`/media-account/detail/${item.id}`);
};
const handlePause = (item) => {
emits('pause', item);
};
const handleReauthorize = (item) => {
emits('reauthorize', item);
};
const openEdit = (item) => {
emits('openEdit', item);
};
const openDelete = (item) => {
emits('delete', item);
};
const formatTime = (time) => {
return exactFormatTime(time, 'YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD HH:mm:ss');
};
const getSyncMediaAccount = (item) => {
return props.syncMediaAccounts.find((v) => v.id === item.id);
};
const getLastSyncedAt = (item) => {
const target = getSyncMediaAccount(item);
if (props.isLoadingTaskStatus && target) {
if (target?.status !== 0) {
return formatTime(target.last_synced_at);
}
}
return formatTime(item.last_synced_at);
};
const rowSelection = {
selectedRowKeys: computed(() => props.selectedRowKeys),
onSelect: (record, selected) => {
emits('select', record, selected);
},
onSelectAll: (selected) => {
emits('selectAll', selected);
},
};
const isSyncing = (item) => {
if (!props.syncMediaAccounts.length) return false;
return getSyncMediaAccount(item)?.status === 0;
};
const isSyncFailed = (item) => {
return getSyncMediaAccount(item)?.status === 2;
};
const getErrorStatusText = (item) => {
const error_status = getSyncMediaAccount(item)?.error_status;
return `异常(${errorStatusMap.get(error_status)?.text ?? ''}`;
};
const handleCancel = async (item) => {
const error_status = getSyncMediaAccount(item)?.error_status;
await deleteSyncStatus(item.id);
item.status = EnumStatus.ABNORMAL;
item.error_status = error_status;
emits('updateSyncStatus', item);
};
const handleConfirm = (item) => {
const error_status = getSyncMediaAccount(item)?.error_status;
if (error_status === EnumErrorStatus.MISSING) {
syncData(item);
}
if (error_status === EnumErrorStatus.LOGIN) {
emits('');
handleReauthorize(item);
}
};
const showConfirmBtn = (item) => {
const error_status = getSyncMediaAccount(item)?.error_status;
return [EnumErrorStatus.MISSING, EnumErrorStatus.LOGIN].includes(error_status);
};
const getConfirmBtnText = (item) => {
const error_status = getSyncMediaAccount(item)?.error_status;
if (error_status === EnumErrorStatus.MISSING) {
return '重新更新';
}
if (error_status === EnumErrorStatus.LOGIN) {
return '重新授权';
}
};
</script>
<style lang="scss" scoped>
:deep(.ant-table) {
.ant-table-tbody {
.ant-table-row {
&.sync {
position: relative;
border-bottom: none !important;
.sync-row {
background-color: #fff;
opacity: 0.9;
content: '';
position: absolute;
left: 0;
top: 0;
z-index: 22;
width: 100%;
height: 100%;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
.sync-col {
.ant-spin {
display: flex;
width: fit-content;
.ant-spin-dot {
margin-right: 8px;
}
}
}
}
}
}
}
}
</style>

View File

@ -16,6 +16,7 @@ import {
Tooltip, Tooltip,
Upload, Upload,
Switch, Switch,
Select,
} from 'ant-design-vue'; } from 'ant-design-vue';
const { TextArea } = Input; const { TextArea } = Input;
import AuthorizedAccountModal from '../authorized-account-modal'; import AuthorizedAccountModal from '../authorized-account-modal';
@ -25,7 +26,7 @@ import SyncDataModal from '../sync-data-modal';
import CommonSelect from '@/components/common-select'; import CommonSelect from '@/components/common-select';
// import { downloadByUrl } from '@/utils/tools'; // import { downloadByUrl } from '@/utils/tools';
import { showExportNotification } from '@/utils/arcoD'; import { showExportNotification } from '@/utils/notification';
import { genRandomId } from '@/utils/tools'; import { genRandomId } from '@/utils/tools';
import { import {
fetchAccountTags, fetchAccountTags,
@ -36,6 +37,7 @@ import {
getTemplateUrl, getTemplateUrl,
batchMediaAccounts, batchMediaAccounts,
getProjectList, getProjectList,
postAccountTags,
} from '@/api/all/propertyMarketing'; } from '@/api/all/propertyMarketing';
import icon1 from '@/assets/img/media-account/icon-download.png'; import icon1 from '@/assets/img/media-account/icon-download.png';
@ -278,6 +280,35 @@ export default {
</> </>
); );
}; };
const handleTagChange = (value) => {
form.value.tag_ids = value;
};
const handleTagInputPressEnter = async (e) => {
const inputValue = e.target.value.trim();
if (!inputValue) return;
const _target = tagOptions.value.find((item) => item.name === inputValue);
if (_target) {
return;
}
try {
const { code, data } = await postAccountTags({ name: inputValue });
if (code === 200 && data) {
tagOptions.value.push({
id: data.id,
name: data.name,
});
e.target.value = '';
form.value.tag_ids = form.value.tag_ids.filter((item) => item !== inputValue);
form.value.tag_ids.push(data.id);
}
} catch (error) {
message.error('添加标签失败');
}
};
expose({ open }); expose({ open });
@ -430,11 +461,28 @@ export default {
</FormItem> </FormItem>
<FormItem label="选择标签"> <FormItem label="选择标签">
<CommonSelect <Select
v-model={form.value.tag_ids} value={form.value.tag_ids}
options={tagOptions.value} mode="tags"
placeholder="请选择…"
size="large" size="large"
placeholder="请选择标签"
allowClear
autoClearSearchValue
class="w-full"
showSearch
showArrow
maxTagCount={5}
optionFilterProp="name"
options={tagOptions.value}
field-names={{ label: 'name', value: 'id' }}
onChange={handleTagChange}
onInputKeyDown={(e) => {
// 检测回车键
if (e.key === 'Enter') {
e.preventDefault();
handleTagInputPressEnter(e);
}
}}
/> />
</FormItem> </FormItem>
<FormItem <FormItem

View File

@ -46,6 +46,7 @@
:multiple="false" :multiple="false"
:options="operators" :options="operators"
@change="handleSearch" @change="handleSearch"
@dropdownVisibleChange="getOperators"
/> />
</div> </div>
</div> </div>
@ -54,10 +55,10 @@
<span class="label">分组</span> <span class="label">分组</span>
<CommonSelect v-model="query.group_ids" multiple :options="groups" @change="handleSearch" class="!w-200px" /> <CommonSelect v-model="query.group_ids" multiple :options="groups" @change="handleSearch" class="!w-200px" />
</div> </div>
<div class="filter-row-item"> <!-- <div class="filter-row-item">-->
<span class="label">所属项目</span> <!-- <span class="label">所属项目</span>-->
<CommonSelect v-model="query.project_ids" :options="projects" @change="handleSearch" class="!w-200px" /> <!-- <CommonSelect v-model="query.project_ids" :options="projects" @change="handleSearch" class="!w-200px" />-->
</div> <!-- </div>-->
<div class="filter-row-item"> <div class="filter-row-item">
<span class="label">标签</span> <span class="label">标签</span>
<CommonSelect v-model="query.tag_ids" :options="tags" @change="handleSearch" class="!w-320px" /> <CommonSelect v-model="query.tag_ids" :options="tags" @change="handleSearch" class="!w-320px" />
@ -150,7 +151,7 @@ onMounted(() => {
getTags(); getTags();
getGroups(); getGroups();
getOperators(); getOperators();
getProjects(); // getProjects();
}); });
defineExpose({ defineExpose({

View File

@ -28,3 +28,82 @@ export const PLATFORM_LIST = [
value: 1, value: 1,
}, },
]; ];
export const SHOW_TYPES = [
{
label: '卡片',
value: 'card',
svgName: 'svg-card',
},
{
label: '列表',
value: 'list',
svgName: 'svg-list',
},
];
export const TABLE_COLUMNS = [
{
title: '账号名称',
dataIndex: 'name',
width: 200,
fixed: 'left',
},
{
title: '更新状态',
dataIndex: 'sync',
class: 'sync-row',
},
{
title: '状态',
dataIndex: 'status',
width: 110,
},
{
title: '数据更新时间',
dataIndex: 'last_synced_at',
width: 140,
},
{
title: '最后授权时间',
dataIndex: 'last_authorized_at',
width: 140,
},
{
title: '平台',
dataIndex: 'platform',
width: 80,
},
{
title: '账号ID',
dataIndex: 'account_id',
width: 140,
},
{
title: '手机号码',
dataIndex: 'mobile',
width: 140,
},
{
title: '运营人员',
dataIndex: 'operator.name',
width: 140,
},
{
title: '分组',
dataIndex: 'group.name',
width: 140,
},
{
title: '标签',
dataIndex: 'tags',
width: 180,
},
{
title: '操作',
dataIndex: 'operation',
width: 180,
fixed: 'right',
},
];

View File

@ -4,7 +4,7 @@
--> -->
<template> <template>
<div class="account-manage-wrap"> <div class="account-manage-wrap">
<div class="filter-wrap bg-#fff rounded-8px"> <div class="filter-wrap mb-16px bg-#fff rounded-8px">
<div class="top flex h-64px px-24px py-10px justify-between items-center"> <div class="top flex h-64px px-24px py-10px justify-between items-center">
<p class="text-18px font-400 lh-26px color-#211F24 title">账号管理</p> <p class="text-18px font-400 lh-26px color-#211F24 title">账号管理</p>
<div class="flex items-center"> <div class="flex items-center">
@ -31,65 +31,74 @@
<FilterBlock ref="filterBlockRef" v-model:query="query" @onSearch="handleSearch" @onReset="handleReset" /> <FilterBlock ref="filterBlockRef" v-model:query="query" @onSearch="handleSearch" @onReset="handleReset" />
</div> </div>
<!--存在异常账号-->
<div <div
v-if="dataSource.length > 0" v-if="hasAbNormalStatus"
class="tip-row flex justify-between items-center px-16px py-10px w-100% my-12px h-48px" class="tip-row flex justify-between items-center px-16px w-100% h-42px mb-16px abnormal"
:class="selectedItems.length > 0 ? 'selected' : isAbNormalStatus ? 'abnormal' : 'normal'"
> >
<div class="flex items-center"> <div class="flex items-center">
<div class="flex items-center"> <div class="flex items-center">
<template v-if="selectedItems.length > 0"> <img :src="icon5" class="mr-8px" height="16" width="16" />
<span class="label"> {{ tipLabel }} </span>
</div>
</div>
<Button class="!h-24px !px-12px" danger size="small" type="primary" @click="handleOpenAbnormalAccount">
<template #default>查看异常账号</template>
</Button>
</div>
<!--操作行-->
<div class="tip-row flex justify-between items-center px-16px py-10px w-100% h-44px mb-16px">
<div class="flex items-center">
<Checkbox <Checkbox
:checked="checkedAll" :checked="checkedAll"
:indeterminate="indeterminate" :indeterminate="indeterminate"
class="mr-8px" class="mr-24px"
@change="(e) => handleChangeAll(e.target.checked)" @change="(e) => handleSelectAll(e.target.checked)"
/>
<span class="label mr-24px">
已选
<span class="color-#6D4CFE">{{ selectedItems.length }}</span>
个账号
</span>
<span class="operation-btn" :class="{ disabled: isDisabledBatchSyncData }" @click="handleBatchSyncData"
>批量更新数据</span
> >
<span class="operation-btn" @click="handleBatchTag">批量标签</span> 全选
<span class="operation-btn" @click="handleBatchGroup">批量分组</span> </Checkbox>
<span class="operation-btn red" @click="handleBatchDelete"> 批量删除 </span>
</template>
<template v-else>
<img :src="isAbNormalStatus ? icon5 : icon4" width="16" height="16" class="mr-8px" />
<span class="label"> {{ tipLabel }} </span>
</template>
</div>
</div>
<template v-if="selectedItems.length > 0"> <template v-if="selectedRows.length">
<img :src="icon6" width="16" height="16" class="cursor-pointer" @click="handleCloseTip" /> <Button :disabled="isDisabledBatchSyncData" class="!h-24px !px-12px" type="text" @click="handleBatchSyncData">
</template> 批量更新数据
<div v-else>
<Space v-if="isAbNormalStatus" class="flex items-center">
<Button type="primary" danger size="small" @click="handleOpenAbnormalAccount">
<template #default>查看异常账号</template>
</Button> </Button>
</Space> <Button class="!h-24px !px-12px" type="text" @click="handleBatchTag"> 批量标签</Button>
<Button class="!h-24px !px-12px" type="text" @click="handleBatchGroup"> 批量分组</Button>
<Button class="!h-24px !px-12px" danger type="text" @click="handleBatchDelete"> 批量删除</Button>
</template>
</div>
<div class="flex items-center">
<RadioGroup v-model:value="showType">
<RadioButton v-for="(item, index) in SHOW_TYPES" :key="index" :value="item.value">
<div class="flex items-center">
<SvgIcon :name="item.svgName" class="mr-4px" size="16" />
<span>{{ item.label }}</span>
</div>
</RadioButton>
</RadioGroup>
</div> </div>
</div> </div>
<div class="card-wrap"> <div class="card-wrap">
<AccountTable <component
v-if="dataSource.length > 0" :is="showType === 'card' ? Card : Table"
:syncMediaAccounts="syncMediaAccounts"
:isLoadingTaskStatus="isLoadingTaskStatus"
:dataSource="dataSource" :dataSource="dataSource"
:isLoadingTaskStatus="isLoadingTaskStatus"
:selectedItems="selectedItems" :selectedItems="selectedItems"
@selectionChange="handleSelectionChange" :selectedRowKeys="selectedRowKeys"
:selectedRows="selectedRows"
:syncMediaAccounts="syncMediaAccounts"
:tableColumns="TABLE_COLUMNS"
@delete="handleDelete" @delete="handleDelete"
@openEdit="handleOpenEdit" @openEdit="handleOpenEdit"
@pause="handlePause"
@reauthorize="handleReauthorize"
@select="handleSelect"
@selectAll="handleSelectAll"
@update="getData" @update="getData"
@updateSyncStatus="handleUpdateSyncStatus" @updateSyncStatus="handleUpdateSyncStatus"
/> />
<NoData v-else />
<div v-if="pageInfo.total > 0" class="pagination-row"> <div v-if="pageInfo.total > 0" class="pagination-row">
<Pagination <Pagination
@ -99,7 +108,7 @@
showSizeChanger showSizeChanger
showQuickJumper showQuickJumper
:current="pageInfo.page" :current="pageInfo.page"
:pageSize="pageInfo.pageSize" :pageSize="pageInfo.page_size"
:pageSizeOptions="['8', '16', '20', '32', '64']" :pageSizeOptions="['8', '16', '20', '32', '64']"
@change="onPageChange" @change="onPageChange"
/> />
@ -113,15 +122,24 @@
<DeleteAccountModal ref="deleteAccountRef" @update="getData" @batchUpdate="onBatchSuccess" /> <DeleteAccountModal ref="deleteAccountRef" @update="getData" @batchUpdate="onBatchSuccess" />
<BatchTagModal ref="batchTagModalRef" @update="onBatchSuccess" /> <BatchTagModal ref="batchTagModalRef" @update="onBatchSuccess" />
<BatchGroupModal ref="batchGroupModalRef" @update="onBatchSuccess" /> <BatchGroupModal ref="batchGroupModalRef" @update="onBatchSuccess" />
<PauseAccountPatchModal ref="pauseAccountPatchModalRef" @success="getData" />
<ReauthorizeAccountModal ref="reauthorizeAccountModalRef" @update="getData" />
<AuthorizedAccountModal ref="authorizedAccountModalRef" @update="getData" />
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, provide } from 'vue'; import { ref, provide } from 'vue';
import { Checkbox, Button, Space, Pagination, notification } from 'ant-design-vue'; import { Checkbox, Button, Pagination, notification, RadioButton, RadioGroup } from 'ant-design-vue';
import FilterBlock from './components/filter-block'; import FilterBlock from './components/filter-block';
import AccountTable from './components/account-table'; import Card from './components/account-table/card.vue';
import Table from './components/account-table/table.vue';
import PauseAccountPatchModal from './components/account-table/pause-account-patch.vue';
import AuthorizedAccountModal from './components/authorized-account-modal';
import ReauthorizeAccountModal from './components/reauthorize-account-modal';
import GroupManageModal from './components/group-manage-modal'; import GroupManageModal from './components/group-manage-modal';
import TagsManageModal from './components/tags-manage-modal'; import TagsManageModal from './components/tags-manage-modal';
import AddAccountModal from './components/add-account-modal'; import AddAccountModal from './components/add-account-modal';
@ -129,10 +147,14 @@ import DeleteAccountModal from './components/account-table/delete-account';
import BatchTagModal from './components/batch-tag-modal'; import BatchTagModal from './components/batch-tag-modal';
import BatchGroupModal from './components/batch-group-modal'; import BatchGroupModal from './components/batch-group-modal';
import { INITIAL_QUERY, INITIAL_PAGE_INFO } from './constants'; import { INITIAL_QUERY, SHOW_TYPES, TABLE_COLUMNS } from './constants';
import { showImportResultNotification } from '@/utils/arcoD'; import { showImportResultNotification } from '@/utils/notification';
import { getTaskStatus } from '@/api/all/common'; import { getTaskStatus } from '@/api/all/common';
import { EnumStatus } from '@/views/property-marketing/media-account/components/status-select/status-box.tsx'; import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination';
import {
EnumErrorStatus,
EnumStatus,
} from '@/views/property-marketing/media-account/components/status-select/status-box.tsx';
import { import {
getMediaAccounts, getMediaAccounts,
getMediaAccountsHealth, getMediaAccountsHealth,
@ -143,13 +165,28 @@ import {
import icon2 from '@/assets/img/media-account/icon-group.png'; import icon2 from '@/assets/img/media-account/icon-group.png';
import icon3 from '@/assets/img/media-account/icon-tag.png'; import icon3 from '@/assets/img/media-account/icon-tag.png';
import icon4 from '@/assets/img/media-account/icon-success.png'; // import icon4 from '@/assets/img/media-account/icon-success.png';
import icon5 from '@/assets/img/media-account/icon-warn.png'; import icon5 from '@/assets/img/media-account/icon-warn.png';
import icon6 from '@/assets/img/media-account/icon-close.png'; // import icon6 from '@/assets/img/media-account/icon-close.png';
let syncDataTimer = null; let syncDataTimer = null;
let queryTaskTimer = null; let queryTaskTimer = null;
const {
selectedRowKeys,
selectedRows,
dataSource,
pageInfo,
DEFAULT_PAGE_INFO,
onPageChange,
handleSelect,
handleSelectAll,
} = useTableSelectionWithPagination({
onPageChange: () => {
getData();
},
});
const groupManageModalRef = ref(null); const groupManageModalRef = ref(null);
const tagsManageModalRef = ref(null); const tagsManageModalRef = ref(null);
const addAccountModalRef = ref(null); const addAccountModalRef = ref(null);
@ -157,25 +194,28 @@ const deleteAccountRef = ref(null);
const batchTagModalRef = ref(null); const batchTagModalRef = ref(null);
const batchGroupModalRef = ref(null); const batchGroupModalRef = ref(null);
const filterBlockRef = ref(null); const filterBlockRef = ref(null);
const pauseAccountPatchModalRef = ref(null);
const reauthorizeAccountModalRef = ref(null);
const authorizedAccountModalRef = ref(null);
const pageInfo = ref(cloneDeep(INITIAL_PAGE_INFO));
const query = ref(cloneDeep(INITIAL_QUERY)); const query = ref(cloneDeep(INITIAL_QUERY));
const dataSource = ref([]); // const dataSource = ref([]);
const selectedItems = ref([]); const selectedItems = ref([]);
const healthData = ref({}); const healthData = ref({});
const syncMediaAccounts = ref([]); const syncMediaAccounts = ref([]);
const isLoadingTaskStatus = ref(false); // 正在查询状态中 const isLoadingTaskStatus = ref(false); // 正在查询状态中
const showType = ref('card');
const isAbNormalStatus = computed(() => healthData.value?.abnormal_number > 0); const hasAbNormalStatus = computed(() => healthData.value?.abnormal_number > 0);
const isDisabledBatchSyncData = computed(() => selectedItems.value.some((item) => item.status !== EnumStatus.NORMAL)); const isDisabledBatchSyncData = computed(() => selectedRows.value.some((item) => item.status !== EnumStatus.NORMAL));
const checkedAll = computed(() => selectedItems.value.length === dataSource.value.length); const checkedAll = computed(() => dataSource.value.length > 0 && selectedRows.value.length === dataSource.value.length);
const indeterminate = computed( const indeterminate = computed(
() => selectedItems.value.length > 0 && selectedItems.value.length < dataSource.value.length, () => selectedRows.value.length > 0 && selectedRows.value.length < dataSource.value.length,
); );
const tipLabel = computed(() => { const tipLabel = computed(() => {
if (!isAbNormalStatus.value) { if (!hasAbNormalStatus.value) {
return '太棒啦!所有账号都在正常运行。'; return '太棒啦!所有账号都在正常运行。';
} }
@ -214,10 +254,10 @@ const getHealthData = async () => {
} }
}; };
const getAccountData = async () => { const getAccountData = async () => {
const { page, pageSize } = pageInfo.value; const { page, page_size } = pageInfo.value;
const { code, data } = await getMediaAccounts({ const { code, data } = await getMediaAccounts({
page, page,
page_size: pageSize, page_size,
...query.value, ...query.value,
}); });
if (code === 200) { if (code === 200) {
@ -233,23 +273,13 @@ const handleSearch = () => {
reload(); reload();
}; };
const handleReset = () => { const handleReset = () => {
pageInfo.value = cloneDeep(INITIAL_PAGE_INFO); pageInfo.value = cloneDeep(DEFAULT_PAGE_INFO);
selectedItems.value = []; selectedRows.value = [];
selectedRowKeys.value = [];
query.value = cloneDeep(INITIAL_QUERY); query.value = cloneDeep(INITIAL_QUERY);
reload(); reload();
}; };
const onPageChange = (current, pageSize) => {
pageInfo.value.page = current;
pageInfo.value.pageSize = pageSize;
getData();
};
const onPageSizeChange = (pageSize) => {
pageInfo.value.pageSize = pageSize;
reload();
};
const handleOpenGroupModal = () => { const handleOpenGroupModal = () => {
groupManageModalRef.value?.open(); groupManageModalRef.value?.open();
}; };
@ -264,24 +294,16 @@ const handleOpenEdit = (item) => {
addAccountModalRef.value?.open(item.id); addAccountModalRef.value?.open(item.id);
}; };
const handleSelectionChange = (val) => {
selectedItems.value = val;
};
const handleChangeAll = (checked) => {
selectedItems.value = checked ? cloneDeep(dataSource.value) : [];
};
const handleBatchDelete = () => { const handleBatchDelete = () => {
const ids = selectedItems.value.map((item) => item.id); const ids = selectedRows.value.map((item) => item.id);
const names = selectedItems.value.map((item) => `"${item.name || '-'}"`).join(''); const names = selectedRows.value.map((item) => `"${item.name || '-'}"`).join('');
deleteAccountRef.value?.open({ id: ids, name: names }); deleteAccountRef.value?.open({ id: ids, name: names });
}; };
const handleDelete = (item) => { const handleDelete = (item) => {
const { id, name } = item; const { id, name } = item;
deleteAccountRef.value?.open({ id, name: `"${name || '-'}"` }); deleteAccountRef.value?.open({ id, name: `"${name || '-'}"` });
}; };
const handleCloseTip = () => {
selectedItems.value = [];
};
// 先立即执行一次 // 先立即执行一次
const getAsyncStatus = async () => { const getAsyncStatus = async () => {
const { code, data } = await getMediaAccountSyncStatus(); const { code, data } = await getMediaAccountSyncStatus();
@ -323,14 +345,14 @@ const handleSyncData = async (item) => {
}; };
const handleBatchTag = () => { const handleBatchTag = () => {
batchTagModalRef.value?.open(selectedItems.value); batchTagModalRef.value?.open(selectedRows.value);
}; };
const handleBatchSyncData = async () => { const handleBatchSyncData = async () => {
if (isDisabledBatchSyncData.value) { if (isDisabledBatchSyncData.value) {
return; return;
} }
const ids = selectedItems.value.map((item) => item.id); const ids = selectedRows.value.map((item) => item.id);
const { code } = await postBatchSyncMediaAccountData({ ids }); const { code } = await postBatchSyncMediaAccountData({ ids });
if (code === 200) { if (code === 200) {
if (!isLoadingTaskStatus.value) { if (!isLoadingTaskStatus.value) {
@ -339,10 +361,11 @@ const handleBatchSyncData = async () => {
} }
}; };
const handleBatchGroup = () => { const handleBatchGroup = () => {
batchGroupModalRef.value?.open(selectedItems.value); batchGroupModalRef.value?.open(selectedRows.value);
}; };
const onBatchSuccess = () => { const onBatchSuccess = () => {
selectedItems.value = []; selectedRowKeys.value = [];
selectedRows.value = [];
getData(); getData();
}; };
const handleOpenAbnormalAccount = () => { const handleOpenAbnormalAccount = () => {
@ -350,6 +373,23 @@ const handleOpenAbnormalAccount = () => {
reload(); reload();
}; };
const isUnauthorizedStatus = (error_status) => {
return [EnumErrorStatus.UNAUTHORIZED].includes(error_status);
};
const handlePause = (item) => {
pauseAccountPatchModalRef.value?.open(item);
};
const handleReauthorize = (item) => {
const { id, platform, error_status } = item;
const isUnauthorized = isUnauthorizedStatus(error_status);
if (isUnauthorized) {
authorizedAccountModalRef.value?.open(id, platform);
} else {
reauthorizeAccountModalRef.value?.open(id, platform);
}
};
// 查询导入账号任务状态 // 查询导入账号任务状态
const getSyncTaskStatus = async (id, notificationId) => { const getSyncTaskStatus = async (id, notificationId) => {
const { code, data } = await getTaskStatus(id); const { code, data } = await getTaskStatus(id);

View File

@ -17,46 +17,52 @@
} }
.tip-row { .tip-row {
border-radius: 2px; border-radius: 2px;
background: #f0edff; background: #fff;
.label { .label {
font-family: $font-family-medium; font-family: $font-family-regular;
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 22px; line-height: 22px;
} }
&.normal {
background: #ebf7f2;
.label {
color: #211f24;
}
}
&.abnormal { &.abnormal {
background: #ffe7e4; background: #FFE9E7;
.label { .label {
color: #211f24; color: #211f24;
} }
} }
.operation-btn {
padding: 0; :deep(.ant-checkbox) {
cursor: pointer; + span {
color: var(--Brand-Brand-6, #6d4cfe); font-family: $font-family-medium;
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
&:not(:last-child) {
margin-right: 16px;
}
&.red {
color: #f64b31;
}
&.disabled {
color: #c5b7ff;
cursor: not-allowed;
} }
} }
:deep(.ant-radio-group) {
.ant-radio-button-wrapper {
color: #737478;
&:first-child {
border-radius: 4px 0 0 4px;
}
&:last-child {
border-radius: 0 4px 4px 0;
}
&:hover {
color: #6D4CFE;
border-color: #6D4CFE;
}
&-checked {
border-color: #6D4CFE;
background: #F0EDFF;
color: #6D4CFE;
}
}
}
} }
.card-wrap { .card-wrap {
display: flex; display: flex;

View File

@ -0,0 +1,159 @@
<template>
<div class="note-detail-wrap h-full flex flex-col">
<div class="flex items-center mb-16px cursor-pointer" @click="handleBack">
<icon-left size="16" />
<span class="cts ml-8px bold">账号详情</span>
</div>
<!--作品详情-->
<div class="bg-#fff rounded-8px px-24px pb-20px max-h-552px flex flex-col mb-16px">
<div class="title-row">
<div class="flex items-center">
<span class="cts !text-18px !lh-26px mr-4px bold">作品详情</span>
<Tooltip title="展示作品的标题、图文、视频等详细内容">
<icon-question-circle class="color-#737478" size="14" />
</Tooltip>
</div>
</div>
<p class="mt-16px cts bold !text-24px !lh-32px">{{ dataSource.title || '-' }}</p>
<!-- <div class="mt-16px">-->
<!-- <ImagePreviewGroup>-->
<!-- <Image-->
<!-- :height="100"-->
<!-- :width="100"-->
<!-- class="rounded-8px mr-8px"-->
<!-- src="https://lingji-test-1334771076.cos.ap-nanjing.myqcloud.com/files/443fb5d6-54ba-4f4e-9a9a-d0a09e83ffb8.png"-->
<!-- />-->
<!-- <Image-->
<!-- :height="100"-->
<!-- :width="100"-->
<!-- class="rounded-8px mr-8px"-->
<!-- src="https://lingji-test-1334771076.cos.ap-nanjing.myqcloud.com/files/443fb5d6-54ba-4f4e-9a9a-d0a09e83ffb8.png"-->
<!-- />-->
<!-- </ImagePreviewGroup>-->
<!-- </div>-->
<p class="mt-16px cts whitespace-pre-line">{{ dataSource.content }}</p>
</div>
<!--评论列表-->
<div class="bg-#fff rounded-8px pb-12px max-h-422px flex flex-col mb-16px">
<div class="title-row !px-24px">
<div class="flex items-center">
<span class="cts !text-18px !lh-26px mr-4px bold">评论列表</span>
<Tooltip title="展示用户对作品的评论">
<icon-question-circle class="color-#737478" size="14" />
</Tooltip>
</div>
</div>
<div class="flex-1 overflow-y-auto !px-24px">
<NoData v-if="!dataSource.comments?.length" class="!p-0" />
<div
v-for="(item, index) in dataSource.comments"
v-else
:key="index"
class="flex items-start justify-between mb-8px"
>
<TextOverTips :context="item.commenter" class="cts !color-#737478 !w-120px mr-20px" />
<div class="flex-1 flex justify-between items-start">
<div class="flex-1 flex items-start">
<p class="mr-4px cts">{{ item.content }}</p>
<p class="cts !color-#737478 flex-shrink-0 num">{{ exactFormatTime(item.commented_at) }}</p>
</div>
<TextOverTips
:context="`${formatNumberShow(item.like_number)}点赞 ${formatNumberShow(item.reply_number)}回复`"
class="cts !color-#737478 ml-20px !w-fit max-w-200px"
/>
</div>
</div>
</div>
</div>
<!--作品数据-->
<div class="bg-#fff rounded-8px px-24px pb-20px h-184px flex flex-col">
<div class="title-row">
<div class="flex items-center">
<span class="cts !text-18px !lh-26px mr-4px bold">作品数据</span>
<Tooltip title="展示作品的曝光量、点赞量等数据指标">
<icon-question-circle class="color-#737478" size="14" />
</Tooltip>
</div>
</div>
<div class="flex-1 grid grid-cols-4 gap-24px">
<div v-for="(item, index) in noteData" :key="index">
<div class="flex items-center mb-4px">
<span class="cts !color-#737478 mr-4px">{{ item.label }}</span>
<Tooltip :title="item.tooltip">
<icon-question-circle class="color-#737478" size="16" />
</Tooltip>
</div>
<span class="cts num">{{ formatNumberShow(dataSource[item.prop]) }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { useRoute, useRouter } from 'vue-router';
import { Image, ImagePreviewGroup, Tooltip } from 'ant-design-vue';
import TextOverTips from '@/components/text-over-tips/index.vue';
import { exactFormatTime, formatNumberShow } from '@/utils/tools';
import { getMediaAccountWorkDetail } from '@/api/all/propertyMarketing';
const noteData = [
{ label: '曝光量', prop: 'exposure_number', tooltip: '内容被展示给用户的总次数,不代表用户实际观看。' },
{
label: '观看量',
prop: 'view_number',
tooltip: '用户点击内容并实际观看的次数,是内容实际触达的重要指标。',
},
{
label: '点赞量',
prop: 'like_number',
tooltip: '单篇笔记获得的点赞总数,反映用户喜好程度。',
},
{
label: '收藏量',
prop: 'collect_number',
tooltip: '用户将内容保存到收藏夹的次数,代表内容被认可为“值得保留”。',
},
{
label: '评论数',
prop: 'comment_number',
tooltip: '内容下方用户留言的总数,体现用户参与度与讨论热度。',
},
{
label: '分享量',
prop: 'share_number',
tooltip: '内容被转发或分享至其他平台或私信的次数,代表外扩传播意愿。',
},
// {
// label: '点击率',
// prop: 'cover_click_rate',
// tooltip: '内容在被曝光后,用户点击进入的比例,反映封面与标题吸引力。',
// },
];
const router = useRouter();
const route = useRoute();
const dataSource = ref({});
const handleBack = () => {
router.go(-1);
};
const getData = async () => {
const { code, data } = await getMediaAccountWorkDetail(route.params.id);
if (code === 200) {
dataSource.value = data;
}
};
onMounted(() => {
getData();
});
</script>
<style lang="scss" scoped>
@import './style.scss';
</style>

View File

@ -0,0 +1,29 @@
.note-detail-wrap {
.cts {
color: var(--Text-1, #211f24);
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
&.bold {
font-family: $font-family-medium;
}
&.num {
font-family: $font-family-manrope-regular;
}
}
.title-row {
display: flex;
height: 64px;
padding: 10px 0;
align-items: center;
}
:deep(.ant-image) {
margin-right: 8px;
}
}

View File

@ -67,7 +67,7 @@ import {
postPlacementAccountDataListExport, postPlacementAccountDataListExport,
} from '@/api/all/propertyMarketing'; } from '@/api/all/propertyMarketing';
import { showExportNotification } from '@/utils/arcoD'; import { showExportNotification } from '@/utils/notification';
import { INITIAL_QUERY, INITIAL_PAGE_INFO } from './constants'; import { INITIAL_QUERY, INITIAL_PAGE_INFO } from './constants';
// import { downloadByUrl } from '@/utils/tools'; // import { downloadByUrl } from '@/utils/tools';

View File

@ -174,7 +174,7 @@ import CommonSelect from '@/components/common-select';
import { PLATFORM_LIST, ENUM_PUT_ACCOUNT_PLATFORM } from '@/utils/platform'; import { PLATFORM_LIST, ENUM_PUT_ACCOUNT_PLATFORM } from '@/utils/platform';
import { showExportNotification } from '@/utils/arcoD'; import { showExportNotification } from '@/utils/notification';
import { genRandomId } from '@/utils/tools'; import { genRandomId } from '@/utils/tools';
import { import {
postPlacementAccounts, postPlacementAccounts,

View File

@ -102,7 +102,7 @@ import DeleteAccountModal from './components/account-table/delete-account';
import { INITIAL_QUERY } from './constants'; import { INITIAL_QUERY } from './constants';
import { getPlacementAccounts, getPlacementAccountsHealth } from '@/api/all/propertyMarketing'; import { getPlacementAccounts, getPlacementAccountsHealth } from '@/api/all/propertyMarketing';
import { getTaskStatus } from '@/api/all/common'; import { getTaskStatus } from '@/api/all/common';
import { showImportResultNotification } from '@/utils/arcoD'; import { showImportResultNotification } from '@/utils/notification';
import icon4 from '@/assets/img/media-account/icon-success.png'; import icon4 from '@/assets/img/media-account/icon-success.png';
import icon5 from '@/assets/img/media-account/icon-warn.png'; import icon5 from '@/assets/img/media-account/icon-warn.png';

View File

@ -72,6 +72,7 @@ export default {
}; };
const onSubmit = async (action) => { const onSubmit = async (action) => {
try {
uploadLoading.value = true; uploadLoading.value = true;
const filteredWorks = map(works.value, (work) => omit(work, 'videoInfo')); const filteredWorks = map(works.value, (work) => omit(work, 'videoInfo'));
const { code, data } = await postWorksBatchWriter({ works: filteredWorks }, writerCode.value); const { code, data } = await postWorksBatchWriter({ works: filteredWorks }, writerCode.value);
@ -98,6 +99,9 @@ export default {
} }
} }
} }
} finally {
uploadLoading.value = false;
}
}; };
const onUpload = async (action) => { const onUpload = async (action) => {