Merge remote-tracking branch 'origin/main' into feature/0724_新媒体账号状态重构

# Conflicts:
#	src/views/property-marketing/media-account/account-manage/components/account-table/index.vue
#	src/views/property-marketing/put-account/account-data/components/filter-block/index.vue
#	src/views/property-marketing/put-account/account-manage/components/account-table/index.vue
#	src/views/property-marketing/put-account/account-manage/components/filter-block/index.vue
This commit is contained in:
rd
2025-07-25 10:33:58 +08:00
106 changed files with 2231 additions and 537 deletions

View File

@ -21,13 +21,13 @@
</a-space>
</div>
<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>
<icon-search />
</template>
<template #default>搜索</template>
</a-button>
<a-button class="w-84px reset-btn" size="medium" @click="handleReset">
<a-button size="medium" @click="handleReset">
<template #icon>
<icon-refresh />
</template>

View File

@ -3,23 +3,11 @@
display: flex;
flex-direction: column;
:deep(.search-btn) {
border-radius: 4px;
border: 1px solid var(--Brand-Brand-6, #6d4cfe);
color: #6d4cfe;
}
:deep(.edit-btn) {
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 {
width: 100%;

View File

@ -1,13 +1,4 @@
.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-form-item {
margin-bottom: 16px;

View File

@ -25,13 +25,13 @@
</div>
<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>
<icon-search />
</template>
<template #default>搜索</template>
</a-button>
<a-button class="w-84px reset-btn" size="medium">
<a-button size="medium">
<template #icon>
<icon-refresh />
</template>

View File

@ -25,13 +25,13 @@
</div>
<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>
<icon-search />
</template>
<template #default>搜索</template>
</a-button>
<a-button class="w-84px reset-btn" size="medium">
<a-button size="medium">
<template #icon>
<icon-refresh />
</template>

View File

@ -15,11 +15,11 @@
>
</div>
<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 #default>导出数据</template>
</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>
<img :src="icon1" width="14" height="14" />
</template>
@ -115,7 +115,7 @@
}}
</template>
<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 v-else-if="column.isRateField" #cell="{ record }">
@ -144,7 +144,7 @@
</a-table-column>
<a-table-column data-index="operation" fixed="right" width="100" title="操作">
<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>
</a-table-column>
</template>

View File

@ -65,7 +65,7 @@
<template #footer>
<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>
</div>
</template>

View File

@ -87,13 +87,5 @@
}
}
.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">
<span class="label">分组</span>
<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>
</div>
<div class="filter-row-item flex items-center">
@ -31,7 +31,7 @@
<div class="filter-row-item flex items-center">
<span class="label">运营人员</span>
<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>
</div>
</div>
@ -47,13 +47,13 @@
</a-select>
</a-space>
</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>
<icon-search />
</template>
<template #default>搜索</template>
</a-button>
<a-button class="w-84px reset-btn" size="medium" @click="handleReset">
<a-button class="w-84px" size="medium" @click="handleReset">
<template #icon>
<icon-refresh />
</template>
@ -66,9 +66,8 @@
<script setup>
import { reactive, defineEmits, defineProps } from 'vue';
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 CommonSelect from '@/components/common-select';
const props = defineProps({
query: {

View File

@ -2,16 +2,6 @@
height: 100%;
display: flex;
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 {
border-radius: 8px;
border: 1px solid #e6e6e8;

View File

@ -36,13 +36,13 @@
/>
</a-space>
</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>
<icon-search />
</template>
<template #default>搜索</template>
</a-button>
<a-button class="w-84px reset-btn" size="medium" @click="handleReset">
<a-button class="w-84px" size="medium" @click="handleReset">
<template #icon>
<icon-refresh />
</template>

View File

@ -19,16 +19,6 @@
padding: 10px 0;
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 {
width: 100%;

View File

@ -15,7 +15,7 @@
<span>确认删除 {{ accountName }} 这个账号吗</span>
</div>
<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
>

View File

@ -46,6 +46,30 @@
<span class="label">运营人员</span>
<span class="cts">{{ item.operator?.name || '-' }}</span>
</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">
<span class="label">分组</span>
<span class="cts">{{ item.group?.name || '-' }}</span>

View File

@ -9,7 +9,7 @@
<span>确认暂停同步 {{ accountName }} 这个账号的数据吗</span>
</div>
<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>
</template>
</a-modal>

View File

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

View File

@ -19,12 +19,11 @@ import {
Message as AMessage,
Textarea,
} 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 ImportPromptModal from '../import-prompt-modal';
import StatusBox from '@/views/property-marketing/media-account/components/status-select/status-box.tsx';
import SyncDataModal from '../sync-data-modal';
import CommonSelect from '@/components/common-select';
// import { downloadByUrl } from '@/utils/tools';
import { showExportNotification } from '@/utils/arcoD';
@ -37,6 +36,7 @@ import {
putMediaAccounts,
getTemplateUrl,
batchMediaAccounts,
getProjectList,
} from '@/api/all/propertyMarketing';
import icon1 from '@/assets/img/media-account/icon-download.png';
@ -58,6 +58,7 @@ const INITIAL_FORM = {
platform: 1,
group_id: undefined,
tag_ids: [],
project_ids: [],
end_work_link: undefined,
cookie: undefined,
};
@ -66,6 +67,7 @@ export default {
setup(props, { emit, expose }) {
const groupOptions = ref([]);
const tagOptions = ref([]);
const projects = ref([]);
const visible = ref(false);
const uploadType = ref('manual');
const uploadStatus = ref(UploadStatus.DEFAULT);
@ -126,6 +128,12 @@ export default {
tagOptions.value = data;
}
};
const getProjects = async () => {
const { code, data } = await getProjectList();
if (code === 200) {
projects.value = data;
}
};
function handleUpload(option) {
const { fileItem } = option;
uploadStatus.value = UploadStatus.WAITING;
@ -141,6 +149,9 @@ export default {
const reset = () => {
formRef.value?.resetFields();
formRef.value?.clearValidate();
groupOptions.value = [];
tagOptions.value = [];
projects.value = [];
form.value = cloneDeep(INITIAL_FORM);
fileName.value = '';
file.value = null;
@ -162,6 +173,7 @@ export default {
}
getGroups();
getTags();
getProjects();
visible.value = true;
};
const getAccountDetail = async () => {
@ -364,8 +376,16 @@ export default {
<FormItem label="号码持有人" field="holder_name">
<Input v-model={form.value.holder_name} placeholder="请输入..." class="w-240px" size="large" />
</FormItem>
<FormItem label="所属项目">
<CommonSelect
v-model={form.value.project_ids}
options={projects.value}
placeholder="请选择…"
size="large"
/>
</FormItem>
<FormItem label="选择分组">
<GroupSelect
<CommonSelect
v-model={form.value.group_id}
multiple={false}
options={groupOptions.value}
@ -373,8 +393,9 @@ export default {
size="large"
/>
</FormItem>
<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
label="笔记链接"
@ -406,7 +427,6 @@ export default {
v-model={form.value.cookie}
placeholder="请输入..."
size="large"
max-length={500}
auto-size={{ minRows: 5, maxRows: 8 }}
/>
</FormItem>
@ -415,7 +435,7 @@ export default {
)}
</Form>
<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 type="primary" size="large" loading={importLoading.value} onClick={onSubmit}>

View File

@ -76,15 +76,10 @@
</div>
<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
v-if="[MODAL_STATE.SUCCESS, MODAL_STATE.FAILED].includes(modalState)"
size="large"
class="cancel-btn"
@click="close"
>
<a-button v-if="[MODAL_STATE.SUCCESS, MODAL_STATE.FAILED].includes(modalState)" size="large" @click="close">
取消
</a-button>
<a-button type="primary" size="large" @click="handleOk">

View File

@ -30,7 +30,7 @@
<a-form-item label="选择分组" required>
<template v-if="editType === 'all'">
<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>
</template>
</a-form-item>
@ -47,7 +47,7 @@
<a-table-column title="选择分组" data-index="group_id">
<template #cell="{ record }">
<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>
</template>
</a-table-column>
@ -56,7 +56,7 @@
</template>
</a-form>
<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>
</template>
</a-modal>
@ -65,7 +65,7 @@
<script setup>
import { ref, reactive } from 'vue';
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';

View File

@ -76,7 +76,7 @@
</template>
</a-form>
<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>
</template>
</a-modal>

View File

@ -33,16 +33,20 @@
<span class="label">平台</span>
<a-space class="w-160px">
<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">{{
item.label
}}</a-option>
<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-space>
</div>
<div class="filter-row-item flex items-center">
<span class="label">运营人员</span>
<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>
</div>
</div>
@ -50,22 +54,28 @@
<div class="filter-row-item flex items-center">
<span class="label">分组</span>
<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>
</div>
<div class="filter-row-item flex items-center">
<span class="label">标签</span>
<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>
</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>
<icon-search />
</template>
<template #default>搜索</template>
</a-button>
<a-button class="w-84px reset-btn" size="medium" @click="handleReset">
<a-button class="w-84px" size="medium" @click="handleReset">
<template #icon>
<icon-refresh />
</template>
@ -77,13 +87,17 @@
<script setup>
import { reactive, defineEmits, defineProps } from 'vue';
import { fetchAccountTags, fetchAccountGroups, fetchAccountOperators } from '@/api/all/propertyMarketing';
import TagSelect from '@/views/property-marketing/media-account/components/tag-select';
import GroupSelect from '@/views/property-marketing/media-account/components/group-select';
import OperatorSelect from '@/views/property-marketing/media-account/components/operator-select';
import {
fetchAccountTags,
getProjectList,
fetchAccountGroups,
fetchAccountOperators,
} from '@/api/all/propertyMarketing';
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({
query: {
@ -97,6 +111,7 @@ const emits = defineEmits('onSearch', 'onReset', 'update:query');
const tags = ref([]);
const groups = ref([]);
const operators = ref([]);
const projects = ref([]);
const handleSearch = () => {
emits('update:query', props.query);
@ -127,11 +142,18 @@ const getOperators = async () => {
operators.value = data;
}
};
const getProjects = async () => {
const { code, data } = await getProjectList();
if (code === 200) {
projects.value = data;
}
};
onMounted(() => {
getTags();
getGroups();
getOperators();
getProjects();
});
defineExpose({

View File

@ -16,7 +16,7 @@
</a-form-item>
</a-form>
<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>
</template>
</a-modal>

View File

@ -9,7 +9,7 @@
<span>确认删除 "{{ groupName }}" 这个分组吗</span>
</div>
<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
>

View File

@ -21,7 +21,7 @@
</div>
</div>
<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>
</template>
</a-modal>

View File

@ -92,13 +92,12 @@
</div>
<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
v-if="modalState === MODAL_STATE.SUCCESS || modalState === MODAL_STATE.FAILED"
size="large"
class="cancel-btn"
@click="close"
>
取消

View File

@ -20,7 +20,7 @@
</div>
</div>
<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>
</template>
</a-modal>

View File

@ -1,7 +1,3 @@
<!--
* @Author: AI
* @Date: 2025-06-27
-->
<template>
<a-modal
v-model:visible="visible"
@ -16,7 +12,7 @@
</a-form-item>
</a-form>
<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>
</template>
</a-modal>

View File

@ -9,7 +9,7 @@
<span>确认删除 "{{ tagName }}" 这个标签吗</span>
</div>
<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
>

View File

@ -9,6 +9,7 @@ export const INITIAL_QUERY = {
operator_id: '',
group_ids: [],
tag_ids: [],
project_ids: [],
};
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">
<p class="text-18px font-400 lh-26px color-#211F24 title">账号管理</p>
<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>
<img :src="icon3" width="16" height="16" />
</template>
<template #default>标签管理</template>
</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>
<img :src="icon2" width="16" height="16" />
</template>
<template #default>分组管理</template>
</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>
<img :src="icon1" width="16" height="16" />
</template>
@ -70,7 +70,7 @@
</template>
<div v-else>
<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>
</a-button>
</a-space>

View File

@ -2,16 +2,6 @@
height: 100%;
display: flex;
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 {
.top {
.title {
@ -47,15 +37,6 @@
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 {
padding: 0;
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

@ -1,65 +0,0 @@
<!--
* @Author: RenXiaoDong
* @Date: 2025-06-25 14:02:40
-->
<template>
<a-select
v-model="selectedTags"
: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 selectedTags = ref(props.multiple ? [] : '');
// 监听外部传入的值变化
watch(
() => props.modelValue,
(newVal) => {
selectedTags.value = newVal;
},
{ immediate: true },
);
// 监听内部值变化,向外部发送更新
watch(selectedTags, (newVal) => {
emits('update:modelValue', newVal);
});
const handleChange = (value) => {
selectedTags.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">
<span class="label">运营人员</span>
<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>
</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-space>
</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>
<icon-search />
</template>
<template #default>搜索</template>
</a-button>
<a-button class="w-84px reset-btn" size="medium" @click="handleReset">
<a-button size="medium" @click="handleReset">
<template #icon>
<icon-refresh />
</template>
@ -74,13 +74,13 @@
</template>
<script setup lang="ts">
import EchartsItem from './components/echarts-item/index';
import { PLATFORM_LIST } from '../common_constants';
import { PLATFORM_LIST } from '@/utils/platform';
import {
getPlacementAccountsTrend,
getPlacementAccountProjectsTrend,
fetchAccountOperators,
} 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 PlanSelect from '@/views/components/common/PlanSelect.vue';

View File

@ -16,11 +16,11 @@
</div>
<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 #default>导出数据</template>
</a-button>
<a-button class="w-110px search-btn" size="medium" @click="openCustomColumn">
<a-button type="outline" size="medium" @click="openCustomColumn">
<template #icon>
<img :src="icon1" width="14" height="14" />
</template>
@ -106,7 +106,7 @@
</div>
</template>
<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 v-else-if="column.isRateField" #cell="{ record }">

View File

@ -19,7 +19,7 @@
<div v-if="!isAccountTab" class="filter-row-item flex items-center">
<span class="label">计划分组</span>
<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>
</div>
<div class="filter-row-item flex items-center">
@ -31,7 +31,7 @@
<div class="filter-row-item flex items-center">
<span class="label">运营人员</span>
<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>
</div>
</div>
@ -55,13 +55,13 @@
/>
</a-space>
</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>
<icon-search />
</template>
<template #default>搜索</template>
</a-button>
<a-button class="w-84px reset-btn" size="medium" @click="handleReset">
<a-button size="medium" @click="handleReset">
<template #icon>
<icon-refresh />
</template>
@ -78,10 +78,9 @@ import {
getPlacementAccountsList,
getPlacementAccountOperators,
} from '@/api/all/propertyMarketing';
import GroupSelect from '../group-select';
import OperatorSelect from '@/views/property-marketing/put-account/components/operator-select';
import StatusSelect from '@/views/property-marketing/put-account/components/status-select';
import CommonSelect from '@/components/common-select';
import StatusSelect from '@/views/property-marketing/media-account/components/status-select';
import AccountSelect from '@/views/property-marketing/put-account/components/account-select';
const props = defineProps({

View File

@ -16,7 +16,7 @@
</a-form-item>
</a-form>
<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>
</template>
</a-modal>

View File

@ -9,7 +9,7 @@
<span>确认删除 "{{ groupName }}" 这个分组吗</span>
</div>
<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
>

View File

@ -16,11 +16,11 @@
</div>
<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 #default>导出数据</template>
</a-button>
<a-button class="w-110px search-btn" size="medium" @click="openCustomColumn">
<a-button type="outline" size="medium" @click="openCustomColumn">
<template #icon>
<img :src="icon1" width="14" height="14" />
</template>
@ -106,7 +106,7 @@
</div>
</template>
<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 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="2" title="计划"></a-tab-pane>
<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>
<img :src="icon2" width="16" height="16" />
</template>

View File

@ -2,16 +2,6 @@
height: 100%;
display: flex;
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 {
border-radius: 8px;
border: 1px solid #e6e6e8;

View File

@ -2,10 +2,6 @@
* @Author: RenXiaoDong
* @Date: 2025-06-26 17:44:16
-->
<!--
* @Author: RenXiaoDong
* @Date: 2025-06-26 17:23:52
-->
<template>
<a-modal
v-model:visible="visible"
@ -19,7 +15,7 @@
<span>确认删除 {{ accountName }} 这个账户吗</span>
</div>
<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
>

View File

@ -41,6 +41,30 @@
<span class="label">运营人员</span>
<span class="cts">{{ item.operator?.name }}</span>
</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">
<span class="label">账户总消耗</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)" />
<a-button
v-if="showPauseButton(item.status)"
class="w-64px search-btn mr-8px"
type="outline"
class="mr-8px"
size="mini"
@click="handlePause(item)"
>
<template #default>暂停同步</template>
</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>
</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>
</a-button>
</div>
@ -76,8 +101,8 @@
<script setup>
import { defineProps, ref, computed } from 'vue';
import { PLATFORM_LIST } from '@/views/property-marketing/put-account/common_constants';
import { EnumPutAccountStatus } from '@/views/property-marketing/put-account/components/status-select/constants';
import { PLATFORM_LIST } from '@/utils/platform';
import { EnumStatus } from '@/views/property-marketing/put-account/components/status-select/constants';
import { formatNumberShow, exactFormatTime } from '@/utils/tools';

View File

@ -9,7 +9,7 @@
<span>确认暂停同步 {{ accountName }} 这个账号的数据吗</span>
</div>
<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>
</template>
</a-modal>

View File

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

View File

@ -111,6 +111,9 @@
</a-radio>
</a-radio-group>
</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">
<a-form-item label="账户总消耗" field="total_use_amount">
<a-input v-model="form.total_use_amount" placeholder="请输入..." size="large" disabled />
@ -131,7 +134,7 @@
</template>
</a-form>
<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">
{{ confirmBtnText }}
</a-button>
@ -148,7 +151,9 @@ import { ref, defineEmits } from 'vue';
import AuthorizedAccountModal from '../authorized-account-modal';
// import ImportPromptModal from '../import-prompt-modal';
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 { genRandomId } from '@/utils/tools';
@ -158,6 +163,7 @@ import {
putPlacementAccounts,
getPlacementAccountsTemplateUrl,
batchPlacementAccounts,
getProjectList,
} from '@/api/all/propertyMarketing';
import icon1 from '@/assets/img/media-account/icon-download.png';
@ -177,6 +183,7 @@ const INITIAL_FORM = {
holder_name: '',
platform: 0,
is_sync_project: 1,
project_ids: [],
};
const visible = ref(false);
@ -191,6 +198,7 @@ const authorizedAccountModalRef = ref(null);
const uploadRef = ref(null);
const file = ref(null);
const form = ref(cloneDeep(INITIAL_FORM));
const projects = ref([]);
const importLoading = ref(false);
const rules = {
@ -245,6 +253,7 @@ const reset = () => {
fileName.value = '';
file.value = null;
isEdit.value = false;
projects.value = [];
importLoading.value = false;
uploadStatus.value = UploadStatus.DEFAULT;
uploadType.value = 'manual';
@ -258,12 +267,19 @@ const open = (accountId = '') => {
id.value = accountId;
isEdit.value = !!accountId;
getProjects();
if (accountId) {
getAccountDetail();
}
visible.value = true;
};
const getProjects = async () => {
const { code, data } = await getProjectList();
if (code === 200) {
projects.value = data;
}
};
const getAccountDetail = async () => {
const { code, data } = await getPlacementAccountsDetail(id.value);
@ -288,7 +304,7 @@ const handleBatchImport = async () => {
'Content-Type': 'multipart/form-data',
},
});
if (code === 200) {
const id = genRandomId();
showExportNotification(`正在导入“${file.value.name}”,请稍后...`, { id });
@ -308,7 +324,7 @@ const handleBatchImport = 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);
if (code === 200) {
update();

View File

@ -55,7 +55,7 @@
</template>
</div>
<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>
</template>
</a-modal>
@ -67,7 +67,6 @@
import dayjs from 'dayjs';
import { defineExpose, ref, computed, defineEmits } from 'vue';
import { exactFormatTime } from '@/utils/tools';
import { ENUM_PLATFORM } from '@/views/property-marketing/put-account/common_constants';
import {
putPlacementAccountsAuthorized,
getPlacementAccountsAuthorizedStatus,

View File

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

View File

@ -18,7 +18,7 @@
</div>
</div>
<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>
</template>
</a-modal>

View File

@ -103,7 +103,7 @@
>已选<span class="color-#6D4CFE num mx-3px">{{ selectedRows.length }}</span>账户</span
>
<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
type="primary"
class="ml-16px"

View File

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

View File

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

View File

@ -2,16 +2,6 @@
height: 100%;
display: flex;
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 {
.top {
.title {
@ -41,15 +31,6 @@
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 {
padding: 0;
cursor: pointer;

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

@ -31,7 +31,7 @@
</template>
<script setup lang="ts">
import { IconDelete } from '@arco-design/web-vue/es/icon';
import { PLATFORM_LIST } from '@/views/property-marketing/put-account/common_constants';
import { PLATFORM_LIST } from '@/utils/platform';
import { Message } from '@arco-design/web-vue';
const columns = [

View File

@ -35,13 +35,13 @@
</div>
<div class="filter-row-item flex items-center">
<a-button class="w-84px search-btn mr-12px" :disabled="disabled" size="medium" @click="handleSearch">
<a-button type="outline" class="mr-12px" :disabled="disabled" size="medium" @click="handleSearch">
<template #icon>
<icon-search />
</template>
<template #default>搜索</template>
</a-button>
<a-button class="w-84px reset-btn" size="medium" @click="handleReset">
<a-button size="medium" @click="handleReset">
<template #icon>
<icon-refresh />
</template>
@ -54,7 +54,7 @@
<script setup lang="ts">
import { defineEmits, defineProps } from 'vue';
import { PLATFORM_LIST } from '@/views/property-marketing/put-account/common_constants';
import { PLATFORM_LIST } from '@/utils/platform';
import AccountSelect from '@/views/components/common/AccountSelect.vue';
import PlanSelect from '@/views/components/common/PlanSelect.vue';

View File

@ -92,7 +92,7 @@
</view>
</template>
<script setup lang="ts">
import { PLATFORM_LIST } from '../../../common_constants';
import { PLATFORM_LIST } from '@/utils/platform';
const props = defineProps({
listData: {

View File

@ -77,7 +77,7 @@ import { reactive, ref } from 'vue';
import MonthData from './components/month-data/index.vue';
import PlacementSuggestions from './components/placement-suggestions/index.vue';
import { PLATFORM_LIST } from '@/views/property-marketing/put-account/common_constants.ts';
import { PLATFORM_LIST } from '@/utils/platform';
import { getPlacementGuideDetail } from '@/api/all/propertyMarketing';
import { useRoute } from 'vue-router';
import { uploadPdf } from '@/views/property-marketing/put-account/investment-guidelines/constants';