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

This commit is contained in:
sexygoat
2026-01-31 16:33:21 +08:00
parent 16d53709ef
commit ecb79dae43
20 changed files with 1369 additions and 649 deletions

2
.env
View File

@@ -12,7 +12,7 @@ VITE_BASE_URL =
VITE_API_URL = https://cmp-api.boss160.cn VITE_API_URL = https://cmp-api.boss160.cn
# 权限模式( frontend backend # 权限模式( frontend backend
VITE_ACCESS_MODE = frontend VITE_ACCESS_MODE = backend
# 是否打开路由信息 # 是否打开路由信息
VITE_OPEN_ROUTE_INFO = false VITE_OPEN_ROUTE_INFO = false

View File

@@ -8,7 +8,7 @@ import type { BaseResponse } from '@/types/api'
/** /**
* 文件用途枚举 * 文件用途枚举
*/ */
export type FilePurpose = 'iot_import' | 'export' | 'attachment' export type FilePurpose = 'iot_import' | 'device_import' | 'export' | 'attachment'
/** /**
* 获取上传 URL 请求参数 * 获取上传 URL 请求参数
@@ -56,18 +56,18 @@ export class StorageService extends BaseService {
* - 预签名 URL 有效期 15 分钟,请及时使用 * - 预签名 URL 有效期 15 分钟,请及时使用
* - 上传时 Content-Type 需与请求时一致 * - 上传时 Content-Type 需与请求时一致
* - file_key 在上传成功后永久有效 * - file_key 在上传成功后永久有效
* - 开发环境通过代理上传,生产环境直接上传
* *
* @param uploadUrl 预签名 URL * @param uploadUrl 预签名 URL(由对象存储生成)
* @param file 文件 * @param file 文件
* @param contentType 文件类型(需与 getUploadUrl 请求时保持一致) * @param contentType 文件类型(需与 getUploadUrl 请求时保持一致)
*/ */
static async uploadFile(uploadUrl: string, file: File, contentType?: string): Promise<void> { static async uploadFile(uploadUrl: string, file: File, contentType?: string): Promise<void> {
try { try {
// 开发环境下,使用代理路径避免 CORS 问题 // 开发环境使用代理解决 CORS 问题
let finalUrl = uploadUrl let finalUrl = uploadUrl
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
// 将对象存储域名替换为代理路径 // 将对象存储域名替换为代理路径
// 例如http://obs-helf.cucloud.cn/cmp/... -> /obs-proxy/cmp/...
finalUrl = uploadUrl.replace(/^https?:\/\/obs-helf\.cucloud\.cn/, '/obs-proxy') finalUrl = uploadUrl.replace(/^https?:\/\/obs-helf\.cucloud\.cn/, '/obs-proxy')
} }
@@ -81,8 +81,7 @@ export class StorageService extends BaseService {
const response = await fetch(finalUrl, { const response = await fetch(finalUrl, {
method: 'PUT', method: 'PUT',
body: file, body: file,
headers, headers
mode: 'cors' // 明确指定 CORS 模式
}) })
if (!response.ok) { if (!response.ok) {

View File

@@ -1,46 +1,57 @@
import { useRoute } from 'vue-router'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { useCommon } from '@/composables/useCommon'
import type { AppRouteRecord } from '@/types/router'
type AuthItem = NonNullable<AppRouteRecord['meta']['authList']>[number]
const userStore = useUserStore()
/** /**
* 按钮权限(前后端模式通用) * 权限判断组合式函数
* 用于在 setup 函数或表格 formatter 中判断权限
*
* 用法: * 用法:
* const { hasAuth } = useAuth() * const { hasAuth, hasAllAuth } = useAuth()
* hasAuth('add') // 检查是否拥有新增权限 * hasAuth('role:add') // 检查是否拥有权限
* hasAuth(['role:add', 'role:edit']) // 检查是否拥有任意一个权限
* hasAllAuth(['role:add', 'role:edit']) // 检查是否拥有全部权限
*/ */
export const useAuth = () => { export const useAuth = () => {
const route = useRoute() const userStore = useUserStore()
const { isFrontendMode } = useCommon()
const { info } = storeToRefs(userStore)
// 前端按钮权限(例如:['add', 'edit']
const frontendAuthList = info.value?.buttons ?? []
// 后端路由 meta 配置的权限列表(例如:[{ auth_mark: 'add' }]
const backendAuthList: AuthItem[] = Array.isArray(route.meta.authList)
? (route.meta.authList as AuthItem[])
: []
/** /**
* 检查是否拥有某权限标识 * 检查是否有指定权限(满足任意一个即可)
* @param auth 权限标识 * @param permission 权限标识,可以是字符串或字符串数组
* @returns 是否有权限 * @returns 是否有权限
*/ */
const hasAuth = (auth: string): boolean => { const hasAuth = (permission: string | string[]): boolean => {
if (isFrontendMode.value) { if (!permission) return false
return frontendAuthList.includes(auth)
if (typeof permission === 'string') {
// 单个权限(同时检查 permissions 和 buttons
return userStore.hasPermission(permission) || userStore.hasButton(permission)
} }
return backendAuthList.some((item) => item?.auth_mark === auth) if (Array.isArray(permission)) {
// 多个权限(满足任意一个即可)
return permission.some(
(perm) => userStore.hasPermission(perm) || userStore.hasButton(perm)
)
}
return false
}
/**
* 检查是否有所有指定权限(需要全部满足)
* @param permissions 权限标识数组
* @returns 是否有全部权限
*/
const hasAllAuth = (permissions: string[]): boolean => {
if (!permissions || !Array.isArray(permissions)) return false
// 需要全部满足
return permissions.every(
(perm) => userStore.hasPermission(perm) || userStore.hasButton(perm)
)
} }
return { return {
hasAuth hasAuth,
hasAllAuth
} }
} }

View File

@@ -208,12 +208,26 @@ export function useLogin() {
return return
} }
// 保存权限、菜单和按钮
if (response.data.permissions) {
userStore.setPermissions(response.data.permissions)
}
if (response.data.menus) {
userStore.setMenus(response.data.menus)
}
if (response.data.buttons) {
userStore.setButtons(response.data.buttons)
}
// 保存记住密码 // 保存记住密码
saveCredentials(formData.username, formData.password, formData.rememberPassword) saveCredentials(formData.username, formData.password, formData.rememberPassword)
// 显示登录成功提示 // 显示登录成功提示
showLoginSuccessNotice() showLoginSuccessNotice()
// 等待数据持久化完成
await new Promise((resolve) => setTimeout(resolve, 100))
// 跳转到重定向页面或首页 // 跳转到重定向页面或首页
const redirectPath = getRedirectPath(route) const redirectPath = getRedirectPath(route)
await router.push(redirectPath || HOME_PAGE) await router.push(redirectPath || HOME_PAGE)

Binary file not shown.

View File

@@ -22,16 +22,20 @@ const permissionDirective: Directive = {
let hasPermission = false let hasPermission = false
if (typeof value === 'string') { if (typeof value === 'string') {
// 单个权限 // 单个权限(同时检查 permissions 和 buttons
hasPermission = userStore.hasPermission(value) hasPermission = userStore.hasPermission(value) || userStore.hasButton(value)
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
// 多个权限 // 多个权限
if (arg === 'all') { if (arg === 'all') {
// 需要全部满足 // 需要全部满足
hasPermission = userStore.hasAllPermissions(value) hasPermission = value.every(
(perm) => userStore.hasPermission(perm) || userStore.hasButton(perm)
)
} else { } else {
// 满足任意一个即可 // 满足任意一个即可
hasPermission = userStore.hasAnyPermission(value) hasPermission = value.some(
(perm) => userStore.hasPermission(perm) || userStore.hasButton(perm)
)
} }
} }

View File

@@ -20,7 +20,7 @@ import { isInWhiteList, hasRoutePermission, isTokenValid, buildLoginRedirect } f
const isRouteRegistered = ref(false) const isRouteRegistered = ref(false)
// 临时开发模式:跳过所有权限验证(开发静态页面时使用) // 临时开发模式:跳过所有权限验证(开发静态页面时使用)
const DEV_MODE_SKIP_AUTH = true const DEV_MODE_SKIP_AUTH = false
/** /**
* 路由全局前置守卫 * 路由全局前置守卫
@@ -232,20 +232,125 @@ async function processFrontendMenu(router: Router): Promise<void> {
} }
/** /**
* 处理后端控制模式的菜单逻辑 * 处理后端控制模式的菜单
*/ */
async function processBackendMenu(router: Router): Promise<void> { async function processBackendMenu(router: Router): Promise<void> {
const closeLoading = loadingService.showLoading() const closeLoading = loadingService.showLoading()
try { try {
const { menuList } = await menuService.getMenuList() const userStore = useUserStore()
await registerAndStoreMenu(router, menuList, closeLoading) const backendMenus = userStore.menus || []
const routeMap = buildRouteMap(asyncRoutes)
const menuList = backendMenus
.map((menu) => convertBackendMenuToRoute(menu, routeMap))
.filter((route) => route !== null)
const finalMenuList = mergeDefaultMenus(menuList, routeMap)
await registerAndStoreMenu(router, finalMenuList, closeLoading)
} catch (error) { } catch (error) {
closeLoading() closeLoading()
throw error throw error
} }
} }
/**
* 合并默认菜单
* 将配置的默认菜单合并到后端返回的菜单中
*/
function mergeDefaultMenus(
backendMenus: AppRouteRecord[],
routeMap: Map<string, AppRouteRecord>
): AppRouteRecord[] {
const defaultMenuPaths = ['/dashboard']
const defaultMenus: AppRouteRecord[] = defaultMenuPaths
.map((path) => {
const route = routeMap.get(path)
return route ? menuDataToRouter(route) : null
})
.filter((menu): menu is AppRouteRecord => menu !== null)
const backendPaths = new Set(backendMenus.map((m) => m.path))
const filteredDefaultMenus = defaultMenus.filter((m) => !backendPaths.has(m.path))
return [...filteredDefaultMenus, ...backendMenus]
}
/**
* 构建 URL 到路由的映射表
*/
function buildRouteMap(routes: AppRouteRecord[], parentPath = ''): Map<string, AppRouteRecord> {
const map = new Map<string, AppRouteRecord>()
routes.forEach((route) => {
// 构建完整路径
const fullPath = route.path.startsWith('/')
? route.path
: parentPath
? `${parentPath}/${route.path}`.replace(/\/+/g, '/')
: `/${route.path}`
// 存储路由映射
map.set(fullPath, route)
// 递归处理子路由
if (route.children && route.children.length > 0) {
const childMap = buildRouteMap(route.children, fullPath)
childMap.forEach((childRoute, childPath) => {
map.set(childPath, childRoute)
})
}
})
return map
}
/**
* 将后端菜单数据转换为路由格式
*/
function convertBackendMenuToRoute(
menu: any,
routeMap: Map<string, AppRouteRecord>,
parentPath = ''
): AppRouteRecord | null {
const menuUrl = menu.url || '/'
const matchedRoute = routeMap.get(menuUrl)
if (!matchedRoute) {
console.warn(`未找到与菜单 URL "${menuUrl}" 匹配的路由定义: ${menu.name}`)
return null
}
const route: AppRouteRecord = {
path: menuUrl,
name: matchedRoute.name,
component: matchedRoute.component,
meta: {
...matchedRoute.meta,
title: menu.name,
permission: menu.perm_code,
sort: menu.sort || matchedRoute.meta?.sort || 0,
icon: menu.icon || matchedRoute.meta?.icon,
// 清除前端定义的 roles 和 permissions使用后端权限控制
roles: undefined,
permissions: undefined
}
}
if (menu.children && menu.children.length > 0) {
const children = menu.children
.map((child: any) => convertBackendMenuToRoute(child, routeMap, menuUrl))
.filter((child: AppRouteRecord | null) => child !== null)
if (children.length > 0) {
route.children = children
}
}
return route
}
/** /**
* 注册路由并存储菜单数据 * 注册路由并存储菜单数据
*/ */

View File

@@ -4,6 +4,7 @@
import type { RouteLocationNormalized } from 'vue-router' import type { RouteLocationNormalized } from 'vue-router'
import type { UserInfo } from '@/types/api' import type { UserInfo } from '@/types/api'
import { useUserStore } from '@/store/modules/user'
/** /**
* 不需要登录的路由白名单 * 不需要登录的路由白名单
@@ -33,6 +34,7 @@ export const isInWhiteList = (path: string): boolean => {
/** /**
* 检查用户是否有权限访问路由 * 检查用户是否有权限访问路由
* 根据文档要求:菜单访问基于 URL按钮访问基于权限标识
*/ */
export const hasRoutePermission = ( export const hasRoutePermission = (
route: RouteLocationNormalized, route: RouteLocationNormalized,
@@ -40,7 +42,30 @@ export const hasRoutePermission = (
): boolean => { ): boolean => {
const { roles = [], permissions = [] } = userInfo const { roles = [], permissions = [] } = userInfo
// 如果路由没有设置权限要求,直接通过 // 如果是超级管理员,直接通过
if (userInfo.user_type === 1) {
return true
}
// 默认始终可访问的路由(不需要菜单权限)
const defaultAllowedPaths = ['/dashboard']
const isDefaultRoute = defaultAllowedPaths.some(
(allowedPath) => route.path === allowedPath || route.path.startsWith(allowedPath + '/')
)
// 检查菜单访问权限(基于 URL
const userStore = useUserStore()
const userMenus = userStore.menus || []
// 如果是默认路由,跳过菜单权限检查
if (!isDefaultRoute && userMenus.length > 0) {
const hasMenuAccess = checkMenuAccess(route.path, userMenus)
if (!hasMenuAccess) {
return false
}
}
// 如果路由没有设置额外的权限要求,直接通过
if (!route.meta?.roles && !route.meta?.permissions) { if (!route.meta?.roles && !route.meta?.permissions) {
return true return true
} }
@@ -58,11 +83,9 @@ export const hasRoutePermission = (
if (route.meta.permissions) { if (route.meta.permissions) {
const routePermissions = route.meta.permissions as string[] const routePermissions = route.meta.permissions as string[]
const hasPermission = routePermissions.some((permission) => { const hasPermission = routePermissions.some((permission) => {
// 支持通配符权限 *:*:*
if (permissions.includes('*:*:*')) { if (permissions.includes('*:*:*')) {
return true return true
} }
// 精确匹配或前缀匹配
return permissions.some((userPermission) => { return permissions.some((userPermission) => {
if (userPermission.endsWith('*')) { if (userPermission.endsWith('*')) {
const prefix = userPermission.slice(0, -1) const prefix = userPermission.slice(0, -1)
@@ -79,6 +102,25 @@ export const hasRoutePermission = (
return true return true
} }
/**
* 递归检查菜单访问权限(基于 URL
*/
function checkMenuAccess(path: string, menus: any[]): boolean {
for (const menu of menus) {
// 检查当前菜单的 URL 是否匹配
if (menu.url && (path === menu.url || path.startsWith(menu.url + '/'))) {
return true
}
// 递归检查子菜单
if (menu.children && menu.children.length > 0) {
if (checkMenuAccess(path, menu.children)) {
return true
}
}
}
return false
}
/** /**
* 检查 Token 是否有效 * 检查 Token 是否有效
* 简单检查,真实项目中应该验证 JWT 或者调用后端接口 * 简单检查,真实项目中应该验证 JWT 或者调用后端接口

View File

@@ -306,32 +306,32 @@ export const asyncRoutes: AppRouteRecord[] = [
keepAlive: true, keepAlive: true,
isHideTab: true isHideTab: true
} }
},
{
path: 'menu',
name: 'Menus',
component: RoutesAlias.Menu,
meta: {
title: 'menus.system.menu',
keepAlive: true,
roles: ['R_SUPER'],
authList: [
{
title: '新增',
auth_mark: 'add'
},
{
title: '编辑',
auth_mark: 'edit'
},
{
title: '删除',
auth_mark: 'delete'
}
]
}
} }
// { // {
// path: 'menu',
// name: 'Menus',
// component: RoutesAlias.Menu,
// meta: {
// title: 'menus.system.menu',
// keepAlive: true,
// roles: ['R_SUPER'],
// authList: [
// {
// title: '新增',
// auth_mark: 'add'
// },
// {
// title: '编辑',
// auth_mark: 'edit'
// },
// {
// title: '删除',
// auth_mark: 'delete'
// }
// ]
// }
// }
// {
// path: 'nested', // path: 'nested',
// name: 'Nested', // name: 'Nested',
// component: '', // component: '',
@@ -741,6 +741,26 @@ export const asyncRoutes: AppRouteRecord[] = [
} }
] ]
}, },
{
path: '/shop-management',
name: 'ShopManagement',
component: RoutesAlias.Home,
meta: {
title: 'menus.product.shop',
icon: '&#xe81a;'
},
children: [
{
path: 'list',
name: 'Shop',
component: RoutesAlias.Shop,
meta: {
title: 'menus.product.shop',
keepAlive: true
}
}
]
},
{ {
path: '/account-management', path: '/account-management',
name: 'AccountManagement', name: 'AccountManagement',
@@ -816,71 +836,71 @@ export const asyncRoutes: AppRouteRecord[] = [
} }
] ]
}, },
{ // {
path: '/product', // path: '/product',
name: 'Product', // name: 'Product',
component: RoutesAlias.Home, // component: RoutesAlias.Home,
meta: { // meta: {
title: 'menus.product.title', // title: 'menus.product.title',
icon: '&#xe81a;' // icon: '&#xe81a;'
}, // },
children: [ // children: [
// { // // {
// path: 'sim-card', // // path: 'sim-card',
// name: 'SimCardManagement', // // name: 'SimCardManagement',
// component: RoutesAlias.SimCardManagement, // // component: RoutesAlias.SimCardManagement,
// meta: { // // meta: {
// title: 'menus.product.simCard', // // title: 'menus.product.simCard',
// keepAlive: true // // keepAlive: true
// } // // }
// }, // // },
// { // // {
// path: 'sim-card-assign', // // path: 'sim-card-assign',
// name: 'SimCardAssign', // // name: 'SimCardAssign',
// component: RoutesAlias.SimCardAssign, // // component: RoutesAlias.SimCardAssign,
// meta: { // // meta: {
// title: 'menus.product.simCardAssign', // // title: 'menus.product.simCardAssign',
// keepAlive: true // // keepAlive: true
// } // // }
// }, // // },
// { // // {
// path: 'package-series', // // path: 'package-series',
// name: 'PackageSeries', // // name: 'PackageSeries',
// component: RoutesAlias.PackageSeries, // // component: RoutesAlias.PackageSeries,
// meta: { // // meta: {
// title: 'menus.product.packageSeries', // // title: 'menus.product.packageSeries',
// keepAlive: true // // keepAlive: true
// } // // }
// }, // // },
// { // // {
// path: 'package-list', // // path: 'package-list',
// name: 'PackageList', // // name: 'PackageList',
// component: RoutesAlias.PackageList, // // component: RoutesAlias.PackageList,
// meta: { // // meta: {
// title: 'menus.product.packageList', // // title: 'menus.product.packageList',
// keepAlive: true // // keepAlive: true
// } // // }
// }, // // },
// { // // {
// path: 'package-assign', // // path: 'package-assign',
// name: 'PackageAssign', // // name: 'PackageAssign',
// component: RoutesAlias.PackageAssign, // // component: RoutesAlias.PackageAssign,
// meta: { // // meta: {
// title: 'menus.product.packageAssign', // // title: 'menus.product.packageAssign',
// keepAlive: true // // keepAlive: true
// } // // }
// }, // // },
{ // {
path: 'shop', // path: 'shop',
name: 'Shop', // name: 'Shop',
component: RoutesAlias.Shop, // component: RoutesAlias.Shop,
meta: { // meta: {
title: 'menus.product.shop', // title: 'menus.product.shop',
keepAlive: true // keepAlive: true
} // }
} // }
] // ]
}, // },
{ {
path: '/asset-management', path: '/asset-management',
name: 'AssetManagement', name: 'AssetManagement',

View File

@@ -24,11 +24,15 @@ export const useUserStore = defineStore(
const accessToken = ref('') const accessToken = ref('')
const refreshToken = ref('') const refreshToken = ref('')
const permissions = ref<string[]>([]) const permissions = ref<string[]>([])
const menus = ref<any[]>([])
const buttons = ref<string[]>([])
const getUserInfo = computed(() => info.value) const getUserInfo = computed(() => info.value)
const getSettingState = computed(() => useSettingStore().$state) const getSettingState = computed(() => useSettingStore().$state)
const getWorktabState = computed(() => useWorktabStore().$state) const getWorktabState = computed(() => useWorktabStore().$state)
const getPermissions = computed(() => permissions.value) const getPermissions = computed(() => permissions.value)
const getMenus = computed(() => menus.value)
const getButtons = computed(() => buttons.value)
const setUserInfo = (newInfo: UserInfo) => { const setUserInfo = (newInfo: UserInfo) => {
info.value = newInfo info.value = newInfo
@@ -38,6 +42,14 @@ export const useUserStore = defineStore(
permissions.value = perms permissions.value = perms
} }
const setMenus = (menuList: any[]) => {
menus.value = menuList
}
const setButtons = (buttonList: string[]) => {
buttons.value = buttonList
}
// 检查是否是超级管理员 // 检查是否是超级管理员
const isSuperAdmin = computed(() => info.value.user_type === 1) const isSuperAdmin = computed(() => info.value.user_type === 1)
@@ -62,6 +74,13 @@ export const useUserStore = defineStore(
return perms.every((perm) => permissions.value.includes(perm)) return perms.every((perm) => permissions.value.includes(perm))
} }
// 检查是否有某个按钮权限
const hasButton = (buttonCode: string): boolean => {
// 超级管理员拥有所有权限
if (isSuperAdmin.value) return true
return buttons.value.includes(buttonCode)
}
const setLoginStatus = (status: boolean) => { const setLoginStatus = (status: boolean) => {
isLogin.value = status isLogin.value = status
} }
@@ -105,6 +124,8 @@ export const useUserStore = defineStore(
accessToken.value = '' accessToken.value = ''
refreshToken.value = '' refreshToken.value = ''
permissions.value = [] permissions.value = []
menus.value = []
buttons.value = []
useWorktabStore().opened = [] useWorktabStore().opened = []
sessionStorage.removeItem('iframeRoutes') sessionStorage.removeItem('iframeRoutes')
resetRouterState(router) resetRouterState(router)
@@ -122,16 +143,23 @@ export const useUserStore = defineStore(
accessToken, accessToken,
refreshToken, refreshToken,
permissions, permissions,
menus,
buttons,
getUserInfo, getUserInfo,
getSettingState, getSettingState,
getWorktabState, getWorktabState,
getPermissions, getPermissions,
getMenus,
getButtons,
isSuperAdmin, isSuperAdmin,
setUserInfo, setUserInfo,
setPermissions, setPermissions,
setMenus,
setButtons,
hasPermission, hasPermission,
hasAnyPermission, hasAnyPermission,
hasAllPermissions, hasAllPermissions,
hasButton,
setLoginStatus, setLoginStatus,
setLanguage, setLanguage,
setSearchHistory, setSearchHistory,
@@ -145,8 +173,18 @@ export const useUserStore = defineStore(
persist: { persist: {
key: 'user', key: 'user',
storage: localStorage, storage: localStorage,
// 只持久化 token 和登录状态,用户信息每次刷新都从接口获取 paths: [
paths: ['accessToken', 'refreshToken', 'isLogin', 'language', 'isLock', 'lockPassword'] 'accessToken',
'refreshToken',
'isLogin',
'language',
'isLock',
'lockPassword',
'info',
'permissions',
'menus',
'buttons'
]
} }
} }
) )

View File

@@ -6,7 +6,7 @@
// biome-ignore lint: disable // biome-ignore lint: disable
export {} export {}
declare global { declare global {
const EffectScope: (typeof import('vue'))['EffectScope'] const EffectScope: typeof import('vue')['EffectScope']
const ElButton: (typeof import('element-plus/es'))['ElButton'] const ElButton: (typeof import('element-plus/es'))['ElButton']
const ElMessage: (typeof import('element-plus/es'))['ElMessage'] const ElMessage: (typeof import('element-plus/es'))['ElMessage']
const ElMessageBox: (typeof import('element-plus/es'))['ElMessageBox'] const ElMessageBox: (typeof import('element-plus/es'))['ElMessageBox']
@@ -14,319 +14,304 @@ declare global {
const ElPopconfirm: (typeof import('element-plus/es'))['ElPopconfirm'] const ElPopconfirm: (typeof import('element-plus/es'))['ElPopconfirm']
const ElPopover: (typeof import('element-plus/es'))['ElPopover'] const ElPopover: (typeof import('element-plus/es'))['ElPopover']
const ElTableColumn: (typeof import('element-plus/es'))['ElTableColumn'] const ElTableColumn: (typeof import('element-plus/es'))['ElTableColumn']
const ElTag: (typeof import('element-plus/es'))['ElTag'] const ElTag: typeof import('element-plus/es')['ElTag']
const acceptHMRUpdate: (typeof import('pinia'))['acceptHMRUpdate'] const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const asyncComputed: (typeof import('@vueuse/core'))['asyncComputed'] const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: (typeof import('@vueuse/core'))['autoResetRef'] const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const computed: (typeof import('vue'))['computed'] const computed: typeof import('vue')['computed']
const computedAsync: (typeof import('@vueuse/core'))['computedAsync'] const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: (typeof import('@vueuse/core'))['computedEager'] const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: (typeof import('@vueuse/core'))['computedInject'] const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: (typeof import('@vueuse/core'))['computedWithControl'] const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const controlledComputed: (typeof import('@vueuse/core'))['controlledComputed'] const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: (typeof import('@vueuse/core'))['controlledRef'] const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: (typeof import('vue'))['createApp'] const createApp: typeof import('vue')['createApp']
const createEventHook: (typeof import('@vueuse/core'))['createEventHook'] const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: (typeof import('@vueuse/core'))['createGlobalState'] const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: (typeof import('@vueuse/core'))['createInjectionState'] const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createPinia: (typeof import('pinia'))['createPinia'] const createPinia: typeof import('pinia')['createPinia']
const createReactiveFn: (typeof import('@vueuse/core'))['createReactiveFn'] const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createReusableTemplate: (typeof import('@vueuse/core'))['createReusableTemplate'] const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: (typeof import('@vueuse/core'))['createSharedComposable'] const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: (typeof import('@vueuse/core'))['createTemplatePromise'] const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createUnrefFn: (typeof import('@vueuse/core'))['createUnrefFn'] const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: (typeof import('vue'))['customRef'] const customRef: typeof import('vue')['customRef']
const debouncedRef: (typeof import('@vueuse/core'))['debouncedRef'] const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: (typeof import('@vueuse/core'))['debouncedWatch'] const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: (typeof import('vue'))['defineAsyncComponent'] const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: (typeof import('vue'))['defineComponent'] const defineComponent: typeof import('vue')['defineComponent']
const defineStore: (typeof import('pinia'))['defineStore'] const defineStore: typeof import('pinia')['defineStore']
const eagerComputed: (typeof import('@vueuse/core'))['eagerComputed'] const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: (typeof import('vue'))['effectScope'] const effectScope: typeof import('vue')['effectScope']
const extendRef: (typeof import('@vueuse/core'))['extendRef'] const extendRef: typeof import('@vueuse/core')['extendRef']
const getActivePinia: (typeof import('pinia'))['getActivePinia'] const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: (typeof import('vue'))['getCurrentInstance'] const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: (typeof import('vue'))['getCurrentScope'] const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: (typeof import('vue'))['h'] const h: typeof import('vue')['h']
const ignorableWatch: (typeof import('@vueuse/core'))['ignorableWatch'] const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: (typeof import('vue'))['inject'] const inject: typeof import('vue')['inject']
const injectLocal: (typeof import('@vueuse/core'))['injectLocal'] const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDefined: (typeof import('@vueuse/core'))['isDefined'] const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: (typeof import('vue'))['isProxy'] const isProxy: typeof import('vue')['isProxy']
const isReactive: (typeof import('vue'))['isReactive'] const isReactive: typeof import('vue')['isReactive']
const isReadonly: (typeof import('vue'))['isReadonly'] const isReadonly: typeof import('vue')['isReadonly']
const isRef: (typeof import('vue'))['isRef'] const isRef: typeof import('vue')['isRef']
const makeDestructurable: (typeof import('@vueuse/core'))['makeDestructurable'] const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: (typeof import('pinia'))['mapActions'] const mapActions: typeof import('pinia')['mapActions']
const mapGetters: (typeof import('pinia'))['mapGetters'] const mapGetters: typeof import('pinia')['mapGetters']
const mapState: (typeof import('pinia'))['mapState'] const mapState: typeof import('pinia')['mapState']
const mapStores: (typeof import('pinia'))['mapStores'] const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: (typeof import('pinia'))['mapWritableState'] const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: (typeof import('vue'))['markRaw'] const markRaw: typeof import('vue')['markRaw']
const nextTick: (typeof import('vue'))['nextTick'] const nextTick: typeof import('vue')['nextTick']
const onActivated: (typeof import('vue'))['onActivated'] const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: (typeof import('vue'))['onBeforeMount'] const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: (typeof import('vue-router'))['onBeforeRouteLeave'] const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: (typeof import('vue-router'))['onBeforeRouteUpdate'] const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: (typeof import('vue'))['onBeforeUnmount'] const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: (typeof import('vue'))['onBeforeUpdate'] const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: (typeof import('@vueuse/core'))['onClickOutside'] const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: (typeof import('vue'))['onDeactivated'] const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: (typeof import('vue'))['onErrorCaptured'] const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: (typeof import('@vueuse/core'))['onKeyStroke'] const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: (typeof import('@vueuse/core'))['onLongPress'] const onLongPress: typeof import('@vueuse/core')['onLongPress']
const onMounted: (typeof import('vue'))['onMounted'] const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: (typeof import('vue'))['onRenderTracked'] const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: (typeof import('vue'))['onRenderTriggered'] const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: (typeof import('vue'))['onScopeDispose'] const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: (typeof import('vue'))['onServerPrefetch'] const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onStartTyping: (typeof import('@vueuse/core'))['onStartTyping'] const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: (typeof import('vue'))['onUnmounted'] const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: (typeof import('vue'))['onUpdated'] const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: (typeof import('vue'))['onWatcherCleanup'] const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const pausableWatch: (typeof import('@vueuse/core'))['pausableWatch'] const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: (typeof import('vue'))['provide'] const provide: typeof import('vue')['provide']
const provideLocal: (typeof import('@vueuse/core'))['provideLocal'] const provideLocal: typeof import('@vueuse/core')['provideLocal']
const reactify: (typeof import('@vueuse/core'))['reactify'] const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: (typeof import('@vueuse/core'))['reactifyObject'] const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: (typeof import('vue'))['reactive'] const reactive: typeof import('vue')['reactive']
const reactiveComputed: (typeof import('@vueuse/core'))['reactiveComputed'] const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
const reactiveOmit: (typeof import('@vueuse/core'))['reactiveOmit'] const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactivePick: (typeof import('@vueuse/core'))['reactivePick'] const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: (typeof import('vue'))['readonly'] const readonly: typeof import('vue')['readonly']
const ref: (typeof import('vue'))['ref'] const ref: typeof import('vue')['ref']
const refAutoReset: (typeof import('@vueuse/core'))['refAutoReset'] const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refDebounced: (typeof import('@vueuse/core'))['refDebounced'] const refDebounced: typeof import('@vueuse/core')['refDebounced']
const refDefault: (typeof import('@vueuse/core'))['refDefault'] const refDefault: typeof import('@vueuse/core')['refDefault']
const refThrottled: (typeof import('@vueuse/core'))['refThrottled'] const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: (typeof import('@vueuse/core'))['refWithControl'] const refWithControl: typeof import('@vueuse/core')['refWithControl']
const resolveComponent: (typeof import('vue'))['resolveComponent'] const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: (typeof import('@vueuse/core'))['resolveRef'] const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: (typeof import('@vueuse/core'))['resolveUnref'] const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const setActivePinia: (typeof import('pinia'))['setActivePinia'] const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: (typeof import('pinia'))['setMapStoreSuffix'] const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: (typeof import('vue'))['shallowReactive'] const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: (typeof import('vue'))['shallowReadonly'] const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: (typeof import('vue'))['shallowRef'] const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: (typeof import('pinia'))['storeToRefs'] const storeToRefs: typeof import('pinia')['storeToRefs']
const syncRef: (typeof import('@vueuse/core'))['syncRef'] const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: (typeof import('@vueuse/core'))['syncRefs'] const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: (typeof import('@vueuse/core'))['templateRef'] const templateRef: typeof import('@vueuse/core')['templateRef']
const throttledRef: (typeof import('@vueuse/core'))['throttledRef'] const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: (typeof import('@vueuse/core'))['throttledWatch'] const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toRaw: (typeof import('vue'))['toRaw'] const toRaw: typeof import('vue')['toRaw']
const toReactive: (typeof import('@vueuse/core'))['toReactive'] const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: (typeof import('vue'))['toRef'] const toRef: typeof import('vue')['toRef']
const toRefs: (typeof import('vue'))['toRefs'] const toRefs: typeof import('vue')['toRefs']
const toValue: (typeof import('vue'))['toValue'] const toValue: typeof import('vue')['toValue']
const triggerRef: (typeof import('vue'))['triggerRef'] const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: (typeof import('@vueuse/core'))['tryOnBeforeMount'] const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: (typeof import('@vueuse/core'))['tryOnBeforeUnmount'] const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const tryOnMounted: (typeof import('@vueuse/core'))['tryOnMounted'] const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: (typeof import('@vueuse/core'))['tryOnScopeDispose'] const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: (typeof import('@vueuse/core'))['tryOnUnmounted'] const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unref: (typeof import('vue'))['unref'] const unref: typeof import('vue')['unref']
const unrefElement: (typeof import('@vueuse/core'))['unrefElement'] const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: (typeof import('@vueuse/core'))['until'] const until: typeof import('@vueuse/core')['until']
const useActiveElement: (typeof import('@vueuse/core'))['useActiveElement'] const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: (typeof import('@vueuse/core'))['useAnimate'] const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useArrayDifference: (typeof import('@vueuse/core'))['useArrayDifference'] const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: (typeof import('@vueuse/core'))['useArrayEvery'] const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: (typeof import('@vueuse/core'))['useArrayFilter'] const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: (typeof import('@vueuse/core'))['useArrayFind'] const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: (typeof import('@vueuse/core'))['useArrayFindIndex'] const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayFindLast: (typeof import('@vueuse/core'))['useArrayFindLast'] const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
const useArrayIncludes: (typeof import('@vueuse/core'))['useArrayIncludes'] const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
const useArrayJoin: (typeof import('@vueuse/core'))['useArrayJoin'] const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: (typeof import('@vueuse/core'))['useArrayMap'] const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: (typeof import('@vueuse/core'))['useArrayReduce'] const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArraySome: (typeof import('@vueuse/core'))['useArraySome'] const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useArrayUnique: (typeof import('@vueuse/core'))['useArrayUnique'] const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
const useAsyncQueue: (typeof import('@vueuse/core'))['useAsyncQueue'] const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: (typeof import('@vueuse/core'))['useAsyncState'] const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: (typeof import('vue'))['useAttrs'] const useAttrs: typeof import('vue')['useAttrs']
const useBase64: (typeof import('@vueuse/core'))['useBase64'] const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: (typeof import('@vueuse/core'))['useBattery'] const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: (typeof import('@vueuse/core'))['useBluetooth'] const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
const useBreakpoints: (typeof import('@vueuse/core'))['useBreakpoints'] const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBroadcastChannel: (typeof import('@vueuse/core'))['useBroadcastChannel'] const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: (typeof import('@vueuse/core'))['useBrowserLocation'] const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: (typeof import('@vueuse/core'))['useCached'] const useCached: typeof import('@vueuse/core')['useCached']
const useClipboard: (typeof import('@vueuse/core'))['useClipboard'] const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useClipboardItems: (typeof import('@vueuse/core'))['useClipboardItems'] const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
const useCloned: (typeof import('@vueuse/core'))['useCloned'] const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: (typeof import('@vueuse/core'))['useColorMode'] const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: (typeof import('@vueuse/core'))['useConfirmDialog'] const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCounter: (typeof import('@vueuse/core'))['useCounter'] const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: (typeof import('vue'))['useCssModule'] const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: (typeof import('@vueuse/core'))['useCssVar'] const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCssVars: (typeof import('vue'))['useCssVars'] const useCssVars: typeof import('vue')['useCssVars']
const useCurrentElement: (typeof import('@vueuse/core'))['useCurrentElement'] const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
const useCycleList: (typeof import('@vueuse/core'))['useCycleList'] const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useDark: (typeof import('@vueuse/core'))['useDark'] const useDark: typeof import('@vueuse/core')['useDark']
const useDateFormat: (typeof import('@vueuse/core'))['useDateFormat'] const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
const useDebounce: (typeof import('@vueuse/core'))['useDebounce'] const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: (typeof import('@vueuse/core'))['useDebounceFn'] const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: (typeof import('@vueuse/core'))['useDebouncedRefHistory'] const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDeviceMotion: (typeof import('@vueuse/core'))['useDeviceMotion'] const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: (typeof import('@vueuse/core'))['useDeviceOrientation'] const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: (typeof import('@vueuse/core'))['useDevicePixelRatio'] const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: (typeof import('@vueuse/core'))['useDevicesList'] const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDisplayMedia: (typeof import('@vueuse/core'))['useDisplayMedia'] const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: (typeof import('@vueuse/core'))['useDocumentVisibility'] const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: (typeof import('@vueuse/core'))['useDraggable'] const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDropZone: (typeof import('@vueuse/core'))['useDropZone'] const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useElementBounding: (typeof import('@vueuse/core'))['useElementBounding'] const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: (typeof import('@vueuse/core'))['useElementByPoint'] const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: (typeof import('@vueuse/core'))['useElementHover'] const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: (typeof import('@vueuse/core'))['useElementSize'] const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: (typeof import('@vueuse/core'))['useElementVisibility'] const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: (typeof import('@vueuse/core'))['useEventBus'] const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: (typeof import('@vueuse/core'))['useEventListener'] const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: (typeof import('@vueuse/core'))['useEventSource'] const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useEyeDropper: (typeof import('@vueuse/core'))['useEyeDropper'] const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: (typeof import('@vueuse/core'))['useFavicon'] const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: (typeof import('@vueuse/core'))['useFetch'] const useFetch: typeof import('@vueuse/core')['useFetch']
const useFileDialog: (typeof import('@vueuse/core'))['useFileDialog'] const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useFileSystemAccess: (typeof import('@vueuse/core'))['useFileSystemAccess'] const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFocus: (typeof import('@vueuse/core'))['useFocus'] const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: (typeof import('@vueuse/core'))['useFocusWithin'] const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: (typeof import('@vueuse/core'))['useFps'] const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: (typeof import('@vueuse/core'))['useFullscreen'] const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: (typeof import('@vueuse/core'))['useGamepad'] const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: (typeof import('@vueuse/core'))['useGeolocation'] const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useId: (typeof import('vue'))['useId'] const useId: typeof import('vue')['useId']
const useIdle: (typeof import('@vueuse/core'))['useIdle'] const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: (typeof import('@vueuse/core'))['useImage'] const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: (typeof import('@vueuse/core'))['useInfiniteScroll'] const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useIntersectionObserver: (typeof import('@vueuse/core'))['useIntersectionObserver'] const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: (typeof import('@vueuse/core'))['useInterval'] const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: (typeof import('@vueuse/core'))['useIntervalFn'] const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: (typeof import('@vueuse/core'))['useKeyModifier'] const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: (typeof import('@vueuse/core'))['useLastChanged'] const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLink: (typeof import('vue-router'))['useLink'] const useLink: typeof import('vue-router')['useLink']
const useLocalStorage: (typeof import('@vueuse/core'))['useLocalStorage'] const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useMagicKeys: (typeof import('@vueuse/core'))['useMagicKeys'] const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: (typeof import('@vueuse/core'))['useManualRefHistory'] const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: (typeof import('@vueuse/core'))['useMediaControls'] const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: (typeof import('@vueuse/core'))['useMediaQuery'] const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: (typeof import('@vueuse/core'))['useMemoize'] const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: (typeof import('@vueuse/core'))['useMemory'] const useMemory: typeof import('@vueuse/core')['useMemory']
const useModel: (typeof import('vue'))['useModel'] const useModel: typeof import('vue')['useModel']
const useMounted: (typeof import('@vueuse/core'))['useMounted'] const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: (typeof import('@vueuse/core'))['useMouse'] const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: (typeof import('@vueuse/core'))['useMouseInElement'] const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: (typeof import('@vueuse/core'))['useMousePressed'] const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: (typeof import('@vueuse/core'))['useMutationObserver'] const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNavigatorLanguage: (typeof import('@vueuse/core'))['useNavigatorLanguage'] const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: (typeof import('@vueuse/core'))['useNetwork'] const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNow: (typeof import('@vueuse/core'))['useNow'] const useNow: typeof import('@vueuse/core')['useNow']
const useObjectUrl: (typeof import('@vueuse/core'))['useObjectUrl'] const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
const useOffsetPagination: (typeof import('@vueuse/core'))['useOffsetPagination'] const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useOnline: (typeof import('@vueuse/core'))['useOnline'] const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: (typeof import('@vueuse/core'))['usePageLeave'] const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: (typeof import('@vueuse/core'))['useParallax'] const useParallax: typeof import('@vueuse/core')['useParallax']
const useParentElement: (typeof import('@vueuse/core'))['useParentElement'] const useParentElement: typeof import('@vueuse/core')['useParentElement']
const usePerformanceObserver: (typeof import('@vueuse/core'))['usePerformanceObserver'] const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
const usePermission: (typeof import('@vueuse/core'))['usePermission'] const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: (typeof import('@vueuse/core'))['usePointer'] const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: (typeof import('@vueuse/core'))['usePointerLock'] const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: (typeof import('@vueuse/core'))['usePointerSwipe'] const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: (typeof import('@vueuse/core'))['usePreferredColorScheme'] const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredContrast: (typeof import('@vueuse/core'))['usePreferredContrast'] const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePreferredDark: (typeof import('@vueuse/core'))['usePreferredDark'] const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: (typeof import('@vueuse/core'))['usePreferredLanguages'] const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: (typeof import('@vueuse/core'))['usePreferredReducedMotion'] const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePrevious: (typeof import('@vueuse/core'))['usePrevious'] const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useRafFn: (typeof import('@vueuse/core'))['useRafFn'] const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: (typeof import('@vueuse/core'))['useRefHistory'] const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: (typeof import('@vueuse/core'))['useResizeObserver'] const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRoute: (typeof import('vue-router'))['useRoute'] const useRoute: typeof import('vue-router')['useRoute']
const useRouter: (typeof import('vue-router'))['useRouter'] const useRouter: typeof import('vue-router')['useRouter']
const useScreenOrientation: (typeof import('@vueuse/core'))['useScreenOrientation'] const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: (typeof import('@vueuse/core'))['useScreenSafeArea'] const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: (typeof import('@vueuse/core'))['useScriptTag'] const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: (typeof import('@vueuse/core'))['useScroll'] const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: (typeof import('@vueuse/core'))['useScrollLock'] const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSessionStorage: (typeof import('@vueuse/core'))['useSessionStorage'] const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: (typeof import('@vueuse/core'))['useShare'] const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: (typeof import('vue'))['useSlots'] const useSlots: typeof import('vue')['useSlots']
const useSorted: (typeof import('@vueuse/core'))['useSorted'] const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: (typeof import('@vueuse/core'))['useSpeechRecognition'] const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: (typeof import('@vueuse/core'))['useSpeechSynthesis'] const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStepper: (typeof import('@vueuse/core'))['useStepper'] const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: (typeof import('@vueuse/core'))['useStorage'] const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: (typeof import('@vueuse/core'))['useStorageAsync'] const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: (typeof import('@vueuse/core'))['useStyleTag'] const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSupported: (typeof import('@vueuse/core'))['useSupported'] const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: (typeof import('@vueuse/core'))['useSwipe'] const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRef: (typeof import('vue'))['useTemplateRef'] const useTemplateRef: typeof import('vue')['useTemplateRef']
const useTemplateRefsList: (typeof import('@vueuse/core'))['useTemplateRefsList'] const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: (typeof import('@vueuse/core'))['useTextDirection'] const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: (typeof import('@vueuse/core'))['useTextSelection'] const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: (typeof import('@vueuse/core'))['useTextareaAutosize'] const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThrottle: (typeof import('@vueuse/core'))['useThrottle'] const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottleFn: (typeof import('@vueuse/core'))['useThrottleFn'] const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: (typeof import('@vueuse/core'))['useThrottledRefHistory'] const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: (typeof import('@vueuse/core'))['useTimeAgo'] const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeout: (typeof import('@vueuse/core'))['useTimeout'] const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: (typeof import('@vueuse/core'))['useTimeoutFn'] const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: (typeof import('@vueuse/core'))['useTimeoutPoll'] const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: (typeof import('@vueuse/core'))['useTimestamp'] const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: (typeof import('@vueuse/core'))['useTitle'] const useTitle: typeof import('@vueuse/core')['useTitle']
const useToNumber: (typeof import('@vueuse/core'))['useToNumber'] const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: (typeof import('@vueuse/core'))['useToString'] const useToString: typeof import('@vueuse/core')['useToString']
const useToggle: (typeof import('@vueuse/core'))['useToggle'] const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: (typeof import('@vueuse/core'))['useTransition'] const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: (typeof import('@vueuse/core'))['useUrlSearchParams'] const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: (typeof import('@vueuse/core'))['useUserMedia'] const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useVModel: (typeof import('@vueuse/core'))['useVModel'] const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: (typeof import('@vueuse/core'))['useVModels'] const useVModels: typeof import('@vueuse/core')['useVModels']
const useVibrate: (typeof import('@vueuse/core'))['useVibrate'] const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: (typeof import('@vueuse/core'))['useVirtualList'] const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useWakeLock: (typeof import('@vueuse/core'))['useWakeLock'] const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebNotification: (typeof import('@vueuse/core'))['useWebNotification'] const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: (typeof import('@vueuse/core'))['useWebSocket'] const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: (typeof import('@vueuse/core'))['useWebWorker'] const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: (typeof import('@vueuse/core'))['useWebWorkerFn'] const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: (typeof import('@vueuse/core'))['useWindowFocus'] const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: (typeof import('@vueuse/core'))['useWindowScroll'] const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: (typeof import('@vueuse/core'))['useWindowSize'] const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const watch: (typeof import('vue'))['watch'] const watch: typeof import('vue')['watch']
const watchArray: (typeof import('@vueuse/core'))['watchArray'] const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: (typeof import('@vueuse/core'))['watchAtMost'] const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: (typeof import('@vueuse/core'))['watchDebounced'] const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchDeep: (typeof import('@vueuse/core'))['watchDeep'] const watchDeep: typeof import('@vueuse/core')['watchDeep']
const watchEffect: (typeof import('vue'))['watchEffect'] const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: (typeof import('@vueuse/core'))['watchIgnorable'] const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchImmediate: (typeof import('@vueuse/core'))['watchImmediate'] const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
const watchOnce: (typeof import('@vueuse/core'))['watchOnce'] const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: (typeof import('@vueuse/core'))['watchPausable'] const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: (typeof import('vue'))['watchPostEffect'] const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: (typeof import('vue'))['watchSyncEffect'] const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const watchThrottled: (typeof import('@vueuse/core'))['watchThrottled'] const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
const watchTriggerable: (typeof import('@vueuse/core'))['watchTriggerable'] const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: (typeof import('@vueuse/core'))['watchWithFilter'] const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: (typeof import('@vueuse/core'))['whenever'] const whenever: typeof import('@vueuse/core')['whenever']
} }
// for type re-export // for type re-export
declare global { declare global {
// @ts-ignore // @ts-ignore
export type { export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
Component,
ComponentPublicInstance,
ComputedRef,
DirectiveBinding,
ExtractDefaultPropTypes,
ExtractPropTypes,
ExtractPublicPropTypes,
InjectionKey,
PropType,
Ref,
MaybeRef,
MaybeRefOrGetter,
VNode,
WritableComputedRef
} from 'vue'
import('vue') import('vue')
} }

View File

@@ -30,7 +30,6 @@
<ElButton type="info" @click="handleBatchSetSeries" :disabled="!selectedDevices.length"> <ElButton type="info" @click="handleBatchSetSeries" :disabled="!selectedDevices.length">
批量设置套餐系列 批量设置套餐系列
</ElButton> </ElButton>
<ElButton @click="handleImportDevice">导入设备</ElButton>
</template> </template>
</ArtTableHeader> </ArtTableHeader>
@@ -300,22 +299,13 @@
{{ currentDeviceDetail.status_name }} {{ currentDeviceDetail.status_name }}
</ElTag> </ElTag>
</ElDescriptionsItem> </ElDescriptionsItem>
<ElDescriptionsItem label="店铺名称">{{ <ElDescriptionsItem label="批次号" :span="2">{{
currentDeviceDetail.shop_name || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="批次号">{{
currentDeviceDetail.batch_no || '--' currentDeviceDetail.batch_no || '--'
}}</ElDescriptionsItem> }}</ElDescriptionsItem>
<ElDescriptionsItem label="激活时间">{{ <ElDescriptionsItem label="创建时间" :span="3">{{
currentDeviceDetail.activated_at || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">{{
currentDeviceDetail.created_at || '--' currentDeviceDetail.created_at || '--'
}}</ElDescriptionsItem> }}</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间">{{
currentDeviceDetail.updated_at || '--'
}}</ElDescriptionsItem>
</ElDescriptions> </ElDescriptions>
</ElDialog> </ElDialog>
</ElCard> </ElCard>
@@ -385,12 +375,9 @@
device_no: '', device_no: '',
device_name: '', device_name: '',
status: undefined as DeviceStatus | undefined, status: undefined as DeviceStatus | undefined,
shop_id: undefined as number | undefined,
batch_no: '', batch_no: '',
device_type: '', device_type: '',
manufacturer: '', manufacturer: ''
created_at_start: '',
created_at_end: ''
} }
// 搜索表单 // 搜索表单
@@ -478,9 +465,7 @@
{ label: '最大插槽数', prop: 'max_sim_slots' }, { label: '最大插槽数', prop: 'max_sim_slots' },
{ label: '已绑定卡数', prop: 'bound_card_count' }, { label: '已绑定卡数', prop: 'bound_card_count' },
{ label: '状态', prop: 'status' }, { label: '状态', prop: 'status' },
{ label: '店铺', prop: 'shop_name' },
{ label: '批次号', prop: 'batch_no' }, { label: '批次号', prop: 'batch_no' },
{ label: '激活时间', prop: 'activated_at' },
{ label: '创建时间', prop: 'created_at' }, { label: '创建时间', prop: 'created_at' },
{ label: '操作', prop: 'operation' } { label: '操作', prop: 'operation' }
] ]
@@ -610,24 +595,12 @@
return h(ElTag, { type: status.type }, () => status.text) return h(ElTag, { type: status.type }, () => status.text)
} }
}, },
{
prop: 'shop_name',
label: '店铺',
minWidth: 120,
formatter: (row: Device) => row.shop_name || '-'
},
{ {
prop: 'batch_no', prop: 'batch_no',
label: '批次号', label: '批次号',
minWidth: 120, minWidth: 160,
formatter: (row: Device) => row.batch_no || '-' formatter: (row: Device) => row.batch_no || '-'
}, },
{
prop: 'activated_at',
label: '激活时间',
width: 180,
formatter: (row: Device) => (row.activated_at ? formatDateTime(row.activated_at) : '-')
},
{ {
prop: 'created_at', prop: 'created_at',
label: '创建时间', label: '创建时间',
@@ -637,14 +610,10 @@
{ {
prop: 'operation', prop: 'operation',
label: '操作', label: '操作',
width: 150, width: 100,
fixed: 'right', fixed: 'right',
formatter: (row: Device) => { formatter: (row: Device) => {
return h('div', { style: 'display: flex; gap: 8px;' }, [ return h('div', { style: 'display: flex; gap: 8px;' }, [
h(ArtButtonTable, {
type: 'view',
onClick: () => viewDeviceDetail(row)
}),
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'delete', type: 'delete',
onClick: () => deleteDevice(row) onClick: () => deleteDevice(row)
@@ -681,16 +650,13 @@
device_no: searchForm.device_no || undefined, device_no: searchForm.device_no || undefined,
device_name: searchForm.device_name || undefined, device_name: searchForm.device_name || undefined,
status: searchForm.status, status: searchForm.status,
shop_id: searchForm.shop_id,
batch_no: searchForm.batch_no || undefined, batch_no: searchForm.batch_no || undefined,
device_type: searchForm.device_type || undefined, device_type: searchForm.device_type || undefined,
manufacturer: searchForm.manufacturer || undefined, manufacturer: searchForm.manufacturer || undefined
created_at_start: searchForm.created_at_start || undefined,
created_at_end: searchForm.created_at_end || undefined
} }
const res = await DeviceService.getDevices(params) const res = await DeviceService.getDevices(params)
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
deviceList.value = res.data.list || [] deviceList.value = res.data.items || []
pagination.total = res.data.total || 0 pagination.total = res.data.total || 0
} }
} catch (error) { } catch (error) {
@@ -735,14 +701,6 @@
selectedDevices.value = selection selectedDevices.value = selection
} }
// 查看设备详情
const viewDeviceDetail = (row: Device) => {
router.push({
path: '/asset-management/device-detail',
query: { id: row.id }
})
}
// 删除设备 // 删除设备
const deleteDevice = (row: Device) => { const deleteDevice = (row: Device) => {
ElMessageBox.confirm(`确定删除设备 ${row.device_no} 吗?删除后将自动解绑所有卡。`, '删除确认', { ElMessageBox.confirm(`确定删除设备 ${row.device_no} 吗?删除后将自动解绑所有卡。`, '删除确认', {
@@ -869,11 +827,6 @@
recallForm.remark = '' recallForm.remark = ''
} }
// 导入设备
const handleImportDevice = () => {
router.push('/batch/device-import')
}
// 批量设置套餐系列 // 批量设置套餐系列
const handleBatchSetSeries = async () => { const handleBatchSetSeries = async () => {
if (selectedDevices.value.length === 0) { if (selectedDevices.value.length === 0) {

View File

@@ -50,17 +50,11 @@
<template #title> <template #title>
<div style="line-height: 1.8"> <div style="line-height: 1.8">
<p><strong>导入说明</strong></p> <p><strong>导入说明</strong></p>
<p>1. 请先下载 CSV 模板文件按照模板格式填写设备信息</p> <p>1. 请先下载 Excel 模板文件按照模板格式填写设备信息</p>
<p>2. 支持 CSV 格式.csv单次最多导入 1000 </p> <p>2. 支持 Excel 格式.xlsx单次最多导入 1000 </p>
<p>3. CSV 文件编码UTF-8推荐 GBK</p> <p>3. 列格式请设置为文本格式避免长数字被转为科学计数法</p>
<p <p>4. 必填列device_no设备号device_name设备名称device_model设备型号device_type设备类型</p>
>4. <p>5. 可选列manufacturer制造商max_sim_slots最大插槽数默认4iccid_1 ~ iccid_4绑定的卡ICCID</p>
必填字段device_no设备号device_name设备名称device_model设备型号</p
>
<p
>5.
可选字段device_type设备类型manufacturer制造商max_sim_slots最大插槽数默认1</p
>
</div> </div>
</template> </template>
</ElAlert> </ElAlert>
@@ -77,12 +71,12 @@
:auto-upload="false" :auto-upload="false"
:on-change="handleFileChange" :on-change="handleFileChange"
:limit="1" :limit="1"
accept=".csv" accept=".xlsx"
> >
<el-icon class="el-icon--upload"><upload-filled /></el-icon> <el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text"> CSV 文件拖到此处<em>点击选择</em></div> <div class="el-upload__text"> Excel 文件拖到此处<em>点击选择</em></div>
<template #tip> <template #tip>
<div class="el-upload__tip">只能上传 CSV 文件且不超过 10MB</div> <div class="el-upload__tip">只能上传 .xlsx 格式的 Excel 文件且不超过 10MB</div>
</template> </template>
</ElUpload> </ElUpload>
@@ -131,18 +125,51 @@
}}</ElDescriptionsItem> }}</ElDescriptionsItem>
</ElDescriptions> </ElDescriptions>
<ElDivider content-position="left">跳过明细</ElDivider>
<div
v-if="currentDetail.skipped_items && currentDetail.skipped_items.length"
style="max-height: 300px; overflow-y: auto; margin-bottom: 20px"
>
<ElTable :data="currentDetail.skipped_items" border size="small">
<ElTableColumn label="行号" prop="line" width="80" />
<ElTableColumn label="设备编号" prop="device_no" width="180" />
<ElTableColumn label="跳过原因" prop="reason" min-width="300">
<template #default="{ row }">
{{ row.reason || '未知原因' }}
</template>
</ElTableColumn>
</ElTable>
</div>
<ElEmpty v-else description="无跳过记录" />
<ElDivider content-position="left">警告明细</ElDivider>
<div
v-if="currentDetail.warning_items && currentDetail.warning_items.length"
style="max-height: 300px; overflow-y: auto; margin-bottom: 20px"
>
<ElTable :data="currentDetail.warning_items" border size="small">
<ElTableColumn label="行号" prop="line" width="80" />
<ElTableColumn label="设备编号" prop="device_no" width="180" />
<ElTableColumn label="警告信息" prop="reason" min-width="300">
<template #default="{ row }">
{{ row.reason || row.message || '未知警告' }}
</template>
</ElTableColumn>
</ElTable>
</div>
<ElEmpty v-else description="无警告记录" />
<ElDivider content-position="left">失败明细</ElDivider> <ElDivider content-position="left">失败明细</ElDivider>
<div <div
v-if="currentDetail.failed_items && currentDetail.failed_items.length" v-if="currentDetail.failed_items && currentDetail.failed_items.length"
style="max-height: 300px; overflow-y: auto" style="max-height: 300px; overflow-y: auto"
> >
<ElTable :data="currentDetail.failed_items" border size="small"> <ElTable :data="currentDetail.failed_items" border size="small">
<ElTableColumn label="行号" type="index" width="80" :index="(index) => index + 1" /> <ElTableColumn label="行号" prop="line" width="80" />
<ElTableColumn label="设备编号" prop="device_no" width="150" /> <ElTableColumn label="设备编号" prop="device_no" width="180" />
<ElTableColumn label="ICCID" prop="iccid" width="200" /> <ElTableColumn label="失败原因" prop="reason" min-width="300">
<ElTableColumn label="失败原因" prop="reason" min-width="200">
<template #default="{ row }"> <template #default="{ row }">
{{ row.reason || row.error || '未知错误' }} {{ row.reason || '未知错误' }}
</template> </template>
</ElTableColumn> </ElTableColumn>
</ElTable> </ElTable>
@@ -151,6 +178,22 @@
<template #footer> <template #footer>
<ElButton @click="detailDialogVisible = false">关闭</ElButton> <ElButton @click="detailDialogVisible = false">关闭</ElButton>
<ElButton
v-if="currentDetail.skip_count > 0"
type="warning"
:icon="Download"
@click="downloadSkippedData"
>
下载跳过数据
</ElButton>
<ElButton
v-if="currentDetail.warning_count > 0"
type="warning"
:icon="Download"
@click="downloadWarningData"
>
下载警告数据
</ElButton>
<ElButton <ElButton
v-if="currentDetail.fail_count > 0" v-if="currentDetail.fail_count > 0"
type="primary" type="primary"
@@ -475,29 +518,78 @@
} }
// 下载模板 // 下载模板
const downloadTemplate = () => { const downloadTemplate = async () => {
// 使用 \t 前缀防止 Excel 将长数字转换为科学计数法 try {
const csvContent = [ // 动态导入 xlsx 库
'device_no,device_name,device_model,device_type,manufacturer,max_sim_slots', const XLSX = await import('xlsx')
'\t862639070731999,智能水表01,WM-2000,智能水表,华为,1',
'\t862639070750932,GPS定位器01,GPS-3000,定位设备,小米,2',
'\t862639070801875,智能燃气表01,GM-1500,智能燃气表,海尔,1'
].join('\n')
const BOM = '\uFEFF' // 创建示例数据
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' }) const templateData = [
{
device_no: '862639070731999',
device_name: '智能水表01',
device_model: 'WM-2000',
device_type: '智能水表',
manufacturer: '华为',
max_sim_slots: 4,
iccid_1: '89860123456789012345',
iccid_2: '',
iccid_3: '',
iccid_4: ''
},
{
device_no: '862639070750932',
device_name: 'GPS定位器01',
device_model: 'GPS-3000',
device_type: '定位设备',
manufacturer: '小米',
max_sim_slots: 2,
iccid_1: '89860123456789012346',
iccid_2: '89860123456789012347',
iccid_3: '',
iccid_4: ''
}
]
const link = document.createElement('a') // 创建工作簿
const url = URL.createObjectURL(blob) const wb = XLSX.utils.book_new()
link.setAttribute('href', url) const ws = XLSX.utils.json_to_sheet(templateData)
link.setAttribute('download', '设备导入模板.csv')
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
ElMessage.success('设备导入模板下载成功') // 设置列宽
ws['!cols'] = [
{ wch: 20 }, // device_no
{ wch: 20 }, // device_name
{ wch: 15 }, // device_model
{ wch: 15 }, // device_type
{ wch: 15 }, // manufacturer
{ wch: 15 }, // max_sim_slots
{ wch: 22 }, // iccid_1
{ wch: 22 }, // iccid_2
{ wch: 22 }, // iccid_3
{ wch: 22 } // iccid_4
]
// 将所有单元格设置为文本格式
const range = XLSX.utils.decode_range(ws['!ref'] || 'A1')
for (let R = range.s.r; R <= range.e.r; ++R) {
for (let C = range.s.c; C <= range.e.c; ++C) {
const cellAddress = XLSX.utils.encode_cell({ r: R, c: C })
if (!ws[cellAddress]) continue
ws[cellAddress].t = 's' // 设置为字符串类型
}
}
// 添加工作表到工作簿
XLSX.utils.book_append_sheet(wb, ws, '设备导入模板')
// 导出文件
XLSX.writeFile(wb, '设备导入模板.xlsx')
ElMessage.success('设备导入模板下载成功')
} catch (error) {
console.error('下载模板失败:', error)
ElMessage.error('下载模板失败')
}
} }
// 文件选择变化 // 文件选择变化
@@ -510,8 +602,8 @@
return return
} }
if (uploadFile.raw && !uploadFile.raw.name.endsWith('.csv')) { if (uploadFile.raw && !uploadFile.raw.name.endsWith('.xlsx')) {
ElMessage.error('只能上传 CSV 文件') ElMessage.error('只能上传 .xlsx 格式的 Excel 文件')
uploadRef.value?.clearFiles() uploadRef.value?.clearFiles()
fileList.value = [] fileList.value = []
return return
@@ -535,7 +627,7 @@
// 提交上传 // 提交上传
const submitUpload = async () => { const submitUpload = async () => {
if (!fileList.value.length) { if (!fileList.value.length) {
ElMessage.warning('请先选择CSV文件') ElMessage.warning('请先选择 Excel 文件')
return return
} }
@@ -546,7 +638,7 @@
ElMessage.info('正在准备上传...') ElMessage.info('正在准备上传...')
const uploadUrlRes = await StorageService.getUploadUrl({ const uploadUrlRes = await StorageService.getUploadUrl({
file_name: file.name, file_name: file.name,
content_type: 'text/csv', content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
purpose: 'iot_import' purpose: 'iot_import'
}) })
@@ -557,7 +649,7 @@
const { upload_url, file_key } = uploadUrlRes.data const { upload_url, file_key } = uploadUrlRes.data
ElMessage.info('正在上传文件...') ElMessage.info('正在上传文件...')
await StorageService.uploadFile(upload_url, file, 'text/csv') await StorageService.uploadFile(upload_url, file, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
ElMessage.info('正在创建导入任务...') ElMessage.info('正在创建导入任务...')
const importRes = await DeviceService.importDevices({ const importRes = await DeviceService.importDevices({
@@ -601,6 +693,94 @@
} }
} }
// 下载跳过数据(从详情对话框)
const downloadSkippedData = () => {
downloadSkippedDataFromDetail(currentDetail.value, currentDetail.value.batch_no)
}
// 下载跳过数据的通用方法
const downloadSkippedDataFromDetail = (detail: any, batchNo: string) => {
const skippedReasons =
detail.skipped_items?.map((item: any) => ({
line: item.line || '-',
deviceNo: item.device_no || '-',
message: item.reason || '未知原因'
})) || []
if (skippedReasons.length === 0) {
ElMessage.warning('没有跳过数据可下载')
return
}
const headers = ['行号', '设备编号', '跳过原因']
const csvRows = [
headers.join(','),
...skippedReasons.map((item: any) =>
[item.line, `\t${item.deviceNo}`, `"${item.message}"`].join(',')
)
]
const csvContent = csvRows.join('\n')
const BOM = '\uFEFF'
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
const url = URL.createObjectURL(blob)
link.setAttribute('href', url)
link.setAttribute('download', `设备导入跳过数据_${batchNo}.csv`)
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
ElMessage.success('跳过数据下载成功')
}
// 下载警告数据(从详情对话框)
const downloadWarningData = () => {
downloadWarningDataFromDetail(currentDetail.value, currentDetail.value.batch_no)
}
// 下载警告数据的通用方法
const downloadWarningDataFromDetail = (detail: any, batchNo: string) => {
const warningReasons =
detail.warning_items?.map((item: any) => ({
line: item.line || '-',
deviceNo: item.device_no || '-',
message: item.reason || item.message || '未知警告'
})) || []
if (warningReasons.length === 0) {
ElMessage.warning('没有警告数据可下载')
return
}
const headers = ['行号', '设备编号', '警告信息']
const csvRows = [
headers.join(','),
...warningReasons.map((item: any) =>
[item.line, `\t${item.deviceNo}`, `"${item.message}"`].join(',')
)
]
const csvContent = csvRows.join('\n')
const BOM = '\uFEFF'
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
const url = URL.createObjectURL(blob)
link.setAttribute('href', url)
link.setAttribute('download', `设备导入警告数据_${batchNo}.csv`)
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
ElMessage.success('警告数据下载成功')
}
// 下载失败数据(从详情对话框) // 下载失败数据(从详情对话框)
const downloadFailData = () => { const downloadFailData = () => {
downloadFailDataFromDetail(currentDetail.value, currentDetail.value.batch_no) downloadFailDataFromDetail(currentDetail.value, currentDetail.value.batch_no)
@@ -609,11 +789,10 @@
// 下载失败数据的通用方法 // 下载失败数据的通用方法
const downloadFailDataFromDetail = (detail: any, batchNo: string) => { const downloadFailDataFromDetail = (detail: any, batchNo: string) => {
const failReasons = const failReasons =
detail.failed_items?.map((item: any, index: number) => ({ detail.failed_items?.map((item: any) => ({
row: index + 1, line: item.line || '-',
deviceCode: item.device_no || '-', deviceNo: item.device_no || '-',
iccid: item.iccid || '-', message: item.reason || '未知错误'
message: item.reason || item.error || '未知错误'
})) || [] })) || []
if (failReasons.length === 0) { if (failReasons.length === 0) {
@@ -621,11 +800,11 @@
return return
} }
const headers = ['行号', '设备编号', 'ICCID', '失败原因'] const headers = ['行号', '设备编号', '失败原因']
const csvRows = [ const csvRows = [
headers.join(','), headers.join(','),
...failReasons.map((item: any) => ...failReasons.map((item: any) =>
[item.row, item.deviceCode, item.iccid, `"${item.message}"`].join(',') [item.line, `\t${item.deviceNo}`, `"${item.message}"`].join(',')
) )
] ]
const csvContent = csvRows.join('\n') const csvContent = csvRows.join('\n')

View File

@@ -33,18 +33,7 @@
> >
批量回收 批量回收
</ElButton> </ElButton>
<ElButton <ElButton type="info" @contextmenu.prevent="showMoreMenu">更多操作</ElButton>
type="info"
:disabled="selectedCards.length === 0"
@click="showSeriesBindingDialog"
>
批量设置套餐系列
</ElButton>
<ElButton type="primary" @click="cardDistribution">网卡分销</ElButton>
<ElButton type="success" @click="batchRecharge">批量充值</ElButton>
<ElButton type="danger" @click="cardRecycle">网卡回收</ElButton>
<ElButton type="info" @click="batchDownload">批量下载</ElButton>
<ElButton type="warning" @click="changePackage">变更套餐</ElButton>
</template> </template>
</ArtTableHeader> </ArtTableHeader>
@@ -501,6 +490,14 @@
</div> </div>
</template> </template>
</ElDialog> </ElDialog>
<!-- 更多操作右键菜单 -->
<ArtMenuRight
ref="moreMenuRef"
:menu-items="moreMenuItems"
:menu-width="180"
@select="handleMoreMenuSelect"
/>
</ElCard> </ElCard>
</div> </div>
</ArtTableFullScreen> </ArtTableFullScreen>
@@ -517,6 +514,8 @@
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import type { import type {
StandaloneIotCard, StandaloneIotCard,
StandaloneCardStatus, StandaloneCardStatus,
@@ -578,6 +577,9 @@
const cardDetailLoading = ref(false) const cardDetailLoading = ref(false)
const currentCardDetail = ref<any>(null) const currentCardDetail = ref<any>(null)
// 更多操作右键菜单
const moreMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
// 搜索表单初始值 // 搜索表单初始值
const initialSearchState = { const initialSearchState = {
status: undefined, status: undefined,
@@ -1423,6 +1425,72 @@
}) })
} }
// 更多操作菜单项配置
const moreMenuItems = computed((): MenuItemType[] => [
{
key: 'seriesBinding',
label: '批量设置套餐系列',
icon: '&#xe88e;',
disabled: selectedCards.value.length === 0
},
{
key: 'distribution',
label: '网卡分销',
icon: '&#xe73b;'
},
{
key: 'recharge',
label: '批量充值',
icon: '&#xe63a;'
},
{
key: 'recycle',
label: '网卡回收',
icon: '&#xe850;'
},
{
key: 'download',
label: '批量下载',
icon: '&#xe78b;'
},
{
key: 'changePackage',
label: '变更套餐',
icon: '&#xe706;'
}
])
// 显示更多操作菜单
const showMoreMenu = (e: MouseEvent) => {
e.preventDefault()
e.stopPropagation()
moreMenuRef.value?.show(e)
}
// 处理更多操作菜单选择
const handleMoreMenuSelect = (item: MenuItemType) => {
switch (item.key) {
case 'seriesBinding':
showSeriesBindingDialog()
break
case 'distribution':
cardDistribution()
break
case 'recharge':
batchRecharge()
break
case 'recycle':
cardRecycle()
break
case 'download':
batchDownload()
break
case 'changePackage':
changePackage()
break
}
}
// 网卡分销 - 正在开发中 // 网卡分销 - 正在开发中
const cardDistribution = () => { const cardDistribution = () => {
ElMessage.info('功能正在开发中') ElMessage.info('功能正在开发中')

View File

@@ -50,10 +50,10 @@
<template #title> <template #title>
<div style="line-height: 1.8"> <div style="line-height: 1.8">
<p><strong>导入说明</strong></p> <p><strong>导入说明</strong></p>
<p>1. 请先下载 CSV 模板文件按照模板格式填写IoT卡信息</p> <p>1. 请先下载 Excel 模板文件按照模板格式填写IoT卡信息</p>
<p>2. 支持 CSV 格式.csv单次最多导入 1000 </p> <p>2. 支持 Excel 格式.xlsx单次最多导入 1000 </p>
<p>3. CSV 文件编码UTF-8推荐 GBK</p> <p>3. 列格式请设置为文本格式避免长数字被转为科学计数法</p>
<p>4. 必填字段iccidICCIDmsisdnMSISDN/手机号</p> <p>4. 必填字段ICCIDMSISDN手机号</p>
<p>5. 必须选择运营商</p> <p>5. 必须选择运营商</p>
</div> </div>
</template> </template>
@@ -79,12 +79,12 @@
:auto-upload="false" :auto-upload="false"
:on-change="handleFileChange" :on-change="handleFileChange"
:limit="1" :limit="1"
accept=".csv" accept=".xlsx"
> >
<el-icon class="el-icon--upload"><upload-filled /></el-icon> <el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text"> CSV 文件拖到此处<em>点击选择</em></div> <div class="el-upload__text"> Excel 文件拖到此处<em>点击选择</em></div>
<template #tip> <template #tip>
<div class="el-upload__tip">只能上传 CSV 文件且不超过 10MB</div> <div class="el-upload__tip">只能上传 .xlsx 格式的 Excel 文件且不超过 10MB</div>
</template> </template>
</ElUpload> </ElUpload>
@@ -133,13 +133,31 @@
}}</ElDescriptionsItem> }}</ElDescriptionsItem>
</ElDescriptions> </ElDescriptions>
<ElDivider content-position="left">跳过明细</ElDivider>
<div
v-if="currentDetail.skipped_items && currentDetail.skipped_items.length"
style="max-height: 300px; overflow-y: auto; margin-bottom: 20px"
>
<ElTable :data="currentDetail.skipped_items" border size="small">
<ElTableColumn label="行号" prop="line" width="80" />
<ElTableColumn label="ICCID" prop="iccid" width="200" />
<ElTableColumn label="MSISDN" prop="msisdn" width="150" />
<ElTableColumn label="跳过原因" prop="reason" min-width="200">
<template #default="{ row }">
{{ row.reason || '未知原因' }}
</template>
</ElTableColumn>
</ElTable>
</div>
<ElEmpty v-else description="无跳过记录" />
<ElDivider content-position="left">失败明细</ElDivider> <ElDivider content-position="left">失败明细</ElDivider>
<div <div
v-if="currentDetail.failed_items && currentDetail.failed_items.length" v-if="currentDetail.failed_items && currentDetail.failed_items.length"
style="max-height: 300px; overflow-y: auto" style="max-height: 300px; overflow-y: auto"
> >
<ElTable :data="currentDetail.failed_items" border size="small"> <ElTable :data="currentDetail.failed_items" border size="small">
<ElTableColumn label="行号" type="index" width="80" :index="(index) => index + 1" /> <ElTableColumn label="行号" prop="line" width="80" />
<ElTableColumn label="ICCID" prop="iccid" width="200" /> <ElTableColumn label="ICCID" prop="iccid" width="200" />
<ElTableColumn label="MSISDN" prop="msisdn" width="150" /> <ElTableColumn label="MSISDN" prop="msisdn" width="150" />
<ElTableColumn label="失败原因" prop="reason" min-width="200"> <ElTableColumn label="失败原因" prop="reason" min-width="200">
@@ -153,6 +171,14 @@
<template #footer> <template #footer>
<ElButton @click="detailDialogVisible = false">关闭</ElButton> <ElButton @click="detailDialogVisible = false">关闭</ElButton>
<ElButton
v-if="currentDetail.skip_count > 0"
type="warning"
:icon="Download"
@click="downloadSkippedData"
>
下载跳过数据
</ElButton>
<ElButton <ElButton
v-if="currentDetail.fail_count > 0" v-if="currentDetail.fail_count > 0"
type="primary" type="primary"
@@ -513,6 +539,51 @@
} }
} }
// 下载跳过数据(从详情对话框)
const downloadSkippedData = () => {
downloadSkippedDataFromDetail(currentDetail.value, currentDetail.value.task_no)
}
// 下载跳过数据的通用方法
const downloadSkippedDataFromDetail = (detail: any, taskNo: string) => {
const skippedReasons =
detail.skipped_items?.map((item: any) => ({
line: item.line || '-',
iccid: item.iccid || '-',
msisdn: item.msisdn || '-',
message: item.reason || '未知原因'
})) || []
if (skippedReasons.length === 0) {
ElMessage.warning('没有跳过数据可下载')
return
}
const headers = ['行号', 'ICCID', 'MSISDN', '跳过原因']
const csvRows = [
headers.join(','),
...skippedReasons.map((item: any) =>
[item.line, item.iccid, item.msisdn, `"${item.message}"`].join(',')
)
]
const csvContent = csvRows.join('\n')
const BOM = '\uFEFF'
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
const url = URL.createObjectURL(blob)
link.setAttribute('href', url)
link.setAttribute('download', `IoT卡导入跳过数据_${taskNo}.csv`)
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
ElMessage.success('跳过数据下载成功')
}
// 下载失败数据(从详情对话框) // 下载失败数据(从详情对话框)
const downloadFailData = () => { const downloadFailData = () => {
downloadFailDataFromDetail(currentDetail.value, currentDetail.value.task_no) downloadFailDataFromDetail(currentDetail.value, currentDetail.value.task_no)
@@ -521,8 +592,8 @@
// 下载失败数据的通用方法 // 下载失败数据的通用方法
const downloadFailDataFromDetail = (detail: any, taskNo: string) => { const downloadFailDataFromDetail = (detail: any, taskNo: string) => {
const failReasons = const failReasons =
detail.failed_items?.map((item: any, index: number) => ({ detail.failed_items?.map((item: any) => ({
row: index + 1, line: item.line || '-',
iccid: item.iccid || '-', iccid: item.iccid || '-',
msisdn: item.msisdn || '-', msisdn: item.msisdn || '-',
message: item.reason || item.error || '未知错误' message: item.reason || item.error || '未知错误'
@@ -537,7 +608,7 @@
const csvRows = [ const csvRows = [
headers.join(','), headers.join(','),
...failReasons.map((item: any) => ...failReasons.map((item: any) =>
[item.row, item.iccid, item.msisdn, `"${item.message}"`].join(',') [item.line, item.iccid, item.msisdn, `"${item.message}"`].join(',')
) )
] ]
const csvContent = csvRows.join('\n') const csvContent = csvRows.join('\n')
@@ -559,29 +630,58 @@
} }
// 下载模板 // 下载模板
const downloadTemplate = () => { const downloadTemplate = async () => {
// 使用 \t 前缀防止 Excel 将长数字转换为科学计数法 try {
const csvContent = [ // 动态导入 xlsx 库
'iccid,msisdn', const XLSX = await import('xlsx')
'\t89860123456789012345,\t13800138000',
'\t89860123456789012346,\t13800138001',
'\t89860123456789012347,\t13800138002'
].join('\n')
const BOM = '\uFEFF' // 创建示例数据
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' }) const templateData = [
{
ICCID: '89860123456789012345',
MSISDN: '13800138000'
},
{
ICCID: '89860123456789012346',
MSISDN: '13800138001'
},
{
ICCID: '89860123456789012347',
MSISDN: '13800138002'
}
]
const link = document.createElement('a') // 创建工作簿
const url = URL.createObjectURL(blob) const wb = XLSX.utils.book_new()
link.setAttribute('href', url) const ws = XLSX.utils.json_to_sheet(templateData)
link.setAttribute('download', 'IoT卡导入模板.csv')
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
ElMessage.success('IoT卡导入模板下载成功') // 设置列宽
ws['!cols'] = [
{ wch: 25 }, // ICCID
{ wch: 15 } // MSISDN
]
// 将所有单元格设置为文本格式,防止科学计数法
const range = XLSX.utils.decode_range(ws['!ref'] || 'A1')
for (let R = range.s.r; R <= range.e.r; ++R) {
for (let C = range.s.c; C <= range.e.c; ++C) {
const cellAddress = XLSX.utils.encode_cell({ r: R, c: C })
if (!ws[cellAddress]) continue
ws[cellAddress].t = 's' // 设置为字符串类型
}
}
// 添加工作表到工作簿
XLSX.utils.book_append_sheet(wb, ws, 'IoT卡导入模板')
// 导出文件
XLSX.writeFile(wb, 'IoT卡导入模板.xlsx')
ElMessage.success('IoT卡导入模板下载成功')
} catch (error) {
console.error('下载模板失败:', error)
ElMessage.error('下载模板失败')
}
} }
// 文件选择变化 // 文件选择变化
@@ -594,8 +694,8 @@
return return
} }
if (uploadFile.raw && !uploadFile.raw.name.endsWith('.csv')) { if (uploadFile.raw && !uploadFile.raw.name.endsWith('.xlsx')) {
ElMessage.error('只能上传 CSV 文件') ElMessage.error('只能上传 .xlsx 格式的 Excel 文件')
uploadRef.value?.clearFiles() uploadRef.value?.clearFiles()
fileList.value = [] fileList.value = []
return return
@@ -625,7 +725,7 @@
} }
if (!fileList.value.length) { if (!fileList.value.length) {
ElMessage.warning('请先选择CSV文件') ElMessage.warning('请先选择 Excel 文件')
return return
} }
@@ -636,7 +736,7 @@
ElMessage.info('正在准备上传...') ElMessage.info('正在准备上传...')
const uploadUrlRes = await StorageService.getUploadUrl({ const uploadUrlRes = await StorageService.getUploadUrl({
file_name: file.name, file_name: file.name,
content_type: 'text/csv', content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
purpose: 'iot_import' purpose: 'iot_import'
}) })
@@ -647,7 +747,7 @@
const { upload_url, file_key } = uploadUrlRes.data const { upload_url, file_key } = uploadUrlRes.data
ElMessage.info('正在上传文件...') ElMessage.info('正在上传文件...')
await StorageService.uploadFile(upload_url, file, 'text/csv') await StorageService.uploadFile(upload_url, file, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
ElMessage.info('正在创建导入任务...') ElMessage.info('正在创建导入任务...')
const importRes = await CardService.importIotCards({ const importRes = await CardService.importIotCards({

View File

@@ -290,7 +290,6 @@
// 列配置 // 列配置
const columnOptions = [ const columnOptions = [
{ label: '店铺ID', prop: 'shop_id' },
{ label: '店铺编码', prop: 'shop_code' }, { label: '店铺编码', prop: 'shop_code' },
{ label: '店铺名称', prop: 'shop_name' }, { label: '店铺名称', prop: 'shop_name' },
{ label: '用户名', prop: 'username' }, { label: '用户名', prop: 'username' },
@@ -306,11 +305,6 @@
// 动态列配置 // 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [ const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'shop_id',
label: '店铺ID',
width: 100
},
{ {
prop: 'shop_code', prop: 'shop_code',
label: '店铺编码', label: '店铺编码',

View File

@@ -74,11 +74,31 @@
:label="t('orderManagement.createForm.iotCardId')" :label="t('orderManagement.createForm.iotCardId')"
prop="iot_card_id" prop="iot_card_id"
> >
<ElInputNumber <ElSelect
v-model="createForm.iot_card_id" v-model="createForm.iot_card_id"
filterable
remote
reserve-keyword
:placeholder="t('orderManagement.createForm.iotCardIdPlaceholder')" :placeholder="t('orderManagement.createForm.iotCardIdPlaceholder')"
:remote-method="searchIotCards"
:loading="cardSearchLoading"
style="width: 100%" style="width: 100%"
/> clearable
>
<ElOption
v-for="card in iotCardOptions"
:key="card.id"
:label="`${card.iccid} (${card.msisdn || '无接入号'})`"
:value="card.id"
>
<div style="display: flex; justify-content: space-between">
<span>{{ card.iccid }}</span>
<span style="color: var(--el-text-color-secondary); font-size: 12px">
{{ card.msisdn || '无接入号' }}
</span>
</div>
</ElOption>
</ElSelect>
</ElFormItem> </ElFormItem>
<ElFormItem <ElFormItem
v-if="createForm.order_type === 'device'" v-if="createForm.order_type === 'device'"
@@ -200,7 +220,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { h } from 'vue' import { h } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { OrderService } from '@/api/modules' import { OrderService, CardService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag } from 'element-plus' import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import type { import type {
@@ -211,7 +231,8 @@
OrderType, OrderType,
BuyerType, BuyerType,
OrderPaymentMethod, OrderPaymentMethod,
OrderCommissionStatus OrderCommissionStatus,
StandaloneIotCard
} from '@/types/api' } from '@/types/api'
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
@@ -342,6 +363,35 @@
const orderList = ref<Order[]>([]) const orderList = ref<Order[]>([])
// IoT卡搜索相关
const iotCardOptions = ref<StandaloneIotCard[]>([])
const cardSearchLoading = ref(false)
// 搜索IoT卡根据ICCID
const searchIotCards = async (query: string) => {
if (!query) {
iotCardOptions.value = []
return
}
cardSearchLoading.value = true
try {
const res = await CardService.getStandaloneIotCards({
iccid: query,
page: 1,
page_size: 20
})
if (res.code === 0) {
iotCardOptions.value = res.data.items || []
}
} catch (error) {
console.error('Search IoT cards failed:', error)
iotCardOptions.value = []
} finally {
cardSearchLoading.value = false
}
}
// 格式化货币 - 将分转换为元 // 格式化货币 - 将分转换为元
const formatCurrency = (amount: number): string => { const formatCurrency = (amount: number): string => {
return `¥${(amount / 100).toFixed(2)}` return `¥${(amount / 100).toFixed(2)}`
@@ -554,8 +604,29 @@
} }
// 显示创建订单对话框 // 显示创建订单对话框
const showCreateDialog = () => { const showCreateDialog = async () => {
createDialogVisible.value = true createDialogVisible.value = true
// 默认加载20条IoT卡数据
await loadDefaultIotCards()
}
// 加载默认IoT卡列表
const loadDefaultIotCards = async () => {
cardSearchLoading.value = true
try {
const res = await CardService.getStandaloneIotCards({
page: 1,
page_size: 20
})
if (res.code === 0) {
iotCardOptions.value = res.data.items || []
}
} catch (error) {
console.error('Load default IoT cards failed:', error)
iotCardOptions.value = []
} finally {
cardSearchLoading.value = false
}
} }
// 对话框关闭后的清理 // 对话框关闭后的清理
@@ -568,6 +639,9 @@
createForm.package_ids = [] createForm.package_ids = []
createForm.iot_card_id = null createForm.iot_card_id = null
createForm.device_id = null createForm.device_id = null
// 清空IoT卡搜索结果
iotCardOptions.value = []
} }
// 创建订单 // 创建订单

View File

@@ -312,12 +312,51 @@
} }
]) ])
// 将扁平数据转换为树形结构
const buildTreeData = (flatData: Permission[]): Permission[] => {
const map = new Map<number, Permission>()
const result: Permission[] = []
// 先创建所有节点的映射
flatData.forEach((item) => {
map.set(item.ID, { ...item, children: [] })
})
// 构建树形结构
map.forEach((item) => {
if (item.parent_id && map.has(item.parent_id)) {
const parent = map.get(item.parent_id)!
if (!parent.children) {
parent.children = []
}
parent.children.push(item)
} else {
// 没有父节点的是根节点
result.push(item)
}
})
// 递归排序
const sortTree = (nodes: Permission[]): Permission[] => {
return nodes
.sort((a, b) => (a.sort || 0) - (b.sort || 0))
.map((node) => ({
...node,
children: node.children && node.children.length > 0 ? sortTree(node.children) : undefined
}))
}
return sortTree(result)
}
// 获取权限列表 // 获取权限列表
const getPermissionList = async () => { const getPermissionList = async () => {
try { try {
const response = await PermissionService.getPermissions(searchForm) const response = await PermissionService.getPermissions(searchForm)
if (response.code === 0) { if (response.code === 0) {
permissionList.value = response.data.items || [] const flatData = response.data.items || []
// 将扁平数据转换为树形结构
permissionList.value = buildTreeData(flatData)
// 构建权限树选项 // 构建权限树选项
buildPermissionTreeOptions() buildPermissionTreeOptions()
} }
@@ -332,7 +371,7 @@
return list.map((item) => ({ return list.map((item) => ({
value: item.ID, value: item.ID,
label: item.perm_name, label: item.perm_name,
children: item.children ? buildTree(item.children) : undefined children: item.children && item.children.length > 0 ? buildTree(item.children) : undefined
})) }))
} }
permissionTreeOptions.value = buildTree(permissionList.value) permissionTreeOptions.value = buildTree(permissionList.value)

View File

@@ -89,24 +89,28 @@
<!-- 分配权限对话框 --> <!-- 分配权限对话框 -->
<ElDialog v-model="permissionDialogVisible" title="分配权限" width="500px"> <ElDialog v-model="permissionDialogVisible" title="分配权限" width="500px">
<ElCheckboxGroup v-model="selectedPermissions"> <ElTree
<div ref="permissionTreeRef"
v-for="permission in allPermissions" :data="permissionTreeData"
:key="permission.ID" show-checkbox
style="margin-bottom: 12px" node-key="id"
> :default-checked-keys="selectedPermissions"
<ElCheckbox :label="permission.ID"> :props="{ children: 'children', label: 'label' }"
{{ permission.perm_name }} :default-expand-all="false"
class="permission-tree"
>
<template #default="{ node, data }">
<span style="display: flex; align-items: center; gap: 8px">
<span>{{ node.label }}</span>
<ElTag <ElTag
:type="permission.perm_type === 1 ? 'info' : 'success'" :type="data.perm_type === 1 ? 'info' : 'success'"
size="small" size="small"
style="margin-left: 8px"
> >
{{ permission.perm_type === 1 ? '菜单' : '按钮' }} {{ data.perm_type === 1 ? '菜单' : '按钮' }}
</ElTag> </ElTag>
</ElCheckbox> </span>
</div> </template>
</ElCheckboxGroup> </ElTree>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<ElButton @click="permissionDialogVisible = false">取消</ElButton> <ElButton @click="permissionDialogVisible = false">取消</ElButton>
@@ -132,30 +136,34 @@
ElMessage, ElMessage,
ElMessageBox, ElMessageBox,
ElTag, ElTag,
ElCheckbox, ElTree,
ElCheckboxGroup,
ElSwitch ElSwitch
} from 'element-plus' } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import type { PlatformRole, Permission } from '@/types/api' import type { PlatformRole, Permission } from '@/types/api'
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText } from '@/config/constants' import { CommonStatus, getStatusText } from '@/config/constants'
defineOptions({ name: 'Role' }) defineOptions({ name: 'Role' })
const { hasAuth } = useAuth()
const dialogVisible = ref(false) const dialogVisible = ref(false)
const permissionDialogVisible = ref(false) const permissionDialogVisible = ref(false)
const loading = ref(false) const loading = ref(false)
const submitLoading = ref(false) const submitLoading = ref(false)
const permissionSubmitLoading = ref(false) const permissionSubmitLoading = ref(false)
const tableRef = ref() const tableRef = ref()
const permissionTreeRef = ref()
const currentRoleId = ref<number>(0) const currentRoleId = ref<number>(0)
const selectedPermissions = ref<number[]>([]) const selectedPermissions = ref<number[]>([])
const originalPermissions = ref<number[]>([]) // 保存原始权限,用于对比 const originalPermissions = ref<number[]>([]) // 保存原始权限,用于对比
const allPermissions = ref<Permission[]>([]) const allPermissions = ref<Permission[]>([])
const permissionTreeData = ref<any[]>([])
// 搜索表单初始值 // 搜索表单初始值
const initialSearchState = { const initialSearchState = {
@@ -272,20 +280,39 @@
width: 200, width: 200,
fixed: 'right', fixed: 'right',
formatter: (row: any) => { formatter: (row: any) => {
return h('div', { style: 'display: flex; gap: 8px;' }, [ const buttons = []
h(ArtButtonTable, {
icon: '&#xe72b;', // 分配权限按钮
onClick: () => showPermissionDialog(row) if (hasAuth('role:permission')) {
}), buttons.push(
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'edit', icon: '&#xe72b;',
onClick: () => showDialog('edit', row) onClick: () => showPermissionDialog(row)
}), })
h(ArtButtonTable, { )
type: 'delete', }
onClick: () => deleteRole(row)
}) // 编辑按钮
]) if (hasAuth('role:edit')) {
buttons.push(
h(ArtButtonTable, {
type: 'edit',
onClick: () => showDialog('edit', row)
})
)
}
// 删除按钮
if (hasAuth('role:delete')) {
buttons.push(
h(ArtButtonTable, {
type: 'delete',
onClick: () => deleteRole(row)
})
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
} }
} }
]) ])
@@ -295,12 +322,59 @@
loadAllPermissions() loadAllPermissions()
}) })
// 将扁平数据转换为树形结构
const buildTreeData = (flatData: Permission[]): any[] => {
const map = new Map<number, any>()
const result: any[] = []
// 先创建所有节点的映射
flatData.forEach((item) => {
map.set(item.ID, {
id: item.ID,
label: item.perm_name,
perm_type: item.perm_type,
children: []
})
})
// 构建树形结构
flatData.forEach((item) => {
const node = map.get(item.ID)!
if (item.parent_id && map.has(item.parent_id)) {
const parent = map.get(item.parent_id)!
parent.children.push(node)
} else {
// 没有父节点的是根节点
result.push(node)
}
})
// 递归排序和清理空children
const sortAndCleanTree = (nodes: any[]): any[] => {
return nodes
.sort((a, b) => {
const aItem = flatData.find((p) => p.ID === a.id)
const bItem = flatData.find((p) => p.ID === b.id)
return (aItem?.sort || 0) - (bItem?.sort || 0)
})
.map((node) => ({
...node,
children:
node.children && node.children.length > 0 ? sortAndCleanTree(node.children) : undefined
}))
}
return sortAndCleanTree(result)
}
// 加载所有权限列表 // 加载所有权限列表
const loadAllPermissions = async () => { const loadAllPermissions = async () => {
try { try {
const res = await PermissionService.getPermissions({ page: 1, page_size: 1000 }) const res = await PermissionService.getPermissions({ page: 1, page_size: 1000 })
if (res.code === 0) { if (res.code === 0) {
allPermissions.value = res.data.items || [] allPermissions.value = res.data.items || []
// 构建树形数据
permissionTreeData.value = buildTreeData(allPermissions.value)
} }
} catch (error) { } catch (error) {
console.error('获取权限列表失败:', error) console.error('获取权限列表失败:', error)
@@ -343,14 +417,21 @@
// 提交分配权限 // 提交分配权限
const handleAssignPermissions = async () => { const handleAssignPermissions = async () => {
if (!permissionTreeRef.value) return
permissionSubmitLoading.value = true permissionSubmitLoading.value = true
try { try {
// 获取选中的节点(包括半选中的父节点)
const checkedKeys = permissionTreeRef.value.getCheckedKeys()
const halfCheckedKeys = permissionTreeRef.value.getHalfCheckedKeys()
const currentPermissions = [...checkedKeys, ...halfCheckedKeys]
// 对比原始权限和当前选中的权限,找出需要新增和移除的权限 // 对比原始权限和当前选中的权限,找出需要新增和移除的权限
const addedPermissions = selectedPermissions.value.filter( const addedPermissions = currentPermissions.filter(
(id) => !originalPermissions.value.includes(id) (id) => !originalPermissions.value.includes(id)
) )
const removedPermissions = originalPermissions.value.filter( const removedPermissions = originalPermissions.value.filter(
(id) => !selectedPermissions.value.includes(id) (id) => !currentPermissions.includes(id)
) )
// 使用 Promise.all 并发执行新增和移除操作 // 使用 Promise.all 并发执行新增和移除操作
@@ -532,3 +613,16 @@
} }
} }
</script> </script>
<style scoped lang="scss">
.permission-tree {
:deep(.el-tree-node) {
margin: 6px 0;
}
:deep(.el-tree-node__content) {
height: 36px;
line-height: 36px;
}
}
</style>

View File

@@ -32,8 +32,9 @@ export default ({ mode }) => {
}, },
// 对象存储代理 - 解决开发环境 CORS 问题 // 对象存储代理 - 解决开发环境 CORS 问题
'/obs-proxy': { '/obs-proxy': {
target: 'http://obs-helf.cucloud.cn', target: 'https://obs-helf.cucloud.cn',
changeOrigin: true, changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/obs-proxy/, '') rewrite: (path) => path.replace(/^\/obs-proxy/, '')
} }
}, },