第一次提交:Vue3 后台前端项目当前最新版

This commit is contained in:
XingQue
2026-06-12 22:04:02 +08:00
commit a7f7bd0c39
74 changed files with 30910 additions and 0 deletions

578
src/views/shop/List.vue Normal file
View File

@@ -0,0 +1,578 @@
<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>
<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="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 } 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 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>

498
src/views/shop/Products.vue Normal file
View File

@@ -0,0 +1,498 @@
<template>
<div class="shop-product-manage">
<!-- 审核模式开关 -->
<el-card shadow="never" class="top-card">
<div class="switch-bar">
<span class="switch-label">商品审核模式</span>
<el-switch
v-model="auditMode"
active-text="开启"
inactive-text="关闭"
:before-change="beforeSwitchAudit"
/>
<span class="switch-desc">开启后商家发布的商品需审核才能上架</span>
</div>
</el-card>
<!-- 第一区域公共商品类型横排 -->
<el-card shadow="never" class="filter-card">
<template #header>
<span class="card-title">商品类型</span>
</template>
<div class="type-tabs">
<el-button
v-for="type in publicTypes"
:key="type.id"
:type="selectedPublicType === type.id ? 'primary' : ''"
size="small"
@click="selectPublicType(type.id)"
>
<el-image
:src="getFullUrl(type.tupian_url)"
style="width:20px;height:20px;vertical-align:middle;margin-right:4px"
fit="contain"
>
<template #error><span></span></template>
</el-image>
{{ type.jieshao || '未命名' }}
</el-button>
</div>
</el-card>
<!-- 第二区域店铺列表分页搜索筛选 -->
<el-card shadow="never" class="filter-card">
<template #header>
<span class="card-title">所属店铺</span>
</template>
<div class="search-bar">
<el-input
v-model="shopSearch.key"
placeholder="店铺ID/用户ID/名称"
clearable
@clear="shopSearch.key=''"
@keyup.enter="searchShops"
style="width:220px"
/>
<el-select v-model="shopSearch.status" placeholder="状态" clearable style="width:120px">
<el-option label="正常" :value="1" />
<el-option label="封禁" :value="0" />
</el-select>
<el-button type="primary" @click="searchShops">查询</el-button>
</div>
<div class="shop-list">
<div
v-for="shop in shopList"
:key="shop.id"
class="shop-item"
:class="{ active: selectedShopId === shop.id }"
@click="selectShop(shop.id)"
>
<div>{{ shop.dianpu_mingcheng }}</div>
<div class="sub">ID:{{ shop.id }} 用户:{{ shop.yonghu_id }}</div>
<el-tag :type="shop.zhuangtai === 1 ? 'success' : 'danger'" size="small">
{{ shop.zhuangtai === 1 ? '正常' : '封禁' }}
</el-tag>
</div>
</div>
<el-pagination
v-model:current-page="shopPage.page"
:page-size="shopPage.size"
:total="shopPage.total"
layout="prev, pager, next"
@current-change="loadShopList"
small
style="margin-top:12px"
/>
</el-card>
<!-- 第三区域店铺商品类型映射分页搜索筛选待审数标记 -->
<el-card shadow="never" class="filter-card">
<template #header>
<span class="card-title">店铺商品类型</span>
<el-badge :value="pendingTotal" class="pending-badge" type="danger" />
</template>
<div class="search-bar">
<el-input
v-model="mappingSearch.keyword"
placeholder="类型介绍/ID/店铺"
clearable
style="width:200px"
@keyup.enter="searchMappings"
/>
<el-select v-model="mappingSearch.shangjia" placeholder="上架状态" clearable style="width:100px">
<el-option label="已上架" :value="true" />
<el-option label="未上架" :value="false" />
</el-select>
<el-select v-model="mappingSearch.fengjin" placeholder="封禁状态" clearable style="width:100px">
<el-option label="未封禁" :value="false" />
<el-option label="已封禁" :value="true" />
</el-select>
<el-select v-model="mappingSearch.shenhe" placeholder="审核状态" clearable style="width:100px">
<el-option label="已通过" :value="true" />
<el-option label="未审核" :value="false" />
</el-select>
<el-button type="primary" @click="searchMappings">查询</el-button>
</div>
<el-table :data="mappingList" v-loading="mappingLoading" stripe border>
<el-table-column prop="id" label="ID" width="70" />
<el-table-column label="类型图片" width="80">
<template #default="{ row }">
<el-image :src="getFullUrl(row.tupian_url)" fit="contain" style="width:40px;height:40px" />
</template>
</el-table-column>
<el-table-column prop="jieshao" label="类型介绍" width="120" />
<el-table-column prop="dianpu_name" label="所属店铺" width="100" />
<el-table-column prop="gonggong_leixing_name" label="映射公共类型" width="110" />
<el-table-column label="上架" width="70">
<template #default="{ row }">
<el-tag :type="row.shangjia_zhuangtai ? 'success' : 'info'" size="small">
{{ row.shangjia_zhuangtai ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="封禁" width="70">
<template #default="{ row }">
<el-tag :type="row.fengjin_zhuangtai ? 'danger' : 'success'" size="small">
{{ row.fengjin_zhuangtai ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="审核状态" width="80">
<template #default="{ row }">
<span :style="{ color: row.shenhe_zhuangtai ? '#67c23a' : '#f56c6c' }">
{{ row.shenhe_zhuangtai ? '已通过' : '待审核' }}
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<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="mappingPage.page"
:page-size="mappingPage.size"
:total="mappingPage.total"
layout="total, prev, pager, next"
@current-change="loadMappings"
style="margin-top:12px"
/>
</el-card>
<!-- 第四区域商品列表子组件 -->
<ShopProductList
:shop-id="selectedShopId"
:shop-product-type-id="null"
:public-type-id="selectedPublicType"
:public-types="publicTypes"
@detail="openProductDetail"
/>
<!-- 详情弹窗修改映射 -->
<el-dialog v-model="detailDialog" title="店铺商品类型详情" width="600px" destroy-on-close>
<el-form v-if="editingMapping" label-width="120px">
<el-form-item label="类型介绍">{{ editingMapping.jieshao }}</el-form-item>
<el-form-item label="上架状态">
<el-switch
v-model="editingMapping.shangjia_zhuangtai"
:active-value="true"
:inactive-value="false"
/>
</el-form-item>
<el-form-item label="封禁状态">
<el-switch
v-model="editingMapping.fengjin_zhuangtai"
:active-value="true"
:inactive-value="false"
/>
</el-form-item>
<el-form-item label="审核状态">
<el-switch
v-model="editingMapping.shenhe_zhuangtai"
:active-value="true"
:inactive-value="false"
active-text="通过"
inactive-text="待审核"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="detailDialog = false">取消</el-button>
<el-button type="primary" @click="confirmSaveMapping">保存修改</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, watch, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '@/utils/request'
import ShopProductList from './components/ShopProductList.vue'
// ========== 审核模式 ==========
const auditMode = ref(false)
// 开关前置确认
const beforeSwitchAudit = () => {
return new Promise((resolve) => {
ElMessageBox.confirm(
`确定要${auditMode.value ? '关闭' : '开启'}商品审核模式吗?`,
'提示',
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
)
.then(() => {
// 用户确认,先保持开关状态(由 v-model 控制),然后发送请求
// 这里不能直接用 resolve(true),因为需要等待异步请求完成才能决定是否允许切换
// 使用 before-change 的标准流程:先发请求,成功则 resolve(true),失败 reject
handleSwitchAudit()
.then(() => resolve(true))
.catch(() => resolve(false)) // 失败时阻止切换
})
.catch(() => {
// 取消操作,阻止切换
resolve(false)
})
})
}
const handleSwitchAudit = async () => {
const targetValue = !auditMode.value
try {
const res = await request.post('/houtai/htxgdpsplx', {
action: 'update_audit_mode',
username: localStorage.getItem('username') || '',
kaiqi_shenhe: targetValue
})
if (res.code === 0) {
ElMessage.success('审核模式已更新')
// 请求成功后 v-model 会自动切换到 targetValue因为 before-change 返回 true 时执行切换
} else {
ElMessage.error(res.msg || '更新失败')
throw new Error('更新失败')
}
} catch (e) {
ElMessage.error('请求失败,未修改')
throw e
}
}
// ========== 公共商品类型 ==========
const publicTypes = ref([])
const selectedPublicType = ref(null)
const getFullUrl = (url) => {
if (!url) return ''
if (url.startsWith('http')) return url
const base = window.$ossURL || ''
return base.replace(/\/$/, '') + '/' + url.replace(/^\//, '')
}
const selectPublicType = (id) => {
selectedPublicType.value = selectedPublicType.value === id ? null : id
}
const loadPublicTypes = async () => {
try {
const res = await request.post('/houtai/htdphqsplx', {
username: localStorage.getItem('username') || ''
})
if (res.code === 0) {
// 数据结构:{ code:0, data:{ public_types:[], kaiqi_shenhe: bool } }
publicTypes.value = res.data.public_types || []
auditMode.value = res.data.kaiqi_shenhe || false
} else if (res.code === 403) {
publicTypes.value = []
ElMessage.error('无访问权限,页面已锁定')
} else {
ElMessage.error(res.msg || '商品类型加载失败')
}
} catch (e) {
ElMessage.error('商品类型加载失败')
}
}
// ========== 店铺列表 ==========
const shopList = ref([])
const selectedShopId = ref(null)
const shopPage = reactive({ page: 1, size: 10, total: 0 })
const shopSearch = reactive({ key: '', status: null })
const selectShop = (id) => {
selectedShopId.value = id
}
const loadShopList = async (page = 1) => {
shopPage.page = page
try {
const res = await request.post('/houtai/htdphqdpys', {
username: localStorage.getItem('username') || '',
page: shopPage.page,
page_size: shopPage.size,
keyword: shopSearch.key,
zhuangtai: shopSearch.status
})
if (res.code === 0) {
shopList.value = res.data.list || []
shopPage.total = res.data.total || 0
} else if (res.code === 403) {
shopList.value = []
ElMessage.error('无权限访问店铺列表')
} else {
ElMessage.error(res.msg || '店铺列表加载失败')
}
} catch (e) {
ElMessage.error('店铺列表加载失败')
}
}
const searchShops = () => {
shopPage.page = 1
loadShopList(1)
}
// ========== 店铺商品类型映射 ==========
const mappingList = ref([])
const mappingLoading = ref(false)
const mappingPage = reactive({ page: 1, size: 10, total: 0 })
const pendingTotal = ref(0)
const mappingSearch = reactive({ keyword: '', shangjia: null, fengjin: null, shenhe: null })
const loadMappings = async (page = 1) => {
mappingLoading.value = true
try {
const res = await request.post('/houtai/htdphqyssplx', {
username: localStorage.getItem('username') || '',
page: page,
page_size: mappingPage.size,
keyword: mappingSearch.keyword,
shangjia_zhuangtai: mappingSearch.shangjia,
fengjin_zhuangtai: mappingSearch.fengjin,
shenhe_zhuangtai: mappingSearch.shenhe,
public_type_id: selectedPublicType.value,
shop_id: selectedShopId.value
})
if (res.code === 0) {
mappingList.value = res.data.list || []
mappingPage.total = res.data.total || 0
pendingTotal.value = res.data.pending_total || 0
} else if (res.code === 403) {
mappingList.value = []
ElMessage.error('无权限访问')
} else {
ElMessage.error(res.msg || '加载失败')
}
} catch (e) {
ElMessage.error('加载店铺商品类型失败')
} finally {
mappingLoading.value = false
}
}
const searchMappings = () => {
mappingPage.page = 1
loadMappings(1)
}
// 监听上层筛选条件变化,自动刷新第三区域
watch([selectedPublicType, selectedShopId], () => {
mappingPage.page = 1
loadMappings(1)
}, { deep: true })
// ========== 映射详情修改 ==========
const detailDialog = ref(false)
const editingMapping = ref(null)
const openDetailDialog = (row) => {
editingMapping.value = { ...row }
detailDialog.value = true
}
const confirmSaveMapping = () => {
ElMessageBox.confirm('确定要保存修改吗?', '确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
saveMappingDetail()
})
.catch(() => {})
}
const saveMappingDetail = async () => {
try {
const res = await request.post('/houtai/htxgdpsplx', {
action: 'update_mapping_status',
username: localStorage.getItem('username') || '',
mapping_id: editingMapping.value.id,
shangjia_zhuangtai: editingMapping.value.shangjia_zhuangtai,
fengjin_zhuangtai: editingMapping.value.fengjin_zhuangtai,
shenhe_zhuangtai: editingMapping.value.shenhe_zhuangtai
})
if (res.code === 0) {
ElMessage.success('修改成功')
detailDialog.value = false
loadMappings(mappingPage.page)
} else {
ElMessage.error(res.msg || '修改失败')
}
} catch (e) {
ElMessage.error('请求失败')
}
}
const openProductDetail = (row) => {
// 可跳转或弹窗
ElMessage.info(`查看商品详情:${row.id}`)
}
// ========== 初始化 ==========
onMounted(async () => {
await loadPublicTypes() // 同时获取审核模式状态
loadShopList()
loadMappings()
})
</script>
<style scoped>
.shop-product-manage {
padding: 20px;
}
.top-card, .filter-card {
margin-bottom: 20px;
}
.card-title {
font-weight: bold;
}
.switch-bar {
display: flex;
align-items: center;
gap: 12px;
}
.switch-desc {
color: #909399;
font-size: 13px;
}
.type-tabs {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.search-bar {
display: flex;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.shop-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.shop-item {
width: 200px;
padding: 12px;
border: 1px solid #dcdfe6;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.shop-item.active {
border-color: #409eff;
background: rgba(64,158,255,0.1);
}
.shop-item:hover {
border-color: #409eff;
}
.sub {
font-size: 12px;
color: #909399;
margin: 4px 0;
}
.pending-badge {
margin-left: 8px;
}
</style>

View File

@@ -0,0 +1,8 @@
<template>
</template>
<script>
</script>
<style>
</style>

View File

@@ -0,0 +1,8 @@
<template>
</template>
<script>
</script>
<style>
</style>

View File

@@ -0,0 +1,8 @@
<template>
</template>
<script>
</script>
<style>
</style>

View File

@@ -0,0 +1,395 @@
<template>
<div class="product-list-section">
<!-- 搜索 + 筛选栏 -->
<el-card shadow="never" class="filter-bar-card">
<div class="filter-row">
<el-input
v-model="search.keyword"
placeholder="商品ID / 标题"
clearable
style="width: 220px"
@keyup.enter="handleSearch"
/>
<el-select v-model="search.shenhe" placeholder="平台审核" clearable style="width: 120px">
<el-option label="已通过" :value="true" />
<el-option label="待审核" :value="false" />
</el-select>
<el-select v-model="search.shangjia" placeholder="上架状态" clearable style="width: 120px">
<el-option label="已上架" :value="true" />
<el-option label="已下架" :value="false" />
</el-select>
<el-select v-model="search.fengjin" placeholder="封禁状态" clearable style="width: 120px">
<el-option label="未封禁" :value="false" />
<el-option label="已封禁" :value="true" />
</el-select>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
</div>
</el-card>
<!-- 待审核数量提示 -->
<div class="pending-tip" v-if="pendingCount > 0">
<el-tag type="danger">待审核商品{{ pendingCount }} </el-tag>
</div>
<!-- 商品表格 -->
<el-card shadow="never">
<el-table :data="productList" v-loading="loading" stripe border style="width: 100%">
<el-table-column prop="id" label="商品ID" width="70" />
<el-table-column prop="biaoqian" label="商品标题" min-width="150" show-overflow-tooltip />
<el-table-column prop="jiage" label="价格" width="90" />
<el-table-column prop="kucun" label="库存" width="70" />
<el-table-column label="平台审核" width="90">
<template #default="{ row }">
<el-tag :type="row.shenhe_zhuangtai ? 'success' : 'danger'" size="small">
{{ row.shenhe_zhuangtai ? '通过' : '待审' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="上架" width="70">
<template #default="{ row }">
<span :style="{ color: row.shangjia_zhuangtai ? '#67c23a' : '#f56c6c' }">
{{ row.shangjia_zhuangtai ? '是' : '否' }}
</span>
</template>
</el-table-column>
<el-table-column label="封禁" width="70">
<template #default="{ row }">
<span :style="{ color: row.fengjin_zhuangtai ? '#f56c6c' : '#67c23a' }">
{{ row.fengjin_zhuangtai ? '是' : '否' }}
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="140" fixed="right">
<template #default="{ row }">
<el-button size="small" type="primary" @click="openDetail(row)">详情</el-button>
<el-button size="small" type="success" @click="openOperationData(row.id)">运营数据</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="page.page"
:page-size="page.size"
:total="page.total"
layout="total, prev, pager, next, jumper"
@current-change="fetchList"
style="margin-top: 16px; justify-content: flex-end;"
/>
</el-card>
<!-- 详情 / 编辑弹窗 -->
<el-dialog
v-model="detailVisible"
:title="isEditing ? '编辑商品' : '商品详情'"
width="750px"
destroy-on-close
top="5vh"
>
<template v-if="currentProduct">
<el-form :model="editForm" label-width="140px" :disabled="!isEditing" class="detail-form">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="商品ID">{{ currentProduct.id }}</el-form-item>
<el-form-item label="商品标题">
<el-input v-model="editForm.biaoqian" />
</el-form-item>
<el-form-item label="商品价格">
<el-input-number v-model="editForm.jiage" :min="0" :precision="2" style="width: 100%" />
</el-form-item>
<el-form-item label="库存">
<el-input-number v-model="editForm.kucun" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item label="所属公共类型">
<template v-if="isEditing">
<el-select v-model="editForm.leixing_id" placeholder="请选择" style="width: 100%">
<el-option
v-for="pt in publicTypes"
:key="pt.id"
:label="pt.jieshao"
:value="pt.id"
/>
</el-select>
</template>
<template v-else>
{{ getPublicTypeName(currentProduct.leixing_id) }}
</template>
</el-form-item>
<el-form-item label="店铺商品类型">
{{ getShopProductTypeName(currentProduct.dianpu_leixing_name) }}
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="真实销量">{{ currentProduct.zhenshi_xiaoliang }}</el-form-item>
<el-form-item label="对外销量">
<el-input-number v-model="editForm.duiwai_xiaoliang" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item label="抢单要求类型">
<el-select v-model="editForm.yaoqiuleixing" :disabled="!isEditing" style="width: 100%">
<el-option label="会员" :value="1" />
<el-option label="佣金" :value="2" />
</el-select>
</el-form-item>
<el-form-item v-if="editForm.yaoqiuleixing === 2" label="佣金">
<el-input-number v-model="editForm.yongjin" :min="0" :precision="2" style="width: 100%" />
</el-form-item>
<el-form-item label="会员ID">
<el-input :model-value="currentProduct.huiyuan_id" disabled />
</el-form-item>
<el-form-item label="开启额外分成">
<el-switch v-model="editForm.kaioi_ewai_dashou_fencheng" :disabled="!isEditing" />
</el-form-item>
<el-form-item v-if="editForm.kaioi_ewai_dashou_fencheng" label="分成金额">
<el-input-number v-model="editForm.ewai_dashou_fencheng" :min="0" :precision="2" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="商品介绍">
<el-input v-model="editForm.jieshao" type="textarea" :rows="3" />
</el-form-item>
<el-form-item label="下单须知">
<el-input v-model="editForm.xiadan_xuzhi" type="textarea" :rows="3" />
</el-form-item>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="上架状态">
<el-switch v-model="editForm.shangjia_zhuangtai" :disabled="!isEditing" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="封禁状态">
<el-switch v-model="editForm.fengjin_zhuangtai" :disabled="!isEditing" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="平台审核">
<el-switch
v-model="editForm.shenhe_zhuangtai"
:disabled="!isEditing"
active-text="通过"
inactive-text="待审"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="detail-actions">
<template v-if="!isEditing">
<el-button type="warning" @click="enterEdit">编辑</el-button>
</template>
<template v-else>
<el-button @click="cancelEdit">取消</el-button>
<el-button type="primary" @click="confirmSave">保存修改</el-button>
</template>
</div>
</template>
</el-dialog>
<!-- 运营数据弹窗嵌套子组件 -->
<el-dialog v-model="operationVisible" title="商品运营数据" width="80%" top="5vh" destroy-on-close>
<ProductOperationData v-if="operationProductId" :product-id="operationProductId" />
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, watch, onMounted, defineProps, defineEmits } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '@/utils/request'
import ProductOperationData from './ProductOperationData.vue'
const props = defineProps({
shopId: { type: Number, default: null },
shopProductTypeId: { type: Number, default: null },
publicTypeId: { type: Number, default: null },
// 父组件传入的公共商品类型列表(必须在 Products.vue 中传递)
publicTypes: { type: Array, default: () => [] }
})
const emit = defineEmits(['detail'])
// ========== 筛选条件 ==========
const search = reactive({
keyword: '',
shenhe: null,
shangjia: null,
fengjin: null
})
const resetSearch = () => {
search.keyword = ''
search.shenhe = null
search.shangjia = null
search.fengjin = null
handleSearch()
}
// ========== 分页与列表 ==========
const loading = ref(false)
const productList = ref([])
const page = reactive({ page: 1, size: 15, total: 0 })
const pendingCount = ref(0)
const fetchList = async (pageNum = 1) => {
loading.value = true
try {
const res = await request.post('/houtai/hqhtdpsplx', {
username: localStorage.getItem('username') || '',
page: pageNum,
page_size: page.size,
shop_id: props.shopId,
shop_product_type_id: props.shopProductTypeId,
public_type_id: props.publicTypeId,
keyword: search.keyword,
shenhe_zhuangtai: search.shenhe,
shangjia_zhuangtai: search.shangjia,
fengjin_zhuangtai: search.fengjin
})
if (res.code === 0) {
productList.value = res.data.list || []
page.total = res.data.total || 0
pendingCount.value = res.data.pending_total || 0
} else {
ElMessage.error(res.msg || '获取商品列表失败')
}
} catch (e) {
ElMessage.error('网络异常')
} finally {
loading.value = false
}
}
const handleSearch = () => {
page.page = 1
fetchList(1)
}
// 监听父组件传入的筛选条件变化,自动重新查询(保持其他筛选不变)
watch(() => [props.shopId, props.shopProductTypeId, props.publicTypeId], () => {
page.page = 1
fetchList(1)
}, { deep: true })
// ========== 详情与编辑 ==========
const detailVisible = ref(false)
const isEditing = ref(false)
const currentProduct = ref(null)
const editForm = ref({})
const getPublicTypeName = (id) => {
if (!id) return '未分配'
const item = props.publicTypes.find(t => t.id === id)
return item ? item.jieshao : '未知类型'
}
const getShopProductTypeName = (name) => {
// 后端应返回 dianpu_leixing_name若没有则显示ID或占位
return name || '无'
}
const openDetail = (row) => {
currentProduct.value = row
// 深拷贝一份供编辑使用
editForm.value = { ...row }
isEditing.value = false
detailVisible.value = true
}
const enterEdit = () => {
isEditing.value = true
}
const cancelEdit = () => {
// 恢复到原始数据(重新从 currentProduct 拷贝)
editForm.value = { ...currentProduct.value }
isEditing.value = false
}
const confirmSave = () => {
ElMessageBox.confirm('确定要保存商品修改吗?', '确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
doSave()
}).catch(() => {})
}
const doSave = async () => {
try {
// 只传递实际被修改的字段,减少数据传输量
const payload = {
action: 'update_product',
username: localStorage.getItem('username') || '',
product_id: currentProduct.value.id,
biaoqian: editForm.value.biaoqian,
jiage: editForm.value.jiage,
kucun: editForm.value.kucun,
leixing_id: editForm.value.leixing_id,
duiwai_xiaoliang: editForm.value.duiwai_xiaoliang,
jieshao: editForm.value.jieshao,
xiadan_xuzhi: editForm.value.xiadan_xuzhi,
yaoqiuleixing: editForm.value.yaoqiuleixing,
yongjin: editForm.value.yaoqiuleixing === 2 ? editForm.value.yongjin : null,
kaioi_ewai_dashou_fencheng: editForm.value.kaioi_ewai_dashou_fencheng,
ewai_dashou_fencheng: editForm.value.kaioi_ewai_dashou_fencheng ? editForm.value.ewai_dashou_fencheng : null,
shangjia_zhuangtai: editForm.value.shangjia_zhuangtai,
fengjin_zhuangtai: editForm.value.fengjin_zhuangtai,
shenhe_zhuangtai: editForm.value.shenhe_zhuangtai
}
const res = await request.post('/houtai/htxgdpspsj', payload)
if (res.code === 0) {
ElMessage.success('商品修改成功')
detailVisible.value = false
fetchList(page.page) // 刷新当前页
} else {
ElMessage.error(res.msg || '修改失败')
}
} catch (e) {
ElMessage.error('请求异常')
}
}
// ========== 运营数据 ==========
const operationVisible = ref(false)
const operationProductId = ref(null)
const openOperationData = (productId) => {
operationProductId.value = productId
operationVisible.value = true
}
// ========== 初始化 ==========
onMounted(() => {
fetchList()
})
</script>
<style scoped>
.product-list-section {
margin-top: 20px;
}
.filter-bar-card {
margin-bottom: 12px;
}
.filter-row {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
}
.pending-tip {
margin-bottom: 12px;
}
.detail-form .el-form-item {
margin-bottom: 16px;
}
.detail-actions {
text-align: right;
margin-top: 20px;
}
</style>