修改工单
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": "代理商管理",
"customerAccount": "客户账号",
"enterpriseCustomer": "企业客户",
"enterpriseCustomerAccounts": "客户账号列表",
"enterpriseCustomerAccounts": "关联账号列表",
"enterpriseCards": "企业卡管理",
"customerCommission": "客户账号佣金"
},

View File

@@ -296,6 +296,15 @@ export const asyncRoutes: AppRouteRecord[] = [
roles: ['R_SUPER']
}
},
{
path: 'carrier-management',
name: 'CarrierManagement',
component: RoutesAlias.CarrierManagement,
meta: {
title: 'menus.account.carrierManagement',
keepAlive: true
}
},
{
path: 'user-center',
name: 'UserCenter',
@@ -611,15 +620,15 @@ export const asyncRoutes: AppRouteRecord[] = [
// }
// },
// 物联网卡管理系统模块
{
path: '/card-management',
name: 'CardManagement',
component: RoutesAlias.Home,
meta: {
title: 'menus.cardManagement.title',
icon: ''
},
children: [
// {
// path: '/card-management',
// name: 'CardManagement',
// component: RoutesAlias.Home,
// meta: {
// title: 'menus.cardManagement.title',
// icon: ''
// },
// children: [
// {
// path: 'card-detail',
// name: 'CardDetail',
@@ -629,15 +638,15 @@ export const asyncRoutes: AppRouteRecord[] = [
// keepAlive: true
// }
// },
{
path: 'single-card',
name: 'SingleCard',
component: RoutesAlias.SingleCard,
meta: {
title: 'menus.cardManagement.singleCard',
keepAlive: true
}
},
// {
// path: 'single-card',
// name: 'SingleCard',
// component: RoutesAlias.SingleCard,
// meta: {
// title: 'menus.cardManagement.singleCard',
// keepAlive: true
// }
// },
// {
// path: 'card-assign',
// name: 'CardAssign',
@@ -701,8 +710,8 @@ export const asyncRoutes: AppRouteRecord[] = [
// keepAlive: true
// }
// }
]
},
// ]
// },
{
path: '/package-management',
name: 'PackageManagement',
@@ -969,6 +978,15 @@ export const asyncRoutes: AppRouteRecord[] = [
icon: ''
},
children: [
{
path: 'single-card',
name: 'SingleCard',
component: RoutesAlias.SingleCard,
meta: {
title: 'menus.cardManagement.singleCard',
keepAlive: true
}
},
{
path: 'iot-card-management',
name: 'StandaloneCardList',
@@ -1072,15 +1090,6 @@ export const asyncRoutes: AppRouteRecord[] = [
// keepAlive: true
// }
// },
{
path: 'carrier-management',
name: 'CarrierManagement',
component: RoutesAlias.CarrierManagement,
meta: {
title: 'menus.account.carrierManagement',
keepAlive: true
}
},
{
path: 'orders',
name: 'OrderManagement',
@@ -1089,7 +1098,7 @@ export const asyncRoutes: AppRouteRecord[] = [
title: 'menus.account.orders',
keepAlive: true
}
},
}
// {
// path: 'my-account',
// name: 'MyAccount',
@@ -1099,10 +1108,12 @@ export const asyncRoutes: AppRouteRecord[] = [
// keepAlive: true
// }
// }
]
},
{
path: 'commission',
path: '/commission',
name: 'CommissionManagement',
component: '',
component: RoutesAlias.Home,
meta: {
title: 'menus.commission.menu.management',
icon: ''
@@ -1150,8 +1161,6 @@ export const asyncRoutes: AppRouteRecord[] = [
}
]
}
]
}
// {
// path: '/settings',
// name: 'Settings',

View File

@@ -68,44 +68,6 @@
>
<ElOption label="超级管理员" :value="1" />
<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>
</ElFormItem>
</ElForm>
@@ -124,6 +86,9 @@
<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>
@@ -142,18 +107,11 @@
</div>
<div class="panel-body">
<ElCheckboxGroup v-model="rolesToAdd" class="role-list">
<div
v-for="role in filteredAvailableRoles"
:key="role.ID"
class="role-item"
>
<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"
>
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag>
</span>
@@ -196,19 +154,11 @@
>
<span class="role-info">
<span>{{ role.role_name }}</span>
<ElTag
:type="role.role_type === 1 ? 'primary' : 'success'"
size="small"
>
<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 type="danger" size="small" link @click="removeSingleRole(role.ID)">
移除
</ElButton>
</div>
@@ -256,6 +206,7 @@
const roleSubmitLoading = 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[]>([])
@@ -517,7 +468,7 @@
{
prop: 'operation',
label: '操作',
width: 200,
width: 240,
fixed: 'right',
formatter: (row: any) => {
const buttons = []
@@ -525,7 +476,7 @@
if (hasAuth('account:patch_role')) {
buttons.push(
h(ArtButtonTable, {
icon: '&#xe72b;',
text: '分配角色',
onClick: () => showRoleDialog(row)
})
)
@@ -534,7 +485,7 @@
if (hasAuth('account:edit')) {
buttons.push(
h(ArtButtonTable, {
type: 'edit',
text: '编辑',
onClick: () => showDialog('edit', row)
})
)
@@ -543,7 +494,7 @@
if (hasAuth('account:delete')) {
buttons.push(
h(ArtButtonTable, {
type: 'delete',
text: '删除',
onClick: () => deleteAccount(row)
})
)
@@ -593,11 +544,23 @@
}
}
// 计算属性:过滤后的可分配角色
// 计算属性:过滤后的可分配角色(根据账号类型过滤)
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()
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) => {
currentAccountId.value = row.id
currentAccountName.value = row.username
currentAccountType.value = row.user_type
selectedRoles.value = []
rolesToAdd.value = []
leftRoleFilter.value = ''
@@ -641,18 +605,31 @@
if (rolesToAdd.value.length === 0) return
try {
// 将选中的角色添加到已分配列表
const newRoles = [...new Set([...selectedRoles.value, ...rolesToAdd.value])]
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('角色添加成功')
ElMessage.success('角色分配成功')
// 刷新列表以更新角色显示
await getAccountList()
} catch (error) {
console.error('添加角色失败:', error)
console.error('分配角色失败:', error)
}
}

View File

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

View File

@@ -188,26 +188,6 @@
@close="handleRecallDialogClose"
>
<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">
<ElRadioGroup v-model="recallForm.selection_type">
<ElRadio label="list">ICCID列表</ElRadio>
@@ -755,7 +735,6 @@
// 批量回收表单
const recallForm = reactive<Partial<RecallStandaloneCardsRequest>>({
selection_type: 'list',
from_shop_id: undefined,
iccids: [],
iccid_start: '',
iccid_end: '',
@@ -766,7 +745,6 @@
// 批量回收表单验证规则
const recallRules = reactive<FormRules>({
from_shop_id: [{ required: true, message: '请选择来源店铺', trigger: 'change' }],
selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }],
iccid_start: [
{
@@ -1285,7 +1263,6 @@
recallDialogVisible.value = true
Object.assign(recallForm, {
selection_type: 'list',
from_shop_id: undefined,
iccids: selectedCards.value.map((card) => card.iccid),
iccid_start: '',
iccid_end: '',
@@ -1293,8 +1270,6 @@
batch_no: '',
remark: ''
})
// 加载默认店铺列表
loadFromShops()
if (recallFormRef.value) {
recallFormRef.value.resetFields()
}
@@ -1378,7 +1353,6 @@
// 根据选卡方式构建请求参数
const params: Partial<RecallStandaloneCardsRequest> = {
selection_type: recallForm.selection_type!,
from_shop_id: recallForm.from_shop_id!,
remark: recallForm.remark
}

View File

@@ -1,45 +1,89 @@
<template>
<ArtTableFullScreen>
<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">
<template #header>
<span>网卡信息</span>
</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">
<ElCol :span="8">
<ElFormItem label="ICCID:">
<span>{{ cardInfo.iccid }}</span>
<span>{{ cardInfo.iccid || '-' }}</span>
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="IMSI:">
<span>{{ cardInfo.imsi }}</span>
<span>{{ cardInfo.imsi || '-' }}</span>
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="手机号码:">
<span>{{ cardInfo.msisdn }}</span>
<span>{{ cardInfo.msisdn || '-' }}</span>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="24">
<ElCol :span="8">
<ElFormItem label="运营商:">
<ElTag :type="getOperatorTagType(cardInfo.operator)">{{
cardInfo.operatorName
}}</ElTag>
<ElTag v-if="cardInfo.operatorName" :type="getOperatorTagType(cardInfo.operator)">
{{ cardInfo.operatorName }}
</ElTag>
<span v-else>-</span>
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="网络类型:">
<span>{{ cardInfo.networkType }}</span>
<span>{{ cardInfo.networkType || '-' }}</span>
</ElFormItem>
</ElCol>
<ElCol :span="8">
<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>
</ElCol>
</ElRow>
@@ -47,7 +91,7 @@
</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>
<span>操作区域</span>
</template>
@@ -66,7 +110,7 @@
</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>
<span>流量信息</span>
</template>
@@ -100,7 +144,7 @@
</ElCard>
<!-- 使用记录 -->
<ElCard shadow="never" class="art-table-card">
<ElCard v-if="hasSearched" shadow="never" class="art-table-card">
<template #header>
<span>使用记录</span>
<ElButton style="float: right" @click="refreshUsageRecords">刷新</ElButton>
@@ -158,6 +202,7 @@
import { ElTag, ElMessageBox, ElMessage, FormInstance } from 'element-plus'
import { useRoute } from 'vue-router'
import type { FormRules } from 'element-plus'
import { CardService } from '@/api/modules'
defineOptions({ name: 'SingleCard' })
@@ -165,26 +210,36 @@
const rechargeDialogVisible = ref(false)
const route = useRoute()
// 网卡信息
const cardInfo = reactive({
iccid: '89860123456789012345',
imsi: '460012345678901',
msisdn: '13800138001',
operator: 'mobile',
operatorName: '中国移动',
networkType: '4G',
status: '1',
statusName: '激活',
activatedDate: '2024-01-15',
expiryDate: '2025-01-15'
// ICCID搜索相关
const searchIccid = ref('')
const hasSearched = ref(false)
// 格式化显示的ICCID4位一组用横杠分隔
const formattedIccid = computed(() => {
if (!cardInfo.iccid) return ''
return cardInfo.iccid.replace(/(\d{4})(?=\d)/g, '$1-')
})
// 流量信息
// 网卡信息 - 默认为空
const cardInfo = reactive({
iccid: '',
imsi: '',
msisdn: '',
operator: '',
operatorName: '',
networkType: '',
status: '',
statusName: '',
activatedDate: '',
expiryDate: ''
})
// 流量信息 - 默认为空
const trafficInfo = reactive({
totalTraffic: '10GB',
usedTraffic: '2.5GB',
remainingTraffic: '7.5GB',
usagePercentage: '25'
totalTraffic: '0MB',
usedTraffic: '0MB',
remainingTraffic: '0MB',
usagePercentage: '0'
})
const pagination = reactive({
@@ -193,25 +248,8 @@
total: 0
})
// 使用记录
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 usageRecords = ref([])
// 充值表单
const rechargeForm = reactive({
@@ -394,40 +432,107 @@
pagination.total = usageRecords.value.length
})
// 处理ICCID搜索
const handleSearchCard = async () => {
if (!searchIccid.value.trim()) {
ElMessage.warning('请输入ICCID')
return
}
await loadCardInfoByIccid(searchIccid.value.trim())
}
// 根据ICCID加载卡片信息
const loadCardInfoByIccid = async (iccid: string) => {
loading.value = true
try {
// 这里应该调用API根据ICCID获取卡片详细信息
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
const response = await CardService.getIotCardDetailByIccid(iccid)
// 模拟更新卡片信息实际应该从API获取
if (response.code === 200 && response.data) {
const data = response.data
hasSearched.value = true
// 更新网卡基本信息
Object.assign(cardInfo, {
iccid: iccid,
imsi: '460012345678901',
msisdn: '13800138001',
operator: 'mobile',
operatorName: '中国移动',
networkType: '4G',
status: '1',
statusName: '激活',
activatedDate: '2024-01-15',
expiryDate: '2025-01-15'
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 || ''
})
ElMessage.success(`已加载ICCID ${iccid} 的详细信息`)
} catch (error) {
// 更新流量信息
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)
ElMessage.error('获取卡片信息失败')
ElMessage.error(error?.message || '获取卡片信息失败')
} finally {
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>
<style lang="scss" scoped>
.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 {
display: flex;
flex-wrap: wrap;

View File

@@ -4,7 +4,9 @@
<ElCard shadow="never">
<div class="detail-header">
<ElButton @click="handleBack">
<template #icon><ElIcon><ArrowLeft /></ElIcon></template>
<template #icon
><ElIcon><ArrowLeft /></ElIcon
></template>
返回
</ElButton>
<h2 class="detail-title">{{ pageTitle }}</h2>
@@ -19,6 +21,13 @@
@search="handleSearch"
></ArtSearchBar>
<!-- 操作按钮 -->
<div style="margin: 10px 0">
<ElButton @click="showAddAccountDialog">
{{ isShopType ? '新增店铺账号' : '新增企业账号' }}
</ElButton>
</div>
<!-- 表格 -->
<ArtTable
ref="tableRef"
@@ -28,28 +37,176 @@
:currentPage="pagination.page"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"v
:marginTop="10"
v
height="60vh"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || (col as any).type" v-bind="col" />
<ElTableColumn
v-for="col in columns"
:key="col.prop || (col as any).type"
v-bind="col"
/>
</template>
</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>
</div>
</ArtTableFullScreen>
</template>
<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 { 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 { 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 { PlatformAccount } from '@/types/api'
import type { PlatformAccount, PlatformRole } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'CommonAccountList' })
@@ -86,6 +243,17 @@
const tableRef = ref()
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 = {
username: '',
@@ -124,19 +292,6 @@
placeholder: '请输入手机号'
}
},
{
label: '用户类型',
prop: 'user_type',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '代理账号', value: 3 },
{ label: '企业账号', value: 4 }
]
},
{
label: '状态',
prop: 'status',
@@ -163,52 +318,74 @@
}
// 列配置
const columns = computed(() => [
const columns = computed(() => {
const baseColumns = [
{
prop: 'username',
label: '用户名',
minWidth: 150
label: '用户名'
},
{
prop: 'phone',
label: '手机号',
width: 130
label: '手机号'
},
{
prop: 'user_type',
label: '用户类型',
width: 110,
formatter: (row: PlatformAccount) => {
return h(ElTag, { type: getUserTypeTag(row.user_type) }, () => getUserTypeName(row.user_type))
return h(ElTag, { type: getUserTypeTag(row.user_type) }, () =>
getUserTypeName(row.user_type)
)
}
},
{
prop: 'shop_id',
label: '店铺ID',
width: 100,
formatter: (row: PlatformAccount) => row.shop_id || '-'
},
{
prop: 'enterprise_id',
label: '企业ID',
width: 100,
formatter: (row: PlatformAccount) => row.enterprise_id || '-'
},
}
]
// 根据页面类型添加不同的列
if (isShopType.value) {
// 店铺类型:显示店铺名称
baseColumns.push({
prop: 'shop_name',
label: '店铺名称',
formatter: (row: PlatformAccount) => (row as any).shop_name || '-'
})
} else {
// 企业类型:显示企业名称
baseColumns.push({
prop: 'enterprise_name',
label: '企业名称',
formatter: (row: PlatformAccount) => (row as any).enterprise_name || '-'
})
}
// 添加状态和创建时间
baseColumns.push(
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: PlatformAccount) => {
return h(ElTag, { type: getStatusTag(row.status) }, () => getStatusName(row.status))
}
},
{
prop: 'CreatedAt',
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: PlatformAccount) => formatDateTime(row.CreatedAt)
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 () => {
@@ -220,7 +397,7 @@
pageSize: pagination.pageSize,
username: searchForm.username || undefined,
phone: searchForm.phone || undefined,
user_type: searchForm.user_type,
user_type: isShopType.value ? 3 : 4, // 根据页面类型自动设置: 3:代理账号, 4:企业账号
status: searchForm.status,
[filterParamKey.value]: entityId // 动态设置 shop_id 或 enterprise_id
}
@@ -274,8 +451,214 @@
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(() => {
getTableData()
loadAllRoles()
})
</script>
@@ -285,9 +668,7 @@
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #e4e7ed;
.detail-title {
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>

View File

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

View File

@@ -1,5 +1,43 @@
<template>
<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">
<!-- 主要内容区域 -->
@@ -150,12 +188,12 @@
min-width="200"
show-overflow-tooltip
/>
<ElTableColumn prop="packageType" label="类型" width="100" />
<ElTableColumn prop="totalFlow" label="总流量" width="100" />
<ElTableColumn prop="usedFlow" label="已用" width="100" />
<ElTableColumn prop="remainFlow" label="剩余" width="100" />
<ElTableColumn prop="expireTime" label="到期时间" width="120" />
<ElTableColumn prop="status" label="状态" width="100">
<ElTableColumn prop="packageType" label="类型" />
<ElTableColumn prop="totalFlow" label="总流量" />
<ElTableColumn prop="usedFlow" label="已用" />
<ElTableColumn prop="remainFlow" label="剩余" />
<ElTableColumn prop="expireTime" label="到期时间" />
<ElTableColumn prop="status" label="状态">
<template #default="scope">
<ElTag :type="getPackageStatusType(scope.row.status)" size="small">
{{ scope.row.status }}
@@ -287,15 +325,10 @@
</div>
</div>
<!-- 加载状态 -->
<!--<div v-else-if="loading" class="loading-state">-->
<!-- <ElSkeleton :rows="10" animated />-->
<!--</div>-->
<!-- 空状态 -->
<!--<div v-else class="empty-state">-->
<!-- <ElEmpty description="暂无卡片数据" />-->
<!--</div>-->
<div v-else class="empty-state">
<ElEmpty description="请在上方输入ICCID进行查询" />
</div>
</div>
</template>
@@ -314,13 +347,30 @@
} from 'element-plus'
import { useRoute } from 'vue-router'
import { EnterpriseService } from '@/api/modules/enterprise'
import { CardService } from '@/api/modules'
defineOptions({ name: 'SingleCard' })
const route = useRoute()
const loading = ref(true)
const loading = 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 获取参数
const enterpriseId = computed(() => {
const id = route.query.enterpriseId || route.query.enterprise_id
@@ -336,10 +386,139 @@
return cardInfo.value?.id ? Number(cardInfo.value.id) : null
})
// 卡片信息
// 卡片信息 - 默认为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 = {
id: 1, // 卡片ID
iccid: '8986062357007989203',
@@ -381,24 +560,9 @@
]
}
// 获取卡片详情
const fetchCardDetail = async () => {
try {
loading.value = true
// 模拟API调用
setTimeout(() => {
cardInfo.value = { ...mockCardData }
loading.value = false
}, 500)
} catch (error) {
ElMessage.error('获取卡片详情失败')
loading.value = false
}
}
// 页面初始化时加载数据
// 页面初始化 - 不自动加载数据,等待用户输入ICCID查询
onMounted(() => {
fetchCardDetail()
// 不再自动加载模拟数据,等待用户查询
})
// 获取状态标签类型
@@ -550,6 +714,89 @@
<style lang="scss" scoped>
.single-card-page {
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 {
&.slide-in {

View File

@@ -18,7 +18,9 @@
@refresh="handleRefresh"
>
<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>
</ArtTableHeader>
@@ -130,7 +132,11 @@
reserve-keyword
:remote-method="handlePackageSearch"
: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
style="width: 100%"
>
@@ -627,14 +633,14 @@
formatter: (row: Order) => {
return h('div', { style: 'display: flex; gap: 8px;' }, [
h(ArtButtonTable, {
icon: '&#xe6cf;',
text: '详情',
tooltip: t('orderManagement.actions.viewDetail'),
onClick: () => showOrderDetail(row)
}),
// 只有待支付和已支付的订单可以取消
row.payment_status === 1 || row.payment_status === 2
? h(ArtButtonTable, {
type: 'delete',
text: '删除',
tooltip: t('orderManagement.actions.cancel'),
onClick: () => handleCancelOrder(row)
})

View File

@@ -25,26 +25,26 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { ShopPackageAllocationService } from '@/api/modules'
import type { ShopPackageAllocationResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { ShopPackageAllocationService } from '@/api/modules'
import type { ShopPackageAllocationResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'PackageAssignDetail' })
defineOptions({ name: 'PackageAssignDetail' })
const route = useRoute()
const router = useRouter()
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const detailData = ref<ShopPackageAllocationResponse | null>(null)
const loading = ref(false)
const detailData = ref<ShopPackageAllocationResponse | null>(null)
// 详情页配置
const detailSections: DetailSection[] = [
// 详情页配置
const detailSections: DetailSection[] = [
{
title: '基本信息',
fields: [
@@ -78,15 +78,15 @@ const detailSections: DetailSection[] = [
{ label: '更新时间', prop: 'updated_at', formatter: (value) => formatDateTime(value) }
]
}
]
]
// 返回上一页
const handleBack = () => {
// 返回上一页
const handleBack = () => {
router.back()
}
}
// 获取详情数据
const fetchDetail = async () => {
// 获取详情数据
const fetchDetail = async () => {
const id = Number(route.params.id)
if (!id) {
ElMessage.error('缺少ID参数')
@@ -105,25 +105,23 @@ const fetchDetail = async () => {
} finally {
loading.value = false
}
}
}
onMounted(() => {
onMounted(() => {
fetchDetail()
})
})
</script>
<style scoped lang="scss">
.package-assign-detail {
.package-assign-detail {
padding: 20px;
}
}
.detail-header {
.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;
@@ -131,9 +129,9 @@ onMounted(() => {
font-weight: 600;
color: var(--el-text-color-primary);
}
}
}
.loading-container {
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
@@ -145,5 +143,5 @@ onMounted(() => {
.el-icon {
font-size: 32px;
}
}
}
</style>

View File

@@ -19,7 +19,9 @@
@refresh="handleRefresh"
>
<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>
</ArtTableHeader>
@@ -113,7 +115,12 @@
<script setup lang="ts">
import { h } from 'vue'
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 type { FormInstance, FormRules } from 'element-plus'
import type { ShopPackageAllocationResponse, PackageResponse, ShopResponse } from '@/types/api'
@@ -386,14 +393,14 @@
{
prop: 'operation',
label: '操作',
width: 200,
width: 220,
fixed: 'right',
formatter: (row: ShopPackageAllocationResponse) => {
const buttons = []
buttons.push(
h(ArtButtonTable, {
type:"view",
text: '详情',
onClick: () => handleViewDetail(row)
})
)
@@ -401,7 +408,7 @@
if (hasAuth('package_assign:edit')) {
buttons.push(
h(ArtButtonTable, {
type:"edit",
text: '编辑',
onClick: () => showDialog('edit', row)
})
)
@@ -410,7 +417,7 @@
if (hasAuth('package_assign:delete')) {
buttons.push(
h(ArtButtonTable, {
type:"delete",
text: '删除',
onClick: () => deleteAllocation(row)
})
)

View File

@@ -25,26 +25,26 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { PackageManageService } from '@/api/modules'
import type { PackageResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { PackageManageService } from '@/api/modules'
import type { PackageResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'PackageDetail' })
defineOptions({ name: 'PackageDetail' })
const route = useRoute()
const router = useRouter()
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const detailData = ref<PackageResponse | null>(null)
const loading = ref(false)
const detailData = ref<PackageResponse | null>(null)
// 详情页配置
const detailSections: DetailSection[] = [
// 详情页配置
const detailSections: DetailSection[] = [
{
title: '基本信息',
fields: [
@@ -150,7 +150,11 @@ const detailSections: DetailSection[] = [
{
label: '一次性佣金金额',
formatter: (_, data) => {
if (data.one_time_commission_amount === null || data.one_time_commission_amount === undefined) return '-'
if (
data.one_time_commission_amount === null ||
data.one_time_commission_amount === undefined
)
return '-'
return `¥${(data.one_time_commission_amount / 100).toFixed(2)}`
}
},
@@ -171,21 +175,26 @@ const detailSections: DetailSection[] = [
{
label: '下一档位阈值',
formatter: (_, data) => {
if (!data.tier_info || data.tier_info.next_threshold === null || data.tier_info.next_threshold === undefined) return '-'
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 = () => {
// 返回上一页
const handleBack = () => {
router.back()
}
}
// 获取详情数据
const fetchDetail = async () => {
// 获取详情数据
const fetchDetail = async () => {
const id = Number(route.params.id)
if (!id) {
ElMessage.error('缺少ID参数')
@@ -204,25 +213,23 @@ const fetchDetail = async () => {
} finally {
loading.value = false
}
}
}
onMounted(() => {
onMounted(() => {
fetchDetail()
})
})
</script>
<style scoped lang="scss">
.package-detail {
.package-detail {
padding: 20px;
}
}
.detail-header {
.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;
@@ -230,9 +237,9 @@ onMounted(() => {
font-weight: 600;
color: var(--el-text-color-primary);
}
}
}
.loading-container {
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
@@ -244,5 +251,5 @@ onMounted(() => {
.el-icon {
font-size: 32px;
}
}
}
</style>

View File

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

View File

@@ -25,26 +25,26 @@
</template>
<script setup lang="ts">
import { ref, onMounted, h } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage, ElTag } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { PackageSeriesService } from '@/api/modules'
import type { PackageSeriesResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { ref, onMounted, h } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage, ElTag } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { PackageSeriesService } from '@/api/modules'
import type { PackageSeriesResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'PackageSeriesDetail' })
defineOptions({ name: 'PackageSeriesDetail' })
const route = useRoute()
const router = useRouter()
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const detailData = ref<PackageSeriesResponse | null>(null)
const loading = ref(false)
const detailData = ref<PackageSeriesResponse | null>(null)
// 详情页配置
const detailSections: DetailSection[] = [
// 详情页配置
const detailSections: DetailSection[] = [
{
title: '基本信息',
fields: [
@@ -112,21 +112,31 @@ const detailSections: DetailSection[] = [
fullWidth: true,
render: (data) => {
const config = data.one_time_commission_config
if (config?.commission_type !== 'tiered' || !config.tiers || config.tiers.length === 0) {
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;' },
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'
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 h(
ElTag,
{ type: 'info', size: 'default' },
() =>
`档位${index + 1}: ${dimensionText}${thresholdText}, 佣金 ${amountText}, ${scopeText}`
)
})
)
@@ -192,15 +202,15 @@ const detailSections: DetailSection[] = [
}
]
}
]
]
// 返回上一页
const handleBack = () => {
// 返回上一页
const handleBack = () => {
router.back()
}
}
// 获取详情数据
const fetchDetail = async () => {
// 获取详情数据
const fetchDetail = async () => {
const id = Number(route.params.id)
if (!id) {
ElMessage.error('缺少ID参数')
@@ -219,25 +229,23 @@ const fetchDetail = async () => {
} finally {
loading.value = false
}
}
}
onMounted(() => {
onMounted(() => {
fetchDetail()
})
})
</script>
<style scoped lang="scss">
.package-series-detail {
.package-series-detail {
padding: 20px;
}
}
.detail-header {
.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;
@@ -245,9 +253,9 @@ onMounted(() => {
font-weight: 600;
color: var(--el-text-color-primary);
}
}
}
.loading-container {
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
@@ -259,5 +267,5 @@ onMounted(() => {
.el-icon {
font-size: 32px;
}
}
}
</style>

View File

@@ -243,7 +243,13 @@
:icon="Delete"
circle
@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>
</ElCard>
@@ -673,7 +679,7 @@
{
prop: 'operation',
label: '操作',
width: 200,
width: 220,
fixed: 'right',
formatter: (row: PackageSeriesResponse) => {
const buttons = []
@@ -681,7 +687,7 @@
// 详情按钮
buttons.push(
h(ArtButtonTable, {
type: 'view',
text: '详情',
onClick: () => handleViewDetail(row)
})
)
@@ -689,7 +695,7 @@
if (hasAuth('package_series:edit')) {
buttons.push(
h(ArtButtonTable, {
type: 'edit',
text: '编辑',
onClick: () => showDialog('edit', row)
})
)
@@ -698,7 +704,7 @@
if (hasAuth('package_series:delete')) {
buttons.push(
h(ArtButtonTable, {
type: 'delete',
text: '删除',
onClick: () => deleteSeries(row)
})
)

View File

@@ -25,26 +25,26 @@
</template>
<script setup lang="ts">
import { ref, onMounted, h } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage, ElTag } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { ShopSeriesAllocationService } from '@/api/modules'
import type { ShopSeriesAllocationResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { ref, onMounted, h } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage, ElTag } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { ShopSeriesAllocationService } from '@/api/modules'
import type { ShopSeriesAllocationResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'SeriesAssignDetail' })
defineOptions({ name: 'SeriesAssignDetail' })
const route = useRoute()
const router = useRouter()
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const detailData = ref<ShopSeriesAllocationResponse | null>(null)
const loading = ref(false)
const detailData = ref<ShopSeriesAllocationResponse | null>(null)
// 详情页配置
const detailSections: DetailSection[] = [
// 详情页配置
const detailSections: DetailSection[] = [
{
title: '基本信息',
fields: [
@@ -136,15 +136,15 @@ const detailSections: DetailSection[] = [
}
]
}
]
]
// 返回上一页
const handleBack = () => {
// 返回上一页
const handleBack = () => {
router.back()
}
}
// 获取详情数据
const fetchDetail = async () => {
// 获取详情数据
const fetchDetail = async () => {
const id = Number(route.params.id)
if (!id) {
ElMessage.error('缺少ID参数')
@@ -163,25 +163,23 @@ const fetchDetail = async () => {
} finally {
loading.value = false
}
}
}
onMounted(() => {
onMounted(() => {
fetchDetail()
})
})
</script>
<style scoped lang="scss">
.series-assign-detail {
.series-assign-detail {
padding: 20px;
}
}
.detail-header {
.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;
@@ -189,9 +187,9 @@ onMounted(() => {
font-weight: 600;
color: var(--el-text-color-primary);
}
}
}
.loading-container {
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
@@ -203,5 +201,5 @@ onMounted(() => {
.el-icon {
font-size: 32px;
}
}
}
</style>

View File

@@ -19,7 +19,9 @@
@refresh="handleRefresh"
>
<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>
</ArtTableHeader>
@@ -565,7 +567,7 @@
{
prop: 'operation',
label: '操作',
width: 180,
width: 220,
fixed: 'right',
formatter: (row: ShopSeriesAllocationResponse) => {
const buttons = []
@@ -573,7 +575,7 @@
// 详情按钮
buttons.push(
h(ArtButtonTable, {
type: 'view',
text: '详情',
onClick: () => handleViewDetail(row)
})
)
@@ -581,7 +583,7 @@
if (hasAuth('series_assign:edit')) {
buttons.push(
h(ArtButtonTable, {
type: 'edit',
text: '编辑',
onClick: () => showDialog('edit', row)
})
)
@@ -590,7 +592,7 @@
if (hasAuth('series_assign:delete')) {
buttons.push(
h(ArtButtonTable, {
type: 'delete',
text: '删除',
onClick: () => deleteAllocation(row)
})
)
@@ -800,7 +802,8 @@
page_size: pagination.page_size,
shop_id: searchForm.shop_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
}
const res = await ShopSeriesAllocationService.getShopSeriesAllocations(params)

View File

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

View File

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