This commit is contained in:
aaron 2025-03-09 23:40:18 +08:00
parent d05216743c
commit 321ff3452b
4 changed files with 961 additions and 0 deletions

View File

@ -73,6 +73,9 @@
<a-menu-item key="community-time-periods">
<router-link to="/community/time-periods">配送时段</router-link>
</a-menu-item>
<a-menu-item key="community-sets">
<router-link to="/community/sets">小区集合</router-link>
</a-menu-item>
</a-sub-menu>
<a-sub-menu key="coupon">
@ -296,6 +299,11 @@ export default defineComponent({
key: 'community-time-periods',
title: '配送时段',
path: '/community/time-periods'
},
{
key: 'community-sets',
title: '小区集合',
path: '/community/sets'
}
]
},

View File

@ -69,6 +69,18 @@ const routes = [
name: 'CommunityTimePeriods',
component: () => import('../views/community/TimePeriodList.vue'),
meta: { title: '小区配送时段' }
},
{
path: 'community/sets',
name: 'CommunitySets',
component: () => import('../views/community/CommunitySetList.vue'),
meta: { title: '小区集合' }
},
{
path: 'community/sets/:id',
name: 'CommunitySetDetail',
component: () => import('../views/community/CommunitySetDetail.vue'),
meta: { title: '小区集合详情' }
}
]
},

View File

