修改工单
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m26s

This commit is contained in:
sexygoat
2026-02-25 16:14:38 +08:00
parent ca3184857e
commit dccad819cf
20 changed files with 2163 additions and 1229 deletions

View File

@@ -422,7 +422,7 @@
"agent": "代理商管理", "agent": "代理商管理",
"customerAccount": "客户账号", "customerAccount": "客户账号",
"enterpriseCustomer": "企业客户", "enterpriseCustomer": "企业客户",
"enterpriseCustomerAccounts": "客户账号列表", "enterpriseCustomerAccounts": "关联账号列表",
"enterpriseCards": "企业卡管理", "enterpriseCards": "企业卡管理",
"customerCommission": "客户账号佣金" "customerCommission": "客户账号佣金"
}, },

View File

@@ -296,6 +296,15 @@ export const asyncRoutes: AppRouteRecord[] = [
roles: ['R_SUPER'] roles: ['R_SUPER']
} }
}, },
{
path: 'carrier-management',
name: 'CarrierManagement',
component: RoutesAlias.CarrierManagement,
meta: {
title: 'menus.account.carrierManagement',
keepAlive: true
}
},
{ {
path: 'user-center', path: 'user-center',
name: 'UserCenter', name: 'UserCenter',
@@ -611,98 +620,98 @@ export const asyncRoutes: AppRouteRecord[] = [
// } // }
// }, // },
// 物联网卡管理系统模块 // 物联网卡管理系统模块
{ // {
path: '/card-management', // path: '/card-management',
name: 'CardManagement', // name: 'CardManagement',
component: RoutesAlias.Home, // component: RoutesAlias.Home,
meta: { // meta: {
title: 'menus.cardManagement.title', // title: 'menus.cardManagement.title',
icon: '' // icon: ''
}, // },
children: [ // children: [
// { // {
// path: 'card-detail', // path: 'card-detail',
// name: 'CardDetail', // name: 'CardDetail',
// component: RoutesAlias.CardDetail, // component: RoutesAlias.CardDetail,
// meta: { // meta: {
// title: 'menus.cardManagement.cardDetail', // title: 'menus.cardManagement.cardDetail',
// keepAlive: true // keepAlive: true
// } // }
// }, // },
{ // {
path: 'single-card', // path: 'single-card',
name: 'SingleCard', // name: 'SingleCard',
component: RoutesAlias.SingleCard, // component: RoutesAlias.SingleCard,
meta: { // meta: {
title: 'menus.cardManagement.singleCard', // title: 'menus.cardManagement.singleCard',
keepAlive: true // keepAlive: true
} // }
}, // },
// { // {
// path: 'card-assign', // path: 'card-assign',
// name: 'CardAssign', // name: 'CardAssign',
// component: RoutesAlias.CardAssign, // component: RoutesAlias.CardAssign,
// meta: { // meta: {
// title: 'menus.cardManagement.cardAssign', // title: 'menus.cardManagement.cardAssign',
// keepAlive: true // keepAlive: true
// } // }
// }, // },
// { // {
// path: 'card-shutdown', // path: 'card-shutdown',
// name: 'CardShutdown', // name: 'CardShutdown',
// component: RoutesAlias.CardShutdown, // component: RoutesAlias.CardShutdown,
// meta: { // meta: {
// title: 'menus.cardManagement.cardShutdown', // title: 'menus.cardManagement.cardShutdown',
// keepAlive: true // keepAlive: true
// } // }
// }, // },
// { // {
// path: 'my-cards', // path: 'my-cards',
// name: 'MyCards', // name: 'MyCards',
// component: RoutesAlias.MyCards, // component: RoutesAlias.MyCards,
// meta: { // meta: {
// title: 'menus.cardManagement.myCards', // title: 'menus.cardManagement.myCards',
// keepAlive: true // keepAlive: true
// } // }
// }, // },
// { // {
// path: 'card-transfer', // path: 'card-transfer',
// name: 'CardTransfer', // name: 'CardTransfer',
// component: RoutesAlias.CardTransfer, // component: RoutesAlias.CardTransfer,
// meta: { // meta: {
// title: 'menus.cardManagement.cardTransfer', // title: 'menus.cardManagement.cardTransfer',
// keepAlive: true // keepAlive: true
// } // }
// }, // },
// { // {
// path: 'card-replacement', // path: 'card-replacement',
// name: 'CardReplacement', // name: 'CardReplacement',
// component: RoutesAlias.CardReplacement, // component: RoutesAlias.CardReplacement,
// meta: { // meta: {
// title: 'menus.cardManagement.cardReplacement', // title: 'menus.cardManagement.cardReplacement',
// keepAlive: true // keepAlive: true
// } // }
// }, // },
// { // {
// path: 'package-gift', // path: 'package-gift',
// name: 'PackageGift', // name: 'PackageGift',
// component: RoutesAlias.PackageGift, // component: RoutesAlias.PackageGift,
// meta: { // meta: {
// title: 'menus.cardManagement.packageGift', // title: 'menus.cardManagement.packageGift',
// keepAlive: true // keepAlive: true
// } // }
// }, // },
// { // {
// path: 'card-change-card', // path: 'card-change-card',
// name: 'CardChangeCard', // name: 'CardChangeCard',
// component: RoutesAlias.CardChangeCard, // component: RoutesAlias.CardChangeCard,
// meta: { // meta: {
// title: 'menus.cardManagement.cardChange', // title: 'menus.cardManagement.cardChange',
// keepAlive: true // keepAlive: true
// } // }
// } // }
] // ]
}, // },
{ {
path: '/package-management', path: '/package-management',
name: 'PackageManagement', name: 'PackageManagement',
@@ -969,6 +978,15 @@ export const asyncRoutes: AppRouteRecord[] = [
icon: '' icon: ''
}, },
children: [ children: [
{
path: 'single-card',
name: 'SingleCard',
component: RoutesAlias.SingleCard,
meta: {
title: 'menus.cardManagement.singleCard',
keepAlive: true
}
},
{ {
path: 'iot-card-management', path: 'iot-card-management',
name: 'StandaloneCardList', name: 'StandaloneCardList',
@@ -1072,15 +1090,6 @@ export const asyncRoutes: AppRouteRecord[] = [
// keepAlive: true // keepAlive: true
// } // }
// }, // },
{
path: 'carrier-management',
name: 'CarrierManagement',
component: RoutesAlias.CarrierManagement,
meta: {
title: 'menus.account.carrierManagement',
keepAlive: true
}
},
{ {
path: 'orders', path: 'orders',
name: 'OrderManagement', name: 'OrderManagement',
@@ -1089,7 +1098,7 @@ export const asyncRoutes: AppRouteRecord[] = [
title: 'menus.account.orders', title: 'menus.account.orders',
keepAlive: true keepAlive: true
} }
}, }
// { // {
// path: 'my-account', // path: 'my-account',
// name: 'MyAccount', // name: 'MyAccount',
@@ -1099,56 +1108,56 @@ export const asyncRoutes: AppRouteRecord[] = [
// keepAlive: true // keepAlive: true
// } // }
// } // }
]
},
{
path: '/commission',
name: 'CommissionManagement',
component: RoutesAlias.Home,
meta: {
title: 'menus.commission.menu.management',
icon: ''
},
children: [
{ {
path: 'commission', path: 'withdrawal-approval',
name: 'CommissionManagement', name: 'WithdrawalApproval',
component: '', component: RoutesAlias.WithdrawalApproval,
meta: { meta: {
title: 'menus.commission.menu.management', title: 'menus.commission.menu.withdrawal',
icon: '' keepAlive: true,
}, roles: ['R_SUPER', 'R_ADMIN']
children: [ }
{ },
path: 'withdrawal-approval', {
name: 'WithdrawalApproval', path: 'withdrawal-settings',
component: RoutesAlias.WithdrawalApproval, name: 'CommissionWithdrawalSettings',
meta: { component: RoutesAlias.CommissionWithdrawalSettings,
title: 'menus.commission.menu.withdrawal', meta: {
keepAlive: true, title: 'menus.commission.menu.withdrawalSettings',
roles: ['R_SUPER', 'R_ADMIN'] keepAlive: true,
} roles: ['R_SUPER', 'R_ADMIN']
}, }
{ },
path: 'withdrawal-settings', {
name: 'CommissionWithdrawalSettings', path: 'my-commission',
component: RoutesAlias.CommissionWithdrawalSettings, name: 'MyCommission',
meta: { component: RoutesAlias.MyCommission,
title: 'menus.commission.menu.withdrawalSettings', meta: {
keepAlive: true, title: 'menus.commission.menu.myCommission',
roles: ['R_SUPER', 'R_ADMIN'] keepAlive: true,
} roles: ['R_AGENT']
}, }
{ },
path: 'my-commission', {
name: 'MyCommission', path: 'agent-commission',
component: RoutesAlias.MyCommission, name: 'AgentCommission',
meta: { component: RoutesAlias.AgentCommission,
title: 'menus.commission.menu.myCommission', meta: {
keepAlive: true, title: 'menus.commission.menu.agentCommission',
roles: ['R_AGENT'] keepAlive: true,
} roles: ['R_SUPER', 'R_ADMIN']
}, }
{
path: 'agent-commission',
name: 'AgentCommission',
component: RoutesAlias.AgentCommission,
meta: {
title: 'menus.commission.menu.agentCommission',
keepAlive: true,
roles: ['R_SUPER', 'R_ADMIN']
}
}
]
} }
] ]
} }

View File

@@ -68,44 +68,6 @@
> >
<ElOption label="超级管理员" :value="1" /> <ElOption label="超级管理员" :value="1" />
<ElOption label="平台用户" :value="2" /> <ElOption label="平台用户" :value="2" />
<ElOption label="代理账号" :value="3" />
<ElOption label="企业账号" :value="4" />
</ElSelect>
</ElFormItem>
<ElFormItem v-if="dialogType === 'add' && formData.user_type === 3" label="关联店铺" prop="shop_id">
<ElSelect
v-model="formData.shop_id"
placeholder="请输入店铺名称搜索"
style="width: 100%"
filterable
remote
:remote-method="handleShopSearch"
clearable
>
<ElOption
v-for="shop in shopList"
:key="shop.id"
:label="shop.shop_name"
:value="shop.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem v-if="dialogType === 'add' && formData.user_type === 4" label="关联企业" prop="enterprise_id">
<ElSelect
v-model="formData.enterprise_id"
placeholder="请输入企业名称搜索"
style="width: 100%"
filterable
remote
:remote-method="handleEnterpriseSearch"
clearable
>
<ElOption
v-for="enterprise in enterpriseList"
:key="enterprise.id"
:label="enterprise.enterprise_name"
:value="enterprise.id"
/>
</ElSelect> </ElSelect>
</ElFormItem> </ElFormItem>
</ElForm> </ElForm>
@@ -124,6 +86,9 @@
<span class="dialog-title">分配角色</span> <span class="dialog-title">分配角色</span>
<div class="account-info"> <div class="account-info">
<span class="account-name">{{ currentAccountName }}</span> <span class="account-name">{{ currentAccountName }}</span>
<ElTag v-if="currentAccountType === 3" type="warning" size="small" style="margin-left: 8px">
代理账号只能分配一个客户角色
</ElTag>
</div> </div>
</div> </div>
</template> </template>
@@ -142,18 +107,11 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
<ElCheckboxGroup v-model="rolesToAdd" class="role-list"> <ElCheckboxGroup v-model="rolesToAdd" class="role-list">
<div <div v-for="role in filteredAvailableRoles" :key="role.ID" class="role-item">
v-for="role in filteredAvailableRoles"
:key="role.ID"
class="role-item"
>
<ElCheckbox :label="role.ID" :disabled="selectedRoles.includes(role.ID)"> <ElCheckbox :label="role.ID" :disabled="selectedRoles.includes(role.ID)">
<span class="role-info"> <span class="role-info">
<span>{{ role.role_name }}</span> <span>{{ role.role_name }}</span>
<ElTag <ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
:type="role.role_type === 1 ? 'primary' : 'success'"
size="small"
>
{{ role.role_type === 1 ? '平台角色' : '客户角色' }} {{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag> </ElTag>
</span> </span>
@@ -196,19 +154,11 @@
> >
<span class="role-info"> <span class="role-info">
<span>{{ role.role_name }}</span> <span>{{ role.role_name }}</span>
<ElTag <ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
:type="role.role_type === 1 ? 'primary' : 'success'"
size="small"
>
{{ role.role_type === 1 ? '平台角色' : '客户角色' }} {{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag> </ElTag>
</span> </span>
<ElButton <ElButton type="danger" size="small" link @click="removeSingleRole(role.ID)">
type="danger"
size="small"
link
@click="removeSingleRole(role.ID)"
>
移除 移除
</ElButton> </ElButton>
</div> </div>
@@ -256,6 +206,7 @@
const roleSubmitLoading = ref(false) const roleSubmitLoading = ref(false)
const currentAccountId = ref<number>(0) const currentAccountId = ref<number>(0)
const currentAccountName = ref<string>('') const currentAccountName = ref<string>('')
const currentAccountType = ref<number>(0)
const selectedRoles = ref<number[]>([]) const selectedRoles = ref<number[]>([])
const allRoles = ref<PlatformRole[]>([]) const allRoles = ref<PlatformRole[]>([])
const rolesToAdd = ref<number[]>([]) const rolesToAdd = ref<number[]>([])
@@ -517,7 +468,7 @@
{ {
prop: 'operation', prop: 'operation',
label: '操作', label: '操作',
width: 200, width: 240,
fixed: 'right', fixed: 'right',
formatter: (row: any) => { formatter: (row: any) => {
const buttons = [] const buttons = []
@@ -525,7 +476,7 @@
if (hasAuth('account:patch_role')) { if (hasAuth('account:patch_role')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
icon: '&#xe72b;', text: '分配角色',
onClick: () => showRoleDialog(row) onClick: () => showRoleDialog(row)
}) })
) )
@@ -534,7 +485,7 @@
if (hasAuth('account:edit')) { if (hasAuth('account:edit')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'edit', text: '编辑',
onClick: () => showDialog('edit', row) onClick: () => showDialog('edit', row)
}) })
) )
@@ -543,7 +494,7 @@
if (hasAuth('account:delete')) { if (hasAuth('account:delete')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'delete', text: '删除',
onClick: () => deleteAccount(row) onClick: () => deleteAccount(row)
}) })
) )
@@ -593,11 +544,23 @@
} }
} }
// 计算属性:过滤后的可分配角色 // 计算属性:过滤后的可分配角色(根据账号类型过滤)
const filteredAvailableRoles = computed(() => { const filteredAvailableRoles = computed(() => {
if (!leftRoleFilter.value) return allRoles.value let roles = allRoles.value
// 根据账号类型过滤角色
if (currentAccountType.value === 3) {
// 代理账号:只显示客户角色
roles = roles.filter((role) => role.role_type === 2)
} else if (currentAccountType.value === 2) {
// 平台用户:只显示平台角色
roles = roles.filter((role) => role.role_type === 1)
}
// 根据搜索关键词过滤
if (!leftRoleFilter.value) return roles
const keyword = leftRoleFilter.value.toLowerCase() const keyword = leftRoleFilter.value.toLowerCase()
return allRoles.value.filter((role) => role.role_name.toLowerCase().includes(keyword)) return roles.filter((role) => role.role_name.toLowerCase().includes(keyword))
}) })
// 计算属性:过滤后的已分配角色 // 计算属性:过滤后的已分配角色
@@ -612,6 +575,7 @@
const showRoleDialog = async (row: any) => { const showRoleDialog = async (row: any) => {
currentAccountId.value = row.id currentAccountId.value = row.id
currentAccountName.value = row.username currentAccountName.value = row.username
currentAccountType.value = row.user_type
selectedRoles.value = [] selectedRoles.value = []
rolesToAdd.value = [] rolesToAdd.value = []
leftRoleFilter.value = '' leftRoleFilter.value = ''
@@ -641,18 +605,31 @@
if (rolesToAdd.value.length === 0) return if (rolesToAdd.value.length === 0) return
try { try {
// 将选中的角色添加到已分配列表 let newRoles: number[]
const newRoles = [...new Set([...selectedRoles.value, ...rolesToAdd.value])]
// 代理账号只能分配一个角色,会覆盖之前的角色
if (currentAccountType.value === 3) {
if (rolesToAdd.value.length > 1) {
ElMessage.warning('代理账号只能分配一个角色')
return
}
// 只保留新选择的一个角色
newRoles = rolesToAdd.value
} else {
// 其他账号类型可以分配多个角色
newRoles = [...new Set([...selectedRoles.value, ...rolesToAdd.value])]
}
await AccountService.assignRolesToAccount(currentAccountId.value, newRoles) await AccountService.assignRolesToAccount(currentAccountId.value, newRoles)
selectedRoles.value = newRoles selectedRoles.value = newRoles
rolesToAdd.value = [] rolesToAdd.value = []
ElMessage.success('角色添加成功') ElMessage.success('角色分配成功')
// 刷新列表以更新角色显示 // 刷新列表以更新角色显示
await getAccountList() await getAccountList()
} catch (error) { } catch (error) {
console.error('添加角色失败:', error) console.error('分配角色失败:', error)
} }
} }

