fetch(modify):修复BUG
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 3m27s

This commit is contained in:
sexygoat
2026-02-03 14:39:45 +08:00
parent 2c6fe4375b
commit de9753f42d
28 changed files with 4344 additions and 5092 deletions

View File

@@ -5,7 +5,6 @@
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
:show-expand="false"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
@@ -121,10 +120,11 @@
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { AccountService } from '@/api/modules/account'
import { RoleService } from '@/api/modules/role'
import { ShopService } from '@/api/modules'
import type { SearchFormItem } from '@/types'
import type { PlatformRole } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText } from '@/config/constants'
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
defineOptions({ name: 'Account' }) // 定义组件名称,用于 KeepAlive 缓存控制
@@ -140,12 +140,20 @@
// 定义表单搜索初始值
const initialSearchState = {
name: '',
phone: ''
phone: '',
user_type: undefined as number | undefined,
shop_id: undefined as number | undefined,
enterprise_id: undefined as number | undefined,
status: undefined as number | undefined
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
// 店铺和企业列表
const shopList = ref<any[]>([])
const enterpriseList = ref<any[]>([])
const pagination = reactive({
currentPage: 1,
pageSize: 20,
@@ -176,7 +184,7 @@
}
// 表单配置项
const formItems: SearchFormItem[] = [
const formItems = computed<SearchFormItem[]>(() => [
{
label: '账号名称',
prop: 'name',
@@ -194,17 +202,59 @@
clearable: true,
placeholder: '请输入手机号'
}
},
{
label: '账号类型',
prop: 'user_type',
type: 'select',
options: [
{ label: '超级管理员', value: 1 },
{ label: '平台用户', value: 2 },
{ label: '代理账号', value: 3 },
{ label: '企业账号', value: 4 }
],
config: {
clearable: true,
placeholder: '请选择账号类型'
}
},
{
label: '关联店铺',
prop: 'shop_id',
type: 'select',
options: shopList.value.map((shop) => ({
label: shop.shop_name,
value: shop.id
})),
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleShopSearch,
placeholder: '请输入店铺名称搜索'
}
},
{
label: '状态',
prop: 'status',
type: 'select',
options: STATUS_SELECT_OPTIONS,
config: {
clearable: true,
placeholder: '请选择状态'
}
}
]
])
// 列配置
const columnOptions = [
{ label: 'ID', prop: 'ID' },
{ label: '账号名称', prop: 'username' },
{ label: '手机号', prop: 'phone' },
{ label: '账号类型', prop: 'user_type_name' },
{ label: '账号类型', prop: 'user_type' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '企业名称', prop: 'enterprise_name' },
{ label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'CreatedAt' },
{ label: '创建时间', prop: 'created_at' },
{ label: '操作', prop: 'operation' }
]
@@ -256,21 +306,20 @@
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'ID',
label: 'ID'
},
{
prop: 'username',
label: '账号名称'
label: '账号名称',
minWidth: 120
},
{
prop: 'phone',
label: '手机号'
label: '手机号',
width: 130
},
{
prop: 'user_type',
label: '账号类型',
width: 120,
formatter: (row: any) => {
const typeMap: Record<number, string> = {
1: '超级管理员',
@@ -281,9 +330,26 @@
return typeMap[row.user_type] || '-'
}
},
{
prop: 'shop_name',
label: '店铺名称',
minWidth: 150,
formatter: (row: any) => {
return row.shop_name || '-'
}
},
{
prop: 'enterprise_name',
label: '企业名称',
minWidth: 150,
formatter: (row: any) => {
return row.enterprise_name || '-'
}
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: any) => {
return h(ElSwitch, {
modelValue: row.status,
@@ -298,13 +364,15 @@
}
},
{
prop: 'CreatedAt',
prop: 'created_at',
label: '创建时间',
formatter: (row: any) => formatDateTime(row.CreatedAt)
width: 180,
formatter: (row: any) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: '操作',
width: 200,
fixed: 'right',
formatter: (row: any) => {
return h('div', { style: 'display: flex; gap: 8px;' }, [
@@ -340,6 +408,7 @@
onMounted(() => {
getAccountList()
loadAllRoles()
loadShopList()
})
// 加载所有角色列表
@@ -400,7 +469,12 @@
const params = {
page: pagination.currentPage,
pageSize: pagination.pageSize,
keyword: formFilters.name || formFilters.phone || undefined
username: formFilters.name || undefined,
phone: formFilters.phone || undefined,
user_type: formFilters.user_type,
shop_id: formFilters.shop_id,
enterprise_id: formFilters.enterprise_id,
status: formFilters.status
}
const res = await AccountService.getAccounts(params)
if (res.code === 0) {
@@ -489,7 +563,7 @@
// 先更新UI
row.status = newStatus
try {
await AccountService.updateAccount(row.ID, { status: newStatus })
await AccountService.updateAccountStatus(row.ID, newStatus as 0 | 1)
ElMessage.success('状态切换成功')
} catch (error) {
// 切换失败,恢复原状态
@@ -497,6 +571,28 @@
console.error(error)
}
}
// 加载店铺列表
const loadShopList = async (keyword: string = '') => {
try {
const res = await ShopService.getShops({
page: 1,
page_size: 20, // 默认加载20条
status: 1, // 只加载启用的店铺
shop_name: keyword || undefined // 根据店铺名称搜索
})
if (res.code === 0) {
shopList.value = res.data.items || []
}
} catch (error) {
console.error('获取店铺列表失败:', error)
}
}
// 店铺搜索处理
const handleShopSearch = (query: string) => {
loadShopList(query)
}
</script>
<style lang="scss" scoped>

View File

@@ -165,7 +165,7 @@
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { AccountService, RoleService } from '@/api/modules'
import { AccountService, RoleService, ShopService } from '@/api/modules'
import type { SearchFormItem } from '@/types'
import type { PlatformRole, PlatformAccount } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
@@ -189,12 +189,19 @@
const initialSearchState = {
username: '',
phone: '',
user_type: undefined as number | undefined,
shop_id: undefined as number | undefined,
enterprise_id: undefined as number | undefined,
status: undefined as number | undefined
}
// 响应式表单数据
const searchForm = reactive({ ...initialSearchState })
// 店铺和企业列表
const shopList = ref<any[]>([])
const enterpriseList = ref<any[]>([])
const pagination = reactive({
currentPage: 1,
pageSize: 20,
@@ -225,7 +232,7 @@
}
// 表单配置项
const searchFormItems: SearchFormItem[] = [
const searchFormItems = computed<SearchFormItem[]>(() => [
{
label: '账号名称',
prop: 'username',
@@ -244,6 +251,35 @@
placeholder: '请输入手机号'
}
},
{
label: '账号类型',
prop: 'user_type',
type: 'select',
options: [
{ label: '超级管理员', value: 1 },
{ label: '平台用户', value: 2 },
{ label: '代理账号', value: 3 },
{ label: '企业账号', value: 4 }
],
config: {
clearable: true,
placeholder: '请选择账号类型'
}
},
{
label: '关联店铺',
prop: 'shop_id',
type: 'select',
options: shopList.value.map((shop) => ({
label: shop.shop_name,
value: shop.id
})),
config: {
clearable: true,
filterable: true,
placeholder: '请选择店铺'
}
},
{
label: '状态',
prop: 'status',
@@ -254,7 +290,7 @@
placeholder: '请选择状态'
}
}
]
])
// 列配置
const columnOptions = [
@@ -262,6 +298,8 @@
{ label: '账号名称', prop: 'username' },
{ label: '手机号', prop: 'phone' },
{ label: '账号类型', prop: 'user_type' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '企业名称', prop: 'enterprise_name' },
{ label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'CreatedAt' },
{ label: '操作', prop: 'operation' }
@@ -284,7 +322,7 @@
formData.user_type = row.user_type
formData.enterprise_id = row.enterprise_id || null
formData.shop_id = row.shop_id || null
formData.status = row.status as CommonStatus
formData.status = row.status as unknown as CommonStatus
formData.password = ''
} else {
formData.id = 0
@@ -333,19 +371,23 @@
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'ID',
label: 'ID'
label: 'ID',
width: 80
},
{
prop: 'username',
label: '账号名称'
label: '账号名称',
minWidth: 120
},
{
prop: 'phone',
label: '手机号'
label: '手机号',
width: 130
},
{
prop: 'user_type',
label: '账号类型',
width: 120,
formatter: (row: PlatformAccount) => {
const typeMap: Record<number, string> = {
1: '超级管理员',
@@ -356,9 +398,26 @@
return typeMap[row.user_type] || '-'
}
},
{
prop: 'shop_name',
label: '店铺名称',
minWidth: 150,
formatter: (row: PlatformAccount) => {
return row.shop_name || '-'
}
},
{
prop: 'enterprise_name',
label: '企业名称',
minWidth: 150,
formatter: (row: PlatformAccount) => {
return row.enterprise_name || '-'
}
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: PlatformAccount) => {
return h(ElSwitch, {
modelValue: row.status,
@@ -375,6 +434,7 @@
{
prop: 'CreatedAt',
label: '创建时间',
width: 180,
formatter: (row: PlatformAccount) => formatDateTime(row.CreatedAt)
},
{
@@ -429,6 +489,7 @@
onMounted(() => {
getAccountList()
loadAllRoles()
loadShopList()
})
// 加载所有角色列表
@@ -512,9 +573,11 @@
const params = {
page: pagination.currentPage,
pageSize: pagination.pageSize,
user_type: 2, // 筛选平台账号
username: searchForm.username || undefined,
phone: searchForm.phone || undefined,
user_type: searchForm.user_type, // 账号类型筛选(可选)
shop_id: searchForm.shop_id, // 店铺筛选
enterprise_id: searchForm.enterprise_id, // 企业筛选
status: searchForm.status
}
const res = await AccountService.getAccounts(params)
@@ -659,6 +722,22 @@
console.error(error)
}
}
// 加载店铺列表
const loadShopList = async () => {
try {
const res = await ShopService.getShops({
page: 1,
page_size: 9999,
status: 1 // 只加载启用的店铺
})
if (res.code === 0) {
shopList.value = res.data.items || []
}
} catch (error) {
console.error('获取店铺列表失败:', error)
}
}
</script>
<style lang="scss" scoped>

View File

@@ -353,6 +353,14 @@
</div>
</ElDialog>
<!-- 设备操作右键菜单 -->
<ArtMenuRight
ref="deviceOperationMenuRef"
:menu-items="deviceOperationMenuItems"
:menu-width="140"
@select="handleDeviceOperationMenuSelect"
/>
<!-- 绑定卡弹窗 -->
<ElDialog v-model="bindCardDialogVisible" title="绑定卡到设备" width="500px">
<ElForm ref="bindCardFormRef" :model="bindCardForm" :rules="bindCardRules" label-width="100px">
@@ -394,6 +402,119 @@
</ElButton>
</template>
</ElDialog>
<!-- 设置限速对话框 -->
<ElDialog v-model="speedLimitDialogVisible" title="设置限速" width="500px">
<ElForm
ref="speedLimitFormRef"
:model="speedLimitForm"
:rules="speedLimitRules"
label-width="120px"
>
<ElFormItem label="设备号">
<span style="font-weight: bold; color: #409eff">{{ currentOperatingDevice }}</span>
</ElFormItem>
<ElFormItem label="下行速率" prop="download_speed">
<ElInputNumber
v-model="speedLimitForm.download_speed"
:min="1"
:step="128"
controls-position="right"
style="width: 100%"
/>
<div style="color: #909399; font-size: 12px; margin-top: 4px">单位: KB/s</div>
</ElFormItem>
<ElFormItem label="上行速率" prop="upload_speed">
<ElInputNumber
v-model="speedLimitForm.upload_speed"
:min="1"
:step="128"
controls-position="right"
style="width: 100%"
/>
<div style="color: #909399; font-size: 12px; margin-top: 4px">单位: KB/s</div>
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="speedLimitDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleConfirmSpeedLimit" :loading="speedLimitLoading">
确认设置
</ElButton>
</template>
</ElDialog>
<!-- 切换SIM卡对话框 -->
<ElDialog v-model="switchCardDialogVisible" title="切换SIM卡" width="500px">
<ElForm
ref="switchCardFormRef"
:model="switchCardForm"
:rules="switchCardRules"
label-width="120px"
>
<ElFormItem label="设备号">
<span style="font-weight: bold; color: #409eff">{{ currentOperatingDevice }}</span>
</ElFormItem>
<ElFormItem label="目标ICCID" prop="target_iccid">
<ElInput
v-model="switchCardForm.target_iccid"
placeholder="请输入要切换到的目标ICCID"
clearable
/>
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="switchCardDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleConfirmSwitchCard" :loading="switchCardLoading">
确认切换
</ElButton>
</template>
</ElDialog>
<!-- 设置WiFi对话框 -->
<ElDialog v-model="setWiFiDialogVisible" title="设置WiFi" width="500px">
<ElForm
ref="setWiFiFormRef"
:model="setWiFiForm"
:rules="setWiFiRules"
label-width="120px"
>
<ElFormItem label="设备号">
<span style="font-weight: bold; color: #409eff">{{ currentOperatingDevice }}</span>
</ElFormItem>
<ElFormItem label="WiFi状态" prop="enabled">
<ElRadioGroup v-model="setWiFiForm.enabled">
<ElRadio :value="1">启用</ElRadio>
<ElRadio :value="0">禁用</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="WiFi名称" prop="ssid">
<ElInput
v-model="setWiFiForm.ssid"
placeholder="请输入WiFi名称1-32个字符"
maxlength="32"
show-word-limit
clearable
/>
</ElFormItem>
<ElFormItem label="WiFi密码" prop="password">
<ElInput
v-model="setWiFiForm.password"
type="password"
placeholder="请输入WiFi密码8-63个字符"
maxlength="63"
show-word-limit
show-password
clearable
/>
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="setWiFiDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleConfirmSetWiFi" :loading="setWiFiLoading">
确认设置
</ElButton>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
@@ -403,7 +524,17 @@
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { DeviceService, ShopService, CardService, PackageSeriesService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElIcon, ElTreeSelect } from 'element-plus'
import {
ElMessage,
ElMessageBox,
ElTag,
ElSwitch,
ElIcon,
ElTreeSelect,
ElInputNumber,
ElRadioGroup,
ElRadio
} from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import type {
@@ -416,6 +547,8 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
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 { CommonStatus, getStatusText } from '@/config/constants'
import type { PackageSeriesResponse } from '@/types/api'
@@ -473,6 +606,59 @@
slot_position: [{ required: true, message: '请选择插槽位置', trigger: 'change' }]
})
// 设备操作相关对话框
const speedLimitDialogVisible = ref(false)
const speedLimitLoading = ref(false)
const speedLimitFormRef = ref<FormInstance>()
const speedLimitForm = reactive({
download_speed: 1024,
upload_speed: 512
})
const speedLimitRules = reactive<FormRules>({
download_speed: [
{ required: true, message: '请输入下行速率', trigger: 'blur' },
{ type: 'number', min: 1, message: '速率不能小于1KB/s', trigger: 'blur' }
],
upload_speed: [
{ required: true, message: '请输入上行速率', trigger: 'blur' },
{ type: 'number', min: 1, message: '速率不能小于1KB/s', trigger: 'blur' }
]
})
const currentOperatingDevice = ref<string>('')
// 设备操作右键菜单
const deviceOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentOperatingDeviceNo = ref<string>('')
const switchCardDialogVisible = ref(false)
const switchCardLoading = ref(false)
const switchCardFormRef = ref<FormInstance>()
const switchCardForm = reactive({
target_iccid: ''
})
const switchCardRules = reactive<FormRules>({
target_iccid: [{ required: true, message: '请输入目标ICCID', trigger: 'blur' }]
})
const setWiFiDialogVisible = ref(false)
const setWiFiLoading = ref(false)
const setWiFiFormRef = ref<FormInstance>()
const setWiFiForm = reactive({
enabled: 1,
ssid: '',
password: ''
})
const setWiFiRules = reactive<FormRules>({
ssid: [
{ required: true, message: '请输入WiFi名称', trigger: 'blur' },
{ min: 1, max: 32, message: 'WiFi名称长度为1-32个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入WiFi密码', trigger: 'blur' },
{ min: 8, max: 63, message: 'WiFi密码长度为8-63个字符', trigger: 'blur' }
]
})
// 搜索表单初始值
const initialSearchState = {
device_no: '',
@@ -559,7 +745,6 @@
// 列配置
const columnOptions = [
{ label: 'ID', prop: 'id' },
{ label: '设备号', prop: 'device_no' },
{ label: '设备名称', prop: 'device_name' },
{ label: '设备型号', prop: 'device_model' },
@@ -794,15 +979,11 @@
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'id',
label: 'ID',
width: 80
},
{
prop: 'device_no',
label: '设备号',
minWidth: 150,
showOverflowTooltip: true,
formatter: (row: Device) => {
return h(
'span',
@@ -827,7 +1008,7 @@
{
prop: 'device_type',
label: '设备类型',
width: 100
width: 120
},
{
prop: 'manufacturer',
@@ -868,7 +1049,7 @@
{
prop: 'batch_no',
label: '批次号',
minWidth: 160,
minWidth: 180,
formatter: (row: Device) => row.batch_no || '-'
},
{
@@ -880,17 +1061,17 @@
{
prop: 'operation',
label: '操作',
width: 180,
width: 200,
fixed: 'right',
formatter: (row: Device) => {
return h('div', { style: 'display: flex; gap: 8px;' }, [
return h('div', { style: 'display: flex; gap: 0; align-items: center;' }, [
h(ArtButtonTable, {
text: '查看卡片',
onClick: () => handleViewCards(row)
}),
h(ArtButtonTable, {
type: 'delete',
onClick: () => deleteDevice(row)
text: '更多操作',
onContextmenu: (e: MouseEvent) => showDeviceOperationMenu(e, row.device_no)
})
])
}
@@ -1220,6 +1401,249 @@
seriesBindingFormRef.value.resetFields()
}
}
// ========== 设备操作相关 ==========
// 设备操作路由
const handleDeviceOperation = (command: string, deviceNo: string) => {
switch (command) {
case 'reboot':
handleRebootDevice(deviceNo)
break
case 'reset':
handleResetDevice(deviceNo)
break
case 'speed-limit':
showSpeedLimitDialog(deviceNo)
break
case 'switch-card':
showSwitchCardDialog(deviceNo)
break
case 'set-wifi':
showSetWiFiDialog(deviceNo)
break
case 'delete':
handleDeleteDeviceByNo(deviceNo)
break
}
}
// 通过设备号删除设备
const handleDeleteDeviceByNo = async (deviceNo: string) => {
// 先根据设备号找到设备对象
const device = deviceList.value.find(d => d.device_no === deviceNo)
if (device) {
deleteDevice(device)
} else {
ElMessage.error('未找到该设备')
}
}
// 重启设备
const handleRebootDevice = (imei: string) => {
ElMessageBox.confirm(`确定要重启设备 ${imei} 吗?`, '重启确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
const res = await DeviceService.rebootDevice(imei)
if (res.code === 0) {
ElMessage.success('重启指令已发送')
} else {
ElMessage.error(res.message || '重启失败')
}
} catch (error: any) {
console.error('重启设备失败:', error)
ElMessage.error(error?.message || '重启失败')
}
})
.catch(() => {
// 用户取消
})
}
// 恢复出厂设置
const handleResetDevice = (imei: string) => {
ElMessageBox.confirm(
`确定要恢复设备 ${imei} 的出厂设置吗?此操作将清除所有配置和数据!`,
'恢复出厂设置确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}
)
.then(async () => {
try {
const res = await DeviceService.resetDevice(imei)
if (res.code === 0) {
ElMessage.success('恢复出厂设置指令已发送')
} else {
ElMessage.error(res.message || '操作失败')
}
} catch (error: any) {
console.error('恢复出厂设置失败:', error)
ElMessage.error(error?.message || '操作失败')
}
})
.catch(() => {
// 用户取消
})
}
// 显示设置限速对话框
const showSpeedLimitDialog = (imei: string) => {
currentOperatingDevice.value = imei
speedLimitForm.download_speed = 1024
speedLimitForm.upload_speed = 512
speedLimitDialogVisible.value = true
}
// 确认设置限速
const handleConfirmSpeedLimit = async () => {
if (!speedLimitFormRef.value) return
await speedLimitFormRef.value.validate(async (valid) => {
if (valid) {
speedLimitLoading.value = true
try {
const res = await DeviceService.setSpeedLimit(currentOperatingDevice.value, {
download_speed: speedLimitForm.download_speed,
upload_speed: speedLimitForm.upload_speed
})
if (res.code === 0) {
ElMessage.success('限速设置成功')
speedLimitDialogVisible.value = false
} else {
ElMessage.error(res.message || '设置失败')
}
} catch (error: any) {
console.error('设置限速失败:', error)
ElMessage.error(error?.message || '设置失败')
} finally {
speedLimitLoading.value = false
}
}
})
}
// 显示切换SIM卡对话框
const showSwitchCardDialog = (imei: string) => {
currentOperatingDevice.value = imei
switchCardForm.target_iccid = ''
switchCardDialogVisible.value = true
}
// 确认切换SIM卡
const handleConfirmSwitchCard = async () => {
if (!switchCardFormRef.value) return
await switchCardFormRef.value.validate(async (valid) => {
if (valid) {
switchCardLoading.value = true
try {
const res = await DeviceService.switchCard(currentOperatingDevice.value, {
target_iccid: switchCardForm.target_iccid
})
if (res.code === 0) {
ElMessage.success('切换SIM卡指令已发送')
switchCardDialogVisible.value = false
} else {
ElMessage.error(res.message || '切换失败')
}
} catch (error: any) {
console.error('切换SIM卡失败:', error)
ElMessage.error(error?.message || '切换失败')
} finally {
switchCardLoading.value = false
}
}
})
}
// 显示设置WiFi对话框
const showSetWiFiDialog = (imei: string) => {
currentOperatingDevice.value = imei
setWiFiForm.enabled = 1
setWiFiForm.ssid = ''
setWiFiForm.password = ''
setWiFiDialogVisible.value = true
}
// 确认设置WiFi
const handleConfirmSetWiFi = async () => {
if (!setWiFiFormRef.value) return
await setWiFiFormRef.value.validate(async (valid) => {
if (valid) {
setWiFiLoading.value = true
try {
const res = await DeviceService.setWiFi(currentOperatingDevice.value, {
enabled: setWiFiForm.enabled,
ssid: setWiFiForm.ssid,
password: setWiFiForm.password
})
if (res.code === 0) {
ElMessage.success('WiFi设置成功')
setWiFiDialogVisible.value = false
} else {
ElMessage.error(res.message || '设置失败')
}
} catch (error: any) {
console.error('设置WiFi失败:', error)
ElMessage.error(error?.message || '设置失败')
} finally {
setWiFiLoading.value = false
}
}
})
}
// 设备操作菜单项配置
const deviceOperationMenuItems = computed((): MenuItemType[] => [
{
key: 'reboot',
label: '重启设备'
},
{
key: 'reset',
label: '恢复出厂'
},
{
key: 'speed-limit',
label: '设置限速'
},
{
key: 'switch-card',
label: '切换SIM卡'
},
{
key: 'set-wifi',
label: '设置WiFi'
},
{
key: 'delete',
label: '删除设备'
}
])
// 显示设备操作菜单
const showDeviceOperationMenu = (e: MouseEvent, deviceNo: string) => {
e.preventDefault()
e.stopPropagation()
currentOperatingDeviceNo.value = deviceNo
deviceOperationMenuRef.value?.show(e)
}
// 处理设备操作菜单选择
const handleDeviceOperationMenuSelect = (item: MenuItemType) => {
const deviceNo = currentOperatingDeviceNo.value
if (!deviceNo) return
handleDeviceOperation(item.key, deviceNo)
}
</script>
<style scoped lang="scss">

View File

@@ -399,9 +399,6 @@
getCarrierTypeText(currentCardDetail.carrier_type)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="卡类型">{{
currentCardDetail.card_type || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="卡业务类型">{{
getCardCategoryText(currentCardDetail.card_category)
}}</ElDescriptionsItem>
@@ -437,9 +434,9 @@
>{{ currentCardDetail.data_usage_mb }} MB</ElDescriptionsItem
>
<ElDescriptionsItem label="首次佣金">
<ElDescriptionsItem label="一次性佣金">
<ElTag :type="currentCardDetail.first_commission_paid ? 'success' : 'info'">
{{ currentCardDetail.first_commission_paid ? '已支付' : '未支付' }}
{{ currentCardDetail.first_commission_paid ? '已产生' : '未产生' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="累计充值">{{
@@ -463,6 +460,111 @@
</template>
</ElDialog>
<!-- 流量使用查询对话框 -->
<ElDialog v-model="flowUsageDialogVisible" title="流量使用查询" width="500px">
<div v-if="flowUsageLoading" style="text-align: center; padding: 40px">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">查询中...</div>
</div>
<ElDescriptions v-else-if="flowUsageData" :column="1" border>
<ElDescriptionsItem label="已用流量">{{
flowUsageData.usedFlow || 0
}}</ElDescriptionsItem>
<ElDescriptionsItem label="流量单位">{{
flowUsageData.unit || 'MB'
}}</ElDescriptionsItem>
<ElDescriptionsItem v-if="flowUsageData.extend" label="扩展信息">{{
flowUsageData.extend
}}</ElDescriptionsItem>
</ElDescriptions>
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" @click="flowUsageDialogVisible = false">关闭</ElButton>
</div>
</template>
</ElDialog>
<!-- 实名状态查询对话框 -->
<ElDialog v-model="realnameStatusDialogVisible" title="实名认证状态" width="500px">
<div v-if="realnameStatusLoading" style="text-align: center; padding: 40px">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">查询中...</div>
</div>
<ElDescriptions v-else-if="realnameStatusData" :column="1" border>
<ElDescriptionsItem label="实名状态">{{
realnameStatusData.status || '未知'
}}</ElDescriptionsItem>
<ElDescriptionsItem v-if="realnameStatusData.extend" label="扩展信息">{{
realnameStatusData.extend
}}</ElDescriptionsItem>
</ElDescriptions>
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" @click="realnameStatusDialogVisible = false">关闭</ElButton>
</div>
</template>
</ElDialog>
<!-- 卡实时状态查询对话框 -->
<ElDialog v-model="cardStatusDialogVisible" title="卡实时状态" width="500px">
<div v-if="cardStatusLoading" style="text-align: center; padding: 40px">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">查询中...</div>
</div>
<ElDescriptions v-else-if="cardStatusData" :column="1" border>
<ElDescriptionsItem label="ICCID">{{
cardStatusData.iccid || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="卡状态">{{
cardStatusData.cardStatus || '未知'
}}</ElDescriptionsItem>
<ElDescriptionsItem v-if="cardStatusData.extend" label="扩展信息">{{
cardStatusData.extend
}}</ElDescriptionsItem>
</ElDescriptions>
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" @click="cardStatusDialogVisible = false">关闭</ElButton>
</div>
</template>
</ElDialog>
<!-- 实名认证链接对话框 -->
<ElDialog v-model="realnameLinkDialogVisible" title="实名认证链接" width="500px">
<div v-if="realnameLinkLoading" style="text-align: center; padding: 40px">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">获取中...</div>
</div>
<div v-else-if="realnameLinkData && realnameLinkData.link" style="text-align: center">
<div style="margin-bottom: 16px">
<img v-if="qrcodeDataURL" :src="qrcodeDataURL" alt="实名认证二维码" />
</div>
<ElDescriptions :column="1" border>
<ElDescriptionsItem label="实名链接">
<a :href="realnameLinkData.link" target="_blank" style="color: var(--el-color-primary)">
{{ realnameLinkData.link }}
</a>
</ElDescriptionsItem>
<ElDescriptionsItem v-if="realnameLinkData.extend" label="扩展信息">{{
realnameLinkData.extend
}}</ElDescriptionsItem>
</ElDescriptions>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" @click="realnameLinkDialogVisible = false">关闭</ElButton>
</div>
</template>
</ElDialog>
<!-- 更多操作右键菜单 -->
<ArtMenuRight
ref="moreMenuRef"
@@ -470,6 +572,14 @@
:menu-width="180"
@select="handleMoreMenuSelect"
/>
<!-- 表格行操作右键菜单 -->
<ArtMenuRight
ref="cardOperationMenuRef"
:menu-items="cardOperationMenuItems"
:menu-width="160"
@select="handleCardOperationMenuSelect"
/>
</ElCard>
</div>
</ArtTableFullScreen>
@@ -479,14 +589,16 @@
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { CardService, ShopService, PackageSeriesService } from '@/api/modules'
import { ElMessage, ElTag, ElIcon } from 'element-plus'
import { ElMessage, ElTag, ElIcon, ElMessageBox } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import QRCode from 'qrcode'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { formatDateTime } from '@/utils/business/format'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import type {
StandaloneIotCard,
StandaloneCardStatus,
@@ -543,8 +655,28 @@
const cardDetailLoading = ref(false)
const currentCardDetail = ref<any>(null)
// IoT卡操作相关对话框
const flowUsageDialogVisible = ref(false)
const flowUsageLoading = ref(false)
const flowUsageData = ref<any>(null)
const realnameStatusDialogVisible = ref(false)
const realnameStatusLoading = ref(false)
const realnameStatusData = ref<any>(null)
const cardStatusDialogVisible = ref(false)
const cardStatusLoading = ref(false)
const cardStatusData = ref<any>(null)
const realnameLinkDialogVisible = ref(false)
const realnameLinkLoading = ref(false)
const realnameLinkData = ref<any>(null)
const qrcodeDataURL = ref<string>('')
// 更多操作右键菜单
const moreMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const cardOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentOperatingIccid = ref<string>('')
// 店铺相关
const targetShopLoading = ref(false)
@@ -728,9 +860,9 @@
const columnOptions = [
{ label: 'ICCID', prop: 'iccid' },
{ label: '卡接入号', prop: 'msisdn' },
{ label: '卡类型', prop: 'card_type' },
{ label: '卡业务类型', prop: 'card_category' },
{ label: '运营商', prop: 'carrier_name' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '成本价', prop: 'cost_price' },
{ label: '分销价', prop: 'distribute_price' },
{ label: '状态', prop: 'status' },
@@ -738,7 +870,7 @@
{ label: '网络状态', prop: 'network_status' },
{ label: '实名状态', prop: 'real_name_status' },
{ label: '累计流量(MB)', prop: 'data_usage_mb' },
{ label: '首次佣金', prop: 'first_commission_paid' },
{ label: '一次性佣金', prop: 'first_commission_paid' },
{ label: '累计充值', prop: 'accumulated_recharge' },
{ label: '创建时间', prop: 'created_at' }
]
@@ -865,21 +997,23 @@
label: '卡接入号',
width: 130
},
{
prop: 'card_type',
label: '卡类型',
width: 100
},
{
prop: 'card_category',
label: '卡业务类型',
width: 100
width: 100,
formatter: (row: StandaloneIotCard) => getCardCategoryText(row.card_category)
},
{
prop: 'carrier_name',
label: '运营商',
width: 150
},
{
prop: 'shop_name',
label: '店铺名称',
minWidth: 150,
formatter: (row: StandaloneIotCard) => row.shop_name || '-'
},
{
prop: 'cost_price',
label: '成本价',
@@ -937,11 +1071,11 @@
},
{
prop: 'first_commission_paid',
label: '首次佣金',
label: '一次性佣金',
width: 100,
formatter: (row: StandaloneIotCard) => {
const type = row.first_commission_paid ? 'success' : 'info'
const text = row.first_commission_paid ? '已支付' : '未支付'
const text = row.first_commission_paid ? '已产生' : '未产生'
return h(ElTag, { type, size: 'small' }, () => text)
}
},
@@ -956,6 +1090,24 @@
label: '创建时间',
width: 180,
formatter: (row: StandaloneIotCard) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: '操作',
width: 200,
fixed: 'right',
formatter: (row: StandaloneIotCard) => {
return h('div', { style: 'display: flex; gap: 0; align-items: center;' }, [
h(ArtButtonTable, {
text: '查询流量',
onClick: () => showFlowUsageDialog(row.iccid)
}),
h(ArtButtonTable, {
text: '更多操作',
onContextmenu: (e: MouseEvent) => showCardOperationMenu(e, row.iccid)
})
])
}
}
])
@@ -1364,28 +1516,47 @@
const moreMenuItems = computed((): MenuItemType[] => [
{
key: 'distribution',
label: '网卡分销',
icon: '&#xe73b;'
label: '网卡分销'
},
{
key: 'recharge',
label: '批量充值',
icon: '&#xe63a;'
label: '批量充值'
},
{
key: 'recycle',
label: '网卡回收',
icon: '&#xe850;'
label: '网卡回收'
},
{
key: 'download',
label: '批量下载',
icon: '&#xe78b;'
label: '批量下载'
},
{
key: 'changePackage',
label: '变更套餐',
icon: '&#xe706;'
label: '变更套餐'
}
])
// 卡操作菜单项配置
const cardOperationMenuItems = computed((): MenuItemType[] => [
{
key: 'realname-status',
label: '查询实名状态'
},
{
key: 'card-status',
label: '查询卡状态'
},
{
key: 'realname-link',
label: '获取实名链接'
},
{
key: 'start-card',
label: '启用卡片'
},
{
key: 'stop-card',
label: '停用卡片'
}
])
@@ -1417,6 +1588,22 @@
}
}
// 显示卡操作菜单
const showCardOperationMenu = (e: MouseEvent, iccid: string) => {
e.preventDefault()
e.stopPropagation()
currentOperatingIccid.value = iccid
cardOperationMenuRef.value?.show(e)
}
// 处理卡操作菜单选择
const handleCardOperationMenuSelect = (item: MenuItemType) => {
const iccid = currentOperatingIccid.value
if (!iccid) return
handleCardOperation(item.key, iccid)
}
// 网卡分销 - 正在开发中
const cardDistribution = () => {
ElMessage.info('功能正在开发中')
@@ -1441,6 +1628,177 @@
const changePackage = () => {
ElMessage.info('功能正在开发中')
}
// IoT卡操作处理函数
const handleCardOperation = (command: string, iccid: string) => {
switch (command) {
case 'realname-status':
showRealnameStatusDialog(iccid)
break
case 'card-status':
showCardStatusDialog(iccid)
break
case 'realname-link':
showRealnameLinkDialog(iccid)
break
case 'start-card':
handleStartCard(iccid)
break
case 'stop-card':
handleStopCard(iccid)
break
}
}
// 查询流量使用
const showFlowUsageDialog = async (iccid: string) => {
flowUsageDialogVisible.value = true
flowUsageLoading.value = true
flowUsageData.value = null
try {
const res = await CardService.getGatewayFlow(iccid)
if (res.code === 0) {
flowUsageData.value = res.data
} else {
ElMessage.error(res.message || '查询失败')
flowUsageDialogVisible.value = false
}
} catch (error: any) {
console.error('查询流量使用失败:', error)
ElMessage.error(error?.message || '查询失败')
flowUsageDialogVisible.value = false
} finally {
flowUsageLoading.value = false
}
}
// 查询实名认证状态
const showRealnameStatusDialog = async (iccid: string) => {
realnameStatusDialogVisible.value = true
realnameStatusLoading.value = true
realnameStatusData.value = null
try {
const res = await CardService.getGatewayRealname(iccid)
if (res.code === 0) {
realnameStatusData.value = res.data
} else {
ElMessage.error(res.message || '查询失败')
realnameStatusDialogVisible.value = false
}
} catch (error: any) {
console.error('查询实名状态失败:', error)
ElMessage.error(error?.message || '查询失败')
realnameStatusDialogVisible.value = false
} finally {
realnameStatusLoading.value = false
}
}
// 查询卡实时状态
const showCardStatusDialog = async (iccid: string) => {
cardStatusDialogVisible.value = true
cardStatusLoading.value = true
cardStatusData.value = null
try {
const res = await CardService.getGatewayStatus(iccid)
if (res.code === 0) {
cardStatusData.value = res.data
} else {
ElMessage.error(res.message || '查询失败')
cardStatusDialogVisible.value = false
}
} catch (error: any) {
console.error('查询卡状态失败:', error)
ElMessage.error(error?.message || '查询失败')
cardStatusDialogVisible.value = false
} finally {
cardStatusLoading.value = false
}
}
// 获取实名认证链接
const showRealnameLinkDialog = async (iccid: string) => {
realnameLinkDialogVisible.value = true
realnameLinkLoading.value = true
realnameLinkData.value = null
qrcodeDataURL.value = ''
try {
const res = await CardService.getRealnameLink(iccid)
if (res.code === 0 && res.data?.link) {
realnameLinkData.value = res.data
// 生成二维码
qrcodeDataURL.value = await QRCode.toDataURL(res.data.link, {
width: 200,
margin: 1
})
} else {
ElMessage.error(res.message || '获取失败')
realnameLinkDialogVisible.value = false
}
} catch (error: any) {
console.error('获取实名链接失败:', error)
ElMessage.error(error?.message || '获取失败')
realnameLinkDialogVisible.value = false
} finally {
realnameLinkLoading.value = false
}
}
// 启用卡片(复机)
const handleStartCard = (iccid: string) => {
ElMessageBox.confirm('确定要启用该卡片吗?', '确认启用', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
const res = await CardService.startCard(iccid)
if (res.code === 0) {
ElMessage.success('启用成功')
getTableData()
} else {
ElMessage.error(res.message || '启用失败')
}
} catch (error: any) {
console.error('启用卡片失败:', error)
ElMessage.error(error?.message || '启用失败')
}
})
.catch(() => {
// 用户取消
})
}
// 停用卡片(停机)
const handleStopCard = (iccid: string) => {
ElMessageBox.confirm('确定要停用该卡片吗?', '确认停用', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
const res = await CardService.stopCard(iccid)
if (res.code === 0) {
ElMessage.success('停用成功')
getTableData()
} else {
ElMessage.error(res.message || '停用失败')
}
} catch (error: any) {
console.error('停用卡片失败:', error)
ElMessage.error(error?.message || '停用失败')
}
})
.catch(() => {
// 用户取消
})
}
</script>
<style lang="scss" scoped>

View File

@@ -66,10 +66,22 @@
</div>
<ElFormItem label="运营商" required style="margin-bottom: 20px">
<ElSelect v-model="selectedCarrierId" placeholder="请选择运营商" style="width: 100%">
<ElOption label="中国移动" :value="1" />
<ElOption label="中国联通" :value="2" />
<ElOption label="中国电信" :value="3" />
<ElSelect
v-model="selectedCarrierId"
placeholder="请输入运营商名称搜索"
style="width: 100%"
filterable
remote
:remote-method="handleCarrierSearch"
:loading="carrierLoading"
clearable
>
<ElOption
v-for="carrier in carrierList"
:key="carrier.id"
:label="carrier.carrier_name"
:value="carrier.id"
/>
</ElSelect>
</ElFormItem>
@@ -194,7 +206,7 @@
<script setup lang="ts">
import { h } from 'vue'
import { CardService } from '@/api/modules'
import { CardService, CarrierService } from '@/api/modules'
import { ElMessage, ElTag, ElFormItem, ElSelect, ElOption } from 'element-plus'
import { Download, UploadFilled, Upload } from '@element-plus/icons-vue'
import type { UploadInstance } from 'element-plus'
@@ -204,6 +216,7 @@
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { StorageService } from '@/api/modules/storage'
import type { IotCardImportTask, IotCardImportTaskStatus } from '@/types/api/card'
import type { Carrier } from '@/types/api'
defineOptions({ name: 'IotCardTask' })
@@ -215,6 +228,8 @@
const importDialogVisible = ref(false)
const detailDialogVisible = ref(false)
const selectedCarrierId = ref<number>()
const carrierList = ref<Carrier[]>([])
const carrierLoading = ref(false)
// 搜索表单初始值
const initialSearchState = {
@@ -235,7 +250,7 @@
})
// 搜索表单配置
const searchFormItems: SearchFormItem[] = [
const searchFormItems = computed<SearchFormItem[]>(() => [
{
label: '任务状态',
prop: 'status',
@@ -255,15 +270,17 @@
label: '运营商',
prop: 'carrier_id',
type: 'select',
options: carrierList.value.map((carrier) => ({
label: carrier.carrier_name,
value: carrier.id
})),
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '中国移动', value: 1 },
{ label: '中国联通', value: 2 },
{ label: '中国电信', value: 3 }
]
filterable: true,
remote: true,
remoteMethod: handleCarrierSearch,
placeholder: '请输入运营商名称搜索'
}
},
{
label: '批次号',
@@ -285,7 +302,7 @@
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
}
}
]
])
// 列配置
const columnOptions = [
@@ -456,6 +473,7 @@
onMounted(() => {
getTableData()
loadCarrierList()
})
// 获取IoT卡任务列表
@@ -717,6 +735,31 @@
importDialogVisible.value = false
}
// 加载运营商列表
const loadCarrierList = async (carrierName?: string) => {
carrierLoading.value = true
try {
const res = await CarrierService.getCarriers({
page: 1,
page_size: 20,
carrier_name: carrierName || undefined,
status: 1 // 只加载启用的运营商
})
if (res.code === 0) {
carrierList.value = res.data.items || []
}
} catch (error) {
console.error('获取运营商列表失败:', error)
} finally {
carrierLoading.value = false
}
}
// 运营商搜索处理
const handleCarrierSearch = (query: string) => {
loadCarrierList(query)
}
// 提交上传
const submitUpload = async () => {
if (!selectedCarrierId.value) {

View File

@@ -140,12 +140,15 @@
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="成本价()" prop="cost_price">
<ElFormItem label="成本价()" prop="cost_price">
<ElInputNumber
v-model="form.cost_price"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
style="width: 100%"
placeholder="请输入成本价"
/>
</ElFormItem>
</ElForm>
@@ -290,7 +293,7 @@
validator: (rule: any, value: any, callback: any) => {
if (value === undefined || value === null || value === '') {
callback(new Error('请输入成本价'))
} else if (form.package_base_price && value < form.package_base_price) {
} else if (form.package_base_price && value < form.package_base_price / 100) {
callback(
new Error(`成本价不能低于套餐价格 ¥${(form.package_base_price / 100).toFixed(2)}`)
)
@@ -617,7 +620,7 @@
form.id = row.id
form.package_id = row.package_id
form.shop_id = row.shop_id
form.cost_price = row.cost_price
form.cost_price = row.cost_price / 100 // 转换为元显示
form.package_base_price = 0
} else {
form.id = 0
@@ -639,9 +642,9 @@
// 从套餐选项中找到选中的套餐
const selectedPackage = packageOptions.value.find((pkg) => pkg.id === packageId)
if (selectedPackage) {
// 将套餐的价格设置为成本价
form.cost_price = selectedPackage.price
form.package_base_price = selectedPackage.price
// 将套餐的价格(分)转换为元显示
form.cost_price = selectedPackage.price / 100
form.package_base_price = selectedPackage.price // 保持原始值(分)用于验证
}
} else {
// 清空时重置成本价
@@ -695,10 +698,13 @@
if (valid) {
submitLoading.value = true
try {
// 将元转换为分提交给后端
const costPriceInCents = Math.round(form.cost_price * 100)
const data = {
package_id: form.package_id,
shop_id: form.shop_id,
cost_price: form.cost_price
cost_price: costPriceInCents
}
if (dialogType.value === 'add') {
@@ -706,7 +712,7 @@
ElMessage.success('新增成功')
} else {
await ShopPackageAllocationService.updateShopPackageAllocation(form.id, {
cost_price: form.cost_price
cost_price: costPriceInCents
})
ElMessage.success('修改成功')
}

View File

@@ -50,12 +50,18 @@
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
<ElFormItem label="套餐编码" prop="package_code">
<ElInput
v-model="form.package_code"
placeholder="请输入套餐编码"
:disabled="dialogType === 'edit'"
clearable
/>
<div style="display: flex; gap: 8px;">
<ElInput
v-model="form.package_code"
placeholder="请输入套餐编码或点击生成"
:disabled="dialogType === 'edit'"
clearable
style="flex: 1;"
/>
<ElButton v-if="dialogType === 'add'" @click="handleGeneratePackageCode">
生成编码
</ElButton>
</div>
</ElFormItem>
<ElFormItem label="套餐名称" prop="package_name">
<ElInput v-model="form.package_name" placeholder="请输入套餐名称" clearable />
@@ -165,7 +171,7 @@
<script setup lang="ts">
import { h } from 'vue'
import { PackageManageService, PackageSeriesService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus'
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElButton } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type { PackageResponse, SeriesSelectOption } from '@/types/api'
import type { SearchFormItem } from '@/types'
@@ -186,6 +192,7 @@
getDataTypeTag,
getShelfStatusText
} from '@/config/constants'
import { generatePackageCode } from '@/utils/codeGenerator'
defineOptions({ name: 'PackageList' })
@@ -651,6 +658,12 @@
})
}
// 生成套餐编码
const handleGeneratePackageCode = () => {
form.package_code = generatePackageCode()
ElMessage.success('编码生成成功')
}
// 处理弹窗关闭事件
const handleDialogClosed = () => {
// 清除表单验证状态

View File

@@ -49,12 +49,18 @@
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
<ElFormItem label="系列编码" prop="series_code">
<ElInput
v-model="form.series_code"
placeholder="请输入系列编码"
:disabled="dialogType === 'edit'"
clearable
/>
<div style="display: flex; gap: 8px;">
<ElInput
v-model="form.series_code"
placeholder="请输入系列编码或点击生成"
:disabled="dialogType === 'edit'"
clearable
style="flex: 1;"
/>
<ElButton v-if="dialogType === 'add'" @click="handleGenerateSeriesCode">
生成编码
</ElButton>
</div>
</ElFormItem>
<ElFormItem label="系列名称" prop="series_name">
<ElInput v-model="form.series_name" placeholder="请输入系列名称" clearable />
@@ -87,7 +93,7 @@
<script setup lang="ts">
import { h } from 'vue'
import { PackageSeriesService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus'
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElButton } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type { PackageSeriesResponse } from '@/types/api'
import type { SearchFormItem } from '@/types'
@@ -100,6 +106,7 @@
frontendStatusToApi,
apiStatusToFrontend
} from '@/config/constants'
import { generateSeriesCode } from '@/utils/codeGenerator'
defineOptions({ name: 'PackageSeries' })
@@ -338,6 +345,12 @@
})
}
// 生成系列编码
const handleGenerateSeriesCode = () => {
form.series_code = generateSeriesCode()
ElMessage.success('编码生成成功')
}
// 删除套餐系列
const deleteSeries = (row: PackageSeriesResponse) => {
ElMessageBox.confirm(`确定删除套餐系列 ${row.series_name} 吗?`, '删除确认', {

View File

@@ -183,6 +183,115 @@
<!-- 客户账号列表弹窗 -->
<CustomerAccountDialog v-model="customerAccountDialogVisible" :shop-id="currentShopId" />
<!-- 店铺操作右键菜单 -->
<ArtMenuRight
ref="shopOperationMenuRef"
:menu-items="shopOperationMenuItems"
:menu-width="140"
@select="handleShopOperationMenuSelect"
/>
<!-- 店铺默认角色管理对话框 -->
<ElDialog
v-model="defaultRolesDialogVisible"
:title="`设置默认角色 - ${currentShop?.shop_name || ''}`"
width="800px"
>
<div v-loading="defaultRolesLoading">
<!-- 当前默认角色列表 -->
<div class="default-roles-section">
<div class="section-header">
<span>当前默认角色</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">
{{ 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>
</template>
</ElTableColumn>
<template #empty>
<div style="padding: 20px 0; color: #909399;">
暂无默认角色请点击"添加角色"按钮进行配置
</div>
</template>
</ElTable>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton @click="defaultRolesDialogVisible = false">关闭</ElButton>
</div>
</template>
</ElDialog>
<!-- 添加角色对话框 -->
<ElDialog
v-model="addRoleDialogVisible"
title="添加默认角色"
width="600px"
append-to-body
>
<div v-loading="rolesLoading">
<ElSelect
v-model="selectedRoleIds"
multiple
filterable
placeholder="请选择要添加的角色"
style="width: 100%"
>
<ElOption
v-for="role in availableRoles"
: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;">
<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>
</div>
</ElOption>
</ElSelect>
<div style="margin-top: 8px; color: #909399; font-size: 12px;">
支持多选已添加的角色将显示为禁用状态
</div>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton @click="addRoleDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleAddDefaultRoles" :loading="addRoleLoading">
确定
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
@@ -202,10 +311,13 @@
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
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 CustomerAccountDialog from '@/components/business/CustomerAccountDialog.vue'
import { ShopService } from '@/api/modules'
import { ShopService, RoleService } from '@/api/modules'
import type { SearchFormItem } from '@/types'
import type { ShopResponse } from '@/types/api'
import type { ShopResponse, ShopRoleResponse } from '@/types/api'
import { RoleType } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
@@ -221,6 +333,21 @@
const parentShopList = ref<ShopResponse[]>([])
const searchParentShopList = ref<ShopResponse[]>([])
// 默认角色管理相关状态
const defaultRolesDialogVisible = ref(false)
const addRoleDialogVisible = ref(false)
const defaultRolesLoading = ref(false)
const rolesLoading = ref(false)
const addRoleLoading = ref(false)
const currentShop = ref<ShopResponse | null>(null)
const currentDefaultRoles = ref<ShopRoleResponse[]>([])
const availableRoles = ref<ShopRoleResponse[]>([])
const selectedRoleIds = ref<number[]>([])
// 右键菜单
const shopOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentOperatingShop = ref<ShopResponse | null>(null)
// 定义表单搜索初始值
const initialSearchState = {
shop_name: '',
@@ -476,7 +603,7 @@
{
prop: 'operation',
label: '操作',
width: 220,
width: 200,
fixed: 'right',
formatter: (row: ShopResponse) => {
return h('div', { style: 'display: flex; gap: 8px;' }, [
@@ -485,12 +612,8 @@
onClick: () => viewCustomerAccounts(row)
}),
h(ArtButtonTable, {
type: 'edit',
onClick: () => showDialog('edit', row)
}),
h(ArtButtonTable, {
type: 'delete',
onClick: () => deleteShop(row)
text: '更多操作',
onContextmenu: (e: MouseEvent) => showShopOperationMenu(e, row)
})
])
}
@@ -750,6 +873,47 @@
customerAccountDialogVisible.value = true
}
// 店铺操作菜单项配置
const shopOperationMenuItems = computed((): MenuItemType[] => [
{
key: 'defaultRoles',
label: '默认角色'
},
{
key: 'edit',
label: '编辑'
},
{
key: 'delete',
label: '删除'
}
])
// 显示店铺操作右键菜单
const showShopOperationMenu = (e: MouseEvent, row: ShopResponse) => {
e.preventDefault()
e.stopPropagation()
currentOperatingShop.value = row
shopOperationMenuRef.value?.show(e)
}
// 处理店铺操作菜单选择
const handleShopOperationMenuSelect = (item: MenuItemType) => {
if (!currentOperatingShop.value) return
switch (item.key) {
case 'defaultRoles':
showDefaultRolesDialog(currentOperatingShop.value)
break
case 'edit':
showDialog('edit', currentOperatingShop.value)
break
case 'delete':
deleteShop(currentOperatingShop.value)
break
}
}
// 状态切换
const handleStatusChange = async (row: ShopResponse, newStatus: number) => {
const oldStatus = row.status
@@ -767,10 +931,144 @@
console.error(error)
}
}
// ========== 默认角色管理功能 ==========
// 显示默认角色管理对话框
const showDefaultRolesDialog = async (row: ShopResponse) => {
currentShop.value = row
defaultRolesDialogVisible.value = true
await loadShopDefaultRoles(row.id)
}
// 加载店铺默认角色列表
const loadShopDefaultRoles = async (shopId: number) => {
defaultRolesLoading.value = true
try {
const res = await ShopService.getShopRoles(shopId)
if (res.code === 0) {
currentDefaultRoles.value = res.data.roles || []
}
} catch (error) {
console.error('获取店铺默认角色失败:', error)
ElMessage.error('获取店铺默认角色失败')
} finally {
defaultRolesLoading.value = false
}
}
// 显示添加角色对话框
const showAddRoleDialog = async () => {
addRoleDialogVisible.value = true
selectedRoleIds.value = []
await loadAvailableRoles()
}
// 加载可用角色列表
const loadAvailableRoles = async () => {
rolesLoading.value = true
try {
const res = await RoleService.getRoles({
page: 1,
page_size: 9999,
status: 1 // RoleStatus.ENABLED
})
if (res.code === 0) {
// 将 PlatformRole 转换为与 ShopRoleResponse 兼容的格式,同时保留 role_type
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 // 这个值在可用角色列表中不使用
}))
}
} catch (error) {
console.error('获取角色列表失败:', error)
ElMessage.error('获取角色列表失败')
} finally {
rolesLoading.value = false
}
}
// 判断角色是否已分配
const isRoleAlreadyAssigned = (roleId: number) => {
return currentDefaultRoles.value.some((r) => r.role_id === roleId)
}
// 添加默认角色
const handleAddDefaultRoles = async () => {
if (selectedRoleIds.value.length === 0) {
ElMessage.warning('请至少选择一个角色')
return
}
if (!currentShop.value) {
ElMessage.error('店铺信息异常')
return
}
addRoleLoading.value = true
try {
const res = await ShopService.assignShopRoles(currentShop.value.id, {
role_ids: selectedRoleIds.value
})
if (res.code === 0) {
ElMessage.success('添加默认角色成功')
addRoleDialogVisible.value = false
// 刷新默认角色列表
await loadShopDefaultRoles(currentShop.value.id)
}
} catch (error) {
console.error('添加默认角色失败:', error)
} finally {
addRoleLoading.value = false
}
}
// 删除默认角色
const handleDeleteDefaultRole = (role: ShopRoleResponse) => {
ElMessageBox.confirm(`确定要删除默认角色 "${role.role_name}" 吗?`, '删除默认角色', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
if (!currentShop.value) {
ElMessage.error('店铺信息异常')
return
}
try {
await ShopService.deleteShopRole(currentShop.value.id, role.role_id)
ElMessage.success('删除成功')
// 刷新默认角色列表
await loadShopDefaultRoles(currentShop.value.id)
} catch (error) {
console.error('删除默认角色失败:', error)
ElMessage.error('删除默认角色失败')
}
})
.catch(() => {
// 用户取消删除
})
}
</script>
<style lang="scss" scoped>
.shop-page {
// 店铺管理页面样式
}
.default-roles-section {
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
font-weight: 500;
color: #303133;
}
}
</style>