Merge pull request 'feature/v1.2灵机空间-项目管理_rxd' (#13) from feature/v1.2灵机空间-项目管理_rxd into main

Reviewed-on: ai-team/lingji-work-fe#13
This commit is contained in:
2025-07-25 02:29:59 +00:00
106 changed files with 2233 additions and 539 deletions

View File

@ -14,6 +14,10 @@ export const updateCustomColumns = (params = {}) => {
return Http.put('/v1/custom-columns', params); return Http.put('/v1/custom-columns', params);
}; };
export const getUserList = (params = {}) => {
return Http.get('/v1/users/list', params);
};
// 任务中心-分页 // 任务中心-分页
export const getTask = (params = {}) => { export const getTask = (params = {}) => {
return Http.get('/v1/tasks', params); return Http.get('/v1/tasks', params);

View File

@ -24,6 +24,11 @@ export const getMediaAccounts = (params = {}) => {
return Http.get('/v1/media-accounts', params); return Http.get('/v1/media-accounts', params);
}; };
// 媒体账号-列表
export const getMediaAccountList = (params = {}) => {
return Http.get('/v1/media-accounts/list', params);
};
// 媒体账号-健康情况 // 媒体账号-健康情况
export const getMediaAccountsHealth = (params = {}) => { export const getMediaAccountsHealth = (params = {}) => {
return Http.get('/v1/media-accounts/health', params); return Http.get('/v1/media-accounts/health', params);
@ -360,4 +365,40 @@ export const getMediaAccountSyncStatus = (params = {}) => {
// 媒体账号-移除同步状态 // 媒体账号-移除同步状态
export const deleteSyncStatus = (id: string) => { export const deleteSyncStatus = (id: string) => {
return Http.delete(`/v1/media-accounts/${id}/sync-status`); return Http.delete(`/v1/media-accounts/${id}/sync-status`);
}; };
// 内容稿件-列表
export const getWorksList = (params = {}) => {
return Http.get('/v1/works/list', params);
};
// 项目管理-分页
export const getProjects = (params = {}) => {
return Http.get('/v1/projects', params);
};
// 项目管理-列表
export const getProjectList = () => {
return Http.get('/v1/projects/list');
};
// 项目管理-删除
export const deleteProject = (id: string) => {
return Http.delete(`/v1/projects/${id}`);
};
// 项目管理-添加
export const postAddProject = (params: {}) => {
return Http.post('/v1/projects', params);
};
// 项目管理-修改
export const putProject = (params = {}) => {
const { id, ...rest } = params as { id: string; [key: string]: any };
return Http.put(`/v1/projects/${id}`, rest);
};
// 项目管理-详情
export const getProjectDetail = (id: string) => {
return Http.get(`/v1/projects/${id}`);
};

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M5.25065 1.3125C5.4638 1.31254 5.67398 1.36363 5.86328 1.46159L7.86328 2.49674C8.05258 2.5947 8.26278 2.64579 8.47591 2.64583L13.3822 2.64583C14.1185 2.64583 14.7155 3.24282 14.7155 3.97917L14.7155 13.3542C14.7155 14.0905 14.1185 14.6875 13.3822 14.6875L2.61784 14.6875C1.88146 14.6875 1.28451 14.0905 1.28451 13.3542L1.28451 2.64583C1.28455 1.90949 1.88149 1.3125 2.61784 1.3125L5.25065 1.3125ZM4.528 9.99023C4.14147 9.9903 3.82815 10.3036 3.82813 10.6901C3.82813 11.0767 4.14146 11.3899 4.528 11.39C4.91459 11.39 5.22787 11.0767 5.22787 10.6901C5.22784 10.3035 4.91458 9.99023 4.528 9.99023ZM7.153 10.0905C6.82179 10.0907 6.55342 10.3589 6.55339 10.6901C6.55339 11.0214 6.82177 11.2902 7.153 11.2904L11.5384 11.2904C11.8698 11.2904 12.1387 11.0215 12.1387 10.6901C12.1386 10.3588 11.8698 10.0905 11.5384 10.0905L7.153 10.0905ZM4.528 6.50846C4.14155 6.50853 3.82828 6.82191 3.82813 7.20833C3.82813 7.59489 4.14146 7.90878 4.528 7.90885C4.91459 7.90885 5.22787 7.59493 5.22787 7.20833C5.22771 6.82187 4.9145 6.50846 4.528 6.50846ZM7.153 6.60872C6.82187 6.60889 6.55355 6.87721 6.55339 7.20833C6.55339 7.5396 6.82177 7.80843 7.153 7.80859H11.5384C11.8698 7.80859 12.1387 7.5397 12.1387 7.20833C12.1385 6.8771 11.8697 6.60872 11.5384 6.60872H7.153Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -17,7 +17,7 @@
</div> </div>
<p class="m-0 p-0 mb-24px s2 ml-32px">退出登录后你将无法收到该账号的通知</p> <p class="m-0 p-0 mb-24px s2 ml-32px">退出登录后你将无法收到该账号的通知</p>
<div class="flex items-center justify-end"> <div class="flex items-center justify-end">
<a-button class="cancel-btn" size="medium" @click="onClose">返回</a-button> <a-button class="!rounded-4px" size="medium" @click="onClose">返回</a-button>
<a-button type="primary" class="ml-16px danger-btn" status="danger" size="medium" @click="onLogout" <a-button type="primary" class="ml-16px danger-btn" status="danger" size="medium" @click="onLogout"
>退出登录</a-button >退出登录</a-button
> >
@ -83,15 +83,12 @@ defineExpose({ open });
} }
.cancel-btn { .cancel-btn {
border-radius: 4px; border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
&:hover {
border: 1px solid var(--BG-500, #b1b2b5);
}
} }
.danger-btn { .danger-btn {
border-radius: 4px;
background: var(--Functional-Danger-6, #f64b31) !important; background: var(--Functional-Danger-6, #f64b31) !important;
border: none !important; &:hover {
background: var(--Functional-Danger-6, #f64b31) !important;
}
} }
} }
} }

View File

@ -4,7 +4,7 @@
<a-menu-item v-for="item in menuList" :key="String(item.id)"> <a-menu-item v-for="item in menuList" :key="String(item.id)">
<template v-if="item.children"> <template v-if="item.children">
<a-dropdown :popup-max-height="false" class="layout-menu-item-dropdown"> <a-dropdown :popup-max-height="false" class="layout-menu-item-dropdown">
<a-button> <a-button type="text">
<span class="menu-item-text mr-2px"> {{ item.name }}</span> <span class="menu-item-text mr-2px"> {{ item.name }}</span>
<icon-caret-down size="16" class="arco-icon-down !mr-0" /> <icon-caret-down size="16" class="arco-icon-down !mr-0" />
</a-button> </a-button>

View File

@ -10,7 +10,7 @@
<span>确认删除 {{ accountName }} 这条记录吗</span> <span>确认删除 {{ accountName }} 这条记录吗</span>
</div> </div>
<template #footer> <template #footer>
<a-button class="cancel-btn" size="large" @click="onClose">取消</a-button> <a-button size="large" @click="onClose">取消</a-button>
<a-button type="primary" class="ml-16px !bg-#f64b31 !border-none" status="danger" size="large" @click="onDelete" <a-button type="primary" class="ml-16px !bg-#f64b31 !border-none" status="danger" size="large" @click="onDelete"
>确定</a-button >确定</a-button
> >

View File

@ -10,7 +10,7 @@
<span>确认删除 {{ accountName }} 这条记录吗</span> <span>确认删除 {{ accountName }} 这条记录吗</span>
</div> </div>
<template #footer> <template #footer>
<a-button class="cancel-btn" size="large" @click="onClose">取消</a-button> <a-button size="large" @click="onClose">取消</a-button>
<a-button type="primary" class="ml-16px !bg-#f64b31 !border-none" status="danger" size="large" @click="onDelete" <a-button type="primary" class="ml-16px !bg-#f64b31 !border-none" status="danger" size="large" @click="onDelete"
>确定</a-button >确定</a-button
> >

View File

@ -4,12 +4,12 @@
--> -->
<template> <template>
<a-select <a-select
v-model="selectedTags" v-model="selectedValues"
:multiple="multiple" :multiple="multiple"
size="medium" size="medium"
:placeholder="placeholder" :placeholder="placeholder"
allow-clear allow-clear
:max-tag-count="3" :max-tag-count="maxTagCount"
@change="handleChange" @change="handleChange"
> >
<a-option v-for="(item, index) in options" :key="index" :value="item.id" :label="item.name"> <a-option v-for="(item, index) in options" :key="index" :value="item.id" :label="item.name">
@ -38,28 +38,30 @@ const props = defineProps({
type: Array, type: Array,
default: () => [], default: () => [],
}, },
maxTagCount: {
type: Number,
default: 3,
},
}); });
const emits = defineEmits(['update:modelValue', 'change']); const emits = defineEmits(['update:modelValue', 'change']);
const selectedTags = ref(props.multiple ? [] : ''); const selectedValues = ref(props.multiple ? [] : '');
//
watch( watch(
() => props.modelValue, () => props.modelValue,
(newVal) => { (newVal) => {
selectedTags.value = newVal; selectedValues.value = newVal;
}, },
{ immediate: true }, { immediate: true },
); );
// watch(selectedValues, (newVal) => {
watch(selectedTags, (newVal) => {
emits('update:modelValue', newVal); emits('update:modelValue', newVal);
}); });
const handleChange = (value) => { const handleChange = (value) => {
selectedTags.value = value; selectedValues.value = value;
emits('change', value); emits('change', value);
}; };
</script> </script>

View File

@ -65,7 +65,7 @@
<template #footer> <template #footer>
<div style="text-align: right"> <div style="text-align: right">
<a-button class="mr-8px cancel-btn" size="medium" @click="close">取消</a-button> <a-button class="mr-8px" size="medium" @click="close">取消</a-button>
<a-button type="primary" size="medium" @click="onSubmit">确定</a-button> <a-button type="primary" size="medium" @click="onSubmit">确定</a-button>
</div> </div>
</template> </template>

View File

@ -87,13 +87,6 @@
} }
} }
.arco-modal-footer { .arco-modal-footer {
.cancel-btn {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
&:hover {
border: 1px solid var(--BG-500, #b1b2b5);
}
}
} }
} }

View File

@ -12,6 +12,7 @@ import { ref, onMounted } from 'vue';
import { getQueryParam } from '@/utils/helper'; import { getQueryParam } from '@/utils/helper';
import { getEnterpriseByInviteCode, joinEnterpriseByInviteCode } from '@/api/all'; import { getEnterpriseByInviteCode, joinEnterpriseByInviteCode } from '@/api/all';
const enterprise = ref(); const enterprise = ref();
const inviteCode = ref(); const inviteCode = ref();
@ -27,8 +28,11 @@ async function handleJoin() {
await joinEnterpriseByInviteCode(inviteCode.value); await joinEnterpriseByInviteCode(inviteCode.value);
AMessage.success('加入成功'); AMessage.success('加入成功');
} }
onMounted(() => { // onMounted(() => {
getEnterprise(); // getEnterprise();
// });
defineExpose({
getEnterprise,
}); });
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -1,35 +1,29 @@
<template> <template>
<svg <svg aria-hidden="true" class="svg-icon" :width="props.size" :height="props.size">
aria-hidden="true" <use :xlink:href="symbolId" :fill="props.color" />
class="svg-icon" </svg>
:width="props.size" </template>
:height="props.size"
> <script setup>
<use :xlink:href="symbolId" :fill="props.color" /> import { computed } from 'vue';
</svg> const props = defineProps({
</template> prefix: {
type: String,
<script setup> default: 'icon',
import { computed } from "vue"; },
const props = defineProps({ name: {
prefix: { type: String,
type: String, required: true,
default: "icon", },
}, color: {
name: { type: String,
type: String, default: '#333',
required: true, },
}, size: {
color: { type: String,
type: String, default: '12',
default: "#333", },
}, });
size: {
type: String, const symbolId = computed(() => `#${props.prefix}-${props.name}`);
default: "12", </script>
},
});
const symbolId = computed(() => `#${props.prefix}-${props.name}`);
</script>

View File

