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

This commit is contained in:
sexygoat
2026-02-27 17:40:02 +08:00
parent f1cb1e53c8
commit 4470a4ef04
17 changed files with 908 additions and 544 deletions

View File

@@ -22,19 +22,10 @@ export interface PaginationParams {
// 分页响应数据 // 分页响应数据
export interface PaginationData<T> { export interface PaginationData<T> {
items: T[] // 后端实际返回的是 items不是 records items: T[] // 数据列表
total: number total: number // 总数
size: number page_size: number // 每页数量
page: number // 后端返回的是 page不是 current page: number // 当前页
pages?: number
}
// 新版分页响应数据(使用 list 字段)
export interface PaginationDataV2<T> {
list: T[] // 新版API使用 list 字段
total: number
page_size: number
page: number
total_pages: number // 总页数 total_pages: number // 总页数
} }
@@ -43,11 +34,6 @@ export interface PaginationResponse<T = any> extends BaseResponse {
data: PaginationData<T> data: PaginationData<T>
} }
// 新版分页响应(使用 list 字段)
export interface PaginationResponseV2<T = any> extends BaseResponse {
data: PaginationDataV2<T>
}
// 列表响应 // 列表响应
export interface ListResponse<T = any> extends BaseResponse { export interface ListResponse<T = any> extends BaseResponse {
data: T[] data: T[]

View File

@@ -86,6 +86,15 @@ export interface UpdatePackageSeriesStatusRequest {
// ==================== 套餐管理 ==================== // ==================== 套餐管理 ====================
/**
* 返佣档位信息
*/
export interface CommissionTierInfo {
current_rate?: string // 当前返佣比例
next_rate?: string // 下一档位返佣比例
next_threshold?: number | null // 下一档位阈值
}
/** /**
* 套餐响应 * 套餐响应
*/ */
@@ -93,22 +102,28 @@ export interface PackageResponse {
id: number id: number
package_code: string package_code: string
package_name: string package_name: string
series_id: number series_id: number | null
series_name?: string series_name?: string | null
package_type: string // 'formal':正式套餐, 'addon':加油包 package_type: string // 'formal':正式套餐, 'addon':附加套餐
data_type: string // 'real':真实流量, 'virtual':虚拟流量 calendar_type?: string // 套餐周期类型 (natural_month:自然月, by_day:按天)
real_data_mb: number // 真流量额度MB duration_days?: number | null // 套餐天数(calendar_type=by_day时有值)
virtual_data_mb: number // 虚流量额度MB duration_months?: number // 套餐时长(月数)
duration_months: number // 有效期(月) data_reset_cycle?: string // 流量重置周期 (daily:每日, monthly:每月, yearly:每年, none:不重置)
price: number // 价格(分 real_data_mb?: number // 真流量额度MB
cost_price: number // 成本价(分 virtual_data_mb?: number // 虚流量额度MB
suggested_retail_price: number // 建议零售价(分) enable_virtual_data?: boolean // 是否启用虚流量
enable_virtual_data: boolean // 是否启用虚流量 enable_realname_activation?: boolean // 是否启用实名激活 (true:需实名后激活, false:立即激活)
shelf_status: number // 上架状态 (1:上架, 2:下架) cost_price?: number // 成本价(分)
status: number // 状态 (1:启用, 2:禁用) suggested_retail_price?: number // 建议零售价(分)
current_commission_rate?: string // 当前返佣比例(仅代理用户可见)
one_time_commission_amount?: number | null // 一次性佣金金额(分,代理视角)
profit_margin?: number | null // 利润空间(分,仅代理用户可见)
tier_info?: CommissionTierInfo // 返佣档位信息
shelf_status?: number // 上架状态 (1:上架, 2:下架)
status?: number // 状态 (1:启用, 2:禁用)
description?: string description?: string
created_at: string created_at?: string
updated_at: string updated_at?: string
} }
/** /**
@@ -129,30 +144,37 @@ export interface PackageQueryParams extends PaginationParams {
export interface CreatePackageRequest { export interface CreatePackageRequest {
package_code: string // 套餐编码,必填 package_code: string // 套餐编码,必填
package_name: string // 套餐名称,必填 package_name: string // 套餐名称,必填
series_id: number // 所属系列ID必填 series_id?: number | null // 所属系列ID可选
package_type: string // 套餐类型,必填 package_type: string // 套餐类型,必填 (formal:正式套餐, addon:附加套餐)
data_type: string // 流量类型,必填 calendar_type?: string | null // 套餐周期类型 (natural_month:自然月, by_day:按天),可选
real_data_mb: number // 真流量额度MB必填 duration_days?: number | null // 套餐天数(calendar_type=by_day时必填),可选
virtual_data_mb: number // 虚流量额度MB,必填 duration_months: number // 套餐时长(月数),必填
duration_months: number // 有效期(月),必填 data_reset_cycle?: string | null // 流量重置周期 (daily:每日, monthly:每月, yearly:每年, none:不重置),可选
price: number // 价格(分),必填 real_data_mb?: number | null // 真流量额度MB可选
description?: string // 描述,可选 virtual_data_mb?: number | null // 虚流量额度MB,可选
enable_virtual_data?: boolean // 是否启用虚流量,可选
enable_realname_activation?: boolean | null // 是否启用实名激活 (true:需实名后激活, false:立即激活),可选
cost_price: number // 成本价(分),必填
suggested_retail_price?: number | null // 建议零售价(分),可选
} }
/** /**
* 更新套餐请求 * 更新套餐请求
*/ */
export interface UpdatePackageRequest { export interface UpdatePackageRequest {
package_code?: string package_name?: string | null // 套餐名称,可选
package_name?: string series_id?: number | null // 所属系列ID可选
series_id?: number package_type?: string | null // 套餐类型,可选 (formal:正式套餐, addon:附加套餐)
package_type?: string calendar_type?: string | null // 套餐周期类型 (natural_month:自然月, by_day:按天),可选
data_type?: string duration_days?: number | null // 套餐天数(calendar_type=by_day时必填),可选
real_data_mb?: number duration_months?: number | null // 套餐时长(月数),可选
virtual_data_mb?: number data_reset_cycle?: string | null // 流量重置周期 (daily:每日, monthly:每月, yearly:每年, none:不重置),可选
duration_months?: number real_data_mb?: number | null // 真流量额度MB可选
price?: number virtual_data_mb?: number | null // 虚流量额度MB可选
description?: string enable_virtual_data?: boolean | null // 是否启用虚流量,可选
enable_realname_activation?: boolean | null // 是否启用实名激活,可选
cost_price?: number | null // 成本价(分),可选
suggested_retail_price?: number | null // 建议零售价(分),可选
} }
/** /**

View File

@@ -34,12 +34,21 @@
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '添加账号' : '编辑账号'" :title="dialogType === 'add' ? '添加账号' : '编辑账号'"
@@ -195,6 +204,8 @@
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { AccountService } from '@/api/modules/account' import { AccountService } from '@/api/modules/account'
import { RoleService } from '@/api/modules/role' import { RoleService } from '@/api/modules/role'
import { ShopService, EnterpriseService } from '@/api/modules' import { ShopService, EnterpriseService } from '@/api/modules'
@@ -216,6 +227,8 @@
const currentAccountId = ref<number>(0) const currentAccountId = ref<number>(0)
const currentAccountName = ref<string>('') const currentAccountName = ref<string>('')
const currentAccountType = ref<number>(0) const currentAccountType = ref<number>(0)
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<any | null>(null)
const selectedRoles = ref<number[]>([]) const selectedRoles = ref<number[]>([])
const allRoles = ref<PlatformRole[]>([]) const allRoles = ref<PlatformRole[]>([])
const rolesToAdd = ref<number[]>([]) const rolesToAdd = ref<number[]>([])
@@ -355,8 +368,7 @@
{ label: '店铺名称', prop: 'shop_name' }, { label: '店铺名称', prop: 'shop_name' },
{ label: '企业名称', prop: 'enterprise_name' }, { label: '企业名称', prop: 'enterprise_name' },
{ label: '状态', prop: 'status' }, { label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' }, { label: '创建时间', prop: 'created_at' }
{ label: '操作', prop: 'operation' }
] ]
// 显示对话框 // 显示对话框
@@ -473,44 +485,6 @@
label: '创建时间', label: '创建时间',
width: 180, width: 180,
formatter: (row: any) => formatDateTime(row.created_at) formatter: (row: any) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: '操作',
width: 240,
fixed: 'right',
formatter: (row: any) => {
const buttons = []
if (hasAuth('account:patch_role')) {
buttons.push(
h(ArtButtonTable, {
text: '分配角色',
onClick: () => showRoleDialog(row)
})
)
}
if (hasAuth('account:edit')) {
buttons.push(
h(ArtButtonTable, {
text: '编辑',
onClick: () => showDialog('edit', row)
})
)
}
if (hasAuth('account:delete')) {
buttons.push(
h(ArtButtonTable, {
text: '删除',
onClick: () => deleteAccount(row)
})
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
} }
]) ])
@@ -851,6 +825,50 @@
const handleEnterpriseSearch = (query: string) => { const handleEnterpriseSearch = (query: string) => {
loadEnterpriseList(query) loadEnterpriseList(query)
} }
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
if (hasAuth('account:patch_role')) {
items.push({ key: 'assignRole', label: '分配角色' })
}
if (hasAuth('account:edit')) {
items.push({ key: 'edit', label: '编辑' })
}
if (hasAuth('account:delete')) {
items.push({ key: 'delete', label: '删除' })
}
return items
})
// 处理表格行右键菜单
const handleRowContextMenu = (row: any, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'assignRole':
showRoleDialog(currentRow.value)
break
case 'edit':
showDialog('edit', currentRow.value)
break
case 'delete':
deleteAccount(currentRow.value)
break
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -97,7 +97,8 @@
<ElInput v-model="form.district" placeholder="请输入区县" /> <ElInput v-model="form.district" placeholder="请输入区县" />
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="12"> <!-- 只有非代理账号才显示归属店铺选择 -->
<ElCol :span="12" v-if="!isAgentAccount">
<ElFormItem label="归属店铺" prop="owner_shop_id"> <ElFormItem label="归属店铺" prop="owner_shop_id">
<ElSelect <ElSelect
v-model="form.owner_shop_id" v-model="form.owner_shop_id"
@@ -219,6 +220,7 @@
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue' import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue' import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
@@ -227,9 +229,13 @@
defineOptions({ name: 'EnterpriseCustomer' }) defineOptions({ name: 'EnterpriseCustomer' })
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
const userStore = useUserStore()
const router = useRouter() const router = useRouter()
// 判断是否是代理账号 (user_type === 3)
const isAgentAccount = computed(() => userStore.info.user_type === 3)
const dialogVisible = ref(false) const dialogVisible = ref(false)
const passwordDialogVisible = ref(false) const passwordDialogVisible = ref(false)
const loading = ref(false) const loading = ref(false)
@@ -488,7 +494,10 @@
onMounted(() => { onMounted(() => {
getTableData() getTableData()
loadShopList() // 只有非代理账号才需要加载店铺列表
if (!isAgentAccount.value) {
loadShopList()
}
}) })
// 加载店铺列表(默认加载20条) // 加载店铺列表(默认加载20条)
@@ -609,7 +618,8 @@
form.contact_phone = '' form.contact_phone = ''
form.login_phone = '' form.login_phone = ''
form.password = '' form.password = ''
form.owner_shop_id = null // 如果是代理账号,自动设置归属店铺为当前登录用户的店铺
form.owner_shop_id = isAgentAccount.value ? userStore.info.shop_id : null
} }
// 重置表单验证状态 // 重置表单验证状态

View File

@@ -30,12 +30,21 @@
:marginTop="10" :marginTop="10"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<!-- 授权详情对话框 --> <!-- 授权详情对话框 -->
<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>
@@ -119,6 +128,8 @@
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import type { import type {
AuthorizationItem, AuthorizationItem,
AuthorizationStatus, AuthorizationStatus,
@@ -138,6 +149,8 @@
const remarkFormRef = ref<FormInstance>() const remarkFormRef = ref<FormInstance>()
const currentRecordId = ref<number>(0) const currentRecordId = ref<number>(0)
const currentRecord = ref<AuthorizationItem | null>(null) const currentRecord = ref<AuthorizationItem | null>(null)
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<AuthorizationItem | null>(null)
// 搜索表单初始值 // 搜索表单初始值
const initialSearchState = { const initialSearchState = {
@@ -227,8 +240,7 @@
{ label: '授权人类型', prop: 'authorizer_type' }, { label: '授权人类型', prop: 'authorizer_type' },
{ label: '授权时间', prop: 'authorized_at' }, { label: '授权时间', prop: 'authorized_at' },
{ label: '状态', prop: 'status' }, { label: '状态', prop: 'status' },
{ label: '备注', prop: 'remark' }, { label: '备注', prop: 'remark' }
{ label: '操作', prop: 'operation' }
] ]
const authorizationList = ref<AuthorizationItem[]>([]) const authorizationList = ref<AuthorizationItem[]>([])
@@ -306,33 +318,6 @@
minWidth: 150, minWidth: 150,
showOverflowTooltip: true, showOverflowTooltip: true,
formatter: (row: AuthorizationItem) => row.remark || '-' formatter: (row: AuthorizationItem) => row.remark || '-'
},
{
prop: 'operation',
label: '操作',
width: 150,
fixed: 'right',
formatter: (row: AuthorizationItem) => {
const buttons = []
buttons.push(
h(ArtButtonTable, {
text: '详情',
onClick: () => viewDetail(row)
})
)
if (hasAuth('authorization_records:update_remark')) {
buttons.push(
h(ArtButtonTable, {
text: '编辑',
onClick: () => showRemarkDialog(row)
})
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
} }
]) ])
@@ -443,6 +428,41 @@
} }
}) })
} }
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
items.push({ key: 'detail', label: '详情' })
if (hasAuth('authorization_records:update_remark')) {
items.push({ key: 'edit', label: '编辑' })
}
return items
})
// 处理表格行右键菜单
const handleRowContextMenu = (row: AuthorizationItem, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'detail':
viewDetail(currentRow.value)
break
case 'edit':
showRemarkDialog(currentRow.value)
break
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -41,11 +41,20 @@
:marginTop="10" :marginTop="10"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
</ElCard> </ElCard>
</div> </div>
@@ -223,6 +232,8 @@
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { StorageService } from '@/api/modules/storage' import { StorageService } from '@/api/modules/storage'
import type { DeviceImportTask, DeviceImportTaskStatus } from '@/types/api/device' import type { DeviceImportTask, DeviceImportTaskStatus } from '@/types/api/device'
@@ -236,6 +247,8 @@
const uploading = ref(false) const uploading = ref(false)
const importDialogVisible = ref(false) const importDialogVisible = ref(false)
const detailDialogVisible = ref(false) const detailDialogVisible = ref(false)
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<DeviceImportTask | null>(null)
// 搜索表单初始值 // 搜索表单初始值
const initialSearchState = { const initialSearchState = {
@@ -306,8 +319,7 @@
{ label: '开始时间', prop: 'started_at' }, { label: '开始时间', prop: 'started_at' },
{ label: '完成时间', prop: 'completed_at' }, { label: '完成时间', prop: 'completed_at' },
{ label: '错误信息', prop: 'error_message' }, { label: '错误信息', prop: 'error_message' },
{ label: '创建时间', prop: 'created_at' }, { label: '创建时间', prop: 'created_at' }
{ label: '操作', prop: 'operation' }
] ]
const taskList = ref<DeviceImportTask[]>([]) const taskList = ref<DeviceImportTask[]>([])
@@ -421,36 +433,6 @@
label: '创建时间', label: '创建时间',
width: 180, width: 180,
formatter: (row: DeviceImportTask) => formatDateTime(row.created_at) formatter: (row: DeviceImportTask) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: '操作',
width: 180,
fixed: 'right',
formatter: (row: DeviceImportTask) => {
const buttons = []
// 显示"查看详情"按钮
buttons.push(
h(ArtButtonTable, {
text: '详情',
onClick: () => viewDetail(row)
})
)
// 如果有失败数据,显示"失败数据"按钮
if (row.fail_count > 0) {
buttons.push(
h(ArtButtonTable, {
text: '失败数据',
type: 'danger',
onClick: () => downloadFailDataByRow(row)
})
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
} }
]) ])
@@ -831,6 +813,43 @@
ElMessage.success('失败数据下载成功') ElMessage.success('失败数据下载成功')
} }
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
if (!currentRow.value) return []
const items: MenuItemType[] = []
items.push({ key: 'detail', label: '详情' })
if (currentRow.value.fail_count > 0) {
items.push({ key: 'failData', label: '失败数据' })
}
return items
})
// 处理表格行右键菜单
const handleRowContextMenu = (row: DeviceImportTask, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'detail':
viewDetail(currentRow.value)
break
case 'failData':
downloadFailDataByRow(currentRow.value)
break
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -397,18 +397,14 @@
<ElDescriptionsItem label="卡业务类型">{{ <ElDescriptionsItem label="卡业务类型">{{
getCardCategoryText(currentCardDetail.card_category) getCardCategoryText(currentCardDetail.card_category)
}}</ElDescriptionsItem> }}</ElDescriptionsItem>
<ElDescriptionsItem label="成本价">{{
formatCardPrice(currentCardDetail.cost_price)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="分销价">{{
formatCardPrice(currentCardDetail.distribute_price)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="状态"> <ElDescriptionsItem label="状态">
<ElTag :type="getCardDetailStatusTagType(currentCardDetail.status)"> <ElTag :type="getCardDetailStatusTagType(currentCardDetail.status)">
{{ getCardDetailStatusText(currentCardDetail.status) }} {{ getCardDetailStatusText(currentCardDetail.status) }}
</ElTag> </ElTag>
</ElDescriptionsItem> </ElDescriptionsItem>
<ElDescriptionsItem label="套餐系列">{{
currentCardDetail.series_name || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="激活状态"> <ElDescriptionsItem label="激活状态">
<ElTag :type="currentCardDetail.activation_status === 1 ? 'success' : 'info'"> <ElTag :type="currentCardDetail.activation_status === 1 ? 'success' : 'info'">
{{ currentCardDetail.activation_status === 1 ? '已激活' : '未激活' }} {{ currentCardDetail.activation_status === 1 ? '已激活' : '未激活' }}
@@ -863,8 +859,6 @@
{ label: '运营商', prop: 'carrier_name' }, { label: '运营商', prop: 'carrier_name' },
{ label: '店铺名称', prop: 'shop_name' }, { label: '店铺名称', prop: 'shop_name' },
{ label: '套餐系列', prop: 'series_name' }, { label: '套餐系列', prop: 'series_name' },
{ label: '成本价', prop: 'cost_price' },
{ label: '分销价', prop: 'distribute_price' },
{ label: '状态', prop: 'status' }, { label: '状态', prop: 'status' },
{ label: '激活状态', prop: 'activation_status' }, { label: '激活状态', prop: 'activation_status' },
{ label: '网络状态', prop: 'network_status' }, { label: '网络状态', prop: 'network_status' },
@@ -1020,18 +1014,6 @@
width: 150, width: 150,
formatter: (row: StandaloneIotCard) => row.series_name || '-' formatter: (row: StandaloneIotCard) => row.series_name || '-'
}, },
{
prop: 'cost_price',
label: '成本价',
width: 100,
formatter: (row: StandaloneIotCard) => `¥${(row.cost_price / 100).toFixed(2)}`
},
{
prop: 'distribute_price',
label: '分销价',
width: 100,
formatter: (row: StandaloneIotCard) => `¥${(row.distribute_price / 100).toFixed(2)}`
},
{ {
prop: 'status', prop: 'status',
label: '状态', label: '状态',

View File

@@ -41,11 +41,20 @@
:marginTop="10" :marginTop="10"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
</ElCard> </ElCard>
</div> </div>
@@ -220,6 +229,8 @@
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { StorageService } from '@/api/modules/storage' import { StorageService } from '@/api/modules/storage'
import type { IotCardImportTask, IotCardImportTaskStatus } from '@/types/api/card' import type { IotCardImportTask, IotCardImportTaskStatus } from '@/types/api/card'
import type { Carrier } from '@/types/api' import type { Carrier } from '@/types/api'
@@ -237,6 +248,8 @@
const selectedCarrierId = ref<number>() const selectedCarrierId = ref<number>()
const carrierList = ref<Carrier[]>([]) const carrierList = ref<Carrier[]>([])
const carrierLoading = ref(false) const carrierLoading = ref(false)
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<IotCardImportTask | null>(null)
// 搜索表单初始值 // 搜索表单初始值
const initialSearchState = { const initialSearchState = {
@@ -324,8 +337,7 @@
{ label: '开始时间', prop: 'started_at' }, { label: '开始时间', prop: 'started_at' },
{ label: '完成时间', prop: 'completed_at' }, { label: '完成时间', prop: 'completed_at' },
{ label: '错误信息', prop: 'error_message' }, { label: '错误信息', prop: 'error_message' },
{ label: '创建时间', prop: 'created_at' }, { label: '创建时间', prop: 'created_at' }
{ label: '操作', prop: 'operation' }
] ]
const taskList = ref<IotCardImportTask[]>([]) const taskList = ref<IotCardImportTask[]>([])
@@ -445,36 +457,6 @@
label: '创建时间', label: '创建时间',
width: 180, width: 180,
formatter: (row: IotCardImportTask) => formatDateTime(row.created_at) formatter: (row: IotCardImportTask) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: '操作',
width: 180,
fixed: 'right',
formatter: (row: IotCardImportTask) => {
const buttons = []
// 显示"查看详情"按钮
buttons.push(
h(ArtButtonTable, {
text: '详情',
onClick: () => viewDetail(row)
})
)
// 如果有失败数据,显示"失败数据"按钮
if (row.fail_count > 0) {
buttons.push(
h(ArtButtonTable, {
text: '失败数据',
type: 'danger',
onClick: () => downloadFailDataByRow(row)
})
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
} }
]) ])
@@ -827,6 +809,43 @@
uploading.value = false uploading.value = false
} }
} }
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
if (!currentRow.value) return []
const items: MenuItemType[] = []
items.push({ key: 'detail', label: '详情' })
if (currentRow.value.fail_count > 0) {
items.push({ key: 'failData', label: '失败数据' })
}
return items
})
// 处理表格行右键菜单
const handleRowContextMenu = (row: IotCardImportTask, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'detail':
viewDetail(currentRow.value)
break
case 'failData':
downloadFailDataByRow(currentRow.value)
break
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -48,11 +48,20 @@
:marginTop="10" :marginTop="10"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
</ElCard> </ElCard>
</div> </div>
</ArtTableFullScreen> </ArtTableFullScreen>
@@ -234,6 +243,8 @@
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime, formatMoney } from '@/utils/business/format' import { formatDateTime, formatMoney } from '@/utils/business/format'
import { import {
CommissionStatusMap, CommissionStatusMap,
@@ -252,6 +263,8 @@
const loading = ref(false) const loading = ref(false)
const tableRef = ref() const tableRef = ref()
const summaryList = ref<ShopCommissionSummaryItem[]>([]) const summaryList = ref<ShopCommissionSummaryItem[]>([])
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<ShopCommissionSummaryItem | null>(null)
// 搜索表单 // 搜索表单
const searchForm = reactive({ const searchForm = reactive({
@@ -302,8 +315,7 @@
{ label: '冻结中', prop: 'frozen_commission' }, { label: '冻结中', prop: 'frozen_commission' },
{ label: '提现中', prop: 'withdrawing_commission' }, { label: '提现中', prop: 'withdrawing_commission' },
{ label: '已提现', prop: 'withdrawn_commission' }, { label: '已提现', prop: 'withdrawn_commission' },
{ label: '未提现', prop: 'unwithdraw_commission' }, { label: '未提现', prop: 'unwithdraw_commission' }
{ label: '操作', prop: 'operation' }
] ]
// 动态列配置 // 动态列配置
@@ -376,24 +388,6 @@
label: '未提现', label: '未提现',
width: 120, width: 120,
formatter: (row: ShopCommissionSummaryItem) => formatMoney(row.unwithdraw_commission) formatter: (row: ShopCommissionSummaryItem) => formatMoney(row.unwithdraw_commission)
},
{
prop: 'operation',
label: '操作',
width: 120,
fixed: 'right',
formatter: (row: ShopCommissionSummaryItem) => {
const buttons = []
if (hasAuth('agent_commission:detail')) {
buttons.push(
h(ArtButtonTable, {
text: '详情',
onClick: () => showDetail(row)
})
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
} }
]) ])
@@ -567,6 +561,36 @@
withdrawalPagination.page = newCurrentPage withdrawalPagination.page = newCurrentPage
loadWithdrawalRecords() loadWithdrawalRecords()
} }
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
if (hasAuth('agent_commission:detail')) {
items.push({ key: 'detail', label: '详情' })
}
return items
})
// 处理表格行右键菜单
const handleRowContextMenu = (row: ShopCommissionSummaryItem, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'detail':
showDetail(currentRow.value)
break
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -36,12 +36,21 @@
:marginTop="10" :marginTop="10"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<!-- 创建订单对话框 --> <!-- 创建订单对话框 -->
<ElDialog <ElDialog
v-model="createDialogVisible" v-model="createDialogVisible"
@@ -284,6 +293,8 @@
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'OrderList' }) defineOptions({ name: 'OrderList' })
@@ -297,6 +308,8 @@
const createDialogVisible = ref(false) const createDialogVisible = ref(false)
const detailDialogVisible = ref(false) const detailDialogVisible = ref(false)
const currentOrder = ref<Order | null>(null) const currentOrder = ref<Order | null>(null)
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<Order | null>(null)
// 搜索表单初始值 // 搜索表单初始值
const initialSearchState: OrderQueryParams = { const initialSearchState: OrderQueryParams = {
@@ -379,8 +392,7 @@
{ label: t('orderManagement.table.totalAmount'), prop: 'total_amount' }, { label: t('orderManagement.table.totalAmount'), prop: 'total_amount' },
{ label: t('orderManagement.table.paymentMethod'), prop: 'payment_method' }, { label: t('orderManagement.table.paymentMethod'), prop: 'payment_method' },
{ label: t('orderManagement.table.paidAt'), prop: 'paid_at' }, { label: t('orderManagement.table.paidAt'), prop: 'paid_at' },
{ label: t('orderManagement.table.createdAt'), prop: 'created_at' }, { label: t('orderManagement.table.createdAt'), prop: 'created_at' }
{ label: t('orderManagement.table.operation'), prop: 'operation' }
] ]
const createFormRef = ref<FormInstance>() const createFormRef = ref<FormInstance>()
@@ -624,29 +636,6 @@
label: t('orderManagement.table.createdAt'), label: t('orderManagement.table.createdAt'),
width: 180, width: 180,
formatter: (row: Order) => formatDateTime(row.created_at) formatter: (row: Order) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: t('orderManagement.table.operation'),
width: 160,
fixed: 'right',
formatter: (row: Order) => {
return h('div', { style: 'display: flex; gap: 8px;' }, [
h(ArtButtonTable, {
text: '详情',
tooltip: t('orderManagement.actions.viewDetail'),
onClick: () => showOrderDetail(row)
}),
// 只有待支付和已支付的订单可以取消
row.payment_status === 1 || row.payment_status === 2
? h(ArtButtonTable, {
text: '删除',
tooltip: t('orderManagement.actions.cancel'),
onClick: () => handleCancelOrder(row)
})
: null
])
}
} }
]) ])
@@ -908,6 +897,44 @@
// 用户取消操作 // 用户取消操作
}) })
} }
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
if (!currentRow.value) return []
const items: MenuItemType[] = []
items.push({ key: 'detail', label: '详情' })
// 只有待支付和已支付的订单可以取消
if (currentRow.value.payment_status === 1 || currentRow.value.payment_status === 2) {
items.push({ key: 'cancel', label: '删除' })
}
return items
})
// 处理表格行右键菜单
const handleRowContextMenu = (row: Order, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'detail':
showOrderDetail(currentRow.value)
break
case 'cancel':
handleCancelOrder(currentRow.value)
break
}
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -37,12 +37,21 @@
:marginTop="10" :marginTop="10"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<!-- 新增/编辑对话框 --> <!-- 新增/编辑对话框 -->
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
@@ -128,6 +137,8 @@
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias' import { RoutesAlias } from '@/router/routesAlias'
import { import {
@@ -149,6 +160,8 @@
const shopLoading = ref(false) const shopLoading = ref(false)
const tableRef = ref() const tableRef = ref()
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<ShopPackageAllocationResponse | null>(null)
const packageOptions = ref<PackageResponse[]>([]) const packageOptions = ref<PackageResponse[]>([])
const shopOptions = ref<ShopResponse[]>([]) const shopOptions = ref<ShopResponse[]>([])
const shopTreeData = ref<ShopResponse[]>([]) const shopTreeData = ref<ShopResponse[]>([])
@@ -276,8 +289,7 @@
{ label: '分配者', prop: 'allocator_shop_name' }, { label: '分配者', prop: 'allocator_shop_name' },
{ label: '成本价', prop: 'cost_price' }, { label: '成本价', prop: 'cost_price' },
{ label: '状态', prop: 'status' }, { label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' }, { label: '创建时间', prop: 'created_at' }
{ label: '操作', prop: 'operation' }
] ]
// 表单验证规则 // 表单验证规则
@@ -389,45 +401,35 @@
label: '创建时间', label: '创建时间',
width: 180, width: 180,
formatter: (row: ShopPackageAllocationResponse) => formatDateTime(row.created_at) formatter: (row: ShopPackageAllocationResponse) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: '操作',
width: 220,
fixed: 'right',
formatter: (row: ShopPackageAllocationResponse) => {
const buttons = []
buttons.push(
h(ArtButtonTable, {
text: '详情',
onClick: () => handleViewDetail(row)
})
)
if (hasAuth('package_assign:edit')) {
buttons.push(
h(ArtButtonTable, {
text: '编辑',
onClick: () => showDialog('edit', row)
})
)
}
if (hasAuth('package_assign:delete')) {
buttons.push(
h(ArtButtonTable, {
text: '删除',
onClick: () => deleteAllocation(row)
})
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
} }
]) ])
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
items.push({
key: 'detail',
label: '详情'
})
if (hasAuth('package_assign:edit')) {
items.push({
key: 'edit',
label: '编辑'
})
}
if (hasAuth('package_assign:delete')) {
items.push({
key: 'delete',
label: '删除'
})
}
return items
})
// 构建树形结构数据 // 构建树形结构数据
const buildTreeData = (items: ShopResponse[]) => { const buildTreeData = (items: ShopResponse[]) => {
const map = new Map<number, ShopResponse & { children?: ShopResponse[] }>() const map = new Map<number, ShopResponse & { children?: ShopResponse[] }>()
@@ -478,7 +480,7 @@
} }
const res = await PackageManageService.getPackages(params) const res = await PackageManageService.getPackages(params)
if (res.code === 0) { if (res.code === 0) {
packageOptions.value = res.data.items || [] packageOptions.value = res.data.items
} }
} catch (error) { } catch (error) {
console.error('加载套餐选项失败:', error) console.error('加载套餐选项失败:', error)
@@ -497,7 +499,7 @@
page_size: 10000 // 使用较大的值获取所有店铺 page_size: 10000 // 使用较大的值获取所有店铺
}) })
if (res.code === 0) { if (res.code === 0) {
shopOptions.value = res.data.items || [] shopOptions.value = res.data.items
// 构建树形结构数据 // 构建树形结构数据
shopTreeData.value = buildTreeData(shopOptions.value) shopTreeData.value = buildTreeData(shopOptions.value)
} }
@@ -516,7 +518,7 @@
page_size: 10 page_size: 10
}) })
if (res.code === 0) { if (res.code === 0) {
searchPackageOptions.value = res.data.items || [] searchPackageOptions.value = res.data.items
} }
} catch (error) { } catch (error) {
console.error('加载搜索栏套餐选项失败:', error) console.error('加载搜索栏套餐选项失败:', error)
@@ -528,7 +530,7 @@
try { try {
const res = await ShopService.getShops({ page: 1, page_size: 10 }) const res = await ShopService.getShops({ page: 1, page_size: 10 })
if (res.code === 0) { if (res.code === 0) {
searchShopOptions.value = res.data.items || [] searchShopOptions.value = res.data.items
} }
} catch (error) { } catch (error) {
console.error('加载搜索栏店铺选项失败:', error) console.error('加载搜索栏店铺选项失败:', error)
@@ -557,7 +559,7 @@
package_name: query package_name: query
}) })
if (res.code === 0) { if (res.code === 0) {
searchPackageOptions.value = res.data.items || [] searchPackageOptions.value = res.data.items
} }
} catch (error) { } catch (error) {
console.error('搜索套餐失败:', error) console.error('搜索套餐失败:', error)
@@ -577,7 +579,7 @@
shop_name: query shop_name: query
}) })
if (res.code === 0) { if (res.code === 0) {
searchShopOptions.value = res.data.items || [] searchShopOptions.value = res.data.items
} }
} catch (error) { } catch (error) {
console.error('搜索店铺失败:', error) console.error('搜索店铺失败:', error)
@@ -589,7 +591,7 @@
try { try {
const res = await ShopService.getShops({ page: 1, page_size: 10 }) const res = await ShopService.getShops({ page: 1, page_size: 10 })
if (res.code === 0) { if (res.code === 0) {
searchAllocatorShopOptions.value = res.data.items || [] searchAllocatorShopOptions.value = res.data.items
} }
} catch (error) { } catch (error) {
console.error('加载搜索栏分配者店铺选项失败:', error) console.error('加载搜索栏分配者店铺选项失败:', error)
@@ -609,7 +611,7 @@
shop_name: query shop_name: query
}) })
if (res.code === 0) { if (res.code === 0) {
searchAllocatorShopOptions.value = res.data.items || [] searchAllocatorShopOptions.value = res.data.items
} }
} catch (error) { } catch (error) {
console.error('搜索分配者店铺失败:', error) console.error('搜索分配者店铺失败:', error)
@@ -624,7 +626,7 @@
page_size: 10 page_size: 10
}) })
if (res.code === 0) { if (res.code === 0) {
searchSeriesAllocationOptions.value = res.data.items || [] searchSeriesAllocationOptions.value = res.data.items
} }
} catch (error) { } catch (error) {
console.error('加载搜索栏系列分配选项失败:', error) console.error('加载搜索栏系列分配选项失败:', error)
@@ -644,7 +646,7 @@
series_name: query series_name: query
}) })
if (res.code === 0) { if (res.code === 0) {
searchSeriesAllocationOptions.value = res.data.items || [] searchSeriesAllocationOptions.value = res.data.items
} }
} catch (error) { } catch (error) {
console.error('搜索系列分配失败:', error) console.error('搜索系列分配失败:', error)
@@ -666,7 +668,7 @@
} }
const res = await ShopPackageAllocationService.getShopPackageAllocations(params) const res = await ShopPackageAllocationService.getShopPackageAllocations(params)
if (res.code === 0) { if (res.code === 0) {
allocationList.value = res.data.items || [] allocationList.value = res.data.items
pagination.total = res.data.total || 0 pagination.total = res.data.total || 0
} }
} catch (error) { } catch (error) {
@@ -845,6 +847,31 @@
const handleViewDetail = (row: ShopPackageAllocationResponse) => { const handleViewDetail = (row: ShopPackageAllocationResponse) => {
router.push(`${RoutesAlias.PackageAssignDetail}/${row.id}`) router.push(`${RoutesAlias.PackageAssignDetail}/${row.id}`)
} }
// 处理表格行右键菜单
const handleRowContextMenu = (row: ShopPackageAllocationResponse, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'detail':
handleViewDetail(currentRow.value)
break
case 'edit':
showDialog('edit', currentRow.value)
break
case 'delete':
deleteAllocation(currentRow.value)
break
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -68,6 +68,43 @@
return `${data.duration_months} 个月` return `${data.duration_months} 个月`
} }
}, },
{
label: '套餐周期类型',
formatter: (_, data) => {
if (!data.calendar_type) return '-'
const typeMap = {
natural_month: '自然月',
by_day: '按天'
}
return typeMap[data.calendar_type] || data.calendar_type
}
},
{
label: '套餐天数',
formatter: (_, data) => {
if (data.calendar_type !== 'by_day' || !data.duration_days) return '-'
return `${data.duration_days}`
}
},
{
label: '流量重置周期',
formatter: (_, data) => {
if (!data.data_reset_cycle) return '-'
const cycleMap = {
daily: '每日',
monthly: '每月',
yearly: '每年',
none: '不重置'
}
return cycleMap[data.data_reset_cycle] || data.data_reset_cycle
}
},
{
label: '启用实名激活',
formatter: (_, data) => {
return data.enable_realname_activation ? '是' : '否'
}
},
{ {
label: '状态', label: '状态',
formatter: (_, data) => { formatter: (_, data) => {
@@ -141,12 +178,6 @@
{ {
title: '佣金配置', title: '佣金配置',
fields: [ fields: [
{
label: '当前返佣比例',
formatter: (_, data) => {
return data.current_commission_rate || '-'
}
},
{ {
label: '一次性佣金金额', label: '一次性佣金金额',
formatter: (_, data) => { formatter: (_, data) => {
@@ -159,7 +190,7 @@
} }
}, },
{ {
label: '当前返佣档位', label: '当前返佣比例',
formatter: (_, data) => { formatter: (_, data) => {
if (!data.tier_info || !data.tier_info.current_rate) return '-' if (!data.tier_info || !data.tier_info.current_rate) return '-'
return data.tier_info.current_rate return data.tier_info.current_rate

View File

@@ -36,12 +36,21 @@
:marginTop="10" :marginTop="10"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<!-- 新增/编辑对话框 --> <!-- 新增/编辑对话框 -->
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
@@ -101,6 +110,54 @@
/> />
</ElSelect> </ElSelect>
</ElFormItem> </ElFormItem>
<ElFormItem label="套餐周期类型" prop="calendar_type">
<ElSelect
v-model="form.calendar_type"
placeholder="请选择套餐周期类型"
style="width: 100%"
clearable
>
<ElOption label="自然月" value="natural_month" />
<ElOption label="按天" value="by_day" />
</ElSelect>
</ElFormItem>
<ElFormItem label="有效期(月)" prop="duration_months">
<ElInputNumber
v-model="form.duration_months"
:min="1"
:max="120"
:controls="false"
style="width: 100%"
placeholder="请输入有效期(月)"
/>
</ElFormItem>
<ElFormItem
v-if="form.calendar_type === 'by_day'"
label="套餐天数"
prop="duration_days"
>
<ElInputNumber
v-model="form.duration_days"
:min="1"
:max="3650"
:controls="false"
style="width: 100%"
placeholder="请输入套餐天数"
/>
</ElFormItem>
<ElFormItem label="流量重置周期" prop="data_reset_cycle">
<ElSelect
v-model="form.data_reset_cycle"
placeholder="请选择流量重置周期"
style="width: 100%"
clearable
>
<ElOption label="每日" value="daily" />
<ElOption label="每月" value="monthly" />
<ElOption label="每年" value="yearly" />
<ElOption label="不重置" value="none" />
</ElSelect>
</ElFormItem>
<ElFormItem label="真流量额度(MB)" prop="real_data_mb"> <ElFormItem label="真流量额度(MB)" prop="real_data_mb">
<ElInputNumber <ElInputNumber
v-model="form.real_data_mb" v-model="form.real_data_mb"
@@ -130,13 +187,11 @@
placeholder="请输入虚流量额度" placeholder="请输入虚流量额度"
/> />
</ElFormItem> </ElFormItem>
<ElFormItem label="有效期(月)" prop="duration_months"> <ElFormItem label="启用实名激活">
<ElInputNumber <ElSwitch
v-model="form.duration_months" v-model="form.enable_realname_activation"
:min="1" active-text="启用"
:max="120" inactive-text="不启用"
:controls="false"
style="width: 100%"
/> />
</ElFormItem> </ElFormItem>
<ElFormItem label="成本价(元)" prop="cost_price"> <ElFormItem label="成本价(元)" prop="cost_price">
@@ -197,6 +252,8 @@
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias' import { RoutesAlias } from '@/router/routesAlias'
import { import {
@@ -221,6 +278,8 @@
const seriesLoading = ref(false) const seriesLoading = ref(false)
const tableRef = ref() const tableRef = ref()
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<PackageResponse | null>(null)
const seriesOptions = ref<SeriesSelectOption[]>([]) const seriesOptions = ref<SeriesSelectOption[]>([])
const searchSeriesOptions = ref<SeriesSelectOption[]>([]) const searchSeriesOptions = ref<SeriesSelectOption[]>([])
@@ -327,8 +386,7 @@
{ label: '建议售价', prop: 'suggested_retail_price' }, { label: '建议售价', prop: 'suggested_retail_price' },
{ label: '上架状态', prop: 'shelf_status' }, { label: '上架状态', prop: 'shelf_status' },
{ label: '状态', prop: 'status' }, { label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' }, { label: '创建时间', prop: 'created_at' }
{ label: '操作', prop: 'operation' }
] ]
// 表单验证规则 // 表单验证规则
@@ -355,6 +413,14 @@
baseRules.virtual_data_mb = [{ required: true, message: '请输入虚流量额度', trigger: 'blur' }] baseRules.virtual_data_mb = [{ required: true, message: '请输入虚流量额度', trigger: 'blur' }]
} }
// 如果套餐周期类型是按天,则套餐天数为必填
if (form.calendar_type === 'by_day') {
baseRules.duration_days = [
{ required: true, message: '请输入套餐天数', trigger: 'blur' },
{ type: 'number', min: 1, max: 3650, message: '套餐天数范围 1-3650 天', trigger: 'blur' }
]
}
return baseRules return baseRules
}) })
@@ -365,10 +431,14 @@
package_name: '', package_name: '',
series_id: undefined, series_id: undefined,
package_type: '', package_type: '',
calendar_type: undefined,
duration_days: undefined,
duration_months: 1,
data_reset_cycle: undefined,
enable_virtual_data: false, enable_virtual_data: false,
enable_realname_activation: false,
real_data_mb: 0, real_data_mb: 0,
virtual_data_mb: 0, virtual_data_mb: 0,
duration_months: 1,
cost_price: 0, cost_price: 0,
suggested_retail_price: undefined, suggested_retail_price: undefined,
description: '' description: ''
@@ -388,12 +458,14 @@
{ {
prop: 'package_name', prop: 'package_name',
label: '套餐名称', label: '套餐名称',
minWidth: 160 minWidth: 160,
showOverflowTooltip: true
}, },
{ {
prop: 'series_name', prop: 'series_name',
label: '所属系列', label: '所属系列',
width: 120 width: 160,
showOverflowTooltip: true
}, },
{ {
prop: 'package_type', prop: 'package_type',
@@ -476,45 +548,35 @@
label: '创建时间', label: '创建时间',
width: 180, width: 180,
formatter: (row: PackageResponse) => formatDateTime(row.created_at) formatter: (row: PackageResponse) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: '操作',
width: 220,
fixed: 'right',
formatter: (row: PackageResponse) => {
const buttons = []
buttons.push(
h(ArtButtonTable, {
text: '详情',
onClick: () => handleViewDetail(row)
})
)
if (hasAuth('package:edit')) {
buttons.push(
h(ArtButtonTable, {
text: '编辑',
onClick: () => showDialog('edit', row)
})
)
}
if (hasAuth('package:delete')) {
buttons.push(
h(ArtButtonTable, {
text: '删除',
onClick: () => deletePackage(row)
})
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
} }
]) ])
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
items.push({
key: 'detail',
label: '详情'
})
if (hasAuth('package:edit')) {
items.push({
key: 'edit',
label: '编辑'
})
}
if (hasAuth('package:delete')) {
items.push({
key: 'delete',
label: '删除'
})
}
return items
})
// 监听虚流量开关变化,关闭时重置虚流量额度 // 监听虚流量开关变化,关闭时重置虚流量额度
watch( watch(
() => form.enable_virtual_data, () => form.enable_virtual_data,
@@ -545,7 +607,7 @@
} }
const res = await PackageSeriesService.getPackageSeries(params) const res = await PackageSeriesService.getPackageSeries(params)
if (res.code === 0) { if (res.code === 0) {
seriesOptions.value = res.data.items || [] seriesOptions.value = res.data.items
} }
} catch (error) { } catch (error) {
console.error('加载系列选项失败:', error) console.error('加载系列选项失败:', error)
@@ -567,7 +629,7 @@
} }
const res = await PackageSeriesService.getPackageSeries(params) const res = await PackageSeriesService.getPackageSeries(params)
if (res.code === 0) { if (res.code === 0) {
searchSeriesOptions.value = res.data.items || [] searchSeriesOptions.value = res.data.items
} }
} catch (error) { } catch (error) {
console.error('加载搜索栏系列选项失败:', error) console.error('加载搜索栏系列选项失败:', error)
@@ -607,8 +669,8 @@
} }
const res = await PackageManageService.getPackages(params) const res = await PackageManageService.getPackages(params)
if (res.code === 0) { if (res.code === 0) {
packageList.value = res.data.items || [] packageList.value = res.data.items
pagination.total = res.data.total || 0 pagination.total = res.data.total
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@@ -658,10 +720,14 @@
form.package_name = row.package_name form.package_name = row.package_name
form.series_id = row.series_id form.series_id = row.series_id
form.package_type = row.package_type form.package_type = row.package_type
form.calendar_type = row.calendar_type || undefined
form.duration_days = row.duration_days || undefined
form.duration_months = row.duration_months
form.data_reset_cycle = row.data_reset_cycle || undefined
form.enable_virtual_data = row.enable_virtual_data || false form.enable_virtual_data = row.enable_virtual_data || false
form.enable_realname_activation = row.enable_realname_activation || false
form.real_data_mb = row.real_data_mb || 0 form.real_data_mb = row.real_data_mb || 0
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.cost_price = row.cost_price / 100 // 分转换为元显示 form.cost_price = row.cost_price / 100 // 分转换为元显示
form.suggested_retail_price = row.suggested_retail_price form.suggested_retail_price = row.suggested_retail_price
? row.suggested_retail_price / 100 ? row.suggested_retail_price / 100
@@ -673,10 +739,14 @@
form.package_name = '' form.package_name = ''
form.series_id = undefined form.series_id = undefined
form.package_type = '' form.package_type = ''
form.calendar_type = undefined
form.duration_days = undefined
form.duration_months = 1
form.data_reset_cycle = undefined
form.enable_virtual_data = false form.enable_virtual_data = false
form.enable_realname_activation = false
form.real_data_mb = 0 form.real_data_mb = 0
form.virtual_data_mb = 0 form.virtual_data_mb = 0
form.duration_months = 1
form.cost_price = 0 form.cost_price = 0
form.suggested_retail_price = undefined form.suggested_retail_price = undefined
form.description = '' form.description = ''
@@ -708,10 +778,14 @@
form.package_name = '' form.package_name = ''
form.series_id = undefined form.series_id = undefined
form.package_type = '' form.package_type = ''
form.calendar_type = undefined
form.duration_days = undefined
form.duration_months = 1
form.data_reset_cycle = undefined
form.enable_virtual_data = false form.enable_virtual_data = false
form.enable_realname_activation = false
form.real_data_mb = 0 form.real_data_mb = 0
form.virtual_data_mb = 0 form.virtual_data_mb = 0
form.duration_months = 1
form.cost_price = 0 form.cost_price = 0
form.suggested_retail_price = undefined form.suggested_retail_price = undefined
form.description = '' form.description = ''
@@ -758,13 +832,23 @@
package_type: form.package_type, package_type: form.package_type,
duration_months: form.duration_months, duration_months: form.duration_months,
cost_price: costPriceInCents, cost_price: costPriceInCents,
enable_virtual_data: form.enable_virtual_data enable_virtual_data: form.enable_virtual_data || false,
enable_realname_activation: form.enable_realname_activation || false
} }
// 可选字段 // 可选字段
if (form.series_id) { if (form.series_id) {
data.series_id = form.series_id data.series_id = form.series_id
} }
if (form.calendar_type) {
data.calendar_type = form.calendar_type
}
if (form.calendar_type === 'by_day' && form.duration_days) {
data.duration_days = form.duration_days
}
if (form.data_reset_cycle) {
data.data_reset_cycle = form.data_reset_cycle
}
if (suggestedRetailPriceInCents !== undefined) { if (suggestedRetailPriceInCents !== undefined) {
data.suggested_retail_price = suggestedRetailPriceInCents data.suggested_retail_price = suggestedRetailPriceInCents
} }
@@ -826,6 +910,31 @@
const handleViewDetail = (row: PackageResponse) => { const handleViewDetail = (row: PackageResponse) => {
router.push(`${RoutesAlias.PackageDetail}/${row.id}`) router.push(`${RoutesAlias.PackageDetail}/${row.id}`)
} }
// 处理表格行右键菜单
const handleRowContextMenu = (row: PackageResponse, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'detail':
handleViewDetail(currentRow.value)
break
case 'edit':
showDialog('edit', currentRow.value)
break
case 'delete':
deletePackage(currentRow.value)
break
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -44,28 +44,13 @@
</template> </template>
</ArtTable> </ArtTable>
<!-- 右键菜单 --> <!-- 套餐系列操作右键菜单 -->
<div <ArtMenuRight
v-if="contextMenuVisible" ref="seriesOperationMenuRef"
:style="{ :menu-items="seriesOperationMenuItems"
position: 'fixed', :menu-width="140"
left: contextMenuPosition.x + 'px', @select="handleSeriesOperationMenuSelect"
top: contextMenuPosition.y + 'px', />
zIndex: 9999
}"
>
<ElCard shadow="always" class="context-menu">
<div class="context-menu-item" @click="handleViewDetail(contextMenuRow)" v-if="hasAuth('package_series:detail')">
<span>详情</span>
</div>
<div class="context-menu-item" @click="showDialog('edit', contextMenuRow)" v-if="hasAuth('package_series:edit')">
<span>编辑</span>
</div>
<div class="context-menu-item danger" @click="deleteSeries(contextMenuRow)" v-if="hasAuth('package_series:delete')">
<span>删除</span>
</div>
</ElCard>
</div>
<!-- 新增/编辑对话框 --> <!-- 新增/编辑对话框 -->
<ElDialog <ElDialog
@@ -389,6 +374,8 @@
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { import {
CommonStatus, CommonStatus,
@@ -410,10 +397,9 @@
const tableRef = ref() const tableRef = ref()
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
// 右键菜单状态 // 右键菜单
const contextMenuVisible = ref(false) const seriesOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const contextMenuPosition = reactive({ x: 0, y: 0 }) const currentOperatingSeries = ref<PackageSeriesResponse | null>(null)
const contextMenuRow = ref<PackageSeriesResponse | null>(null)
// 搜索表单初始值 // 搜索表单初始值
const initialSearchState = { const initialSearchState = {
@@ -708,27 +694,74 @@
onMounted(() => { onMounted(() => {
getTableData() getTableData()
// 添加全局点击事件监听,关闭右键菜单
document.addEventListener('click', closeContextMenu)
}) })
onBeforeUnmount(() => { // 套餐系列操作菜单项配置
// 移除全局点击事件监听 const seriesOperationMenuItems = computed((): MenuItemType[] => {
document.removeEventListener('click', closeContextMenu) const items: MenuItemType[] = []
// 详情
if (hasAuth('package_series:detail')) {
items.push({
key: 'detail',
label: '详情'
})
}
// 编辑
if (hasAuth('package_series:edit')) {
items.push({
key: 'edit',
label: '编辑'
})
}
// 删除
if (hasAuth('package_series:delete')) {
items.push({
key: 'delete',
label: '删除'
})
}
return items
}) })
// 处理右键菜单 // 显示套餐系列操作右键菜单
const handleRowContextMenu = (row: PackageSeriesResponse, column: any, event: MouseEvent) => { const showSeriesOperationMenu = (e: MouseEvent, row: PackageSeriesResponse) => {
event.preventDefault() e.preventDefault()
contextMenuRow.value = row e.stopPropagation()
contextMenuPosition.x = event.clientX currentOperatingSeries.value = row
contextMenuPosition.y = event.clientY seriesOperationMenuRef.value?.show(e)
contextMenuVisible.value = true
} }
// 关闭右键菜单 // 处理表格行右键菜单
const closeContextMenu = () => { const handleRowContextMenu = (row: PackageSeriesResponse, column: any, event: MouseEvent) => {
contextMenuVisible.value = false // 如果用户有编辑或删除权限,显示右键菜单
if (
hasAuth('package_series:edit') ||
hasAuth('package_series:delete') ||
hasAuth('package_series:detail')
) {
showSeriesOperationMenu(event, row)
}
}
// 处理套餐系列操作菜单选择
const handleSeriesOperationMenuSelect = (item: MenuItemType) => {
if (!currentOperatingSeries.value) return
switch (item.key) {
case 'detail':
handleViewDetail(currentOperatingSeries.value)
break
case 'edit':
showDialog('edit', currentOperatingSeries.value)
break
case 'delete':
deleteSeries(currentOperatingSeries.value)
break
}
} }
// 获取套餐系列列表 // 获取套餐系列列表
@@ -744,8 +777,8 @@
} }
const res = await PackageSeriesService.getPackageSeries(params) const res = await PackageSeriesService.getPackageSeries(params)
if (res.code === 0) { if (res.code === 0) {
seriesList.value = res.data.items || [] seriesList.value = res.data.items
pagination.total = res.data.total || 0 pagination.total = res.data.total
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@@ -1058,35 +1091,4 @@
.dialog-footer { .dialog-footer {
text-align: right; text-align: right;
} }
.context-menu {
min-width: 120px;
padding: 4px 0;
background: white;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
:deep(.el-card__body) {
padding: 0;
}
.context-menu-item {
padding: 10px 20px;
cursor: pointer;
transition: background-color 0.2s;
font-size: 14px;
color: var(--el-text-color-primary);
&:hover {
background-color: var(--el-fill-color-light);
}
&.danger {
color: var(--el-color-danger);
&:hover {
background-color: var(--el-color-danger-light-9);
}
}
}
}
</style> </style>

View File

@@ -37,12 +37,21 @@
:marginTop="10" :marginTop="10"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<!-- 新增/编辑对话框 --> <!-- 新增/编辑对话框 -->
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
@@ -205,6 +214,8 @@
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { import {
CommonStatus, CommonStatus,
@@ -225,6 +236,8 @@
const shopLoading = ref(false) const shopLoading = ref(false)
const tableRef = ref() const tableRef = ref()
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<ShopSeriesAllocationResponse | null>(null)
const seriesOptions = ref<PackageSeriesResponse[]>([]) const seriesOptions = ref<PackageSeriesResponse[]>([])
const shopOptions = ref<ShopResponse[]>([]) const shopOptions = ref<ShopResponse[]>([])
const shopTreeData = ref<ShopResponse[]>([]) const shopTreeData = ref<ShopResponse[]>([])
@@ -340,8 +353,7 @@
{ label: '强充金额', prop: 'force_recharge_amount' }, { label: '强充金额', prop: 'force_recharge_amount' },
{ label: '强充触发类型', prop: 'force_recharge_trigger_type' }, { label: '强充触发类型', prop: 'force_recharge_trigger_type' },
{ label: '状态', prop: 'status' }, { label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' }, { label: '创建时间', prop: 'created_at' }
{ label: '操作', prop: 'operation' }
] ]
// 表单数据 // 表单数据
@@ -563,43 +575,6 @@
label: '创建时间', label: '创建时间',
width: 180, width: 180,
formatter: (row: ShopSeriesAllocationResponse) => formatDateTime(row.created_at) formatter: (row: ShopSeriesAllocationResponse) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: '操作',
width: 220,
fixed: 'right',
formatter: (row: ShopSeriesAllocationResponse) => {
const buttons = []
// 详情按钮
buttons.push(
h(ArtButtonTable, {
text: '详情',
onClick: () => handleViewDetail(row)
})
)
if (hasAuth('series_assign:edit')) {
buttons.push(
h(ArtButtonTable, {
text: '编辑',
onClick: () => showDialog('edit', row)
})
)
}
if (hasAuth('series_assign:delete')) {
buttons.push(
h(ArtButtonTable, {
text: '删除',
onClick: () => deleteAllocation(row)
})
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
} }
]) ])
@@ -653,7 +628,7 @@
} }
const res = await PackageSeriesService.getPackageSeries(params) const res = await PackageSeriesService.getPackageSeries(params)
if (res.code === 0) { if (res.code === 0) {
seriesOptions.value = res.data.items || [] seriesOptions.value = res.data.items
} }
} catch (error) { } catch (error) {
console.error('加载系列选项失败:', error) console.error('加载系列选项失败:', error)
@@ -672,7 +647,7 @@
page_size: 10000 // 使用较大的值获取所有店铺 page_size: 10000 // 使用较大的值获取所有店铺
}) })
if (res.code === 0) { if (res.code === 0) {
shopOptions.value = res.data.items || [] shopOptions.value = res.data.items
// 构建树形结构数据 // 构建树形结构数据
shopTreeData.value = buildTreeData(shopOptions.value) shopTreeData.value = buildTreeData(shopOptions.value)
} }
@@ -692,7 +667,7 @@
status: 1 status: 1
}) })
if (res.code === 0) { if (res.code === 0) {
searchSeriesOptions.value = res.data.items || [] searchSeriesOptions.value = res.data.items
} }
} catch (error) { } catch (error) {
console.error('加载搜索栏系列选项失败:', error) console.error('加载搜索栏系列选项失败:', error)
@@ -704,7 +679,7 @@
try { try {
const res = await ShopService.getShops({ page: 1, page_size: 10 }) const res = await ShopService.getShops({ page: 1, page_size: 10 })
if (res.code === 0) { if (res.code === 0) {
searchShopOptions.value = res.data.items || [] searchShopOptions.value = res.data.items
} }
} catch (error) { } catch (error) {
console.error('加载搜索栏店铺选项失败:', error) console.error('加载搜索栏店铺选项失败:', error)
@@ -716,7 +691,7 @@
try { try {
const res = await ShopService.getShops({ page: 1, page_size: 10 }) const res = await ShopService.getShops({ page: 1, page_size: 10 })
if (res.code === 0) { if (res.code === 0) {
searchAllocatorShopOptions.value = res.data.items || [] searchAllocatorShopOptions.value = res.data.items
} }
} catch (error) { } catch (error) {
console.error('加载搜索栏分配者店铺选项失败:', error) console.error('加载搜索栏分配者店铺选项失败:', error)
@@ -746,7 +721,7 @@
status: 1 status: 1
}) })
if (res.code === 0) { if (res.code === 0) {
searchSeriesOptions.value = res.data.items || [] searchSeriesOptions.value = res.data.items
} }
} catch (error) { } catch (error) {
console.error('搜索系列失败:', error) console.error('搜索系列失败:', error)
@@ -766,7 +741,7 @@
shop_name: query shop_name: query
}) })
if (res.code === 0) { if (res.code === 0) {
searchShopOptions.value = res.data.items || [] searchShopOptions.value = res.data.items
} }
} catch (error) { } catch (error) {
console.error('搜索店铺失败:', error) console.error('搜索店铺失败:', error)
@@ -786,7 +761,7 @@
shop_name: query shop_name: query
}) })
if (res.code === 0) { if (res.code === 0) {
searchAllocatorShopOptions.value = res.data.items || [] searchAllocatorShopOptions.value = res.data.items
} }
} catch (error) { } catch (error) {
console.error('搜索分配者店铺失败:', error) console.error('搜索分配者店铺失败:', error)
@@ -808,7 +783,7 @@
} }
const res = await ShopSeriesAllocationService.getShopSeriesAllocations(params) const res = await ShopSeriesAllocationService.getShopSeriesAllocations(params)
if (res.code === 0) { if (res.code === 0) {
allocationList.value = res.data.items || [] allocationList.value = res.data.items
pagination.total = res.data.total || 0 pagination.total = res.data.total || 0
} }
} catch (error) { } catch (error) {
@@ -1011,6 +986,48 @@
console.error(error) console.error(error)
} }
} }
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
items.push({ key: 'detail', label: '详情' })
if (hasAuth('series_assign:edit')) {
items.push({ key: 'edit', label: '编辑' })
}
if (hasAuth('series_assign:delete')) {
items.push({ key: 'delete', label: '删除' })
}
return items
})
// 处理表格行右键菜单
const handleRowContextMenu = (row: ShopSeriesAllocationResponse, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'detail':
handleViewDetail(currentRow.value)
break
case 'edit':
showDialog('edit', currentRow.value)
break
case 'delete':
deleteAllocation(currentRow.value)
break
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -190,6 +190,7 @@
{ label: '权限类型', prop: 'perm_type' }, { label: '权限类型', prop: 'perm_type' },
{ label: '菜单路径', prop: 'url' }, { label: '菜单路径', prop: 'url' },
{ label: '适用端口', prop: 'platform' }, { label: '适用端口', prop: 'platform' },
// { label: '可用角色类型', prop: 'available_for_role_types' },
{ label: '状态', prop: 'status' }, { label: '状态', prop: 'status' },
{ label: '排序', prop: 'sort' }, { label: '排序', prop: 'sort' },
{ label: '操作', prop: 'operation' } { label: '操作', prop: 'operation' }
@@ -242,7 +243,7 @@
{ {
prop: 'perm_name', prop: 'perm_name',
label: '权限名称', label: '权限名称',
width: 200 width: 240
}, },
{ {
prop: 'perm_code', prop: 'perm_code',
@@ -276,6 +277,24 @@
return platformMap[row.platform || 'all'] || row.platform return platformMap[row.platform || 'all'] || row.platform
} }
}, },
// {
// prop: 'available_for_role_types',
// label: '可用角色类型',
// width: 160,
// formatter: (row: PermissionTreeNode) => {
// if (!row.available_for_role_types) {
// return '-'
// }
// const roleTypeMap: Record<string, string> = {
// '1': '平台角色',
// '2': '客户角色'
// }
// const types = row.available_for_role_types
// .split(',')
// .map((type) => roleTypeMap[type.trim()] || type)
// return types.join(', ')
// }
// },
{ {
prop: 'status', prop: 'status',
label: '状态', label: '状态',
@@ -417,7 +436,7 @@
} }
// 显示对话框 // 显示对话框
const showDialog = (type: string, row?: PermissionTreeNode) => { const showDialog = async (type: string, row?: PermissionTreeNode) => {
dialogVisible.value = true dialogVisible.value = true
dialogType.value = type dialogType.value = type
@@ -428,18 +447,35 @@
if (type === 'edit' && row) { if (type === 'edit' && row) {
currentRow.value = row currentRow.value = row
currentPermissionId.value = row.id currentPermissionId.value = row.id
// 需要从API获取完整的权限数据,因为树节点可能不包含所有字段 // 从API获取完整的权限数据包含parent_id
// 暂时使用树节点的数据 try {
Object.assign(form, { const response = await PermissionService.getPermission(row.id)
perm_name: row.perm_name, if (response.code === 0 && response.data) {
perm_code: row.perm_code, Object.assign(form, {
perm_type: row.perm_type, perm_name: response.data.perm_name,
parent_id: undefined, // 树结构中没有parent_id需要从API获取 perm_code: response.data.perm_code,
url: row.url || '', perm_type: response.data.perm_type,
platform: row.platform || 'all', parent_id: response.data.parent_id || undefined,
sort: row.sort || 0, url: response.data.url || '',
status: row.status ?? CommonStatus.ENABLED platform: response.data.platform || 'all',
}) sort: response.data.sort || 0,
status: response.data.status ?? CommonStatus.ENABLED
})
}
} catch (error) {
console.error('获取权限详情失败:', error)
// 如果API获取失败使用树节点数据作为备选
Object.assign(form, {
perm_name: row.perm_name,
perm_code: row.perm_code,
perm_type: row.perm_type,
parent_id: undefined,
url: row.url || '',
platform: row.platform || 'all',
sort: row.sort || 0,
status: row.status ?? CommonStatus.ENABLED
})
}
} else { } else {
currentPermissionId.value = 0 currentPermissionId.value = 0
resetForm() resetForm()

View File

@@ -34,12 +34,21 @@
:marginTop="10" :marginTop="10"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
> >
<template #default> <template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template> </template>
</ArtTable> </ArtTable>
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<!-- 新增/编辑对话框 --> <!-- 新增/编辑对话框 -->
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
@@ -226,6 +235,8 @@
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText } from '@/config/constants' import { CommonStatus, getStatusText } from '@/config/constants'
@@ -253,6 +264,8 @@
const leftTreeFilter = ref('') // 左侧树搜索关键字 const leftTreeFilter = ref('') // 左侧树搜索关键字
const rightTreeFilter = ref('') // 右侧树搜索关键字 const rightTreeFilter = ref('') // 右侧树搜索关键字
const isHandlingCheck = ref(false) // 标志位:是否正在处理勾选事件 const isHandlingCheck = ref(false) // 标志位:是否正在处理勾选事件
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<PlatformRole | null>(null)
// 搜索表单初始值 // 搜索表单初始值
const initialSearchState = { const initialSearchState = {
@@ -317,8 +330,7 @@
{ label: '角色描述', prop: 'role_desc' }, { label: '角色描述', prop: 'role_desc' },
{ label: '角色类型', prop: 'role_type' }, { label: '角色类型', prop: 'role_type' },
{ label: '状态', prop: 'status' }, { label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'CreatedAt' }, { label: '创建时间', prop: 'CreatedAt' }
{ label: '操作', prop: 'operation' }
] ]
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
@@ -391,47 +403,6 @@
label: '创建时间', label: '创建时间',
width: 180, width: 180,
formatter: (row: any) => formatDateTime(row.CreatedAt) formatter: (row: any) => formatDateTime(row.CreatedAt)
},
{
prop: 'operation',
label: '操作',
width: 240,
fixed: 'right',
formatter: (row: any) => {
const buttons = []
// 分配权限按钮
if (hasAuth('role:permission')) {
buttons.push(
h(ArtButtonTable, {
text: '分配权限',
onClick: () => showPermissionDialog(row)
})
)
}
// 编辑按钮
if (hasAuth('role:edit')) {
buttons.push(
h(ArtButtonTable, {
text: '编辑',
onClick: () => showDialog('edit', row)
})
)
}
// 删除按钮
if (hasAuth('role:delete')) {
buttons.push(
h(ArtButtonTable, {
text: '删除',
onClick: () => deleteRole(row)
})
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
} }
]) ])
@@ -1038,6 +1009,50 @@
console.error(error) console.error(error)
} }
} }
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
if (hasAuth('role:permission')) {
items.push({ key: 'assignPermission', label: '分配权限' })
}
if (hasAuth('role:edit')) {
items.push({ key: 'edit', label: '编辑' })
}
if (hasAuth('role:delete')) {
items.push({ key: 'delete', label: '删除' })
}
return items
})
// 处理表格行右键菜单
const handleRowContextMenu = (row: any, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'assignPermission':
showPermissionDialog(currentRow.value)
break
case 'edit':
showDialog('edit', currentRow.value)
break
case 'delete':
deleteRole(currentRow.value)
break
}
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">