fetch(modify):修改bug
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m45s

This commit is contained in:
sexygoat
2026-02-05 17:22:41 +08:00
parent d97dc5f007
commit b94c043a56
24 changed files with 2734 additions and 446 deletions

View File

@@ -193,3 +193,16 @@ body {
align-items: center; align-items: center;
gap: 20px; gap: 20px;
} }
// 表单分段标题样式 - 用于对话框中的表单分段
.form-section-title {
margin: 24px 0 16px 0;
padding-left: 12px;
border-left: 3px solid var(--el-color-primary);
.title-text {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}

View File

@@ -0,0 +1,159 @@
<template>
<div class="detail-container">
<ElCard v-for="(section, index) in sections" :key="index" class="detail-section">
<template #header>
<div class="section-title">{{ section.title }}</div>
</template>
<div :class="['section-fields', section.columns === 1 ? 'single-column' : 'double-column']">
<div
v-for="(field, fieldIndex) in section.fields"
:key="fieldIndex"
class="field-item"
:class="{ 'full-width': field.fullWidth }"
>
<div class="field-label">{{ field.label }}:</div>
<div class="field-value">
<!-- 自定义渲染 -->
<template v-if="field.render">
<component :is="field.render(data)" />
</template>
<!-- 默认渲染 -->
<template v-else>
{{ formatFieldValue(field, data) }}
</template>
</div>
</div>
</div>
</ElCard>
</div>
</template>
<script setup lang="ts">
import { ElCard } from 'element-plus'
export interface DetailField {
label: string // 字段标签
prop?: string // 数据属性路径,支持点号分隔的嵌套路径,如 'one_time_commission_config.enable'
formatter?: (value: any, data: any) => string // 自定义格式化函数
render?: (data: any) => any // 自定义渲染函数,返回 VNode
fullWidth?: boolean // 是否占据整行
}
export interface DetailSection {
title: string // 分组标题
fields: DetailField[] // 字段列表
columns?: 1 | 2 // 列数默认2列
}
interface Props {
sections: DetailSection[] // 详情页分组配置
data: any // 详情数据
}
const props = defineProps<Props>()
/**
* 根据点号分隔的路径获取嵌套对象的值
*/
const getNestedValue = (obj: any, path: string): any => {
if (!path) return obj
return path.split('.').reduce((acc, part) => acc?.[part], obj)
}
/**
* 格式化字段值
*/
const formatFieldValue = (field: DetailField, data: any): string => {
const value = field.prop ? getNestedValue(data, field.prop) : data
// 如果有自定义格式化函数
if (field.formatter) {
return field.formatter(value, data)
}
// 默认处理
if (value === null || value === undefined || value === '') {
return '-'
}
return String(value)
}
</script>
<style scoped lang="scss">
.detail-container {
max-height: 70vh;
overflow-y: auto;
padding: 4px;
}
.detail-section {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.section-fields {
display: grid;
gap: 16px 24px;
&.single-column {
grid-template-columns: 1fr;
}
&.double-column {
grid-template-columns: repeat(2, 1fr);
}
}
.field-item {
display: flex;
align-items: flex-start;
min-height: 32px;
&.full-width {
grid-column: 1 / -1;
}
.field-label {
flex-shrink: 0;
width: 140px;
font-weight: 500;
color: var(--el-text-color-regular);
line-height: 32px;
}
.field-value {
flex: 1;
color: var(--el-text-color-primary);
line-height: 32px;
word-break: break-word;
}
}
@media (max-width: 768px) {
.section-fields {
&.double-column {
grid-template-columns: 1fr;
}
}
.field-item {
flex-direction: column;
.field-label {
width: 100%;
margin-bottom: 4px;
}
}
}
</style>

View File

@@ -13,7 +13,7 @@
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
text?: string text?: string
type?: 'add' | 'edit' | 'delete' | 'more' type?: 'add' | 'edit' | 'delete' | 'more' | 'view'
icon?: string // 自定义图标 icon?: string // 自定义图标
iconClass?: BgColorEnum // 自定义按钮背景色、文字颜色 iconClass?: BgColorEnum // 自定义按钮背景色、文字颜色
iconColor?: string // 外部传入的图标文字颜色 iconColor?: string // 外部传入的图标文字颜色
@@ -31,7 +31,8 @@
{ type: 'add', icon: '&#xe602;', color: BgColorEnum.PRIMARY }, { type: 'add', icon: '&#xe602;', color: BgColorEnum.PRIMARY },
{ type: 'edit', icon: '&#xe642;', color: BgColorEnum.SECONDARY }, { type: 'edit', icon: '&#xe642;', color: BgColorEnum.SECONDARY },
{ type: 'delete', icon: '&#xe783;', color: BgColorEnum.ERROR }, { type: 'delete', icon: '&#xe783;', color: BgColorEnum.ERROR },
{ type: 'more', icon: '&#xe6df;', color: '' } { type: 'more', icon: '&#xe6df;', color: '' },
{ type: 'view', icon: '&#xe6df;', color: BgColorEnum.SECONDARY }
] as const ] as const
// 计算最终使用的图标:优先使用外部传入的 icon否则根据 type 获取默认图标 // 计算最终使用的图标:优先使用外部传入的 icon否则根据 type 获取默认图标

View File