View File

@@ -39,25 +39,39 @@
<!-- 授权详情对话框 --> <!-- 授权详情对话框 -->
<ElDialog v-model="detailDialogVisible" title="授权详情" width="700px"> <ElDialog v-model="detailDialogVisible" title="授权详情" width="700px">
<ElDescriptions v-if="currentRecord" :column="2" border> <ElDescriptions v-if="currentRecord" :column="2" border>
<ElDescriptionsItem label="企业ID">{{ currentRecord.enterprise_id }}</ElDescriptionsItem> <ElDescriptionsItem label="企业ID">{{
<ElDescriptionsItem label="企业名称">{{ currentRecord.enterprise_name }}</ElDescriptionsItem> currentRecord.enterprise_id
}}</ElDescriptionsItem>
<ElDescriptionsItem label="企业名称">{{
currentRecord.enterprise_name
}}</ElDescriptionsItem>
<ElDescriptionsItem label="卡ID">{{ currentRecord.card_id }}</ElDescriptionsItem> <ElDescriptionsItem label="卡ID">{{ currentRecord.card_id }}</ElDescriptionsItem>
<ElDescriptionsItem label="ICCID">{{ currentRecord.iccid }}</ElDescriptionsItem> <ElDescriptionsItem label="ICCID">{{ currentRecord.iccid }}</ElDescriptionsItem>
<ElDescriptionsItem label="手机号">{{ currentRecord.msisdn || '--' }}</ElDescriptionsItem> <ElDescriptionsItem label="手机号">{{
<ElDescriptionsItem label="授权人ID">{{ currentRecord.authorized_by }}</ElDescriptionsItem> currentRecord.msisdn || '--'
<ElDescriptionsItem label="授权人">{{ currentRecord.authorizer_name }}</ElDescriptionsItem> }}</ElDescriptionsItem>
<ElDescriptionsItem label="授权人ID">{{
currentRecord.authorized_by
}}</ElDescriptionsItem>
<ElDescriptionsItem label="授权人">{{
currentRecord.authorizer_name
}}</ElDescriptionsItem>
<ElDescriptionsItem label="授权人类型"> <ElDescriptionsItem label="授权人类型">
<ElTag :type="getAuthorizerTypeTag(currentRecord.authorizer_type)"> <ElTag :type="getAuthorizerTypeTag(currentRecord.authorizer_type)">
{{ getAuthorizerTypeText(currentRecord.authorizer_type) }} {{ getAuthorizerTypeText(currentRecord.authorizer_type) }}
</ElTag> </ElTag>
</ElDescriptionsItem> </ElDescriptionsItem>
<ElDescriptionsItem label="授权时间" :span="2">{{ formatDateTime(currentRecord.authorized_at) }}</ElDescriptionsItem> <ElDescriptionsItem label="授权时间" :span="2">{{
formatDateTime(currentRecord.authorized_at)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="状态" :span="2"> <ElDescriptionsItem label="状态" :span="2">
<ElTag :type="getStatusTag(currentRecord.status)"> <ElTag :type="getStatusTag(currentRecord.status)">
{{ getStatusText(currentRecord.status) }} {{ getStatusText(currentRecord.status) }}
</ElTag> </ElTag>
</ElDescriptionsItem> </ElDescriptionsItem>
<ElDescriptionsItem label="备注" :span="2">{{ currentRecord.remark || '--' }}</ElDescriptionsItem> <ElDescriptionsItem label="备注" :span="2">{{
currentRecord.remark || '--'
}}</ElDescriptionsItem>
</ElDescriptions> </ElDescriptions>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
@@ -311,7 +325,7 @@
if (hasAuth('authorization_records:update_remark')) { if (hasAuth('authorization_records:update_remark')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'edit', text: '编辑',
onClick: () => showRemarkDialog(row) onClick: () => showRemarkDialog(row)
}) })
) )

View File

