feat: 同步数据/批量同步

This commit is contained in:
rd
2025-07-14 16:29:04 +08:00
parent cdb8f677bc
commit ca31db446d
9 changed files with 222 additions and 21 deletions

View File

@ -332,4 +332,17 @@ export const postPlacementAccountsSync = (id: string) => {
return Http.post(`/v1/placement-accounts/${id}/sync-data`);
};
// 媒体账号-同步数据
export const postSyncMediaAccountData = (id: string) => {
return Http.post(`/v1/media-accounts/${id}/sync-data`);
};
// 媒体账号-批量同步数据
export const postBatchSyncMediaAccountData = (params: {}) => {
return Http.post(`/v1/media-accounts/batch-sync-data`, params);
};
// 媒体账号-查询同步状态
export const getMediaAccountSyncStatus = (params = {}) => {
return Http.get('/v1/media-accounts/sync-status', params);
};

View File

@ -70,6 +70,9 @@
</a-button>
<template #content>
<a-doption class="color-#211F24" @click="openEdit(item)">编辑</a-doption>
<a-doption v-if="item.status === EnumStatus.NORMAL" class="color-#211F24" @click="handleReauthorize(item)"
>重新授权</a-doption
>
<a-doption v-if="showPauseButton(item.status)" class="color-#211F24" @click="handlePause(item)"
>暂停同步</a-doption
>
@ -113,7 +116,7 @@
</template>
<script setup>
import { defineProps, ref, computed } from 'vue';
import { defineProps, ref, computed, inject } from 'vue';
import { STATUS_LIST, EnumStatus } from '@/views/property-marketing/media-account/components/status-select/constants';
import PauseAccountPatchModal from './pause-account-patch';
@ -183,6 +186,9 @@ const handlePause = (item) => {
const showPauseButton = (status) => {
return ![EnumStatus.PAUSE, EnumStatus.UNAUTHORIZED].includes(status);
};
const showSyncDataButton = (status) => {
return [EnumStatus.NORMAL, EnumStatus.ABNORMAL_MISSING].includes(status);
};
const isUnauthorizedStatus = (status) => {
return [EnumStatus.UNAUTHORIZED].includes(status);
};
@ -201,13 +207,19 @@ const getTooltipText = (status) => {
return STATUS_LIST.find((v) => v.value === status)?.tooltip ?? '-';
};
const handleSyncData = inject('handleSyncData');
const onBtnClick = (item) => {
if (showSyncDataButton(item.status)) {
handleSyncData && handleSyncData(item);
return;
}
if (isUnauthorizedStatus(item.status)) {
handleReauthorize(item);
return;
}
if ([EnumStatus.PAUSE, EnumStatus.NORMAL].includes(item.status) || isAbnormalStatus(item.status)) {
if ([EnumStatus.PAUSE].includes(item.status) || isAbnormalStatus(item.status)) {
handleReauthorize(item);
return;
}
@ -216,11 +228,15 @@ const onBtnClick = (item) => {
};
const getBtnText = (item) => {
if (showSyncDataButton(item.status)) {
return '更新数据';
}
if (isUnauthorizedStatus(item.status)) {
return '去授权';
}
if ([EnumStatus.PAUSE, EnumStatus.NORMAL].includes(item.status) || isAbnormalStatus(item.status)) {
if ([EnumStatus.PAUSE].includes(item.status) || isAbnormalStatus(item.status)) {
return '重新授权';
}

View File

@ -162,6 +162,8 @@
<AuthorizedAccountModal ref="authorizedAccountModalRef" @update="emits('update')" />
<ImportPromptModal ref="importPromptModalRef" />
</a-modal>
<SyncDataModal ref="syncDataModalRef" />
</template>
<script setup>
@ -171,6 +173,7 @@ import GroupSelect from '@/views/property-marketing/media-account/components/gro
import AuthorizedAccountModal from '../authorized-account-modal';
import ImportPromptModal from '../import-prompt-modal';
import StatusBox from '../status-box';
import SyncDataModal from '../sync-data-modal';
import {
fetchAccountTags,
@ -220,6 +223,7 @@ const importPromptModalRef = ref(null);
const uploadRef = ref(null);
const isCustomCookie = ref(false);
const form = ref(cloneDeep(INITIAL_FORM));
const syncDataModalRef = ref(null);
const rules = {
mobile: [
@ -245,8 +249,13 @@ const rules = {
const isBatchImport = computed(() => uploadType.value === 'batch');
const confirmBtnText = computed(() => {
if (isBatchImport.value) return '确定导入';
return isEdit.value ? '确定' : '生成授权码';
if (isBatchImport.value) {
return '确定导入';
} else if (isEdit.value) {
return '确定';
} else {
return isCustomCookie.value ? '确认添加' : '生成授权码';
}
});
// 获取分组数据
@ -354,7 +363,11 @@ const handleAddAccount = async () => {
onClose();
const { id, platform } = data;
!_isCustomCookie && startAuthorized(id, platform);
if (_isCustomCookie) {
syncDataModalRef.value.open(id);
} else {
startAuthorized(id, platform);
}
}
};
@ -375,6 +388,11 @@ async function onSubmit() {
formRef.value.validate(async (errors) => {
if (!errors) {
if (isCustomCookie.value && !form.value.cookie) {
AMessage.warning('请填写Cookie值');
return;
}
isEdit.value ? handleEditAccount() : handleAddAccount();
}
});

View File

@ -28,7 +28,7 @@
<template v-else>
<!-- 完成状态 -->
<template v-if="modalState === MODAL_STATE.SUCCESS || modalState === MODAL_STATE.FAILED">
<template v-if="[MODAL_STATE.SUCCESS, MODAL_STATE.FAILED].includes(modalState)">
<img :src="modalState === MODAL_STATE.SUCCESS ? icon2 : icon3" width="80" height="80" class="mb-16px" />
<p class="s2">{{ `数据初始化${modalState === MODAL_STATE.SUCCESS ? '成功' : '失败'}` }}</p>
<p v-if="modalState === MODAL_STATE.FAILED" class="red-text">失败原因{{ failReason || '-' }}</p>
@ -61,10 +61,8 @@
></div>
</div>
</template>
<!-- 正常二维码 -->
<a-image v-else :src="qrCodeUrl" width="160" height="160" />
<!-- 二维码失效遮罩 -->
<div v-if="modalState === MODAL_STATE.QR_EXPIRED" class="mask cursor-pointer" @click="handleRefreshQrCode">
<icon-refresh size="24" class="mb-13px" />
@ -82,7 +80,7 @@
重新生成
</a-button>
<a-button
v-if="modalState === MODAL_STATE.SUCCESS || modalState === MODAL_STATE.FAILED"
v-if="[MODAL_STATE.SUCCESS, MODAL_STATE.FAILED].includes(modalState)"
size="large"
class="cancel-btn"
@click="close"
@ -94,12 +92,15 @@
</a-button>
</template>
</a-modal>
<SyncDataModal ref="syncDataModalRef" />
</template>
<script setup>
import { defineExpose, ref, computed } from 'vue';
import { Message as AMessage } from '@arco-design/web-vue';
import { getAuthorizedImage, getMediaAccountsAuthorizedStatus } from '@/api/all/propertyMarketing';
import SyncDataModal from '../sync-data-modal';
import icon1 from '@/assets/img/media-account/icon-default-qrcode.png';
import icon2 from '@/assets/img/media-account/icon-feedback-success.png';
@ -127,6 +128,7 @@ const progress = ref(0);
const id = ref('');
const platform = ref('');
const qrCodeUrl = ref('');
const syncDataModalRef = ref(null);
let progressTimer = null;
let overdueTimer = null;
@ -140,7 +142,7 @@ const confirmBtnText = computed(() => {
[MODAL_STATE.QR_READY]: '完成扫码',
[MODAL_STATE.QR_EXPIRED]: '重新生成',
[MODAL_STATE.LOADING]: '处理中...',
[MODAL_STATE.SUCCESS]: '继续添加',
[MODAL_STATE.SUCCESS]: '确认添加',
[MODAL_STATE.FAILED]: '重新扫码',
};
return btnTextMap[modalState.value] || '确定';
@ -299,6 +301,7 @@ const handleOk = () => {
// 授权成功,关闭弹窗
if (modalState.value === MODAL_STATE.SUCCESS) {
syncDataModalRef.value.open(id.value);
close();
return;
}

View File

@ -108,12 +108,15 @@
</a-button>
</template>
</a-modal>
<SyncDataModal ref="syncDataModalRef" />
</template>
<script setup>
import { defineExpose, ref, computed } from 'vue';
import { Message as AMessage } from '@arco-design/web-vue';
import { getMediaAccountsAuthorizedStatus, getAuthorizedImage } from '@/api/all/propertyMarketing';
import SyncDataModal from '../sync-data-modal';
import icon1 from '@/assets/img/media-account/icon-default-qrcode.png';
import icon2 from '@/assets/img/media-account/icon-feedback-success.png';
@ -142,6 +145,7 @@ const id = ref('');
const platform = ref('');
const qrCodeUrl = ref('');
const isNicknameChanged = ref(false); // 昵称发生变化
const syncDataModalRef = ref(null);
let progressTimer = null;
let overdueTimer = null;
@ -155,7 +159,7 @@ const confirmBtnText = computed(() => {
[MODAL_STATE.QR_READY]: '完成扫码',
[MODAL_STATE.QR_EXPIRED]: '重新生成',
[MODAL_STATE.LOADING]: '处理中...',
[MODAL_STATE.SUCCESS]: isNicknameChanged.value ? '确定覆盖' : '继续添加',
[MODAL_STATE.SUCCESS]: isNicknameChanged.value ? '确定覆盖' : '确认添加',
[MODAL_STATE.FAILED]: '重新扫码',
};
return btnTextMap[modalState.value] || '确定';
@ -318,6 +322,7 @@ const handleOk = () => {
console.log('确定覆盖');
// 这里可以添加覆盖逻辑
} else {
syncDataModalRef.value.open(id.value);
close();
}
return;

View File

@ -0,0 +1,58 @@
<!--
* @Author: RenXiaoDong
* @Date: 2025-06-25 17:51:46
-->
<template>
<a-modal
v-model:visible="visible"
width="480px"
title="更新数据"
modal-class="sync-data-modal"
:mask-closable="false"
@close="close"
>
<div class="flex flex-col items-center">
<div class="flex items-center">
<img :src="icon1" width="20" height="20" class="mr-12px" />
<div class="flex flex-col">
<p class="tip">为确保数据准确建议立即同步账号数据您也可以在账号管理列表中手动触发更新</p>
</div>
</div>
</div>
<template #footer>
<a-button size="large" class="cancel-btn" @click="close">稍后再说</a-button>
<a-button type="primary" size="large" @click="handleOk"> 更新数据 </a-button>
</template>
</a-modal>
</template>
<script setup>
import { defineExpose } from 'vue';
import icon1 from '@/assets/img/media-account/icon-warn-1.png';
const handleSyncData = inject('handleSyncData');
const id = ref('');
const visible = ref(false);
const open = (accountId) => {
id.value = accountId;
visible.value = true;
};
const close = () => {
id.value = '';
visible.value = false;
};
const handleOk = () => {
handleSyncData({ id: id.value });
close();
};
defineExpose({
open,
});
</script>
<style lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,14 @@
@import "@/views/property-marketing/component.scss";
.sync-data-modal {
border-radius: 8px;
.arco-modal-body {
.tip {
color: var(--Text-1, #211f24);
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
}
}
}

View File

@ -51,6 +51,9 @@
个账号
</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>
@ -111,7 +114,7 @@
</template>
<script setup>
import { ref } from 'vue';
import { ref, provide } from 'vue';
import FilterBlock from './components/filter-block';
import AccountTable from './components/account-table';
@ -121,9 +124,17 @@ 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 { getMediaAccounts, getMediaAccountsHealth } from '@/api/all/propertyMarketing';
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';
@ -132,6 +143,8 @@ 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);
@ -145,8 +158,10 @@ const query = ref(cloneDeep(INITIAL_QUERY));
const dataSource = ref([]);
const selectedItems = ref([]);
const healthData = ref({});
const startSyncData = 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(
@ -180,10 +195,6 @@ const tipLabel = computed(() => {
return `共有 ${total_abnormal_number} 个账号存在授权异常,其中:${abnormalLabels.join('')}`;
});
onMounted(() => {
getData();
});
const getData = () => {
getHealthData();
getAccountData();
@ -262,10 +273,54 @@ const handleDelete = (item) => {
const handleCloseTip = () => {
selectedItems.value = [];
};
const startSyncDataPolling = () => {
startSyncData.value = true;
clearSyncDataTimer();
syncDataTimer = setInterval(async () => {
const { code, data } = await getMediaAccountSyncStatus();
if (code === 200) {
// 所有任务都结束了,才停止轮询,刷新页面
const isEnd = data.every((item) => item.status !== 0);
if (isEnd) {
clearSyncDataTimer();
startSyncData.value = false;
getData();
}
}
}, 5000);
};
const handleSyncData = async (item) => {
Notification.info({
title: '账号正在同步数据。',
});
const { code } = await postSyncMediaAccountData(item.id);
if (code === 200) {
if (!startSyncData.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 (!startSyncData.value) {
startSyncDataPolling();
}
}
};
const handleBatchGroup = () => {
batchGroupModalRef.value?.open(selectedItems.value);
};
@ -277,6 +332,21 @@ 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">

View File

@ -60,7 +60,7 @@
padding: 0;
cursor: pointer;
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;
@ -71,6 +71,10 @@
&.red {
color: #F64B31;
}
&.disabled {
color: #C5B7FF;
cursor: not-allowed;
}
}
}
.card-wrap {