@@ -390,10 +390,14 @@
"packageCreate": "Create Package", "packageCreate": "Create Package",
"packageBatch": "Batch Management", "packageBatch": "Batch Management",
"packageList": "My Packages", "packageList": "My Packages",
"packageDetail": "Package Detail",
"packageChange": "Package Change", "packageChange": "Package Change",
"packageAssign": "Package Assignment", "packageAssign": "Package Assignment",
"packageAssignDetail": "Package Assignment Detail",
"seriesAssign": "Series Assignment", "seriesAssign": "Series Assignment",
"seriesAssignDetail": "Series Assignment Detail",
"packageSeries": "Package Series", "packageSeries": "Package Series",
"packageSeriesDetail": "Package Series Detail",
"packageCommission": "Package Commission Cards" "packageCommission": "Package Commission Cards"
}, },
"accountManagement": { "accountManagement": {
@@ -406,6 +410,7 @@
"agent": "Agent Management", "agent": "Agent Management",
"customerAccount": "Customer Account", "customerAccount": "Customer Account",
"enterpriseCustomer": "Enterprise Customer", "enterpriseCustomer": "Enterprise Customer",
"enterpriseCustomerAccounts": "Enterprise Customer Accounts",
"enterpriseCards": "Enterprise Card Management", "enterpriseCards": "Enterprise Card Management",
"customerCommission": "Customer Commission" "customerCommission": "Customer Commission"
}, },
@@ -429,7 +434,8 @@
"packageSeries": "Package Series Management", "packageSeries": "Package Series Management",
"packageList": "Package Management", "packageList": "Package Management",
"packageAssign": "Package Assignment", "packageAssign": "Package Assignment",
"shop": "Shop Management" "shop": "Shop Management",
"shopAccounts": "Shop Accounts"
}, },
"assetManagement": { "assetManagement": {
"title": "Asset Management", "title": "Asset Management",

View File

@@ -402,10 +402,14 @@
"packageCreate": "新建套餐", "packageCreate": "新建套餐",
"packageBatch": "批量管理", "packageBatch": "批量管理",
"packageList": "套餐管理", "packageList": "套餐管理",
"packageDetail": "套餐详情",
"packageChange": "套餐变更", "packageChange": "套餐变更",
"packageAssign": "单套餐分配", "packageAssign": "单套餐分配",
"packageAssignDetail": "套餐分配详情",
"seriesAssign": "套餐系列分配", "seriesAssign": "套餐系列分配",
"seriesAssignDetail": "系列分配详情",
"packageSeries": "套餐系列", "packageSeries": "套餐系列",
"packageSeriesDetail": "套餐系列详情",
"packageCommission": "套餐佣金网卡" "packageCommission": "套餐佣金网卡"
}, },
"accountManagement": { "accountManagement": {
@@ -418,6 +422,7 @@
"agent": "代理商管理", "agent": "代理商管理",
"customerAccount": "客户账号", "customerAccount": "客户账号",
"enterpriseCustomer": "企业客户", "enterpriseCustomer": "企业客户",
"enterpriseCustomerAccounts": "企业客户账号列表",
"enterpriseCards": "企业卡管理", "enterpriseCards": "企业卡管理",
"customerCommission": "客户账号佣金" "customerCommission": "客户账号佣金"
}, },
@@ -432,7 +437,8 @@
"packageSeries": "套餐系列管理", "packageSeries": "套餐系列管理",
"packageList": "套餐管理", "packageList": "套餐管理",
"packageAssign": "套餐分配", "packageAssign": "套餐分配",
"shop": "店铺管理" "shop": "店铺管理",
"shopAccounts": "店铺账号列表"
}, },
"assetManagement": { "assetManagement": {
"title": "资产管理", "title": "资产管理",

View File

@@ -721,6 +721,16 @@ export const asyncRoutes: AppRouteRecord[] = [
keepAlive: true keepAlive: true
} }
}, },
{
path: 'package-series/detail/:id',
name: 'PackageSeriesDetail',
component: RoutesAlias.PackageSeriesDetail,
meta: {
title: 'menus.packageManagement.packageSeriesDetail',
isHide: true,
keepAlive: false
}
},
{ {
path: 'package-list', path: 'package-list',
name: 'PackageList', name: 'PackageList',
@@ -730,6 +740,16 @@ export const asyncRoutes: AppRouteRecord[] = [
keepAlive: true keepAlive: true
} }
}, },
{
path: 'package-list/detail/:id',
name: 'PackageDetail',
component: RoutesAlias.PackageDetail,
meta: {
title: 'menus.packageManagement.packageDetail',
isHide: true,
keepAlive: false
}
},
{ {
path: 'package-assign', path: 'package-assign',
name: 'PackageAssign', name: 'PackageAssign',
@@ -739,6 +759,16 @@ export const asyncRoutes: AppRouteRecord[] = [
keepAlive: true keepAlive: true
} }
}, },
{
path: 'package-assign/detail/:id',
name: 'PackageAssignDetail',
component: RoutesAlias.PackageAssignDetail,
meta: {
title: 'menus.packageManagement.packageAssignDetail',
isHide: true,
keepAlive: false
}
},
{ {
path: 'series-assign', path: 'series-assign',
name: 'SeriesAssign', name: 'SeriesAssign',
@@ -747,6 +777,16 @@ export const asyncRoutes: AppRouteRecord[] = [
title: 'menus.packageManagement.seriesAssign', title: 'menus.packageManagement.seriesAssign',
keepAlive: true keepAlive: true
} }
},
{
path: 'series-assign/detail/:id',
name: 'SeriesAssignDetail',
component: RoutesAlias.SeriesAssignDetail,
meta: {
title: 'menus.packageManagement.seriesAssignDetail',
isHide: true,
keepAlive: false
}
} }
] ]
}, },
@@ -824,6 +864,25 @@ export const asyncRoutes: AppRouteRecord[] = [
// keepAlive: true // keepAlive: true
// } // }
// }, // },
{
path: 'enterprise-customer',
name: 'EnterpriseCustomer',
component: RoutesAlias.EnterpriseCustomer,
meta: {
title: 'menus.accountManagement.enterpriseCustomer',
keepAlive: true
}
},
{
path: 'enterprise-customer/customer-accounts/:id',
name: 'EnterpriseCustomerAccounts',
component: RoutesAlias.EnterpriseCustomerAccounts,
meta: {
title: 'menus.accountManagement.enterpriseCustomerAccounts',
isHide: true,
keepAlive: false
}
},
{ {
path: 'enterprise-cards', path: 'enterprise-cards',
name: 'EnterpriseCards', name: 'EnterpriseCards',
@@ -1013,15 +1072,6 @@ export const asyncRoutes: AppRouteRecord[] = [
// keepAlive: true // keepAlive: true
// } // }
// }, // },
{
path: 'enterprise-customer',
name: 'EnterpriseCustomer',
component: RoutesAlias.EnterpriseCustomer,
meta: {
title: 'menus.accountManagement.enterpriseCustomer',
keepAlive: true
}
},
{ {
path: 'carrier-management', path: 'carrier-management',
name: 'CarrierManagement', name: 'CarrierManagement',

View File

@@ -68,10 +68,14 @@ export enum RoutesAlias {
PackageCreate = '/package-management/package-create', // 新建套餐 PackageCreate = '/package-management/package-create', // 新建套餐
PackageBatch = '/package-management/package-batch', // 批量管理 PackageBatch = '/package-management/package-batch', // 批量管理
PackageList = '/package-management/package-list', // 套餐管理 PackageList = '/package-management/package-list', // 套餐管理
PackageDetail = '/package-management/package-list/detail', // 套餐详情
PackageChange = '/package-management/package-change', // 套餐变更 PackageChange = '/package-management/package-change', // 套餐变更
PackageAssign = '/package-management/package-assign', // 单套餐分配 PackageAssign = '/package-management/package-assign', // 单套餐分配
PackageAssignDetail = '/package-management/package-assign/detail', // 单套餐分配详情
SeriesAssign = '/package-management/series-assign', // 套餐系列分配 SeriesAssign = '/package-management/series-assign', // 套餐系列分配
SeriesAssignDetail = '/package-management/series-assign/detail', // 套餐系列分配详情
PackageSeries = '/package-management/package-series', // 套餐系列 PackageSeries = '/package-management/package-series', // 套餐系列
PackageSeriesDetail = '/package-management/package-series/detail', // 套餐系列详情
PackageCommission = '/package-management/package-commission', // 套餐佣金网卡 PackageCommission = '/package-management/package-commission', // 套餐佣金网卡
// 账号管理 // 账号管理
@@ -82,12 +86,14 @@ export enum RoutesAlias {
AgentManagement = '/account-management/agent', // 代理商管理 AgentManagement = '/account-management/agent', // 代理商管理
ShopAccount = '/account-management/shop-account', // 代理账号管理 ShopAccount = '/account-management/shop-account', // 代理账号管理
EnterpriseCustomer = '/account-management/enterprise-customer', // 企业客户管理 EnterpriseCustomer = '/account-management/enterprise-customer', // 企业客户管理
EnterpriseCustomerAccounts = '/common/account-list', // 企业客户账号列表(通用)
EnterpriseCards = '/account-management/enterprise-cards', // 企业卡管理 EnterpriseCards = '/account-management/enterprise-cards', // 企业卡管理
CustomerCommission = '/account-management/customer-commission', // 客户账号佣金 CustomerCommission = '/account-management/customer-commission', // 客户账号佣金
// 产品管理 // 产品管理
SimCardManagement = '/product/sim-card', // 网卡产品管理 SimCardManagement = '/product/sim-card', // 网卡产品管理
SimCardAssign = '/product/sim-card-assign', // 号卡分配 SimCardAssign = '/product/sim-card-assign', // 号卡分配
ShopAccounts = '/common/account-list', // 店铺账号列表(通用)
// 资产管理 // 资产管理
StandaloneCardList = '/asset-management/iot-card-management', // IoT卡管理 StandaloneCardList = '/asset-management/iot-card-management', // IoT卡管理

View File

@@ -21,17 +21,17 @@ export interface OneTimeCommissionTier {
* 套餐系列一次性佣金配置 * 套餐系列一次性佣金配置
*/ */
export interface SeriesOneTimeCommissionConfig { export interface SeriesOneTimeCommissionConfig {
enable: boolean // 是否启用一次性佣金 enable?: boolean // 是否启用一次性佣金
commission_type: 'fixed' | 'tiered' // 佣金类型:固定或梯度 commission_type?: 'fixed' | 'tiered' // 佣金类型:固定或梯度
commission_amount?: number // 固定佣金金额commission_type=fixed时使用 commission_amount?: number // 固定佣金金额commission_type=fixed时使用
threshold: number // 触发阈值(分) threshold?: number // 触发阈值(分)
trigger_type: 'first_recharge' | 'accumulated_recharge' // 触发类型:首次充值或累计充值 trigger_type?: 'first_recharge' | 'accumulated_recharge' // 触发类型:首或累计充值
tiers?: OneTimeCommissionTier[] | null // 梯度配置列表commission_type=tiered时使用 tiers?: OneTimeCommissionTier[] | null // 梯度配置列表commission_type=tiered时使用
enable_force_recharge: boolean // 是否启用强充 enable_force_recharge?: boolean // 是否启用强充
force_amount?: number // 强充金额(分) force_amount?: number // 强充金额(分)
force_calc_type?: 'fixed' | 'dynamic' // 强充计算类型:固定或动态 force_calc_type?: 'fixed' | 'dynamic' // 强充计算类型:固定或动态
validity_type: 'permanent' | 'fixed_date' | 'relative' // 时效类型:永久、固定日期或相对时长 validity_type?: 'permanent' | 'fixed_date' | 'relative' // 时效类型:永久、固定日期或相对时长
validity_value?: string // 时效值(日期字符串或月数) validity_value?: string // 时效值(日期或月数)
} }
/** /**

View File

@@ -79,6 +79,7 @@ declare module 'vue' {
CommissionDisplay: typeof import('./../components/business/CommissionDisplay.vue')['default'] CommissionDisplay: typeof import('./../components/business/CommissionDisplay.vue')['default']
ContainerSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ContainerSettings.vue')['default'] ContainerSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ContainerSettings.vue')['default']
CustomerAccountDialog: typeof import('./../components/business/CustomerAccountDialog.vue')['default'] CustomerAccountDialog: typeof import('./../components/business/CustomerAccountDialog.vue')['default']
DetailPage: typeof import('./../components/common/DetailPage.vue')['default']
ElAlert: typeof import('element-plus/es')['ElAlert'] ElAlert: typeof import('element-plus/es')['ElAlert']
ElAvatar: typeof import('element-plus/es')['ElAvatar'] ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']

View File

@@ -24,7 +24,7 @@
<!-- 表格 --> <!-- 表格 -->
<ArtTable <ArtTable
ref="tableRef" ref="tableRef"
row-key="ID" row-key="id"
:loading="loading" :loading="loading"
:data="tableData" :data="tableData"
:currentPage="pagination.currentPage" :currentPage="pagination.currentPage"
@@ -43,9 +43,9 @@
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '添加账号' : '编辑账号'" :title="dialogType === 'add' ? '添加账号' : '编辑账号'"
width="500px" width="30%"
> >
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="100px"> <ElForm ref="formRef" :model="formData" :rules="rules" label-width="80px">
<ElFormItem label="账号名称" prop="username"> <ElFormItem label="账号名称" prop="username">
<ElInput v-model="formData.username" placeholder="请输入账号名称" /> <ElInput v-model="formData.username" placeholder="请输入账号名称" />
</ElFormItem> </ElFormItem>
@@ -84,8 +84,8 @@
<!-- 分配角色对话框 --> <!-- 分配角色对话框 -->
<ElDialog v-model="roleDialogVisible" title="分配角色" width="500px"> <ElDialog v-model="roleDialogVisible" title="分配角色" width="500px">
<ElCheckboxGroup v-model="selectedRoles"> <ElCheckboxGroup v-model="selectedRoles">
<div v-for="role in allRoles" :key="role.ID" style="margin-bottom: 12px"> <div v-for="role in allRoles" :key="role.id" style="margin-bottom: 12px">
<ElCheckbox :label="role.ID"> <ElCheckbox :value="role.id">
{{ role.role_name }} {{ role.role_name }}
<ElTag <ElTag
:type="role.role_type === 1 ? 'primary' : 'success'" :type="role.role_type === 1 ? 'primary' : 'success'"
@@ -113,6 +113,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { h } from 'vue' import { h } from 'vue'
import { useRoute } from 'vue-router'
import { FormInstance, ElSwitch, ElCheckbox, ElCheckboxGroup, ElTag } from 'element-plus' import { FormInstance, ElSwitch, ElCheckbox, ElCheckboxGroup, ElTag } from 'element-plus'
import { ElMessageBox, ElMessage } from 'element-plus' import { ElMessageBox, ElMessage } from 'element-plus'
import type { FormRules } from 'element-plus' import type { FormRules } from 'element-plus'
@@ -130,6 +131,7 @@
defineOptions({ name: 'Account' }) // 定义组件名称,用于 KeepAlive 缓存控制 defineOptions({ name: 'Account' }) // 定义组件名称,用于 KeepAlive 缓存控制
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
const route = useRoute()
const dialogType = ref('add') const dialogType = ref('add')
const dialogVisible = ref(false) const dialogVisible = ref(false)
@@ -272,7 +274,7 @@
} }
if (type === 'edit' && row) { if (type === 'edit' && row) {
formData.id = row.ID formData.id = row.id
formData.username = row.username formData.username = row.username
formData.phone = row.phone formData.phone = row.phone
formData.user_type = row.user_type formData.user_type = row.user_type
@@ -295,7 +297,7 @@
}) })
.then(async () => { .then(async () => {
try { try {
await AccountService.deleteAccount(row.ID) await AccountService.deleteAccount(row.id)
ElMessage.success('删除成功') ElMessage.success('删除成功')
getAccountList() getAccountList()
} catch (error) { } catch (error) {
@@ -425,6 +427,12 @@
}) })
onMounted(() => { onMounted(() => {
// 从 URL 查询参数中读取 shop_id
const shopIdParam = route.query.shop_id
if (shopIdParam) {
formFilters.shop_id = Number(shopIdParam)
}
getAccountList() getAccountList()
loadAllRoles() loadAllRoles()
loadShopList() loadShopList()
@@ -444,7 +452,7 @@
// 显示分配角色对话框 // 显示分配角色对话框
const showRoleDialog = async (row: any) => { const showRoleDialog = async (row: any) => {
currentAccountId.value = row.ID currentAccountId.value = row.id
selectedRoles.value = [] selectedRoles.value = []
try { try {
@@ -452,11 +460,11 @@
await loadAllRoles() await loadAllRoles()
// 先加载当前账号的角色,再打开对话框 // 先加载当前账号的角色,再打开对话框
const res = await AccountService.getAccountRoles(row.ID) const res = await AccountService.getAccountRoles(row.id)
if (res.code === 0) { if (res.code === 0) {
// 提取角色ID数组 // 提取角色ID数组
const roles = res.data || [] const roles = res.data || []
selectedRoles.value = roles.map((role: any) => role.ID) selectedRoles.value = roles.map((role: any) => role.id)
// 数据加载完成后再打开对话框 // 数据加载完成后再打开对话框
roleDialogVisible.value = true roleDialogVisible.value = true
} }
@@ -582,7 +590,7 @@
// 先更新UI // 先更新UI
row.status = newStatus row.status = newStatus
try { try {
await AccountService.updateAccountStatus(row.ID, newStatus as 0 | 1) await AccountService.updateAccountStatus(row.id, newStatus as 0 | 1)
ElMessage.success('状态切换成功') ElMessage.success('状态切换成功')
} catch (error) { } catch (error) {
// 切换失败,恢复原状态 // 切换失败,恢复原状态

View File

@@ -168,12 +168,6 @@
</template> </template>
</ElDialog> </ElDialog>
<!-- 客户账号列表弹窗 -->
<CustomerAccountDialog
v-model="customerAccountDialogVisible"
:enterprise-id="currentEnterpriseId"
/>
<!-- 修改密码对话框 --> <!-- 修改密码对话框 -->
<ElDialog v-model="passwordDialogVisible" title="修改密码" width="400px"> <ElDialog v-model="passwordDialogVisible" title="修改密码" width="400px">
<ElForm ref="passwordFormRef" :model="passwordForm" :rules="passwordRules"> <ElForm ref="passwordFormRef" :model="passwordForm" :rules="passwordRules">
@@ -199,6 +193,14 @@
</div> </div>
</template> </template>
</ElDialog> </ElDialog>
<!-- 企业客户操作右键菜单 -->
<ArtMenuRight
ref="enterpriseOperationMenuRef"
:menu-items="enterpriseOperationMenuItems"
:menu-width="140"
@select="handleEnterpriseOperationMenuSelect"
/>
</ElCard> </ElCard>
</div> </div>
</ArtTableFullScreen> </ArtTableFullScreen>
@@ -215,9 +217,11 @@
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import CustomerAccountDialog from '@/components/business/CustomerAccountDialog.vue' import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { BgColorEnum } from '@/enums/appEnum' import { BgColorEnum } from '@/enums/appEnum'
import { RoutesAlias } from '@/router/routesAlias'
defineOptions({ name: 'EnterpriseCustomer' }) defineOptions({ name: 'EnterpriseCustomer' })
@@ -227,7 +231,6 @@
const dialogVisible = ref(false) const dialogVisible = ref(false)
const passwordDialogVisible = ref(false) const passwordDialogVisible = ref(false)
const customerAccountDialogVisible = ref(false)
const loading = ref(false) const loading = ref(false)
const submitLoading = ref(false) const submitLoading = ref(false)
const passwordSubmitLoading = ref(false) const passwordSubmitLoading = ref(false)
@@ -236,6 +239,10 @@
const currentEnterpriseId = ref<number>(0) const currentEnterpriseId = ref<number>(0)
const shopList = ref<ShopResponse[]>([]) const shopList = ref<ShopResponse[]>([])
// 右键菜单
const enterpriseOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentOperatingEnterprise = ref<EnterpriseItem | null>(null)
// 搜索表单初始值 // 搜索表单初始值
const initialSearchState = { const initialSearchState = {
enterprise_name: '', enterprise_name: '',
@@ -450,47 +457,30 @@
{ {
prop: 'operation', prop: 'operation',
label: '操作', label: '操作',
width: 340, width: 200,
fixed: 'right', fixed: 'right',
formatter: (row: EnterpriseItem) => { formatter: (row: EnterpriseItem) => {
const buttons = [] const buttons = []
if (hasAuth('enterprise_customer:edit')) {
buttons.push(
h(ArtButtonTable, {
text: '编辑',
iconClass: BgColorEnum.SECONDARY,
onClick: () => showDialog('edit', row)
})
)
}
if (hasAuth('enterprise_customer:look_customer')) {
buttons.push(
h(ArtButtonTable, {
text: '查看客户',
iconClass: BgColorEnum.PRIMARY,
onClick: () => viewCustomerAccounts(row)
})
)
}
if (hasAuth('enterprise_customer:card_authorization')) { if (hasAuth('enterprise_customer:card_authorization')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
text: '卡授权', text: '卡授权',
iconClass: BgColorEnum.PRIMARY,
onClick: () => manageCards(row) onClick: () => manageCards(row)
}) })
) )
} }
if (hasAuth('enterprise_customer:update_pwd')) { // 只要有编辑、账号列表、修改密码权限之一,就显示更多操作按钮
if (
hasAuth('enterprise_customer:edit') ||
hasAuth('enterprise_customer:look_customer') ||
hasAuth('enterprise_customer:update_pwd')
) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
text: '修改密码', text: '更多操作',
iconClass: BgColorEnum.WARNING, onContextmenu: (e: MouseEvent) => showEnterpriseOperationMenu(e, row)
onClick: () => showPasswordDialog(row)
}) })
) )
} }
@@ -749,8 +739,11 @@
// 查看客户账号 // 查看客户账号
const viewCustomerAccounts = (row: EnterpriseItem) => { const viewCustomerAccounts = (row: EnterpriseItem) => {
currentEnterpriseId.value = row.id router.push({
customerAccountDialogVisible.value = true name: 'EnterpriseCustomerAccounts',
params: { id: row.id },
query: { type: 'enterprise' }
})
} }
// 卡管理 // 卡管理
@@ -760,6 +753,59 @@
query: { id: row.id } query: { id: row.id }
}) })
} }
// 企业客户操作菜单项配置
const enterpriseOperationMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
if (hasAuth('enterprise_customer:look_customer')) {
items.push({
key: 'accountList',
label: '账号列表'
})
}
if (hasAuth('enterprise_customer:edit')) {
items.push({
key: 'edit',
label: '编辑'
})
}
if (hasAuth('enterprise_customer:update_pwd')) {
items.push({
key: 'updatePassword',
label: '修改密码'
})
}
return items
})
// 显示企业客户操作右键菜单
const showEnterpriseOperationMenu = (e: MouseEvent, row: EnterpriseItem) => {
e.preventDefault()
e.stopPropagation()
currentOperatingEnterprise.value = row
enterpriseOperationMenuRef.value?.show(e)
}
// 处理企业客户操作菜单选择
const handleEnterpriseOperationMenuSelect = (item: MenuItemType) => {
if (!currentOperatingEnterprise.value) return
switch (item.key) {
case 'accountList':
viewCustomerAccounts(currentOperatingEnterprise.value)
break
case 'edit':
showDialog('edit', currentOperatingEnterprise.value)
break
case 'updatePassword':
showPasswordDialog(currentOperatingEnterprise.value)
break
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -45,9 +45,9 @@
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '新增平台账号' : '编辑平台账号'" :title="dialogType === 'add' ? '新增平台账号' : '编辑平台账号'"
width="500px" width="30%"
> >
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px"> <ElForm ref="formRef" :model="formData" :rules="rules" label-width="80px">
<ElFormItem label="账号名称" prop="username"> <ElFormItem label="账号名称" prop="username">
<ElInput <ElInput
v-model="formData.username" v-model="formData.username"
@@ -297,14 +297,14 @@
// 列配置 // 列配置
const columnOptions = [ const columnOptions = [
{ label: 'ID', prop: 'ID' }, { label: 'ID', prop: 'id' },
{ label: '账号名称', prop: 'username' }, { label: '账号名称', prop: 'username' },
{ label: '手机号', prop: 'phone' }, { label: '手机号', prop: 'phone' },
{ label: '账号类型', prop: 'user_type' }, { label: '账号类型', prop: 'user_type' },
{ label: '店铺名称', prop: 'shop_name' }, { label: '店铺名称', prop: 'shop_name' },
{ label: '企业名称', prop: 'enterprise_name' }, { label: '企业名称', prop: 'enterprise_name' },
{ label: '状态', prop: 'status' }, { label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'CreatedAt' }, { label: '创建时间', prop: 'created_at' },
{ label: '操作', prop: 'operation' } { label: '操作', prop: 'operation' }
] ]
@@ -319,7 +319,7 @@
} }
if (type === 'edit' && row) { if (type === 'edit' && row) {
formData.id = row.ID formData.id = row.id
formData.username = row.username formData.username = row.username
formData.phone = row.phone formData.phone = row.phone
formData.user_type = row.user_type formData.user_type = row.user_type
@@ -348,7 +348,7 @@
}) })
.then(async () => { .then(async () => {
try { try {
await AccountService.deleteAccount(row.ID) await AccountService.deleteAccount(row.id)
ElMessage.success('删除成功') ElMessage.success('删除成功')
getAccountList() getAccountList()
} catch (error) { } catch (error) {
@@ -362,7 +362,7 @@
// 显示修改密码对话框 // 显示修改密码对话框
const showPasswordDialog = (row: PlatformAccount) => { const showPasswordDialog = (row: PlatformAccount) => {
currentAccountId.value = row.ID currentAccountId.value = row.id
passwordForm.new_password = '' passwordForm.new_password = ''
passwordDialogVisible.value = true passwordDialogVisible.value = true
if (passwordFormRef.value) { if (passwordFormRef.value) {
@@ -373,8 +373,8 @@
// 动态列配置 // 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [ const { columnChecks, columns } = useCheckedColumns(() => [
{ {
prop: 'ID', prop: 'id',
label: 'ID', label: 'id',
width: 80 width: 80
}, },
{ {
@@ -435,10 +435,10 @@
} }
}, },
{ {
prop: 'CreatedAt', prop: 'created_at',
label: '创建时间', label: '创建时间',
width: 180, width: 180,
formatter: (row: PlatformAccount) => formatDateTime(row.CreatedAt) formatter: (row: PlatformAccount) => formatDateTime(row.created_at)
}, },
{ {
prop: 'operation', prop: 'operation',
@@ -530,7 +530,7 @@
// 显示分配角色对话框 // 显示分配角色对话框
const showRoleDialog = async (row: PlatformAccount) => { const showRoleDialog = async (row: PlatformAccount) => {
currentAccountId.value = row.ID currentAccountId.value = row.id
selectedRoles.value = [] selectedRoles.value = []
try { try {
@@ -538,11 +538,11 @@
await loadAllRoles() await loadAllRoles()
// 先加载当前账号的角色,再打开对话框 // 先加载当前账号的角色,再打开对话框
const res = await AccountService.getAccountRoles(row.ID) const res = await AccountService.getAccountRoles(row.id)
if (res.code === 0) { if (res.code === 0) {
// 提取角色ID数组 // 提取角色ID数组
const roles = res.data || [] const roles = res.data || []
selectedRoles.value = roles.map((role: any) => role.ID) selectedRoles.value = roles.map((role: any) => role.id)
// 数据加载完成后再打开对话框 // 数据加载完成后再打开对话框
roleDialogVisible.value = true roleDialogVisible.value = true
} }
@@ -738,7 +738,7 @@
// 先更新UI // 先更新UI
row.status = newStatus row.status = newStatus
try { try {
await AccountService.updateAccountStatus(row.ID, newStatus as 0 | 1) await AccountService.updateAccountStatus(row.id, newStatus as 0 | 1)
ElMessage.success('状态切换成功') ElMessage.success('状态切换成功')
} catch (error) { } catch (error) {
// 切换失败,恢复原状态 // 切换失败,恢复原状态

View File

@@ -250,7 +250,7 @@
dialogType.value = type dialogType.value = type
if (type === 'edit' && row) { if (type === 'edit' && row) {
formData.id = row.ID formData.id = row.id
formData.username = row.username formData.username = row.username
formData.phone = row.phone formData.phone = row.phone
formData.shop_id = row.shop_id || 0 formData.shop_id = row.shop_id || 0
@@ -273,7 +273,7 @@
// 显示修改密码对话框 // 显示修改密码对话框
const showPasswordDialog = (row: PlatformAccount) => { const showPasswordDialog = (row: PlatformAccount) => {
currentAccountId.value = row.ID currentAccountId.value = row.id
passwordForm.new_password = '' passwordForm.new_password = ''
// 重置表单验证状态 // 重置表单验证状态
@@ -290,7 +290,7 @@
// 先更新UI // 先更新UI
row.status = newStatus row.status = newStatus
try { try {
await AccountService.updateAccountStatus(row.ID, newStatus as 0 | 1) await AccountService.updateAccountStatus(row.id, newStatus as 0 | 1)
ElMessage.success('状态切换成功') ElMessage.success('状态切换成功')
} catch (error) { } catch (error) {
// 切换失败,恢复原状态 // 切换失败,恢复原状态
@@ -337,7 +337,7 @@
{ {
prop: 'created_at', prop: 'created_at',
label: '创建时间', label: '创建时间',
formatter: (row: PlatformAccount) => formatDateTime(row.CreatedAt) formatter: (row: PlatformAccount) => formatDateTime(row.created_at)
}, },
{ {
prop: 'operation', prop: 'operation',

View File

@@ -0,0 +1,300 @@
<template>
<ArtTableFullScreen>
<div class="account-list-page" id="table-full-screen">
<ElCard shadow="never">
<div class="detail-header">
<ElButton @click="handleBack">
<template #icon><ElIcon><ArrowLeft /></ElIcon></template>
返回
</ElButton>
<h2 class="detail-title">{{ pageTitle }}</h2>
</div>
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="false"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="ID"
:loading="loading"
:data="accountList"
:currentPage="pagination.page"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
height="60vh"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || (col as any).type" v-bind="col" />
</template>
</ArtTable>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElTag, ElIcon, ElButton, ElCard } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import { AccountService } from '@/api/modules'
import type { SearchFormItem } from '@/types'
import type { PlatformAccount } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'CommonAccountList' })
const route = useRoute()
const router = useRouter()
// 判断页面类型:通过 query 参数判断
const isShopType = computed(() => route.query.type === 'shop')
// 页面标题
const pageTitle = computed(() => (isShopType.value ? '店铺账号列表' : '企业客户账号列表'))
// 过滤参数键名
const filterParamKey = computed(() => (isShopType.value ? 'shop_id' : 'enterprise_id'))
// 用户类型映射
const getUserTypeName = (type: number): string => {
const typeMap: Record<number, string> = {
1: '超级管理员',
2: '平台用户',
3: '代理账号',
4: '企业账号'
}
return typeMap[type] || '未知'
}
// 状态名称映射
const getStatusName = (status: number): string => {
return status === 1 ? '启用' : '禁用'
}
const loading = ref(false)
const tableRef = ref()
const accountList = ref<PlatformAccount[]>([])
// 搜索表单初始值
const initialSearchState = {
username: '',
phone: '',
user_type: undefined as number | undefined,
status: undefined as number | undefined
}
// 搜索表单
const searchForm = reactive({ ...initialSearchState })
// 分页
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0
})
// 搜索表单配置
const searchFormItems: SearchFormItem[] = [
{
label: '用户名',
prop: 'username',
type: 'input',
config: {
clearable: true,
placeholder: '请输入用户名'
}
},
{
label: '手机号',
prop: 'phone',
type: 'input',
config: {
clearable: true,
placeholder: '请输入手机号'
}
},
{
label: '用户类型',
prop: 'user_type',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '代理账号', value: 3 },
{ label: '企业账号', value: 4 }
]
},
{
label: '状态',
prop: 'status',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 }
]
}
]
// 获取用户类型标签类型
const getUserTypeTag = (type: number) => {
return type === 3 ? 'success' : 'primary'
}
// 获取状态标签类型
const getStatusTag = (status: number) => {
return status === 1 ? 'success' : 'info'
}
// 列配置
const columns = computed(() => [
{
prop: 'username',
label: '用户名',
minWidth: 150
},
{
prop: 'phone',
label: '手机号',
width: 130
},
{
prop: 'user_type',
label: '用户类型',
width: 110,
formatter: (row: PlatformAccount) => {
return h(ElTag, { type: getUserTypeTag(row.user_type) }, () => getUserTypeName(row.user_type))
}
},
{
prop: 'shop_id',
label: '店铺ID',
width: 100,
formatter: (row: PlatformAccount) => row.shop_id || '-'
},
{
prop: 'enterprise_id',
label: '企业ID',
width: 100,
formatter: (row: PlatformAccount) => row.enterprise_id || '-'
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: PlatformAccount) => {
return h(ElTag, { type: getStatusTag(row.status) }, () => getStatusName(row.status))
}
},
{
prop: 'CreatedAt',
label: '创建时间',
width: 180,
formatter: (row: PlatformAccount) => formatDateTime(row.CreatedAt)
}
])
// 获取账号列表
const getTableData = async () => {
loading.value = true
try {
const entityId = Number(route.params.id)
const params: any = {
page: pagination.page,
pageSize: pagination.pageSize,
username: searchForm.username || undefined,
phone: searchForm.phone || undefined,
user_type: searchForm.user_type,
status: searchForm.status,
[filterParamKey.value]: entityId // 动态设置 shop_id 或 enterprise_id
}
// 清理空值
Object.keys(params).forEach((key) => {
if (params[key] === '' || params[key] === undefined) {
delete params[key]
}
})
const res = await AccountService.getAccounts(params)
if (res.code === 0) {
accountList.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error(error)
ElMessage.error('获取账号列表失败')
} finally {
loading.value = false
}
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.page = 1
getTableData()
}
// 搜索
const handleSearch = () => {
pagination.page = 1
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
// 返回上一页
const handleBack = () => {
router.back()
}
onMounted(() => {
getTableData()
})
</script>
<style lang="scss" scoped>
.account-list-page {
.detail-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #e4e7ed;
.detail-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #303133;
}
}
}
</style>

View File

@@ -0,0 +1,149 @@
<template>
<div class="package-assign-detail">
<ElCard shadow="never">
<!-- 页面头部 -->
<div class="detail-header">
<ElButton @click="handleBack">
<template #icon>
<ElIcon><ArrowLeft /></ElIcon>
</template>
返回
</ElButton>
<h2 class="detail-title">套餐分配详情</h2>
</div>
<!-- 详情内容 -->
<DetailPage v-if="detailData" :sections="detailSections" :data="detailData" />
<!-- 加载中 -->
<div v-if="loading" class="loading-container">
<ElIcon class="is-loading"><Loading /></ElIcon>
<span>加载中...</span>
</div>
</ElCard>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { ShopPackageAllocationService } from '@/api/modules'
import type { ShopPackageAllocationResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'PackageAssignDetail' })
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const detailData = ref<ShopPackageAllocationResponse | null>(null)
// 详情页配置
const detailSections: DetailSection[] = [
{
title: '基本信息',
fields: [
{ label: 'ID', prop: 'id' },
{ label: '套餐编码', prop: 'package_code' },
{ label: '套餐名称', prop: 'package_name' },
{ label: '系列名称', prop: 'series_name' },
{ label: '被分配店铺', prop: 'shop_name' },
{
label: '分配者店铺',
formatter: (_, data) => {
if (data.allocator_shop_id === 0) {
return '平台'
}
return data.allocator_shop_name || '-'
}
},
{
label: '成本价',
formatter: (_, data) => {
return `¥${(data.cost_price / 100).toFixed(2)}`
}
},
{
label: '状态',
formatter: (_, data) => {
return data.status === 1 ? '启用' : '禁用'
}
},
{ label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },
{ label: '更新时间', prop: 'updated_at', formatter: (value) => formatDateTime(value) }
]
}
]
// 返回上一页
const handleBack = () => {
router.back()
}
// 获取详情数据
const fetchDetail = async () => {
const id = Number(route.params.id)
if (!id) {
ElMessage.error('缺少ID参数')
return
}
loading.value = true
try {
const res = await ShopPackageAllocationService.getShopPackageAllocationDetail(id)
if (res.code === 0) {
detailData.value = res.data
}
} catch (error) {
console.error(error)
ElMessage.error('获取详情失败')
} finally {
loading.value = false
}
}
onMounted(() => {
fetchDetail()
})
</script>
<style scoped lang="scss">
.package-assign-detail {
padding: 20px;
}
.detail-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--el-border-color-light);
.detail-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary);
.el-icon {
font-size: 32px;
}
}
</style>

View File

@@ -5,7 +5,8 @@
<ArtSearchBar <ArtSearchBar
v-model:filter="searchForm" v-model:filter="searchForm"
:items="searchFormItems" :items="searchFormItems"
:show-expand="false" :show-expand="true"
label-width="85"
@reset="handleReset" @reset="handleReset"
@search="handleSearch" @search="handleSearch"
></ArtSearchBar> ></ArtSearchBar>
@@ -40,70 +41,15 @@
</template> </template>
</ArtTable> </ArtTable>
<!-- 修改成本价对话框 -->
<ElDialog
v-model="costPriceDialogVisible"
title="修改成本价"
width="500px"
:close-on-click-modal="false"
@closed="handleCostPriceDialogClosed"
>
<ElForm
ref="costPriceFormRef"
:model="costPriceForm"
:rules="costPriceRules"
label-width="120px"
>
<ElFormItem label="套餐名称">
<ElInput v-model="costPriceForm.package_name" disabled />
</ElFormItem>
<ElFormItem label="店铺名称">
<ElInput v-model="costPriceForm.shop_name" disabled />
</ElFormItem>
<ElFormItem label="原成本价(元)">
<ElInputNumber
v-model="costPriceForm.old_cost_price"
disabled
:precision="2"
:controls="false"
style="width: 100%"
/>
</ElFormItem>
<ElFormItem label="新成本价(元)" prop="cost_price">
<ElInputNumber
v-model="costPriceForm.cost_price"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
style="width: 100%"
placeholder="请输入新成本价"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="costPriceDialogVisible = false">取消</ElButton>
<ElButton
type="primary"
@click="handleCostPriceSubmit(costPriceFormRef)"
:loading="costPriceSubmitLoading"
>
提交
</ElButton>
</div>
</template>
</ElDialog>
<!-- 新增/编辑对话框 --> <!-- 新增/编辑对话框 -->
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '新增分配' : '编辑分配'" :title="dialogType === 'add' ? '新增分配' : '编辑分配'"
width="600px" width="30%"
:close-on-click-modal="false" :close-on-click-modal="false"
@closed="handleDialogClosed" @closed="handleDialogClosed"
> >
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px"> <ElForm ref="formRef" :model="form" :rules="rules" label-width="90px">
<ElFormItem label="选择套餐" prop="package_id" v-if="dialogType === 'add'"> <ElFormItem label="选择套餐" prop="package_id" v-if="dialogType === 'add'">
<ElSelect <ElSelect
v-model="form.package_id" v-model="form.package_id"
@@ -125,23 +71,18 @@
</ElSelect> </ElSelect>
</ElFormItem> </ElFormItem>
<ElFormItem label="选择店铺" prop="shop_id" v-if="dialogType === 'add'"> <ElFormItem label="选择店铺" prop="shop_id" v-if="dialogType === 'add'">
<ElSelect <ElTreeSelect
v-model="form.shop_id" v-model="form.shop_id"
:data="shopTreeData"
:props="{ label: 'shop_name', value: 'id', children: 'children' }"
placeholder="请选择店铺" placeholder="请选择店铺"
style="width: 100%" style="width: 100%"
filterable filterable
remote
:remote-method="searchShop"
:loading="shopLoading"
clearable clearable
> :loading="shopLoading"
<ElOption check-strictly
v-for="shop in shopOptions" :render-after-expand="false"
:key="shop.id" />
:label="shop.shop_name"
:value="shop.id"
/>
</ElSelect>
</ElFormItem> </ElFormItem>
<ElFormItem label="成本价(元)" prop="cost_price"> <ElFormItem label="成本价(元)" prop="cost_price">
<ElInputNumber <ElInputNumber
@@ -171,7 +112,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { h } from 'vue' import { h } from 'vue'
import { ShopPackageAllocationService, PackageManageService, ShopService } from '@/api/modules' import { useRouter } from 'vue-router'
import { ShopPackageAllocationService, PackageManageService, ShopService, ShopSeriesAllocationService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElSwitch } from 'element-plus' import { ElMessage, ElMessageBox, ElSwitch } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import type { ShopPackageAllocationResponse, PackageResponse, ShopResponse } from '@/types/api' import type { ShopPackageAllocationResponse, PackageResponse, ShopResponse } from '@/types/api'
@@ -180,6 +122,7 @@
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
import { import {
CommonStatus, CommonStatus,
getStatusText, getStatusText,
@@ -190,26 +133,29 @@
defineOptions({ name: 'PackageAssign' }) defineOptions({ name: 'PackageAssign' })
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
const router = useRouter()
const dialogVisible = ref(false) const dialogVisible = ref(false)
const costPriceDialogVisible = ref(false)
const loading = ref(false) const loading = ref(false)
const submitLoading = ref(false) const submitLoading = ref(false)
const costPriceSubmitLoading = ref(false)
const packageLoading = ref(false) const packageLoading = ref(false)
const shopLoading = ref(false) const shopLoading = ref(false)
const tableRef = ref() const tableRef = ref()
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const costPriceFormRef = ref<FormInstance>()
const packageOptions = ref<PackageResponse[]>([]) const packageOptions = ref<PackageResponse[]>([])
const shopOptions = ref<ShopResponse[]>([]) const shopOptions = ref<ShopResponse[]>([])
const shopTreeData = ref<ShopResponse[]>([])
const searchPackageOptions = ref<PackageResponse[]>([]) const searchPackageOptions = ref<PackageResponse[]>([])
const searchShopOptions = ref<ShopResponse[]>([]) const searchShopOptions = ref<ShopResponse[]>([])
const searchAllocatorShopOptions = ref<ShopResponse[]>([])
const searchSeriesAllocationOptions = ref<any[]>([])
// 搜索表单初始值 // 搜索表单初始值
const initialSearchState = { const initialSearchState = {
shop_id: undefined as number | undefined, shop_id: undefined as number | undefined,
package_id: undefined as number | undefined, package_id: undefined as number | undefined,
series_allocation_id: undefined as number | undefined,
allocator_shop_id: undefined as number | undefined,
status: undefined as number | undefined status: undefined as number | undefined
} }
@@ -219,7 +165,7 @@
// 搜索表单配置 // 搜索表单配置
const searchFormItems = computed<SearchFormItem[]>(() => [ const searchFormItems = computed<SearchFormItem[]>(() => [
{ {
label: '店铺', label: '被分配店铺',
prop: 'shop_id', prop: 'shop_id',
type: 'select', type: 'select',
config: { config: {
@@ -254,6 +200,44 @@
value: p.id value: p.id
})) }))
}, },
{
label: '系列分配',
prop: 'series_allocation_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchSeriesAllocation,
loading: loading.value,
placeholder: '请选择或搜索系列分配'
},
options: () =>
searchSeriesAllocationOptions.value.map((s) => ({
label: `${s.series_name} - ${s.shop_name}`,
value: s.id
}))
},
{
label: '分配者店铺',
prop: 'allocator_shop_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchAllocatorShop,
loading: shopLoading.value,
placeholder: '请选择或搜索分配者店铺'
},
options: () => [
{ label: '平台', value: 0 },
...searchAllocatorShopOptions.value.map((s) => ({
label: s.shop_name,
value: s.id
}))
]
},
{ {
label: '状态', label: '状态',
prop: 'status', prop: 'status',
@@ -278,10 +262,11 @@
// 列配置 // 列配置
const columnOptions = [ const columnOptions = [
{ label: 'ID', prop: 'id' },
{ label: '套餐编码', prop: 'package_code' }, { label: '套餐编码', prop: 'package_code' },
{ label: '套餐名称', prop: 'package_name' }, { label: '套餐名称', prop: 'package_name' },
{ label: '店铺名称', prop: 'shop_name' }, { label: '系列名称', prop: 'series_name' },
{ label: '被分配店铺', prop: 'shop_name' },
{ label: '分配者', prop: 'allocator_shop_name' },
{ label: '成本价', prop: 'cost_price' }, { label: '成本价', prop: 'cost_price' },
{ label: '状态', prop: 'status' }, { label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' }, { label: '创建时间', prop: 'created_at' },
@@ -320,45 +305,47 @@
package_base_price: 0 // 存储选中套餐的成本价,用于验证 package_base_price: 0 // 存储选中套餐的成本价,用于验证
}) })
// 成本价表单验证规则
const costPriceRules = reactive<FormRules>({
cost_price: [{ required: true, message: '请输入新成本价', trigger: 'blur' }]
})
// 成本价表单数据
const costPriceForm = reactive<any>({
id: 0,
package_name: '',
shop_name: '',
old_cost_price: 0,
cost_price: 0
})
const allocationList = ref<ShopPackageAllocationResponse[]>([]) const allocationList = ref<ShopPackageAllocationResponse[]>([])
const dialogType = ref('add') const dialogType = ref('add')
// 动态列配置 // 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [ const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'id',
label: 'ID',
width: 80
},
{ {
prop: 'package_code', prop: 'package_code',
label: '套餐编码', label: '套餐编码',
minWidth: 150 minWidth: 200,
showOverflowTooltip: true
}, },
{ {
prop: 'package_name', prop: 'package_name',
label: '套餐名称', label: '套餐名称',
minWidth: 180 minWidth: 180
}, },
{
prop: 'series_name',
label: '系列名称',
minWidth: 150
},
{ {
prop: 'shop_name', prop: 'shop_name',
label: '店铺名称', label: '被分配店铺',
minWidth: 180 minWidth: 180
}, },
{
prop: 'allocator_shop_name',
label: '分配者',
formatter: (row: ShopPackageAllocationResponse) => {
// 如果是平台分配(allocator_shop_id为0),显示"平台"标签
if (row.allocator_shop_id === 0) {
return h(
'span',
{ style: 'color: #409eff; font-weight: bold' },
row.allocator_shop_name || '平台'
)
}
return row.allocator_shop_name
}
},
{ {
prop: 'cost_price', prop: 'cost_price',
label: '成本价', label: '成本价',
@@ -399,24 +386,22 @@
{ {
prop: 'operation', prop: 'operation',
label: '操作', label: '操作',
width: 230, width: 200,
fixed: 'right', fixed: 'right',
formatter: (row: ShopPackageAllocationResponse) => { formatter: (row: ShopPackageAllocationResponse) => {
const buttons = [] const buttons = []
if (hasAuth('package_assign:update_cost')) { buttons.push(
buttons.push( h(ArtButtonTable, {
h(ArtButtonTable, { type:"view",
text: '修改成本价', onClick: () => handleViewDetail(row)
onClick: () => showCostPriceDialog(row) })
}) )
)
}
if (hasAuth('package_assign:edit')) { if (hasAuth('package_assign:edit')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'edit', type:"edit",
onClick: () => showDialog('edit', row) onClick: () => showDialog('edit', row)
}) })
) )
@@ -425,7 +410,7 @@
if (hasAuth('package_assign:delete')) { if (hasAuth('package_assign:delete')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'delete', type:"delete",
onClick: () => deleteAllocation(row) onClick: () => deleteAllocation(row)
}) })
) )
@@ -436,11 +421,40 @@
} }
]) ])
// 构建树形结构数据
const buildTreeData = (items: ShopResponse[]) => {
const map = new Map<number, ShopResponse & { children?: ShopResponse[] }>()
const tree: ShopResponse[] = []
// 先将所有项放入 map
items.forEach((item) => {
map.set(item.id, { ...item, children: [] })
})
// 构建树形结构
items.forEach((item) => {
const node = map.get(item.id)!
if (item.parent_id && map.has(item.parent_id)) {
// 有父节点,添加到父节点的 children 中
const parent = map.get(item.parent_id)!
if (!parent.children) parent.children = []
parent.children.push(node)
} else {
// 没有父节点或父节点不存在,作为根节点
tree.push(node)
}
})
return tree
}
onMounted(() => { onMounted(() => {
loadPackageOptions() loadPackageOptions()
loadShopOptions() loadShopOptions()
loadSearchPackageOptions() loadSearchPackageOptions()
loadSearchShopOptions() loadSearchShopOptions()
loadSearchAllocatorShopOptions()
loadSearchSeriesAllocationOptions()
getTableData() getTableData()
}) })
@@ -466,20 +480,19 @@
} }
} }
// 加载店铺选项(用于新增对话框,默认加载10条 // 加载店铺选项(用于新增对话框,加载所有店铺并构建树形结构
const loadShopOptions = async (shopName?: string) => { const loadShopOptions = async () => {
shopLoading.value = true shopLoading.value = true
try { try {
const params: any = { // 加载所有店铺,不分页
const res = await ShopService.getShops({
page: 1, page: 1,
page_size: 10 page_size: 10000 // 使用较大的值获取所有店铺
} })
if (shopName) {
params.shop_name = shopName
}
const res = await ShopService.getShops(params)
if (res.code === 0) { if (res.code === 0) {
shopOptions.value = res.data.items || [] shopOptions.value = res.data.items || []
// 构建树形结构数据
shopTreeData.value = buildTreeData(shopOptions.value)
} }
} catch (error) { } catch (error) {
console.error('加载店铺选项失败:', error) console.error('加载店铺选项失败:', error)
@@ -524,15 +537,6 @@
} }
} }
// 搜索店铺(用于新增对话框)
const searchShop = (query: string) => {
if (query) {
loadShopOptions(query)
} else {
loadShopOptions()
}
}
// 搜索套餐(用于搜索栏) // 搜索套餐(用于搜索栏)
const handleSearchPackage = async (query: string) => { const handleSearchPackage = async (query: string) => {
if (!query) { if (!query) {
@@ -573,6 +577,73 @@
} }
} }
// 加载搜索栏分配者店铺选项(默认加载10条)
const loadSearchAllocatorShopOptions = async () => {
try {
const res = await ShopService.getShops({ page: 1, page_size: 10 })
if (res.code === 0) {
searchAllocatorShopOptions.value = res.data.items || []
}
} catch (error) {
console.error('加载搜索栏分配者店铺选项失败:', error)
}
}
// 搜索分配者店铺(用于搜索栏)
const handleSearchAllocatorShop = async (query: string) => {
if (!query) {
loadSearchAllocatorShopOptions()
return
}
try {
const res = await ShopService.getShops({
page: 1,
page_size: 10,
shop_name: query
})
if (res.code === 0) {
searchAllocatorShopOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索分配者店铺失败:', error)
}
}
// 加载搜索栏系列分配选项(默认加载10条)
const loadSearchSeriesAllocationOptions = async () => {
try {
const res = await ShopSeriesAllocationService.getShopSeriesAllocations({
page: 1,
page_size: 10
})
if (res.code === 0) {
searchSeriesAllocationOptions.value = res.data.items || []
}
} catch (error) {
console.error('加载搜索栏系列分配选项失败:', error)
}
}
// 搜索系列分配(用于搜索栏)
const handleSearchSeriesAllocation = async (query: string) => {
if (!query) {
loadSearchSeriesAllocationOptions()
return
}
try {
const res = await ShopSeriesAllocationService.getShopSeriesAllocations({
page: 1,
page_size: 10,
series_name: query
})
if (res.code === 0) {
searchSeriesAllocationOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索系列分配失败:', error)
}
}
// 获取分配列表 // 获取分配列表
const getTableData = async () => { const getTableData = async () => {
loading.value = true loading.value = true
@@ -582,6 +653,8 @@
page_size: pagination.page_size, page_size: pagination.page_size,
shop_id: searchForm.shop_id || undefined, shop_id: searchForm.shop_id || undefined,
package_id: searchForm.package_id || undefined, package_id: searchForm.package_id || undefined,
series_allocation_id: searchForm.series_allocation_id || undefined,
allocator_shop_id: searchForm.allocator_shop_id || undefined,
status: searchForm.status || undefined status: searchForm.status || undefined
} }
const res = await ShopPackageAllocationService.getShopPackageAllocations(params) const res = await ShopPackageAllocationService.getShopPackageAllocations(params)
@@ -657,9 +730,9 @@
// 从套餐选项中找到选中的套餐 // 从套餐选项中找到选中的套餐
const selectedPackage = packageOptions.value.find((pkg) => pkg.id === packageId) const selectedPackage = packageOptions.value.find((pkg) => pkg.id === packageId)
if (selectedPackage) { if (selectedPackage) {
// 将套餐的价(分)转换为元显示 // 将套餐的成本价(分)转换为元显示
form.cost_price = selectedPackage.price / 100 form.cost_price = selectedPackage.cost_price / 100
form.package_base_price = selectedPackage.price // 保持原始值(分)用于验证 form.package_base_price = selectedPackage.cost_price // 保持原始值(分)用于验证
} }
} else { } else {
// 清空时重置成本价 // 清空时重置成本价
@@ -744,60 +817,6 @@
}) })
} }
// 显示修改成本价对话框
const showCostPriceDialog = (row: ShopPackageAllocationResponse) => {
costPriceDialogVisible.value = true
costPriceForm.id = row.id
costPriceForm.package_name = row.package_name
costPriceForm.shop_name = row.shop_name
costPriceForm.old_cost_price = row.cost_price / 100 // 分转换为元显示
costPriceForm.cost_price = row.cost_price / 100 // 分转换为元显示
// 重置表单验证状态
nextTick(() => {
costPriceFormRef.value?.clearValidate()
})
}
// 处理成本价弹窗关闭事件
const handleCostPriceDialogClosed = () => {
// 清除表单验证状态
costPriceFormRef.value?.clearValidate()
// 重置表单数据
costPriceForm.id = 0
costPriceForm.package_name = ''
costPriceForm.shop_name = ''
costPriceForm.old_cost_price = 0
costPriceForm.cost_price = 0
}
// 提交成本价修改
const handleCostPriceSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (valid) {
costPriceSubmitLoading.value = true
try {
// 将元转换为分提交给后端
const costPriceInCents = Math.round(costPriceForm.cost_price * 100)
await ShopPackageAllocationService.updateShopPackageAllocationCostPrice(
costPriceForm.id,
costPriceInCents
)
ElMessage.success('修改成本价成功')
costPriceDialogVisible.value = false
await getTableData()
} catch (error) {
console.error(error)
} finally {
costPriceSubmitLoading.value = false
}
}
})
}
// 状态切换 // 状态切换
const handleStatusChange = async ( const handleStatusChange = async (
row: ShopPackageAllocationResponse, row: ShopPackageAllocationResponse,
@@ -814,6 +833,11 @@
console.error(error) console.error(error)
} }
} }
// 查看详情
const handleViewDetail = (row: ShopPackageAllocationResponse) => {
router.push(`${RoutesAlias.PackageAssignDetail}/${row.id}`)
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -57,11 +57,11 @@
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
title="新增套餐" title="新增套餐"
width="800px" width="55%"
align-center align-center
:close-on-click-modal="false" :close-on-click-modal="false"
> >
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="140px"> <ElForm ref="formRef" :model="formData" :rules="rules" label-width="150px">
<ElRow :gutter="24"> <ElRow :gutter="24">
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="套餐系列" prop="series"> <ElFormItem label="套餐系列" prop="series">

View File

@@ -0,0 +1,248 @@
<template>
<div class="package-detail">
<ElCard shadow="never">
<!-- 页面头部 -->
<div class="detail-header">
<ElButton @click="handleBack">
<template #icon>
<ElIcon><ArrowLeft /></ElIcon>
</template>
返回
</ElButton>
<h2 class="detail-title">套餐详情</h2>
</div>
<!-- 详情内容 -->
<DetailPage v-if="detailData" :sections="detailSections" :data="detailData" />
<!-- 加载中 -->
<div v-if="loading" class="loading-container">
<ElIcon class="is-loading"><Loading /></ElIcon>
<span>加载中...</span>
</div>
</ElCard>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { PackageManageService } from '@/api/modules'
import type { PackageResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'PackageDetail' })
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const detailData = ref<PackageResponse | null>(null)
// 详情页配置
const detailSections: DetailSection[] = [
{
title: '基本信息',
fields: [
{ label: 'ID', prop: 'id' },
{ label: '套餐编码', prop: 'package_code' },
{ label: '套餐名称', prop: 'package_name' },
{ label: '套餐系列', prop: 'series_name' },
{
label: '套餐类型',
formatter: (_, data) => {
const typeMap = {
formal: '正式套餐',
addon: '附加套餐'
}
return typeMap[data.package_type] || data.package_type
}
},
{
label: '套餐时长',
formatter: (_, data) => {
return `${data.duration_months} 个月`
}
},
{
label: '状态',
formatter: (_, data) => {
return data.status === 1 ? '启用' : '禁用'
}
},
{
label: '上架状态',
formatter: (_, data) => {
return data.shelf_status === 1 ? '上架' : '下架'
}
},
{ label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },
{ label: '更新时间', prop: 'updated_at', formatter: (value) => formatDateTime(value) }
]
},
{
title: '流量配置',
fields: [
{
label: '真流量额度',
formatter: (_, data) => {
if (data.real_data_mb >= 1024) {
return `${(data.real_data_mb / 1024).toFixed(2)} GB`
}
return `${data.real_data_mb} MB`
}
},
{
label: '启用虚流量',
formatter: (_, data) => {
return data.enable_virtual_data ? '是' : '否'
}
},
{
label: '虚流量额度',
formatter: (_, data) => {
if (!data.enable_virtual_data) return '-'
if (data.virtual_data_mb >= 1024) {
return `${(data.virtual_data_mb / 1024).toFixed(2)} GB`
}
return `${data.virtual_data_mb} MB`
}
}
]
},
{
title: '价格信息',
fields: [
{
label: '成本价',
formatter: (_, data) => {
return `¥${(data.cost_price / 100).toFixed(2)}`
}
},
{
label: '建议售价',
formatter: (_, data) => {
return `¥${(data.suggested_retail_price / 100).toFixed(2)}`
}
},
{
label: '利润空间',
formatter: (_, data) => {
if (data.profit_margin === null || data.profit_margin === undefined) return '-'
return `¥${(data.profit_margin / 100).toFixed(2)}`
}
}
]
},
{
title: '佣金配置',
fields: [
{
label: '当前返佣比例',
formatter: (_, data) => {
return data.current_commission_rate || '-'
}
},
{
label: '一次性佣金金额',
formatter: (_, data) => {
if (data.one_time_commission_amount === null || data.one_time_commission_amount === undefined) return '-'
return `¥${(data.one_time_commission_amount / 100).toFixed(2)}`
}
},
{
label: '当前返佣档位',
formatter: (_, data) => {
if (!data.tier_info || !data.tier_info.current_rate) return '-'
return data.tier_info.current_rate
}
},
{
label: '下一档位比例',
formatter: (_, data) => {
if (!data.tier_info || !data.tier_info.next_rate) return '-'
return data.tier_info.next_rate
}
},
{
label: '下一档位阈值',
formatter: (_, data) => {
if (!data.tier_info || data.tier_info.next_threshold === null || data.tier_info.next_threshold === undefined) return '-'
return data.tier_info.next_threshold
}
}
]
}
]
// 返回上一页
const handleBack = () => {
router.back()
}
// 获取详情数据
const fetchDetail = async () => {
const id = Number(route.params.id)
if (!id) {
ElMessage.error('缺少ID参数')
return
}
loading.value = true
try {
const res = await PackageManageService.getPackageDetail(id)
if (res.code === 0) {
detailData.value = res.data
}
} catch (error) {
console.error(error)
ElMessage.error('获取详情失败')
} finally {
loading.value = false
}
}
onMounted(() => {
fetchDetail()
})
</script>
<style scoped lang="scss">
.package-detail {
padding: 20px;
}
.detail-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--el-border-color-light);
.detail-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary);
.el-icon {
font-size: 32px;
}
}
</style>

View File

@@ -5,7 +5,7 @@
<ArtSearchBar <ArtSearchBar
v-model:filter="searchForm" v-model:filter="searchForm"
:items="searchFormItems" :items="searchFormItems"
:show-expand="false" :show-expand="true"
@reset="handleReset" @reset="handleReset"
@search="handleSearch" @search="handleSearch"
></ArtSearchBar> ></ArtSearchBar>
@@ -48,7 +48,7 @@
:close-on-click-modal="false" :close-on-click-modal="false"
@closed="handleDialogClosed" @closed="handleDialogClosed"
> >
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px"> <ElForm ref="formRef" :model="form" :rules="rules" label-width="150px">
<ElFormItem label="套餐编码" prop="package_code"> <ElFormItem label="套餐编码" prop="package_code">
<div style="display: flex; gap: 8px;"> <div style="display: flex; gap: 8px;">
<ElInput <ElInput
@@ -99,17 +99,7 @@
/> />
</ElSelect> </ElSelect>
</ElFormItem> </ElFormItem>
<ElFormItem label="流量类型" prop="data_type"> <ElFormItem label="流量额度(MB)" prop="real_data_mb">
<ElSelect v-model="form.data_type" placeholder="请选择流量类型" style="width: 100%">
<ElOption
v-for="option in DATA_TYPE_OPTIONS"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="真流量额度(MB)" prop="real_data_mb" v-if="form.data_type === 'real'">
<ElInputNumber <ElInputNumber
v-model="form.real_data_mb" v-model="form.real_data_mb"
:min="0" :min="0"
@@ -118,10 +108,17 @@
placeholder="请输入真流量额度" placeholder="请输入真流量额度"
/> />
</ElFormItem> </ElFormItem>
<ElFormItem label="启用虚流量">
<ElSwitch
v-model="form.enable_virtual_data"
active-text="启用"
inactive-text="不启用"
/>
</ElFormItem>
<ElFormItem <ElFormItem
label="虚流量额度(MB)" label="虚流量额度(MB)"
prop="virtual_data_mb" prop="virtual_data_mb"
v-if="form.data_type === 'virtual'" v-if="form.enable_virtual_data"
> >
<ElInputNumber <ElInputNumber
v-model="form.virtual_data_mb" v-model="form.virtual_data_mb"
@@ -140,15 +137,26 @@
style="width: 100%" style="width: 100%"
/> />
</ElFormItem> </ElFormItem>
<ElFormItem label="价(元)" prop="price"> <ElFormItem label="成本价(元)" prop="cost_price">
<ElInputNumber <ElInputNumber
v-model="form.price" v-model="form.cost_price"
:min="0" :min="0"
:precision="2" :precision="2"
:step="0.01" :step="0.01"
:controls="false" :controls="false"
style="width: 100%" style="width: 100%"
placeholder="请输入价" placeholder="请输入成本价"
/>
</ElFormItem>
<ElFormItem label="建议售价(元)" prop="suggested_retail_price">
<ElInputNumber
v-model="form.suggested_retail_price"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
style="width: 100%"
placeholder="请输入建议售价(可选)"
/> />
</ElFormItem> </ElFormItem>
<ElFormItem label="套餐描述" prop="description"> <ElFormItem label="套餐描述" prop="description">
@@ -178,6 +186,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { h } from 'vue' import { h } from 'vue'
import { useRouter } from 'vue-router'
import { PackageManageService, PackageSeriesService } from '@/api/modules' import { PackageManageService, PackageSeriesService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElButton } from 'element-plus' import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElButton } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
@@ -187,25 +196,22 @@
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
import { import {
CommonStatus, CommonStatus,
getStatusText, getStatusText,
frontendStatusToApi, frontendStatusToApi,
apiStatusToFrontend, apiStatusToFrontend,
PACKAGE_TYPE_OPTIONS, PACKAGE_TYPE_OPTIONS,
DATA_TYPE_OPTIONS,
SHELF_STATUS_OPTIONS,
getPackageTypeLabel, getPackageTypeLabel,
getPackageTypeTag, getPackageTypeTag
getDataTypeLabel,
getDataTypeTag,
getShelfStatusText
} from '@/config/constants' } from '@/config/constants'
import { generatePackageCode } from '@/utils/codeGenerator' import { generatePackageCode } from '@/utils/codeGenerator'
defineOptions({ name: 'PackageList' }) defineOptions({ name: 'PackageList' })
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
const router = useRouter()
const dialogVisible = ref(false) const dialogVisible = ref(false)
const loading = ref(false) const loading = ref(false)
@@ -308,16 +314,15 @@
// 列配置 // 列配置
const columnOptions = [ const columnOptions = [
{ label: 'ID', prop: 'id' },
{ label: '套餐编码', prop: 'package_code' }, { label: '套餐编码', prop: 'package_code' },
{ label: '套餐名称', prop: 'package_name' }, { label: '套餐名称', prop: 'package_name' },
{ label: '所属系列', prop: 'series_name' }, { label: '所属系列', prop: 'series_name' },
{ label: '套餐类型', prop: 'package_type' }, { label: '套餐类型', prop: 'package_type' },
{ label: '流量类型', prop: 'data_type' },
{ label: '真流量', prop: 'real_data_mb' }, { label: '真流量', prop: 'real_data_mb' },
{ label: '虚流量', prop: 'virtual_data_mb' }, { label: '虚流量', prop: 'virtual_data_mb' },
{ label: '有效期', prop: 'duration_months' }, { label: '有效期', prop: 'duration_months' },
{ label: '价', prop: 'price' }, { label: '成本价', prop: 'cost_price' },
{ label: '建议售价', prop: 'suggested_retail_price' },
{ label: '上架状态', prop: 'shelf_status' }, { label: '上架状态', prop: 'shelf_status' },
{ label: '状态', prop: 'status' }, { label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' }, { label: '创建时间', prop: 'created_at' },
@@ -335,20 +340,16 @@
{ required: true, message: '请输入套餐名称', trigger: 'blur' }, { required: true, message: '请输入套餐名称', trigger: 'blur' },
{ min: 1, max: 255, message: '长度在 1 到 255 个字符', trigger: 'blur' } { min: 1, max: 255, message: '长度在 1 到 255 个字符', trigger: 'blur' }
], ],
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
package_type: [{ required: true, message: '请选择套餐类型', trigger: 'change' }], package_type: [{ required: true, message: '请选择套餐类型', trigger: 'change' }],
data_type: [{ required: true, message: '请选择流量类型', trigger: 'change' }],
duration_months: [ duration_months: [
{ required: true, message: '请输入有效期', trigger: 'blur' }, { required: true, message: '请输入有效期', trigger: 'blur' },
{ type: 'number', min: 1, max: 120, message: '有效期范围 1-120 月', trigger: 'blur' } { type: 'number', min: 1, max: 120, message: '有效期范围 1-120 月', trigger: 'blur' }
], ],
price: [{ required: true, message: '请输入价', trigger: 'blur' }] cost_price: [{ required: true, message: '请输入成本价', trigger: 'blur' }]
} }
// 根据流量类型动态添加验证规则 // 如果启用虚流量,则虚流量额度为必填
if (form.data_type === 'real') { if (form.enable_virtual_data) {
baseRules.real_data_mb = [{ required: true, message: '请输入真流量额度', trigger: 'blur' }]
} else if (form.data_type === 'virtual') {
baseRules.virtual_data_mb = [{ required: true, message: '请输入虚流量额度', trigger: 'blur' }] baseRules.virtual_data_mb = [{ required: true, message: '请输入虚流量额度', trigger: 'blur' }]
} }
@@ -362,11 +363,12 @@
package_name: '', package_name: '',
series_id: undefined, series_id: undefined,
package_type: '', package_type: '',
data_type: '', enable_virtual_data: false,
real_data_mb: 0, real_data_mb: 0,
virtual_data_mb: 0, virtual_data_mb: 0,
duration_months: 1, duration_months: 1,
price: 0, cost_price: 0,
suggested_retail_price: undefined,
description: '' description: ''
}) })
@@ -375,20 +377,16 @@
// 动态列配置 // 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [ const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'id',
label: 'ID',
width: 80
},
{ {
prop: 'package_code', prop: 'package_code',
label: '套餐编码', label: '套餐编码',
minWidth: 150 showOverflowTooltip: true,
width: 210
}, },
{ {
prop: 'package_name', prop: 'package_name',
label: '套餐名称', label: '套餐名称',
minWidth: 180 minWidth: 160
}, },
{ {
prop: 'series_name', prop: 'series_name',
@@ -405,16 +403,6 @@
) )
} }
}, },
{
prop: 'data_type',
label: '流量类型',
width: 100,
formatter: (row: PackageResponse) => {
return h(ElTag, { type: getDataTypeTag(row.data_type), size: 'small' }, () =>
getDataTypeLabel(row.data_type)
)
}
},
{ {
prop: 'real_data_mb', prop: 'real_data_mb',
label: '真流量', label: '真流量',
@@ -434,10 +422,17 @@
formatter: (row: PackageResponse) => `${row.duration_months}` formatter: (row: PackageResponse) => `${row.duration_months}`
}, },
{ {
prop: 'price', prop: 'cost_price',
label: '价', label: '成本价',
width: 100, width: 100,
formatter: (row: PackageResponse) => `¥${(row.price / 100).toFixed(2)}` formatter: (row: PackageResponse) => `¥${(row.cost_price / 100).toFixed(2)}`
},
{
prop: 'suggested_retail_price',
label: '建议售价',
width: 100,
formatter: (row: PackageResponse) =>
row.suggested_retail_price ? `¥${(row.suggested_retail_price / 100).toFixed(2)}` : '-'
}, },
{ {
prop: 'shelf_status', prop: 'shelf_status',
@@ -483,11 +478,18 @@
{ {
prop: 'operation', prop: 'operation',
label: '操作', label: '操作',
width: 150, width: 200,
fixed: 'right', fixed: 'right',
formatter: (row: PackageResponse) => { formatter: (row: PackageResponse) => {
const buttons = [] const buttons = []
buttons.push(
h(ArtButtonTable, {
type: 'view',
onClick: () => handleViewDetail(row)
})
)
if (hasAuth('package:edit')) { if (hasAuth('package:edit')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
@@ -511,14 +513,12 @@
} }
]) ])
// 监听流量类型变化,重置未使用的流量字段 // 监听流量开关变化,关闭时重置虚流量额度
watch( watch(
() => form.data_type, () => form.enable_virtual_data,
(newType) => { (enabled) => {
if (newType === 'real') { if (!enabled) {
form.virtual_data_mb = 0 form.virtual_data_mb = 0
} else if (newType === 'virtual') {
form.real_data_mb = 0
} }
} }
) )
@@ -656,11 +656,12 @@
form.package_name = row.package_name form.package_name = row.package_name
form.series_id = row.series_id form.series_id = row.series_id
form.package_type = row.package_type form.package_type = row.package_type
form.data_type = row.data_type form.enable_virtual_data = row.enable_virtual_data || false
form.real_data_mb = row.real_data_mb form.real_data_mb = row.real_data_mb || 0
form.virtual_data_mb = row.virtual_data_mb form.virtual_data_mb = row.virtual_data_mb || 0
form.duration_months = row.duration_months form.duration_months = row.duration_months
form.price = row.price / 100 // 分转换为元显示 form.cost_price = row.cost_price / 100 // 分转换为元显示
form.suggested_retail_price = row.suggested_retail_price ? row.suggested_retail_price / 100 : undefined
form.description = row.description || '' form.description = row.description || ''
} else { } else {
form.id = 0 form.id = 0
@@ -668,11 +669,12 @@
form.package_name = '' form.package_name = ''
form.series_id = undefined form.series_id = undefined
form.package_type = '' form.package_type = ''
form.data_type = '' form.enable_virtual_data = false
form.real_data_mb = 0 form.real_data_mb = 0
form.virtual_data_mb = 0 form.virtual_data_mb = 0
form.duration_months = 1 form.duration_months = 1
form.price = 0 form.cost_price = 0
form.suggested_retail_price = undefined
form.description = '' form.description = ''
} }
@@ -685,6 +687,10 @@
// 生成套餐编码 // 生成套餐编码
const handleGeneratePackageCode = () => { const handleGeneratePackageCode = () => {
form.package_code = generatePackageCode() form.package_code = generatePackageCode()
// 生成编码后清除该字段的验证错误
nextTick(() => {
formRef.value?.clearValidate('package_code')
})
ElMessage.success('编码生成成功') ElMessage.success('编码生成成功')
} }
@@ -698,11 +704,12 @@
form.package_name = '' form.package_name = ''
form.series_id = undefined form.series_id = undefined
form.package_type = '' form.package_type = ''
form.data_type = '' form.enable_virtual_data = false
form.real_data_mb = 0 form.real_data_mb = 0
form.virtual_data_mb = 0 form.virtual_data_mb = 0
form.duration_months = 1 form.duration_months = 1
form.price = 0 form.cost_price = 0
form.suggested_retail_price = undefined
form.description = '' form.description = ''
} }
@@ -736,19 +743,32 @@
submitLoading.value = true submitLoading.value = true
try { try {
// 将元转换为分提交给后端 // 将元转换为分提交给后端
const priceInCents = Math.round(form.price * 100) const costPriceInCents = Math.round(form.cost_price * 100)
const suggestedRetailPriceInCents = form.suggested_retail_price
? Math.round(form.suggested_retail_price * 100)
: undefined
const data = { const data: any = {
package_code: form.package_code, package_code: form.package_code,
package_name: form.package_name, package_name: form.package_name,
series_id: form.series_id,
package_type: form.package_type, package_type: form.package_type,
data_type: form.data_type,
real_data_mb: form.real_data_mb,
virtual_data_mb: form.virtual_data_mb,
duration_months: form.duration_months, duration_months: form.duration_months,
price: priceInCents, cost_price: costPriceInCents,
description: form.description || undefined enable_virtual_data: form.enable_virtual_data
}
// 可选字段
if (form.series_id) {
data.series_id = form.series_id
}
if (suggestedRetailPriceInCents !== undefined) {
data.suggested_retail_price = suggestedRetailPriceInCents
}
if (form.real_data_mb) {
data.real_data_mb = form.real_data_mb
}
if (form.enable_virtual_data && form.virtual_data_mb) {
data.virtual_data_mb = form.virtual_data_mb
} }
if (dialogType.value === 'add') { if (dialogType.value === 'add') {
@@ -797,6 +817,11 @@
console.error(error) console.error(error)
} }
} }
// 查看详情
const handleViewDetail = (row: PackageResponse) => {
router.push(`${RoutesAlias.PackageDetail}/${row.id}`)
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -0,0 +1,263 @@
<template>
<div class="package-series-detail">
<ElCard shadow="never">
<!-- 页面头部 -->
<div class="detail-header">
<ElButton @click="handleBack">
<template #icon>
<ElIcon><ArrowLeft /></ElIcon>
</template>
返回
</ElButton>
<h2 class="detail-title">套餐系列详情</h2>
</div>
<!-- 详情内容 -->
<DetailPage v-if="detailData" :sections="detailSections" :data="detailData" />
<!-- 加载中 -->
<div v-if="loading" class="loading-container">
<ElIcon class="is-loading"><Loading /></ElIcon>
<span>加载中...</span>
</div>
</ElCard>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, h } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage, ElTag } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { PackageSeriesService } from '@/api/modules'
import type { PackageSeriesResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'PackageSeriesDetail' })
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const detailData = ref<PackageSeriesResponse | null>(null)
// 详情页配置
const detailSections: DetailSection[] = [
{
title: '基本信息',
fields: [
{ label: 'ID', prop: 'id' },
{ label: '系列编码', prop: 'series_code' },
{ label: '系列名称', prop: 'series_name' },
{
label: '状态',
formatter: (_, data) => {
return data.status === 1 ? '启用' : '禁用'
}
},
{
label: '描述',
prop: 'description',
fullWidth: true
},
{ label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },
{ label: '更新时间', prop: 'updated_at', formatter: (value) => formatDateTime(value) }
]
},
{
title: '一次性佣金配置',
fields: [
{
label: '启用状态',
formatter: (_, data) => {
return data.one_time_commission_config?.enable ? '已启用' : '未启用'
}
},
{
label: '佣金类型',
formatter: (_, data) => {
const config = data.one_time_commission_config
if (!config?.commission_type) return '-'
return config.commission_type === 'fixed' ? '固定佣金' : '梯度佣金'
}
},
{
label: '固定佣金金额',
formatter: (_, data) => {
const config = data.one_time_commission_config
if (config?.commission_type !== 'fixed' || !config.commission_amount) return '-'
return `¥${(config.commission_amount / 100).toFixed(2)}`
}
},
{
label: '触发阈值',
formatter: (_, data) => {
const config = data.one_time_commission_config
if (!config?.threshold) return '-'
return `¥${(config.threshold / 100).toFixed(2)}`
}
},
{
label: '触发类型',
formatter: (_, data) => {
const config = data.one_time_commission_config
if (!config?.trigger_type) return '-'
return config.trigger_type === 'first_recharge' ? '首次充值' : '累计充值'
}
},
{
label: '梯度配置',
fullWidth: true,
render: (data) => {
const config = data.one_time_commission_config
if (config?.commission_type !== 'tiered' || !config.tiers || config.tiers.length === 0) {
return h('span', '-')
}
return h('div', { style: 'display: flex; flex-direction: column; gap: 8px;' },
config.tiers.map((tier: any, index: number) => {
const dimensionText = tier.dimension === 'sales_count' ? '销量' : '销售额'
const thresholdText = tier.dimension === 'sales_amount'
? `¥${(tier.threshold / 100).toFixed(2)}`
: tier.threshold
const amountText = `¥${(tier.amount / 100).toFixed(2)}`
const scopeText = tier.stat_scope === 'self' ? '仅自己' : '自己+下级'
return h(ElTag, { type: 'info', size: 'default' },
() => `档位${index + 1}: ${dimensionText}${thresholdText}, 佣金 ${amountText}, ${scopeText}`
)
})
)
}
}
]
},
{
title: '强充配置',
fields: [
{
label: '启用状态',
formatter: (_, data) => {
return data.one_time_commission_config?.enable_force_recharge ? '已启用' : '未启用'
}
},
{
label: '强充金额',
formatter: (_, data) => {
const config = data.one_time_commission_config
if (!config?.force_amount) return '-'
return `¥${(config.force_amount / 100).toFixed(2)}`
}
},
{
label: '强充计算类型',
formatter: (_, data) => {
const config = data.one_time_commission_config
if (!config?.force_calc_type) return '-'
return config.force_calc_type === 'fixed' ? '固定' : '动态'
}
}
]
},
{
title: '时效配置',
fields: [
{
label: '时效类型',
formatter: (_, data) => {
const config = data.one_time_commission_config
if (!config?.validity_type) return '-'
const typeMap = {
permanent: '永久',
fixed_date: '固定日期',
relative: '相对时长'
}
return typeMap[config.validity_type] || '-'
}
},
{
label: '时效值',
formatter: (_, data) => {
const config = data.one_time_commission_config
if (!config?.validity_value) return '-'
if (config.validity_type === 'relative') {
return `${config.validity_value}个月`
} else if (config.validity_type === 'fixed_date') {
return config.validity_value
}
return '-'
}
}
]
}
]
// 返回上一页
const handleBack = () => {
router.back()
}
// 获取详情数据
const fetchDetail = async () => {
const id = Number(route.params.id)
if (!id) {
ElMessage.error('缺少ID参数')
return
}
loading.value = true
try {
const res = await PackageSeriesService.getPackageSeriesDetail(id)
if (res.code === 0) {
detailData.value = res.data
}
} catch (error) {
console.error(error)
ElMessage.error('获取详情失败')
} finally {
loading.value = false
}
}
onMounted(() => {
fetchDetail()
})
</script>
<style scoped lang="scss">
.package-series-detail {
padding: 20px;
}
.detail-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--el-border-color-light);
.detail-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary);
.el-icon {
font-size: 32px;
}
}
</style>

View File

@@ -6,6 +6,7 @@
v-model:filter="searchForm" v-model:filter="searchForm"
:items="searchFormItems" :items="searchFormItems"
:show-expand="false" :show-expand="false"
label-width="85"
@reset="handleReset" @reset="handleReset"
@search="handleSearch" @search="handleSearch"
></ArtSearchBar> ></ArtSearchBar>
@@ -18,7 +19,9 @@
@refresh="handleRefresh" @refresh="handleRefresh"
> >
<template #left> <template #left>
<ElButton type="primary" @click="showDialog('add')" v-permission="'package_series:add'">新增套餐系列</ElButton> <ElButton type="primary" @click="showDialog('add')" v-permission="'package_series:add'"
>新增套餐系列</ElButton
>
</template> </template>
</ArtTableHeader> </ArtTableHeader>
@@ -44,18 +47,18 @@
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '新增套餐系列' : '编辑套餐系列'" :title="dialogType === 'add' ? '新增套餐系列' : '编辑套餐系列'"
width="500px" width="45%"
:close-on-click-modal="false" :close-on-click-modal="false"
> >
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px"> <ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
<ElFormItem label="系列编码" prop="series_code"> <ElFormItem label="系列编码" prop="series_code">
<div style="display: flex; gap: 8px;"> <div style="display: flex; gap: 8px">
<ElInput <ElInput
v-model="form.series_code" v-model="form.series_code"
placeholder="请输入系列编码或点击生成" placeholder="请输入系列编码或点击生成"
:disabled="dialogType === 'edit'" :disabled="dialogType === 'edit'"
clearable clearable
style="flex: 1;" style="flex: 1"
/> />
<ElButton v-if="dialogType === 'add'" @click="handleGenerateSeriesCode"> <ElButton v-if="dialogType === 'add'" @click="handleGenerateSeriesCode">
生成编码 生成编码
@@ -75,6 +78,261 @@
show-word-limit show-word-limit
/> />
</ElFormItem> </ElFormItem>
<!-- 一次性佣金配置 -->
<div class="form-section-title">
<span class="title-text">一次性佣金配置</span>
</div>
<ElFormItem label="启用一次性佣金">
<ElSwitch
v-model="form.one_time_commission_config.enable"
active-text="启用"
inactive-text="不启用"
/>
</ElFormItem>
<template v-if="form.one_time_commission_config.enable">
<!-- 佣金类型 -->
<ElFormItem label="佣金类型">
<ElRadioGroup v-model="form.one_time_commission_config.commission_type">
<ElRadio value="fixed">固定佣金</ElRadio>
<ElRadio value="tiered">梯度佣金</ElRadio>
</ElRadioGroup>
</ElFormItem>
<!-- 触发阈值 -->
<ElFormItem label="触发阈值">
<ElInputNumber
v-model="form.one_time_commission_config.threshold"
:min="0"
:precision="2"
placeholder="触发阈值(元)"
style="width: 100%"
/>
<span
style="color: var(--el-text-color-secondary); font-size: 12px; margin-left: 8px"
>单位</span
>
</ElFormItem>
<!-- 触发类型 -->
<ElFormItem label="触发类型">
<ElRadioGroup v-model="form.one_time_commission_config.trigger_type">
<ElRadio value="first_recharge">首次充值</ElRadio>
<ElRadio value="accumulated_recharge">累计充值</ElRadio>
</ElRadioGroup>
</ElFormItem>
<!-- 固定佣金金额 -->
<ElFormItem
v-if="form.one_time_commission_config.commission_type === 'fixed'"
label="佣金金额"
>
<ElInputNumber
v-model="form.one_time_commission_config.commission_amount"
:min="0"
:precision="2"
placeholder="佣金金额(元)"
style="width: 100%"
/>
<span
style="color: var(--el-text-color-secondary); font-size: 12px; margin-left: 8px"
>单位</span
>
</ElFormItem>
<!-- 梯度佣金配置 -->
<ElFormItem
v-if="form.one_time_commission_config.commission_type === 'tiered'"
label="梯度配置"
>
<div style="width: 100%">
<div
v-for="(tier, index) in form.one_time_commission_config.tiers"
:key="index"
style="margin-bottom: 12px"
>
<ElCard shadow="hover">
<div style="display: flex; gap: 12px; align-items: flex-start">
<div style="flex: 1; display: flex; flex-direction: column; gap: 12px">
<!-- 第一行阈值和维度 -->
<div style="display: flex; gap: 12px">
<div style="flex: 1">
<div
style="
margin-bottom: 4px;
font-size: 12px;
color: var(--el-text-color-regular);
"
>
达标阈值{{ tier.dimension === 'sales_amount' ? '(元)' : '' }}
</div>
<ElInputNumber
v-model="tier.threshold"
:min="0"
:precision="tier.dimension === 'sales_amount' ? 2 : 0"
:placeholder="
tier.dimension === 'sales_amount'
? '达标阈值(元)'
: '达标阈值(数量)'
"
style="width: 100%"
/>
</div>
<div style="flex: 1">
<div
style="
margin-bottom: 4px;
font-size: 12px;
color: var(--el-text-color-regular);
"
>统计维度</div
>
<ElSelect
v-model="tier.dimension"
placeholder="统计维度"
style="width: 100%"
>
<ElOption label="销量" value="sales_count" />
<ElOption label="销售额" value="sales_amount" />
</ElSelect>
</div>
</div>
<!-- 第二行佣金金额和统计范围 -->
<div style="display: flex; gap: 12px">
<div style="flex: 1">
<div
style="
margin-bottom: 4px;
font-size: 12px;
color: var(--el-text-color-regular);
"
>佣金金额</div
>
<ElInputNumber
v-model="tier.amount"
:min="0"
:precision="2"
placeholder="佣金金额"
style="width: 100%"
/>
</div>
<div style="flex: 1">
<div
style="
margin-bottom: 4px;
font-size: 12px;
color: var(--el-text-color-regular);
"
>统计范围</div
>
<ElSelect
v-model="tier.stat_scope"
placeholder="统计范围"
style="width: 100%"
>
<ElOption label="仅自己" value="self" />
<ElOption label="自己+下级" value="self_and_sub" />
</ElSelect>
</div>
</div>
</div>
<ElButton
type="danger"
:icon="Delete"
circle
@click="removeTier(index)"
style="flex-shrink: 0; margin-top: 28px; width: 32px; height: 32px; padding: 0"
/>
</div>
</ElCard>
</div>
<ElButton type="primary" :icon="Plus" @click="addTier" style="width: 100%">
添加梯度
</ElButton>
</div>
</ElFormItem>
<!-- 强充配置 -->
<div class="form-section-title">
<span class="title-text">强充配置</span>
</div>
<ElFormItem label="启用强充">
<ElSwitch
v-model="form.one_time_commission_config.enable_force_recharge"
active-text="启用"
inactive-text="不启用"
/>
</ElFormItem>
<template v-if="form.one_time_commission_config.enable_force_recharge">
<ElFormItem label="强充金额">
<ElInputNumber
v-model="form.one_time_commission_config.force_amount"
:min="0"
:precision="2"
placeholder="强充金额(元)"
style="width: 100%"
/>
<span
style="color: var(--el-text-color-secondary); font-size: 12px; margin-left: 8px"
>单位</span
>
</ElFormItem>
<ElFormItem label="强充计算类型">
<ElRadioGroup v-model="form.one_time_commission_config.force_calc_type">
<ElRadio value="fixed">固定</ElRadio>
<ElRadio value="dynamic">动态</ElRadio>
</ElRadioGroup>
</ElFormItem>
</template>
<!-- 时效配置 -->
<div class="form-section-title">
<span class="title-text">时效配置</span>
</div>
<ElFormItem label="时效类型">
<ElRadioGroup v-model="form.one_time_commission_config.validity_type">
<ElRadio value="permanent">永久</ElRadio>
<ElRadio value="fixed_date">固定日期</ElRadio>
<ElRadio value="relative">相对时长</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="form.one_time_commission_config.validity_type === 'fixed_date'"
label="有效期至"
>
<ElDatePicker
v-model="form.one_time_commission_config.validity_value"
type="date"
placeholder="选择日期"
style="width: 100%"
value-format="YYYY-MM-DD"
/>
</ElFormItem>
<ElFormItem
v-if="form.one_time_commission_config.validity_type === 'relative'"
label="有效月数"
>
<ElInputNumber
v-model="form.one_time_commission_config.validity_value"
:min="1"
:precision="0"
placeholder="有效月数"
style="width: 100%"
/>
<span
style="color: var(--el-text-color-secondary); font-size: 12px; margin-left: 8px"
>单位</span
>
</ElFormItem>
</template>
</ElForm> </ElForm>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
@@ -94,6 +352,7 @@
import { h } from 'vue' import { h } from 'vue'
import { PackageSeriesService } from '@/api/modules' import { PackageSeriesService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElButton } from 'element-plus' import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElButton } from 'element-plus'
import { Plus, Delete } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import type { PackageSeriesResponse } from '@/types/api' import type { PackageSeriesResponse } from '@/types/api'
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
@@ -108,10 +367,12 @@
apiStatusToFrontend apiStatusToFrontend
} from '@/config/constants' } from '@/config/constants'
import { generateSeriesCode } from '@/utils/codeGenerator' import { generateSeriesCode } from '@/utils/codeGenerator'
import { useRouter } from 'vue-router'
defineOptions({ name: 'PackageSeries' }) defineOptions({ name: 'PackageSeries' })
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
const router = useRouter()
const dialogVisible = ref(false) const dialogVisible = ref(false)
const loading = ref(false) const loading = ref(false)
@@ -122,7 +383,8 @@
// 搜索表单初始值 // 搜索表单初始值
const initialSearchState = { const initialSearchState = {
series_name: '', series_name: '',
status: undefined as number | undefined status: undefined as number | undefined,
enable_one_time_commission: undefined as boolean | undefined
} }
// 搜索表单 // 搜索表单
@@ -151,6 +413,19 @@
{ label: '启用', value: 1 }, { label: '启用', value: 1 },
{ label: '禁用', value: 2 } { label: '禁用', value: 2 }
] ]
},
{
label: '一次性佣金',
prop: 'enable_one_time_commission',
type: 'select',
config: {
clearable: true,
placeholder: '请选择'
},
options: () => [
{ label: '已启用', value: true },
{ label: '未启用', value: false }
]
} }
] ]
@@ -163,13 +438,19 @@
// 列配置 // 列配置
const columnOptions = [ const columnOptions = [
{ label: 'ID', prop: 'id' },
{ label: '系列编码', prop: 'series_code' }, { label: '系列编码', prop: 'series_code' },
{ label: '系列名称', prop: 'series_name' }, { label: '系列名称', prop: 'series_name' },
{ label: '描述', prop: 'description' }, { label: '描述', prop: 'description' },
{ label: '一次性佣金', prop: 'enable_one_time_commission' },
{ label: '佣金类型', prop: 'commission_type' },
{ label: '触发阈值', prop: 'commission_threshold' },
{ label: '触发类型', prop: 'trigger_type' },
{ label: '强充状态', prop: 'enable_force_recharge' },
{ label: '强充金额', prop: 'force_amount' },
{ label: '强充计算类型', prop: 'force_calc_type' },
{ label: '时效类型', prop: 'validity_type' },
{ label: '状态', prop: 'status' }, { label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' }, { label: '创建时间', prop: 'created_at' },
{ label: '更新时间', prop: 'updated_at' },
{ label: '操作', prop: 'operation' } { label: '操作', prop: 'operation' }
] ]
@@ -191,7 +472,20 @@
id: 0, id: 0,
series_code: '', series_code: '',
series_name: '', series_name: '',
description: '' description: '',
one_time_commission_config: {
enable: false,
commission_type: 'fixed',
commission_amount: undefined,
threshold: undefined,
trigger_type: 'first_recharge',
tiers: [],
enable_force_recharge: false,
force_amount: undefined,
force_calc_type: 'fixed',
validity_type: 'permanent',
validity_value: undefined
}
}) })
const seriesList = ref<PackageSeriesResponse[]>([]) const seriesList = ref<PackageSeriesResponse[]>([])
@@ -199,25 +493,151 @@
// 动态列配置 // 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [ const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'id',
label: 'ID',
width: 80
},
{ {
prop: 'series_code', prop: 'series_code',
label: '系列编码', label: '系列编码',
minWidth: 150 width: 200,
showOverflowTooltip: true
}, },
{ {
prop: 'series_name', prop: 'series_name',
label: '系列名称', label: '系列名称',
minWidth: 150 minWidth: 150
}, },
{ {
prop: 'description', prop: 'enable_one_time_commission',
label: '描述', label: '一次性佣金',
minWidth: 200 width: 110,
formatter: (row: PackageSeriesResponse) => {
if (!row.one_time_commission_config) {
return h(ElTag, { type: 'info', size: 'small' }, () => '未配置')
}
return h(
ElTag,
{
type: row.one_time_commission_config.enable ? 'success' : 'info',
size: 'small'
},
() => (row.one_time_commission_config.enable ? '已启用' : '未启用')
)
}
},
{
prop: 'commission_type',
label: '佣金类型',
width: 100,
formatter: (row: PackageSeriesResponse) => {
if (!row.one_time_commission_config?.commission_type) {
return '-'
}
const typeMap = {
fixed: '固定',
tiered: '梯度'
}
return typeMap[row.one_time_commission_config.commission_type] || '-'
}
},
{
prop: 'commission_threshold',
label: '触发阈值',
width: 120,
formatter: (row: PackageSeriesResponse) => {
if (!row.one_time_commission_config?.threshold) {
return '-'
}
return `¥${(row.one_time_commission_config.threshold / 100).toFixed(2)}`
}
},
{
prop: 'trigger_type',
label: '触发类型',
width: 110,
formatter: (row: PackageSeriesResponse) => {
if (!row.one_time_commission_config?.trigger_type) {
return '-'
}
const typeMap = {
first_recharge: '首次充值',
accumulated_recharge: '累计充值'
}
return typeMap[row.one_time_commission_config.trigger_type] || '-'
}
},
{
prop: 'enable_force_recharge',
label: '强充状态',
width: 100,
formatter: (row: PackageSeriesResponse) => {
if (!row.one_time_commission_config) {
return '-'
}
return h(
ElTag,
{
type: row.one_time_commission_config.enable_force_recharge ? 'warning' : 'info',
size: 'small'
},
() => (row.one_time_commission_config.enable_force_recharge ? '已启用' : '未启用')
)
}
},
{
prop: 'force_amount',
label: '强充金额',
width: 120,
formatter: (row: PackageSeriesResponse) => {
if (!row.one_time_commission_config?.force_amount) {
return '-'
}
return h(
'span',
{ style: 'color: #f56c6c; font-weight: bold;' },
`¥${(row.one_time_commission_config.force_amount / 100).toFixed(2)}`
)
}
},
{
prop: 'force_calc_type',
label: '强充计算类型',
width: 120,
formatter: (row: PackageSeriesResponse) => {
if (!row.one_time_commission_config?.force_calc_type) {
return '-'
}
const typeMap = {
fixed: '固定',
dynamic: '动态'
}
return typeMap[row.one_time_commission_config.force_calc_type] || '-'
}
},
{
prop: 'validity_type',
label: '时效类型',
width: 180,
formatter: (row: PackageSeriesResponse) => {
if (!row.one_time_commission_config?.validity_type) {
return '-'
}
const typeMap = {
permanent: '永久',
fixed_date: '固定日期',
relative: '相对时长'
}
const validityType = typeMap[row.one_time_commission_config.validity_type] || '-'
// 如果有时效值,显示详情
if (row.one_time_commission_config.validity_value) {
if (row.one_time_commission_config.validity_type === 'relative') {
return `${validityType}(${row.one_time_commission_config.validity_value}月)`
} else if (row.one_time_commission_config.validity_type === 'fixed_date') {
return `${validityType}(${row.one_time_commission_config.validity_value})`
}
}
return validityType
}
}, },
{ {
prop: 'status', prop: 'status',
@@ -238,26 +658,34 @@
}) })
} }
}, },
{
prop: 'description',
label: '描述',
minWidth: 200,
showOverflowTooltip: true
},
{ {
prop: 'created_at', prop: 'created_at',
label: '创建时间', label: '创建时间',
width: 180, width: 180,
formatter: (row: PackageSeriesResponse) => formatDateTime(row.created_at) formatter: (row: PackageSeriesResponse) => formatDateTime(row.created_at)
}, },
{
prop: 'updated_at',
label: '更新时间',
width: 180,
formatter: (row: PackageSeriesResponse) => formatDateTime(row.updated_at)
},
{ {
prop: 'operation', prop: 'operation',
label: '操作', label: '操作',
width: 150, width: 200,
fixed: 'right', fixed: 'right',
formatter: (row: PackageSeriesResponse) => { formatter: (row: PackageSeriesResponse) => {
const buttons = [] const buttons = []
// 详情按钮
buttons.push(
h(ArtButtonTable, {
type: 'view',
onClick: () => handleViewDetail(row)
})
)
if (hasAuth('package_series:edit')) { if (hasAuth('package_series:edit')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
@@ -293,7 +721,8 @@
page: pagination.page, page: pagination.page,
page_size: pagination.page_size, page_size: pagination.page_size,
series_name: searchForm.series_name || undefined, series_name: searchForm.series_name || undefined,
status: searchForm.status || undefined status: searchForm.status || undefined,
enable_one_time_commission: searchForm.enable_one_time_commission ?? undefined
} }
const res = await PackageSeriesService.getPackageSeries(params) const res = await PackageSeriesService.getPackageSeries(params)
if (res.code === 0) { if (res.code === 0) {
@@ -347,11 +776,80 @@
form.series_code = row.series_code form.series_code = row.series_code
form.series_name = row.series_name form.series_name = row.series_name
form.description = row.description || '' form.description = row.description || ''
// 填充佣金配置
if (row.one_time_commission_config) {
// 转换梯度配置:分 -> 元
const convertedTiers = (row.one_time_commission_config.tiers || []).map((tier: any) => ({
...tier,
// 只有销售额维度的阈值需要转换
threshold:
tier.dimension === 'sales_amount' && tier.threshold != null
? tier.threshold / 100
: tier.threshold,
// 佣金金额转换
amount: tier.amount != null ? tier.amount / 100 : tier.amount
}))
form.one_time_commission_config = {
enable: row.one_time_commission_config.enable || false,
commission_type: row.one_time_commission_config.commission_type || 'fixed',
// 固定佣金金额:分 -> 元
commission_amount:
row.one_time_commission_config.commission_amount != null
? row.one_time_commission_config.commission_amount / 100
: undefined,
// 触发阈值:分 -> 元
threshold:
row.one_time_commission_config.threshold != null
? row.one_time_commission_config.threshold / 100
: undefined,
trigger_type: row.one_time_commission_config.trigger_type || 'first_recharge',
tiers: convertedTiers,
enable_force_recharge: row.one_time_commission_config.enable_force_recharge || false,
// 强充金额:分 -> 元
force_amount:
row.one_time_commission_config.force_amount != null
? row.one_time_commission_config.force_amount / 100
: undefined,
force_calc_type: row.one_time_commission_config.force_calc_type || 'fixed',
validity_type: row.one_time_commission_config.validity_type || 'permanent',
validity_value: row.one_time_commission_config.validity_value
}
} else {
// 重置为默认值
form.one_time_commission_config = {
enable: false,
commission_type: 'fixed',
commission_amount: undefined,
threshold: undefined,
trigger_type: 'first_recharge',
tiers: [],
enable_force_recharge: false,
force_amount: undefined,
force_calc_type: 'fixed',
validity_type: 'permanent',
validity_value: undefined
}
}
} else { } else {
form.id = 0 form.id = 0
form.series_code = '' form.series_code = ''
form.series_name = '' form.series_name = ''
form.description = '' form.description = ''
form.one_time_commission_config = {
enable: false,
commission_type: 'fixed',
commission_amount: undefined,
threshold: undefined,
trigger_type: 'first_recharge',
tiers: [],
enable_force_recharge: false,
force_amount: undefined,
force_calc_type: 'fixed',
validity_type: 'permanent',
validity_value: undefined
}
} }
// 重置表单验证状态 // 重置表单验证状态
@@ -363,6 +861,10 @@
// 生成系列编码 // 生成系列编码
const handleGenerateSeriesCode = () => { const handleGenerateSeriesCode = () => {
form.series_code = generateSeriesCode() form.series_code = generateSeriesCode()
// 清除该字段的验证错误提示
nextTick(() => {
formRef.value?.clearValidate('series_code')
})
ElMessage.success('编码生成成功') ElMessage.success('编码生成成功')
} }
@@ -395,12 +897,84 @@
if (valid) { if (valid) {
submitLoading.value = true submitLoading.value = true
try { try {
const data = { // 构建请求数据
series_code: form.series_code, const data: any = {
series_name: form.series_name, series_name: form.series_name,
description: form.description || undefined description: form.description || undefined,
enable_one_time_commission: form.one_time_commission_config.enable
} }
// 新增模式下才包含 series_code
if (dialogType.value === 'add') {
data.series_code = form.series_code
}
// 构建佣金配置对象
const commissionConfig: any = {
enable: form.one_time_commission_config.enable
}
// 只有启用时才添加其他配置
if (form.one_time_commission_config.enable) {
commissionConfig.commission_type = form.one_time_commission_config.commission_type
// 触发阈值:元 -> 分
commissionConfig.threshold =
form.one_time_commission_config.threshold != null
? Math.round(form.one_time_commission_config.threshold * 100)
: undefined
commissionConfig.trigger_type = form.one_time_commission_config.trigger_type
// 添加固定佣金金额:元 -> 分
if (form.one_time_commission_config.commission_type === 'fixed') {
commissionConfig.commission_amount =
form.one_time_commission_config.commission_amount != null
? Math.round(form.one_time_commission_config.commission_amount * 100)
: undefined
}
// 添加梯度配置:元 -> 分
if (form.one_time_commission_config.commission_type === 'tiered') {
const convertedTiers = form.one_time_commission_config.tiers.map((tier: any) => ({
...tier,
// 只有销售额维度的阈值需要转换
threshold:
tier.dimension === 'sales_amount' && tier.threshold != null
? Math.round(tier.threshold * 100)
: tier.threshold,
// 佣金金额转换:元 -> 分
amount: tier.amount != null ? Math.round(tier.amount * 100) : tier.amount
}))
commissionConfig.tiers = convertedTiers.length > 0 ? convertedTiers : null
}
// 添加强充配置:元 -> 分
if (form.one_time_commission_config.enable_force_recharge) {
commissionConfig.enable_force_recharge = true
commissionConfig.force_amount =
form.one_time_commission_config.force_amount != null
? Math.round(form.one_time_commission_config.force_amount * 100)
: undefined
commissionConfig.force_calc_type = form.one_time_commission_config.force_calc_type
}
// 添加时效配置
commissionConfig.validity_type = form.one_time_commission_config.validity_type
if (form.one_time_commission_config.validity_type !== 'permanent') {
// relative 类型的 validity_value 需要转换为字符串
if (form.one_time_commission_config.validity_type === 'relative') {
commissionConfig.validity_value = String(
form.one_time_commission_config.validity_value
)
} else {
commissionConfig.validity_value = form.one_time_commission_config.validity_value
}
}
}
// 添加佣金配置到请求数据
data.one_time_commission_config = commissionConfig
if (dialogType.value === 'add') { if (dialogType.value === 'add') {
await PackageSeriesService.createPackageSeries(data) await PackageSeriesService.createPackageSeries(data)
ElMessage.success('新增成功') ElMessage.success('新增成功')
@@ -436,6 +1010,26 @@
console.error(error) console.error(error)
} }
} }
// 添加梯度
const addTier = () => {
form.one_time_commission_config.tiers.push({
threshold: undefined,
dimension: 'sales_count',
amount: undefined,
stat_scope: 'self'
})
}
// 删除梯度
const removeTier = (index: number) => {
form.one_time_commission_config.tiers.splice(index, 1)
}
// 查看详情
const handleViewDetail = (row: PackageSeriesResponse) => {
router.push(`/package-management/package-series/detail/${row.id}`)
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -0,0 +1,207 @@
<template>
<div class="series-assign-detail">
<ElCard shadow="never">
<!-- 页面头部 -->
<div class="detail-header">
<ElButton @click="handleBack">
<template #icon>
<ElIcon><ArrowLeft /></ElIcon>
</template>
返回
</ElButton>
<h2 class="detail-title">系列分配详情</h2>
</div>
<!-- 详情内容 -->
<DetailPage v-if="detailData" :sections="detailSections" :data="detailData" />
<!-- 加载中 -->
<div v-if="loading" class="loading-container">
<ElIcon class="is-loading"><Loading /></ElIcon>
<span>加载中...</span>
</div>
</ElCard>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, h } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage, ElTag } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { ShopSeriesAllocationService } from '@/api/modules'
import type { ShopSeriesAllocationResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'SeriesAssignDetail' })
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const detailData = ref<ShopSeriesAllocationResponse | null>(null)
// 详情页配置
const detailSections: DetailSection[] = [
{
title: '基本信息',
fields: [
{ label: 'ID', prop: 'id' },
{ label: '系列编码', prop: 'series_code' },
{ label: '系列名称', prop: 'series_name' },
{ label: '店铺名称', prop: 'shop_name' },
{
label: '分配者店铺',
formatter: (_, data) => {
if (data.allocator_shop_id === 0) {
return '平台'
}
return data.allocator_shop_name || '-'
}
},
{
label: '状态',
formatter: (_, data) => {
return data.status === 1 ? '启用' : '禁用'
}
},
{ label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },
{ label: '更新时间', prop: 'updated_at', formatter: (value) => formatDateTime(value) }
]
},
{
title: '一次性佣金配置',
fields: [
{
label: '启用状态',
formatter: (_, data) => {
return data.enable_one_time_commission ? '已启用' : '未启用'
}
},
{
label: '佣金金额上限',
formatter: (_, data) => {
if (!data.one_time_commission_amount) return '-'
return `¥${(data.one_time_commission_amount / 100).toFixed(2)}`
}
},
{
label: '触发阈值',
formatter: (_, data) => {
if (!data.one_time_commission_threshold) return '-'
return `¥${(data.one_time_commission_threshold / 100).toFixed(2)}`
}
},
{
label: '触发类型',
formatter: (_, data) => {
if (!data.one_time_commission_trigger) return '-'
const typeMap = {
first_recharge: '首次充值',
accumulated_recharge: '累计充值'
}
return typeMap[data.one_time_commission_trigger] || '-'
}
}
]
},
{
title: '强制充值配置',
fields: [
{
label: '启用状态',
formatter: (_, data) => {
return data.enable_force_recharge ? '已启用' : '未启用'
}
},
{
label: '强充金额',
formatter: (_, data) => {
if (!data.force_recharge_amount) return '-'
return `¥${(data.force_recharge_amount / 100).toFixed(2)}`
}
},
{
label: '强充触发类型',
formatter: (_, data) => {
if (!data.force_recharge_trigger_type) return '-'
const typeMap = {
1: '单次充值',
2: '累计充值'
}
return typeMap[data.force_recharge_trigger_type] || '-'
}
}
]
}
]
// 返回上一页
const handleBack = () => {
router.back()
}
// 获取详情数据
const fetchDetail = async () => {
const id = Number(route.params.id)
if (!id) {
ElMessage.error('缺少ID参数')
return
}
loading.value = true
try {
const res = await ShopSeriesAllocationService.getShopSeriesAllocationDetail(id)
if (res.code === 0) {
detailData.value = res.data
}
} catch (error) {
console.error(error)
ElMessage.error('获取详情失败')
} finally {
loading.value = false
}
}
onMounted(() => {
fetchDetail()
})
</script>
<style scoped lang="scss">
.series-assign-detail {
padding: 20px;
}
.detail-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--el-border-color-light);
.detail-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary);
.el-icon {
font-size: 32px;
}
}
</style>

View File

@@ -5,7 +5,8 @@
<ArtSearchBar <ArtSearchBar
v-model:filter="searchForm" v-model:filter="searchForm"
:items="searchFormItems" :items="searchFormItems"
:show-expand="false" :show-expand="true"
label-width="85"
@reset="handleReset" @reset="handleReset"
@search="handleSearch" @search="handleSearch"
></ArtSearchBar> ></ArtSearchBar>
@@ -44,11 +45,11 @@
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '新增系列分配' : '编辑系列分配'" :title="dialogType === 'add' ? '新增系列分配' : '编辑系列分配'"
width="650px" width="35%"
:close-on-click-modal="false" :close-on-click-modal="false"
@closed="handleDialogClosed" @closed="handleDialogClosed"
> >
<ElForm ref="formRef" :model="form" :rules="rules" label-width="160px"> <ElForm ref="formRef" :model="form" :rules="rules" label-width="130px">
<!-- 基本信息 --> <!-- 基本信息 -->
<ElFormItem label="选择套餐系列" prop="series_id" v-if="dialogType === 'add'"> <ElFormItem label="选择套餐系列" prop="series_id" v-if="dialogType === 'add'">
<ElSelect <ElSelect
@@ -84,37 +85,36 @@
</div> </div>
</div> </div>
<ElFormItem label="选择店铺" prop="shop_id" v-if="dialogType === 'add'"> <ElFormItem label="选择店铺" prop="shop_id" v-if="dialogType === 'add'">
<ElSelect <ElTreeSelect
v-model="form.shop_id" v-model="form.shop_id"
:data="shopTreeData"
:props="{ label: 'shop_name', value: 'id', children: 'children' }"
placeholder="请选择店铺" placeholder="请选择店铺"
style="width: 100%" style="width: 100%"
filterable filterable
remote
:remote-method="searchShop"
:loading="shopLoading"
clearable clearable
> :loading="shopLoading"
<ElOption check-strictly
v-for="shop in shopOptions" :render-after-expand="false"
:key="shop.id" />
:label="shop.shop_name"
:value="shop.id"
/>
</ElSelect>
</ElFormItem> </ElFormItem>
<!-- 一次性佣金配置 --> <!-- 一次性佣金配置 -->
<ElDivider content-position="left">一次性佣金配置</ElDivider> <div class="form-section-title">
<span class="title-text">一次性佣金配置</span>
</div>
<ElFormItem label="佣金金额上限()" prop="one_time_commission_amount"> <ElFormItem label="佣金金额上限()" prop="one_time_commission_amount">
<ElInputNumber <ElInputNumber
v-model="form.one_time_commission_amount" v-model="form.one_time_commission_amount"
:min="0" :min="0"
:precision="2"
:step="0.01"
:controls="false" :controls="false"
style="width: 100%" style="width: 100%"
placeholder="请输入该代理能拿的一次性佣金金额上限()" placeholder="请输入该代理能拿的一次性佣金金额上限()"
/> />
<div class="form-tip">该代理在此系列分配下能获得的一次性佣金金额上限单位</div> <div class="form-tip">该代理在此系列分配下能获得的一次性佣金金额上限单位</div>
</ElFormItem> </ElFormItem>
<ElFormItem label="启用一次性佣金"> <ElFormItem label="启用一次性佣金">
@@ -122,13 +122,15 @@
</ElFormItem> </ElFormItem>
<template v-if="form.enable_one_time_commission"> <template v-if="form.enable_one_time_commission">
<ElFormItem label="触发阈值()" prop="one_time_commission_threshold"> <ElFormItem label="触发阈值()" prop="one_time_commission_threshold">
<ElInputNumber <ElInputNumber
v-model="form.one_time_commission_threshold" v-model="form.one_time_commission_threshold"
:min="0" :min="0"
:precision="2"
:step="0.01"
:controls="false" :controls="false"
style="width: 100%" style="width: 100%"
placeholder="请输入触发阈值()" placeholder="请输入触发阈值()"
/> />
<div class="form-tip">达到此充值金额后触发一次性佣金</div> <div class="form-tip">达到此充值金额后触发一次性佣金</div>
</ElFormItem> </ElFormItem>
@@ -142,20 +144,24 @@
</template> </template>
<!-- 强制充值配置 --> <!-- 强制充值配置 -->
<ElDivider content-position="left">强制充值配置可选</ElDivider> <div class="form-section-title">
<span class="title-text">强制充值配置可选</span>
</div>
<ElFormItem label="启用强制充值"> <ElFormItem label="启用强制充值">
<ElSwitch v-model="form.enable_force_recharge" /> <ElSwitch v-model="form.enable_force_recharge" />
</ElFormItem> </ElFormItem>
<template v-if="form.enable_force_recharge"> <template v-if="form.enable_force_recharge">
<ElFormItem label="强充金额()" prop="force_recharge_amount"> <ElFormItem label="强充金额()" prop="force_recharge_amount">
<ElInputNumber <ElInputNumber
v-model="form.force_recharge_amount" v-model="form.force_recharge_amount"
:min="0" :min="0"
:precision="2"
:step="0.01"
:controls="false" :controls="false"
style="width: 100%" style="width: 100%"
placeholder="请输入强制充值金额()" placeholder="请输入强制充值金额()"
/> />
<div class="form-tip">用户需要达到的强制充值金额</div> <div class="form-tip">用户需要达到的强制充值金额</div>
</ElFormItem> </ElFormItem>
@@ -184,6 +190,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { h } from 'vue' import { h } from 'vue'
import { useRouter } from 'vue-router'
import { ShopSeriesAllocationService, PackageSeriesService, ShopService } from '@/api/modules' import { ShopSeriesAllocationService, PackageSeriesService, ShopService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElSwitch, ElTag } from 'element-plus' import { ElMessage, ElMessageBox, ElSwitch, ElTag } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
@@ -207,6 +214,7 @@
defineOptions({ name: 'SeriesAssign' }) defineOptions({ name: 'SeriesAssign' })
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
const router = useRouter()
const dialogVisible = ref(false) const dialogVisible = ref(false)
const loading = ref(false) const loading = ref(false)
@@ -217,13 +225,16 @@
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const seriesOptions = ref<PackageSeriesResponse[]>([]) const seriesOptions = ref<PackageSeriesResponse[]>([])
const shopOptions = ref<ShopResponse[]>([]) const shopOptions = ref<ShopResponse[]>([])
const shopTreeData = ref<ShopResponse[]>([])
const searchSeriesOptions = ref<PackageSeriesResponse[]>([]) const searchSeriesOptions = ref<PackageSeriesResponse[]>([])
const searchShopOptions = ref<ShopResponse[]>([]) const searchShopOptions = ref<ShopResponse[]>([])
const searchAllocatorShopOptions = ref<ShopResponse[]>([])
// 搜索表单初始值 // 搜索表单初始值
const initialSearchState = { const initialSearchState = {
shop_id: undefined as number | undefined, shop_id: undefined as number | undefined,
series_id: undefined as number | undefined, series_id: undefined as number | undefined,
allocator_shop_id: undefined as number | undefined,
status: undefined as number | undefined status: undefined as number | undefined
} }
@@ -233,7 +244,7 @@
// 搜索表单配置 // 搜索表单配置
const searchFormItems = computed<SearchFormItem[]>(() => [ const searchFormItems = computed<SearchFormItem[]>(() => [
{ {
label: '店铺', label: '被分配店铺',
prop: 'shop_id', prop: 'shop_id',
type: 'select', type: 'select',
config: { config: {
@@ -242,7 +253,7 @@
remote: true, remote: true,
remoteMethod: handleSearchShop, remoteMethod: handleSearchShop,
loading: shopLoading.value, loading: shopLoading.value,
placeholder: '请选择或搜索店铺' placeholder: '请选择或搜索被分配的店铺'
}, },
options: () => options: () =>
searchShopOptions.value.map((s) => ({ searchShopOptions.value.map((s) => ({
@@ -250,6 +261,28 @@
value: s.id value: s.id
})) }))
}, },
{
label: '分配者店铺',
prop: 'allocator_shop_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchAllocatorShop,
loading: shopLoading.value,
placeholder: '请选择或搜索分配者店铺'
},
options: () => [
{ label: '平台', value: 0 },
...searchAllocatorShopOptions.value.map((s) => ({
label: s.shop_name,
value: s.id
}))
]
},
{ {
label: '套餐系列', label: '套餐系列',
prop: 'series_id', prop: 'series_id',
@@ -268,6 +301,7 @@
value: s.id value: s.id
})) }))
}, },
{ {
label: '状态', label: '状态',
prop: 'status', prop: 'status',
@@ -292,13 +326,17 @@
// 列配置 // 列配置
const columnOptions = [ const columnOptions = [
{ label: 'ID', prop: 'id' }, { label: '系列编码', prop: 'series_code' },
{ label: '系列名称', prop: 'series_name' }, { label: '系列名称', prop: 'series_name' },
{ label: '店铺名称', prop: 'shop_name' }, { label: '店铺名称', prop: 'shop_name' },
{ label: '分配者店铺', prop: 'allocator_shop_name' }, { label: '分配者店铺', prop: 'allocator_shop_name' },
{ label: '一次性佣金金额', prop: 'one_time_commission_amount' }, { label: '一次性佣金金额', prop: 'one_time_commission_amount' },
{ label: '一次性佣金状态', prop: 'enable_one_time_commission' }, { label: '一次性佣金状态', prop: 'enable_one_time_commission' },
{ label: '触发类型', prop: 'one_time_commission_trigger' },
{ label: '触发阈值', prop: 'one_time_commission_threshold' },
{ label: '强制充值', prop: 'enable_force_recharge' }, { label: '强制充值', prop: 'enable_force_recharge' },
{ label: '强充金额', prop: 'force_recharge_amount' },
{ label: '强充触发类型', prop: 'force_recharge_trigger_type' },
{ label: '状态', prop: 'status' }, { label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' }, { label: '创建时间', prop: 'created_at' },
{ label: '操作', prop: 'operation' } { label: '操作', prop: 'operation' }
@@ -372,9 +410,10 @@
// 动态列配置 // 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [ const { columnChecks, columns } = useCheckedColumns(() => [
{ {
prop: 'id', prop: 'series_code',
label: 'ID', label: '系列编码',
width: 80 width: 200,
showOverflowTooltip: true
}, },
{ {
prop: 'series_name', prop: 'series_name',
@@ -384,20 +423,28 @@
{ {
prop: 'shop_name', prop: 'shop_name',
label: '店铺名称', label: '店铺名称',
minWidth: 180 minWidth: 140
}, },
{ {
prop: 'allocator_shop_name', prop: 'allocator_shop_name',
label: '分配者店铺', label: '分配者店铺',
minWidth: 150, minWidth: 120,
formatter: (row: ShopSeriesAllocationResponse) => { formatter: (row: ShopSeriesAllocationResponse) => {
// 如果是平台分配(allocator_shop_id为0),显示"平台"标签
if (row.allocator_shop_id === 0) {
return h(
'span',
{ style: 'color: #409eff; font-weight: bold' },
row.allocator_shop_name || '平台'
)
}
return row.allocator_shop_name || '-' return row.allocator_shop_name || '-'
} }
}, },
{ {
prop: 'one_time_commission_amount', prop: 'one_time_commission_amount',
label: '一次性佣金金额', label: '一次性佣金金额',
width: 150, width: 140,
formatter: (row: ShopSeriesAllocationResponse) => { formatter: (row: ShopSeriesAllocationResponse) => {
return h( return h(
'span', 'span',
@@ -418,6 +465,36 @@
) )
} }
}, },
{
prop: 'one_time_commission_trigger',
label: '触发类型',
width: 110,
formatter: (row: ShopSeriesAllocationResponse) => {
if (!row.one_time_commission_trigger) {
return '-'
}
const typeMap = {
first_recharge: '首次充值',
accumulated_recharge: '累计充值'
}
return typeMap[row.one_time_commission_trigger] || '-'
}
},
{
prop: 'one_time_commission_threshold',
label: '触发阈值',
width: 120,
formatter: (row: ShopSeriesAllocationResponse) => {
if (!row.one_time_commission_threshold) {
return '-'
}
return h(
'span',
{ style: 'color: #e6a23c' },
`¥${(row.one_time_commission_threshold / 100).toFixed(2)}`
)
}
},
{ {
prop: 'enable_force_recharge', prop: 'enable_force_recharge',
label: '强制充值', label: '强制充值',
@@ -430,6 +507,36 @@
) )
} }
}, },
{
prop: 'force_recharge_amount',
label: '强充金额',
width: 120,
formatter: (row: ShopSeriesAllocationResponse) => {
if (!row.force_recharge_amount) {
return '-'
}
return h(
'span',
{ style: 'color: #f56c6c; font-weight: bold' },
`¥${(row.force_recharge_amount / 100).toFixed(2)}`
)
}
},
{
prop: 'force_recharge_trigger_type',
label: '强充触发类型',
width: 120,
formatter: (row: ShopSeriesAllocationResponse) => {
if (!row.force_recharge_trigger_type) {
return '-'
}
const typeMap = {
1: '单次充值',
2: '累计充值'
}
return typeMap[row.force_recharge_trigger_type] || '-'
}
},
{ {
prop: 'status', prop: 'status',
label: '状态', label: '状态',
@@ -458,11 +565,19 @@
{ {
prop: 'operation', prop: 'operation',
label: '操作', label: '操作',
width: 150, width: 180,
fixed: 'right', fixed: 'right',
formatter: (row: ShopSeriesAllocationResponse) => { formatter: (row: ShopSeriesAllocationResponse) => {
const buttons = [] const buttons = []
// 详情按钮
buttons.push(
h(ArtButtonTable, {
type: 'view',
onClick: () => handleViewDetail(row)
})
)
if (hasAuth('series_assign:edit')) { if (hasAuth('series_assign:edit')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
@@ -486,11 +601,39 @@
} }
]) ])
// 构建树形结构数据
const buildTreeData = (items: ShopResponse[]) => {
const map = new Map<number, ShopResponse & { children?: ShopResponse[] }>()
const tree: ShopResponse[] = []
// 先将所有项放入 map
items.forEach((item) => {
map.set(item.id, { ...item, children: [] })
})
// 构建树形结构
items.forEach((item) => {
const node = map.get(item.id)!
if (item.parent_id && map.has(item.parent_id)) {
// 有父节点,添加到父节点的 children 中
const parent = map.get(item.parent_id)!
if (!parent.children) parent.children = []
parent.children.push(node)
} else {
// 没有父节点或父节点不存在,作为根节点
tree.push(node)
}
})
return tree
}
onMounted(() => { onMounted(() => {
loadSeriesOptions() loadSeriesOptions()
loadShopOptions() loadShopOptions()
loadSearchSeriesOptions() loadSearchSeriesOptions()
loadSearchShopOptions() loadSearchShopOptions()
loadSearchAllocatorShopOptions()
getTableData() getTableData()
}) })
@@ -517,20 +660,19 @@
} }
} }
// 加载店铺选项(用于新增对话框,默认加载10条 // 加载店铺选项(用于新增对话框,加载所有店铺并构建树形结构
const loadShopOptions = async (shopName?: string) => { const loadShopOptions = async () => {
shopLoading.value = true shopLoading.value = true
try { try {
const params: any = { // 加载所有店铺,不分页
const res = await ShopService.getShops({
page: 1, page: 1,
page_size: 10 page_size: 10000 // 使用较大的值获取所有店铺
} })
if (shopName) {
params.shop_name = shopName
}
const res = await ShopService.getShops(params)
if (res.code === 0) { if (res.code === 0) {
shopOptions.value = res.data.items || [] shopOptions.value = res.data.items || []
// 构建树形结构数据
shopTreeData.value = buildTreeData(shopOptions.value)
} }
} catch (error) { } catch (error) {
console.error('加载店铺选项失败:', error) console.error('加载店铺选项失败:', error)
@@ -567,6 +709,18 @@
} }
} }
// 加载搜索栏分配者店铺选项(默认加载10条)
const loadSearchAllocatorShopOptions = async () => {
try {
const res = await ShopService.getShops({ page: 1, page_size: 10 })
if (res.code === 0) {
searchAllocatorShopOptions.value = res.data.items || []
}
} catch (error) {
console.error('加载搜索栏分配者店铺选项失败:', error)
}
}
// 搜索系列(用于新增对话框) // 搜索系列(用于新增对话框)
const searchSeries = (query: string) => { const searchSeries = (query: string) => {
if (query) { if (query) {
@@ -576,15 +730,6 @@
} }
} }
// 搜索店铺(用于新增对话框)
const searchShop = (query: string) => {
if (query) {
loadShopOptions(query)
} else {
loadShopOptions()
}
}
// 搜索系列(用于搜索栏) // 搜索系列(用于搜索栏)
const handleSearchSeries = async (query: string) => { const handleSearchSeries = async (query: string) => {
if (!query) { if (!query) {
@@ -606,7 +751,7 @@
} }
} }
// 搜索店铺(用于搜索栏) // 搜索店铺(用于搜索栏-被分配的店铺)
const handleSearchShop = async (query: string) => { const handleSearchShop = async (query: string) => {
if (!query) { if (!query) {
loadSearchShopOptions() loadSearchShopOptions()
@@ -626,6 +771,26 @@
} }
} }
// 搜索分配者店铺(用于搜索栏)
const handleSearchAllocatorShop = async (query: string) => {
if (!query) {
loadSearchAllocatorShopOptions()
return
}
try {
const res = await ShopService.getShops({
page: 1,
page_size: 10,
shop_name: query
})
if (res.code === 0) {
searchAllocatorShopOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索分配者店铺失败:', error)
}
}
// 获取分配列表 // 获取分配列表
const getTableData = async () => { const getTableData = async () => {
loading.value = true loading.value = true
@@ -635,6 +800,7 @@
page_size: pagination.page_size, page_size: pagination.page_size,
shop_id: searchForm.shop_id || undefined, shop_id: searchForm.shop_id || undefined,
series_id: searchForm.series_id || undefined, series_id: searchForm.series_id || undefined,
allocator_shop_id: searchForm.allocator_shop_id !== undefined ? searchForm.allocator_shop_id : undefined,
status: searchForm.status || undefined status: searchForm.status || undefined
} }
const res = await ShopSeriesAllocationService.getShopSeriesAllocations(params) const res = await ShopSeriesAllocationService.getShopSeriesAllocations(params)
@@ -692,11 +858,16 @@
form.shop_name = row.shop_name form.shop_name = row.shop_name
form.allocator_shop_name = row.allocator_shop_name form.allocator_shop_name = row.allocator_shop_name
form.enable_one_time_commission = row.enable_one_time_commission form.enable_one_time_commission = row.enable_one_time_commission
form.one_time_commission_amount = row.one_time_commission_amount // 将分转换为元显示
form.one_time_commission_amount = row.one_time_commission_amount / 100
form.one_time_commission_threshold = row.one_time_commission_threshold form.one_time_commission_threshold = row.one_time_commission_threshold
? row.one_time_commission_threshold / 100
: undefined
form.one_time_commission_trigger = row.one_time_commission_trigger || 'first_recharge' form.one_time_commission_trigger = row.one_time_commission_trigger || 'first_recharge'
form.enable_force_recharge = row.enable_force_recharge form.enable_force_recharge = row.enable_force_recharge
form.force_recharge_amount = row.force_recharge_amount form.force_recharge_amount = row.force_recharge_amount
? row.force_recharge_amount / 100
: undefined
form.force_recharge_trigger_type = row.force_recharge_trigger_type form.force_recharge_trigger_type = row.force_recharge_trigger_type
} else { } else {
form.id = 0 form.id = 0
@@ -740,6 +911,11 @@
form.force_recharge_trigger_type = undefined form.force_recharge_trigger_type = undefined
} }
// 查看详情
const handleViewDetail = (row: ShopSeriesAllocationResponse) => {
router.push(`/package-management/series-assign/detail/${row.id}`)
}
// 删除分配 // 删除分配
const deleteAllocation = (row: ShopSeriesAllocationResponse) => { const deleteAllocation = (row: ShopSeriesAllocationResponse) => {
ElMessageBox.confirm( ElMessageBox.confirm(
@@ -773,21 +949,24 @@
if (valid) { if (valid) {
submitLoading.value = true submitLoading.value = true
try { try {
// 将元转换为分提交给后端
const data: any = { const data: any = {
one_time_commission_amount: form.one_time_commission_amount, one_time_commission_amount: Math.round(form.one_time_commission_amount * 100),
enable_one_time_commission: form.enable_one_time_commission, enable_one_time_commission: form.enable_one_time_commission,
enable_force_recharge: form.enable_force_recharge enable_force_recharge: form.enable_force_recharge
} }
// 如果启用了一次性佣金,加入相关字段 // 如果启用了一次性佣金,加入相关字段
if (form.enable_one_time_commission) { if (form.enable_one_time_commission) {
data.one_time_commission_threshold = form.one_time_commission_threshold data.one_time_commission_threshold = Math.round(
(form.one_time_commission_threshold || 0) * 100
)
data.one_time_commission_trigger = form.one_time_commission_trigger data.one_time_commission_trigger = form.one_time_commission_trigger
} }
// 如果启用了强制充值,加入相关字段 // 如果启用了强制充值,加入相关字段
if (form.enable_force_recharge) { if (form.enable_force_recharge) {
data.force_recharge_amount = form.force_recharge_amount data.force_recharge_amount = Math.round((form.force_recharge_amount || 0) * 100)
data.force_recharge_trigger_type = form.force_recharge_trigger_type data.force_recharge_trigger_type = form.force_recharge_trigger_type
} }

View File

@@ -132,7 +132,9 @@
<!-- 新增店铺时的初始账号信息 --> <!-- 新增店铺时的初始账号信息 -->
<template v-if="dialogType === 'add'"> <template v-if="dialogType === 'add'">
<ElDivider content-position="left">初始账号信息</ElDivider> <div class="form-section-title">
<span class="title-text">初始账号信息</span>
</div>
<ElRow :gutter="20"> <ElRow :gutter="20">
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="用户名" prop="init_username"> <ElFormItem label="用户名" prop="init_username">
@@ -181,9 +183,6 @@
</template> </template>
</ElDialog> </ElDialog>
<!-- 客户账号列表弹窗 -->
<CustomerAccountDialog v-model="customerAccountDialogVisible" :shop-id="currentShopId" />
<!-- 店铺操作右键菜单 --> <!-- 店铺操作右键菜单 -->
<ArtMenuRight <ArtMenuRight
ref="shopOperationMenuRef" ref="shopOperationMenuRef"
@@ -202,7 +201,7 @@
<!-- 当前默认角色列表 --> <!-- 当前默认角色列表 -->
<div class="default-roles-section"> <div class="default-roles-section">
<div class="section-header"> <div class="section-header">
<span>当前默认角色</span> <span style="color:white;">当前默认角色</span>
<ElButton type="primary" @click="showAddRoleDialog"> <ElButton type="primary" @click="showAddRoleDialog">
添加角色 添加角色
</ElButton> </ElButton>
@@ -299,6 +298,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { h } from 'vue' import { h } from 'vue'
import { useRouter } from 'vue-router'
import { import {
FormInstance, FormInstance,
ElMessage, ElMessage,
@@ -314,25 +314,24 @@
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue' import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue' import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import CustomerAccountDialog from '@/components/business/CustomerAccountDialog.vue'
import { ShopService, RoleService } from '@/api/modules' import { ShopService, RoleService } from '@/api/modules'
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import type { ShopResponse, ShopRoleResponse } from '@/types/api' import type { ShopResponse, ShopRoleResponse } from '@/types/api'
import { RoleType } from '@/types/api' import { RoleType } from '@/types/api'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants' import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
import { RoutesAlias } from '@/router/routesAlias'
defineOptions({ name: 'Shop' }) defineOptions({ name: 'Shop' })
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
const router = useRouter()
const dialogType = ref('add') const dialogType = ref('add')
const dialogVisible = ref(false) const dialogVisible = ref(false)
const customerAccountDialogVisible = ref(false)
const loading = ref(false) const loading = ref(false)
const submitLoading = ref(false) const submitLoading = ref(false)
const parentShopLoading = ref(false) const parentShopLoading = ref(false)
const currentShopId = ref<number>(0)
const parentShopList = ref<ShopResponse[]>([]) const parentShopList = ref<ShopResponse[]>([])
const searchParentShopList = ref<ShopResponse[]>([]) const searchParentShopList = ref<ShopResponse[]>([])
@@ -614,7 +613,7 @@
if (hasAuth('shop:look_customer')) { if (hasAuth('shop:look_customer')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
text: '查看客户', text: '账号列表',
onClick: () => viewCustomerAccounts(row) onClick: () => viewCustomerAccounts(row)
}) })
) )
@@ -884,8 +883,12 @@
// 查看客户账号 // 查看客户账号
const viewCustomerAccounts = (row: ShopResponse) => { const viewCustomerAccounts = (row: ShopResponse) => {
currentShopId.value = row.id // 跳转到账号列表页面,通过 query 参数区分店铺类型
customerAccountDialogVisible.value = true router.push({
name: 'EnterpriseCustomerAccounts',
params: { id: row.id },
query: { type: 'shop' }
})
} }
// 店铺操作菜单项配置 // 店铺操作菜单项配置