@@ -188,26 +188,6 @@
@close="handleRecallDialogClose" @close="handleRecallDialogClose"
> >
<ElForm ref="recallFormRef" :model="recallForm" :rules="recallRules" label-width="120px"> <ElForm ref="recallFormRef" :model="recallForm" :rules="recallRules" label-width="120px">
<ElFormItem label="来源店铺" prop="from_shop_id">
<ElSelect
v-model="recallForm.from_shop_id"
placeholder="请选择或搜索来源店铺"
filterable
remote
reserve-keyword
:remote-method="searchFromShops"
:loading="fromShopLoading"
clearable
style="width: 100%"
>
<ElOption
v-for="shop in fromShopList"
:key="shop.id"
:label="shop.shop_name"
:value="shop.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="选卡方式" prop="selection_type"> <ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="recallForm.selection_type"> <ElRadioGroup v-model="recallForm.selection_type">
<ElRadio label="list">ICCID列表</ElRadio> <ElRadio label="list">ICCID列表</ElRadio>
@@ -755,7 +735,6 @@
// 批量回收表单 // 批量回收表单
const recallForm = reactive<Partial<RecallStandaloneCardsRequest>>({ const recallForm = reactive<Partial<RecallStandaloneCardsRequest>>({
selection_type: 'list', selection_type: 'list',
from_shop_id: undefined,
iccids: [], iccids: [],
iccid_start: '', iccid_start: '',
iccid_end: '', iccid_end: '',
@@ -766,7 +745,6 @@
// 批量回收表单验证规则 // 批量回收表单验证规则
const recallRules = reactive<FormRules>({ const recallRules = reactive<FormRules>({
from_shop_id: [{ required: true, message: '请选择来源店铺', trigger: 'change' }],
selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }], selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }],
iccid_start: [ iccid_start: [
{ {
@@ -1285,7 +1263,6 @@
recallDialogVisible.value = true recallDialogVisible.value = true
Object.assign(recallForm, { Object.assign(recallForm, {
selection_type: 'list', selection_type: 'list',
from_shop_id: undefined,
iccids: selectedCards.value.map((card) => card.iccid), iccids: selectedCards.value.map((card) => card.iccid),
iccid_start: '', iccid_start: '',
iccid_end: '', iccid_end: '',
@@ -1293,8 +1270,6 @@
batch_no: '', batch_no: '',
remark: '' remark: ''
}) })
// 加载默认店铺列表
loadFromShops()
if (recallFormRef.value) { if (recallFormRef.value) {
recallFormRef.value.resetFields() recallFormRef.value.resetFields()
} }
@@ -1378,7 +1353,6 @@
// 根据选卡方式构建请求参数 // 根据选卡方式构建请求参数
const params: Partial<RecallStandaloneCardsRequest> = { const params: Partial<RecallStandaloneCardsRequest> = {
selection_type: recallForm.selection_type!, selection_type: recallForm.selection_type!,
from_shop_id: recallForm.from_shop_id!,
remark: recallForm.remark remark: recallForm.remark
} }

View File

@@ -1,45 +1,89 @@
<template> <template>
<ArtTableFullScreen> <ArtTableFullScreen>
<div class="single-card-page" id="table-full-screen"> <div class="single-card-page" id="table-full-screen">
<!-- ICCID查询区域 -->
<ElCard shadow="never" class="search-card" style="margin-bottom: 16px">
<template #header>
<span>ICCID查询</span>
</template>
<div class="iccid-search">
<ElInput
v-model="searchIccid"
placeholder="请输入ICCID"
clearable
style="width: 400px; margin-right: 16px"
@keyup.enter="handleSearchCard"
>
<template #prepend>ICCID</template>
</ElInput>
<ElButton type="primary" @click="handleSearchCard" :loading="loading">查询</ElButton>
</div>
<!-- 格式化显示的ICCID -->
<div v-if="formattedIccid" class="formatted-iccid">
{{ formattedIccid }}
</div>
</ElCard>
<!-- 网卡信息卡片 --> <!-- 网卡信息卡片 -->
<ElCard shadow="never" class="card-info-card" style="margin-bottom: 16px"> <ElCard shadow="never" class="card-info-card" style="margin-bottom: 16px">
<template #header> <template #header>
<span>网卡信息</span> <span>网卡信息</span>
</template> </template>
<ElForm :model="cardInfo" label-width="120px" :inline="false"> <div v-if="!hasSearched" class="empty-state">
<p style="text-align: center; color: var(--el-text-color-secondary); padding: 40px">
请在上方输入ICCID进行查询
</p>
</div>
<ElForm v-else :model="cardInfo" label-width="120px" :inline="false">
<ElRow :gutter="24"> <ElRow :gutter="24">
<ElCol :span="8"> <ElCol :span="8">
<ElFormItem label="ICCID:"> <ElFormItem label="ICCID:">
<span>{{ cardInfo.iccid }}</span> <span>{{ cardInfo.iccid || '-' }}</span>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="8"> <ElCol :span="8">
<ElFormItem label="IMSI:"> <ElFormItem label="IMSI:">
<span>{{ cardInfo.imsi }}</span> <span>{{ cardInfo.imsi || '-' }}</span>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="8"> <ElCol :span="8">
<ElFormItem label="手机号码:"> <ElFormItem label="手机号码:">
<span>{{ cardInfo.msisdn }}</span> <span>{{ cardInfo.msisdn || '-' }}</span>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
</ElRow> </ElRow>
<ElRow :gutter="24"> <ElRow :gutter="24">
<ElCol :span="8"> <ElCol :span="8">
<ElFormItem label="运营商:"> <ElFormItem label="运营商:">
<ElTag :type="getOperatorTagType(cardInfo.operator)">{{ <ElTag v-if="cardInfo.operatorName" :type="getOperatorTagType(cardInfo.operator)">
cardInfo.operatorName {{ cardInfo.operatorName }}
}}</ElTag> </ElTag>
<span v-else>-</span>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="8"> <ElCol :span="8">
<ElFormItem label="网络类型:"> <ElFormItem label="网络类型:">
<span>{{ cardInfo.networkType }}</span> <span>{{ cardInfo.networkType || '-' }}</span>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="8"> <ElCol :span="8">
<ElFormItem label="状态:"> <ElFormItem label="状态:">
<ElTag :type="getStatusTagType(cardInfo.status)">{{ cardInfo.statusName }}</ElTag> <ElTag v-if="cardInfo.statusName" :type="getStatusTagType(cardInfo.status)">
{{ cardInfo.statusName }}
</ElTag>
<span v-else>-</span>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="24">
<ElCol :span="8">
<ElFormItem label="激活时间:">
<span>{{ cardInfo.activatedDate || '-' }}</span>
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="过期时间:">
<span>{{ cardInfo.expiryDate || '-' }}</span>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
</ElRow> </ElRow>
@@ -47,7 +91,7 @@
</ElCard> </ElCard>
<!-- 操作区域 --> <!-- 操作区域 -->
<ElCard shadow="never" class="operation-card" style="margin-bottom: 16px"> <ElCard v-if="hasSearched" shadow="never" class="operation-card" style="margin-bottom: 16px">
<template #header> <template #header>
<span>操作区域</span> <span>操作区域</span>
</template> </template>
@@ -66,7 +110,7 @@
</ElCard> </ElCard>
<!-- 流量信息 --> <!-- 流量信息 -->
<ElCard shadow="never" class="traffic-card" style="margin-bottom: 16px"> <ElCard v-if="hasSearched" shadow="never" class="traffic-card" style="margin-bottom: 16px">
<template #header> <template #header>
<span>流量信息</span> <span>流量信息</span>
</template> </template>
@@ -100,7 +144,7 @@
</ElCard> </ElCard>
<!-- 使用记录 --> <!-- 使用记录 -->
<ElCard shadow="never" class="art-table-card"> <ElCard v-if="hasSearched" shadow="never" class="art-table-card">
<template #header> <template #header>
<span>使用记录</span> <span>使用记录</span>
<ElButton style="float: right" @click="refreshUsageRecords">刷新</ElButton> <ElButton style="float: right" @click="refreshUsageRecords">刷新</ElButton>
@@ -158,6 +202,7 @@
import { ElTag, ElMessageBox, ElMessage, FormInstance } from 'element-plus' import { ElTag, ElMessageBox, ElMessage, FormInstance } from 'element-plus'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import type { FormRules } from 'element-plus' import type { FormRules } from 'element-plus'
import { CardService } from '@/api/modules'
defineOptions({ name: 'SingleCard' }) defineOptions({ name: 'SingleCard' })
@@ -165,26 +210,36 @@
const rechargeDialogVisible = ref(false) const rechargeDialogVisible = ref(false)
const route = useRoute() const route = useRoute()
// 网卡信息 // ICCID搜索相关
const cardInfo = reactive({ const searchIccid = ref('')
iccid: '89860123456789012345', const hasSearched = ref(false)
imsi: '460012345678901',
msisdn: '13800138001', // 格式化显示的ICCID4位一组用横杠分隔
operator: 'mobile', const formattedIccid = computed(() => {
operatorName: '中国移动', if (!cardInfo.iccid) return ''
networkType: '4G', return cardInfo.iccid.replace(/(\d{4})(?=\d)/g, '$1-')
status: '1',
statusName: '激活',
activatedDate: '2024-01-15',
expiryDate: '2025-01-15'
}) })
// 流量信息 // 网卡信息 - 默认为空
const cardInfo = reactive({
iccid: '',
imsi: '',
msisdn: '',
operator: '',
operatorName: '',
networkType: '',
status: '',
statusName: '',
activatedDate: '',
expiryDate: ''
})
// 流量信息 - 默认为空
const trafficInfo = reactive({ const trafficInfo = reactive({
totalTraffic: '10GB', totalTraffic: '0MB',
usedTraffic: '2.5GB', usedTraffic: '0MB',
remainingTraffic: '7.5GB', remainingTraffic: '0MB',
usagePercentage: '25' usagePercentage: '0'
}) })
const pagination = reactive({ const pagination = reactive({
@@ -193,25 +248,8 @@
total: 0 total: 0
}) })
// 使用记录 // 使用记录 - 默认为空
const usageRecords = ref([ const usageRecords = ref([])
{
id: 1,
date: '2024-11-07',
time: '14:30:25',
dataUsage: '125.6MB',
fee: '0.12',
location: '北京市朝阳区'
},
{
id: 2,
date: '2024-11-07',
time: '13:45:12',
dataUsage: '256.8MB',
fee: '0.26',
location: '北京市朝阳区'
}
])
// 充值表单 // 充值表单
const rechargeForm = reactive({ const rechargeForm = reactive({
@@ -394,40 +432,107 @@
pagination.total = usageRecords.value.length pagination.total = usageRecords.value.length
}) })
// 处理ICCID搜索
const handleSearchCard = async () => {
if (!searchIccid.value.trim()) {
ElMessage.warning('请输入ICCID')
return
}
await loadCardInfoByIccid(searchIccid.value.trim())
}
// 根据ICCID加载卡片信息 // 根据ICCID加载卡片信息
const loadCardInfoByIccid = async (iccid: string) => { const loadCardInfoByIccid = async (iccid: string) => {
loading.value = true loading.value = true
try { try {
// 这里应该调用API根据ICCID获取卡片详细信息 const response = await CardService.getIotCardDetailByIccid(iccid)
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
// 模拟更新卡片信息实际应该从API获取 if (response.code === 200 && response.data) {
Object.assign(cardInfo, { const data = response.data
iccid: iccid, hasSearched.value = true
imsi: '460012345678901',
msisdn: '13800138001',
operator: 'mobile',
operatorName: '中国移动',
networkType: '4G',
status: '1',
statusName: '激活',
activatedDate: '2024-01-15',
expiryDate: '2025-01-15'
})
ElMessage.success(`已加载ICCID ${iccid} 的详细信息`) // 更新网卡基本信息
} catch (error) { Object.assign(cardInfo, {
iccid: data.iccid || '',
imsi: data.imsi || '',
msisdn: data.msisdn || '',
operator: data.carrier_type || '',
operatorName: data.carrier_name || '',
networkType: data.network_type || '',
status: String(data.status || ''),
statusName: getStatusName(data.status),
activatedDate: data.activated_at || '',
expiryDate: data.expire_at || ''
})
// 更新流量信息
const totalMB = data.data_usage_mb || 0
const usedMB = data.current_month_usage_mb || 0
const remainingMB = totalMB - usedMB
const percentage = totalMB > 0 ? Math.round((usedMB / totalMB) * 100) : 0
Object.assign(trafficInfo, {
totalTraffic: formatDataSize(totalMB),
usedTraffic: formatDataSize(usedMB),
remainingTraffic: formatDataSize(remainingMB),
usagePercentage: String(percentage)
})
ElMessage.success('查询成功')
} else {
ElMessage.error(response.message || '查询失败')
}
} catch (error: any) {
console.error('获取卡片信息失败:', error) console.error('获取卡片信息失败:', error)
ElMessage.error('获取卡片信息失败') ElMessage.error(error?.message || '获取卡片信息失败')
} finally { } finally {
loading.value = false loading.value = false
} }
} }
// 获取状态名称
const getStatusName = (status: number) => {
const statusMap: Record<number, string> = {
0: '未激活',
1: '激活',
2: '停用',
3: '已过期',
4: '已注销'
}
return statusMap[status] || '未知'
}
// 格式化数据大小
const formatDataSize = (mb: number) => {
if (mb >= 1024) {
return `${(mb / 1024).toFixed(2)}GB`
}
return `${mb.toFixed(2)}MB`
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.single-card-page { .single-card-page {
.search-card {
.iccid-search {
display: flex;
align-items: center;
}
.formatted-iccid {
margin-top: 16px;
padding: 16px;
background-color: var(--el-fill-color-light);
border-radius: 4px;
font-size: 32px;
font-weight: 600;
letter-spacing: 2px;
text-align: center;
color: var(--el-color-primary);
font-family: 'Courier New', Courier, monospace;
}
}
.operation-buttons { .operation-buttons {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

@@ -4,7 +4,9 @@
<ElCard shadow="never"> <ElCard shadow="never">
<div class="detail-header"> <div class="detail-header">
<ElButton @click="handleBack"> <ElButton @click="handleBack">
<template #icon><ElIcon><ArrowLeft /></ElIcon></template> <template #icon
><ElIcon><ArrowLeft /></ElIcon
></template>
返回 返回
</ElButton> </ElButton>
<h2 class="detail-title">{{ pageTitle }}</h2> <h2 class="detail-title">{{ pageTitle }}</h2>
@@ -19,6 +21,13 @@
@search="handleSearch" @search="handleSearch"
></ArtSearchBar> ></ArtSearchBar>
<!-- 操作按钮 -->
<div style="margin: 10px 0">
<ElButton @click="showAddAccountDialog">
{{ isShopType ? '新增店铺账号' : '新增企业账号' }}
</ElButton>
</div>
<!-- 表格 --> <!-- 表格 -->
<ArtTable <ArtTable
ref="tableRef" ref="tableRef"
@@ -28,28 +37,176 @@
:currentPage="pagination.page" :currentPage="pagination.page"
:pageSize="pagination.pageSize" :pageSize="pagination.pageSize"
:total="pagination.total" :total="pagination.total"
:marginTop="10"v :marginTop="10"
v
height="60vh" height="60vh"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || (col as any).type" v-bind="col" /> <ElTableColumn
v-for="col in columns"
:key="col.prop || (col as any).type"
v-bind="col"
/>
</template> </template>
</ArtTable> </ArtTable>
<!-- 新增账号对话框 -->
<ElDialog
v-model="dialogVisible"
:title="isShopType ? '新增店铺账号' : '新增企业账号'"
width="500px"
>
<ElForm ref="formRef" :model="accountForm" :rules="accountRules" label-width="100px">
<ElFormItem label="用户名" prop="username">
<ElInput v-model="accountForm.username" placeholder="请输入用户名" />
</ElFormItem>
<ElFormItem label="密码" prop="password">
<ElInput
v-model="accountForm.password"
type="password"
placeholder="请输入密码"
show-password
/>
</ElFormItem>
<ElFormItem label="手机号" prop="phone">
<ElInput v-model="accountForm.phone" placeholder="请输入手机号" maxlength="11" />
</ElFormItem>
<ElFormItem label="账号类型" prop="user_type">
<ElInput :value="isShopType ? '代理账号' : '企业账号'" disabled />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
提交
</ElButton>
</div>
</template>
</ElDialog>
<!-- 分配角色对话框 -->
<ElDialog v-model="roleDialogVisible" width="900px">
<template #header>
<div class="dialog-header">
<span class="dialog-title">分配角色</span>
<div class="account-info">
<span class="account-name">{{ currentAccountName }}</span>
<ElTag v-if="currentAccountType === 3" type="warning" size="small" style="margin-left: 8px">
代理账号只能分配一个客户角色
</ElTag>
</div>
</div>
</template>
<div class="role-transfer-container">
<!-- 左侧可分配角色列表 -->
<div class="transfer-panel">
<div class="panel-header">
<span class="panel-title">可分配角色</span>
<ElInput
v-model="leftRoleFilter"
placeholder="搜索角色"
clearable
size="small"
style="width: 180px"
/>
</div>
<div class="panel-body">
<ElCheckboxGroup v-model="rolesToAdd" class="role-list">
<div v-for="role in filteredAvailableRoles" :key="role.ID" class="role-item">
<ElCheckbox :label="role.ID" :disabled="selectedRoles.includes(role.ID)">
<span class="role-info">
<span>{{ role.role_name }}</span>
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag>
</span>
</ElCheckbox>
</div>
</ElCheckboxGroup>
</div>
</div>
<!-- 中间操作按钮 -->
<div class="transfer-buttons">
<ElButton
type="primary"
:icon="'ArrowRight'"
@click="addRoles"
:disabled="rolesToAdd.length === 0"
>
添加
</ElButton>
</div>
<!-- 右侧已分配角色列表 -->
<div class="transfer-panel">
<div class="panel-header">
<span class="panel-title">已分配角色</span>
<ElInput
v-model="rightRoleFilter"
placeholder="搜索角色"
clearable
size="small"
style="width: 180px"
/>
</div>
<div class="panel-body">
<div class="role-list">
<div
v-for="role in filteredAssignedRoles"
:key="role.ID"
class="role-item assigned-role-item"
>
<span class="role-info">
<span>{{ role.role_name }}</span>
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag>
</span>
<ElButton type="danger" size="small" link @click="removeSingleRole(role.ID)">
移除
</ElButton>
</div>
</div>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton @click="roleDialogVisible = false">关闭</ElButton>
</div>
</template>
</ElDialog>
</ElCard> </ElCard>
</div> </div>
</ArtTableFullScreen> </ArtTableFullScreen>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { h, computed, ref, reactive, onMounted } from 'vue' import { h, computed, ref, reactive, onMounted, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElTag, ElIcon, ElButton, ElCard } from 'element-plus' import {
ElMessage,
ElTag,
ElIcon,
ElButton,
ElCard,
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElCheckbox,
ElCheckboxGroup
} from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue' import { ArrowLeft } from '@element-plus/icons-vue'
import { AccountService } from '@/api/modules' import type { FormInstance, FormRules } from 'element-plus'
import { AccountService, RoleService } from '@/api/modules'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import type { PlatformAccount } from '@/types/api' import type { PlatformAccount, PlatformRole } from '@/types/api'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'CommonAccountList' }) defineOptions({ name: 'CommonAccountList' })
@@ -86,6 +243,17 @@
const tableRef = ref() const tableRef = ref()
const accountList = ref<PlatformAccount[]>([]) const accountList = ref<PlatformAccount[]>([])
// 分配角色相关变量
const roleDialogVisible = ref(false)
const currentAccountId = ref<number>(0)
const currentAccountName = ref<string>('')
const currentAccountType = ref<number>(0)
const selectedRoles = ref<number[]>([])
const allRoles = ref<PlatformRole[]>([])
const rolesToAdd = ref<number[]>([])
const leftRoleFilter = ref('')
const rightRoleFilter = ref('')
// 搜索表单初始值 // 搜索表单初始值
const initialSearchState = { const initialSearchState = {
username: '', username: '',
@@ -124,19 +292,6 @@
placeholder: '请输入手机号' placeholder: '请输入手机号'
} }
}, },
{
label: '用户类型',
prop: 'user_type',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '代理账号', value: 3 },
{ label: '企业账号', value: 4 }
]
},
{ {
label: '状态', label: '状态',
prop: 'status', prop: 'status',
@@ -163,52 +318,74 @@
} }
// 列配置 // 列配置
const columns = computed(() => [ const columns = computed(() => {
{ const baseColumns = [
prop: 'username', {
label: '用户名', prop: 'username',
minWidth: 150 label: '用户名'
}, },
{ {
prop: 'phone', prop: 'phone',
label: '手机号', label: '手机号'
width: 130 },
}, {
{ prop: 'user_type',
prop: 'user_type', label: '用户类型',
label: '用户类型', formatter: (row: PlatformAccount) => {
width: 110, return h(ElTag, { type: getUserTypeTag(row.user_type) }, () =>
formatter: (row: PlatformAccount) => { getUserTypeName(row.user_type)
return h(ElTag, { type: getUserTypeTag(row.user_type) }, () => getUserTypeName(row.user_type)) )
}
} }
}, ]
{
prop: 'shop_id', // 根据页面类型添加不同的列
label: '店铺ID', if (isShopType.value) {
width: 100, // 店铺类型:显示店铺名称
formatter: (row: PlatformAccount) => row.shop_id || '-' baseColumns.push({
}, prop: 'shop_name',
{ label: '店铺名称',
prop: 'enterprise_id', formatter: (row: PlatformAccount) => (row as any).shop_name || '-'
label: '企业ID', })
width: 100, } else {
formatter: (row: PlatformAccount) => row.enterprise_id || '-' // 企业类型:显示企业名称
}, baseColumns.push({
{ prop: 'enterprise_name',
prop: 'status', label: '企业名称',
label: '状态', formatter: (row: PlatformAccount) => (row as any).enterprise_name || '-'
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)
} }
])
// 添加状态和创建时间
baseColumns.push(
{
prop: 'status',
label: '状态',
formatter: (row: PlatformAccount) => {
return h(ElTag, { type: getStatusTag(row.status) }, () => getStatusName(row.status))
}
},
{
prop: 'created_at',
label: '创建时间',
formatter: (row: PlatformAccount) => formatDateTime((row as any).created_at)
},
{
prop: 'operation',
label: '操作',
width: 120,
fixed: 'right',
formatter: (row: PlatformAccount) => {
return h(ArtButtonTable, {
text: '分配角色',
onClick: () => showRoleDialog(row)
})
}
}
)
return baseColumns
})
// 获取账号列表 // 获取账号列表
const getTableData = async () => { const getTableData = async () => {
@@ -220,7 +397,7 @@
pageSize: pagination.pageSize, pageSize: pagination.pageSize,
username: searchForm.username || undefined, username: searchForm.username || undefined,
phone: searchForm.phone || undefined, phone: searchForm.phone || undefined,
user_type: searchForm.user_type, user_type: isShopType.value ? 3 : 4, // 根据页面类型自动设置: 3:代理账号, 4:企业账号
status: searchForm.status, status: searchForm.status,
[filterParamKey.value]: entityId // 动态设置 shop_id 或 enterprise_id [filterParamKey.value]: entityId // 动态设置 shop_id 或 enterprise_id
} }
@@ -274,8 +451,214 @@
router.back() router.back()
} }
// 对话框相关
const dialogVisible = ref(false)
const submitLoading = ref(false)
const formRef = ref<FormInstance>()
// 账号表单数据
const accountForm = reactive({
username: '',
password: '',
phone: ''
})
// 表单验证规则
const accountRules = reactive<FormRules>({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度为 6-20 个字符', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ len: 11, message: '手机号必须为 11 位', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
]
})
// 显示新增账号对话框
const showAddAccountDialog = () => {
// 重置表单
accountForm.username = ''
accountForm.password = ''
accountForm.phone = ''
// 重置表单验证状态
nextTick(() => {
formRef.value?.clearValidate()
})
dialogVisible.value = true
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
const entityId = Number(route.params.id)
const data: any = {
username: accountForm.username,
password: accountForm.password,
phone: accountForm.phone,
user_type: isShopType.value ? 3 : 4 // 3:代理账号, 4:企业账号
}
// 根据类型添加关联ID
if (isShopType.value) {
data.shop_id = entityId
} else {
data.enterprise_id = entityId
}
await AccountService.createAccount(data)
ElMessage.success('添加账号成功')
dialogVisible.value = false
// 刷新列表
getTableData()
} catch (error) {
console.error(error)
ElMessage.error('添加账号失败')
} finally {
submitLoading.value = false
}
}
})
}
// 加载所有角色列表
const loadAllRoles = async () => {
try {
const res = await RoleService.getRoles({ page: 1, pageSize: 100 })
if (res.code === 0) {
allRoles.value = res.data.items || []
}
} catch (error) {
console.error('获取角色列表失败:', error)
}
}
// 计算属性:过滤后的可分配角色(根据账号类型过滤)
const filteredAvailableRoles = computed(() => {
let roles = allRoles.value
// 根据账号类型过滤角色
if (currentAccountType.value === 3) {
// 代理账号:只显示客户角色
roles = roles.filter((role) => role.role_type === 2)
} else if (currentAccountType.value === 2) {
// 平台用户:只显示平台角色
roles = roles.filter((role) => role.role_type === 1)
} else if (currentAccountType.value === 4) {
// 企业账号:只显示客户角色
roles = roles.filter((role) => role.role_type === 2)
}
// 根据搜索关键词过滤
if (!leftRoleFilter.value) return roles
const keyword = leftRoleFilter.value.toLowerCase()
return roles.filter((role) => role.role_name.toLowerCase().includes(keyword))
})
// 计算属性:过滤后的已分配角色
const filteredAssignedRoles = computed(() => {
const assignedRolesList = allRoles.value.filter((role) => selectedRoles.value.includes(role.ID))
if (!rightRoleFilter.value) return assignedRolesList
const keyword = rightRoleFilter.value.toLowerCase()
return assignedRolesList.filter((role) => role.role_name.toLowerCase().includes(keyword))
})
// 显示分配角色对话框
const showRoleDialog = async (row: PlatformAccount) => {
currentAccountId.value = (row as any).id
currentAccountName.value = row.username
currentAccountType.value = row.user_type
selectedRoles.value = []
rolesToAdd.value = []
leftRoleFilter.value = ''
rightRoleFilter.value = ''
try {
// 每次打开对话框时重新加载最新的角色列表
await loadAllRoles()
// 先加载当前账号的角色,再打开对话框
const res = await AccountService.getAccountRoles((row as any).id)
if (res.code === 0) {
// 提取角色ID数组
const roles = res.data || []
// 兼容 ID 和 id 两种字段名
selectedRoles.value = roles.map((role: any) => role.ID || role.id)
// 数据加载完成后再打开对话框
roleDialogVisible.value = true
}
} catch (error) {
console.error('获取账号角色失败:', error)
}
}
// 批量添加角色
const addRoles = async () => {
if (rolesToAdd.value.length === 0) return
try {
let newRoles: number[]
// 代理账号只能分配一个角色,会覆盖之前的角色
if (currentAccountType.value === 3) {
if (rolesToAdd.value.length > 1) {
ElMessage.warning('代理账号只能分配一个角色')
return
}
// 只保留新选择的一个角色
newRoles = rolesToAdd.value
} else {
// 其他账号类型可以分配多个角色
newRoles = [...new Set([...selectedRoles.value, ...rolesToAdd.value])]
}
await AccountService.assignRolesToAccount(currentAccountId.value, newRoles)
selectedRoles.value = newRoles
rolesToAdd.value = []
ElMessage.success('角色分配成功')
// 刷新列表以更新角色显示
await getTableData()
} catch (error) {
console.error('分配角色失败:', error)
}
}
// 移除单个角色
const removeSingleRole = async (roleId: number) => {
try {
// 从已分配列表中移除该角色
const newRoles = selectedRoles.value.filter((id) => id !== roleId)
await AccountService.assignRolesToAccount(currentAccountId.value, newRoles)
selectedRoles.value = newRoles
ElMessage.success('角色移除成功')
// 刷新列表以更新角色显示
await getTableData()
} catch (error) {
console.error('移除角色失败:', error)
ElMessage.error('角色移除失败')
}
}
onMounted(() => { onMounted(() => {
getTableData() getTableData()
loadAllRoles()
}) })
</script> </script>
@@ -285,9 +668,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
margin-bottom: 24px;
padding-bottom: 16px; padding-bottom: 16px;
border-bottom: 1px solid #e4e7ed;
.detail-title { .detail-title {
margin: 0; margin: 0;
@@ -297,4 +678,128 @@
} }
} }
} }
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
.dialog-title {
font-size: 18px;
font-weight: 600;
}
.account-info {
display: flex;
align-items: center;
gap: 8px;
.account-name {
font-size: 14px;
color: var(--el-text-color-regular);
}
}
}
.role-transfer-container {
display: flex;
justify-content: space-between;
align-items: stretch;
gap: 20px;
padding: 20px 0;
min-height: 500px;
.transfer-panel {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid var(--el-border-color);
border-radius: 4px;
overflow: hidden;
max-width: 380px;
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color);
.panel-title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.panel-body {
flex: 1;
overflow-y: auto;
padding: 12px;
.role-list {
display: flex;
flex-direction: column;
gap: 8px;
.role-item {
padding: 10px 12px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 4px;
transition: all 0.2s;
&:hover {
background: var(--el-fill-color-light);
border-color: var(--el-border-color);
}
.role-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
:deep(.el-checkbox) {
width: 100%;
.el-checkbox__label {
width: 100%;
}
}
}
.assigned-role-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
.role-info {
flex: 1;
}
.el-button {
flex-shrink: 0;
}
}
}
}
}
.transfer-buttons {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 12px;
padding: 0 10px;
.el-button {
width: 100px;
}
}
}
</style> </style>