@ -0,0 +1,500 @@
<template>
<page-container>
<div class="community-set-detail">
<div class="detail-header">
<div class="header-info">
<div class="info-card">
<h1 class="set-name">{{ setInfo.set_name || '加载中...' }}</h1>
<div class="operator-info" v-if="setInfo.user_name">
<span class="label">运营商</span>
<span class="value">{{ setInfo.user_name }}</span>
</div>
<div class="meta-info" v-if="setInfo.create_time">
<span class="label">创建时间</span>
<span class="value">{{ setInfo.create_time }}</span>
</div>
</div>
</div>
</div>
<a-divider />
<div class="communities-list">
<div class="list-header">
<h2>关联小区列表</h2>
<a-button type="primary" @click="showAddCommunityModal">
添加小区
</a-button>
</div>
<a-spin :spinning="loading">
<a-empty v-if="communities.length === 0" description="暂无关联小区" />
<a-table
v-else
:columns="columns"
:data-source="communities"
:pagination="pagination"
:loading="loading"
@change="handleTableChange"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-button type="link" danger @click="handleRemoveCommunity(record)">
移除关联
</a-button>
</template>
</template>
</a-table>
</a-spin>
</div>
</div>
<!-- 添加小区模态框 -->
<a-modal
v-model:visible="addCommunityVisible"
title="添加小区"
:footer="null"
>
<div class="search-community">
<a-input-search
v-model:value="searchValue"
placeholder="请输入小区名称搜索"
enter-button
@search="handleSearchCommunity"
/>
</div>
<div v-if="searchLoading" class="search-loading">
<a-spin />
</div>
<div v-else-if="searchResults.length > 0" class="search-results">
<div class="search-title">搜索结果</div>
<a-radio-group v-model:value="selectedCommunityId">
<div v-for="community in searchResults" :key="community.id" class="search-item">
<a-radio :value="community.id">
<div class="community-info">
<div class="community-name">{{ community.name }}</div>
<div class="community-address">{{ community.address }}</div>
</div>
</a-radio>
</div>
</a-radio-group>
<div class="search-actions">
<a-button type="primary" :disabled="!selectedCommunityId" :loading="submitting" @click="handleAddCommunity">
确认添加
</a-button>
<a-button style="margin-left: 8px;" @click="handleCancelAdd">
取消
</a-button>
</div>
</div>
<div v-else-if="searchValue && !searchLoading" class="no-results">
未找到匹配的小区
</div>
<div v-else class="search-tip">
请输入小区名称进行搜索
</div>
</a-modal>
</page-container>
</template>
<script>
import { defineComponent, ref, reactive, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { message, Modal, Table, Tag, Spin, Empty, Divider } from 'ant-design-vue'
import request from '@/utils/request'
import PageContainer from '@/components/PageContainer.vue'
export default defineComponent({
components: {
PageContainer,
ATable: Table,
ATag: Tag,
ASpin: Spin,
AEmpty: Empty,
ADivider: Divider
},
setup() {
const route = useRoute()
const setId = ref(route.params.id)
const loading = ref(false)
const setInfo = ref({})
const communities = ref([])
//
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total} 条记录`
})
//
const columns = [
{
title: 'ID',
dataIndex: 'community_id',
key: 'community_id',
width: 80
},
{
title: '小区名称',
dataIndex: 'community_name',
key: 'community_name',
width: 200
},
{
title: '地址',
dataIndex: 'address',
key: 'address',
ellipsis: true
},
{
title: '操作',
key: 'action',
width: 120,
fixed: 'right'
}
]
//
const addCommunityVisible = ref(false)
const submitting = ref(false)
const searchValue = ref('')
const searchLoading = ref(false)
const searchResults = ref([])
const selectedCommunityId = ref(null)
//
const fetchSetInfo = async () => {
try {
loading.value = true
const res = await request.get(`/api/community-sets/list/all`)
if (res.code === 200 && res.data && res.data.items) {
const set = res.data.items.find(item => item.id === Number(setId.value))
if (set) {
setInfo.value = set
} else {
message.error('未找到小区集合信息')
}
} else {
message.error(res.message || '获取集合信息失败')
}
} catch (error) {
console.error('获取小区集合信息失败:', error)
message.error('获取集合信息失败')
} finally {
loading.value = false
}
}
//
const fetchCommunities = async () => {
try {
loading.value = true
const params = {
skip: (pagination.value.current - 1) * pagination.value.pageSize,
limit: pagination.value.pageSize
}
const res = await request.get(`/api/community-set-mappings/set/${setId.value}/communities`, { params })
if (res.code === 200 && res.data) {
communities.value = res.data.items || []
pagination.value.total = res.data.total || 0
} else {
message.error(res.message || '获取关联小区失败')
}
} catch (error) {
console.error('获取关联小区失败:', error)
message.error('获取关联小区失败')
} finally {
loading.value = false
}
}
//
const handleTableChange = (pag) => {
pagination.value.current = pag.current
pagination.value.pageSize = pag.pageSize
fetchCommunities()
}
//
const getStatusText = (status) => {
const statusMap = {
'OPENING': '营业中',
'CLOSED': '已关闭',
'PREPARING': '筹备中'
}
return statusMap[status] || status
}
//
const getStatusColor = (status) => {
const colorMap = {
'OPENING': 'green',
'CLOSED': 'red',
'PREPARING': 'orange'
}
return colorMap[status] || 'default'
}
//
const showAddCommunityModal = () => {
addCommunityVisible.value = true
searchValue.value = ''
searchResults.value = []
selectedCommunityId.value = null
}
//
const handleSearchCommunity = async () => {
if (!searchValue.value) {
message.warning('请输入小区名称')
return
}
try {
searchLoading.value = true
const res = await request.get(`/api/community/search_by_name/${searchValue.value}`)
if (res.code === 200) {
searchResults.value = res.data || []
if (searchResults.value.length > 0) {
selectedCommunityId.value = searchResults.value[0].id
}
} else {
message.warning(res.message || '搜索小区失败')
searchResults.value = []
}
} catch (error) {
console.error('搜索小区失败:', error)
message.error('搜索小区失败')
searchResults.value = []
} finally {
searchLoading.value = false
}
}
//
const handleAddCommunity = async () => {
if (!selectedCommunityId.value) {
message.warning('请选择要添加的小区')
return
}
try {
submitting.value = true
const res = await request.post('/api/community-set-mappings/', {
set_id: Number(setId.value),
community_id: selectedCommunityId.value
})
if (res.code === 200) {
message.success('添加小区成功')
addCommunityVisible.value = false
fetchCommunities() //
} else {
message.error(res.message || '添加小区失败')
}
} catch (error) {
console.error('添加小区失败:', error)
message.error('添加小区失败')
} finally {
submitting.value = false
}
}
//
const handleCancelAdd = () => {
addCommunityVisible.value = false
}
//
const handleRemoveCommunity = (community) => {
Modal.confirm({
title: '确认移除',
content: `确定要移除小区 "${community.community_name}" 的关联吗?`,
onOk: async () => {
try {
const res = await request.delete(`/api/community-set-mappings/${community.id}`)
if (res.code === 200) {
message.success('移除关联成功')
fetchCommunities() //
} else {
message.error(res.message || '移除关联失败')
}
} catch (error) {
console.error('移除小区关联失败:', error)
message.error('移除关联失败')
}
}
})
}
onMounted(() => {
fetchSetInfo()
fetchCommunities()
})
return {
setId,
loading,
setInfo,
communities,
columns,
pagination,
addCommunityVisible,
submitting,
searchValue,
searchLoading,
searchResults,
selectedCommunityId,
getStatusText,
getStatusColor,
showAddCommunityModal,
handleSearchCommunity,
handleAddCommunity,
handleCancelAdd,
handleRemoveCommunity,
handleTableChange
}
}
})
</script>
<style scoped>
.detail-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
}
.header-info {
flex: 1;
}
.info-card {
background-color: #fafafa;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
border: 1px solid #f0f0f0;
}
.set-name {
margin: 0;
margin-bottom: 16px;
font-size: 24px;
color: #1890ff;
font-weight: 600;
}
.operator-info, .meta-info {
font-size: 14px;
margin-bottom: 8px;
display: flex;
align-items: center;
}
.operator-info .label, .meta-info .label {
color: #666;
margin-right: 8px;
min-width: 70px;
}
.operator-info .value, .meta-info .value {
font-weight: 500;
color: #333;
}
.communities-list {
margin-top: 24px;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.list-header h2 {
margin-bottom: 0;
}
:deep(.ant-table-content) {
overflow-x: auto;
}
:deep(.ant-table-cell) {
vertical-align: middle;
}
.search-community {
margin-bottom: 16px;
}
.search-loading {
display: flex;
justify-content: center;
padding: 24px 0;
}
.search-results {
max-height: 300px;
overflow-y: auto;
padding: 8px 0;
}
.search-item {
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.search-item:last-child {
border-bottom: none;
}
.community-info {
margin-left: 8px;
}
.community-name {
font-weight: 500;
}
.community-address {
font-size: 12px;
color: #666;
}
.no-results {
text-align: center;
color: #999;
padding: 24px 0;
}
.search-title {
font-weight: bold;
margin-bottom: 12px;
}
.search-actions {
margin-top: 16px;
text-align: right;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
.search-tip {
text-align: center;
color: #999;
padding: 24px 0;
}
</style>

View File

@ -0,0 +1,441 @@
<template>
<page-container>
<div class="community-set-list">
<div class="table-header">
<h1>小区集合</h1>
<a-button type="primary" @click="showCreateModal">
创建小区集合
</a-button>
</div>
<a-table
:columns="columns"
:data-source="tableData"
:loading="loading"
:pagination="pagination"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" @click="handleView(record)">
查看详情
</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
<!-- 创建小区集合模态框 -->
<a-modal
v-model:visible="createModalVisible"
title="创建小区集合"
@ok="handleCreateSubmit"
@cancel="handleCreateCancel"
:confirmLoading="submitting"
>
<a-form :model="createForm" layout="vertical">
<a-form-item
label="集合名称"
name="set_name"
:rules="[{ required: true, message: '请输入集合名称' }]"
>
<a-input v-model:value="createForm.set_name" placeholder="请输入集合名称" />
</a-form-item>
<a-form-item
label="运营商"
name="user_id"
:rules="[{ required: true, message: '运营商必须选择' }]"
>
<div class="user-search">
<a-input-search
v-model:value="phoneSearchValue"
placeholder="请输入手机号查询"
enter-button
@search="handlePhoneSearch"
/>
</div>
<div v-if="searchedUser" class="user-result">
<div class="user-info">
<div class="user-name">{{ searchedUser.nickname || '未设置昵称' }}</div>
<div class="user-phone">{{ formatPhone(searchedUser.phone) }}</div>
<div class="user-roles">
<a-tag v-if="hasPartnerRole(searchedUser)" color="green">运营商</a-tag>
</div>
</div>
<div class="user-action">
<a-button
type="primary"
size="small"
@click="selectUser(searchedUser)"
:disabled="isUserSelected(searchedUser)"
>
{{ isUserSelected(searchedUser) ? '已选择' : '选择' }}
</a-button>
</div>
</div>
<div v-if="selectedUser" class="selected-user">
<div class="selected-info">
<div class="user-name">{{ selectedUser.nickname || '未设置昵称' }}</div>
<div class="user-phone">{{ formatPhone(selectedUser.phone) }}</div>
<a-button type="link" danger size="small" @click="clearSelectedUser">
取消选择
</a-button>
</div>
</div>
</a-form-item>
</a-form>
</a-modal>
</page-container>
</template>
<script>
import { defineComponent, ref, reactive, onMounted, computed } from 'vue'
import { message } from 'ant-design-vue'
import request from '@/utils/request'
import PageContainer from '@/components/PageContainer.vue'
import { useRouter } from 'vue-router'
export default defineComponent({
components: {
PageContainer
},
setup() {
const loading = ref(false)
const tableData = ref([])
const submitting = ref(false)
const router = useRouter()
//
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total} 条记录`
})
//
const createModalVisible = ref(false)
const createForm = reactive({
set_name: '',
user_id: null
})
//
const phoneSearchValue = ref('')
const searchedUser = ref(null)
const selectedUser = ref(null)
//
const isUserSelected = (user) => {
return selectedUser.value && selectedUser.value.userid === user.userid
}
//
const hasPartnerRole = (user) => {
return user && user.roles && user.roles.includes('partner')
}
//
const selectUser = (user) => {
selectedUser.value = user
createForm.user_id = user.userid
message.success(`已选择:${user.nickname || user.phone}`)
}
//
const clearSelectedUser = () => {
selectedUser.value = null
createForm.user_id = null
}
//
const getRoleName = (role) => {
const roleMap = {
'admin': '管理员',
'user': '普通用户',
'merchant': '商家',
'deliveryman': '配送员',
'partner': '运营商'
}
return roleMap[role] || role
}
//
const getRoleColor = (role) => {
const colorMap = {
'admin': 'red',
'user': 'blue',
'merchant': 'orange',
'deliveryman': 'purple',
'partner': 'green'
}
return colorMap[role] || 'default'
}
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
},
{
title: '集合名称',
dataIndex: 'set_name',
key: 'set_name',
width: 200,
},
{
title: '运营商',
dataIndex: 'user_name',
key: 'user_name',
width: 150,
},
{
title: '小区数量',
dataIndex: 'community_count',
key: 'community_count',
width: 120,
},
{
title: '创建时间',
dataIndex: 'create_time',
key: 'create_time',
width: 180,
},
{
title: '操作',
key: 'action',
width: 150,
}
]
// 4*
const formatPhone = (phone) => {
if (!phone) return ''
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}
//
const fetchData = async () => {
try {
loading.value = true
const params = {
skip: (pagination.value.current - 1) * pagination.value.pageSize,
limit: pagination.value.pageSize
}
const res = await request.get('/api/community-sets/list/all', { params })
if (res.code === 200 && res.data) {
tableData.value = res.data.items || []
pagination.value.total = res.data.total || 0
} else {
message.error(res.message || '获取数据失败')
}
} catch (error) {
console.error('获取小区集合列表失败:', error)
message.error('获取数据失败')
} finally {
loading.value = false
}
}
//
const showCreateModal = () => {
createForm.set_name = ''
createForm.user_id = null
phoneSearchValue.value = ''
searchedUser.value = null
selectedUser.value = null
createModalVisible.value = true
}
//
const handleCreateSubmit = async () => {
if (!createForm.set_name) {
message.warning('请输入集合名称')
return
}
if (!createForm.user_id) {
message.warning('请选择运营商')
return
}
try {
submitting.value = true
const res = await request.post('/api/community-sets/', {
set_name: createForm.set_name,
user_id: createForm.user_id
})
if (res.code === 200) {
message.success('创建成功')
createModalVisible.value = false
fetchData() //
} else {
message.error(res.message || '创建失败')
}
} catch (error) {
console.error('创建小区集合失败:', error)
message.error('创建失败')
} finally {
submitting.value = false
}
}
//
const handleCreateCancel = () => {
createModalVisible.value = false
}
//
const handlePhoneSearch = async () => {
if (!phoneSearchValue.value) {
message.warning('请输入手机号')
return
}
try {
const res = await request.get(`/api/user/search_by_phone/${phoneSearchValue.value}`)
if (res.code === 200 && res.data) {
searchedUser.value = res.data
// user_id
} else {
message.warning(res.message || '未找到该用户')
searchedUser.value = null
}
} catch (error) {
console.error('搜索用户失败:', error)
message.error('搜索失败')
searchedUser.value = null
}
}
//
const handleView = (record) => {
router.push(`/community/sets/${record.id}`)
}
//
const handleTableChange = (pag) => {
pagination.value.current = pag.current
pagination.value.pageSize = pag.pageSize
fetchData()
}
onMounted(() => {
fetchData()
})
return {
loading,
tableData,
columns,
pagination,
createModalVisible,
createForm,
submitting,
phoneSearchValue,
searchedUser,
selectedUser,
formatPhone,
getRoleName,
getRoleColor,
isUserSelected,
hasPartnerRole,
selectUser,
clearSelectedUser,
showCreateModal,
handleCreateSubmit,
handleCreateCancel,
handlePhoneSearch,
handleView,
handleTableChange
}
}
})
</script>
<style scoped>
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.table-header h1 {
margin: 0;
}
:deep(.ant-table-content) {
overflow-x: auto;
}
.user-search {
margin-bottom: 16px;
}
.user-result {
margin-top: 8px;
padding: 12px;
background-color: #f5f5f5;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.user-info {
flex: 1;
}
.user-name {
font-weight: bold;
font-size: 16px;
margin-bottom: 4px;
}
.user-phone {
color: #666;
margin-bottom: 4px;
}
.user-roles {
margin-top: 8px;
}
.user-action {
margin-left: 16px;
}
.selected-user {
margin-top: 16px;
padding: 12px;
background-color: #e6f7ff;
border: 1px solid #91d5ff;
border-radius: 4px;
}
.selected-info {
display: flex;
align-items: center;
}
.selected-info .user-name {
margin-right: 16px;
margin-bottom: 0;
}
.selected-info .user-phone {
margin-bottom: 0;
}
</style>