dman-web-admin/src/views/community/CommunityList.vue
2025-03-12 14:56:18 +08:00

1776 lines
47 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<page-container>
<div class="community-list">
<div class="table-header">
<h1>小区列表</h1>
<a-button type="primary" @click="showAddModal">添加小区</a-button>
</div>
<a-table
:columns="columns"
:data-source="tableData"
:pagination="pagination"
:loading="loading"
@change="handleTableChange"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-dropdown>
<a-tag :color="getStatusColor(record.status)" class="status-tag">
{{ getStatusText(record.status) }}
<down-outlined />
</a-tag>
<template #overlay>
<a-menu @click="({ key }) => handleStatusChange(record, key)">
<a-menu-item key="OPENING">运营中</a-menu-item>
<a-menu-item key="UNOPEN">未启用</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<template v-if="column.key === 'location'">
<a @click="showMap(record)">查看位置</a>
</template>
<template v-if="column.key === 'create_time'">
{{ formatDateTime(record.create_time) }}
</template>
<template v-if="column.key === 'qrcode'">
<a v-if="record.qy_group_qrcode" @click="previewQRCode(record.qy_group_qrcode)">查看二维码</a>
<span v-else>-</span>
</template>
<template v-if="column.key === 'delivery_price'">
<div class="delivery-price-info">
<div>基础费:<span style="color: #1890ff; font-weight: bold;">{{ record.base_price }}</span> 元/单</div>
<div>
超 <span style="color: #1890ff; font-weight: bold;">{{ record.extra_package_threshold }}</span> 件,加收 <span style="color: #1890ff; font-weight: bold;">{{ record.extra_package_price }}</span> 元/件
</div>
</div>
</template>
<template v-if="column.key === 'profit_sharing'">
<div v-if="record.profit_sharing" class="profit-sharing-info-table">
<div class="profit-rate-item">
<span class="rate-label">平台</span>
<span class="rate-value">{{ record.profit_sharing.platform_rate }}%</span>
<span class="rate-label">运营商</span>
<span class="rate-value">{{ record.profit_sharing.partner_rate }}%</span>
</div>
<div class="profit-rate-item">
<span class="rate-label">服务商</span>
<span class="rate-value">{{ record.profit_sharing.admin_rate }}%</span>
<span class="rate-label">配送员</span>
<span class="rate-value">{{ record.profit_sharing.delivery_rate }}%</span>
</div>
</div>
<a-tag v-else color="orange">未设置</a-tag>
</template>
<template v-if="column.key === 'admin'">
<div v-if="record.admin" class="admin-info-table">
<div class="admin-info-row">
<span class="admin-name">{{ record.admin.nickname || '未知' }}</span>
</div>
<div class="admin-info-row">
<span class="admin-phone">{{ record.admin.phone || '-' }}</span>
</div>
</div>
<a-tag v-else color="orange">未设置</a-tag>
</template>
<template v-if="column.key === 'action'">
<div class="action-buttons">
<a @click="handleEdit(record)">编辑</a>
<a-divider type="vertical" />
<a @click="handleEditDeliveryPrice(record)">配送定价</a>
<a-divider type="vertical" />
<a @click="handleEditProfitSharing(record)">设置分润</a>
<a-divider type="vertical" />
<a @click="handleSetAdmin(record)">{{ record.admin ? '修改服务商' : '设置服务商' }}</a>
</div>
</template>
</template>
</a-table>
<a-modal
v-model:visible="mapVisible"
title="小区位置"
:footer="null"
width="800px"
@cancel="closeMap"
>
<div id="map-container" style="height: 500px;"></div>
</a-modal>
<a-modal
v-model:visible="modalVisible"
:title="isEdit ? '编辑小区' : '添加小区'"
@ok="handleSubmit"
@cancel="handleCancel"
:confirmLoading="confirmLoading"
width="680px"
>
<template #footer>
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" :loading="confirmLoading" @click="handleSubmit">
保存
</a-button>
</a-space>
</template>
<a-form
ref="formRef"
:model="formState"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
class="community-form"
>
<a-form-item label="小区名称" name="name" required>
<a-input v-model:value="formState.name" placeholder="请输入小区名称" />
</a-form-item>
<map-picker
v-model="formState.location"
label="地址搜索"
/>
<a-form-item
label="企业微信Webhook"
name="webot_webhook"
required
:rules="[
{ required: true, message: '请输入Webhook地址' },
{ type: 'url', message: '请输入正确的URL地址' }
]"
>
<a-input
v-model:value="formState.webot_webhook"
placeholder="请输入企业微信群机器人Webhook地址"
:maxLength="500"
/>
<div class="form-item-tip">
小区相关通知将推送到此企业微信群
</div>
</a-form-item>
<a-form-item
label="群二维码"
name="qy_group_qrcode"
required
:rules="[{ required: true, message: '请上传群二维码' }]"
>
<div class="qrcode-upload-wrapper">
<a-upload
v-model:file-list="fileList"
name="file"
:customRequest="handleQRCodeUpload"
@remove="handleQRCodeRemove"
list-type="picture-card"
accept=".jpg,.jpeg,.png"
:max-count="1"
@preview="previewQRCode"
>
<div v-if="!fileList.length">
<plus-outlined />
<div style="margin-top: 8px">上传群二维码</div>
</div>
</a-upload>
</div>
</a-form-item>
</a-form>
</a-modal>
<!-- 配送定价模态框 -->
<a-modal
v-model:visible="deliveryPriceModalVisible"
title="配送定价设置"
@ok="handleDeliveryPriceSave"
@cancel="handleDeliveryPriceCancel"
:confirmLoading="deliveryPriceSaving"
width="480px"
>
<a-form layout="vertical" class="delivery-price-form">
<a-form-item
label="基础配送费(元)"
required
:rules="[{ required: true, message: '请输入基础配送费' }]"
>
<a-input-number
v-model:value="deliveryPriceForm.base_price"
:min="0"
:precision="2"
:step="0.5"
style="width: 100%"
placeholder="请输入基础配送费"
/>
<div class="form-item-tip">每单基础配送费</div>
</a-form-item>
<a-form-item
label="差额临界数(件)"
required
:rules="[{ required: true, message: '请输入差额临界数' }]"
>
<a-input-number
v-model:value="deliveryPriceForm.extra_package_threshold"
:min="1"
:precision="0"
style="width: 100%"
placeholder="请输入差额临界数"
/>
<div class="form-item-tip">超过该件数将收取超额配送费</div>
</a-form-item>
<a-form-item
label="超额配送费(元/件)"
required
:rules="[{ required: true, message: '请输入超额配送费' }]"
>
<a-input-number
v-model:value="deliveryPriceForm.extra_package_price"
:min="0"
:precision="2"
:step="0.5"
style="width: 100%"
placeholder="请输入超额配送费"
/>
<div class="form-item-tip">超过临界数后,每件商品额外收取的费用</div>
</a-form-item>
</a-form>
</a-modal>
<!-- 分润设置模态框 -->
<a-modal
v-model:visible="profitSharingModalVisible"
title="分润设置"
@ok="handleProfitSharingSave"
@cancel="handleProfitSharingCancel"
:confirmLoading="profitSharingSaving"
width="500px"
>
<div class="profit-sharing-header">
<div class="profit-sharing-title">
<span>小区名称:</span>
<span class="profit-sharing-community-name">{{ currentCommunityName }}</span>
</div>
<div class="profit-sharing-tip">
注意平台分润比例将自动计算确保总比例等于100%
</div>
</div>
<div class="profit-sharing-form">
<div class="profit-rate-row">
<div class="profit-rate-label">平台分润比例 (%)</div>
<div class="profit-rate-input">
<a-input-number
v-model:value="profitSharingForm.platform_rate"
:min="0"
:max="100"
:precision="0"
style="width: 100%"
disabled
class="platform-rate-input"
/>
</div>
<div class="profit-rate-tip">自动计算</div>
</div>
<div class="profit-rate-row">
<div class="profit-rate-label">运营商分润比例 (%)</div>
<div class="profit-rate-input">
<a-input-number
v-model:value="profitSharingForm.partner_rate"
:min="0"
:max="100"
:precision="0"
style="width: 100%"
placeholder="请输入"
@change="calculatePlatformRate"
/>
</div>
<div class="profit-rate-tip">运营商获得的分润</div>
</div>
<div class="profit-rate-row">
<div class="profit-rate-label">服务商分润比例 (%)</div>
<div class="profit-rate-input">
<a-input-number
v-model:value="profitSharingForm.admin_rate"
:min="0"
:max="100"
:precision="0"
style="width: 100%"
placeholder="请输入"
@change="calculatePlatformRate"
/>
</div>
<div class="profit-rate-tip">小区服务商获得的分润</div>
</div>
<div class="profit-rate-row">
<div class="profit-rate-label">配送员分润比例 (%)</div>
<div class="profit-rate-input">
<a-input-number
v-model:value="profitSharingForm.delivery_rate"
:min="0"
:max="100"
:precision="0"
style="width: 100%"
placeholder="请输入"
@change="calculatePlatformRate"
/>
</div>
<div class="profit-rate-tip">配送员获得的分润</div>
</div>
</div>
<div class="total-rate-info" :class="{ 'rate-error': !isRateValid }">
<span>总比例:{{ getTotalRate() }}%</span>
<span v-if="!isRateValid" class="rate-error-message">总比例必须等于100%</span>
</div>
</a-modal>
<!-- 设置服务商模态框 -->
<a-modal
v-model:visible="adminModalVisible"
:title="currentCommunity && currentCommunity.admin ? '修改服务商' : '设置服务商'"
@ok="handleAdminSave"
@cancel="handleAdminCancel"
:confirmLoading="adminSaving"
width="480px"
>
<div class="admin-search-form">
<a-form layout="vertical">
<a-form-item label="搜索用户">
<a-input
v-model:value="adminSearchPhone"
placeholder="请输入手机号搜索,按回车键搜索"
:loading="adminSearching"
@pressEnter="handleSearchAdmin"
/>
</a-form-item>
</a-form>
<div v-if="adminSearchResult && !currentCommunity.admin" class="admin-search-result">
<a-card :class="{ 'admin-card-selected': adminSearchResult.selected }" @click="selectSearchResult(adminSearchResult)">
<div class="admin-info">
<div class="admin-info-item">
<span class="admin-info-label">用户ID:</span>
<span class="admin-info-value">{{ adminSearchResult.userid }}</span>
</div>
<div class="admin-info-item">
<span class="admin-info-label">姓名:</span>
<span class="admin-info-value">{{ adminSearchResult.nickname }}</span>
</div>
<div class="admin-info-item">
<span class="admin-info-label">手机号:</span>
<span class="admin-info-value">{{ adminSearchResult.phone }}</span>
</div>
</div>
</a-card>
</div>
<div v-if="currentCommunity" class="current-admin-info">
<div class="current-admin-title">当前服务商信息</div>
<div v-if="currentCommunity.admin" class="admin-info">
<div class="admin-info-item">
<span class="admin-info-label">用户ID:</span>
<span class="admin-info-value">{{ currentCommunity.admin.userid || currentCommunity.admin_id }}</span>
</div>
<div class="admin-info-item">
<span class="admin-info-label">姓名:</span>
<span class="admin-info-value">{{ currentCommunity.admin.nickname || currentCommunity.admin_name || '未知' }}</span>
</div>
<div class="admin-info-item">
<span class="admin-info-label">手机号:</span>
<span class="admin-info-value">{{ currentCommunity.admin.phone || currentCommunity.admin_phone || '-' }}</span>
</div>
</div>
<div v-else class="no-admin-info">
<a-empty description="暂无服务商" />
</div>
</div>
</div>
</a-modal>
</div>
<!-- 二维码预览模态框 -->
<a-modal
v-model:visible="previewVisible"
:title="previewTitle"
:footer="null"
@cancel="handlePreviewCancel"
>
<img style="width: 100%" :src="previewImage" />
</a-modal>
</page-container>
</template>
<script>
import { defineComponent, ref, onMounted, nextTick } from 'vue'
import { message, Tag, Menu, Dropdown, Image, Upload } from 'ant-design-vue'
import { getCommunityList, createCommunity, updateCommunityStatus, updateCommunity, uploadImage } from '@/api/community'
import { loadAMap, createMap } from '@/utils/amap.js'
import dayjs from 'dayjs'
import PageContainer from '@/components/PageContainer.vue'
import { DownOutlined, PlusOutlined } from '@ant-design/icons-vue'
import MapPicker from '@/components/MapPicker/index.vue'
import request from '../../utils/request'
export default defineComponent({
components: {
PageContainer,
ATag: Tag,
ADropdown: Dropdown,
AMenu: Menu,
AMenuItem: Menu.Item,
DownOutlined,
MapPicker,
AImage: Image,
AUpload: Upload,
PlusOutlined
},
setup() {
const loading = ref(false)
const tableData = ref([])
const mapVisible = ref(false)
const currentMap = ref(null)
const currentMarker = ref(null)
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total} 条记录`
})
// 获取状态显示文本
const getStatusText = (status) => {
const statusMap = {
'OPENING': '运营中',
'UNOPEN': '未启用'
}
return statusMap[status] || status
}
// 获取状态标签颜色
const getStatusColor = (status) => {
const colorMap = {
'OPENING': 'green',
'UNOPEN': 'orange'
}
return colorMap[status] || 'default'
}
// 格式化日期时间
const formatDateTime = (value) => {
if (!value) return ''
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
}
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
align: 'center',
},
{
title: '小区名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: '配送定价',
key: 'delivery_price',
width: 200,
},
{
title: '分润比例',
key: 'profit_sharing',
width: 220,
},
{
title: '服务商',
key: 'admin',
width: 150,
},
{
title: '位置',
key: 'location',
width: 100,
align: 'center',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
align: 'center',
},
{
title: '群二维码',
key: 'qrcode',
width: 100,
align: 'center',
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center',
fixed: 'right'
},
]
// 获取小区列表数据
const fetchData = async () => {
try {
loading.value = true
const params = {
skip: (pagination.value.current - 1) * pagination.value.pageSize,
limit: pagination.value.pageSize
}
const res = await getCommunityList(params)
if (res.code === 200) {
tableData.value = res.data.items
pagination.value.total = res.data.total
} 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
fetchData()
}
// 显示地图
const showMap = async (record) => {
mapVisible.value = true
await nextTick()
try {
await loadAMap()
if (!currentMap.value) {
currentMap.value = createMap('map-container', {
zoom: 15,
viewMode: '3D'
})
}
const position = [record.longitude, record.latitude]
currentMap.value.setCenter(position)
if (currentMarker.value) {
currentMap.value.remove(currentMarker.value)
}
currentMarker.value = new window.AMap.Marker({
position,
title: record.name
})
currentMap.value.add(currentMarker.value)
} catch (error) {
console.error('加载地图失败:', error)
message.error('加载地图失败')
}
}
// 关闭地图
const closeMap = () => {
mapVisible.value = false
}
// 添加小区相关的响应式变量
const modalVisible = ref(false)
const confirmLoading = ref(false)
const isEdit = ref(false)
const formRef = ref(null)
const currentId = ref(null)
const formState = ref({
name: '',
location: {
address: '',
longitude: null,
latitude: null
},
qy_group_qrcode: '',
status: 'UNOPEN',
webot_webhook: ''
})
const rules = {
name: [{ required: true, message: '请输入小区名称' }],
'location.address': [{ required: true, message: '请选择地址' }],
'location.longitude': [{ required: true, message: '请在地图上选择位置' }],
'location.latitude': [{ required: true, message: '请在地图上选择位置' }],
webot_webhook: [
{ required: true, message: '请输入Webhook地址' },
{ type: 'url', message: '请输入正确的URL地址' }
],
qy_group_qrcode: [
{ required: true, message: '请上传群二维码' }
]
}
// 添加上传相关的响应式变量
const fileList = ref([])
const previewVisible = ref(false)
const previewImage = ref('')
const previewTitle = ref('')
// 显示编辑模态框
const handleEdit = (record) => {
isEdit.value = true
currentId.value = record.id
modalVisible.value = true
// 填充表单数据
formState.value = {
name: record.name,
location: {
address: record.address,
longitude: record.longitude,
latitude: record.latitude
},
qy_group_qrcode: record.qy_group_qrcode,
status: record.status,
webot_webhook: record.webot_webhook || ''
}
// 如果有二维码,设置文件列表
if (record.qy_group_qrcode) {
fileList.value = [{
uid: '-1',
name: '群二维码',
status: 'done',
url: record.qy_group_qrcode
}]
} else {
fileList.value = []
}
}
// 显示添加模态框
const showAddModal = () => {
isEdit.value = false
currentId.value = null
modalVisible.value = true
formState.value = {
name: '',
location: {
address: '',
longitude: null,
latitude: null
},
qy_group_qrcode: '',
status: 'UNOPEN',
webot_webhook: ''
}
fileList.value = []
}
// 修改预览图片函数
const previewQRCode = async (file) => {
// 如果直接传入URL字符串
if (typeof file === 'string') {
previewImage.value = file
previewVisible.value = true
previewTitle.value = '群二维码预览'
return
}
// 如果是已上传的图片,直接使用 url
if (file.url) {
previewImage.value = file.url
previewVisible.value = true
previewTitle.value = file.name || '群二维码预览'
return
}
// 如果是新上传的图片,需要处理 File 对象
if (file.originFileObj) {
try {
const preview = await getBase64(file.originFileObj)
previewImage.value = preview
previewVisible.value = true
previewTitle.value = file.name || '群二维码预览'
} catch (error) {
console.error('预览图片失败:', error)
message.error('预览失败')
}
}
}
// 关闭预览
const handlePreviewCancel = () => {
previewVisible.value = false
}
// 文件转base64
const getBase64 = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => resolve(reader.result)
reader.onerror = error => reject(error)
})
}
// 修改上传处理函数
const handleQRCodeUpload = async ({ file, onSuccess, onError }) => {
const formData = new FormData()
formData.append('file', file)
try {
const res = await request.post('/api/upload/image', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
if (res.code === 200) {
formState.value.qy_group_qrcode = res.data.url
// 更新文件列表以支持预览
fileList.value = [{
uid: file.uid,
name: file.name,
status: 'done',
url: res.data.url
}]
message.success('上传成功')
onSuccess(res)
} else {
message.error(res.message || '上传失败')
onError(new Error(res.message || '上传失败'))
}
} catch (error) {
console.error('上传图片失败:', error)
message.error('上传失败')
onError(error)
}
}
// 删除二维码
const handleQRCodeRemove = (file) => {
formState.value.qy_group_qrcode = ''
fileList.value = []
}
// 统一提交处理
const handleSubmit = () => {
formRef.value.validate().then(async () => {
try {
confirmLoading.value = true
const params = {
name: formState.value.name,
address: formState.value.location.address,
longitude: formState.value.location.longitude,
latitude: formState.value.location.latitude,
qy_group_qrcode: formState.value.qy_group_qrcode,
status: formState.value.status,
webot_webhook: formState.value.webot_webhook
}
let res
if (isEdit.value) {
res = await updateCommunity(currentId.value, params)
} else {
res = await createCommunity(params)
}
if (res.code === 200) {
message.success(isEdit.value ? '更新成功' : '添加成功')
modalVisible.value = false
fetchData() // 刷新列表
} else {
message.error(res.message || (isEdit.value ? '更新失败' : '添加失败'))
}
} catch (error) {
console.error(isEdit.value ? '更新小区失败:' : '添加小区失败:', error)
message.error(isEdit.value ? '更新失败' : '添加失败')
} finally {
confirmLoading.value = false
}
})
}
// 取消处理
const handleCancel = () => {
formRef.value?.resetFields()
fileList.value = []
modalVisible.value = false
}
// 处理状态变更
const handleStatusChange = async (record, status) => {
try {
const res = await updateCommunityStatus(record.id, status)
if (res.code === 200) {
message.success('状态更新成功')
// 更新本地数据
const index = tableData.value.findIndex(item => item.id === record.id)
if (index !== -1) {
tableData.value[index].status = status
}
} else {
message.error(res.message || '状态更新失败')
}
} catch (error) {
console.error('更新小区状态失败:', error)
message.error('状态更新失败')
}
}
// 添加配送定价相关的状态
const deliveryPriceModalVisible = ref(false)
const deliveryPriceSaving = ref(false)
const deliveryPriceForm = ref({
base_price: 0,
extra_package_threshold: 1,
extra_package_price: 0
})
const currentCommunityId = ref(null)
// 显示配送定价模态框
const handleEditDeliveryPrice = (record) => {
currentCommunityId.value = record.id
deliveryPriceForm.value = {
base_price: record.base_price || 0,
extra_package_threshold: record.extra_package_threshold || 1,
extra_package_price: record.extra_package_price || 0
}
deliveryPriceModalVisible.value = true
}
// 保存配送定价
const handleDeliveryPriceSave = async () => {
try {
deliveryPriceSaving.value = true
const res = await request.put(`/api/community/${currentCommunityId.value}`, {
base_price: deliveryPriceForm.value.base_price,
extra_package_threshold: deliveryPriceForm.value.extra_package_threshold,
extra_package_price: deliveryPriceForm.value.extra_package_price
})
if (res.code === 200) {
message.success('配送定价设置成功')
deliveryPriceModalVisible.value = false
fetchData() // 刷新列表
} else {
message.error(res.message || '设置失败')
}
} catch (error) {
console.error('设置配送定价失败:', error)
message.error('设置失败')
} finally {
deliveryPriceSaving.value = false
}
}
// 取消配送定价设置
const handleDeliveryPriceCancel = () => {
deliveryPriceModalVisible.value = false
currentCommunityId.value = null
deliveryPriceForm.value = {
base_price: 0,
extra_package_threshold: 1,
extra_package_price: 0
}
}
// 添加分润设置相关的状态
const profitSharingModalVisible = ref(false)
const profitSharingSaving = ref(false)
const currentCommunityName = ref('')
const isRateValid = ref(true)
const profitSharingForm = ref({
platform_rate: 10,
partner_rate: 10,
admin_rate: 10,
delivery_rate: 70
})
// 显示分润设置模态框
const handleEditProfitSharing = async (record) => {
currentCommunityId.value = record.id
currentCommunityName.value = record.name
profitSharingSaving.value = true
try {
// 检查是否已有分润设置
const hasProfitSharing = record.profit_sharing !== null
if (hasProfitSharing) {
// 如果已有分润设置,使用现有数据
profitSharingForm.value = {
platform_rate: record.profit_sharing.platform_rate,
partner_rate: record.profit_sharing.partner_rate,
admin_rate: record.profit_sharing.admin_rate,
delivery_rate: record.profit_sharing.delivery_rate
}
} else {
// 如果没有分润设置,使用默认值
profitSharingForm.value = {
platform_rate: 10,
partner_rate: 10,
admin_rate: 10,
delivery_rate: 70
}
}
validateTotalRate()
profitSharingModalVisible.value = true
} catch (error) {
console.error('获取分润设置失败:', error)
message.error('获取分润设置失败')
} finally {
profitSharingSaving.value = false
}
}
// 计算平台分润比例
const calculatePlatformRate = () => {
const otherRatesTotal =
profitSharingForm.value.partner_rate +
profitSharingForm.value.admin_rate +
profitSharingForm.value.delivery_rate
// 确保其他比例之和不超过100%
if (otherRatesTotal > 100) {
message.warning('其他分润比例之和不能超过100%')
return
}
// 计算平台分润比例
profitSharingForm.value.platform_rate = 100 - otherRatesTotal
// 验证总比例
validateTotalRate()
}
// 计算总比例
const getTotalRate = () => {
return (
profitSharingForm.value.platform_rate +
profitSharingForm.value.partner_rate +
profitSharingForm.value.admin_rate +
profitSharingForm.value.delivery_rate
)
}
// 验证总比例是否为100%
const validateTotalRate = () => {
const total = getTotalRate()
isRateValid.value = total === 100
return isRateValid.value
}
// 保存分润设置
const handleProfitSharingSave = async () => {
if (!validateTotalRate()) {
message.error('所有比例之和必须等于100%')
return
}
try {
profitSharingSaving.value = true
const params = {
platform_rate: profitSharingForm.value.platform_rate,
partner_rate: profitSharingForm.value.partner_rate,
admin_rate: profitSharingForm.value.admin_rate,
delivery_rate: profitSharingForm.value.delivery_rate
}
// 查找当前编辑的小区
const currentCommunity = tableData.value.find(item => item.id === currentCommunityId.value)
const hasProfitSharing = currentCommunity && currentCommunity.profit_sharing !== null
let res
if (hasProfitSharing) {
// 如果已有分润设置,调用修改接口
res = await request.put(`/api/community-profit-sharings/community/${currentCommunityId.value}`, params)
} else {
// 如果没有分润设置,调用创建接口
res = await request.post('/api/community-profit-sharings/', {
...params,
community_id: currentCommunityId.value
})
}
if (res.code === 200) {
// 查找当前编辑的小区在表格数据中的索引
const index = tableData.value.findIndex(item => item.id === currentCommunityId.value)
if (index !== -1) {
// 更新本地数据,避免重新请求整个列表
if (!tableData.value[index].profit_sharing) {
tableData.value[index].profit_sharing = {}
}
tableData.value[index].profit_sharing = {
platform_rate: params.platform_rate,
partner_rate: params.partner_rate,
admin_rate: params.admin_rate,
delivery_rate: params.delivery_rate
}
}
message.success('分润设置保存成功')
profitSharingModalVisible.value = false
} else {
message.error(res.message || '保存失败')
}
} catch (error) {
console.error('保存分润设置失败:', error)
message.error('保存失败')
} finally {
profitSharingSaving.value = false
}
}
// 取消分润设置
const handleProfitSharingCancel = () => {
profitSharingModalVisible.value = false
currentCommunityId.value = null
currentCommunityName.value = ''
}
// 添加服务商相关的状态
const adminModalVisible = ref(false)
const adminSaving = ref(false)
const adminSearching = ref(false)
const adminSearchPhone = ref('')
const adminSearchResult = ref(null)
const currentCommunity = ref(null)
// 显示设置服务商模态框
const handleSetAdmin = (record) => {
currentCommunity.value = { ...record }
adminSearchPhone.value = ''
adminSearchResult.value = null
// 如果已有服务商,不需要设置搜索结果
// 如果没有服务商,则需要通过搜索来选择
adminModalVisible.value = true
}
// 搜索用户
const handleSearchAdmin = async () => {
if (!adminSearchPhone.value || adminSearchPhone.value.length < 5) {
message.warning('请输入有效的手机号')
return
}
try {
adminSearching.value = true
const res = await request.get(`/api/user/search_by_phone/${adminSearchPhone.value}`)
if (res.code === 200) {
if (res.data) {
adminSearchResult.value = {
...res.data,
selected: false
}
} else {
adminSearchResult.value = null
message.warning('未找到该用户')
}
} else {
message.error(res.message || '搜索失败')
}
} catch (error) {
console.error('搜索用户失败:', error)
message.error('搜索失败')
} finally {
adminSearching.value = false
}
}
// 选择搜索结果
const selectSearchResult = (result) => {
if (adminSearchResult.value) {
adminSearchResult.value.selected = !adminSearchResult.value.selected
}
}
// 保存服务商设置
const handleAdminSave = async () => {
// 如果已有服务商,则直接关闭模态框,不做任何操作
if (currentCommunity.value.admin) {
adminModalVisible.value = false
return
}
if (!adminSearchResult.value) {
message.warning('请先搜索并选择服务商')
return
}
if (!adminSearchResult.value.selected) {
message.warning('请先选择搜索结果中的用户作为服务商')
return
}
try {
adminSaving.value = true
const params = {
admin_id: adminSearchResult.value.userid
}
const res = await request.put(`/api/community/${currentCommunity.value.id}`, params)
if (res.code === 200) {
message.success('服务商设置成功')
adminModalVisible.value = false
// 刷新列表数据
fetchData()
} else {
message.error(res.message || '设置失败')
}
} catch (error) {
console.error('设置服务商失败:', error)
message.error('设置失败')
} finally {
adminSaving.value = false
}
}
// 取消服务商设置
const handleAdminCancel = () => {
adminModalVisible.value = false
adminSearchPhone.value = ''
adminSearchResult.value = null
currentCommunity.value = null
}
onMounted(() => {
fetchData()
})
return {
loading,
columns,
tableData,
pagination,
mapVisible,
handleTableChange,
showMap,
closeMap,
getStatusText,
getStatusColor,
formatDateTime,
modalVisible,
confirmLoading,
isEdit,
formState,
formRef,
rules,
showAddModal,
handleSubmit,
handleCancel,
handleStatusChange,
fileList,
handleQRCodeUpload,
handleQRCodeRemove,
handleEdit,
previewVisible,
previewImage,
previewTitle,
handlePreviewCancel,
previewQRCode,
deliveryPriceModalVisible,
deliveryPriceSaving,
deliveryPriceForm,
handleEditDeliveryPrice,
handleDeliveryPriceSave,
handleDeliveryPriceCancel,
profitSharingModalVisible,
profitSharingSaving,
profitSharingForm,
currentCommunityName,
isRateValid,
handleEditProfitSharing,
handleProfitSharingSave,
handleProfitSharingCancel,
getTotalRate,
validateTotalRate,
calculatePlatformRate,
adminModalVisible,
adminSaving,
adminSearching,
adminSearchPhone,
adminSearchResult,
currentCommunity,
handleSetAdmin,
handleSearchAdmin,
handleAdminSave,
handleAdminCancel,
selectSearchResult
}
}
})
</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;
}
:deep(.ant-modal-body) {
padding: 24px;
max-width: 100%;
}
:deep(.ant-form) {
max-width: 600px;
margin: 0 auto;
}
:deep(.ant-form-item) {
margin-bottom: 16px;
}
:deep(.ant-input),
:deep(.ant-input-number),
:deep(.ant-auto-complete) {
width: 100% !important;
}
:deep(.ant-input-number-input) {
height: 32px;
}
:deep(.ant-form-item-control-input) {
width: 100%;
}
:deep(.ant-col) {
padding-right: 12px;
padding-left: 12px;
}
:deep(.ant-select-item-option-content) {
white-space: normal;
word-break: break-all;
}
:deep(.ant-row) {
margin-bottom: 0 !important;
}
:deep(.ant-form-item) {
margin-bottom: 16px;
}
:deep(.ant-col) {
padding-right: 8px;
padding-left: 8px;
}
/* 地图区域样式 */
.map-section {
margin-bottom: 24px;
padding: 16px;
background: #fafafa;
border-radius: 4px;
}
.search-item {
margin-bottom: 16px;
}
.map-container {
border: 1px solid #f0f0f0;
border-radius: 2px;
overflow: hidden;
}
:deep(.ant-form-item-label) {
height: 32px;
line-height: 32px;
text-align: left;
padding-right: 12px;
/* 调整必填星号的样式 */
> label.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before {
color: #ff4d4f;
font-size: 14px;
margin-right: 4px;
}
}
:deep(.ant-form-item) {
margin-bottom: 24px;
}
:deep(.ant-input),
:deep(.ant-input-number),
:deep(.ant-auto-complete) {
width: 100%;
}
:deep(.ant-input-group) {
display: flex;
}
:deep(.ant-input-group .ant-input-number) {
border-radius: 0;
&:first-child {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
}
&:last-child {
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
}
}
:deep(.ant-form) {
max-width: 600px;
margin: 0 auto;
}
:deep(.ant-modal-body) {
padding: 24px;
}
:deep(.ant-modal-footer) {
text-align: left;
padding: 16px 24px;
border-top: 1px solid #f0f0f0;
}
/* 优化表单样式,减少高度 */
.community-form {
padding: 4px 0;
}
:deep(.ant-form-item) {
margin-bottom: 12px !important;
}
:deep(.ant-form-item-label) {
height: 28px;
line-height: 28px;
padding-bottom: 0;
}
:deep(.ant-form-item-control) {
line-height: 28px;
}
:deep(.ant-input),
:deep(.ant-select),
:deep(.ant-auto-complete),
:deep(.ant-input-number) {
height: 30px;
}
:deep(.ant-select-selector),
:deep(.ant-auto-complete .ant-input) {
height: 30px !important;
line-height: 30px !important;
padding: 0 11px !important;
.ant-select-selection-search-input {
height: 28px !important;
}
.ant-select-selection-item {
line-height: 28px !important;
}
}
:deep(.ant-input-number-input) {
height: 28px;
line-height: 28px;
}
.form-item-tip {
color: rgba(0, 0, 0, 0.45);
font-size: 12px;
margin-top: 2px;
line-height: 1.2;
}
.qrcode-upload-wrapper {
:deep(.ant-upload-list-picture-card-container) {
width: 90px;
height: 90px;
margin-right: 8px;
margin-bottom: 8px;
}
:deep(.ant-upload.ant-upload-select-picture-card) {
width: 90px;
height: 90px;
margin-right: 8px;
margin-bottom: 8px;
}
:deep(.ant-upload-list-picture-card .ant-upload-list-item) {
padding: 2px;
}
}
/* 调整地图组件样式 */
:deep(.map-picker-container) {
margin-bottom: 12px;
}
:deep(.map-picker-input) {
margin-bottom: 8px;
}
:deep(.map-container) {
height: 200px !important; /* 减少地图高度 */
}
.status-tag {
cursor: pointer;
user-select: none;
display: inline-flex;
align-items: center;
gap: 4px;
}
:deep(.anticon) {
font-size: 12px;
}
/* 调整 Select 组件的样式 */
:deep(.ant-select-selector) {
height: 32px !important;
.ant-select-selection-item {
line-height: 30px !important; /* 调整文字行高 */
padding-top: 0 !important; /* 移除顶部内边距 */
padding-bottom: 0 !important; /* 移除底部内边距 */
}
}
/* 调整选项的样式 */
:deep(.ant-select-dropdown) {
.ant-select-item {
padding: 5px 12px; /* 调整选项内边距 */
line-height: 22px; /* 调整选项行高 */
}
}
/* 确保输入框内的文字垂直居中 */
:deep(.ant-select-selection-search-input) {
height: 30px !important;
line-height: 30px !important;
}
/* 确保占位符文字垂直居中 */
:deep(.ant-select-selection-placeholder) {
line-height: 30px !important;
}
.delivery-price-info {
line-height: 1.5;
font-size: 13px;
> div {
margin-bottom: 2px;
&:last-child {
margin-bottom: 0;
color: rgba(0, 0, 0, 0.65);
font-size: 12px;
}
}
}
/* 添加分润设置相关的样式 */
.profit-sharing-info {
margin-bottom: 16px;
padding: 12px;
background-color: #f9f9f9;
border-radius: 4px;
}
.profit-sharing-header {
margin-bottom: 16px;
padding: 12px;
background-color: #f9f9f9;
border-radius: 4px;
}
.profit-sharing-title {
margin-bottom: 8px;
font-size: 14px;
}
.profit-sharing-community-name {
font-weight: 500;
color: #1890ff;
}
.profit-sharing-tip {
font-size: 12px;
color: #ff4d4f;
}
.profit-sharing-form {
margin-bottom: 16px;
}
.profit-rate-row {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.profit-rate-label {
width: 150px;
font-size: 14px;
color: rgba(0, 0, 0, 0.85);
}
.profit-rate-input {
width: 100px;
margin-right: 12px;
}
.profit-rate-tip {
flex: 1;
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
.total-rate-info {
margin-top: 16px;
padding: 8px 12px;
background-color: #f6ffed;
border-radius: 4px;
border: 1px solid #b7eb8f;
color: #52c41a;
font-weight: 500;
text-align: center;
}
.rate-error {
background-color: #fff2f0;
border-color: #ffccc7;
color: #ff4d4f;
}
.rate-error-message {
margin-left: 8px;
font-weight: normal;
}
/* 表格中分润比例显示样式 */
.profit-sharing-info-table {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 12px;
padding: 4px 0;
}
.profit-rate-item {
display: flex;
align-items: center;
line-height: 1.5;
}
.rate-label {
color: rgba(0, 0, 0, 0.65);
margin-right: 4px;
margin-left: 8px;
white-space: nowrap;
}
.rate-label:first-child {
margin-left: 0;
}
.rate-value {
font-weight: 500;
color: #1890ff;
margin-right: 12px;
white-space: nowrap;
}
/* 操作列按钮样式 */
.action-buttons {
display: flex;
justify-content: center;
align-items: center;
white-space: nowrap;
}
.action-buttons a {
white-space: nowrap;
padding: 0 4px;
}
:deep(.ant-divider-vertical) {
margin: 0 8px;
}
/* 平台分润比例输入框样式 */
.platform-rate-input {
background-color: #f5f5f5;
}
:deep(.platform-rate-input .ant-input-number-input) {
color: #1890ff;
font-weight: 500;
}
/* 配送定价表单样式 */
.delivery-price-form {
padding: 0;
}
.delivery-price-form :deep(.ant-form-item) {
margin-bottom: 12px !important;
}
.delivery-price-form :deep(.ant-form-item:last-child) {
margin-bottom: 0 !important;
}
.delivery-price-form :deep(.ant-form-item-label) {
padding-bottom: 4px;
}
.delivery-price-form :deep(.ant-form-item-label > label) {
font-size: 14px;
height: 22px;
}
.delivery-price-form :deep(.ant-input-number) {
width: 100% !important;
}
.delivery-price-form .form-item-tip {
margin-top: 2px;
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
line-height: 1.2;
}
/* 服务商设置相关样式 */
.admin-search-form {
margin-bottom: 16px;
}
.admin-search-result {
margin-top: 16px;
margin-bottom: 16px;
}
.admin-card-selected {
border: 2px solid #1890ff;
cursor: pointer;
}
.admin-search-result :deep(.ant-card) {
cursor: pointer;
transition: all 0.3s;
}
.admin-search-result :deep(.ant-card:hover) {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.current-admin-info {
margin-top: 24px;
border-top: 1px dashed #e8e8e8;
padding-top: 16px;
}
.current-admin-title {
font-size: 14px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 12px;
}
.admin-info {
background-color: #f9f9f9;
padding: 12px;
border-radius: 4px;
}
.admin-info-item {
margin-bottom: 8px;
display: flex;
}
.admin-info-item:last-child {
margin-bottom: 0;
}
.admin-info-label {
width: 70px;
color: rgba(0, 0, 0, 0.65);
}
.admin-info-value {
flex: 1;
color: rgba(0, 0, 0, 0.85);
font-weight: 500;
}
.no-admin-info {
padding: 16px 0;
}
/* 表格中服务商信息显示样式 */
.admin-info-table {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
padding: 4px 0;
}
.admin-info-row {
line-height: 1.5;
}
.admin-name {
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
.admin-phone {
color: rgba(0, 0, 0, 0.65);
}
</style>