View File

@@ -387,7 +387,7 @@
if (hasAuth('agent_commission:detail')) { if (hasAuth('agent_commission:detail')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
icon: '&#xe72b;', text: '详情',
onClick: () => showDetail(row) onClick: () => showDetail(row)
}) })
) )

View File

@@ -1,5 +1,43 @@
<template> <template>
<div class="single-card-page"> <div class="single-card-page">
<!-- ICCID查询区域 -->
<ElCard shadow="never" class="search-card" style="margin-bottom: 24px">
<template #header>
<div class="card-header">
<span>ICCID查询</span>
</div>
</template>
<div class="iccid-search">
<div class="iccid-input-wrapper">
<ElInput
v-model="searchIccid"
placeholder="请输入ICCID"
clearable
style="width: 400px"
@focus="iccidInputFocused = true"
@blur="iccidInputFocused = false"
@keyup.enter="handleSearchCard"
/>
<!-- 放大显示区域 -->
<Transition name="zoom-fade">
<div v-if="iccidInputFocused && searchIccid" class="iccid-magnifier">
<div class="magnifier-arrow"></div>
<div class="magnifier-content">
{{ formatIccidWithDashes(searchIccid) }}
</div>
</div>
</Transition>
</div>
<ElButton
type="primary"
@click="handleSearchCard"
:loading="loading"
style="margin-left: 16px"
>查询</ElButton
>
</div>
</ElCard>
<!-- 卡片内容区域 --> <!-- 卡片内容区域 -->
<div v-if="cardInfo" class="card-content-area slide-in"> <div v-if="cardInfo" class="card-content-area slide-in">
<!-- 主要内容区域 --> <!-- 主要内容区域 -->
@@ -150,12 +188,12 @@
min-width="200" min-width="200"
show-overflow-tooltip show-overflow-tooltip
/> />
<ElTableColumn prop="packageType" label="类型" width="100" /> <ElTableColumn prop="packageType" label="类型" />
<ElTableColumn prop="totalFlow" label="总流量" width="100" /> <ElTableColumn prop="totalFlow" label="总流量" />
<ElTableColumn prop="usedFlow" label="已用" width="100" /> <ElTableColumn prop="usedFlow" label="已用" />
<ElTableColumn prop="remainFlow" label="剩余" width="100" /> <ElTableColumn prop="remainFlow" label="剩余" />
<ElTableColumn prop="expireTime" label="到期时间" width="120" /> <ElTableColumn prop="expireTime" label="到期时间" />
<ElTableColumn prop="status" label="状态" width="100"> <ElTableColumn prop="status" label="状态">
<template #default="scope"> <template #default="scope">
<ElTag :type="getPackageStatusType(scope.row.status)" size="small"> <ElTag :type="getPackageStatusType(scope.row.status)" size="small">
{{ scope.row.status }} {{ scope.row.status }}
@@ -287,15 +325,10 @@
</div> </div>
</div> </div>
<!-- 加载状态 -->
<!--<div v-else-if="loading" class="loading-state">-->
<!-- <ElSkeleton :rows="10" animated />-->
<!--</div>-->
<!-- 空状态 --> <!-- 空状态 -->
<!--<div v-else class="empty-state">--> <div v-else class="empty-state">
<!-- <ElEmpty description="暂无卡片数据" />--> <ElEmpty description="请在上方输入ICCID进行查询" />
<!--</div>--> </div>
</div> </div>
</template> </template>
@@ -314,13 +347,30 @@
} from 'element-plus' } from 'element-plus'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { EnterpriseService } from '@/api/modules/enterprise' import { EnterpriseService } from '@/api/modules/enterprise'
import { CardService } from '@/api/modules'
defineOptions({ name: 'SingleCard' }) defineOptions({ name: 'SingleCard' })
const route = useRoute() const route = useRoute()
const loading = ref(true) const loading = ref(false)
const operationLoading = ref(false) const operationLoading = ref(false)
// ICCID搜索相关
const searchIccid = ref('')
const iccidInputFocused = ref(false)
// 格式化ICCID为4位一组用横杠分隔
const formatIccidWithDashes = (iccid: string) => {
if (!iccid) return ''
return iccid.replace(/(\d{4})(?=\d)/g, '$1-')
}
// 格式化显示的ICCID用于查询结果显示
const formattedIccid = computed(() => {
if (!cardInfo.value?.iccid) return ''
return formatIccidWithDashes(cardInfo.value.iccid)
})
// 从 URL 获取参数 // 从 URL 获取参数
const enterpriseId = computed(() => { const enterpriseId = computed(() => {
const id = route.query.enterpriseId || route.query.enterprise_id const id = route.query.enterpriseId || route.query.enterprise_id
@@ -336,10 +386,139 @@
return cardInfo.value?.id ? Number(cardInfo.value.id) : null return cardInfo.value?.id ? Number(cardInfo.value.id) : null
}) })
// 卡片信息 // 卡片信息 - 默认为null,等待查询
const cardInfo = ref<any>(null) const cardInfo = ref<any>(null)
// 模拟卡片数据 // 处理ICCID搜索
const handleSearchCard = async () => {
if (!searchIccid.value.trim()) {
ElMessage.warning('请输入ICCID')
return
}
await fetchCardDetailByIccid(searchIccid.value.trim())
}
// 根据ICCID获取卡片详情
const fetchCardDetailByIccid = async (iccid: string) => {
try {
loading.value = true
const response = await CardService.getIotCardDetailByIccid(iccid)
if (response.code === 0 && response.data) {
const data = response.data
// 映射API数据到页面显示格式
cardInfo.value = {
id: data.id,
iccid: data.iccid || '',
accessNumber: data.msisdn || '--',
imei: data.imei || '--',
expireTime: data.expire_at || '--',
operator: getCarrierTypeName(data.carrier_type),
cardStatus: getCardStatusName(data.status),
cardType: getCardCategoryName(data.card_category),
supplier: data.supplier_name || '--',
importTime: data.created_at || '--',
phoneBind: data.phone_bind || '--',
trafficPool: data.traffic_pool || '--',
agent: data.shop_name || data.carrier_name || '--',
operatorStatus: getActivationStatusName(data.activation_status),
operatorRealName: getRealNameStatusName(data.real_name_status),
walletBalance: data.wallet_balance ? `${data.wallet_balance}` : '0.00元',
walletPasswordStatus: data.wallet_password_status || '--',
realNameAuth: data.real_name_status === 1,
virtualNumber: data.virtual_number || '--',
// 流量信息
packageSeries: data.series_name || '--',
packageTotalFlow: formatDataSize(data.data_usage_mb || 0),
usedFlow: formatDataSize(data.current_month_usage_mb || 0),
remainFlow: formatDataSize(
(data.data_usage_mb || 0) - (data.current_month_usage_mb || 0)
),
usedFlowPercentage:
data.data_usage_mb > 0
? `${((data.current_month_usage_mb / data.data_usage_mb) * 100).toFixed(2)}%`
: '未设置',
realUsedFlow: formatDataSize(data.current_month_usage_mb || 0),
actualFlow: formatDataSize(data.current_month_usage_mb || 0),
packageList: data.packages || []
}
ElMessage.success('查询成功')
} else {
ElMessage.error(response.msg || '查询失败')
cardInfo.value = null
}
} catch (error: any) {
console.error('获取卡片信息失败:', error)
ElMessage.error(error?.message || '获取卡片信息失败')
cardInfo.value = null
} finally {
loading.value = false
}
}
// 格式化数据大小
const formatDataSize = (mb: number) => {
if (mb >= 1024) {
return `${(mb / 1024).toFixed(2)}GB`
}
return `${mb.toFixed(2)}MB`
}
// 获取运营商类型名称
const getCarrierTypeName = (type: string) => {
const typeMap: Record<string, string> = {
CMCC: '中国移动',
CUCC: '中国联通',
CTCC: '中国电信'
}
return typeMap[type] || type || '未知'
}
// 获取卡片类别名称
const getCardCategoryName = (category: string) => {
const categoryMap: Record<string, string> = {
normal: '普通卡',
premium: '高级卡',
enterprise: '企业卡'
}
return categoryMap[category] || category || '未知'
}
// 获取卡片状态名称
const getCardStatusName = (status: number) => {
const statusMap: Record<number, string> = {
0: '未激活',
1: '正常',
2: '停用',
3: '已过期',
4: '已注销'
}
return statusMap[status] || '未知'
}
// 获取激活状态名称
const getActivationStatusName = (status: number) => {
const statusMap: Record<number, string> = {
0: '未激活',
1: '已激活',
2: '已停用'
}
return statusMap[status] || '未知'
}
// 获取实名状态名称
const getRealNameStatusName = (status: number) => {
const statusMap: Record<number, string> = {
0: '未实名',
1: '已实名',
2: '实名失败'
}
return statusMap[status] || '未知'
}
// 模拟卡片数据(保留作为参考)
const mockCardData = { const mockCardData = {
id: 1, // 卡片ID id: 1, // 卡片ID
iccid: '8986062357007989203', iccid: '8986062357007989203',
@@ -381,24 +560,9 @@
] ]
} }
// 获取卡片详情 // 页面初始化 - 不自动加载数据,等待用户输入ICCID查询
const fetchCardDetail = async () => {
try {
loading.value = true
// 模拟API调用
setTimeout(() => {
cardInfo.value = { ...mockCardData }
loading.value = false
}, 500)
} catch (error) {
ElMessage.error('获取卡片详情失败')
loading.value = false
}
}
// 页面初始化时加载数据
onMounted(() => { onMounted(() => {
fetchCardDetail() // 不再自动加载模拟数据,等待用户查询
}) })
// 获取状态标签类型 // 获取状态标签类型
@@ -550,6 +714,89 @@
<style lang="scss" scoped> <style lang="scss" scoped>
.single-card-page { .single-card-page {
padding: 20px 0; padding: 20px 0;
// ICCID搜索卡片
.search-card {
position: relative;
overflow: visible;
:deep(.el-card__body) {
overflow: visible;
}
.iccid-search {
display: flex;
align-items: flex-start;
overflow: visible;
.iccid-input-wrapper {
position: relative;
display: inline-block;
// 放大显示区域
.iccid-magnifier {
position: absolute;
top: calc(100% + 12px);
left: 0;
white-space: nowrap;
// 三角箭头 - 上边框样式
.magnifier-arrow {
position: absolute;
top: -9px;
left: 80px;
width: 16px;
height: 16px;
background-color: var(--el-bg-color, #fff);
border-top: 2px solid var(--el-color-primary);
border-left: 2px solid var(--el-color-primary);
transform: rotate(45deg);
}
// 内容区域
.magnifier-content {
display: inline-block;
padding: 16px 24px;
font-size: 28px;
font-weight: 600;
line-height: 1.4;
color: var(--el-color-primary);
text-align: center;
letter-spacing: 2px;
white-space: nowrap;
background-color: var(--el-bg-color, #fff);
border: 2px solid var(--el-color-primary);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
font-family: 'Courier New', Courier, monospace;
}
}
}
}
}
// 放大效果的过渡动画
.zoom-fade-enter-active,
.zoom-fade-leave-active {
transition: all 0.2s ease;
}
.zoom-fade-enter-from {
opacity: 0;
transform: scale(0.8) translateY(-10px);
}
.zoom-fade-leave-to {
opacity: 0;
transform: scale(0.8) translateY(-10px);
}
.zoom-fade-enter-to,
.zoom-fade-leave-from {
opacity: 1;
transform: scale(1) translateY(0);
}
// 卡片内容区域 // 卡片内容区域
.card-content-area { .card-content-area {
&.slide-in { &.slide-in {

View File

@@ -18,7 +18,9 @@
@refresh="handleRefresh" @refresh="handleRefresh"
> >
<template #left> <template #left>
<ElButton @click="showCreateDialog" v-permission="'orders:add'">{{ t('orderManagement.createOrder') }}</ElButton> <ElButton @click="showCreateDialog" v-permission="'orders:add'">{{
t('orderManagement.createOrder')
}}</ElButton>
</template> </template>
</ArtTableHeader> </ArtTableHeader>
@@ -130,7 +132,11 @@
reserve-keyword reserve-keyword
:remote-method="handlePackageSearch" :remote-method="handlePackageSearch"
:loading="packageSearchLoading" :loading="packageSearchLoading"
:disabled="(!createForm.iot_card_id && !createForm.device_id) || (createForm.order_type === 'single_card' && !createForm.iot_card_id) || (createForm.order_type === 'device' && !createForm.device_id)" :disabled="
(!createForm.iot_card_id && !createForm.device_id) ||
(createForm.order_type === 'single_card' && !createForm.iot_card_id) ||
(createForm.order_type === 'device' && !createForm.device_id)
"
clearable clearable
style="width: 100%" style="width: 100%"
> >
@@ -627,14 +633,14 @@
formatter: (row: Order) => { formatter: (row: Order) => {
return h('div', { style: 'display: flex; gap: 8px;' }, [ return h('div', { style: 'display: flex; gap: 8px;' }, [
h(ArtButtonTable, { h(ArtButtonTable, {
icon: '&#xe6cf;', text: '详情',
tooltip: t('orderManagement.actions.viewDetail'), tooltip: t('orderManagement.actions.viewDetail'),
onClick: () => showOrderDetail(row) onClick: () => showOrderDetail(row)
}), }),
// 只有待支付和已支付的订单可以取消 // 只有待支付和已支付的订单可以取消
row.payment_status === 1 || row.payment_status === 2 row.payment_status === 1 || row.payment_status === 2
? h(ArtButtonTable, { ? h(ArtButtonTable, {
type: 'delete', text: '删除',
tooltip: t('orderManagement.actions.cancel'), tooltip: t('orderManagement.actions.cancel'),
onClick: () => handleCancelOrder(row) onClick: () => handleCancelOrder(row)
}) })

View File

@@ -25,125 +25,123 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage } from 'element-plus' import { ElCard, ElButton, ElIcon, ElMessage } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue' import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue' import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue' import type { DetailSection } from '@/components/common/DetailPage.vue'
import { ShopPackageAllocationService } from '@/api/modules' import { ShopPackageAllocationService } from '@/api/modules'
import type { ShopPackageAllocationResponse } from '@/types/api' import type { ShopPackageAllocationResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'PackageAssignDetail' }) defineOptions({ name: 'PackageAssignDetail' })
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const loading = ref(false) const loading = ref(false)
const detailData = ref<ShopPackageAllocationResponse | null>(null) const detailData = ref<ShopPackageAllocationResponse | null>(null)
// 详情页配置 // 详情页配置
const detailSections: DetailSection[] = [ const detailSections: DetailSection[] = [
{ {
title: '基本信息', title: '基本信息',
fields: [ fields: [
{ label: 'ID', prop: 'id' }, { 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: 'shop_name' }, { label: '被分配店铺', prop: 'shop_name' },
{ {
label: '分配者店铺', label: '分配者店铺',
formatter: (_, data) => { formatter: (_, data) => {
if (data.allocator_shop_id === 0) { if (data.allocator_shop_id === 0) {
return '平台' return '平台'
}
return data.allocator_shop_name || '-'
} }
return data.allocator_shop_name || '-' },
} {
}, label: '成本价',
{ formatter: (_, data) => {
label: '成本价', return `¥${(data.cost_price / 100).toFixed(2)}`
formatter: (_, data) => { }
return `¥${(data.cost_price / 100).toFixed(2)}` },
} {
}, label: '状态',
{ formatter: (_, data) => {
label: '状态', return data.status === 1 ? '启用' : '禁用'
formatter: (_, data) => { }
return data.status === 1 ? '启用' : '禁用' },
} { label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },
}, { label: '更新时间', prop: 'updated_at', formatter: (value) => formatDateTime(value) }
{ 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() 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> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.package-assign-detail { .package-assign-detail {
padding: 20px; 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 { .detail-header {
display: flex; display: flex;
flex-direction: column; align-items: center;
align-items: center; gap: 16px;
justify-content: center; padding-bottom: 16px;
padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary);
.el-icon { .detail-title {
font-size: 32px; 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> </style>

View File

@@ -19,7 +19,9 @@
@refresh="handleRefresh" @refresh="handleRefresh"
> >
<template #left> <template #left>
<ElButton type="primary" @click="showDialog('add')" v-permission="'package_assign:add'">新增分配</ElButton> <ElButton type="primary" @click="showDialog('add')" v-permission="'package_assign:add'"
>新增分配</ElButton
>
</template> </template>
</ArtTableHeader> </ArtTableHeader>
@@ -113,7 +115,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { h } from 'vue' import { h } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ShopPackageAllocationService, PackageManageService, ShopService, ShopSeriesAllocationService } from '@/api/modules' 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'
@@ -386,14 +393,14 @@
{ {
prop: 'operation', prop: 'operation',
label: '操作', label: '操作',
width: 200, width: 220,
fixed: 'right', fixed: 'right',
formatter: (row: ShopPackageAllocationResponse) => { formatter: (row: ShopPackageAllocationResponse) => {
const buttons = [] const buttons = []
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type:"view", text: '详情',
onClick: () => handleViewDetail(row) onClick: () => handleViewDetail(row)
}) })
) )
@@ -401,7 +408,7 @@
if (hasAuth('package_assign:edit')) { if (hasAuth('package_assign:edit')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type:"edit", text: '编辑',
onClick: () => showDialog('edit', row) onClick: () => showDialog('edit', row)
}) })
) )
@@ -410,7 +417,7 @@
if (hasAuth('package_assign:delete')) { if (hasAuth('package_assign:delete')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type:"delete", text: '删除',
onClick: () => deleteAllocation(row) onClick: () => deleteAllocation(row)
}) })
) )

View File

@@ -25,224 +25,231 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage } from 'element-plus' import { ElCard, ElButton, ElIcon, ElMessage } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue' import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue' import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue' import type { DetailSection } from '@/components/common/DetailPage.vue'
import { PackageManageService } from '@/api/modules' import { PackageManageService } from '@/api/modules'
import type { PackageResponse } from '@/types/api' import type { PackageResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'PackageDetail' }) defineOptions({ name: 'PackageDetail' })
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const loading = ref(false) const loading = ref(false)
const detailData = ref<PackageResponse | null>(null) const detailData = ref<PackageResponse | null>(null)
// 详情页配置 // 详情页配置
const detailSections: DetailSection[] = [ const detailSections: DetailSection[] = [
{ {
title: '基本信息', title: '基本信息',
fields: [ fields: [
{ label: 'ID', prop: 'id' }, { 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: '套餐类型', label: '套餐类型',
formatter: (_, data) => { formatter: (_, data) => {
const typeMap = { const typeMap = {
formal: '正式套餐', formal: '正式套餐',
addon: '附加套餐' addon: '附加套餐'
}
return typeMap[data.package_type] || data.package_type
} }
return typeMap[data.package_type] || data.package_type },
} {
}, label: '套餐时长',
{ formatter: (_, data) => {
label: '套餐时长', return `${data.duration_months} 个月`
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) => {
label: '启用虚流量', return data.status === 1 ? '启用' : '禁用'
formatter: (_, data) => { }
return data.enable_virtual_data ? '是' : '否' },
} {
}, label: '上架状态',
{ formatter: (_, data) => {
label: '虚流量额度', return data.shelf_status === 1 ? '上架' : '下架'
formatter: (_, data) => { }
if (!data.enable_virtual_data) return '-' },
if (data.virtual_data_mb >= 1024) { { label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },
return `${(data.virtual_data_mb / 1024).toFixed(2)} GB` { 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`
} }
return `${data.virtual_data_mb} MB`
} }
} ]
] },
}, {
{ title: '价格信息',
title: '价格信息', fields: [
fields: [ {
{ label: '成本价',
label: '成本价', formatter: (_, data) => {
formatter: (_, data) => { return `¥${(data.cost_price / 100).toFixed(2)}`
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)}`
}
} }
}, ]
{ },
label: '建议售价', {
formatter: (_, data) => { title: '佣金配置',
return `¥${(data.suggested_retail_price / 100).toFixed(2)}` 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
}
} }
}, ]
{
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() 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> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.package-detail { .package-detail {
padding: 20px; 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 { .detail-header {
display: flex; display: flex;
flex-direction: column; align-items: center;
align-items: center; gap: 16px;
justify-content: center; padding-bottom: 16px;
padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary);
.el-icon { .detail-title {
font-size: 32px; 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> </style>

View File

@@ -18,7 +18,9 @@
@refresh="handleRefresh" @refresh="handleRefresh"
> >
<template #left> <template #left>
<ElButton type="primary" @click="showDialog('add')" v-permission="'package:add'">新增套餐</ElButton> <ElButton type="primary" @click="showDialog('add')" v-permission="'package:add'"
>新增套餐</ElButton
>
</template> </template>
</ArtTableHeader> </ArtTableHeader>
@@ -50,13 +52,13 @@
> >
<ElForm ref="formRef" :model="form" :rules="rules" label-width="150px"> <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
v-model="form.package_code" v-model="form.package_code"
placeholder="请输入套餐编码或点击生成" placeholder="请输入套餐编码或点击生成"
:disabled="dialogType === 'edit'" :disabled="dialogType === 'edit'"
clearable clearable
style="flex: 1;" style="flex: 1"
/> />
<ElButton v-if="dialogType === 'add'" @click="handleGeneratePackageCode"> <ElButton v-if="dialogType === 'add'" @click="handleGeneratePackageCode">
生成编码 生成编码
@@ -478,14 +480,14 @@
{ {
prop: 'operation', prop: 'operation',
label: '操作', label: '操作',
width: 200, width: 220,
fixed: 'right', fixed: 'right',
formatter: (row: PackageResponse) => { formatter: (row: PackageResponse) => {
const buttons = [] const buttons = []
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'view', text: '详情',
onClick: () => handleViewDetail(row) onClick: () => handleViewDetail(row)
}) })
) )
@@ -493,7 +495,7 @@
if (hasAuth('package:edit')) { if (hasAuth('package:edit')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'edit', text: '编辑',
onClick: () => showDialog('edit', row) onClick: () => showDialog('edit', row)
}) })
) )
@@ -502,7 +504,7 @@
if (hasAuth('package:delete')) { if (hasAuth('package:delete')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'delete', text: '删除',
onClick: () => deletePackage(row) onClick: () => deletePackage(row)
}) })
) )
@@ -661,7 +663,9 @@
form.virtual_data_mb = row.virtual_data_mb || 0 form.virtual_data_mb = row.virtual_data_mb || 0
form.duration_months = row.duration_months form.duration_months = row.duration_months
form.cost_price = row.cost_price / 100 // 分转换为元显示 form.cost_price = row.cost_price / 100 // 分转换为元显示
form.suggested_retail_price = row.suggested_retail_price ? row.suggested_retail_price / 100 : undefined 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

View File

@@ -25,239 +25,247 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, h } from 'vue' import { ref, onMounted, h } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage, ElTag } from 'element-plus' import { ElCard, ElButton, ElIcon, ElMessage, ElTag } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue' import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue' import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue' import type { DetailSection } from '@/components/common/DetailPage.vue'
import { PackageSeriesService } from '@/api/modules' import { PackageSeriesService } from '@/api/modules'
import type { PackageSeriesResponse } from '@/types/api' import type { PackageSeriesResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'PackageSeriesDetail' }) defineOptions({ name: 'PackageSeriesDetail' })
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const loading = ref(false) const loading = ref(false)
const detailData = ref<PackageSeriesResponse | null>(null) const detailData = ref<PackageSeriesResponse | null>(null)
// 详情页配置 // 详情页配置
const detailSections: DetailSection[] = [ const detailSections: DetailSection[] = [
{ {
title: '基本信息', title: '基本信息',
fields: [ fields: [
{ label: 'ID', prop: 'id' }, { label: 'ID', prop: 'id' },
{ label: '系列编码', prop: 'series_code' }, { label: '系列编码', prop: 'series_code' },
{ label: '系列名称', prop: 'series_name' }, { label: '系列名称', prop: 'series_name' },
{ {
label: '状态', label: '状态',
formatter: (_, data) => { formatter: (_, data) => {
return data.status === 1 ? '启用' : '禁用' 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) => { label: '描述',
const dimensionText = tier.dimension === 'sales_count' ? '销量' : '销售额' prop: 'description',
const thresholdText = tier.dimension === 'sales_amount' fullWidth: true
? `¥${(tier.threshold / 100).toFixed(2)}` },
: tier.threshold { label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },
const amountText = `¥${(tier.amount / 100).toFixed(2)}` { label: '更新时间', prop: 'updated_at', formatter: (value) => formatDateTime(value) }
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 ? '已启用' : '未启用'
]
},
{
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) => {
label: '时效值', const config = data.one_time_commission_config
formatter: (_, data) => { if (!config?.commission_type) return '-'
const config = data.one_time_commission_config return config.commission_type === 'fixed' ? '固定佣金' : '梯度佣金'
if (!config?.validity_value) return '-' }
if (config.validity_type === 'relative') { },
return `${config.validity_value}个月` {
} else if (config.validity_type === 'fixed_date') { label: '固定佣金金额',
return config.validity_value 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}`
)
})
)
} }
return '-'
} }
} ]
] },
} {
] title: '强充配置',
fields: [
// 返回上一页 {
const handleBack = () => { label: '启用状态',
router.back() formatter: (_, data) => {
} return data.one_time_commission_config?.enable_force_recharge ? '已启用' : '未启用'
}
// 获取详情数据 },
const fetchDetail = async () => { {
const id = Number(route.params.id) label: '强充金额',
if (!id) { formatter: (_, data) => {
ElMessage.error('缺少ID参数') const config = data.one_time_commission_config
return if (!config?.force_amount) return '-'
} return `¥${(config.force_amount / 100).toFixed(2)}`
}
loading.value = true },
try { {
const res = await PackageSeriesService.getPackageSeriesDetail(id) label: '强充计算类型',
if (res.code === 0) { formatter: (_, data) => {
detailData.value = res.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 '-'
}
}
]
} }
} catch (error) { ]
console.error(error)
ElMessage.error('获取详情失败')
} finally {
loading.value = false
}
}
onMounted(() => { // 返回上一页
fetchDetail() 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> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.package-series-detail { .package-series-detail {
padding: 20px; 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 { .detail-header {
display: flex; display: flex;
flex-direction: column; align-items: center;
align-items: center; gap: 16px;
justify-content: center; padding-bottom: 16px;
padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary);
.el-icon { .detail-title {
font-size: 32px; 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> </style>

View File

@@ -243,7 +243,13 @@
:icon="Delete" :icon="Delete"
circle circle
@click="removeTier(index)" @click="removeTier(index)"
style="flex-shrink: 0; margin-top: 28px; width: 32px; height: 32px; padding: 0" style="
flex-shrink: 0;
margin-top: 28px;
width: 32px;
height: 32px;
padding: 0;
"
/> />
</div> </div>
</ElCard> </ElCard>
@@ -673,7 +679,7 @@
{ {
prop: 'operation', prop: 'operation',
label: '操作', label: '操作',
width: 200, width: 220,
fixed: 'right', fixed: 'right',
formatter: (row: PackageSeriesResponse) => { formatter: (row: PackageSeriesResponse) => {
const buttons = [] const buttons = []
@@ -681,7 +687,7 @@
// 详情按钮 // 详情按钮
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'view', text: '详情',
onClick: () => handleViewDetail(row) onClick: () => handleViewDetail(row)
}) })
) )
@@ -689,7 +695,7 @@
if (hasAuth('package_series:edit')) { if (hasAuth('package_series:edit')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'edit', text: '编辑',
onClick: () => showDialog('edit', row) onClick: () => showDialog('edit', row)
}) })
) )
@@ -698,7 +704,7 @@
if (hasAuth('package_series:delete')) { if (hasAuth('package_series:delete')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'delete', text: '删除',
onClick: () => deleteSeries(row) onClick: () => deleteSeries(row)
}) })
) )

View File

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

View File

@@ -19,7 +19,9 @@
@refresh="handleRefresh" @refresh="handleRefresh"
> >
<template #left> <template #left>
<ElButton type="primary" @click="showDialog('add')" v-permission="'series_assign:add'">新增系列分配</ElButton> <ElButton type="primary" @click="showDialog('add')" v-permission="'series_assign:add'"
>新增系列分配</ElButton
>
</template> </template>
</ArtTableHeader> </ArtTableHeader>
@@ -565,7 +567,7 @@
{ {
prop: 'operation', prop: 'operation',
label: '操作', label: '操作',
width: 180, width: 220,
fixed: 'right', fixed: 'right',
formatter: (row: ShopSeriesAllocationResponse) => { formatter: (row: ShopSeriesAllocationResponse) => {
const buttons = [] const buttons = []
@@ -573,7 +575,7 @@
// 详情按钮 // 详情按钮
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'view', text: '详情',
onClick: () => handleViewDetail(row) onClick: () => handleViewDetail(row)
}) })
) )
@@ -581,7 +583,7 @@
if (hasAuth('series_assign:edit')) { if (hasAuth('series_assign:edit')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'edit', text: '编辑',
onClick: () => showDialog('edit', row) onClick: () => showDialog('edit', row)
}) })
) )
@@ -590,7 +592,7 @@
if (hasAuth('series_assign:delete')) { if (hasAuth('series_assign:delete')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'delete', text: '删除',
onClick: () => deleteAllocation(row) onClick: () => deleteAllocation(row)
}) })
) )
@@ -800,7 +802,8 @@
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, 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)

View File

@@ -47,7 +47,7 @@
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '新增店铺' : '编辑店铺'" :title="dialogType === 'add' ? '新增店铺' : '编辑店铺'"
width="800px" width="50%"
> >
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="100px"> <ElForm ref="formRef" :model="formData" :rules="rules" label-width="100px">
<ElRow :gutter="20"> <ElRow :gutter="20">
@@ -162,6 +162,32 @@
/> />
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="12">
<ElFormItem label="默认角色" prop="default_role_id">
<ElSelect
v-model="formData.default_role_id"
placeholder="请选择默认角色"
filterable
remote
:remote-method="searchDefaultRoles"
:loading="defaultRoleLoading"
clearable
style="width: 100%"
>
<ElOption
v-for="role in defaultRoleList"
:key="role.ID"
:label="role.role_name"
:value="role.ID"
>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>{{ role.role_name }}</span>
<ElTag type="success" size="small">客户角色</ElTag>
</div>
</ElOption>
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow> </ElRow>
</template> </template>
@@ -195,42 +221,38 @@
<ElDialog <ElDialog
v-model="defaultRolesDialogVisible" v-model="defaultRolesDialogVisible"
:title="`设置默认角色 - ${currentShop?.shop_name || ''}`" :title="`设置默认角色 - ${currentShop?.shop_name || ''}`"
width="800px" width="50%"
> >
<div v-loading="defaultRolesLoading"> <div v-loading="defaultRolesLoading">
<!-- 当前默认角色列表 --> <!-- 当前默认角色列表 -->
<div class="default-roles-section"> <div class="default-roles-section">
<div class="section-header"> <div class="section-header">
<span style="color:white;">当前默认角色</span> <span style="color: white">当前默认角色</span>
<ElButton type="primary" @click="showAddRoleDialog"> <ElButton type="primary" @click="showAddRoleDialog"> 设置默认角色 </ElButton>
添加角色
</ElButton>
</div> </div>
<ElTable :data="currentDefaultRoles" border stripe style="margin-top: 12px"> <ElTable :data="currentDefaultRoles" border stripe style="margin-top: 12px">
<ElTableColumn prop="role_name" label="角色名称" width="150" /> <ElTableColumn prop="role_name" label="角色名称" width="150" />
<ElTableColumn prop="role_desc" label="角色描述" min-width="200" /> <ElTableColumn prop="role_desc" label="角色描述" min-width="200" />
<ElTableColumn prop="status" label="状态" width="80"> <ElTableColumn prop="status" label="状态" width="80">
<template #default="{ row }"> <template #default="{ row }">
<ElTag :type="row.status === CommonStatus.ENABLED ? 'success' : 'info'" size="small"> <ElTag
:type="row.status === CommonStatus.ENABLED ? 'success' : 'info'"
size="small"
>
{{ getStatusText(row.status) }} {{ getStatusText(row.status) }}
</ElTag> </ElTag>
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn label="操作" width="100" fixed="right"> <ElTableColumn label="操作" width="100" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<ElButton <ElButton type="danger" text size="small" @click="handleDeleteDefaultRole(row)">
type="danger"
text
size="small"
@click="handleDeleteDefaultRole(row)"
>
删除 删除
</ElButton> </ElButton>
</template> </template>
</ElTableColumn> </ElTableColumn>
<template #empty> <template #empty>
<div style="padding: 20px 0; color: #909399;"> <div style="padding: 20px 0; color: #909399">
暂无默认角色请点击"添加角色"按钮进行配置 暂无默认角色请点击"设置默认角色"按钮进行配置
</div> </div>
</template> </template>
</ElTable> </ElTable>
@@ -243,19 +265,13 @@
</template> </template>
</ElDialog> </ElDialog>
<!-- 添加角色对话框 --> <!-- 设置默认角色对话框 -->
<ElDialog <ElDialog v-model="addRoleDialogVisible" title="设置默认角色" width="50%" append-to-body>
v-model="addRoleDialogVisible"
title="添加默认角色"
width="600px"
append-to-body
>
<div v-loading="rolesLoading"> <div v-loading="rolesLoading">
<ElSelect <ElSelect
v-model="selectedRoleIds" v-model="selectedRoleId"
multiple
filterable filterable
placeholder="请选择要添加的角色" placeholder="请选择默认角色"
style="width: 100%" style="width: 100%"
> >
<ElOption <ElOption
@@ -263,23 +279,15 @@
:key="role.role_id" :key="role.role_id"
:label="role.role_name" :label="role.role_name"
:value="role.role_id" :value="role.role_id"
:disabled="isRoleAlreadyAssigned(role.role_id)"
> >
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;"> <div style="display: flex; gap: 8px; align-items: center">
<div style="display: flex; gap: 8px; align-items: center;"> <span>{{ role.role_name }}</span>
<span>{{ role.role_name }}</span> <ElTag type="success" size="small">客户角色</ElTag>
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag>
</div>
<span v-if="isRoleAlreadyAssigned(role.role_id)" style="color: #909399; font-size: 12px;">
已添加
</span>
</div> </div>
</ElOption> </ElOption>
</ElSelect> </ElSelect>
<div style="margin-top: 8px; color: #909399; font-size: 12px;"> <div style="margin-top: 8px; color: #909399; font-size: 12px">
支持多选已添加的角色将显示为禁用状态 只能选择一个客户角色设置后将覆盖之前的默认角色
</div> </div>
</div> </div>
<template #footer> <template #footer>
@@ -334,6 +342,8 @@
const parentShopLoading = ref(false) const parentShopLoading = ref(false)
const parentShopList = ref<ShopResponse[]>([]) const parentShopList = ref<ShopResponse[]>([])
const searchParentShopList = ref<ShopResponse[]>([]) const searchParentShopList = ref<ShopResponse[]>([])
const defaultRoleLoading = ref(false)
const defaultRoleList = ref<any[]>([])
// 默认角色管理相关状态 // 默认角色管理相关状态
const defaultRolesDialogVisible = ref(false) const defaultRolesDialogVisible = ref(false)
@@ -344,7 +354,7 @@
const currentShop = ref<ShopResponse | null>(null) const currentShop = ref<ShopResponse | null>(null)
const currentDefaultRoles = ref<ShopRoleResponse[]>([]) const currentDefaultRoles = ref<ShopRoleResponse[]>([])
const availableRoles = ref<ShopRoleResponse[]>([]) const availableRoles = ref<ShopRoleResponse[]>([])
const selectedRoleIds = ref<number[]>([]) const selectedRoleId = ref<number | undefined>(undefined)
// 右键菜单 // 右键菜单
const shopOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>() const shopOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
@@ -474,6 +484,9 @@
const showDialog = (type: string, row?: ShopResponse) => { const showDialog = (type: string, row?: ShopResponse) => {
dialogType.value = type dialogType.value = type
// 先清除验证状态
formRef.value?.clearValidate()
if (type === 'edit' && row) { if (type === 'edit' && row) {
formData.id = row.id formData.id = row.id
formData.shop_name = row.shop_name formData.shop_name = row.shop_name
@@ -489,6 +502,7 @@
formData.init_username = '' formData.init_username = ''
formData.init_password = '' formData.init_password = ''
formData.init_phone = '' formData.init_phone = ''
formData.default_role_id = undefined
} else { } else {
formData.id = 0 formData.id = 0
formData.shop_name = '' formData.shop_name = ''
@@ -504,9 +518,10 @@
formData.init_username = '' formData.init_username = ''
formData.init_password = '' formData.init_password = ''
formData.init_phone = '' formData.init_phone = ''
formData.default_role_id = undefined
} }
// 重置表单验证状态 // 再次确保清除验证状态
nextTick(() => { nextTick(() => {
formRef.value?.clearValidate() formRef.value?.clearValidate()
}) })
@@ -540,12 +555,13 @@
{ {
prop: 'shop_name', prop: 'shop_name',
label: '店铺名称', label: '店铺名称',
minWidth: 120 minWidth: 160,
showOverflowTooltip: true
}, },
{ {
prop: 'shop_code', prop: 'shop_code',
label: '店铺编号', label: '店铺编号',
width: 150, width: 140,
showOverflowTooltip: true showOverflowTooltip: true
}, },
{ {
@@ -560,6 +576,7 @@
prop: 'region', prop: 'region',
label: '所在地区', label: '所在地区',
minWidth: 170, minWidth: 170,
showOverflowTooltip: true,
formatter: (row: ShopResponse) => { formatter: (row: ShopResponse) => {
const parts: string[] = [] const parts: string[] = []
if (row.province) parts.push(row.province) if (row.province) parts.push(row.province)
@@ -652,13 +669,39 @@
status: CommonStatus.ENABLED, status: CommonStatus.ENABLED,
init_username: '', init_username: '',
init_password: '', init_password: '',
init_phone: '' init_phone: '',
default_role_id: undefined as number | undefined
}) })
// 搜索默认角色(仅加载客户角色)
const searchDefaultRoles = async (query: string) => {
defaultRoleLoading.value = true
try {
const params: any = {
page: 1,
pageSize: 20,
role_type: 2, // 仅客户角色
status: 1 // 仅启用的角色
}
if (query) {
params.role_name = query
}
const res = await RoleService.getRoles(params)
if (res.code === 0) {
defaultRoleList.value = res.data.items || []
}
} catch (error) {
console.error('获取默认角色列表失败:', error)
} finally {
defaultRoleLoading.value = false
}
}
onMounted(() => { onMounted(() => {
getShopList() getShopList()
loadParentShopList() loadParentShopList()
loadSearchParentShopList() loadSearchParentShopList()
searchDefaultRoles('') // 加载初始默认角色列表
}) })
// 加载上级店铺列表(用于新增对话框,默认加载20条) // 加载上级店铺列表(用于新增对话框,默认加载20条)
@@ -810,6 +853,9 @@
{ required: true, message: '请输入初始账号手机号', trigger: 'blur' }, { required: true, message: '请输入初始账号手机号', trigger: 'blur' },
{ len: 11, message: '手机号必须为 11 位', trigger: 'blur' }, { len: 11, message: '手机号必须为 11 位', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' } { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
],
default_role_id: [
{ required: true, message: '请选择默认角色', trigger: 'blur' }
] ]
}) })
@@ -827,7 +873,8 @@
shop_code: formData.shop_code, shop_code: formData.shop_code,
init_username: formData.init_username, init_username: formData.init_username,
init_password: formData.init_password, init_password: formData.init_password,
init_phone: formData.init_phone init_phone: formData.init_phone,
default_role_id: formData.default_role_id
} }
// 可选字段 // 可选字段
@@ -985,31 +1032,35 @@
} }
} }
// 显示添加角色对话框 // 显示设置默认角色对话框
const showAddRoleDialog = async () => { const showAddRoleDialog = async () => {
addRoleDialogVisible.value = true addRoleDialogVisible.value = true
selectedRoleIds.value = [] // 如果已有默认角色,预选第一个
selectedRoleId.value = currentDefaultRoles.value.length > 0
? currentDefaultRoles.value[0].role_id
: undefined
await loadAvailableRoles() await loadAvailableRoles()
} }
// 加载可用角色列表 // 加载可用角色列表(仅客户角色)
const loadAvailableRoles = async () => { const loadAvailableRoles = async () => {
rolesLoading.value = true rolesLoading.value = true
try { try {
const res = await RoleService.getRoles({ const res = await RoleService.getRoles({
page: 1, page: 1,
page_size: 9999, page_size: 9999,
role_type: 2, // 仅客户角色
status: 1 // RoleStatus.ENABLED status: 1 // RoleStatus.ENABLED
}) })
if (res.code === 0) { if (res.code === 0) {
// 将 PlatformRole 转换为与 ShopRoleResponse 兼容的格式,同时保留 role_type // 将 PlatformRole 转换为与 ShopRoleResponse 兼容的格式
availableRoles.value = (res.data.items || []).map((role) => ({ availableRoles.value = (res.data.items || []).map((role) => ({
...role, ...role,
role_id: role.ID, role_id: role.ID,
role_name: role.role_name, role_name: role.role_name,
role_desc: role.role_desc, role_desc: role.role_desc,
role_type: role.role_type, // 保留角色类型用于显示 role_type: role.role_type,
shop_id: 0 // 这个值在可用角色列表中不使用 shop_id: 0
})) }))
} }
} catch (error) { } catch (error) {
@@ -1020,15 +1071,10 @@
} }
} }
// 判断角色是否已分配 // 设置默认角色
const isRoleAlreadyAssigned = (roleId: number) => {
return currentDefaultRoles.value.some((r) => r.role_id === roleId)
}
// 添加默认角色
const handleAddDefaultRoles = async () => { const handleAddDefaultRoles = async () => {
if (selectedRoleIds.value.length === 0) { if (!selectedRoleId.value) {
ElMessage.warning('请至少选择一个角色') ElMessage.warning('请选择默认角色')
return return
} }
@@ -1039,17 +1085,18 @@
addRoleLoading.value = true addRoleLoading.value = true
try { try {
// 传递数组但只包含一个角色ID
const res = await ShopService.assignShopRoles(currentShop.value.id, { const res = await ShopService.assignShopRoles(currentShop.value.id, {
role_ids: selectedRoleIds.value role_ids: [selectedRoleId.value]
}) })
if (res.code === 0) { if (res.code === 0) {
ElMessage.success('添加默认角色成功') ElMessage.success('设置默认角色成功')
addRoleDialogVisible.value = false addRoleDialogVisible.value = false
// 刷新默认角色列表 // 刷新默认角色列表
await loadShopDefaultRoles(currentShop.value.id) await loadShopDefaultRoles(currentShop.value.id)
} }
} catch (error) { } catch (error) {
console.error('添加默认角色失败:', error) console.error('设置默认角色失败:', error)
} finally { } finally {
addRoleLoading.value = false addRoleLoading.value = false
} }

View File

@@ -127,16 +127,10 @@
<template #default="{ node, data }"> <template #default="{ node, data }">
<span style="display: flex; align-items: center; gap: 8px"> <span style="display: flex; align-items: center; gap: 8px">
<span>{{ node.label }}</span> <span>{{ node.label }}</span>
<ElTag <ElTag :type="data.perm_type === 1 ? 'info' : 'success'" size="small">
:type="data.perm_type === 1 ? 'info' : 'success'"
size="small"
>
{{ data.perm_type === 1 ? '菜单' : '按钮' }} {{ data.perm_type === 1 ? '菜单' : '按钮' }}
</ElTag> </ElTag>
<ElTag <ElTag :type="data.status === 1 ? 'success' : 'info'" size="small">
:type="data.status === 1 ? 'success' : 'info'"
size="small"
>
{{ data.status === 1 ? '启用' : '禁用' }} {{ data.status === 1 ? '启用' : '禁用' }}
</ElTag> </ElTag>
</span> </span>
@@ -183,16 +177,10 @@
<span class="tree-node-content"> <span class="tree-node-content">
<span class="tree-node-label"> <span class="tree-node-label">
<span>{{ node.label }}</span> <span>{{ node.label }}</span>
<ElTag <ElTag :type="data.perm_type === 1 ? 'info' : 'success'" size="small">
:type="data.perm_type === 1 ? 'info' : 'success'"
size="small"
>
{{ data.perm_type === 1 ? '菜单' : '按钮' }} {{ data.perm_type === 1 ? '菜单' : '按钮' }}
</ElTag> </ElTag>
<ElTag <ElTag :type="data.status === 1 ? 'success' : 'info'" size="small">
:type="data.status === 1 ? 'success' : 'info'"
size="small"
>
{{ data.status === 1 ? '启用' : '禁用' }} {{ data.status === 1 ? '启用' : '禁用' }}
</ElTag> </ElTag>
</span> </span>
@@ -231,7 +219,9 @@
ElTree, ElTree,
ElSwitch, ElSwitch,
ElButton, ElButton,
ElInput ElInput,
ElSelect,
ElOption
} from 'element-plus' } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import type { PlatformRole, PermissionTreeNode } from '@/types/api' import type { PlatformRole, PermissionTreeNode } from '@/types/api'
@@ -268,7 +258,9 @@
// 搜索表单初始值 // 搜索表单初始值
const initialSearchState = { const initialSearchState = {
role_name: '' role_name: '',
role_type: undefined as number | undefined,
status: undefined as number | undefined
} }
// 搜索表单 // 搜索表单
@@ -284,6 +276,32 @@
clearable: true, clearable: true,
placeholder: '请输入角色名称' placeholder: '请输入角色名称'
} }
},
{
label: '角色类型',
prop: 'role_type',
type: 'select',
config: {
clearable: true,
placeholder: '请选择角色类型'
},
options: () => [
{ label: '平台角色', value: 1 },
{ label: '客户角色', value: 2 }
]
},
{
label: '状态',
prop: 'status',
type: 'select',
config: {
clearable: true,
placeholder: '请选择状态'
},
options: () => [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 }
]
} }
] ]
@@ -373,13 +391,13 @@
{ {
prop: 'CreatedAt', prop: 'CreatedAt',
label: '创建时间', label: '创建时间',
minWidth: 180, width: 180,
formatter: (row: any) => formatDateTime(row.CreatedAt) formatter: (row: any) => formatDateTime(row.CreatedAt)
}, },
{ {
prop: 'operation', prop: 'operation',
label: '操作', label: '操作',
width: 200, width: 240,
fixed: 'right', fixed: 'right',
formatter: (row: any) => { formatter: (row: any) => {
const buttons = [] const buttons = []
@@ -388,7 +406,7 @@
if (hasAuth('role:permission')) { if (hasAuth('role:permission')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
icon: '&#xe72b;', text: '分配权限',
onClick: () => showPermissionDialog(row) onClick: () => showPermissionDialog(row)
}) })
) )
@@ -398,7 +416,7 @@
if (hasAuth('role:edit')) { if (hasAuth('role:edit')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'edit', text: '编辑',
onClick: () => showDialog('edit', row) onClick: () => showDialog('edit', row)
}) })
) )
@@ -408,7 +426,7 @@
if (hasAuth('role:delete')) { if (hasAuth('role:delete')) {
buttons.push( buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'delete', text: '删除',
onClick: () => deleteRole(row) onClick: () => deleteRole(row)
}) })
) )
@@ -613,14 +631,14 @@
// 保存右侧树的展开节点 // 保存右侧树的展开节点
const expandedKeys = rightTreeRef.value?.store?.nodesMap const expandedKeys = rightTreeRef.value?.store?.nodesMap
? Object.keys(rightTreeRef.value.store.nodesMap) ? Object.keys(rightTreeRef.value.store.nodesMap)
.filter(key => rightTreeRef.value.store.nodesMap[key].expanded) .filter((key) => rightTreeRef.value.store.nodesMap[key].expanded)
.map(key => Number(key)) .map((key) => Number(key))
: [] : []
await RoleService.removePermission(currentRoleId.value, permId) await RoleService.removePermission(currentRoleId.value, permId)
// 更新已选权限列表 // 更新已选权限列表
selectedPermissions.value = selectedPermissions.value.filter(id => id !== permId) selectedPermissions.value = selectedPermissions.value.filter((id) => id !== permId)
// 重新构建左右两侧树 // 重新构建左右两侧树
const fullTreeData = buildTreeData(originalPermissionTree.value) const fullTreeData = buildTreeData(originalPermissionTree.value)
@@ -641,7 +659,7 @@
// 等待DOM更新后恢复展开状态 // 等待DOM更新后恢复展开状态
await nextTick() await nextTick()
if (rightTreeRef.value && expandedKeys.length > 0) { if (rightTreeRef.value && expandedKeys.length > 0) {
expandedKeys.forEach(key => { expandedKeys.forEach((key) => {
const node = rightTreeRef.value.store.nodesMap[key] const node = rightTreeRef.value.store.nodesMap[key]
if (node && !node.isLeaf) { if (node && !node.isLeaf) {
node.expanded = true node.expanded = true
@@ -705,7 +723,6 @@
} }
} }
// 获取角色列表 // 获取角色列表
const getTableData = async () => { const getTableData = async () => {
loading.value = true loading.value = true
@@ -713,7 +730,9 @@
const params = { const params = {
page: pagination.page, page: pagination.page,
pageSize: pagination.pageSize, pageSize: pagination.pageSize,
role_name: searchForm.role_name || undefined role_name: searchForm.role_name || undefined,
role_type: searchForm.role_type,
status: searchForm.status
} }
const res = await RoleService.getRoles(params) const res = await RoleService.getRoles(params)
if (res.code === 0) { if (res.code === 0) {