@ -9,6 +9,7 @@ interface UseTableSelectionWithPaginationOptions {
}; };
onPageChange?: (page: number) => void; onPageChange?: (page: number) => void;
onPageSizeChange?: (size: number) => void; onPageSizeChange?: (size: number) => void;
onSelectChange?: () => void;
} }
const DEFAULT_PAGE_INFO = { const DEFAULT_PAGE_INFO = {
@ -39,6 +40,7 @@ export function useTableSelectionWithPagination(options: UseTableSelectionWithPa
} else { } else {
selectedRows.value = selectedRows.value.filter((v) => v[rowKey] !== record[rowKey]); selectedRows.value = selectedRows.value.filter((v) => v[rowKey] !== record[rowKey]);
} }
options.onSelectChange?.();
}; };
// 全选/取消全选 // 全选/取消全选
@ -56,6 +58,7 @@ export function useTableSelectionWithPagination(options: UseTableSelectionWithPa
selectedRowKeys.value = selectedRowKeys.value.filter((key) => !currentPageKeys.includes(key)); selectedRowKeys.value = selectedRowKeys.value.filter((key) => !currentPageKeys.includes(key));
selectedRows.value = selectedRows.value.filter((row) => !currentPageKeys.includes(row[rowKey])); selectedRows.value = selectedRows.value.filter((row) => !currentPageKeys.includes(row[rowKey]));
} }
options.onSelectChange?.();
}; };
const onPageChange = (page: number) => { const onPageChange = (page: number) => {
@ -67,6 +70,9 @@ export function useTableSelectionWithPagination(options: UseTableSelectionWithPa
pageInfo.value.page = 1; pageInfo.value.page = 1;
options.onPageSizeChange?.(size); options.onPageSizeChange?.(size);
}; };
const resetPageInfo = () => {
pageInfo.value = cloneDeep(DEFAULT_PAGE_INFO)
}
const rowSelection = computed(() => ({ const rowSelection = computed(() => ({
type: 'checkbox', type: 'checkbox',
@ -84,6 +90,7 @@ export function useTableSelectionWithPagination(options: UseTableSelectionWithPa
rowSelection, rowSelection,
handleSelect, handleSelect,
handleSelectAll, handleSelectAll,
resetPageInfo,
DEFAULT_PAGE_INFO, DEFAULT_PAGE_INFO,
}; };
} }

View File

@ -1,13 +1,17 @@
<script setup lang="ts"> <script setup>
import { useAppStore } from '@/stores'; import { useAppStore } from '@/stores';
import { useResponsive } from '@/hooks'; import { useResponsive } from '@/hooks';
import JoinModal from '@/components/join-modal.vue'; import JoinModal from '@/components/join-modal.vue';
import { getQueryParam } from '@/utils/helper'; import { getQueryParam } from '@/utils/helper';
import { useUserStore } from '@/stores';
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
const joinEnterpriseVisible = ref(false); const joinEnterpriseVisible = ref(false);
const joinModalRef = ref(null);
const appStore = useAppStore(); const appStore = useAppStore();
const userStore = useUserStore();
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
@ -36,14 +40,15 @@ const paddingStyle = computed(() => {
onMounted(() => { onMounted(() => {
checkHasInviteCode(); checkHasInviteCode();
}); });
const setCollapsed = (val: boolean) => { const setCollapsed = (val) => {
appStore.updateSettings({ menuCollapse: val }); appStore.updateSettings({ menuCollapse: val });
}; };
const checkHasInviteCode = () => { const checkHasInviteCode = () => {
const inviteCode = getQueryParam('invite_code'); const inviteCode = getQueryParam('invite_code');
if (inviteCode) { if (userStore.isLogin && inviteCode) {
joinEnterpriseVisible.value = true; joinEnterpriseVisible.value = true;
joinModalRef.value?.getEnterprise?.();
} }
}; };
const drawerVisible = ref(false); const drawerVisible = ref(false);
@ -57,7 +62,7 @@ provide('toggleDrawerMenu', () => {
<template> <template>
<a-layout :class="['layout', { mobile: appStore.hideMenu }]"> <a-layout :class="['layout', { mobile: appStore.hideMenu }]">
<JoinModal v-model:visible="joinEnterpriseVisible" /> <JoinModal v-model:visible="joinEnterpriseVisible" ref="joinModalRef" />
<div v-if="navbar" class="layout-navbar"> <div v-if="navbar" class="layout-navbar">
<base-navbar /> <base-navbar />
</div> </div>

View File

@ -18,9 +18,10 @@ export default function setupUserLoginInfoGuard(router: Router) {
const routeName = to?.name as string; const routeName = to?.name as string;
const requiresAuth = to?.meta?.requiresAuth || false; const requiresAuth = to?.meta?.requiresAuth || false;
const requireLogin = to?.meta?.requireLogin || false; const requireLogin = to?.meta?.requireLogin || false;
const query = to?.query ?? {};
if (requireLogin && !userStore.isLogin) { if (requireLogin && !userStore.isLogin) {
goUserLogin(); goUserLogin(query);
next(); next();
return; return;
} }

View File

@ -9,6 +9,7 @@ import IconRepository from '@/assets/svg/svg-repository.svg';
import IconMediaAccount from '@/assets/svg/svg-mediaAccount.svg'; import IconMediaAccount from '@/assets/svg/svg-mediaAccount.svg';
import IconPutAccount from '@/assets/svg/svg-putAccount.svg'; import IconPutAccount from '@/assets/svg/svg-putAccount.svg';
import IconIntelligentSolution from '@/assets/svg/svg-intelligentSolution.svg'; import IconIntelligentSolution from '@/assets/svg/svg-intelligentSolution.svg';
import IconProjectManagement from '@/assets/svg/svg-projectManagement.svg';
const COMPONENTS: AppRouteRecordRaw[] = [ const COMPONENTS: AppRouteRecordRaw[] = [
{ {
@ -195,6 +196,32 @@ const COMPONENTS: AppRouteRecordRaw[] = [
// }, // },
// ], // ],
// }, // },
{
path: '/project-manage',
name: 'ProjectManagement',
redirect: 'project-manage/project-list',
meta: {
locale: '项目管理',
icon: IconProjectManagement,
requiresAuth: true,
requireLogin: true,
roles: ['*'],
id: MENU_GROUP_IDS.PROPERTY_ID,
},
children: [
{
path: 'project-list',
name: 'ProjectList',
meta: {
locale: '项目列表',
requiresAuth: true,
requireLogin: true,
roles: ['*'],
},
component: () => import('@/views/property-marketing/project-manage/project-list'),
},
],
},
]; ];
export default COMPONENTS; export default COMPONENTS;

View File

@ -85,6 +85,13 @@ export const MENU_LIST = [
// 'IntelligentSolutionCompetitiveProductAnalysisReport', // 'IntelligentSolutionCompetitiveProductAnalysisReport',
// ], // ],
// }, // },
{
name: '项目管理',
routeName: 'ProjectList',
includeRouteNames: [
'ProjectList',
],
},
], ],
}, },
]; ];

View File

@ -0,0 +1,183 @@
.arco-btn {
border-radius: 2px;
border: 1px solid #d7d7d9 !important;
color: #3c4043 !important;
font-family: $font-family-regular;
font-size: 14px !important;
font-style: normal;
font-weight: 400 !important;
line-height: 22px !important;
&.arco-btn-disabled {
border-color: #f2f3f5 !important;
color: #b1b2b5 !important;
}
&:not(.arco-btn-disabled) {
&:hover {
border-color: #e6e6e8 !important;
color: #737478 !important;
}
}
}
.arco-btn-primary {
background-color: $color-primary !important;
border: none !important;
color: #fff !important;
&.arco-btn-disabled {
color: #fff !important;
background-color: $color-primary-3 !important;
}
&:not(.arco-btn-disabled) {
&:hover {
color: #fff !important;
background-color: $color-primary-5 !important;
}
}
//success
&.arco-btn-status-success {
background-color: $color-success !important;
&.arco-btn-disabled {
background-color: $color-success-3 !important;
}
&:not(.arco-btn-disabled) {
&:hover {
background-color: $color-success-5 !important;
}
}
}
// danger
&.arco-btn-status-danger {
background-color: $color-error !important;
&.arco-btn-disabled {
background-color: $color-error-3 !important;
}
&:not(.arco-btn-disabled) {
&:hover {
background-color: $color-error-5 !important;
}
}
}
// warning
&.arco-btn-status-warning {
background-color: $color-warning !important;
&.arco-btn-disabled {
background-color: $color-warning-3 !important;
}
&:not(.arco-btn-disabled) {
&:hover {
background-color: $color-warning-5 !important;
}
}
}
}
.arco-btn-outline {
border: 1px solid $color-primary !important;
color: $color-primary !important;
&.arco-btn-disabled {
border-color: $color-primary-3 !important;
color: $color-primary-3 !important;
}
&:not(.arco-btn-disabled) {
&:hover {
border-color: $color-primary-5 !important;
color: $color-primary-5 !important;
}
}
&.arco-btn-status-success {
border: 1px solid $color-success !important;
color: $color-success !important;
&.arco-btn-disabled {
border-color: $color-success-3 !important;
color: $color-success-3 !important;
}
&:not(.arco-btn-disabled) {
&:hover {
border-color: $color-success-5 !important;
color: $color-success-5 !important;
}
}
}
&.arco-btn-status-danger {
border: 1px solid $color-error !important;
color: $color-error !important;
&.arco-btn-disabled {
border-color: $color-error-3 !important;
color: $color-error-3 !important;
}
&:not(.arco-btn-disabled) {
&:hover {
border-color: $color-error-5 !important;
color: $color-error-5 !important;
}
}
}
&.arco-btn-status-warning {
border: 1px solid $color-warning !important;
color: $color-warning !important;
&.arco-btn-disabled {
border-color: $color-warning-3 !important;
color: $color-warning-3 !important;
}
&:not(.arco-btn-disabled) {
&:hover {
border-color: $color-warning-5 !important;
color: $color-warning-5 !important;
}
}
}
}
.arco-btn-text {
background-color: transparent !important;
border: none !important;
color: $color-primary !important;
&.arco-btn-disabled {
color: $color-primary-2 !important;
}
&:not(.arco-btn-disabled) {
&:hover {
color: $color-primary-5 !important;
}
}
&.arco-btn-status-success {
color: $color-success !important;
&.arco-btn-disabled {
color: $color-success-2 !important;
}
&:not(.arco-btn-disabled) {
&:hover {
color: $color-success-5 !important;
}
}
}
&.arco-btn-status-danger {
color: $color-error !important;
&.arco-btn-disabled {
color: $color-error-2 !important;
}
&:not(.arco-btn-disabled) {
&:hover {
color: $color-error-5 !important;
}
}
}
&.arco-btn-status-warning {
color: $color-warning !important;
&.arco-btn-disabled {
color: $color-warning-2 !important;
}
&:not(.arco-btn-disabled) {
&:hover {
color: $color-warning-5 !important;
}
}
}
}

View File

@ -0,0 +1,22 @@
.arco-form {
.arco-form-item {
margin-bottom: 16px !important;
.arco-form-item-label-col {
padding-right: 12px !important;
.arco-form-item-label {
color: #211f24;
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
.arco-form-item-label-required-symbol {
color: #f64b31;
margin-right: 4px;
font-size: 14px;
font-family: $font-family-regular;
}
}
}
}
}

View File

@ -6,4 +6,7 @@
@import './modal.scss'; @import './modal.scss';
@import "./textarea.scss"; @import "./textarea.scss";
@import "./select.scss"; @import "./select.scss";
@import "./date-picker.scss" @import "./date-picker.scss";
@import "./button.scss";
@import "./steps.scss";
@import "./form.scss";

View File

@ -0,0 +1,47 @@
.arco-steps {
.arco-steps-item {
.arco-steps-item-node {
.arco-steps-icon {
width: 28px;
height: 28px;
border-radius: 32px;
color: #3c4043;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 24px;
font-family: $font-family-manrope-medium;
background-color: #f2f3f5;
}
}
.arco-steps-item-content {
.arco-steps-item-title {
color: #3c4043;
font-family: $font-family-regular;
font-size: 16px;
font-style: normal;
font-weight: 400;
&::after {
background-color: #e6e6e8 !important;
}
}
}
&-active,
&-finish {
.arco-steps-item-node {
.arco-steps-icon {
color: #fff;
background-color: #6d4cfe;
}
}
.arco-steps-item-content {
.arco-steps-item-title {
font-family: $font-family-medium;
&::after {
background-color: #6d4cfe !important;
}
}
}
}
}
}

View File

@ -2,9 +2,9 @@
* @Author: RenXiaoDong * @Author: RenXiaoDong
* @Date: 2025-06-30 16:03:42 * @Date: 2025-06-30 16:03:42
*/ */
@import './font.scss';
@import './lib/variable.scss'; @import './lib/variable.scss';
@import './lib/reset.scss'; @import './lib/reset.scss';
@import './vars.scss'; @import './vars.scss';
@import './components/index.scss'; @import './components/index.scss';
// @import './font.scss';

View File

@ -11,7 +11,7 @@ html,
body { body {
background: $color-background; background: $color-background;
font-family: $font-family-regular; font-family: $font-family-regular;
font-size: $font-size-14; font-size: 14px;
-webkit-print-color-adjust: exact; -webkit-print-color-adjust: exact;
} }

View File

@ -4,6 +4,7 @@ $font-family-regular: 'PingFangSC-Regular', 'Microsoft Yahei', Arial, sans-serif
$font-family-medium: 'PingFangSC-Medium', 'Microsoft Yahei', Arial, sans-serif; $font-family-medium: 'PingFangSC-Medium', 'Microsoft Yahei', Arial, sans-serif;
$font-family-light: 'PingFangSC-Light', 'Microsoft Yahei', Arial, sans-serif; $font-family-light: 'PingFangSC-Light', 'Microsoft Yahei', Arial, sans-serif;
$font-family-bold: 'PingFangSC-Semibold', 'PingFangSC-Regular', 'Microsoft Yahei', Arial, sans-serif; $font-family-bold: 'PingFangSC-Semibold', 'PingFangSC-Regular', 'Microsoft Yahei', Arial, sans-serif;
// 数字字体包 // 数字字体包
$font-family-manrope-regular: 'Manrope-Regular'; $font-family-manrope-regular: 'Manrope-Regular';
$font-family-manrope-medium: 'Manrope-Medium'; $font-family-manrope-medium: 'Manrope-Medium';
@ -12,4 +13,44 @@ $font-family-manrope-semiBold: 'Manrope-SemiBold';
$color-background: #f9f9f9; $color-background: #f9f9f9;
$font-size-14: 14px; $color-primary: #6d4cfe; // 常规
$color-primary-5: #8A70FE; // hover
$color-primary-7: #573DCB; // click
$color-primary-3: #A794FE; // disabled
$color-primary-2: #C5B7FF; // text disabled
$color-primary-1: #F0EDFF; // 浅色
$color-success: #25C883;
$color-success-5: #57CF9C;
$color-success-7: #1BAE71;
$color-success-3: #81DBB5;
$color-success-2: #ABE7CE;
$color-success-1: #EBF7F2;
$color-warning: #FFAE00;
$color-warning-5: #FFBE33;
$color-warning-7: #CC8B00;
$color-warning-3: #FFCF66;
$color-warning-2: #FFDF99;
$color-warning-1: #FFF7E5;
$color-error: #F64B31;
$color-error-5: #F86F5A;
$color-error-7: #C53C27;
$color-error-3: #FA9383;
$color-error-2: #FBB7AD;
$color-error-1: #FFE9E7;
$color-blue: #2A59F3;
$color-blue-5: #557AF6;
$color-blue-7: #2247C2;
$color-blue-3: #7F9CF8;
$color-blue-2: #AABDFA;
$color-blue-1: #E5ECFF;
$color-teal: #39C6E9;
$color-teal-5: #60D2ED;
$color-teal-7: #2E9EBA;
$color-teal-3: #88DDF2;
$color-teal-2: #B0E8F6;
$color-teal-1: #E1F9FF;

View File

@ -0,0 +1,14 @@
@mixin multi-ellipsis($lines) {
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: $lines;
/* autoprefixer: ignore next */
-webkit-box-orient: vertical;
}
@mixin ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@ -0,0 +1 @@
@import "./ellipsis.scss"

55
src/utils/platform.ts Normal file
View File

@ -0,0 +1,55 @@
import icon1 from '@/assets/img/media-account/icon-jl.png';
import icon2 from '@/assets/img/media-account/icon-jg.png';
import icon3 from '@/assets/img/media-account/icon-bili.png';
import icon4 from '@/assets/img/media-account/icon-dy.png';
import icon5 from '@/assets/img/media-account/icon-xhs.png';
// 投放账户
export enum ENUM_PUT_ACCOUNT_PLATFORM {
jl = 0,
jg = 1,
bili = 2,
};
// 新媒体账号
export enum ENUM_MEDIA_ACCOUNT_PLATFORM {
dy = 0,
xhs = 1,
}
export const PLATFORM_LIST = [
{
label: '巨量',
value: ENUM_PUT_ACCOUNT_PLATFORM.jl,
icon: icon1,
},
{
label: '聚光',
value: ENUM_PUT_ACCOUNT_PLATFORM.jg,
icon: icon2,
},
{
label: 'B站',
value: ENUM_PUT_ACCOUNT_PLATFORM.bili,
icon: icon3,
},
];
export const MEDIA_ACCOUNT_PLATFORMS = [
{
label: '抖音',
value: 0,
icon: icon4,
},
{
label: '小红书',
value: 1,
icon: icon5,
},
];
export const getPutAccountPlatformLogo = (value: ENUM_PUT_ACCOUNT_PLATFORM) => {
return PLATFORM_LIST.find((v) => v.value === value)?.icon ?? null;
};
export const getMediaAccountPlatformLogo = (value: ENUM_MEDIA_ACCOUNT_PLATFORM) => {
return MEDIA_ACCOUNT_PLATFORMS.find((v) => v.value === value)?.icon ?? null;
};

View File

@ -85,7 +85,7 @@ export function formatTableField(fieldItem: any, rowValue: any, showExactValue =
return `${fieldItem.prefix || ''}${value}${fieldItem.suffix || ''}`; return `${fieldItem.prefix || ''}${value}${fieldItem.suffix || ''}`;
} }
export function exactFormatTime(val: number, curYearFmt = 'MM-DD HH:mm', otherYearFmt = 'YYYY-MM-DD HH:mm') { export function exactFormatTime(val: number, curYearFmt = 'MM-DD HH:mm:ss', otherYearFmt = 'YYYY-MM-DD HH:mm:ss') {
if (!val) return '-'; if (!val) return '-';
const year = dayjs(val * 1000).year(); const year = dayjs(val * 1000).year();
const currYear = dayjs().year(); const currYear = dayjs().year();

View File

@ -133,7 +133,7 @@
</a-space> </a-space>
</div> </div>
<template #footer> <template #footer>
<a-button size="large" class="cancel-btn" @click="handleCancel">取消</a-button> <a-button size="large" @click="handleCancel">取消</a-button>
<a-button type="primary" size="large" class="rounded-4px" @click="handleOk"> 确定 </a-button> <a-button type="primary" size="large" class="rounded-4px" @click="handleOk"> 确定 </a-button>
</template> </template>
</a-modal> </a-modal>
@ -397,14 +397,6 @@ const handleOk = () => {
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
border-top: 1px solid var(--Border-1, #d7d7d9); border-top: 1px solid var(--Border-1, #d7d7d9);
.cancel-btn {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
&:hover {
border: 1px solid var(--BG-500, #b1b2b5);
}
}
} }
} }
</style> </style>

View File

@ -260,7 +260,7 @@
</a-space> </a-space>
</div> </div>
<template #footer> <template #footer>
<a-button size="large" class="cancel-btn" @click="handleCancel">取消</a-button> <a-button size="large" @click="handleCancel">取消</a-button>
<a-button type="primary" size="large" class="rounded-4px" @click="handleOk"> 确定 </a-button> <a-button type="primary" size="large" class="rounded-4px" @click="handleOk"> 确定 </a-button>
</template> </template>
</a-modal> </a-modal>
@ -713,14 +713,6 @@ onMounted(() => {
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
border-top: 1px solid var(--Border-1, #d7d7d9); border-top: 1px solid var(--Border-1, #d7d7d9);
.cancel-btn {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
&:hover {
border: 1px solid var(--BG-500, #b1b2b5);
}
}
} }
} }
</style> </style>

View File

@ -110,7 +110,7 @@
</a-space> </a-space>
</div> </div>
<template #footer> <template #footer>
<a-button size="large" class="cancel-btn" @click="handleCancel">取消</a-button> <a-button size="large" @click="handleCancel">取消</a-button>
<a-button type="primary" size="large" class="rounded-4px" @click="handleOk"> 确定 </a-button> <a-button type="primary" size="large" class="rounded-4px" @click="handleOk"> 确定 </a-button>
</template> </template>
</a-modal> </a-modal>
@ -318,14 +318,6 @@ const search = () => {
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
border-top: 1px solid var(--Border-1, #d7d7d9); border-top: 1px solid var(--Border-1, #d7d7d9);
.cancel-btn {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
&:hover {
border: 1px solid var(--BG-500, #b1b2b5);
}
}
} }
} }
</style> </style>

View File

@ -54,16 +54,15 @@
</div> </div>
</a-form-item> </a-form-item>
<a-form-item hide-label class="mt-68px mb-16px"> <a-form-item hide-label class="mt-68px mb-16px">
<div <a-button
type="primary" type="primary"
class="w-480 h-48 text-16px rounded-8px text-center text-white leading-48px" class="w-480 h-48 !text-16px !rounded-8px"
:class="disabledSubmitBtn ? 'cursor-no-drop' : 'cursor-pointer'" :class="disabledSubmitBtn ? 'cursor-no-drop' : 'cursor-pointer'"
:style="{ backgroundColor: disabledSubmitBtn ? '#C5B7FF' : '#6D4CFE' }"
:disabled="disabledSubmitBtn" :disabled="disabledSubmitBtn"
@click="handleSubmit" @click="handleSubmit"
> >
{{ isLogin ? '登录' : '注册并开通企业账号' }} {{ isLogin ? '登录' : '注册并开通企业账号' }}
</div> </a-button>
</a-form-item> </a-form-item>
</a-form> </a-form>
<a-space class="text-12px color-#737478 justify-start items-center"> <a-space class="text-12px color-#737478 justify-start items-center">
@ -126,13 +125,16 @@
<script setup lang="ts"> <script setup lang="ts">
import PuzzleVerification from './components/PuzzleVerification.vue'; import PuzzleVerification from './components/PuzzleVerification.vue';
import { fetchLoginCaptCha, fetchAuthorizationsCaptcha, fetchProfileInfo } from '@/api/all/login'; import { fetchLoginCaptCha, fetchAuthorizationsCaptcha, fetchProfileInfo } from '@/api/all/login';
import { joinEnterpriseByInviteCode } from '@/api/all';
import { ref, reactive, onUnmounted, computed } from 'vue'; import { ref, reactive, onUnmounted, computed } from 'vue';
import { useUserStore } from '@/stores'; import { useUserStore } from '@/stores';
import { useEnterpriseStore } from '@/stores/modules/enterprise'; import { useEnterpriseStore } from '@/stores/modules/enterprise';
import { handleUserLogin } from '@/utils/user'; import { handleUserLogin } from '@/utils/user';
import router from '@/router'; import router from '@/router';
import { useRoute } from 'vue-router';
const formRef = ref(); const formRef = ref();
const route = useRoute();
const userStore = useUserStore(); const userStore = useUserStore();
const enterpriseStore = useEnterpriseStore(); const enterpriseStore = useEnterpriseStore();
const countdown = ref(0); const countdown = ref(0);
@ -145,7 +147,6 @@ const submitting = ref(false);
const hasCheck = ref(false); const hasCheck = ref(false);
const mobileNumber = ref(''); const mobileNumber = ref('');
const selectedAccountIndex = ref(0); const selectedAccountIndex = ref(0);
const accounts = ref([]); const accounts = ref([]);
const loginForm = reactive({ const loginForm = reactive({
@ -300,6 +301,15 @@ const handleSubmit = async () => {
// 处理登录成功逻辑 // 处理登录成功逻辑
AMessage.success(isLogin.value ? '登录成功' : '注册成功'); AMessage.success(isLogin.value ? '登录成功' : '注册成功');
userStore.setToken(data.access_token); userStore.setToken(data.access_token);
const { invite_code } = route.query;
if (invite_code) {
const { code } = await joinEnterpriseByInviteCode(invite_code as string);
if (code === 200) {
AMessage.success('加入企业成功');
}
}
getProfileInfo(); getProfileInfo();
} }
} catch (error) { } catch (error) {

View File

@ -21,13 +21,13 @@
</a-space> </a-space>
</div> </div>
<div class="filter-row flex"> <div class="filter-row flex">
<a-button class="w-84px search-btn mr-12px" size="medium" @click="handleSearch"> <a-button type="outline" class="mr-12px" size="medium" @click="handleSearch">
<template #icon> <template #icon>
<icon-search /> <icon-search />
</template> </template>
<template #default>搜索</template> <template #default>搜索</template>
</a-button> </a-button>
<a-button class="w-84px reset-btn" size="medium" @click="handleReset"> <a-button size="medium" @click="handleReset">
<template #icon> <template #icon>
<icon-refresh /> <icon-refresh />
</template> </template>

View File

@ -3,23 +3,11 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
:deep(.search-btn) {
border-radius: 4px;
border: 1px solid var(--Brand-Brand-6, #6d4cfe);
color: #6d4cfe;
}
:deep(.edit-btn) { :deep(.edit-btn) {
border: 1px solid var(--Brand-Brand-6, #6d4cfe); border: 1px solid var(--Brand-Brand-6, #6d4cfe);
color: #6d4cfe; color: #6d4cfe;
} }
:deep(.reset-btn) {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
}
.table-wrap { .table-wrap {
width: 100%; width: 100%;

View File

@ -1,13 +1,4 @@
.arco-modal { .arco-modal {
.cancel-btn {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
&:hover {
border: 1px solid var(--BG-500, #b1b2b5);
}
}
.arco-modal-body { .arco-modal-body {
.arco-form-item { .arco-form-item {
margin-bottom: 16px; margin-bottom: 16px;

View File

@ -25,13 +25,13 @@
</div> </div>
<div class="filter-row flex"> <div class="filter-row flex">
<a-button class="w-84px search-btn mr-12px" size="medium"> <a-button type="outline" class="mr-12px" size="medium">
<template #icon> <template #icon>
<icon-search /> <icon-search />
</template> </template>
<template #default>搜索</template> <template #default>搜索</template>
</a-button> </a-button>
<a-button class="w-84px reset-btn" size="medium"> <a-button size="medium">
<template #icon> <template #icon>
<icon-refresh /> <icon-refresh />
</template> </template>

View File

@ -25,13 +25,13 @@
</div> </div>
<div class="filter-row flex"> <div class="filter-row flex">
<a-button class="w-84px search-btn mr-12px" size="medium"> <a-button type="outline" class="mr-12px" size="medium">
<template #icon> <template #icon>
<icon-search /> <icon-search />
</template> </template>
<template #default>搜索</template> <template #default>搜索</template>
</a-button> </a-button>
<a-button class="w-84px reset-btn" size="medium"> <a-button size="medium">
<template #icon> <template #icon>
<icon-refresh /> <icon-refresh />
</template> </template>

View File

@ -15,11 +15,11 @@
> >
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<a-button class="w-110px search-btn mr-12px" size="medium" @click="handleExport"> <a-button type="outline" class="w-110px mr-12px" size="medium" @click="handleExport">
<template #icon> <icon-download /> </template> <template #icon> <icon-download /> </template>
<template #default>导出数据</template> <template #default>导出数据</template>
</a-button> </a-button>
<a-button class="w-110px search-btn" size="medium" @click="openCustomColumn"> <a-button type="outline" class="w-110px" size="medium" @click="openCustomColumn">
<template #icon> <template #icon>
<img :src="icon1" width="14" height="14" /> <img :src="icon1" width="14" height="14" />
</template> </template>
@ -119,7 +119,7 @@
}} }}
</template> </template>
<template v-else-if="column.dataIndex === 'operation'" #cell="{ record }"> <template v-else-if="column.dataIndex === 'operation'" #cell="{ record }">
<a-button type="outline" size="small" class="search-btn" @click="handleDetail(record)">详情</a-button> <a-button type="outline" size="small" @click="handleDetail(record)">详情</a-button>
</template> </template>
<template v-else-if="column.isRateField" #cell="{ record }"> <template v-else-if="column.isRateField" #cell="{ record }">
@ -148,7 +148,7 @@
</a-table-column> </a-table-column>
<a-table-column data-index="operation" fixed="right" width="100" title="操作"> <a-table-column data-index="operation" fixed="right" width="100" title="操作">
<template #cell="{ record }"> <template #cell="{ record }">
<a-button type="outline" size="small" class="search-btn" @click="handleDetail(record)">详情</a-button> <a-button type="outline" size="small" @click="handleDetail(record)">详情</a-button>
</template> </template>
</a-table-column> </a-table-column>
</template> </template>

View File

@ -65,7 +65,7 @@
<template #footer> <template #footer>
<div style="text-align: right"> <div style="text-align: right">
<a-button class="mr-8px cancel-btn" size="medium" @click="close">取消</a-button> <a-button class="mr-8px" size="medium" @click="close">取消</a-button>
<a-button type="primary" size="medium" @click="onSubmit">确定</a-button> <a-button type="primary" size="medium" @click="onSubmit">确定</a-button>
</div> </div>
</template> </template>

View File

@ -87,13 +87,5 @@
} }
} }
.arco-modal-footer { .arco-modal-footer {
.cancel-btn {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
&:hover {
border: 1px solid var(--BG-500, #b1b2b5);
}
}
} }
} }

View File

@ -19,7 +19,7 @@
<div class="filter-row-item flex items-center"> <div class="filter-row-item flex items-center">
<span class="label">分组</span> <span class="label">分组</span>
<a-space class="w-200px"> <a-space class="w-200px">
<GroupSelect v-model="query.group_ids" multiple :options="groups" @change="handleSearch" /> <CommonSelect v-model="query.group_ids" :options="groups" @change="handleSearch" />
</a-space> </a-space>
</div> </div>
<div class="filter-row-item flex items-center"> <div class="filter-row-item flex items-center">
@ -31,7 +31,7 @@
<div class="filter-row-item flex items-center"> <div class="filter-row-item flex items-center">
<span class="label">运营人员</span> <span class="label">运营人员</span>
<a-space class="w-160px"> <a-space class="w-160px">
<OperatorSelect v-model="query.operator_id" :options="operators" @change="handleSearch" /> <CommonSelect v-model="query.operator_id" :multiple="false" :options="operators" @change="handleSearch" />
</a-space> </a-space>
</div> </div>
</div> </div>
@ -47,13 +47,13 @@
</a-select> </a-select>
</a-space> </a-space>
</div> </div>
<a-button class="w-84px search-btn mr-12px" size="medium" @click="handleSearch"> <a-button type="outline" class="w-84px mr-12px" size="medium" @click="handleSearch">
<template #icon> <template #icon>
<icon-search /> <icon-search />
</template> </template>
<template #default>搜索</template> <template #default>搜索</template>
</a-button> </a-button>
<a-button class="w-84px reset-btn" size="medium" @click="handleReset"> <a-button class="w-84px" size="medium" @click="handleReset">
<template #icon> <template #icon>
<icon-refresh /> <icon-refresh />
</template> </template>
@ -66,9 +66,8 @@
<script setup> <script setup>
import { reactive, defineEmits, defineProps } from 'vue'; import { reactive, defineEmits, defineProps } from 'vue';
import { fetchAccountGroups, fetchAccountOperators } from '@/api/all/propertyMarketing'; import { fetchAccountGroups, fetchAccountOperators } from '@/api/all/propertyMarketing';
import GroupSelect from '@/views/property-marketing/media-account/components/group-select';
import OperatorSelect from '@/views/property-marketing/media-account/components/operator-select';
import StatusSelect from '@/views/property-marketing/media-account/components/status-select'; import StatusSelect from '@/views/property-marketing/media-account/components/status-select';
import CommonSelect from '@/components/common-select';
const props = defineProps({ const props = defineProps({
query: { query: {

View File

@ -2,16 +2,6 @@
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
:deep(.search-btn) {
border-radius: 4px;
border: 1px solid var(--Brand-Brand-6, #6d4cfe);
color: #6d4cfe;
}
:deep(.reset-btn) {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
}
.filter-wrap { .filter-wrap {
border-radius: 8px; border-radius: 8px;
border: 1px solid #e6e6e8; border: 1px solid #e6e6e8;

View File

@ -36,13 +36,13 @@
/> />
</a-space> </a-space>
</div> </div>
<a-button class="w-84px search-btn mr-12px" size="medium" @click="handleSearch"> <a-button type="outline" class="w-84px mr-12px" size="medium" @click="handleSearch">
<template #icon> <template #icon>
<icon-search /> <icon-search />
</template> </template>
<template #default>搜索</template> <template #default>搜索</template>
</a-button> </a-button>
<a-button class="w-84px reset-btn" size="medium" @click="handleReset"> <a-button class="w-84px" size="medium" @click="handleReset">
<template #icon> <template #icon>
<icon-refresh /> <icon-refresh />
</template> </template>

View File

@ -19,16 +19,6 @@
padding: 10px 0; padding: 10px 0;
align-items: center; align-items: center;
} }
:deep(.search-btn) {
border-radius: 4px;
border: 1px solid var(--Brand-Brand-6, #6d4cfe);
color: #6d4cfe;
}
:deep(.reset-btn) {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
}
.table-wrap { .table-wrap {
width: 100%; width: 100%;

View File

@ -15,7 +15,7 @@
<span>确认删除 {{ accountName }} 这个账号吗</span> <span>确认删除 {{ accountName }} 这个账号吗</span>
</div> </div>
<template #footer> <template #footer>
<a-button class="cancel-btn" size="large" @click="onClose">取消</a-button> <a-button size="large" @click="onClose">取消</a-button>
<a-button type="primary" class="ml-16px danger-btn" status="danger" size="large" @click="onDelete" <a-button type="primary" class="ml-16px danger-btn" status="danger" size="large" @click="onDelete"
>确认删除</a-button >确认删除</a-button
> >

View File

@ -46,6 +46,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">
<span class="label">所属项目</span>
<span v-if="!item.projects.length" class="cts">-</span>
<div v-else class="flex items-center">
<a-tooltip
v-if="item.projects.length > 2"
position="bottom"
:content="
item.projects
.slice(2)
.map((v) => v.name)
.join(',')
"
>
<div class="tag-box">
<span class="text">{{ `+${item.projects.length - 2}` }}</span>
</div>
</a-tooltip>
<div v-for="(project, index) in item.projects.slice(0, 2)" :key="index" class="tag-box">
<span class="text">{{ project.name }}</span>
</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>
@ -76,7 +100,7 @@
</div> </div>
<div class="operate-row"> <div class="operate-row">
<a-dropdown trigger="hover"> <a-dropdown trigger="hover">
<a-button class="w-52px search-btn mr-8px" size="mini"> <a-button class="w-52px mr-8px" type="outline" size="mini">
<template #default>更多</template> <template #default>更多</template>
</a-button> </a-button>
<template #content> <template #content>
@ -89,7 +113,7 @@
> >
<a-doption class="color-#F64B31" @click="openDelete(item)">删除</a-doption> <a-doption class="color-#F64B31" @click="openDelete(item)">删除</a-doption>
</template> </template>
<a-button class="search-btn" size="mini" @click="onBtnClick(item)"> <a-button type="outline" size="mini" @click="onBtnClick(item)">
<template #default>{{ getBtnText(item) }}</template> <template #default>{{ getBtnText(item) }}</template>
</a-button> </a-button>
</a-dropdown> </a-dropdown>
@ -101,8 +125,8 @@
<span class="name !mb-0">更新数据失败</span> <span class="name !mb-0">更新数据失败</span>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<a-button class="search-btn mr-8px" size="mini" @click="onDeleteSyncStatus(item)">取消</a-button> <a-button type="outline" class="mr-8px" size="mini" @click="onDeleteSyncStatus(item)">取消</a-button>
<a-button class="search-btn" size="mini" @click="syncData(item)">重新更新</a-button> <a-button type="outline" class="" size="mini" @click="syncData(item)">重新更新</a-button>
</div> </div>
</div> </div>
</a-spin> </a-spin>

View File

@ -9,7 +9,7 @@
<span>确认暂停同步 {{ accountName }} 这个账号的数据吗</span> <span>确认暂停同步 {{ accountName }} 这个账号的数据吗</span>
</div> </div>
<template #footer> <template #footer>
<a-button class="cancel-btn" size="large" @click="onClose">取消</a-button> <a-button size="large" @click="onClose">取消</a-button>
<a-button type="primary" class="ml-16px" size="large" @click="onConfirm">确定</a-button> <a-button type="primary" class="ml-16px" size="large" @click="onConfirm">确定</a-button>
</template> </template>
</a-modal> </a-modal>

View File

@ -1,9 +1,3 @@
@mixin ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-container { .card-container {
flex: 1; flex: 1;
display: grid; display: grid;

View File

@ -19,12 +19,11 @@ import {
Message as AMessage, Message as AMessage,
Textarea, Textarea,
} from '@arco-design/web-vue'; } from '@arco-design/web-vue';
import TagSelect from '@/views/property-marketing/media-account/components/tag-select';
import GroupSelect from '@/views/property-marketing/media-account/components/group-select';
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 SyncDataModal from '../sync-data-modal';
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/arcoD';
@ -37,6 +36,7 @@ import {
putMediaAccounts, putMediaAccounts,
getTemplateUrl, getTemplateUrl,
batchMediaAccounts, batchMediaAccounts,
getProjectList,
} 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';
@ -58,6 +58,7 @@ const INITIAL_FORM = {
platform: 1, platform: 1,
group_id: undefined, group_id: undefined,
tag_ids: [], tag_ids: [],
project_ids: [],
end_work_link: undefined, end_work_link: undefined,
cookie: undefined, cookie: undefined,
}; };
@ -66,6 +67,7 @@ export default {
setup(props, { emit, expose }) { setup(props, { emit, expose }) {
const groupOptions = ref([]); const groupOptions = ref([]);
const tagOptions = ref([]); const tagOptions = ref([]);
const projects = ref([]);
const visible = ref(false); const visible = ref(false);
const uploadType = ref('manual'); const uploadType = ref('manual');
const uploadStatus = ref(UploadStatus.DEFAULT); const uploadStatus = ref(UploadStatus.DEFAULT);
@ -126,6 +128,12 @@ export default {
tagOptions.value = data; tagOptions.value = data;
} }
}; };
const getProjects = async () => {
const { code, data } = await getProjectList();
if (code === 200) {
projects.value = data;
}
};
function handleUpload(option) { function handleUpload(option) {
const { fileItem } = option; const { fileItem } = option;
uploadStatus.value = UploadStatus.WAITING; uploadStatus.value = UploadStatus.WAITING;
@ -141,6 +149,9 @@ export default {
const reset = () => { const reset = () => {
formRef.value?.resetFields(); formRef.value?.resetFields();
formRef.value?.clearValidate(); formRef.value?.clearValidate();
groupOptions.value = [];
tagOptions.value = [];
projects.value = [];
form.value = cloneDeep(INITIAL_FORM); form.value = cloneDeep(INITIAL_FORM);
fileName.value = ''; fileName.value = '';
file.value = null; file.value = null;
@ -162,6 +173,7 @@ export default {
} }
getGroups(); getGroups();
getTags(); getTags();
getProjects();
visible.value = true; visible.value = true;
}; };
const getAccountDetail = async () => { const getAccountDetail = async () => {
@ -364,8 +376,16 @@ export default {
<FormItem label="号码持有人" field="holder_name"> <FormItem label="号码持有人" field="holder_name">
<Input v-model={form.value.holder_name} placeholder="请输入..." class="w-240px" size="large" /> <Input v-model={form.value.holder_name} placeholder="请输入..." class="w-240px" size="large" />
</FormItem> </FormItem>
<FormItem label="所属项目">
<CommonSelect
v-model={form.value.project_ids}
options={projects.value}
placeholder="请选择…"
size="large"
/>
</FormItem>
<FormItem label="选择分组"> <FormItem label="选择分组">
<GroupSelect <CommonSelect
v-model={form.value.group_id} v-model={form.value.group_id}
multiple={false} multiple={false}
options={groupOptions.value} options={groupOptions.value}
@ -373,8 +393,9 @@ export default {
size="large" size="large"
/> />
</FormItem> </FormItem>
<FormItem label="选择标签"> <FormItem label="选择标签">
<TagSelect v-model={form.value.tag_ids} options={tagOptions.value} placeholder="请选择…" size="large" /> <CommonSelect v-model={form.value.tag_ids} options={tagOptions.value} placeholder="请选择…" size="large" />
</FormItem> </FormItem>
<FormItem <FormItem
label="笔记链接" label="笔记链接"
@ -406,7 +427,6 @@ export default {
v-model={form.value.cookie} v-model={form.value.cookie}
placeholder="请输入..." placeholder="请输入..."
size="large" size="large"
max-length={500}
auto-size={{ minRows: 5, maxRows: 8 }} auto-size={{ minRows: 5, maxRows: 8 }}
/> />
</FormItem> </FormItem>
@ -415,7 +435,7 @@ export default {
)} )}
</Form> </Form>
<div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px;"> <div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px;">
<Button size="large" class="cancel-btn" onClick={onClose}> <Button size="large" onClick={onClose}>
取消 取消
</Button> </Button>
<Button type="primary" size="large" loading={importLoading.value} onClick={onSubmit}> <Button type="primary" size="large" loading={importLoading.value} onClick={onSubmit}>

View File

@ -76,15 +76,10 @@
</div> </div>
<template #footer> <template #footer>
<a-button v-if="modalState === MODAL_STATE.QR_READY" size="large" class="cancel-btn" @click="handleRefreshQrCode"> <a-button v-if="modalState === MODAL_STATE.QR_READY" size="large" @click="handleRefreshQrCode">
重新生成 重新生成
</a-button> </a-button>
<a-button <a-button v-if="[MODAL_STATE.SUCCESS, MODAL_STATE.FAILED].includes(modalState)" size="large" @click="close">
v-if="[MODAL_STATE.SUCCESS, MODAL_STATE.FAILED].includes(modalState)"
size="large"
class="cancel-btn"
@click="close"
>
取消 取消
</a-button> </a-button>
<a-button type="primary" size="large" @click="handleOk"> <a-button type="primary" size="large" @click="handleOk">

View File

@ -30,7 +30,7 @@
<a-form-item label="选择分组" required> <a-form-item label="选择分组" required>
<template v-if="editType === 'all'"> <template v-if="editType === 'all'">
<div class="flex items-center w-100%"> <div class="flex items-center w-100%">
<GroupSelect v-model="form.group_id" :options="groupOptions" :multiple="false" class="flex-1" /> <CommonSelect v-model="form.group_id" :options="groupOptions" :multiple="false" class="flex-1" />
</div> </div>
</template> </template>
</a-form-item> </a-form-item>
@ -47,7 +47,7 @@
<a-table-column title="选择分组" data-index="group_id"> <a-table-column title="选择分组" data-index="group_id">
<template #cell="{ record }"> <template #cell="{ record }">
<div class="flex items-center w-100%"> <div class="flex items-center w-100%">
<GroupSelect v-model="record.group_id" :options="groupOptions" :multiple="false" /> <CommonSelect v-model="record.group_id" :options="groupOptions" :multiple="false" />
</div> </div>
</template> </template>
</a-table-column> </a-table-column>
@ -56,7 +56,7 @@
</template> </template>
</a-form> </a-form>
<template #footer> <template #footer>
<a-button size="large" class="mr-16px cancel-btn" @click="onClose">取消</a-button> <a-button size="large" @click="onClose">取消</a-button>
<a-button type="primary" size="large" @click="onSubmit">确定</a-button> <a-button type="primary" size="large" @click="onSubmit">确定</a-button>
</template> </template>
</a-modal> </a-modal>
@ -65,7 +65,7 @@
<script setup> <script setup>
import { ref, reactive } from 'vue'; import { ref, reactive } from 'vue';
import { fetchAccountGroups, batchPutGroup } from '@/api/all/propertyMarketing'; import { fetchAccountGroups, batchPutGroup } from '@/api/all/propertyMarketing';
import GroupSelect from '@/views/property-marketing/media-account/components/group-select'; import CommonSelect from '@/components/common-select';
import icon1 from '@/assets/img/icon-question.png'; import icon1 from '@/assets/img/icon-question.png';

View File

@ -76,7 +76,7 @@
</template> </template>
</a-form> </a-form>
<template #footer> <template #footer>
<a-button size="large" class="mr-16px cancel-btn" @click="onClose">取消</a-button> <a-button size="large" @click="onClose">取消</a-button>
<a-button type="primary" size="large" @click="onSubmit">确定</a-button> <a-button type="primary" size="large" @click="onSubmit">确定</a-button>
</template> </template>
</a-modal> </a-modal>

View File

@ -33,16 +33,20 @@
<span class="label">平台</span> <span class="label">平台</span>
<a-space class="w-160px"> <a-space class="w-160px">
<a-select v-model="query.platform" size="medium" placeholder="全部" allow-clear @change="handleSearch"> <a-select v-model="query.platform" size="medium" placeholder="全部" allow-clear @change="handleSearch">
<a-option v-for="(item, index) in PLATFORM_LIST" :key="index" :value="item.value" :label="item.label">{{ <a-option
item.label v-for="(item, index) in MEDIA_ACCOUNT_PLATFORMS"
}}</a-option> :key="index"
:value="item.value"
:label="item.label"
>{{ item.label }}</a-option
>
</a-select> </a-select>
</a-space> </a-space>
</div> </div>
<div class="filter-row-item flex items-center"> <div class="filter-row-item flex items-center">
<span class="label">运营人员</span> <span class="label">运营人员</span>
<a-space class="w-160px"> <a-space class="w-160px">
<OperatorSelect v-model="query.operator_id" :options="operators" @change="handleSearch" /> <CommonSelect v-model="query.operator_id" :multiple="false" :options="operators" @change="handleSearch" />
</a-space> </a-space>
</div> </div>
</div> </div>
@ -50,22 +54,28 @@
<div class="filter-row-item flex items-center"> <div class="filter-row-item flex items-center">
<span class="label">分组</span> <span class="label">分组</span>
<a-space class="w-200px"> <a-space class="w-200px">
<GroupSelect v-model="query.group_ids" multiple :options="groups" @change="handleSearch" /> <CommonSelect v-model="query.group_ids" multiple :options="groups" @change="handleSearch" />
</a-space>
</div>
<div class="filter-row-item flex items-center">
<span class="label">所属项目</span>
<a-space class="w-200px">
<CommonSelect v-model="query.project_ids" :options="projects" @change="handleSearch" />
</a-space> </a-space>
</div> </div>
<div class="filter-row-item flex items-center"> <div class="filter-row-item flex items-center">
<span class="label">标签</span> <span class="label">标签</span>
<a-space class="w-320px"> <a-space class="w-320px">
<TagSelect v-model="query.tag_ids" :options="tags" @change="handleSearch" /> <CommonSelect v-model="query.tag_ids" :options="tags" @change="handleSearch" />
</a-space> </a-space>
</div> </div>
<a-button class="w-84px search-btn mr-12px" size="medium" @click="handleSearch"> <a-button type="outline" class="w-84px mr-12px" size="medium" @click="handleSearch">
<template #icon> <template #icon>
<icon-search /> <icon-search />
</template> </template>
<template #default>搜索</template> <template #default>搜索</template>
</a-button> </a-button>
<a-button class="w-84px reset-btn" size="medium" @click="handleReset"> <a-button class="w-84px" size="medium" @click="handleReset">
<template #icon> <template #icon>
<icon-refresh /> <icon-refresh />
</template> </template>
@ -77,13 +87,17 @@
<script setup> <script setup>
import { reactive, defineEmits, defineProps } from 'vue'; import { reactive, defineEmits, defineProps } from 'vue';
import { fetchAccountTags, fetchAccountGroups, fetchAccountOperators } from '@/api/all/propertyMarketing'; import {
import TagSelect from '@/views/property-marketing/media-account/components/tag-select'; fetchAccountTags,
import GroupSelect from '@/views/property-marketing/media-account/components/group-select'; getProjectList,
import OperatorSelect from '@/views/property-marketing/media-account/components/operator-select'; fetchAccountGroups,
fetchAccountOperators,
} from '@/api/all/propertyMarketing';
import StatusSelect from '@/views/property-marketing/media-account/components/status-select'; import StatusSelect from '@/views/property-marketing/media-account/components/status-select';
import CommonSelect from '@/components/common-select';
import { INITIAL_QUERY, PLATFORM_LIST } from '@/views/property-marketing/media-account/account-manage/constants'; import { INITIAL_QUERY } from '@/views/property-marketing/media-account/account-manage/constants';
import { MEDIA_ACCOUNT_PLATFORMS } from '@/utils/platform';
const props = defineProps({ const props = defineProps({
query: { query: {
@ -97,6 +111,7 @@ const emits = defineEmits('onSearch', 'onReset', 'update:query');
const tags = ref([]); const tags = ref([]);
const groups = ref([]); const groups = ref([]);
const operators = ref([]); const operators = ref([]);
const projects = ref([]);
const handleSearch = () => { const handleSearch = () => {
emits('update:query', props.query); emits('update:query', props.query);
@ -127,11 +142,18 @@ const getOperators = async () => {
operators.value = data; operators.value = data;
} }
}; };
const getProjects = async () => {
const { code, data } = await getProjectList();
if (code === 200) {
projects.value = data;
}
};
onMounted(() => { onMounted(() => {
getTags(); getTags();
getGroups(); getGroups();
getOperators(); getOperators();
getProjects();
}); });
defineExpose({ defineExpose({

View File

@ -16,7 +16,7 @@
</a-form-item> </a-form-item>
</a-form> </a-form>
<template #footer> <template #footer>
<a-button class="cancel-btn" @click="onClose">取消</a-button> <a-button @click="onClose">取消</a-button>
<a-button type="primary" class="ml-16px" @click="onSubmit">确认</a-button> <a-button type="primary" class="ml-16px" @click="onSubmit">确认</a-button>
</template> </template>
</a-modal> </a-modal>

View File

@ -9,7 +9,7 @@
<span>确认删除 "{{ groupName }}" 这个分组吗</span> <span>确认删除 "{{ groupName }}" 这个分组吗</span>
</div> </div>
<template #footer> <template #footer>
<a-button class="cancel-btn" size="large" @click="onClose">取消</a-button> <a-button size="large" @click="onClose">取消</a-button>
<a-button type="primary" class="ml-16px danger-btn" status="danger" size="large" @click="onDelete" <a-button type="primary" class="ml-16px danger-btn" status="danger" size="large" @click="onDelete"
>确认删除</a-button >确认删除</a-button
> >

View File

@ -21,7 +21,7 @@
</div> </div>
</div> </div>
<template #footer> <template #footer>
<a-button size="large" class="cancel-btn" @click="close">取消</a-button> <a-button size="large" @click="close">取消</a-button>
<a-button type="primary" size="large" @click="handleOk"> 去授权 </a-button> <a-button type="primary" size="large" @click="handleOk"> 去授权 </a-button>
</template> </template>
</a-modal> </a-modal>

View File

@ -92,13 +92,12 @@
</div> </div>
<template #footer> <template #footer>
<a-button v-if="modalState === MODAL_STATE.QR_READY" size="large" class="cancel-btn" @click="handleRefreshQrCode"> <a-button v-if="modalState === MODAL_STATE.QR_READY" size="large" @click="handleRefreshQrCode">
重新生成 重新生成
</a-button> </a-button>
<a-button <a-button
v-if="modalState === MODAL_STATE.SUCCESS || modalState === MODAL_STATE.FAILED" v-if="modalState === MODAL_STATE.SUCCESS || modalState === MODAL_STATE.FAILED"
size="large" size="large"
class="cancel-btn"
@click="close" @click="close"
> >
取消 取消

View File

@ -20,7 +20,7 @@
</div> </div>
</div> </div>
<template #footer> <template #footer>
<a-button size="large" class="cancel-btn" @click="close">稍后再说</a-button> <a-button size="large" @click="close">稍后再说</a-button>
<a-button type="primary" size="large" @click="handleOk"> 更新数据 </a-button> <a-button type="primary" size="large" @click="handleOk"> 更新数据 </a-button>
</template> </template>
</a-modal> </a-modal>

View File

@ -1,7 +1,3 @@
<!--
* @Author: AI
* @Date: 2025-06-27
-->
<template> <template>
<a-modal <a-modal
v-model:visible="visible" v-model:visible="visible"
@ -16,7 +12,7 @@
</a-form-item> </a-form-item>
</a-form> </a-form>
<template #footer> <template #footer>
<a-button class="cancel-btn" @click="onClose">取消</a-button> <a-button @click="onClose">取消</a-button>
<a-button type="primary" class="ml-16px" @click="onSubmit">确认</a-button> <a-button type="primary" class="ml-16px" @click="onSubmit">确认</a-button>
</template> </template>
</a-modal> </a-modal>

View File

@ -9,7 +9,7 @@
<span>确认删除 "{{ tagName }}" 这个标签吗</span> <span>确认删除 "{{ tagName }}" 这个标签吗</span>
</div> </div>
<template #footer> <template #footer>
<a-button class="cancel-btn" size="large" @click="onClose">取消</a-button> <a-button size="large" @click="onClose">取消</a-button>
<a-button type="primary" class="ml-16px danger-btn" status="danger" size="large" @click="onDelete" <a-button type="primary" class="ml-16px danger-btn" status="danger" size="large" @click="onDelete"
>确认删除</a-button >确认删除</a-button
> >

View File

@ -9,6 +9,7 @@ export const INITIAL_QUERY = {
operator_id: '', operator_id: '',
group_ids: [], group_ids: [],
tag_ids: [], tag_ids: [],
project_ids: [],
}; };
export const INITIAL_PAGE_INFO = { export const INITIAL_PAGE_INFO = {

View File

@ -8,19 +8,19 @@
<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">
<a-button class="w-112px mr-12px search-btn" size="medium" @click="handleOpenTagsModal"> <a-button class="w-112px mr-12px" type="outline" size="medium" @click="handleOpenTagsModal">
<template #icon> <template #icon>
<img :src="icon3" width="16" height="16" /> <img :src="icon3" width="16" height="16" />
</template> </template>
<template #default>标签管理</template> <template #default>标签管理</template>
</a-button> </a-button>
<a-button class="w-112px mr-12px search-btn" size="medium" @click="handleOpenGroupModal"> <a-button class="w-112px mr-12px" type="outline" size="medium" @click="handleOpenGroupModal">
<template #icon> <template #icon>
<img :src="icon2" width="16" height="16" /> <img :src="icon2" width="16" height="16" />
</template> </template>
<template #default>分组管理</template> <template #default>分组管理</template>
</a-button> </a-button>
<a-button type="primary" class="w-112px search-btn" size="medium" @click="handleOpenAccountModal"> <a-button type="primary" class="w-112px" size="medium" @click="handleOpenAccountModal">
<template #icon> <template #icon>
<img :src="icon1" width="16" height="16" /> <img :src="icon1" width="16" height="16" />
</template> </template>
@ -70,7 +70,7 @@
</template> </template>
<div v-else> <div v-else>
<a-space v-if="isAbNormalStatus" class="flex items-center"> <a-space v-if="isAbNormalStatus" class="flex items-center">
<a-button class="w-96px err-btn" size="mini" @click="handleOpenAbnormalAccount"> <a-button type="primary" status="danger" size="mini" @click="handleOpenAbnormalAccount">
<template #default>查看异常账号</template> <template #default>查看异常账号</template>
</a-button> </a-button>
</a-space> </a-space>

View File

@ -2,16 +2,6 @@
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
:deep(.search-btn) {
border-radius: 4px;
border: 1px solid var(--Brand-Brand-6, #6d4cfe);
color: #6d4cfe;
}
:deep(.reset-btn) {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
}
.filter-wrap { .filter-wrap {
.top { .top {
.title { .title {
@ -47,15 +37,6 @@
color: #211f24; color: #211f24;
} }
} }
.err-btn {
background-color: #f64b31 !important;
color: var(--BG-white, #fff);
font-family: 'PingFang SC';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 166.667% */
}
.operation-btn { .operation-btn {
padding: 0; padding: 0;
cursor: pointer; cursor: pointer;

View File

@ -1,64 +0,0 @@
<!--
* @Author: RenXiaoDong
* @Date: 2025-06-25 14:02:40
-->
<template>
<a-select
v-model="selectedGroups"
:multiple="multiple"
size="medium"
:placeholder="placeholder"
allow-clear
:max-tag-count="3"
@change="handleChange"
>
<a-option v-for="(item, index) in options" :key="index" :value="item.id" :label="item.name">
{{ item.name }}
</a-option>
</a-select>
</template>
<script setup>
import { ref, watch } from 'vue';
const props = defineProps({
modelValue: {
type: [Array, String, Number],
default: () => [],
},
multiple: {
type: Boolean,
default: true,
},
placeholder: {
type: String,
default: '全部',
},
options: {
type: Array,
default: () => [],
},
});
const emits = defineEmits(['update:modelValue', 'change']);
const selectedGroups = ref(props.multiple ? [] : '');
// 监听外部传入的值变化
watch(
() => props.modelValue,
(newVal) => {
selectedGroups.value = newVal;
},
{ immediate: true },
);
// 监听内部值变化,向外部发送更新
watch(selectedGroups, (newVal) => {
emits('update:modelValue', newVal);
});
const handleChange = (value) => {
selectedGroups.value = value;
emits('change', value);
};
</script>

View File

@ -1,64 +0,0 @@
<!--
* @Author: RenXiaoDong
* @Date: 2025-06-25 14:02:40
-->
<template>
<a-select
v-model="selectedOperators"
:multiple="multiple"
size="medium"
:placeholder="placeholder"
allow-clear
@change="handleChange"
>
<a-option v-for="(item, index) in options" :key="index" :value="item.id" :label="item.name">
{{ item.name }}
</a-option>
</a-select>
</template>
<script setup>
import { ref, watch } from 'vue';
const props = defineProps({
modelValue: {
type: [Array, String, Number],
default: () => [],
},
multiple: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
default: '全部',
},
options: {
type: Array,
default: () => [],
},
});
const emits = defineEmits(['update:modelValue', 'change']);
const selectedOperators = ref(props.multiple ? [] : '');
// 监听外部传入的值变化
watch(
() => props.modelValue,
(newVal) => {
selectedOperators.value = newVal;
},
{ immediate: true },
);
// 监听内部值变化,向外部发送更新
watch(selectedOperators, (newVal) => {
emits('update:modelValue', newVal);
});
const handleChange = (value) => {
selectedOperators.value = value;
emits('change', value);
};
</script>

View File

@ -0,0 +1,156 @@
<template>
<a-modal
v-model:visible="visible"
title="添加项目"
modal-class="add-project-modal"
width="960px"
:mask-closable="false"
@close="onClose"
>
<div class="content">
<a-steps changeable :current="currentStep" @change="setCurrent" class="mb-24px mx-79px">
<a-step v-for="(step, index) in STEPS" :key="index">{{ step.label }}</a-step>
</a-steps>
<component :is="activeComp" v-model:formQuery="formQuery" ref="compRef" />
</div>
<template #footer>
<div class="flex justify-between items-center w-100%">
<div>
<a-button type="outline" size="medium" @click="onPrev" v-if="!isFirstStep">上一步</a-button>
</div>
<div class="flex items-center">
<a-button size="medium" class="mr-8px" @click="onCancel">取消</a-button>
<a-button type="primary" size="medium" @click="onSubmit">{{ isLastStep ? '确认添加' : '下一步' }}</a-button>
</div>
</div>
</template>
</a-modal>
</template>
<script setup>
import { postAddProject, putProject, getProjectDetail } from '@/api/all/propertyMarketing';
import StepOne from './stepOne.vue';
import StepTwo from './stepTwo.vue';
import StepThree from './stepThree.vue';
import StepFour from './stepFour.vue';
const STEPS = [
{
label: '项目信息',
index: 1,
comp: StepOne,
},
{
label: '关联平台账号',
index: 2,
comp: StepTwo,
},
{
label: '关联渠道账户',
index: 3,
comp: StepThree,
},
{
label: '关联内容稿件',
index: 4,
comp: StepFour,
},
];
const INITIAL_QUERY = {
name: '',
budget: '',
target: '',
background: '',
media_account_ids: [],
placement_account_ids: [],
work_ids: [],
};
const update = inject('update');
const visible = ref(false);
const currentStep = ref(1);
const formQuery = ref(cloneDeep(INITIAL_QUERY));
const compRef = ref(null);
const projectId = ref(null);
const isFirstStep = computed(() => currentStep.value === 1);
const isEdit = computed(() => projectId.value);
const isLastStep = computed(() => currentStep.value === STEPS.length);
const activeComp = computed(() => STEPS.find((v) => v.index === currentStep.value)?.comp ?? null);
const open = (id = null) => {
projectId.value = id;
if (id) {
getDetail();
}
visible.value = true;
};
const getDetail = (id) => {
getProjectDetail(projectId.value).then((res) => {
if (res.code === 200) {
formQuery.value = res.data;
}
});
};
const onClose = () => {
currentStep.value = 1;
formQuery.value = cloneDeep(INITIAL_QUERY);
projectId.value = '';
compRef.value?.reset?.();
visible.value = false;
};
const onCancel = () => {
visible.value = false;
};
const onSubmit = async () => {
if (isFirstStep.value) {
const valid = await compRef.value.validate();
if (!valid) {
return;
}
}
if (isLastStep.value) {
isEdit.value ? handleEdit() : handleAdd();
} else {
currentStep.value++;
}
};
const handleAdd = () => {
postAddProject(formQuery.value).then((res) => {
if (res.code === 200) {
onClose();
update();
}
});
};
const handleEdit = () => {
putProject({ id: projectId.value, ...formQuery.value }).then((res) => {
if (res.code === 200) {
onClose();
update();
}
});
};
const setCurrent = async (current) => {
if (isFirstStep.value) {
const valid = await compRef.value.validate();
if (!valid) {
return;
}
}
currentStep.value = current;
};
const onPrev = () => {
currentStep.value--;
};
defineExpose({ open });
</script>

View File

@ -0,0 +1,267 @@
<template>
<div class="table-wrap flex h-448px">
<div class="left flex-1 pr-12px flex flex-col">
<div class="flex items-center mb-16px">
<a-input
v-model="query.uid"
class="w-160px mr-16px"
placeholder="搜索序号"
size="medium"
allow-clear
@change="handleSearch"
>
<template #prefix>
<icon-search />
</template>
</a-input>
<a-input
v-model="query.title"
class="w-220px mr-16px"
placeholder="搜索内容稿件标题"
size="medium"
allow-clear
@change="handleSearch"
>
<template #prefix>
<icon-search />
</template>
</a-input>
<a-select
v-model="query.uploader_id"
size="medium"
placeholder="选择上传人员"
class="w-160px"
allow-clear
@change="handleSearch"
>
<a-option v-for="(item, index) in uploaders" :key="index" :value="item.id" :label="item.name">
{{ item.name || '-' }}
</a-option>
</a-select>
</div>
<a-table
ref="tableRef"
:data="dataSource"
column-resizable
row-key="id"
:row-selection="rowSelection"
:pagination="false"
:scroll="{ x: '100%', y: '100%' }"
class="flex-1 overflow-hidden"
:selected-keys="selectedRowKeys"
bordered
@select="handleSelect"
@select-all="handleSelectAll"
>
<template #empty>
<NoData text="暂无账户" />
</template>
<template #columns>
<a-table-column
v-for="column in TABLE_COLUMNS"
:key="column.dataIndex"
:data-index="column.dataIndex"
:fixed="column.fixed"
:width="column.width"
:min-width="column.minWidth"
:sortable="column.sortable"
:align="column.align"
ellipsis
tooltip
>
<template #title>
<div class="flex items-center">
<span class="cts mr-4px">{{ column.title }}</span>
<a-tooltip v-if="column.tooltip" :content="column.tooltip" position="top">
<icon-question-circle class="tooltip-icon color-#737478" size="16" />
</a-tooltip>
</div>
</template>
<template #cell="{ record }">
<template v-if="column.dataIndex === 'created_at'">
{{ exactFormatTime(record.created_at) }}
</template>
<template v-else>
{{ formatTableField(column, record, true) }}
</template>
</template>
</a-table-column>
</template>
</a-table>
</div>
<div class="right w-320px px-12px flex flex-col">
<div class="flex justify-between">
<p class="mb-16px s1">{{ `已选择(${selectedRows?.length ?? 0}` }}</p>
<a-button type="text" @click="onClearSelect" v-if="selectedRows.length">清空</a-button>
</div>
<div class="flex-1 overflow-y-auto overflow-x-hidden">
<template v-if="selectedRows?.length">
<div class="tag-item mb-8px" v-for="item in selectedRows" :key="item.id">
<a-tooltip :content="item.name">
<p class="name mr-4px">{{ item.name || '-' }}</p>
</a-tooltip>
<icon-close size="12" class="color-#3C4043 cursor-pointer flex-shrink-0" @click="onDelete(item)" />
</div>
</template>
<NoData v-else text="暂无账户" />
</div>
</div>
</div>
</template>
<script setup>
import { formatTableField, exactFormatTime } from '@/utils/tools';
import { getPlacementAccountOperators, getWorksList } from '@/api/all/propertyMarketing';
import { getUserList } from '@/api/all/common';
import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination';
const TABLE_COLUMNS = [
{
title: '序号',
dataIndex: 'uid',
width: 80,
},
{
title: '内容稿件标题',
dataIndex: 'title',
},
{
title: '上传时间',
dataIndex: 'created_at',
width: 120,
},
{
title: '上传人员',
dataIndex: 'uploader.name',
width: 120,
},
];
const emit = defineEmits(['update:formQuery']);
const props = defineProps({
formQuery: {
type: Object,
default: () => {},
},
});
const { dataSource, selectedRowKeys, selectedRows, rowSelection, handleSelect, handleSelectAll } =
useTableSelectionWithPagination({
onSelectChange: () => {
updateFormQuery();
},
});
const query = ref({
uid: '',
uploader_id: '',
title: '',
});
const uploaders = ref([]);
const allData = ref([]);
const updateFormQuery = () => {
emit('update:formQuery', {
...props.formQuery,
work_ids: selectedRowKeys.value,
});
};
const handleSearch = () => {
const { uid, title, uploader_id } = query.value;
dataSource.value = allData.value.filter((item) => {
const uinMatch = uid === '' ? true : item.uid.includes(uid);
const titleMatch = title === '' ? true : item.title === title;
const operatorIdMatch = uploader_id === '' ? true : item.uploader?.id === uploader_id;
return uinMatch && titleMatch && operatorIdMatch;
});
};
const getUsers = async () => {
const { code, data } = await getUserList();
if (code === 200) {
uploaders.value = data.map( v => ({
...v,
name: v.name || v.mobile
}));
console.log(uploaders.value);
}
};
const getTableData = async () => {
const { code, data } = await getWorksList();
if (code === 200) {
allData.value = data ?? [];
dataSource.value = data ?? [];
initSelect();
}
};
const initSelect = () => {
if (props.formQuery.work_ids?.length) {
selectedRowKeys.value = props.formQuery.work_ids;
selectedRows.value = dataSource.value.filter((v) => selectedRowKeys.value.includes(v.id));
}
};
const onClearSelect = () => {
selectedRowKeys.value = [];
selectedRows.value = [];
updateFormQuery();
};
const onDelete = (item) => {
const { id } = item;
selectedRowKeys.value = selectedRowKeys.value.filter((v) => v !== id);
selectedRows.value = selectedRows.value.filter((v) => v.id !== id);
updateFormQuery();
};
onMounted(() => {
getUsers();
getTableData();
});
</script>
<style lang="scss" scoped>
.table-wrap {
.s1 {
color: var(--Text-2, #3c4043);
font-family: font-family-regular;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
}
.left {
border-right: 1px solid var(--Border-2, #e6e6e8);
:deep(.arco-table) {
}
}
.right {
.tag-item {
max-width: 100%;
width: fit-content;
overflow: hidden;
display: flex;
height: 24px;
padding: 0px 8px;
align-items: center;
border-radius: 2px;
background: var(--BG-200, #f2f3f5);
.name {
color: var(--Text-2, #3c4043);
font-family: font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
flex: 1;
@include ellipsis;
}
}
}
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<a-form ref="formRef" :model="formQuery" :rules="rules" layout="horizontal" auto-label-width class="h-448px">
<a-form-item label="项目名称" required field="name">
<a-input v-model="formQuery.name" placeholder="请输入项目名称" size="large" class="!w-400px" />
</a-form-item>
<a-form-item label="项目预算" field="budget">
<a-input v-model="formQuery.budget" placeholder="请输入项目预算" size="large" class="!w-400px" />
</a-form-item>
<a-form-item label="项目目标" field="target">
<a-textarea
v-model="formQuery.target"
placeholder="请输入项目目标"
:max-length="500"
show-word-limit
class="h-154px"
/>
</a-form-item>
<a-form-item label="项目背景" field="background">
<a-textarea
v-model="formQuery.background"
placeholder="请输入项目背景"
:max-length="500"
show-word-limit
class="h-154px"
/>
</a-form-item>
</a-form>
</template>
<script setup>
const props = defineProps({
formQuery: {
type: Object,
default: () => {},
},
});
const formRef = ref(null);
const rules = {
name: [{ required: true, message: '请输入项目名称' }],
};
const validate = async () => {
const errors = await formRef.value.validate();
return !errors
};
const reset = () => {
formRef.value.resetFields();
};
defineExpose({ validate, reset });
</script>

View File

@ -0,0 +1,259 @@
<template>
<div class="table-wrap flex h-448px">
<div class="left flex-1 pr-12px flex flex-col">
<div class="flex items-center mb-16px">
<a-input
v-model="query.name"
class="w-220px mr-16px"
placeholder="搜索账户"
size="medium"
allow-clear
@change="handleSearch"
>
<template #prefix>
<icon-search />
</template>
</a-input>
<a-select
v-model="query.platform"
class="mr-16px w-160px"
size="medium"
placeholder="选择平台"
allow-clear
@change="handleSearch"
>
<a-option
v-for="(item, index) in PLATFORM_LIST"
:key="index"
:value="item.value"
:label="item.label"
>{{ item.label }}</a-option
>
</a-select>
<a-select
v-model="query.operator_id"
size="medium"
placeholder="选择运营人员"
class="w-160px"
allow-clear
@change="handleSearch"
>
<a-option v-for="(item, index) in operators" :key="index" :value="item.id" :label="item.name">
{{ item.name }}
</a-option>
</a-select>
</div>
<a-table
ref="tableRef"
:data="dataSource"
column-resizable
row-key="id"
:row-selection="rowSelection"
:pagination="false"
:scroll="{ x: '100%', y: '100%' }"
class="flex-1 overflow-hidden"
:selected-keys="selectedRowKeys"
bordered
@select="handleSelect"
@select-all="handleSelectAll"
>
<template #empty>
<NoData text="暂无账户"/>
</template>
<template #columns>
<a-table-column
v-for="column in TABLE_COLUMNS"
:key="column.dataIndex"
:data-index="column.dataIndex"
:fixed="column.fixed"
:width="column.width"
:min-width="column.minWidth"
:sortable="column.sortable"
:align="column.align"
ellipsis
tooltip
>
<template #title>
<div class="flex items-center">
<span class="cts mr-4px">{{ column.title }}</span>
<a-tooltip v-if="column.tooltip" :content="column.tooltip" position="top">
<icon-question-circle class="tooltip-icon color-#737478" size="16" />
</a-tooltip>
</div>
</template>
<template #cell="{ record }">
<template v-if="column.dataIndex === 'platform'">
<img :src="getPutAccountPlatformLogo(record.platform)" width="19" height="19" />
</template>
<template v-else>
{{ formatTableField(column, record, true) }}
</template>
</template>
</a-table-column>
</template>
</a-table>
</div>
<div class="right w-320px px-12px flex flex-col">
<div class="flex justify-between">
<p class="mb-16px s1">{{ `已选择(${selectedRows?.length ?? 0}` }}</p>
<a-button type="text" @click="onClearSelect" v-if="selectedRows.length">清空</a-button>
</div>
<div class="flex-1 overflow-y-auto overflow-x-hidden">
<template v-if="selectedRows?.length">
<div class="tag-item mb-8px" v-for="item in selectedRows" :key="item.id">
<a-tooltip :content="item.name">
<p class="name mr-4px">{{ item.name || '-' }}</p>
</a-tooltip>
<icon-close size="12" class="color-#3C4043 cursor-pointer flex-shrink-0" @click="onDelete(item)" />
</div>
</template>
<NoData v-else text="暂无账户" />
</div>
</div>
</div>
</template>
<script setup>
import { PLATFORM_LIST, getPutAccountPlatformLogo } from '@/utils/platform';
import { formatTableField } from '@/utils/tools';
import { getPlacementAccountOperators, getPlacementAccountsList } from '@/api/all/propertyMarketing';
import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination';
const TABLE_COLUMNS = [
{
title: '账号名称',
dataIndex: 'name',
},
{
title: '渠道',
dataIndex: 'platform',
width: 80,
},
{
title: '运营人员',
dataIndex: 'operator.name',
width: 140,
},
];
const emit = defineEmits(['update:formQuery']);
const props = defineProps({
formQuery: {
type: Object,
default: () => {},
},
});
const { dataSource, selectedRowKeys, selectedRows, rowSelection, handleSelect, handleSelectAll } =
useTableSelectionWithPagination({
onSelectChange: () => {
updateFormQuery();
},
});
const query = ref({
name: '',
operator_id: '',
platform: '',
});
const operators = ref([]);
const allData = ref([]);
const updateFormQuery = () => {
emit('update:formQuery', {
...props.formQuery,
placement_account_ids: selectedRowKeys.value,
});
};
const handleSearch = () => {
const { name, platform, operator_id } = query.value;
dataSource.value = allData.value.filter((item) => {
const nameMatch = name === '' ? true : item.name.includes(name);
const platformMatch = platform === '' ? true : item.platform === platform;
const operatorIdMatch = operator_id === '' ? true : item.operator_id === operator_id;
return nameMatch && platformMatch && operatorIdMatch;
});
};
const getOperators = async () => {
const { code, data } = await getPlacementAccountOperators();
if (code === 200) {
operators.value = data;
}
};
const getTableData = async () => {
const { code, data } = await getPlacementAccountsList();
if (code === 200) {
allData.value = data ?? [];
dataSource.value = data ?? [];
initSelect();
}
};
const initSelect = () => {
if (props.formQuery.placement_account_ids?.length) {
selectedRowKeys.value = props.formQuery.placement_account_ids;
selectedRows.value = dataSource.value.filter((v) => selectedRowKeys.value.includes(v.id));
}
};
const onClearSelect = () => {
selectedRowKeys.value = [];
selectedRows.value = [];
updateFormQuery();
};
const onDelete = (item) => {
const { id } = item;
selectedRowKeys.value = selectedRowKeys.value.filter((v) => v !== id);
selectedRows.value = selectedRows.value.filter((v) => v.id !== id);
updateFormQuery();
};
onMounted(() => {
getOperators();
getTableData();
});
</script>
<style lang="scss" scoped>
.table-wrap {
.s1 {
color: var(--Text-2, #3c4043);
font-family: font-family-regular;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
}
.left {
border-right: 1px solid var(--Border-2, #e6e6e8);
:deep(.arco-table) {
}
}
.right {
.tag-item {
max-width: 100%;
width: fit-content;
overflow: hidden;
display: flex;
height: 24px;
padding: 0px 8px;
align-items: center;
border-radius: 2px;
background: var(--BG-200, #f2f3f5);
.name {
color: var(--Text-2, #3c4043);
font-family: font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
flex: 1;
@include ellipsis;
}
}
}
}
</style>

View File

@ -0,0 +1,253 @@
<template>
<div class="table-wrap flex h-448px">
<div class="left flex-1 pr-12px flex flex-col">
<div class="flex items-center mb-16px">
<a-input
v-model="query.name"
class="w-220px mr-16px"
placeholder="搜索账号"
size="medium"
allow-clear
@change="handleSearch"
>
<template #prefix>
<icon-search />
</template>
</a-input>
<a-select
v-model="query.platform"
class="mr-16px w-160px"
size="medium"
placeholder="选择平台"
allow-clear
@change="handleSearch"
>
<a-option
v-for="(item, index) in MEDIA_ACCOUNT_PLATFORMS"
:key="index"
:value="item.value"
:label="item.label"
>{{ item.label }}</a-option
>
</a-select>
<a-select
v-model="query.operator_id"
size="medium"
placeholder="选择运营人员"
class="w-160px"
allow-clear
@change="handleSearch"
>
<a-option v-for="(item, index) in operators" :key="index" :value="item.id" :label="item.name">
{{ item.name }}
</a-option>
</a-select>
</div>
<a-table
ref="tableRef"
:data="dataSource"
column-resizable
row-key="id"
:row-selection="rowSelection"
:pagination="false"
:scroll="{ x: '100%', y: '100%' }"
class="flex-1 overflow-hidden"
:selected-keys="selectedRowKeys"
bordered
@select="handleSelect"
@select-all="handleSelectAll"
>
<template #empty>
<NoData text="暂无账号"/>
</template>
<template #columns>
<a-table-column
v-for="column in TABLE_COLUMNS"
:key="column.dataIndex"
:data-index="column.dataIndex"
:fixed="column.fixed"
:width="column.width"
:min-width="column.minWidth"
:sortable="column.sortable"
:align="column.align"
ellipsis
tooltip
>
<template #title>
<div class="flex items-center">
<span class="cts mr-4px">{{ column.title }}</span>
<a-tooltip v-if="column.tooltip" :content="column.tooltip" position="top">
<icon-question-circle class="tooltip-icon color-#737478" size="16" />
</a-tooltip>
</div>
</template>
<template #cell="{ record }">
<template v-if="column.dataIndex === 'platform'">
<img :src="getMediaAccountPlatformLogo(record.platform)" width="19" height="19" />
</template>
<template v-else>
{{ formatTableField(column, record, true) }}
</template>
</template>
</a-table-column>
</template>
</a-table>
</div>
<div class="right w-320px px-12px flex flex-col">
<div class="flex justify-between">
<p class="mb-16px s1">{{ `已选择(${selectedRows?.length ?? 0}` }}</p>
<a-button type="text" @click="onClearSelect" v-if="selectedRows.length">清空</a-button>
</div>
<div class="flex-1 overflow-y-auto">
<div class="flex flex-wrap" v-if="selectedRows?.length">
<div class="tag-item mr-8px mb-8px" v-for="item in selectedRows" :key="item.id">
<span class="name mr-4px">{{ item.name || '-' }}</span>
<icon-close size="12" class="color-#3C4043 cursor-pointer" @click="onDelete(item)" />
</div>
</div>
<NoData v-else text="暂无账号" />
</div>
</div>
</div>
</template>
<script setup>
import { MEDIA_ACCOUNT_PLATFORMS, getMediaAccountPlatformLogo } from '@/utils/platform';
import { formatTableField } from '@/utils/tools';
import { fetchAccountOperators, getMediaAccountList } from '@/api/all/propertyMarketing';
import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination';
const TABLE_COLUMNS = [
{
title: '账号名称',
dataIndex: 'name',
},
{
title: '平台',
dataIndex: 'platform',
width: 80,
},
{
title: '运营人员',
dataIndex: 'operator.name',
width: 140,
},
];
const emit = defineEmits(['update:formQuery']);
const props = defineProps({
formQuery: {
type: Object,
default: () => {},
},
});
const { dataSource, selectedRowKeys, selectedRows, rowSelection, handleSelect, handleSelectAll } =
useTableSelectionWithPagination({
onSelectChange: () => {
updateFormQuery();
},
});
const query = ref({
name: '',
operator_id: '',
platform: '',
});
const operators = ref([]);
const allData = ref([]);
const updateFormQuery = () => {
emit('update:formQuery', {
...props.formQuery,
media_account_ids: selectedRowKeys.value,
});
};
const handleSearch = () => {
const { name, platform, operator_id } = query.value;
dataSource.value = allData.value.filter((item) => {
const nameMatch = name === '' ? true : item.name.includes(name);
const platformMatch = platform === '' ? true : item.platform === platform;
const operatorIdMatch = operator_id === '' ? true : item.operator_id === operator_id;
return nameMatch && platformMatch && operatorIdMatch;
});
};
const getOperators = async () => {
const { code, data } = await fetchAccountOperators();
if (code === 200) {
operators.value = data;
}
};
const getTableData = async () => {
const { code, data } = await getMediaAccountList();
if (code === 200) {
allData.value = data ?? [];
dataSource.value = data ?? [];
initSelect();
}
};
const initSelect = () => {
if (props.formQuery.media_account_ids?.length) {
selectedRowKeys.value = props.formQuery.media_account_ids;
selectedRows.value = dataSource.value.filter((v) => selectedRowKeys.value.includes(v.id));
}
};
const onClearSelect = () => {
selectedRowKeys.value = [];
selectedRows.value = [];
updateFormQuery();
};
const onDelete = (item) => {
const { id } = item;
selectedRowKeys.value = selectedRowKeys.value.filter((v) => v !== id);
selectedRows.value = selectedRows.value.filter((v) => v.id !== id);
updateFormQuery();
};
onMounted(() => {
getOperators();
getTableData();
});
</script>
<style lang="scss" scoped>
.table-wrap {
.s1 {
color: var(--Text-2, #3c4043);
font-family: font-family-regular;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
}
.left {
border-right: 1px solid var(--Border-2, #e6e6e8);
:deep(.arco-table) {
}
}
.right {
.tag-item {
width: fit-content;
display: flex;
height: 24px;
padding: 0px 8px;
align-items: center;
border-radius: 2px;
background: var(--BG-200, #f2f3f5);
.name {
color: var(--Text-2, #3c4043);
font-family: font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
}
}
}
}
</style>

View File

@ -0,0 +1,3 @@
.add-project-modal {
}

View File

@ -0,0 +1,70 @@
<!-- eslint-disable vue/no-mutating-props -->
<!--
* @Author: RenXiaoDong
* @Date: 2025-06-25 14:02:40
-->
<template>
<div class="filter-wrap px-24px pt-12px pb-24px">
<div class="filter-row flex">
<div class="filter-row-item flex items-center">
<span class="label">项目名称</span>
<a-space size="medium">
<a-input
v-model="query.name"
class="w-240px"
placeholder="请搜索..."
size="medium"
allow-clear
@change="handleSearch"
>
<template #prefix>
<icon-search />
</template>
</a-input>
</a-space>
</div>
<div class="filter-row-item flex items-center">
<a-button type="outline" class="mr-12px" size="medium" @click="handleSearch">
<template #icon>
<icon-search />
</template>
<template #default>搜索</template>
</a-button>
<a-button size="medium" @click="handleReset">
<template #icon>
<icon-refresh />
</template>
<template #default>重置</template>
</a-button>
</div>
</div>
</div>
</template>
<script setup>
import { defineEmits, defineProps } from 'vue';
const props = defineProps({
query: {
type: Object,
required: true,
},
});
const emits = defineEmits('onSearch', 'onReset', 'update:query');
const handleSearch = () => {
emits('update:query', props.query);
nextTick(() => {
emits('onSearch');
});
};
const handleReset = () => {
emits('onReset');
};
</script>
<style scoped lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,23 @@
.filter-wrap {
.filter-row {
.filter-row-item {
&:not(:last-child) {
margin-right: 24px;
}
.label {
margin-right: 8px;
color: #211f24;
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
flex-shrink: 0;
line-height: 22px; /* 157.143% */
}
:deep(.arco-space-item) {
width: 100%;
}
}
}
}

View File

@ -0,0 +1,46 @@
/*
* @Author: RenXiaoDong
* @Date: 2025-06-28 10:33:06
*/
export const TABLE_COLUMNS = [
{
title: '项目名称',
dataIndex: 'name',
width: 240,
fixed: 'left',
},
{
title: '项目预算',
dataIndex: 'budget',
width: 180,
prefix: "¥"
},
{
title: '关联平台账号',
dataIndex: 'media_account_count',
width: 180,
},
{
title: '关联渠道账户',
dataIndex: 'placement_account_count',
width: 180,
},
{
title: '关联内容稿件',
dataIndex: 'work_count',
width: 180,
},
{
title: '创建时间',
dataIndex: 'create_at',
width: 180,
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
{
title: '操作',
dataIndex: 'operation',
width: 100,
},
];

View File

@ -0,0 +1,63 @@
<!--
* @Author: RenXiaoDong
* @Date: 2025-06-26 17:44:16
-->
<template>
<a-modal
v-model:visible="visible"
title="删除项目"
width="480px"
modal-class="project-manage-modal"
@close="onClose"
>
<div class="flex items-center">
<img :src="icon1" width="20" height="20" class="mr-12px" />
<span>确认删除 {{ projectName }} 这个项目吗</span>
</div>
<template #footer>
<a-button size="large" @click="onClose">取消</a-button>
<a-button type="primary" class="ml-16px" status="danger" size="large" @click="onDelete"
>确认删除</a-button
>
</template>
</a-modal>
</template>
<script setup>
import { ref } from 'vue';
import { deleteProject } from '@/api/all/propertyMarketing';
import icon1 from '@/assets/img/media-account/icon-warn-1.png';
const update = inject('update');
const visible = ref(false);
const projectId = ref(null);
const projectName = ref('');
const isBatch = computed(() => Array.isArray(projectId.value));
function onClose() {
visible.value = false;
projectId.value = null;
projectName.value = '';
}
const open = (record) => {
const { id = null, name = '' } = record;
projectId.value = id;
projectName.value = name;
visible.value = true;
};
async function onDelete() {
const { code } = await deleteProject(projectId.value);
if (code === 200) {
AMessage.success('删除成功');
update()
onClose();
}
}
defineExpose({ open });
</script>

View File

@ -0,0 +1,88 @@
<template>
<a-table
ref="tableRef"
:data="dataSource"
row-key="id"
column-resizable
:pagination="false"
:scroll="{ x: '100%' }"
class="flex-1 project-table w-100%"
bordered
@sorter-change="handleSorterChange"
>
<template #empty>
<NoData text="暂无项目"/>
</template>
<template #columns>
<a-table-column
v-for="column in TABLE_COLUMNS"
:key="column.dataIndex"
:data-index="column.dataIndex"
:fixed="column.fixed"
:width="column.width"
:min-width="column.minWidth"
:sortable="column.sortable"
:align="column.align"
ellipsis
tooltip
>
<template #title>
<div class="flex items-center">
<span class="cts mr-4px">{{ column.title }}</span>
<a-tooltip v-if="column.tooltip" :content="column.tooltip" position="top">
<icon-question-circle class="tooltip-icon color-#737478" size="16" />
</a-tooltip>
</div>
</template>
<template v-if="column.dataIndex === 'create_at'" #cell="{ record }">
{{ exactFormatTime(record.create_at) }}
</template>
<template v-else-if="column.dataIndex === 'operation'" #cell="{ record }">
<div class="flex items-center">
<img class="mr-8px cursor-pointer" :src="icon1" width="14" height="14" @click="onDelete(record)" />
<a-button type="outline" size="mini" @click="onEdit(record)">编辑</a-button>
</div>
</template>
<template v-else #cell="{ record }">
{{ formatTableField(column, record, true) }}
</template>
</a-table-column>
</template>
</a-table>
</template>
<script setup>
import { ref } from 'vue';
import { formatTableField, exactFormatTime } from '@/utils/tools';
import { TABLE_COLUMNS } from './constants';
import icon1 from '@/assets/img/media-account/icon-delete.png';
const emits = defineEmits(['edit', 'sorterChange', 'delete']);
const props = defineProps({
dataSource: {
type: Array,
default: () => [],
},
});
const tableRef = ref(null);
// 处理排序变化
const handleSorterChange = (column, order) => {
emits('sorterChange', column, order === 'ascend' ? 'asc' : 'desc');
};
const onDelete = (item) => {
emits('delete', item);
};
const onEdit = (item) => {
emits('edit', item);
};
</script>
<style scoped lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,10 @@
.project-table {
.cts {
color: var(--Text-1, #211f24);
font-family: $font-family-medium;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
}

View File

@ -0,0 +1,5 @@
export const INITIAL_QUERY = {
name: '',
sort_column: undefined,
sort_order: undefined,
};

View File

@ -0,0 +1,125 @@
<template>
<div class="project-list-wrap">
<div class="filter-wrap bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid mb-16px">
<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 type="primary" class="w-112px search-btn" size="medium" @click="handleOpenAddProjectModal">
<template #icon>
<img :src="icon1" width="16" height="16" />
</template>
<template #default>添加项目</template>
</a-button>
</div>
</div>
<FilterBlock v-model:query="query" @onSearch="handleSearch" @onReset="handleReset" />
</div>
<div
class="table-wrap bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid px-24px py-24px flex-1 flex flex-col"
>
<ProjectTable
:dataSource="dataSource"
@sorterChange="handleSorterChange"
@delete="handleDelete"
@edit="handleEdit"
/>
<div v-if="pageInfo.total > 0" class="pagination-box">
<a-pagination
:total="pageInfo.total"
size="mini"
show-total
show-jumper
show-page-size
:current="pageInfo.page"
:page-size="pageInfo.page_size"
@change="onPageChange"
@page-size-change="onPageSizeChange"
/>
</div>
</div>
<AddProjectModal ref="addProjectModalRef" />
<DeleteProjectModal ref="deleteProjectModalRef" />
</div>
</template>
<script setup>
import { INITIAL_QUERY } from './constants';
import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination';
import { getProjects } from '@/api/all/propertyMarketing';
import FilterBlock from './components/filter-block';
import ProjectTable from './components/project-table';
import AddProjectModal from './components/add-project-modal';
import DeleteProjectModal from './components/project-table/delete-project-modal.vue';
import icon1 from '@/assets/img/media-account/icon-add.png';
const { dataSource, pageInfo, onPageChange, onPageSizeChange, resetPageInfo } = useTableSelectionWithPagination({
onPageChange: () => {
getData();
},
onPageSizeChange: () => {
getData();
},
});
const addProjectModalRef = ref(null);
const deleteProjectModalRef = ref(null);
const query = ref(cloneDeep(INITIAL_QUERY));
const getData = async () => {
const { page, page_size } = pageInfo.value;
const { code, data } = await getProjects({
...query.value,
page,
page_size,
});
if (code === 200) {
dataSource.value = data?.data ?? [];
pageInfo.value.total = data.total;
}
};
const handleSearch = () => {
reload();
};
const handleReset = () => {
resetPageInfo();
query.value = cloneDeep(INITIAL_QUERY);
reload();
};
const reload = () => {
pageInfo.value.page = 1;
getData();
};
const handleOpenAddProjectModal = () => {
addProjectModalRef.value?.open();
};
const handleSorterChange = (column, order) => {
query.value.sort_column = column;
query.value.sort_order = order;
reload();
};
const handleDelete = (item) => {
const { id, name } = item;
deleteProjectModalRef.value?.open({ id, name: `${name}` });
};
const handleEdit = (item) => {
addProjectModalRef.value?.open(item.id);
};
onMounted(() => {
getData();
});
provide('update', getData);
</script>
<style scoped lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,29 @@
.project-list-wrap {
height: 100%;
display: flex;
flex-direction: column;
.filter-wrap {
.top {
.title {
font-family: $font-family-medium;
font-style: normal;
}
:deep(.arco-btn) {
.arco-btn-icon {
line-height: 16px;
}
}
}
}
.table-wrap {
display: flex;
flex-direction: column;
.pagination-box {
display: flex;
width: 100%;
padding: 16px 24px;
justify-content: flex-end;
align-items: center;
}
}
}

View File

@ -32,7 +32,7 @@
<div class="filter-row-item flex items-center"> <div class="filter-row-item flex items-center">
<span class="label">运营人员</span> <span class="label">运营人员</span>
<a-space class="w-160px"> <a-space class="w-160px">
<OperatorSelect v-model="query.operator_id" :options="operators" /> <CommonSelect v-model="query.operator_id" :multiple="false" :options="operators" />
</a-space> </a-space>
</div> </div>
</div> </div>
@ -43,13 +43,13 @@
<a-range-picker v-model="query.data_time" size="medium" allow-clear format="YYYY-MM-DD" class="w-100%" /> <a-range-picker v-model="query.data_time" size="medium" allow-clear format="YYYY-MM-DD" class="w-100%" />
</a-space> </a-space>
</div> </div>
<a-button class="w-84px search-btn mr-12px" size="medium" @click="handleSearch"> <a-button type="outline" class="mr-12px" size="medium" @click="handleSearch">
<template #icon> <template #icon>
<icon-search /> <icon-search />
</template> </template>
<template #default>搜索</template> <template #default>搜索</template>
</a-button> </a-button>
<a-button class="w-84px reset-btn" size="medium" @click="handleReset"> <a-button size="medium" @click="handleReset">
<template #icon> <template #icon>
<icon-refresh /> <icon-refresh />
</template> </template>
@ -74,13 +74,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import EchartsItem from './components/echarts-item/index'; import EchartsItem from './components/echarts-item/index';
import { PLATFORM_LIST } from '../common_constants'; import { PLATFORM_LIST } from '@/utils/platform';
import { import {
getPlacementAccountsTrend, getPlacementAccountsTrend,
getPlacementAccountProjectsTrend, getPlacementAccountProjectsTrend,
fetchAccountOperators, fetchAccountOperators,
} from '@/api/all/propertyMarketing'; } from '@/api/all/propertyMarketing';
import OperatorSelect from '@/views/property-marketing/media-account/components/operator-select/index.vue'; import CommonSelect from '@/components/common-select';
import AccountSelect from '@/views/components/common/AccountSelect.vue'; import AccountSelect from '@/views/components/common/AccountSelect.vue';
import PlanSelect from '@/views/components/common/PlanSelect.vue'; import PlanSelect from '@/views/components/common/PlanSelect.vue';

View File

@ -16,11 +16,11 @@
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<a-button class="w-110px search-btn mr-12px" size="medium" @click="handleExport"> <a-button type="outline" class="mr-12px" size="medium" @click="handleExport">
<template #icon> <icon-download /> </template> <template #icon> <icon-download /> </template>
<template #default>导出数据</template> <template #default>导出数据</template>
</a-button> </a-button>
<a-button class="w-110px search-btn" size="medium" @click="openCustomColumn"> <a-button type="outline" size="medium" @click="openCustomColumn">
<template #icon> <template #icon>
<img :src="icon1" width="14" height="14" /> <img :src="icon1" width="14" height="14" />
</template> </template>
@ -106,7 +106,7 @@
</div> </div>
</template> </template>
<template v-else-if="column.dataIndex === 'operation'" #cell="{ record }"> <template v-else-if="column.dataIndex === 'operation'" #cell="{ record }">
<a-button type="outline" size="small" class="search-btn" @click="handleDetail(record)">详情</a-button> <a-button type="outline" size="small" @click="handleDetail(record)">详情</a-button>
</template> </template>
<template v-else-if="column.isRateField" #cell="{ record }"> <template v-else-if="column.isRateField" #cell="{ record }">

View File

@ -19,7 +19,7 @@
<div v-if="!isAccountTab" class="filter-row-item flex items-center"> <div v-if="!isAccountTab" class="filter-row-item flex items-center">
<span class="label">计划分组</span> <span class="label">计划分组</span>
<a-space class="w-200px"> <a-space class="w-200px">
<group-select v-model="query.group_ids" multiple :options="groups" @change="handleSearch" /> <CommonSelect v-model="query.group_ids" multiple :options="groups" @change="handleSearch" />
</a-space> </a-space>
</div> </div>
<div class="filter-row-item flex items-center"> <div class="filter-row-item flex items-center">
@ -31,7 +31,7 @@
<div class="filter-row-item flex items-center"> <div class="filter-row-item flex items-center">
<span class="label">运营人员</span> <span class="label">运营人员</span>
<a-space class="w-160px"> <a-space class="w-160px">
<OperatorSelect v-model="query.operator_id" :options="operators" @change="handleSearch" /> <CommonSelect v-model="query.operator_id" :multiple="false" :options="operators" @change="handleSearch" />
</a-space> </a-space>
</div> </div>
</div> </div>
@ -55,13 +55,13 @@
/> />
</a-space> </a-space>
</div> </div>
<a-button class="w-84px search-btn mr-12px" size="medium" @click="handleSearch"> <a-button type="outline" class="mr-12px" size="medium" @click="handleSearch">
<template #icon> <template #icon>
<icon-search /> <icon-search />
</template> </template>
<template #default>搜索</template> <template #default>搜索</template>
</a-button> </a-button>
<a-button class="w-84px reset-btn" size="medium" @click="handleReset"> <a-button size="medium" @click="handleReset">
<template #icon> <template #icon>
<icon-refresh /> <icon-refresh />
</template> </template>
@ -78,9 +78,8 @@ import {
getPlacementAccountsList, getPlacementAccountsList,
getPlacementAccountOperators, getPlacementAccountOperators,
} from '@/api/all/propertyMarketing'; } from '@/api/all/propertyMarketing';
import GroupSelect from '../group-select';
import OperatorSelect from '@/views/property-marketing/put-account/components/operator-select'; import CommonSelect from '@/components/common-select';
import StatusSelect from '@/views/property-marketing/media-account/components/status-select'; import StatusSelect from '@/views/property-marketing/media-account/components/status-select';
import AccountSelect from '@/views/property-marketing/put-account/components/account-select'; import AccountSelect from '@/views/property-marketing/put-account/components/account-select';

View File

@ -16,7 +16,7 @@
</a-form-item> </a-form-item>
</a-form> </a-form>
<template #footer> <template #footer>
<a-button class="cancel-btn" @click="onClose">取消</a-button> <a-button @click="onClose">取消</a-button>
<a-button type="primary" class="ml-16px" @click="onSubmit">确认</a-button> <a-button type="primary" class="ml-16px" @click="onSubmit">确认</a-button>
</template> </template>
</a-modal> </a-modal>

View File

@ -9,7 +9,7 @@
<span>确认删除 "{{ groupName }}" 这个分组吗</span> <span>确认删除 "{{ groupName }}" 这个分组吗</span>
</div> </div>
<template #footer> <template #footer>
<a-button class="cancel-btn" size="large" @click="onClose">取消</a-button> <a-button size="large" @click="onClose">取消</a-button>
<a-button type="primary" class="ml-16px danger-btn" status="danger" size="large" @click="onDelete" <a-button type="primary" class="ml-16px danger-btn" status="danger" size="large" @click="onDelete"
>确认删除</a-button >确认删除</a-button
> >

View File

@ -16,11 +16,11 @@
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<a-button class="w-110px search-btn mr-12px" size="medium" @click="handleExport"> <a-button type="outline" class="mr-12px" size="medium" @click="handleExport">
<template #icon> <icon-download /> </template> <template #icon> <icon-download /> </template>
<template #default>导出数据</template> <template #default>导出数据</template>
</a-button> </a-button>
<a-button class="w-110px search-btn" size="medium" @click="openCustomColumn"> <a-button type="outline" size="medium" @click="openCustomColumn">
<template #icon> <template #icon>
<img :src="icon1" width="14" height="14" /> <img :src="icon1" width="14" height="14" />
</template> </template>
@ -106,7 +106,7 @@
</div> </div>
</template> </template>
<template v-else-if="column.dataIndex === 'operation'" #cell="{ record }"> <template v-else-if="column.dataIndex === 'operation'" #cell="{ record }">
<a-button type="outline" size="small" class="search-btn" @click="handleDetail(record)">详情</a-button> <a-button type="outline" size="small" @click="handleDetail(record)">详情</a-button>
</template> </template>
<template v-else-if="column.isRateField" #cell="{ record }"> <template v-else-if="column.isRateField" #cell="{ record }">

View File

@ -9,7 +9,7 @@
<a-tab-pane key="1" title="账户"></a-tab-pane> <a-tab-pane key="1" title="账户"></a-tab-pane>
<a-tab-pane key="2" title="计划"></a-tab-pane> <a-tab-pane key="2" title="计划"></a-tab-pane>
<template v-if="!isAccountTab" #extra> <template v-if="!isAccountTab" #extra>
<a-button class="w-112px mr-12px search-btn flex items-center" size="medium" @click="handleOpenGroupModal"> <a-button type="outline" class="mr-12px flex items-center" size="medium" @click="handleOpenGroupModal">
<template #icon> <template #icon>
<img :src="icon2" width="16" height="16" /> <img :src="icon2" width="16" height="16" />
</template> </template>

View File

@ -2,16 +2,6 @@
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
:deep(.search-btn) {
border-radius: 4px;
border: 1px solid var(--Brand-Brand-6, #6d4cfe);
color: #6d4cfe;
}
:deep(.reset-btn) {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
}
.filter-wrap { .filter-wrap {
border-radius: 8px; border-radius: 8px;
border: 1px solid #e6e6e8; border: 1px solid #e6e6e8;

View File

@ -2,10 +2,6 @@
* @Author: RenXiaoDong * @Author: RenXiaoDong
* @Date: 2025-06-26 17:44:16 * @Date: 2025-06-26 17:44:16
--> -->
<!--
* @Author: RenXiaoDong
* @Date: 2025-06-26 17:23:52
-->
<template> <template>
<a-modal <a-modal
v-model:visible="visible" v-model:visible="visible"
@ -19,7 +15,7 @@
<span>确认删除 {{ accountName }} 这个账户吗</span> <span>确认删除 {{ accountName }} 这个账户吗</span>
</div> </div>
<template #footer> <template #footer>
<a-button class="cancel-btn" size="large" @click="onClose">取消</a-button> <a-button size="large" @click="onClose">取消</a-button>
<a-button type="primary" class="ml-16px danger-btn" status="danger" size="large" @click="onDelete" <a-button type="primary" class="ml-16px danger-btn" status="danger" size="large" @click="onDelete"
>确认删除</a-button >确认删除</a-button
> >

View File

@ -41,6 +41,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">
<span class="label">所属项目</span>
<span v-if="!item.projects.length" class="cts">-</span>
<div v-else class="flex items-center">
<a-tooltip
v-if="item.projects.length > 2"
position="bottom"
:content="
item.projects
.slice(2)
.map((v) => v.name)
.join(',')
"
>
<div class="tag-box">
<span class="text">{{ `+${item.projects.length - 2}` }}</span>
</div>
</a-tooltip>
<div v-for="(project, index) in item.projects.slice(0, 2)" :key="index" class="tag-box">
<span class="text">{{ project.name }}</span>
</div>
</div>
</div>
<div class="field-row"> <div class="field-row">
<span class="label">账户总消耗</span> <span class="label">账户总消耗</span>
<span class="cts">{{ `${formatNumberShow({ value: item.total_use_amount, showExactValue: true })}` }}</span> <span class="cts">{{ `${formatNumberShow({ value: item.total_use_amount, showExactValue: true })}` }}</span>
@ -53,17 +77,18 @@
<img :src="icon1" width="16" height="16" class="mr-8px cursor-pointer" @click="openDelete(item)" /> <img :src="icon1" width="16" height="16" class="mr-8px cursor-pointer" @click="openDelete(item)" />
<a-button <a-button
v-if="showPauseButton(item.status)" v-if="showPauseButton(item.status)"
class="w-64px search-btn mr-8px" type="outline"
class="mr-8px"
size="mini" size="mini"
@click="handlePause(item)" @click="handlePause(item)"
> >
<template #default>暂停同步</template> <template #default>暂停同步</template>
</a-button> </a-button>
<a-button class="w-64px search-btn mr-8px" size="mini" @click="handleReauthorize(item)"> <a-button type="outline" class="mr-8px" size="mini" @click="handleReauthorize(item)">
<template #default>获取凭证</template> <template #default>获取凭证</template>
</a-button> </a-button>
<a-button class="w-40px search-btn" size="mini" @click="openEdit(item)"> <a-button type="outline" size="mini" @click="openEdit(item)">
<template #default>编辑</template> <template #default>编辑</template>
</a-button> </a-button>
</div> </div>
@ -76,7 +101,7 @@
<script setup> <script setup>
import { defineProps, ref, computed } from 'vue'; import { defineProps, ref, computed } from 'vue';
import { PLATFORM_LIST } from '@/views/property-marketing/put-account/common_constants'; import { PLATFORM_LIST } from '@/utils/platform';
import { EnumStatus } from '@/views/property-marketing/media-account/components/status-select/constants'; import { EnumStatus } from '@/views/property-marketing/media-account/components/status-select/constants';
import { formatNumberShow, exactFormatTime } from '@/utils/tools'; import { formatNumberShow, exactFormatTime } from '@/utils/tools';

View File

@ -9,7 +9,7 @@
<span>确认暂停同步 {{ accountName }} 这个账号的数据吗</span> <span>确认暂停同步 {{ accountName }} 这个账号的数据吗</span>
</div> </div>
<template #footer> <template #footer>
<a-button class="cancel-btn" size="large" @click="onClose">取消</a-button> <a-button size="large" @click="onClose">取消</a-button>
<a-button type="primary" class="ml-16px !bg-#f64b31" size="large" @click="onConfirm">确定</a-button> <a-button type="primary" class="ml-16px !bg-#f64b31" size="large" @click="onConfirm">确定</a-button>
</template> </template>
</a-modal> </a-modal>

View File

@ -1,9 +1,3 @@
@mixin ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-container { .card-container {
flex: 1; flex: 1;
display: grid; display: grid;

View File

@ -111,6 +111,9 @@
</a-radio> </a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item label="所属项目" field="project_ids">
<CommonSelect v-model="form.project_ids" :options="projects" placeholder="请选择…" size="large" />
</a-form-item>
<template v-if="isEdit"> <template v-if="isEdit">
<a-form-item label="账户总消耗" field="total_use_amount"> <a-form-item label="账户总消耗" field="total_use_amount">
<a-input v-model="form.total_use_amount" placeholder="请输入..." size="large" disabled /> <a-input v-model="form.total_use_amount" placeholder="请输入..." size="large" disabled />
@ -131,7 +134,7 @@
</template> </template>
</a-form> </a-form>
<template #footer> <template #footer>
<a-button size="large" class="cancel-btn" @click="onClose">取消</a-button> <a-button size="large" @click="onClose">取消</a-button>
<a-button type="primary" size="large" @click="onSubmit" :loading="importLoading"> <a-button type="primary" size="large" @click="onSubmit" :loading="importLoading">
{{ confirmBtnText }} {{ confirmBtnText }}
</a-button> </a-button>
@ -148,7 +151,9 @@ import { ref, defineEmits } from 'vue';
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 { PLATFORM_LIST, ENUM_PLATFORM } from '@/views/property-marketing/put-account/common_constants'; import CommonSelect from '@/components/common-select';
import { PLATFORM_LIST, ENUM_PUT_ACCOUNT_PLATFORM } from '@/utils/platform';
import { showExportNotification } from '@/utils/arcoD'; import { showExportNotification } from '@/utils/arcoD';
import { genRandomId } from '@/utils/tools'; import { genRandomId } from '@/utils/tools';
@ -158,6 +163,7 @@ import {
putPlacementAccounts, putPlacementAccounts,
getPlacementAccountsTemplateUrl, getPlacementAccountsTemplateUrl,
batchPlacementAccounts, batchPlacementAccounts,
getProjectList,
} 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';
@ -177,6 +183,7 @@ const INITIAL_FORM = {
holder_name: '', holder_name: '',
platform: 0, platform: 0,
is_sync_project: 1, is_sync_project: 1,
project_ids: [],
}; };
const visible = ref(false); const visible = ref(false);
@ -191,6 +198,7 @@ const authorizedAccountModalRef = ref(null);
const uploadRef = ref(null); const uploadRef = ref(null);
const file = ref(null); const file = ref(null);
const form = ref(cloneDeep(INITIAL_FORM)); const form = ref(cloneDeep(INITIAL_FORM));
const projects = ref([]);
const importLoading = ref(false); const importLoading = ref(false);
const rules = { const rules = {
@ -245,6 +253,7 @@ const reset = () => {
fileName.value = ''; fileName.value = '';
file.value = null; file.value = null;
isEdit.value = false; isEdit.value = false;
projects.value = [];
importLoading.value = false; importLoading.value = false;
uploadStatus.value = UploadStatus.DEFAULT; uploadStatus.value = UploadStatus.DEFAULT;
uploadType.value = 'manual'; uploadType.value = 'manual';
@ -258,12 +267,19 @@ const open = (accountId = '') => {
id.value = accountId; id.value = accountId;
isEdit.value = !!accountId; isEdit.value = !!accountId;
getProjects();
if (accountId) { if (accountId) {
getAccountDetail(); getAccountDetail();
} }
visible.value = true; visible.value = true;
}; };
const getProjects = async () => {
const { code, data } = await getProjectList();
if (code === 200) {
projects.value = data;
}
};
const getAccountDetail = async () => { const getAccountDetail = async () => {
const { code, data } = await getPlacementAccountsDetail(id.value); const { code, data } = await getPlacementAccountsDetail(id.value);
@ -288,7 +304,7 @@ const handleBatchImport = async () => {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },
}); });
if (code === 200) { if (code === 200) {
const id = genRandomId(); const id = genRandomId();
showExportNotification(`正在导入“${file.value.name}”,请稍后...`, { id }); showExportNotification(`正在导入“${file.value.name}”,请稍后...`, { id });
@ -308,7 +324,7 @@ const handleBatchImport = async () => {
const handleAdd = async () => { const handleAdd = async () => {
// 聚光无子账号 // 聚光无子账号
if (form.value.platform === ENUM_PLATFORM.jg) { if (form.value.platform === ENUM_PUT_ACCOUNT_PLATFORM.jg) {
const { code, data } = await postPlacementAccounts(form.value); const { code, data } = await postPlacementAccounts(form.value);
if (code === 200) { if (code === 200) {
update(); update();

View File

@ -55,7 +55,7 @@
</template> </template>
</div> </div>
<template #footer> <template #footer>
<a-button size="large" class="cancel-btn" @click="close">取消</a-button> <a-button size="large" @click="close">取消</a-button>
<a-button type="primary" size="large" @click="handleOk">{{ confirmBtnText }} </a-button> <a-button type="primary" size="large" @click="handleOk">{{ confirmBtnText }} </a-button>
</template> </template>
</a-modal> </a-modal>
@ -67,7 +67,6 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { defineExpose, ref, computed, defineEmits } from 'vue'; import { defineExpose, ref, computed, defineEmits } from 'vue';
import { exactFormatTime } from '@/utils/tools'; import { exactFormatTime } from '@/utils/tools';
import { ENUM_PLATFORM } from '@/views/property-marketing/put-account/common_constants';
import { import {
putPlacementAccountsAuthorized, putPlacementAccountsAuthorized,
getPlacementAccountsAuthorizedStatus, getPlacementAccountsAuthorizedStatus,

View File

@ -42,18 +42,24 @@
<div class="filter-row-item flex items-center"> <div class="filter-row-item flex items-center">
<span class="label">运营人员</span> <span class="label">运营人员</span>
<a-space class="w-160px"> <a-space class="w-160px">
<OperatorSelect v-model="query.operator_id" :options="operators" @change="handleSearch" /> <CommonSelect v-model="query.operator_id" :multiple="false" :options="operators" @change="handleSearch" />
</a-space> </a-space>
</div> </div>
</div> </div>
<div class="filter-row flex"> <div class="filter-row flex">
<a-button class="w-84px search-btn mr-12px" size="medium" @click="handleSearch"> <div class="filter-row-item flex items-center">
<span class="label">所属项目</span>
<a-space class="w-200px">
<CommonSelect v-model="query.project_ids" :options="projects" @change="handleSearch" />
</a-space>
</div>
<a-button type="outline" class="mr-12px" size="medium" @click="handleSearch">
<template #icon> <template #icon>
<icon-search /> <icon-search />
</template> </template>
<template #default>搜索</template> <template #default>搜索</template>
</a-button> </a-button>
<a-button class="w-84px reset-btn" size="medium" @click="handleReset"> <a-button size="medium" @click="handleReset">
<template #icon> <template #icon>
<icon-refresh /> <icon-refresh />
</template> </template>
@ -65,10 +71,10 @@
<script setup> <script setup>
import { defineEmits, defineProps } from 'vue'; import { defineEmits, defineProps } from 'vue';
import { getPlacementAccountOperators } from '@/api/all/propertyMarketing'; import { getPlacementAccountOperators, getProjectList } from '@/api/all/propertyMarketing';
import { PLATFORM_LIST } from '@/views/property-marketing/put-account/common_constants'; import { PLATFORM_LIST } from '@/utils/platform';
import StatusSelect from '@/views/property-marketing/media-account/components/status-select'; import StatusSelect from '@/views/property-marketing/media-account/components/status-select';
import OperatorSelect from '@/views/property-marketing/put-account/components/operator-select'; import CommonSelect from '@/components/common-select';
const props = defineProps({ const props = defineProps({
query: { query: {
@ -80,6 +86,7 @@ const props = defineProps({
const emits = defineEmits('onSearch', 'onReset', 'update:query'); const emits = defineEmits('onSearch', 'onReset', 'update:query');
const operators = ref([]); const operators = ref([]);
const projects = ref([]);
const handleSearch = () => { const handleSearch = () => {
emits('update:query', props.query); emits('update:query', props.query);
@ -97,9 +104,16 @@ const getOperators = async () => {
operators.value = data; operators.value = data;
} }
}; };
const getProjects = async () => {
const { code, data } = await getProjectList();
if (code === 200) {
projects.value = data;
}
};
onMounted(() => { onMounted(() => {
getOperators(); getOperators();
getProjects();
}); });
</script> </script>

View File

@ -18,7 +18,7 @@
</div> </div>
</div> </div>
<template #footer> <template #footer>
<a-button size="large" class="mr-16px cancel-btn" @click="close">取消</a-button> <a-button size="large" @click="close">取消</a-button>
<a-button type="primary" size="large" @click="handleOk"> 去授权 </a-button> <a-button type="primary" size="large" @click="handleOk"> 去授权 </a-button>
</template> </template>
</a-modal> </a-modal>

View File

@ -103,7 +103,7 @@
>已选<span class="color-#6D4CFE num mx-3px">{{ selectedRows.length }}</span>账户</span >已选<span class="color-#6D4CFE num mx-3px">{{ selectedRows.length }}</span>账户</span
> >
<div class="flex items-center"> <div class="flex items-center">
<a-button class="cancel-btn" size="large" @click="onClose">取消</a-button> <a-button size="large" @click="onClose">取消</a-button>
<a-button <a-button
type="primary" type="primary"
class="ml-16px" class="ml-16px"

View File

@ -8,4 +8,5 @@ export const INITIAL_QUERY = {
status: '', status: '',
platform: '', platform: '',
operator_id: '', operator_id: '',
project_ids: [],
}; };

View File

@ -8,7 +8,7 @@
<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">
<a-button type="primary" class="w-112px search-btn" size="medium" @click="handleOpenAccountModal"> <a-button type="primary" class="w-112px" size="medium" @click="handleOpenAccountModal">
<template #icon> <template #icon>
<img :src="icon1" width="16" height="16" /> <img :src="icon1" width="16" height="16" />
</template> </template>
@ -53,7 +53,7 @@
</template> </template>
<div v-else> <div v-else>
<a-space v-if="isAbNormalStatus" class="flex items-center"> <a-space v-if="isAbNormalStatus" class="flex items-center">
<a-button class="w-96px err-btn" size="mini" @click="handleOpenAbnormalAccount"> <a-button type="primary" status="danger" size="mini" @click="handleOpenAbnormalAccount">
<template #default>查看异常账号</template> <template #default>查看异常账号</template>
</a-button> </a-button>
</a-space> </a-space>
@ -79,7 +79,7 @@
show-page-size show-page-size
:page-size-options="[8, 16, 20, 32, 64]" :page-size-options="[8, 16, 20, 32, 64]"
:current="pageInfo.page" :current="pageInfo.page"
:page-size="pageInfo.pageSize" :page-size="pageInfo.page_size"
@change="onPageChange" @change="onPageChange"
@page-size-change="onPageSizeChange" @page-size-change="onPageSizeChange"
/> />
@ -117,7 +117,7 @@ let queryTaskTimer = null;
const pageInfo = ref({ const pageInfo = ref({
page: 1, page: 1,
pageSize: 20, page_size: 20,
total: 0, total: 0,
}); });
const query = ref(cloneDeep(INITIAL_QUERY)); const query = ref(cloneDeep(INITIAL_QUERY));
@ -173,10 +173,10 @@ const getHealthData = async () => {
} }
}; };
const getAccountData = async () => { const getAccountData = async () => {
const { page, pageSize } = pageInfo.value; const { page, page_size } = pageInfo.value;
const { code, data, total } = await getPlacementAccounts({ const { code, data, total } = await getPlacementAccounts({
page, page,
page_size: pageSize, page_size,
...query.value, ...query.value,
}); });
if (code === 200) { if (code === 200) {
@ -189,11 +189,11 @@ const reload = () => {
getData(); getData();
}; };
const handleSearch = () => { const handleSearch = () => {
getData(); reload();
}; };
const handleReset = () => { const handleReset = () => {
pageInfo.value.page = 1; pageInfo.value.page = 1;
pageInfo.value.pageSize = 20; pageInfo.value.page_size = 20;
pageInfo.value.total = 0; pageInfo.value.total = 0;
selectedItems.value = []; selectedItems.value = [];
query.value = cloneDeep(INITIAL_QUERY); query.value = cloneDeep(INITIAL_QUERY);
@ -205,7 +205,7 @@ const onPageChange = (current) => {
getData(); getData();
}; };
const onPageSizeChange = (pageSize) => { const onPageSizeChange = (pageSize) => {
pageInfo.value.pageSize = pageSize; pageInfo.value.page_size = pageSize;
reload(); reload();
}; };

View File

@ -2,16 +2,6 @@
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
:deep(.search-btn) {
border-radius: 4px;
border: 1px solid var(--Brand-Brand-6, #6d4cfe);
color: #6d4cfe;
}
:deep(.reset-btn) {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
}
.filter-wrap { .filter-wrap {
.top { .top {
.title { .title {
@ -41,15 +31,6 @@
color: #211f24; color: #211f24;
} }
} }
.err-btn {
background-color: #f64b31 !important;
color: var(--BG-white, #fff);
font-family: 'PingFang SC';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 166.667% */
}
.operation-btn { .operation-btn {
padding: 0; padding: 0;
cursor: pointer; cursor: pointer;

Some files were not shown because too many files have changed in this diff Show More