fetch(modify):修复BUG
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 3m27s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 3m27s
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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: ''
|
||||
label: '网卡分销'
|
||||
},
|
||||
{
|
||||
key: 'recharge',
|
||||
label: '批量充值',
|
||||
icon: ''
|
||||
label: '批量充值'
|
||||
},
|
||||
{
|
||||
key: 'recycle',
|
||||
label: '网卡回收',
|
||||
icon: ''
|
||||
label: '网卡回收'
|
||||
},
|
||||
{
|
||||
key: 'download',
|
||||
label: '批量下载',
|
||||
icon: ''
|
||||
label: '批量下载'
|
||||
},
|
||||
{
|
||||
key: 'changePackage',
|
||||
label: '变更套餐',
|
||||
icon: ''
|
||||
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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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('修改成功')
|
||||
}
|
||||
|
||||
@@ -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 = () => {
|
||||
// 清除表单验证状态
|
||||
|
||||
@@ -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} 吗?`, '删除确认', {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user