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

@ -294,7 +294,7 @@ export const getPlacementAccountProjectsTrend = (params = {}) => {
export const getPlacementGuide = (params: {}) => { export const getPlacementGuide = (params: {}) => {
return Http.get(`/v1/placement-account-projects/getGuideList`, params); return Http.get(`/v1/placement-account-projects/getGuideList`, params);
}; };
//查询投放指南历史 // 查询投放指南历史
export const getPlacementGuideHistory = (params: {}) => { export const getPlacementGuideHistory = (params: {}) => {
return Http.get(`/v1/placement-account-projects/getGuideListHistory`, params); return Http.get(`/v1/placement-account-projects/getGuideListHistory`, params);
}; };
@ -312,7 +312,7 @@ export const getPlacementGuideDetail = (id: string) => {
return Http.get(`/v1/placement-account-projects/historylog/${id}`); return Http.get(`/v1/placement-account-projects/historylog/${id}`);
}; };
//删除记录 // 删除记录
export const deleteHistorylog = (id: string) => { export const deleteHistorylog = (id: string) => {
return Http.delete(`/v1/placement-account-projects/historylog/${id}`); return Http.delete(`/v1/placement-account-projects/historylog/${id}`);
}; };
@ -332,4 +332,17 @@ export const postPlacementAccountsSync = (id: string) => {
return Http.post(`/v1/placement-accounts/${id}/sync-data`); 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> </a-button>
<template #content> <template #content>
<a-doption class="color-#211F24" @click="openEdit(item)">编辑</a-doption> <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 v-if="showPauseButton(item.status)" class="color-#211F24" @click="handlePause(item)"
>暂停同步</a-doption >暂停同步</a-doption
> >
@ -113,7 +116,7 @@
</template> </template>
<script setup> <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 { STATUS_LIST, EnumStatus } from '@/views/property-marketing/media-account/components/status-select/constants';
import PauseAccountPatchModal from './pause-account-patch'; import PauseAccountPatchModal from './pause-account-patch';
@ -183,6 +186,9 @@ const handlePause = (item) => {
const showPauseButton = (status) => { const showPauseButton = (status) => {
return ![EnumStatus.PAUSE, EnumStatus.UNAUTHORIZED].includes(status); return ![EnumStatus.PAUSE, EnumStatus.UNAUTHORIZED].includes(status);
}; };
const showSyncDataButton = (status) => {
return [EnumStatus.NORMAL, EnumStatus.ABNORMAL_MISSING].includes(status);
};
const isUnauthorizedStatus = (status) => { const isUnauthorizedStatus = (status) => {
return [EnumStatus.UNAUTHORIZED].includes(status); return [EnumStatus.UNAUTHORIZED].includes(status);
}; };
@ -201,13 +207,19 @@ const getTooltipText = (status) => {
return STATUS_LIST.find((v) => v.value === status)?.tooltip ?? '-'; return STATUS_LIST.find((v) => v.value === status)?.tooltip ?? '-';
}; };
const handleSyncData = inject('handleSyncData');
const onBtnClick = (item) => { const onBtnClick = (item) => {
if (showSyncDataButton(item.status)) {
handleSyncData && handleSyncData(item);
return;
}
if (isUnauthorizedStatus(item.status)) { if (isUnauthorizedStatus(item.status)) {
handleReauthorize(item); handleReauthorize(item);
return; return;
} }
if ([EnumStatus.PAUSE, EnumStatus.NORMAL].includes(item.status) || isAbnormalStatus(item.status)) { if ([EnumStatus.PAUSE].includes(item.status) || isAbnormalStatus(item.status)) {
handleReauthorize(item); handleReauthorize(item);
return; return;
} }
@ -216,11 +228,15 @@ const onBtnClick = (item) => {
}; };
const getBtnText = (item) => { const getBtnText = (item) => {
if (showSyncDataButton(item.status)) {
return '更新数据';
}
if (isUnauthorizedStatus(item.status)) { if (isUnauthorizedStatus(item.status)) {
return '去授权'; return '去授权';
} }
if ([EnumStatus.PAUSE, EnumStatus.NORMAL].includes(item.status) || isAbnormalStatus(item.status)) { if ([EnumStatus.PAUSE].includes(item.status) || isAbnormalStatus(item.status)) {
return '重新授权'; return '重新授权';
} }

View File

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

View File

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

View File

@ -108,12 +108,15 @@
</a-button> </a-button>
</template> </template>
</a-modal> </a-modal>
<SyncDataModal ref="syncDataModalRef" />
</template> </template>
<script setup> <script setup>
import { defineExpose, ref, computed } from 'vue'; import { defineExpose, ref, computed } from 'vue';
import { Message as AMessage } from '@arco-design/web-vue'; import { Message as AMessage } from '@arco-design/web-vue';
import { getMediaAccountsAuthorizedStatus, getAuthorizedImage } from '@/api/all/propertyMarketing'; 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 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';
@ -142,6 +145,7 @@ const id = ref('');
const platform = ref(''); const platform = ref('');
const qrCodeUrl = ref(''); const qrCodeUrl = ref('');
const isNicknameChanged = ref(false); // 昵称发生变化 const isNicknameChanged = ref(false); // 昵称发生变化
const syncDataModalRef = ref(null);
let progressTimer = null; let progressTimer = null;
let overdueTimer = null; let overdueTimer = null;
@ -155,7 +159,7 @@ const confirmBtnText = computed(() => {
[MODAL_STATE.QR_READY]: '完成扫码', [MODAL_STATE.QR_READY]: '完成扫码',
[MODAL_STATE.QR_EXPIRED]: '重新生成', [MODAL_STATE.QR_EXPIRED]: '重新生成',
[MODAL_STATE.LOADING]: '处理中...', [MODAL_STATE.LOADING]: '处理中...',
[MODAL_STATE.SUCCESS]: isNicknameChanged.value ? '确定覆盖' : '继续添加', [MODAL_STATE.SUCCESS]: isNicknameChanged.value ? '确定覆盖' : '确认添加',
[MODAL_STATE.FAILED]: '重新扫码', [MODAL_STATE.FAILED]: '重新扫码',
}; };
return btnTextMap[modalState.value] || '确定'; return btnTextMap[modalState.value] || '确定';
@ -318,6 +322,7 @@ const handleOk = () => {
console.log('确定覆盖'); console.log('确定覆盖');
// 这里可以添加覆盖逻辑 // 这里可以添加覆盖逻辑
} else { } else {
syncDataModalRef.value.open(id.value);
close(); close();
} }
return; 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>
<span class="operation-btn" :class="{ disabled: isDisabledBatchSyncData }" @click="handleBatchSyncData"
>批量更新数据</span
>
<span class="operation-btn" @click="handleBatchTag">批量标签</span> <span class="operation-btn" @click="handleBatchTag">批量标签</span>
<span class="operation-btn" @click="handleBatchGroup">批量分组</span> <span class="operation-btn" @click="handleBatchGroup">批量分组</span>
<span class="operation-btn red" @click="handleBatchDelete"> 批量删除 </span> <span class="operation-btn red" @click="handleBatchDelete"> 批量删除 </span>
@ -111,7 +114,7 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { ref, provide } from 'vue';
import FilterBlock from './components/filter-block'; import FilterBlock from './components/filter-block';
import AccountTable from './components/account-table'; 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 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 { Notification } from '@arco-design/web-vue';
import { INITIAL_QUERY, INITIAL_PAGE_INFO } from './constants'; 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 icon1 from '@/assets/img/media-account/icon-add.png';
import icon2 from '@/assets/img/media-account/icon-group.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 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;
const groupManageModalRef = ref(null); const groupManageModalRef = ref(null);
const tagsManageModalRef = ref(null); const tagsManageModalRef = ref(null);
const addAccountModalRef = ref(null); const addAccountModalRef = ref(null);
@ -145,8 +158,10 @@ 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 startSyncData = ref(false);
const isAbNormalStatus = computed(() => healthData.value?.total_abnormal_number > 0); 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 checkedAll = computed(() => selectedItems.value.length === dataSource.value.length);
const indeterminate = computed( const indeterminate = computed(
@ -180,10 +195,6 @@ const tipLabel = computed(() => {
return `共有 ${total_abnormal_number} 个账号存在授权异常,其中:${abnormalLabels.join('')}`; return `共有 ${total_abnormal_number} 个账号存在授权异常,其中:${abnormalLabels.join('')}`;
}); });
onMounted(() => {
getData();
});
const getData = () => { const getData = () => {
getHealthData(); getHealthData();
getAccountData(); getAccountData();
@ -262,10 +273,54 @@ const handleDelete = (item) => {
const handleCloseTip = () => { const handleCloseTip = () => {
selectedItems.value = []; 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 = () => { const handleBatchTag = () => {
batchTagModalRef.value?.open(selectedItems.value); 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 = () => { const handleBatchGroup = () => {
batchGroupModalRef.value?.open(selectedItems.value); batchGroupModalRef.value?.open(selectedItems.value);
}; };
@ -277,6 +332,21 @@ const handleOpenAbnormalAccount = () => {
query.value.status = 2; query.value.status = 2;
reload(); reload();
}; };
const clearSyncDataTimer = () => {
if (syncDataTimer) {
clearInterval(syncDataTimer);
syncDataTimer = null;
}
};
onMounted(() => {
getData();
});
onUnmounted(() => {
clearSyncDataTimer();
});
provide('handleSyncData', handleSyncData);
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

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