Files
lingji-work-fe/src/views/property-marketing/media-account/account-manage/index.vue

362 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<!--
* @Author: RenXiaoDong
* @Date: 2025-06-25 10:00:50
-->
<template>
<div class="account-manage-wrap">
<div class="filter-wrap bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid">
<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>
<div class="flex items-center">
<a-button class="w-112px mr-12px" type="outline" size="medium" @click="handleOpenTagsModal">
<template #icon>
<img :src="icon3" width="16" height="16" />
</template>
<template #default>标签管理</template>
</a-button>
<a-button class="w-112px mr-12px" type="outline" size="medium" @click="handleOpenGroupModal">
<template #icon>
<img :src="icon2" width="16" height="16" />
</template>
<template #default>分组管理</template>
</a-button>
<a-button type="primary" class="w-112px" size="medium" @click="handleOpenAccountModal">
<template #icon>
<img :src="icon1" width="16" height="16" />
</template>
<template #default>添加账号</template>
</a-button>
</div>
</div>
<FilterBlock ref="filterBlockRef" v-model:query="query" @onSearch="handleSearch" @onReset="handleReset" />
</div>
<div
v-if="dataSource.length > 0"
class="tip-row flex justify-between px-16px py-10px w-100% my-12px h-42px"
:class="selectedItems.length > 0 ? 'selected' : isAbNormalStatus ? 'abnormal' : 'normal'"
>
<div class="flex items-center">
<div class="flex items-center">
<template v-if="selectedItems.length > 0">
<a-checkbox
:model-value="checkedAll"
:indeterminate="indeterminate"
class="mr-8px"
@change="handleChangeAll"
/>
<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>
<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">
<img :src="icon6" width="16" height="16" class="cursor-pointer" @click="handleCloseTip" />
</template>
<div v-else>
<a-space v-if="isAbNormalStatus" class="flex items-center">
<a-button class="w-96px err-btn" size="mini" @click="handleOpenAbnormalAccount">
<template #default>查看异常账号</template>
</a-button>
</a-space>
</div>
</div>
<div class="card-wrap">
<AccountTable
v-if="dataSource.length > 0"
:syncMediaAccounts="syncMediaAccounts"
:isLoadingTaskStatus="isLoadingTaskStatus"
:dataSource="dataSource"
:selectedItems="selectedItems"
@selectionChange="handleSelectionChange"
@delete="handleDelete"
@openEdit="handleOpenEdit"
@update="getData"
/>
<NoData v-else />
<div v-if="pageInfo.total > 0" class="pagination-box">
<a-pagination
:total="pageInfo.total"
size="mini"
show-total
show-jumper
show-page-size
:page-size-options="[8, 16, 20, 32, 64]"
:current="pageInfo.page"
:page-size="pageInfo.pageSize"
@change="onPageChange"
@page-size-change="onPageSizeChange"
/>
</div>
</div>
<GroupManageModal ref="groupManageModalRef" @update="filterBlockRef?.getGroups" />
<TagsManageModal ref="tagsManageModalRef" @update="filterBlockRef?.getTags" />
<AddAccountModal ref="addAccountModalRef" @update="getData" />
<DeleteAccountModal ref="deleteAccountRef" @update="getData" @batchUpdate="onBatchSuccess" />
<BatchTagModal ref="batchTagModalRef" @update="onBatchSuccess" />
<BatchGroupModal ref="batchGroupModalRef" @update="onBatchSuccess" />
</div>
</template>
<script setup>
import { ref, provide } from 'vue';
import FilterBlock from './components/filter-block';
import AccountTable from './components/account-table';
import GroupManageModal from './components/group-manage-modal';
import TagsManageModal from './components/tags-manage-modal';
import AddAccountModal from './components/add-account-modal';
import DeleteAccountModal from './components/account-table/delete-account';
import BatchTagModal from './components/batch-tag-modal';
import BatchGroupModal from './components/batch-group-modal';
import { Notification } from '@arco-design/web-vue';
import { INITIAL_QUERY, INITIAL_PAGE_INFO } from './constants';
import { EnumStatus } from '@/views/property-marketing/media-account/components/status-select/constants';
import {
getMediaAccounts,
getMediaAccountsHealth,
postSyncMediaAccountData,
postBatchSyncMediaAccountData,
getMediaAccountSyncStatus,
} from '@/api/all/propertyMarketing';
import icon1 from '@/assets/img/media-account/icon-add.png';
import icon2 from '@/assets/img/media-account/icon-group.png';
import icon3 from '@/assets/img/media-account/icon-tag.png';
import icon4 from '@/assets/img/media-account/icon-success.png';
import icon5 from '@/assets/img/media-account/icon-warn.png';
import icon6 from '@/assets/img/media-account/icon-close.png';
let syncDataTimer = null;
const groupManageModalRef = ref(null);
const tagsManageModalRef = ref(null);
const addAccountModalRef = ref(null);
const deleteAccountRef = ref(null);
const batchTagModalRef = ref(null);
const batchGroupModalRef = ref(null);
const filterBlockRef = ref(null);
const pageInfo = ref(cloneDeep(INITIAL_PAGE_INFO));
const query = ref(cloneDeep(INITIAL_QUERY));
const dataSource = ref([]);
const selectedItems = ref([]);
const healthData = ref({});
const syncMediaAccounts = ref([]);
const isLoadingTaskStatus = ref(false); // 正在查询状态中
const isAbNormalStatus = computed(() => healthData.value?.total_abnormal_number > 0);
const isDisabledBatchSyncData = computed(() => selectedItems.value.some((item) => item.status !== EnumStatus.NORMAL));
const checkedAll = computed(() => selectedItems.value.length === dataSource.value.length);
const indeterminate = computed(
() => selectedItems.value.length > 0 && selectedItems.value.length < dataSource.value.length,
);
const tipLabel = computed(() => {
if (!isAbNormalStatus.value) {
return '太棒啦!所有账号都在正常运行。';
}
const {
total_abnormal_number = 0,
login_invalid_number = 0,
too_many_requests_number = 0,
account_frozen_number = 0,
miss_data_number = 0,
abnormal_number = 0,
} = healthData.value;
// 定义异常类型映射
const abnormalTypes = [
{ count: login_invalid_number, label: 'cookie过期' },
{ count: too_many_requests_number, label: '请求频繁' },
{ count: account_frozen_number, label: '账号被封' },
{ count: miss_data_number, label: '数据缺失' },
{ count: abnormal_number, label: '其他异常' },
];
// 过滤出有异常的项并格式化
const abnormalLabels = abnormalTypes.filter(({ count }) => count > 0).map(({ count, label }) => `${count}${label}`);
return `共有 ${total_abnormal_number} 个账号存在授权异常,其中:${abnormalLabels.join('')}`;
});
const getData = () => {
getHealthData();
getAccountData();
};
const getHealthData = async () => {
const { code, data } = await getMediaAccountsHealth();
if (code === 200) {
healthData.value = data;
}
};
const getAccountData = async () => {
const { page, pageSize } = pageInfo.value;
const { code, data } = await getMediaAccounts({
page,
page_size: pageSize,
...query.value,
});
if (code === 200) {
dataSource.value = data?.data ?? [];
pageInfo.value.total = data?.total ?? 0;
}
};
const reload = () => {
pageInfo.value.page = 1;
getData();
};
const handleSearch = () => {
reload();
};
const handleReset = () => {
pageInfo.value = cloneDeep(INITIAL_PAGE_INFO);
selectedItems.value = [];
query.value = cloneDeep(INITIAL_QUERY);
reload();
};
const onPageChange = (current) => {
pageInfo.value.page = current;
getData();
};
const onPageSizeChange = (pageSize) => {
pageInfo.value.pageSize = pageSize;
reload();
};
const handleOpenGroupModal = () => {
groupManageModalRef.value?.open();
};
const handleOpenTagsModal = () => {
tagsManageModalRef.value?.open();
};
const handleOpenAccountModal = () => {
addAccountModalRef.value?.open();
};
const handleOpenEdit = (item) => {
addAccountModalRef.value?.open(item.id);
};
const handleSelectionChange = (val) => {
selectedItems.value = val;
};
const handleChangeAll = (checked) => {
selectedItems.value = checked ? cloneDeep(dataSource.value) : [];
};
const handleBatchDelete = () => {
const ids = selectedItems.value.map((item) => item.id);
const names = selectedItems.value.map((item) => `"${item.name || '-'}"`).join('');
deleteAccountRef.value?.open({ id: ids, name: names });
};
const handleDelete = (item) => {
const { id, name } = item;
deleteAccountRef.value?.open({ id, name: `"${name || '-'}"` });
};
const handleCloseTip = () => {
selectedItems.value = [];
};
// 先立即执行一次
const getAsyncStatus = async () => {
const { code, data } = await getMediaAccountSyncStatus();
if (code === 200) {
syncMediaAccounts.value = data;
// 所有任务都结束了,才停止轮询,刷新页面
const isEnd = data.every((item) => item.status !== 0);
if (isEnd) {
clearSyncDataTimer();
isLoadingTaskStatus.value = false;
getData();
}
}
};
const startSyncDataPolling = () => {
isLoadingTaskStatus.value = true;
clearSyncDataTimer();
getAsyncStatus();
syncDataTimer = setInterval(getAsyncStatus, 3000);
};
const handleSyncData = async (item) => {
const { code } = await postSyncMediaAccountData(item.id);
if (code === 200) {
if (!isLoadingTaskStatus.value) {
startSyncDataPolling();
}
}
};
const handleBatchTag = () => {
batchTagModalRef.value?.open(selectedItems.value);
};
const handleBatchSyncData = async () => {
if (isDisabledBatchSyncData.value) {
return;
}
const ids = selectedItems.value.map((item) => item.id);
const { code } = await postBatchSyncMediaAccountData({ ids });
if (code === 200) {
if (!isLoadingTaskStatus.value) {
startSyncDataPolling();
}
}
};
const handleBatchGroup = () => {
batchGroupModalRef.value?.open(selectedItems.value);
};
const onBatchSuccess = () => {
selectedItems.value = [];
getData();
};
const handleOpenAbnormalAccount = () => {
query.value.status = 2;
reload();
};
const clearSyncDataTimer = () => {
if (syncDataTimer) {
clearInterval(syncDataTimer);
syncDataTimer = null;
}
};
onMounted(() => {
getData();
});
onUnmounted(() => {
clearSyncDataTimer();
});
provide('handleSyncData', handleSyncData);
</script>
<style scoped lang="scss">
@import './style.scss';
</style>