feat: 初始化项目管理列表

This commit is contained in:
rd
2025-07-21 12:01:32 +08:00
parent aa7155aa72
commit 9febe14997
14 changed files with 467 additions and 8 deletions

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

@ -67,6 +67,9 @@ export function useTableSelectionWithPagination(options: UseTableSelectionWithPa
pageInfo.value.page = 1;
options.onPageSizeChange?.(size);
};
const resetPageInfo = () => {
pageInfo.value = cloneDeep(DEFAULT_PAGE_INFO)
}
const rowSelection = computed(() => ({
type: 'checkbox',
@ -83,5 +86,6 @@ export function useTableSelectionWithPagination(options: UseTableSelectionWithPa
rowSelection,
handleSelect,
handleSelectAll,
resetPageInfo
};
}

View File

@ -9,6 +9,7 @@ import IconRepository from '@/assets/svg/icon-repository.svg';
import IconMediaAccount from '@/assets/svg/icon-mediaAccount.svg';
import IconPutAccount from '@/assets/svg/icon-putAccount.svg';
import IconIntelligentSolution from '@/assets/svg/icon-intelligentSolution.svg';
import IconProjectManagement from '@/assets/svg/icon-projectManagement.svg';
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;

View File

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

View File

@ -85,7 +85,7 @@ export function formatTableField(fieldItem: any, rowValue: any, showExactValue =
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 '-';
const year = dayjs(val * 1000).year();
const currYear = dayjs().year();

View File

@ -0,0 +1,70 @@
<!-- eslint-disable vue/no-mutating-props -->
<!--
* @Author: RenXiaoDong
* @Date: 2025-06-25 14:02:40
-->
<template>
<div class="container 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.search"
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 class="w-84px search-btn 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">
<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 @@
.container {
.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,45 @@
/*
* @Author: RenXiaoDong
* @Date: 2025-06-28 10:33:06
*/
export const TABLE_COLUMNS = [
{
title: '项目名称',
dataIndex: 'name',
width: 240,
fixed: 'left',
},
{
title: '项目预算',
dataIndex: 'key1',
width: 180,
},
{
title: '关联平台账号',
dataIndex: 'key2',
width: 180,
},
{
title: '关联渠道账户',
dataIndex: 'key3',
width: 180,
},
{
title: '关联内容稿件',
dataIndex: 'key4',
width: 180,
},
{
title: '创建时间',
dataIndex: 'create_at',
width: 180,
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
{
title: '操作',
dataIndex: 'operation',
width: 100,
},
];

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 />
</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" class="search-btn" @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 = {
search: '',
column: '',
order: '',
};

View File

@ -0,0 +1,138 @@
<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>
</div>
</template>
<script setup>
import { INITIAL_QUERY } from './constants';
import { useTableSelectionWithPagination } from '@/hooks/useTableSelectionWithPagination';
import FilterBlock from './components/filter-block';
import ProjectTable from './components/project-table';
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 query = ref(cloneDeep(INITIAL_QUERY));
const getData = async () => {
dataSource.value = [
{
id: 1,
name: '闲鱼用户增长投放规划',
key1: 500131,
key2: 4141,
key3: 55,
key4: 12,
create_at: 1753069077,
},
{
id: 2,
name: '闲鱼用户增长投放规划',
key1: 500131,
key2: 4141,
key3: 55,
key4: 12,
create_at: 1753069077,
},
];
pageInfo.value.total = 2;
// const { page, page_size } = pageInfo.value;
// const { code, data } = await postSubAccount({
// ...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.column = column;
query.value.order = order;
reload();
};
const handleDelete = (item) => {
console.log('handleDelete', item);
};
const handleEdit = (item) => {
console.log('handleDelete', item);
};
onMounted(() => {
getData();
});
provide('update', getData);
</script>
<style scoped lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,39 @@
.project-list-wrap {
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 {
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

@ -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"
/>
@ -114,7 +114,7 @@ const deleteAccountRef = ref(null);
const pageInfo = ref({
page: 1,
pageSize: 20,
page_size: 20,
total: 0,
});
const query = ref(cloneDeep(INITIAL_QUERY));
@ -174,10 +174,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) {
@ -190,11 +190,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);
@ -206,7 +206,7 @@ const onPageChange = (current) => {
getData();
};
const onPageSizeChange = (pageSize) => {
pageInfo.value.pageSize = pageSize;
pageInfo.value.page_size = pageSize;
reload();
};