Files
Vue3/src/views/shop/List.vue
XingQue f1ba100d56 feat: 店铺管理页新增复制店铺商品功能
在店铺详情中支持填写源/目标用户ID并选择覆盖或补充模式调用新接口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 22:58:57 +08:00

670 lines
23 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>
<div class="shop-list-page">
<!-- 默认配置区域 -->
<el-card class="default-config-card" shadow="never">
<template #header>
<span class="card-title">系统默认配置</span>
</template>
<div class="default-config-content">
<div class="config-item">
<span class="config-label">默认订单抽成费率</span>
<span class="config-value">{{ formatPercent(defaultConfig.moren_choucheng_feilv) }}</span>
</div>
<div class="config-item">
<span class="config-label">默认可提现限额</span>
<span class="config-value">¥{{ defaultConfig.moren_tixian_xiane }}</span>
</div>
<el-button type="primary" size="small" @click="openDefaultConfigDialog">修改默认配置</el-button>
</div>
</el-card>
<!-- 筛选区域 -->
<el-card class="filter-card" shadow="never">
<el-form :inline="true" :model="filterForm" class="filter-form">
<el-form-item label="店铺名称">
<el-input v-model="filterForm.dianpu_mingcheng" placeholder="输入名称" clearable />
</el-form-item>
<el-form-item label="店铺状态">
<el-select v-model="filterForm.zhuangtai" placeholder="全部" clearable>
<el-option label="正常" :value="1" />
<el-option label="封禁" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="filterForm.lianxi_dianhua" placeholder="输入电话" clearable />
</el-form-item>
<el-form-item label="用户ID">
<el-input v-model="filterForm.user_id" placeholder="输入用户ID" clearable />
</el-form-item>
<el-form-item label="提现余额">
<el-input-number v-model="filterForm.ketixian_yue_min" :min="0" placeholder="≥" style="width: 120px" />
<span style="margin: 0 8px"></span>
<el-input-number v-model="filterForm.ketixian_yue_max" :min="0" placeholder="≤" style="width: 120px" />
</el-form-item>
<el-form-item label="赚取总额">
<el-input-number v-model="filterForm.zhengqu_zonge_min" :min="0" placeholder="≥" style="width: 120px" />
<span style="margin: 0 8px"></span>
<el-input-number v-model="filterForm.zhengqu_zonge_max" :min="0" placeholder="≤" style="width: 120px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetFilter">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 列表表格 -->
<el-card class="table-card" shadow="never">
<div class="table-header-actions">
<el-button type="success" @click="openAddDialog">添加店铺</el-button>
</div>
<el-table :data="shopList" v-loading="loading" stripe border style="width: 100%">
<el-table-column label="头像" width="80">
<template #default="{ row }">
<el-avatar :size="40" :src="getFullAvatar(row.dianpu_touxiang)">
<el-icon :size="24"><Shop /></el-icon>
</el-avatar>
</template>
</el-table-column>
<el-table-column prop="dianpu_mingcheng" label="店铺名称" min-width="120" />
<el-table-column prop="id" label="店铺ID" width="80" />
<el-table-column prop="pingzheng__yonghu" label="用户ID" width="100" />
<el-table-column label="可提现余额" width="130">
<template #default="{ row }">¥{{ row.ketixian_yue }}</template>
</el-table-column>
<el-table-column label="赚取总额" width="130">
<template #default="{ row }">¥{{ row.zhengqu_zonge }}</template>
</el-table-column>
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag :type="row.zhuangtai === 1 ? 'success' : 'danger'" size="small">
{{ row.zhuangtai === 1 ? '正常' : '封禁' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button size="small" type="primary" @click="openDetailDialog(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
layout="total, prev, pager, next, jumper"
@current-change="fetchShopList"
style="margin-top: 20px; justify-content: flex-end;"
/>
</el-card>
<!-- 详情/编辑弹窗 -->
<el-dialog v-model="detailVisible" title="店铺详情" width="700px" destroy-on-close>
<template v-if="detailForm">
<el-form :model="detailForm" label-width="140px" :disabled="!isEditing">
<el-form-item label="店铺ID">
<el-input v-model="detailForm.id" disabled />
</el-form-item>
<el-form-item label="用户ID">
<el-input :value="detailForm.pingzheng__yonghu || ''" disabled />
</el-form-item>
<el-form-item label="店铺名称">
<el-input v-model="detailForm.dianpu_mingcheng" />
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="detailForm.lianxi_dianhua" />
</el-form-item>
<el-form-item label="微信号">
<el-input v-model="detailForm.weixinhao" />
</el-form-item>
<el-form-item label="店铺状态">
<el-switch
v-model="detailForm.zhuangtai"
:active-value="1"
:inactive-value="0"
active-text="正常"
inactive-text="封禁"
/>
</el-form-item>
<el-form-item label="可提现余额">
<el-input-number v-model="detailForm.ketixian_yue" :min="0" :precision="2" />
</el-form-item>
<el-form-item label="开启收益抽成">
<el-switch v-model="detailForm.kaiqi_shouyi_choucheng" />
</el-form-item>
<el-form-item v-if="detailForm.kaiqi_shouyi_choucheng" label="抽成费率(%">
<el-input-number
v-model="detailForm.shouyi_choucheng_feilv_percent"
:min="0"
:max="100"
:precision="2"
placeholder="例如 10 表示 10%"
/>
<span class="form-tip">输入百分比数值 10 表示 10%</span>
</el-form-item>
<el-form-item label="开启每日提现限额">
<el-switch v-model="detailForm.kaiqi_meiri_xiane" />
</el-form-item>
<el-form-item v-if="detailForm.kaiqi_meiri_xiane" label="每日提现限额">
<el-input-number v-model="detailForm.meiri_tixian_xiane" :min="0" :precision="2" />
</el-form-item>
</el-form>
<div class="detail-actions">
<el-button v-if="!isEditing" type="warning" @click="enterEdit">编辑</el-button>
<el-button v-if="!isEditing" type="success" plain @click="openCopyCatalogDialog">复制店铺商品</el-button>
<template v-else>
<el-button type="primary" @click="saveDetail">保存</el-button>
<el-button @click="cancelEdit">取消</el-button>
</template>
</div>
<div class="operation-data-link">
<el-button type="info" plain @click="openOperationDataDialog(detailForm.id)">
查看店铺运营数据
</el-button>
</div>
</template>
</el-dialog>
<!-- 添加店铺弹窗 -->
<el-dialog v-model="addVisible" title="添加店铺" width="600px" destroy-on-close>
<el-form :model="addForm" label-width="160px">
<el-form-item label="用户ID" required>
<el-input v-model="addForm.yonghu_id" placeholder="7位用户ID" maxlength="7" />
</el-form-item>
<el-form-item label="登录账号" required>
<el-input v-model="addForm.zhanghao" placeholder="账号" />
</el-form-item>
<el-form-item label="登录密码" required>
<el-input v-model="addForm.mima" placeholder="密码" show-password />
</el-form-item>
<el-form-item label="店铺名称" required>
<el-input v-model="addForm.dianpu_mingcheng" placeholder="店铺名称" />
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="addForm.lianxi_dianhua" placeholder="选填" />
</el-form-item>
<el-form-item label="微信号">
<el-input v-model="addForm.weixinhao" placeholder="选填" />
</el-form-item>
<el-form-item label="开启收益抽成">
<el-switch v-model="addForm.kaiqi_shouyi_choucheng" />
</el-form-item>
<el-form-item v-if="addForm.kaiqi_shouyi_choucheng" label="抽成费率(%">
<el-input-number v-model="addForm.shouyi_choucheng_feilv_percent" :min="0" :max="100" :precision="2" />
</el-form-item>
<el-form-item label="开启每日提现限额">
<el-switch v-model="addForm.kaiqi_meiri_xiane" />
</el-form-item>
<el-form-item v-if="addForm.kaiqi_meiri_xiane" label="每日提现限额">
<el-input-number v-model="addForm.meiri_tixian_xiane" :min="0" :precision="2" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="addVisible = false">取消</el-button>
<el-button type="primary" @click="submitAdd">确定添加</el-button>
</template>
</el-dialog>
<!-- 默认配置修改弹窗 -->
<el-dialog v-model="defaultConfigVisible" title="修改默认配置" width="500px">
<el-form label-width="160px">
<el-form-item label="默认抽成费率(%">
<el-input-number
v-model="defaultEditForm.moren_choucheng_feilv_percent"
:min="0"
:max="100"
:precision="2"
/>
</el-form-item>
<el-form-item label="默认可提现限额">
<el-input-number v-model="defaultEditForm.moren_tixian_xiane" :min="0" :precision="2" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="defaultConfigVisible = false">取消</el-button>
<el-button type="primary" @click="saveDefaultConfig">保存</el-button>
</template>
</el-dialog>
<!-- 复制店铺商品弹窗 -->
<el-dialog v-model="copyCatalogVisible" title="复制店铺商品" width="560px" destroy-on-close>
<el-alert
type="info"
:closable="false"
show-icon
style="margin-bottom: 16px"
title="将源店铺的商品类型、专区、商品完整复制到目标店铺;图片会在存储中独立复制,互不影响。"
/>
<el-form :model="copyCatalogForm" label-width="140px">
<el-form-item label="源店铺用户ID" required>
<el-input v-model="copyCatalogForm.source_user_uid" placeholder="被复制店铺的小程序用户ID" maxlength="7" />
</el-form-item>
<el-form-item label="目标店铺用户ID" required>
<el-input v-model="copyCatalogForm.target_user_uid" placeholder="接收复制的小程序用户ID" maxlength="7" />
</el-form-item>
<el-form-item label="复制模式" required>
<el-radio-group v-model="copyCatalogForm.copy_mode">
<el-radio value="append">补充保留目标店原有追加复制</el-radio>
<el-radio value="replace">全部覆盖清空目标店后复制</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="copyCatalogVisible = false">取消</el-button>
<el-button type="primary" :loading="copyCatalogLoading" @click="submitCopyCatalog">开始复制</el-button>
</template>
</el-dialog>
<!-- 运营数据弹窗内嵌子组件 -->
<el-dialog v-model="operationVisible" title="店铺运营数据" width="80%" top="5vh" destroy-on-close>
<ShopOperationData v-if="operationDianpuId" :dianpu-id="operationDianpuId" />
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Shop } from '@element-plus/icons-vue'
import request from '@/utils/request'
import ShopOperationData from './components/ShopOperationData.vue' // 引入子组件
// ==================== 默认配置相关 ====================
const defaultConfig = reactive({
moren_choucheng_feilv: 0,
moren_tixian_xiane: 0
})
// ==================== 筛选 ====================
const filterForm = reactive({
dianpu_mingcheng: '',
zhuangtai: null, // 1正常 0封禁 全部为null
lianxi_dianhua: '',
user_id: '',
ketixian_yue_min: null,
ketixian_yue_max: null,
zhengqu_zonge_min: null,
zhengqu_zonge_max: null
})
const shopList = ref([])
const loading = ref(false)
const pagination = reactive({
page: 1,
pageSize: 10,
total: 0
})
// 工具前端费率百分比转换接口内部存储为小数如0.1表示10%
const toPercent = (decimal) => {
if (decimal == null) return 0
return Number((decimal * 100).toFixed(2))
}
const toDecimal = (percent) => {
if (percent == null) return 0
return Number((percent / 100).toFixed(4))
}
const formatPercent = (decimal) => {
return toPercent(decimal) + '%'
}
// 头像拼接
const getFullAvatar = (url) => {
if (!url) return ''
if (url.startsWith('http')) return url
const base = window.$ossURL || ''
return base.replace(/\/$/, '') + '/' + url.replace(/^\//, '')
}
// 获取店铺列表
const fetchShopList = async () => {
loading.value = true
try {
const params = {
page: pagination.page,
page_size: pagination.pageSize,
username: localStorage.getItem('username') || '',
...filterForm
}
const res = await request({
url: '/houtai/hqdplbsj',
method: 'POST', // 假设为POST根据实际调整
data: params
})
if (res.code === 0 || res.code === 200) {
const data = res.data
shopList.value = data.list || []
pagination.total = data.total || 0
// 默认配置可能随列表一起返回
if (data.default_config) {
Object.assign(defaultConfig, data.default_config)
}
} else {
ElMessage.error(res.msg || '获取店铺列表失败')
}
} catch (error) {
console.error('获取列表失败:', error)
ElMessage.error('网络错误')
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.page = 1
fetchShopList()
}
const resetFilter = () => {
Object.keys(filterForm).forEach(key => (filterForm[key] = key.includes('min') || key.includes('max') ? null : ''))
filterForm.zhuangtai = null
handleSearch()
}
// ==================== 详情编辑弹窗 ====================
const detailVisible = ref(false)
const isEditing = ref(false)
const detailForm = ref(null)
const openDetailDialog = (row) => {
// 深拷贝,并计算百分比展示
detailForm.value = {
...row,
shouyi_choucheng_feilv_percent: toPercent(row.shouyi_choucheng_feilv),
// 补全可能缺失的开关
kaiqi_shouyi_choucheng: row.kaiqi_shouyi_choucheng || false,
kaiqi_meiri_xiane: row.kaiqi_meiri_xiane || false,
}
isEditing.value = false
detailVisible.value = true
}
const enterEdit = () => {
isEditing.value = true
}
const cancelEdit = () => {
isEditing.value = false
// 恢复到原始数据
const original = shopList.value.find(item => item.id === detailForm.value.id)
if (original) {
detailForm.value = {
...original,
shouyi_choucheng_feilv_percent: toPercent(original.shouyi_choucheng_feilv),
kaiqi_shouyi_choucheng: original.kaiqi_shouyi_choucheng || false,
kaiqi_meiri_xiane: original.kaiqi_meiri_xiane || false,
}
}
}
const saveDetail = async () => {
const form = detailForm.value
if (!form) return
// 转换抽成费率
const feilv = toDecimal(form.shouyi_choucheng_feilv_percent)
const data = {
action: 'update_dianpu', // 明确操作类型
dianpu_id: form.id,
username: localStorage.getItem('username') || '',
dianpu_mingcheng: form.dianpu_mingcheng,
lianxi_dianhua: form.lianxi_dianhua,
weixinhao: form.weixinhao,
zhuangtai: form.zhuangtai,
ketixian_yue: form.ketixian_yue,
kaiqi_shouyi_choucheng: form.kaiqi_shouyi_choucheng,
shouyi_choucheng_feilv: feilv,
kaiqi_meiri_xiane: form.kaiqi_meiri_xiane,
meiri_tixian_xiane: form.meiri_tixian_xiane || 0
}
try {
const res = await request.post('/houtai/xgdpxx', data)
if (res.code === 0 || res.code === 200) {
ElMessage.success('修改成功')
detailVisible.value = false
fetchShopList() // 刷新列表
} else {
ElMessage.error(res.msg || '修改失败')
}
} catch (error) {
ElMessage.error('请求异常')
}
}
// ==================== 添加店铺弹窗 ====================
const addVisible = ref(false)
const addForm = reactive({
yonghu_id: '',
zhanghao: '',
mima: '',
dianpu_mingcheng: '',
lianxi_dianhua: '',
weixinhao: '',
kaiqi_shouyi_choucheng: false,
shouyi_choucheng_feilv_percent: 0,
kaiqi_meiri_xiane: false,
meiri_tixian_xiane: 0
})
const openAddDialog = () => {
Object.assign(addForm, {
yonghu_id: '',
zhanghao: '',
mima: '',
dianpu_mingcheng: '',
lianxi_dianhua: '',
weixinhao: '',
kaiqi_shouyi_choucheng: false,
shouyi_choucheng_feilv_percent: 0,
kaiqi_meiri_xiane: false,
meiri_tixian_xiane: 0
})
addVisible.value = true
}
const submitAdd = async () => {
if (!addForm.yonghu_id || !addForm.zhanghao || !addForm.mima || !addForm.dianpu_mingcheng) {
ElMessage.warning('请填写必填项用户ID、账号、密码、店铺名称')
return
}
const feilv = toDecimal(addForm.shouyi_choucheng_feilv_percent)
const data = {
action: 'add_dianpu',
username: localStorage.getItem('username') || '',
yonghu_id: addForm.yonghu_id,
zhanghao: addForm.zhanghao,
mima: addForm.mima,
dianpu_mingcheng: addForm.dianpu_mingcheng,
lianxi_dianhua: addForm.lianxi_dianhua,
weixinhao: addForm.weixinhao,
kaiqi_shouyi_choucheng: addForm.kaiqi_shouyi_choucheng,
shouyi_choucheng_feilv: feilv,
kaiqi_meiri_xiane: addForm.kaiqi_meiri_xiane,
meiri_tixian_xiane: addForm.meiri_tixian_xiane
}
try {
const res = await request.post('/houtai/xgdpxx', data)
if (res.code === 0 || res.code === 200) {
ElMessage.success('添加成功')
addVisible.value = false
fetchShopList()
} else {
ElMessage.error(res.msg || '添加失败')
}
} catch (error) {
ElMessage.error('请求异常')
}
}
// ==================== 默认配置修改 ====================
const defaultConfigVisible = ref(false)
const defaultEditForm = reactive({
moren_choucheng_feilv_percent: 0,
moren_tixian_xiane: 0
})
const openDefaultConfigDialog = () => {
defaultEditForm.moren_choucheng_feilv_percent = toPercent(defaultConfig.moren_choucheng_feilv)
defaultEditForm.moren_tixian_xiane = defaultConfig.moren_tixian_xiane
defaultConfigVisible.value = true
}
const saveDefaultConfig = async () => {
const feilv = toDecimal(defaultEditForm.moren_choucheng_feilv_percent)
try {
const res = await request.post('/houtai/xgdpxx', {
action: 'update_default',
username: localStorage.getItem('username') || '',
moren_choucheng_feilv: feilv,
moren_tixian_xiane: defaultEditForm.moren_tixian_xiane
})
if (res.code === 0 || res.code === 200) {
ElMessage.success('默认配置已更新')
defaultConfig.moren_choucheng_feilv = feilv
defaultConfig.moren_tixian_xiane = defaultEditForm.moren_tixian_xiane
defaultConfigVisible.value = false
} else {
ElMessage.error(res.msg || '更新失败')
}
} catch (error) {
ElMessage.error('请求异常')
}
}
// ==================== 复制店铺商品 ====================
const copyCatalogVisible = ref(false)
const copyCatalogLoading = ref(false)
const copyCatalogForm = reactive({
source_user_uid: '',
target_user_uid: '',
copy_mode: 'append',
})
const openCopyCatalogDialog = () => {
const form = detailForm.value
copyCatalogForm.source_user_uid = form?.pingzheng__yonghu ? String(form.pingzheng__yonghu) : ''
copyCatalogForm.target_user_uid = ''
copyCatalogForm.copy_mode = 'append'
copyCatalogVisible.value = true
}
const submitCopyCatalog = async () => {
const sourceUid = copyCatalogForm.source_user_uid.trim()
const targetUid = copyCatalogForm.target_user_uid.trim()
if (!sourceUid || !targetUid) {
ElMessage.warning('请填写源店铺与目标店铺的用户ID')
return
}
if (sourceUid === targetUid) {
ElMessage.warning('源店铺与目标店铺不能相同')
return
}
if (copyCatalogForm.copy_mode === 'replace') {
try {
await ElMessageBox.confirm(
'全部覆盖将清空目标店铺现有的商品类型、专区和商品(含图片),此操作不可撤销,是否继续?',
'危险操作确认',
{ type: 'warning', confirmButtonText: '确认覆盖', cancelButtonText: '取消' }
)
} catch {
return
}
}
copyCatalogLoading.value = true
try {
const res = await request.post('/houtai/htfzdpspsj', {
username: localStorage.getItem('username') || '',
source_user_uid: sourceUid,
target_user_uid: targetUid,
copy_mode: copyCatalogForm.copy_mode,
})
if (res.code === 0 || res.code === 200) {
ElMessage.success(res.msg || '复制成功')
copyCatalogVisible.value = false
} else {
ElMessage.error(res.msg || '复制失败')
}
} catch (error) {
ElMessage.error('请求异常')
} finally {
copyCatalogLoading.value = false
}
}
// ==================== 运营数据弹窗 ====================
const operationVisible = ref(false)
const operationDianpuId = ref(null)
const openOperationDataDialog = (dianpuId) => {
operationDianpuId.value = dianpuId
operationVisible.value = true
}
// 初始化
onMounted(() => {
fetchShopList()
})
</script>
<style scoped>
.shop-list-page {
padding: 20px;
}
.default-config-card,
.filter-card,
.table-card {
margin-bottom: 20px;
}
.card-title {
font-weight: bold;
}
.default-config-content {
display: flex;
align-items: center;
gap: 40px;
flex-wrap: wrap;
}
.config-item {
display: flex;
align-items: baseline;
gap: 8px;
}
.config-label {
color: #606266;
}
.config-value {
font-weight: 600;
color: #409eff;
}
.filter-form .el-form-item {
margin-bottom: 10px;
}
.table-header-actions {
margin-bottom: 16px;
}
.detail-actions {
margin-top: 24px;
text-align: right;
}
.operation-data-link {
margin-top: 20px;
text-align: center;
}
.form-tip {
margin-left: 8px;
color: #909399;
font-size: 12px;
}
</style>