fetch(modify):修改bug
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m20s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m20s
This commit is contained in:
@@ -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 请求参数
|
||||
@@ -56,18 +56,18 @@ export class StorageService extends BaseService {
|
||||
* - 预签名 URL 有效期 15 分钟,请及时使用
|
||||
* - 上传时 Content-Type 需与请求时一致
|
||||
* - file_key 在上传成功后永久有效
|
||||
* - 开发环境通过代理上传,生产环境直接上传
|
||||
*
|
||||
* @param uploadUrl 预签名 URL
|
||||
* @param uploadUrl 预签名 URL(由对象存储生成)
|
||||
* @param file 文件
|
||||
* @param contentType 文件类型(需与 getUploadUrl 请求时保持一致)
|
||||
*/
|
||||
static async uploadFile(uploadUrl: string, file: File, contentType?: string): Promise<void> {
|
||||
try {
|
||||
// 在开发环境下,使用代理路径避免 CORS 问题
|
||||
// 开发环境使用代理解决 CORS 问题
|
||||
let finalUrl = uploadUrl
|
||||
if (import.meta.env.DEV) {
|
||||
// 将对象存储的域名替换为代理路径
|
||||
// 例如:http://obs-helf.cucloud.cn/cmp/... -> /obs-proxy/cmp/...
|
||||
// 将对象存储域名替换为代理路径
|
||||
finalUrl = uploadUrl.replace(/^https?:\/\/obs-helf\.cucloud\.cn/, '/obs-proxy')
|
||||
}
|
||||
|
||||
@@ -81,8 +81,7 @@ export class StorageService extends BaseService {
|
||||
const response = await fetch(finalUrl, {
|
||||
method: 'PUT',
|
||||
body: file,
|
||||
headers,
|
||||
mode: 'cors' // 明确指定 CORS 模式
|
||||
headers
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -1,46 +1,57 @@
|
||||
import { useRoute } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
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()
|
||||
* hasAuth('add') // 检查是否拥有新增权限
|
||||
* const { hasAuth, hasAllAuth } = useAuth()
|
||||
* hasAuth('role:add') // 检查是否拥有权限
|
||||
* hasAuth(['role:add', 'role:edit']) // 检查是否拥有任意一个权限
|
||||
* hasAllAuth(['role:add', 'role:edit']) // 检查是否拥有全部权限
|
||||
*/
|
||||
export const useAuth = () => {
|
||||
const route = useRoute()
|
||||
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[])
|
||||
: []
|
||||
const userStore = useUserStore()
|
||||
|
||||
/**
|
||||
* 检查是否拥有某权限标识
|
||||
* @param auth 权限标识
|
||||
* 检查是否有指定权限(满足任意一个即可)
|
||||
* @param permission 权限标识,可以是字符串或字符串数组
|
||||
* @returns 是否有权限
|
||||
*/
|
||||
const hasAuth = (auth: string): boolean => {
|
||||
if (isFrontendMode.value) {
|
||||
return frontendAuthList.includes(auth)
|
||||
const hasAuth = (permission: string | string[]): boolean => {
|
||||
if (!permission) return false
|
||||
|
||||
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 {
|
||||
hasAuth
|
||||
hasAuth,
|
||||
hasAllAuth
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,12 +208,26 @@ export function useLogin() {
|
||||
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)
|
||||
|
||||
// 显示登录成功提示
|
||||
showLoginSuccessNotice()
|
||||
|
||||
// 等待数据持久化完成
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// 跳转到重定向页面或首页
|
||||
const redirectPath = getRedirectPath(route)
|
||||
await router.push(redirectPath || HOME_PAGE)
|
||||
|
||||
Binary file not shown.
@@ -22,16 +22,20 @@ const permissionDirective: Directive = {
|
||||
let hasPermission = false
|
||||
|
||||
if (typeof value === 'string') {
|
||||
// 单个权限
|
||||
hasPermission = userStore.hasPermission(value)
|
||||
// 单个权限(同时检查 permissions 和 buttons)
|
||||
hasPermission = userStore.hasPermission(value) || userStore.hasButton(value)
|
||||
} else if (Array.isArray(value)) {
|
||||
// 多个权限
|
||||
if (arg === 'all') {
|
||||
// 需要全部满足
|
||||
hasPermission = userStore.hasAllPermissions(value)
|
||||
hasPermission = value.every(
|
||||
(perm) => userStore.hasPermission(perm) || userStore.hasButton(perm)
|
||||
)
|
||||
} else {
|
||||
// 满足任意一个即可
|
||||
hasPermission = userStore.hasAnyPermission(value)
|
||||
hasPermission = value.some(
|
||||
(perm) => userStore.hasPermission(perm) || userStore.hasButton(perm)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import { isInWhiteList, hasRoutePermission, isTokenValid, buildLoginRedirect } f
|
||||
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> {
|
||||
const closeLoading = loadingService.showLoading()
|
||||
|
||||
try {
|
||||
const { menuList } = await menuService.getMenuList()
|
||||
await registerAndStoreMenu(router, menuList, closeLoading)
|
||||
const userStore = useUserStore()
|
||||
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) {
|
||||
closeLoading()
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册路由并存储菜单数据
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
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 = (
|
||||
route: RouteLocationNormalized,
|
||||
@@ -40,7 +42,30 @@ export const hasRoutePermission = (
|
||||
): boolean => {
|
||||
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) {
|
||||
return true
|
||||
}
|
||||
@@ -58,11 +83,9 @@ export const hasRoutePermission = (
|
||||
if (route.meta.permissions) {
|
||||
const routePermissions = route.meta.permissions as string[]
|
||||
const hasPermission = routePermissions.some((permission) => {
|
||||
// 支持通配符权限 *:*:*
|
||||
if (permissions.includes('*:*:*')) {
|
||||
return true
|
||||
}
|
||||
// 精确匹配或前缀匹配
|
||||
return permissions.some((userPermission) => {
|
||||
if (userPermission.endsWith('*')) {
|
||||
const prefix = userPermission.slice(0, -1)
|
||||
@@ -79,6 +102,25 @@ export const hasRoutePermission = (
|
||||
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 是否有效
|
||||
* 简单检查,真实项目中应该验证 JWT 或者调用后端接口
|
||||
|
||||
@@ -306,32 +306,32 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
keepAlive: 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',
|
||||
// name: 'Nested',
|
||||
// component: '',
|
||||
@@ -741,6 +741,26 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/shop-management',
|
||||
name: 'ShopManagement',
|
||||
component: RoutesAlias.Home,
|
||||
meta: {
|
||||
title: 'menus.product.shop',
|
||||
icon: ''
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'Shop',
|
||||
component: RoutesAlias.Shop,
|
||||
meta: {
|
||||
title: 'menus.product.shop',
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/account-management',
|
||||
name: 'AccountManagement',
|
||||
@@ -816,71 +836,71 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/product',
|
||||
name: 'Product',
|
||||
component: RoutesAlias.Home,
|
||||
meta: {
|
||||
title: 'menus.product.title',
|
||||
icon: ''
|
||||
},
|
||||
children: [
|
||||
// {
|
||||
// path: 'sim-card',
|
||||
// name: 'SimCardManagement',
|
||||
// component: RoutesAlias.SimCardManagement,
|
||||
// meta: {
|
||||
// title: 'menus.product.simCard',
|
||||
// keepAlive: true
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// path: 'sim-card-assign',
|
||||
// name: 'SimCardAssign',
|
||||
// component: RoutesAlias.SimCardAssign,
|
||||
// meta: {
|
||||
// title: 'menus.product.simCardAssign',
|
||||
// keepAlive: true
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// path: 'package-series',
|
||||
// name: 'PackageSeries',
|
||||
// component: RoutesAlias.PackageSeries,
|
||||
// meta: {
|
||||
// title: 'menus.product.packageSeries',
|
||||
// keepAlive: true
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// path: 'package-list',
|
||||
// name: 'PackageList',
|
||||
// component: RoutesAlias.PackageList,
|
||||
// meta: {
|
||||
// title: 'menus.product.packageList',
|
||||
// keepAlive: true
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// path: 'package-assign',
|
||||
// name: 'PackageAssign',
|
||||
// component: RoutesAlias.PackageAssign,
|
||||
// meta: {
|
||||
// title: 'menus.product.packageAssign',
|
||||
// keepAlive: true
|
||||
// }
|
||||
// },
|
||||
{
|
||||
path: 'shop',
|
||||
name: 'Shop',
|
||||
component: RoutesAlias.Shop,
|
||||
meta: {
|
||||
title: 'menus.product.shop',
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// {
|
||||
// path: '/product',
|
||||
// name: 'Product',
|
||||
// component: RoutesAlias.Home,
|
||||
// meta: {
|
||||
// title: 'menus.product.title',
|
||||
// icon: ''
|
||||
// },
|
||||
// children: [
|
||||
// // {
|
||||
// // path: 'sim-card',
|
||||
// // name: 'SimCardManagement',
|
||||
// // component: RoutesAlias.SimCardManagement,
|
||||
// // meta: {
|
||||
// // title: 'menus.product.simCard',
|
||||
// // keepAlive: true
|
||||
// // }
|
||||
// // },
|
||||
// // {
|
||||
// // path: 'sim-card-assign',
|
||||
// // name: 'SimCardAssign',
|
||||
// // component: RoutesAlias.SimCardAssign,
|
||||
// // meta: {
|
||||
// // title: 'menus.product.simCardAssign',
|
||||
// // keepAlive: true
|
||||
// // }
|
||||
// // },
|
||||
// // {
|
||||
// // path: 'package-series',
|
||||
// // name: 'PackageSeries',
|
||||
// // component: RoutesAlias.PackageSeries,
|
||||
// // meta: {
|
||||
// // title: 'menus.product.packageSeries',
|
||||
// // keepAlive: true
|
||||
// // }
|
||||
// // },
|
||||
// // {
|
||||
// // path: 'package-list',
|
||||
// // name: 'PackageList',
|
||||
// // component: RoutesAlias.PackageList,
|
||||
// // meta: {
|
||||
// // title: 'menus.product.packageList',
|
||||
// // keepAlive: true
|
||||
// // }
|
||||
// // },
|
||||
// // {
|
||||
// // path: 'package-assign',
|
||||
// // name: 'PackageAssign',
|
||||
// // component: RoutesAlias.PackageAssign,
|
||||
// // meta: {
|
||||
// // title: 'menus.product.packageAssign',
|
||||
// // keepAlive: true
|
||||
// // }
|
||||
// // },
|
||||
// {
|
||||
// path: 'shop',
|
||||
// name: 'Shop',
|
||||
// component: RoutesAlias.Shop,
|
||||
// meta: {
|
||||
// title: 'menus.product.shop',
|
||||
// keepAlive: true
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
{
|
||||
path: '/asset-management',
|
||||
name: 'AssetManagement',
|
||||
|
||||
@@ -24,11 +24,15 @@ export const useUserStore = defineStore(
|
||||
const accessToken = ref('')
|
||||
const refreshToken = ref('')
|
||||
const permissions = ref<string[]>([])
|
||||
const menus = ref<any[]>([])
|
||||
const buttons = ref<string[]>([])
|
||||
|
||||
const getUserInfo = computed(() => info.value)
|
||||
const getSettingState = computed(() => useSettingStore().$state)
|
||||
const getWorktabState = computed(() => useWorktabStore().$state)
|
||||
const getPermissions = computed(() => permissions.value)
|
||||
const getMenus = computed(() => menus.value)
|
||||
const getButtons = computed(() => buttons.value)
|
||||
|
||||
const setUserInfo = (newInfo: UserInfo) => {
|
||||
info.value = newInfo
|
||||
@@ -38,6 +42,14 @@ export const useUserStore = defineStore(
|
||||
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)
|
||||
|
||||
@@ -62,6 +74,13 @@ export const useUserStore = defineStore(
|
||||
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) => {
|
||||
isLogin.value = status
|
||||
}
|
||||
@@ -105,6 +124,8 @@ export const useUserStore = defineStore(
|
||||
accessToken.value = ''
|
||||
refreshToken.value = ''
|
||||
permissions.value = []
|
||||
menus.value = []
|
||||
buttons.value = []
|
||||
useWorktabStore().opened = []
|
||||
sessionStorage.removeItem('iframeRoutes')
|
||||
resetRouterState(router)
|
||||
@@ -122,16 +143,23 @@ export const useUserStore = defineStore(
|
||||
accessToken,
|
||||
refreshToken,
|
||||
permissions,
|
||||
menus,
|
||||
buttons,
|
||||
getUserInfo,
|
||||
getSettingState,
|
||||
getWorktabState,
|
||||
getPermissions,
|
||||
getMenus,
|
||||
getButtons,
|
||||
isSuperAdmin,
|
||||
setUserInfo,
|
||||
setPermissions,
|
||||
setMenus,
|
||||
setButtons,
|
||||
hasPermission,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
hasButton,
|
||||
setLoginStatus,
|
||||
setLanguage,
|
||||
setSearchHistory,
|
||||
@@ -145,8 +173,18 @@ export const useUserStore = defineStore(
|
||||
persist: {
|
||||
key: 'user',
|
||||
storage: localStorage,
|
||||
// 只持久化 token 和登录状态,用户信息每次刷新都从接口获取
|
||||
paths: ['accessToken', 'refreshToken', 'isLogin', 'language', 'isLock', 'lockPassword']
|
||||
paths: [
|
||||
'accessToken',
|
||||
'refreshToken',
|
||||
'isLogin',
|
||||
'language',
|
||||
'isLock',
|
||||
'lockPassword',
|
||||
'info',
|
||||
'permissions',
|
||||
'menus',
|
||||
'buttons'
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
607
src/types/auto-imports.d.ts
vendored
607
src/types/auto-imports.d.ts
vendored
@@ -6,7 +6,7 @@
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: (typeof import('vue'))['EffectScope']
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const ElButton: (typeof import('element-plus/es'))['ElButton']
|
||||
const ElMessage: (typeof import('element-plus/es'))['ElMessage']
|
||||
const ElMessageBox: (typeof import('element-plus/es'))['ElMessageBox']
|
||||
@@ -14,319 +14,304 @@ declare global {
|
||||
const ElPopconfirm: (typeof import('element-plus/es'))['ElPopconfirm']
|
||||
const ElPopover: (typeof import('element-plus/es'))['ElPopover']
|
||||
const ElTableColumn: (typeof import('element-plus/es'))['ElTableColumn']
|
||||
const ElTag: (typeof import('element-plus/es'))['ElTag']
|
||||
const acceptHMRUpdate: (typeof import('pinia'))['acceptHMRUpdate']
|
||||
const asyncComputed: (typeof import('@vueuse/core'))['asyncComputed']
|
||||
const autoResetRef: (typeof import('@vueuse/core'))['autoResetRef']
|
||||
const computed: (typeof import('vue'))['computed']
|
||||
const computedAsync: (typeof import('@vueuse/core'))['computedAsync']
|
||||
const computedEager: (typeof import('@vueuse/core'))['computedEager']
|
||||
const computedInject: (typeof import('@vueuse/core'))['computedInject']
|
||||
const computedWithControl: (typeof import('@vueuse/core'))['computedWithControl']
|
||||
const controlledComputed: (typeof import('@vueuse/core'))['controlledComputed']
|
||||
const controlledRef: (typeof import('@vueuse/core'))['controlledRef']
|
||||
const createApp: (typeof import('vue'))['createApp']
|
||||
const createEventHook: (typeof import('@vueuse/core'))['createEventHook']
|
||||
const createGlobalState: (typeof import('@vueuse/core'))['createGlobalState']
|
||||
const createInjectionState: (typeof import('@vueuse/core'))['createInjectionState']
|
||||
const createPinia: (typeof import('pinia'))['createPinia']
|
||||
const createReactiveFn: (typeof import('@vueuse/core'))['createReactiveFn']
|
||||
const createReusableTemplate: (typeof import('@vueuse/core'))['createReusableTemplate']
|
||||
const createSharedComposable: (typeof import('@vueuse/core'))['createSharedComposable']
|
||||
const createTemplatePromise: (typeof import('@vueuse/core'))['createTemplatePromise']
|
||||
const createUnrefFn: (typeof import('@vueuse/core'))['createUnrefFn']
|
||||
const customRef: (typeof import('vue'))['customRef']
|
||||
const debouncedRef: (typeof import('@vueuse/core'))['debouncedRef']
|
||||
const debouncedWatch: (typeof import('@vueuse/core'))['debouncedWatch']
|
||||
const defineAsyncComponent: (typeof import('vue'))['defineAsyncComponent']
|
||||
const defineComponent: (typeof import('vue'))['defineComponent']
|
||||
const defineStore: (typeof import('pinia'))['defineStore']
|
||||
const eagerComputed: (typeof import('@vueuse/core'))['eagerComputed']
|
||||
const effectScope: (typeof import('vue'))['effectScope']
|
||||
const extendRef: (typeof import('@vueuse/core'))['extendRef']
|
||||
const getActivePinia: (typeof import('pinia'))['getActivePinia']
|
||||
const getCurrentInstance: (typeof import('vue'))['getCurrentInstance']
|
||||
const getCurrentScope: (typeof import('vue'))['getCurrentScope']
|
||||
const h: (typeof import('vue'))['h']
|
||||
const ignorableWatch: (typeof import('@vueuse/core'))['ignorableWatch']
|
||||
const inject: (typeof import('vue'))['inject']
|
||||
const injectLocal: (typeof import('@vueuse/core'))['injectLocal']
|
||||
const isDefined: (typeof import('@vueuse/core'))['isDefined']
|
||||
const isProxy: (typeof import('vue'))['isProxy']
|
||||
const isReactive: (typeof import('vue'))['isReactive']
|
||||
const isReadonly: (typeof import('vue'))['isReadonly']
|
||||
const isRef: (typeof import('vue'))['isRef']
|
||||
const makeDestructurable: (typeof import('@vueuse/core'))['makeDestructurable']
|
||||
const mapActions: (typeof import('pinia'))['mapActions']
|
||||
const mapGetters: (typeof import('pinia'))['mapGetters']
|
||||
const mapState: (typeof import('pinia'))['mapState']
|
||||
const mapStores: (typeof import('pinia'))['mapStores']
|
||||
const mapWritableState: (typeof import('pinia'))['mapWritableState']
|
||||
const markRaw: (typeof import('vue'))['markRaw']
|
||||
const nextTick: (typeof import('vue'))['nextTick']
|
||||
const onActivated: (typeof import('vue'))['onActivated']
|
||||
const onBeforeMount: (typeof import('vue'))['onBeforeMount']
|
||||
const onBeforeRouteLeave: (typeof import('vue-router'))['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: (typeof import('vue-router'))['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: (typeof import('vue'))['onBeforeUnmount']
|
||||
const onBeforeUpdate: (typeof import('vue'))['onBeforeUpdate']
|
||||
const onClickOutside: (typeof import('@vueuse/core'))['onClickOutside']
|
||||
const onDeactivated: (typeof import('vue'))['onDeactivated']
|
||||
const onErrorCaptured: (typeof import('vue'))['onErrorCaptured']
|
||||
const onKeyStroke: (typeof import('@vueuse/core'))['onKeyStroke']
|
||||
const onLongPress: (typeof import('@vueuse/core'))['onLongPress']
|
||||
const onMounted: (typeof import('vue'))['onMounted']
|
||||
const onRenderTracked: (typeof import('vue'))['onRenderTracked']
|
||||
const onRenderTriggered: (typeof import('vue'))['onRenderTriggered']
|
||||
const onScopeDispose: (typeof import('vue'))['onScopeDispose']
|
||||
const onServerPrefetch: (typeof import('vue'))['onServerPrefetch']
|
||||
const onStartTyping: (typeof import('@vueuse/core'))['onStartTyping']
|
||||
const onUnmounted: (typeof import('vue'))['onUnmounted']
|
||||
const onUpdated: (typeof import('vue'))['onUpdated']
|
||||
const onWatcherCleanup: (typeof import('vue'))['onWatcherCleanup']
|
||||
const pausableWatch: (typeof import('@vueuse/core'))['pausableWatch']
|
||||
const provide: (typeof import('vue'))['provide']
|
||||
const provideLocal: (typeof import('@vueuse/core'))['provideLocal']
|
||||
const reactify: (typeof import('@vueuse/core'))['reactify']
|
||||
const reactifyObject: (typeof import('@vueuse/core'))['reactifyObject']
|
||||
const reactive: (typeof import('vue'))['reactive']
|
||||
const reactiveComputed: (typeof import('@vueuse/core'))['reactiveComputed']
|
||||
const reactiveOmit: (typeof import('@vueuse/core'))['reactiveOmit']
|
||||
const reactivePick: (typeof import('@vueuse/core'))['reactivePick']
|
||||
const readonly: (typeof import('vue'))['readonly']
|
||||
const ref: (typeof import('vue'))['ref']
|
||||
const refAutoReset: (typeof import('@vueuse/core'))['refAutoReset']
|
||||
const refDebounced: (typeof import('@vueuse/core'))['refDebounced']
|
||||
const refDefault: (typeof import('@vueuse/core'))['refDefault']
|
||||
const refThrottled: (typeof import('@vueuse/core'))['refThrottled']
|
||||
const refWithControl: (typeof import('@vueuse/core'))['refWithControl']
|
||||
const resolveComponent: (typeof import('vue'))['resolveComponent']
|
||||
const resolveRef: (typeof import('@vueuse/core'))['resolveRef']
|
||||
const resolveUnref: (typeof import('@vueuse/core'))['resolveUnref']
|
||||
const setActivePinia: (typeof import('pinia'))['setActivePinia']
|
||||
const setMapStoreSuffix: (typeof import('pinia'))['setMapStoreSuffix']
|
||||
const shallowReactive: (typeof import('vue'))['shallowReactive']
|
||||
const shallowReadonly: (typeof import('vue'))['shallowReadonly']
|
||||
const shallowRef: (typeof import('vue'))['shallowRef']
|
||||
const storeToRefs: (typeof import('pinia'))['storeToRefs']
|
||||
const syncRef: (typeof import('@vueuse/core'))['syncRef']
|
||||
const syncRefs: (typeof import('@vueuse/core'))['syncRefs']
|
||||
const templateRef: (typeof import('@vueuse/core'))['templateRef']
|
||||
const throttledRef: (typeof import('@vueuse/core'))['throttledRef']
|
||||
const throttledWatch: (typeof import('@vueuse/core'))['throttledWatch']
|
||||
const toRaw: (typeof import('vue'))['toRaw']
|
||||
const toReactive: (typeof import('@vueuse/core'))['toReactive']
|
||||
const toRef: (typeof import('vue'))['toRef']
|
||||
const toRefs: (typeof import('vue'))['toRefs']
|
||||
const toValue: (typeof import('vue'))['toValue']
|
||||
const triggerRef: (typeof import('vue'))['triggerRef']
|
||||
const tryOnBeforeMount: (typeof import('@vueuse/core'))['tryOnBeforeMount']
|
||||
const tryOnBeforeUnmount: (typeof import('@vueuse/core'))['tryOnBeforeUnmount']
|
||||
const tryOnMounted: (typeof import('@vueuse/core'))['tryOnMounted']
|
||||
const tryOnScopeDispose: (typeof import('@vueuse/core'))['tryOnScopeDispose']
|
||||
const tryOnUnmounted: (typeof import('@vueuse/core'))['tryOnUnmounted']
|
||||
const unref: (typeof import('vue'))['unref']
|
||||
const unrefElement: (typeof import('@vueuse/core'))['unrefElement']
|
||||
const until: (typeof import('@vueuse/core'))['until']
|
||||
const useActiveElement: (typeof import('@vueuse/core'))['useActiveElement']
|
||||
const useAnimate: (typeof import('@vueuse/core'))['useAnimate']
|
||||
const useArrayDifference: (typeof import('@vueuse/core'))['useArrayDifference']
|
||||
const useArrayEvery: (typeof import('@vueuse/core'))['useArrayEvery']
|
||||
const useArrayFilter: (typeof import('@vueuse/core'))['useArrayFilter']
|
||||
const useArrayFind: (typeof import('@vueuse/core'))['useArrayFind']
|
||||
const useArrayFindIndex: (typeof import('@vueuse/core'))['useArrayFindIndex']
|
||||
const useArrayFindLast: (typeof import('@vueuse/core'))['useArrayFindLast']
|
||||
const useArrayIncludes: (typeof import('@vueuse/core'))['useArrayIncludes']
|
||||
const useArrayJoin: (typeof import('@vueuse/core'))['useArrayJoin']
|
||||
const useArrayMap: (typeof import('@vueuse/core'))['useArrayMap']
|
||||
const useArrayReduce: (typeof import('@vueuse/core'))['useArrayReduce']
|
||||
const useArraySome: (typeof import('@vueuse/core'))['useArraySome']
|
||||
const useArrayUnique: (typeof import('@vueuse/core'))['useArrayUnique']
|
||||
const useAsyncQueue: (typeof import('@vueuse/core'))['useAsyncQueue']
|
||||
const useAsyncState: (typeof import('@vueuse/core'))['useAsyncState']
|
||||
const useAttrs: (typeof import('vue'))['useAttrs']
|
||||
const useBase64: (typeof import('@vueuse/core'))['useBase64']
|
||||
const useBattery: (typeof import('@vueuse/core'))['useBattery']
|
||||
const useBluetooth: (typeof import('@vueuse/core'))['useBluetooth']
|
||||
const useBreakpoints: (typeof import('@vueuse/core'))['useBreakpoints']
|
||||
const useBroadcastChannel: (typeof import('@vueuse/core'))['useBroadcastChannel']
|
||||
const useBrowserLocation: (typeof import('@vueuse/core'))['useBrowserLocation']
|
||||
const useCached: (typeof import('@vueuse/core'))['useCached']
|
||||
const useClipboard: (typeof import('@vueuse/core'))['useClipboard']
|
||||
const useClipboardItems: (typeof import('@vueuse/core'))['useClipboardItems']
|
||||
const useCloned: (typeof import('@vueuse/core'))['useCloned']
|
||||
const useColorMode: (typeof import('@vueuse/core'))['useColorMode']
|
||||
const useConfirmDialog: (typeof import('@vueuse/core'))['useConfirmDialog']
|
||||
const useCounter: (typeof import('@vueuse/core'))['useCounter']
|
||||
const useCssModule: (typeof import('vue'))['useCssModule']
|
||||
const useCssVar: (typeof import('@vueuse/core'))['useCssVar']
|
||||
const useCssVars: (typeof import('vue'))['useCssVars']
|
||||
const useCurrentElement: (typeof import('@vueuse/core'))['useCurrentElement']
|
||||
const useCycleList: (typeof import('@vueuse/core'))['useCycleList']
|
||||
const useDark: (typeof import('@vueuse/core'))['useDark']
|
||||
const useDateFormat: (typeof import('@vueuse/core'))['useDateFormat']
|
||||
const useDebounce: (typeof import('@vueuse/core'))['useDebounce']
|
||||
const useDebounceFn: (typeof import('@vueuse/core'))['useDebounceFn']
|
||||
const useDebouncedRefHistory: (typeof import('@vueuse/core'))['useDebouncedRefHistory']
|
||||
const useDeviceMotion: (typeof import('@vueuse/core'))['useDeviceMotion']
|
||||
const useDeviceOrientation: (typeof import('@vueuse/core'))['useDeviceOrientation']
|
||||
const useDevicePixelRatio: (typeof import('@vueuse/core'))['useDevicePixelRatio']
|
||||
const useDevicesList: (typeof import('@vueuse/core'))['useDevicesList']
|
||||
const useDisplayMedia: (typeof import('@vueuse/core'))['useDisplayMedia']
|
||||
const useDocumentVisibility: (typeof import('@vueuse/core'))['useDocumentVisibility']
|
||||
const useDraggable: (typeof import('@vueuse/core'))['useDraggable']
|
||||
const useDropZone: (typeof import('@vueuse/core'))['useDropZone']
|
||||
const useElementBounding: (typeof import('@vueuse/core'))['useElementBounding']
|
||||
const useElementByPoint: (typeof import('@vueuse/core'))['useElementByPoint']
|
||||
const useElementHover: (typeof import('@vueuse/core'))['useElementHover']
|
||||
const useElementSize: (typeof import('@vueuse/core'))['useElementSize']
|
||||
const useElementVisibility: (typeof import('@vueuse/core'))['useElementVisibility']
|
||||
const useEventBus: (typeof import('@vueuse/core'))['useEventBus']
|
||||
const useEventListener: (typeof import('@vueuse/core'))['useEventListener']
|
||||
const useEventSource: (typeof import('@vueuse/core'))['useEventSource']
|
||||
const useEyeDropper: (typeof import('@vueuse/core'))['useEyeDropper']
|
||||
const useFavicon: (typeof import('@vueuse/core'))['useFavicon']
|
||||
const useFetch: (typeof import('@vueuse/core'))['useFetch']
|
||||
const useFileDialog: (typeof import('@vueuse/core'))['useFileDialog']
|
||||
const useFileSystemAccess: (typeof import('@vueuse/core'))['useFileSystemAccess']
|
||||
const useFocus: (typeof import('@vueuse/core'))['useFocus']
|
||||
const useFocusWithin: (typeof import('@vueuse/core'))['useFocusWithin']
|
||||
const useFps: (typeof import('@vueuse/core'))['useFps']
|
||||
const useFullscreen: (typeof import('@vueuse/core'))['useFullscreen']
|
||||
const useGamepad: (typeof import('@vueuse/core'))['useGamepad']
|
||||
const useGeolocation: (typeof import('@vueuse/core'))['useGeolocation']
|
||||
const useId: (typeof import('vue'))['useId']
|
||||
const useIdle: (typeof import('@vueuse/core'))['useIdle']
|
||||
const useImage: (typeof import('@vueuse/core'))['useImage']
|
||||
const useInfiniteScroll: (typeof import('@vueuse/core'))['useInfiniteScroll']
|
||||
const useIntersectionObserver: (typeof import('@vueuse/core'))['useIntersectionObserver']
|
||||
const useInterval: (typeof import('@vueuse/core'))['useInterval']
|
||||
const useIntervalFn: (typeof import('@vueuse/core'))['useIntervalFn']
|
||||
const useKeyModifier: (typeof import('@vueuse/core'))['useKeyModifier']
|
||||
const useLastChanged: (typeof import('@vueuse/core'))['useLastChanged']
|
||||
const useLink: (typeof import('vue-router'))['useLink']
|
||||
const useLocalStorage: (typeof import('@vueuse/core'))['useLocalStorage']
|
||||
const useMagicKeys: (typeof import('@vueuse/core'))['useMagicKeys']
|
||||
const useManualRefHistory: (typeof import('@vueuse/core'))['useManualRefHistory']
|
||||
const useMediaControls: (typeof import('@vueuse/core'))['useMediaControls']
|
||||
const useMediaQuery: (typeof import('@vueuse/core'))['useMediaQuery']
|
||||
const useMemoize: (typeof import('@vueuse/core'))['useMemoize']
|
||||
const useMemory: (typeof import('@vueuse/core'))['useMemory']
|
||||
const useModel: (typeof import('vue'))['useModel']
|
||||
const useMounted: (typeof import('@vueuse/core'))['useMounted']
|
||||
const useMouse: (typeof import('@vueuse/core'))['useMouse']
|
||||
const useMouseInElement: (typeof import('@vueuse/core'))['useMouseInElement']
|
||||
const useMousePressed: (typeof import('@vueuse/core'))['useMousePressed']
|
||||
const useMutationObserver: (typeof import('@vueuse/core'))['useMutationObserver']
|
||||
const useNavigatorLanguage: (typeof import('@vueuse/core'))['useNavigatorLanguage']
|
||||
const useNetwork: (typeof import('@vueuse/core'))['useNetwork']
|
||||
const useNow: (typeof import('@vueuse/core'))['useNow']
|
||||
const useObjectUrl: (typeof import('@vueuse/core'))['useObjectUrl']
|
||||
const useOffsetPagination: (typeof import('@vueuse/core'))['useOffsetPagination']
|
||||
const useOnline: (typeof import('@vueuse/core'))['useOnline']
|
||||
const usePageLeave: (typeof import('@vueuse/core'))['usePageLeave']
|
||||
const useParallax: (typeof import('@vueuse/core'))['useParallax']
|
||||
const useParentElement: (typeof import('@vueuse/core'))['useParentElement']
|
||||
const usePerformanceObserver: (typeof import('@vueuse/core'))['usePerformanceObserver']
|
||||
const usePermission: (typeof import('@vueuse/core'))['usePermission']
|
||||
const usePointer: (typeof import('@vueuse/core'))['usePointer']
|
||||
const usePointerLock: (typeof import('@vueuse/core'))['usePointerLock']
|
||||
const usePointerSwipe: (typeof import('@vueuse/core'))['usePointerSwipe']
|
||||
const usePreferredColorScheme: (typeof import('@vueuse/core'))['usePreferredColorScheme']
|
||||
const usePreferredContrast: (typeof import('@vueuse/core'))['usePreferredContrast']
|
||||
const usePreferredDark: (typeof import('@vueuse/core'))['usePreferredDark']
|
||||
const usePreferredLanguages: (typeof import('@vueuse/core'))['usePreferredLanguages']
|
||||
const usePreferredReducedMotion: (typeof import('@vueuse/core'))['usePreferredReducedMotion']
|
||||
const usePrevious: (typeof import('@vueuse/core'))['usePrevious']
|
||||
const useRafFn: (typeof import('@vueuse/core'))['useRafFn']
|
||||
const useRefHistory: (typeof import('@vueuse/core'))['useRefHistory']
|
||||
const useResizeObserver: (typeof import('@vueuse/core'))['useResizeObserver']
|
||||
const useRoute: (typeof import('vue-router'))['useRoute']
|
||||
const useRouter: (typeof import('vue-router'))['useRouter']
|
||||
const useScreenOrientation: (typeof import('@vueuse/core'))['useScreenOrientation']
|
||||
const useScreenSafeArea: (typeof import('@vueuse/core'))['useScreenSafeArea']
|
||||
const useScriptTag: (typeof import('@vueuse/core'))['useScriptTag']
|
||||
const useScroll: (typeof import('@vueuse/core'))['useScroll']
|
||||
const useScrollLock: (typeof import('@vueuse/core'))['useScrollLock']
|
||||
const useSessionStorage: (typeof import('@vueuse/core'))['useSessionStorage']
|
||||
const useShare: (typeof import('@vueuse/core'))['useShare']
|
||||
const useSlots: (typeof import('vue'))['useSlots']
|
||||
const useSorted: (typeof import('@vueuse/core'))['useSorted']
|
||||
const useSpeechRecognition: (typeof import('@vueuse/core'))['useSpeechRecognition']
|
||||
const useSpeechSynthesis: (typeof import('@vueuse/core'))['useSpeechSynthesis']
|
||||
const useStepper: (typeof import('@vueuse/core'))['useStepper']
|
||||
const useStorage: (typeof import('@vueuse/core'))['useStorage']
|
||||
const useStorageAsync: (typeof import('@vueuse/core'))['useStorageAsync']
|
||||
const useStyleTag: (typeof import('@vueuse/core'))['useStyleTag']
|
||||
const useSupported: (typeof import('@vueuse/core'))['useSupported']
|
||||
const useSwipe: (typeof import('@vueuse/core'))['useSwipe']
|
||||
const useTemplateRef: (typeof import('vue'))['useTemplateRef']
|
||||
const useTemplateRefsList: (typeof import('@vueuse/core'))['useTemplateRefsList']
|
||||
const useTextDirection: (typeof import('@vueuse/core'))['useTextDirection']
|
||||
const useTextSelection: (typeof import('@vueuse/core'))['useTextSelection']
|
||||
const useTextareaAutosize: (typeof import('@vueuse/core'))['useTextareaAutosize']
|
||||
const useThrottle: (typeof import('@vueuse/core'))['useThrottle']
|
||||
const useThrottleFn: (typeof import('@vueuse/core'))['useThrottleFn']
|
||||
const useThrottledRefHistory: (typeof import('@vueuse/core'))['useThrottledRefHistory']
|
||||
const useTimeAgo: (typeof import('@vueuse/core'))['useTimeAgo']
|
||||
const useTimeout: (typeof import('@vueuse/core'))['useTimeout']
|
||||
const useTimeoutFn: (typeof import('@vueuse/core'))['useTimeoutFn']
|
||||
const useTimeoutPoll: (typeof import('@vueuse/core'))['useTimeoutPoll']
|
||||
const useTimestamp: (typeof import('@vueuse/core'))['useTimestamp']
|
||||
const useTitle: (typeof import('@vueuse/core'))['useTitle']
|
||||
const useToNumber: (typeof import('@vueuse/core'))['useToNumber']
|
||||
const useToString: (typeof import('@vueuse/core'))['useToString']
|
||||
const useToggle: (typeof import('@vueuse/core'))['useToggle']
|
||||
const useTransition: (typeof import('@vueuse/core'))['useTransition']
|
||||
const useUrlSearchParams: (typeof import('@vueuse/core'))['useUrlSearchParams']
|
||||
const useUserMedia: (typeof import('@vueuse/core'))['useUserMedia']
|
||||
const useVModel: (typeof import('@vueuse/core'))['useVModel']
|
||||
const useVModels: (typeof import('@vueuse/core'))['useVModels']
|
||||
const useVibrate: (typeof import('@vueuse/core'))['useVibrate']
|
||||
const useVirtualList: (typeof import('@vueuse/core'))['useVirtualList']
|
||||
const useWakeLock: (typeof import('@vueuse/core'))['useWakeLock']
|
||||
const useWebNotification: (typeof import('@vueuse/core'))['useWebNotification']
|
||||
const useWebSocket: (typeof import('@vueuse/core'))['useWebSocket']
|
||||
const useWebWorker: (typeof import('@vueuse/core'))['useWebWorker']
|
||||
const useWebWorkerFn: (typeof import('@vueuse/core'))['useWebWorkerFn']
|
||||
const useWindowFocus: (typeof import('@vueuse/core'))['useWindowFocus']
|
||||
const useWindowScroll: (typeof import('@vueuse/core'))['useWindowScroll']
|
||||
const useWindowSize: (typeof import('@vueuse/core'))['useWindowSize']
|
||||
const watch: (typeof import('vue'))['watch']
|
||||
const watchArray: (typeof import('@vueuse/core'))['watchArray']
|
||||
const watchAtMost: (typeof import('@vueuse/core'))['watchAtMost']
|
||||
const watchDebounced: (typeof import('@vueuse/core'))['watchDebounced']
|
||||
const watchDeep: (typeof import('@vueuse/core'))['watchDeep']
|
||||
const watchEffect: (typeof import('vue'))['watchEffect']
|
||||
const watchIgnorable: (typeof import('@vueuse/core'))['watchIgnorable']
|
||||
const watchImmediate: (typeof import('@vueuse/core'))['watchImmediate']
|
||||
const watchOnce: (typeof import('@vueuse/core'))['watchOnce']
|
||||
const watchPausable: (typeof import('@vueuse/core'))['watchPausable']
|
||||
const watchPostEffect: (typeof import('vue'))['watchPostEffect']
|
||||
const watchSyncEffect: (typeof import('vue'))['watchSyncEffect']
|
||||
const watchThrottled: (typeof import('@vueuse/core'))['watchThrottled']
|
||||
const watchTriggerable: (typeof import('@vueuse/core'))['watchTriggerable']
|
||||
const watchWithFilter: (typeof import('@vueuse/core'))['watchWithFilter']
|
||||
const whenever: (typeof import('@vueuse/core'))['whenever']
|
||||
const ElTag: typeof import('element-plus/es')['ElTag']
|
||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||
const computedEager: typeof import('@vueuse/core')['computedEager']
|
||||
const computedInject: typeof import('@vueuse/core')['computedInject']
|
||||
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
|
||||
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
|
||||
const controlledRef: typeof import('@vueuse/core')['controlledRef']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const createEventHook: typeof import('@vueuse/core')['createEventHook']
|
||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||
const createPinia: typeof import('pinia')['createPinia']
|
||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
|
||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
|
||||
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
||||
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const defineStore: typeof import('pinia')['defineStore']
|
||||
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const injectLocal: typeof import('@vueuse/core')['injectLocal']
|
||||
const isDefined: typeof import('@vueuse/core')['isDefined']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||
const mapActions: typeof import('pinia')['mapActions']
|
||||
const mapGetters: typeof import('pinia')['mapGetters']
|
||||
const mapState: typeof import('pinia')['mapState']
|
||||
const mapStores: typeof import('pinia')['mapStores']
|
||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
||||
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const provideLocal: typeof import('@vueuse/core')['provideLocal']
|
||||
const reactify: typeof import('@vueuse/core')['reactify']
|
||||
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
|
||||
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
|
||||
const reactivePick: typeof import('@vueuse/core')['reactivePick']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
|
||||
const refDebounced: typeof import('@vueuse/core')['refDebounced']
|
||||
const refDefault: typeof import('@vueuse/core')['refDefault']
|
||||
const refThrottled: typeof import('@vueuse/core')['refThrottled']
|
||||
const refWithControl: typeof import('@vueuse/core')['refWithControl']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||
const syncRef: typeof import('@vueuse/core')['syncRef']
|
||||
const syncRefs: typeof import('@vueuse/core')['syncRefs']
|
||||
const templateRef: typeof import('@vueuse/core')['templateRef']
|
||||
const throttledRef: typeof import('@vueuse/core')['throttledRef']
|
||||
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toReactive: typeof import('@vueuse/core')['toReactive']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
||||
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
||||
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
||||
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
||||
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
||||
const until: typeof import('@vueuse/core')['until']
|
||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||
const useAnimate: typeof import('@vueuse/core')['useAnimate']
|
||||
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
|
||||
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
|
||||
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
|
||||
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
|
||||
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
|
||||
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
|
||||
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
|
||||
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
|
||||
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
|
||||
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
|
||||
const useArraySome: typeof import('@vueuse/core')['useArraySome']
|
||||
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
|
||||
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useBase64: typeof import('@vueuse/core')['useBase64']
|
||||
const useBattery: typeof import('@vueuse/core')['useBattery']
|
||||
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
|
||||
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
|
||||
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
||||
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
||||
const useCached: typeof import('@vueuse/core')['useCached']
|
||||
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
||||
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
|
||||
const useCloned: typeof import('@vueuse/core')['useCloned']
|
||||
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
|
||||
const useCycleList: typeof import('@vueuse/core')['useCycleList']
|
||||
const useDark: typeof import('@vueuse/core')['useDark']
|
||||
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
|
||||
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
||||
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
||||
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
||||
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
|
||||
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
||||
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
||||
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
||||
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
|
||||
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
||||
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
||||
const useDropZone: typeof import('@vueuse/core')['useDropZone']
|
||||
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
|
||||
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
|
||||
const useElementHover: typeof import('@vueuse/core')['useElementHover']
|
||||
const useElementSize: typeof import('@vueuse/core')['useElementSize']
|
||||
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
|
||||
const useEventBus: typeof import('@vueuse/core')['useEventBus']
|
||||
const useEventListener: typeof import('@vueuse/core')['useEventListener']
|
||||
const useEventSource: typeof import('@vueuse/core')['useEventSource']
|
||||
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
|
||||
const useFavicon: typeof import('@vueuse/core')['useFavicon']
|
||||
const useFetch: typeof import('@vueuse/core')['useFetch']
|
||||
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
|
||||
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
|
||||
const useFocus: typeof import('@vueuse/core')['useFocus']
|
||||
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
|
||||
const useFps: typeof import('@vueuse/core')['useFps']
|
||||
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
||||
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
||||
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useIdle: typeof import('@vueuse/core')['useIdle']
|
||||
const useImage: typeof import('@vueuse/core')['useImage']
|
||||
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
||||
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
|
||||
const useInterval: typeof import('@vueuse/core')['useInterval']
|
||||
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
|
||||
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
|
||||
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
||||
const useLink: typeof import('vue-router')['useLink']
|
||||
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
||||
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
||||
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
|
||||
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
|
||||
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
|
||||
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
||||
const useMemory: typeof import('@vueuse/core')['useMemory']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const useMounted: typeof import('@vueuse/core')['useMounted']
|
||||
const useMouse: typeof import('@vueuse/core')['useMouse']
|
||||
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
||||
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
|
||||
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
|
||||
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
|
||||
const useNetwork: typeof import('@vueuse/core')['useNetwork']
|
||||
const useNow: typeof import('@vueuse/core')['useNow']
|
||||
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
|
||||
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
|
||||
const useOnline: typeof import('@vueuse/core')['useOnline']
|
||||
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
||||
const useParallax: typeof import('@vueuse/core')['useParallax']
|
||||
const useParentElement: typeof import('@vueuse/core')['useParentElement']
|
||||
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
|
||||
const usePermission: typeof import('@vueuse/core')['usePermission']
|
||||
const usePointer: typeof import('@vueuse/core')['usePointer']
|
||||
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
|
||||
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
||||
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
||||
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
|
||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
||||
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
||||
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
||||
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||
const useScroll: typeof import('@vueuse/core')['useScroll']
|
||||
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
|
||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||
const useShare: typeof import('@vueuse/core')['useShare']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useSorted: typeof import('@vueuse/core')['useSorted']
|
||||
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
||||
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
||||
const useStepper: typeof import('@vueuse/core')['useStepper']
|
||||
const useStorage: typeof import('@vueuse/core')['useStorage']
|
||||
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
||||
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
||||
const useSupported: typeof import('@vueuse/core')['useSupported']
|
||||
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
||||
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
|
||||
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
||||
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
|
||||
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
||||
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
||||
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
|
||||
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
||||
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
||||
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
||||
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
||||
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
||||
const useTitle: typeof import('@vueuse/core')['useTitle']
|
||||
const useToNumber: typeof import('@vueuse/core')['useToNumber']
|
||||
const useToString: typeof import('@vueuse/core')['useToString']
|
||||
const useToggle: typeof import('@vueuse/core')['useToggle']
|
||||
const useTransition: typeof import('@vueuse/core')['useTransition']
|
||||
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
||||
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
||||
const useVModel: typeof import('@vueuse/core')['useVModel']
|
||||
const useVModels: typeof import('@vueuse/core')['useVModels']
|
||||
const useVibrate: typeof import('@vueuse/core')['useVibrate']
|
||||
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
|
||||
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
|
||||
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
|
||||
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
|
||||
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
|
||||
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
|
||||
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
|
||||
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
|
||||
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchArray: typeof import('@vueuse/core')['watchArray']
|
||||
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
||||
const watchDeep: typeof import('@vueuse/core')['watchDeep']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
||||
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
|
||||
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
|
||||
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
|
||||
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||
const whenever: typeof import('@vueuse/core')['whenever']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type {
|
||||
Component,
|
||||
ComponentPublicInstance,
|
||||
ComputedRef,
|
||||
DirectiveBinding,
|
||||
ExtractDefaultPropTypes,
|
||||
ExtractPropTypes,
|
||||
ExtractPublicPropTypes,
|
||||
InjectionKey,
|
||||
PropType,
|
||||
Ref,
|
||||
MaybeRef,
|
||||
MaybeRefOrGetter,
|
||||
VNode,
|
||||
WritableComputedRef
|
||||
} from 'vue'
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
<ElButton type="info" @click="handleBatchSetSeries" :disabled="!selectedDevices.length">
|
||||
批量设置套餐系列
|
||||
</ElButton>
|
||||
<ElButton @click="handleImportDevice">导入设备</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
@@ -300,22 +299,13 @@
|
||||
{{ currentDeviceDetail.status_name }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="店铺名称">{{
|
||||
currentDeviceDetail.shop_name || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="批次号">{{
|
||||
<ElDescriptionsItem label="批次号" :span="2">{{
|
||||
currentDeviceDetail.batch_no || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="激活时间">{{
|
||||
currentDeviceDetail.activated_at || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="创建时间">{{
|
||||
<ElDescriptionsItem label="创建时间" :span="3">{{
|
||||
currentDeviceDetail.created_at || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="更新时间">{{
|
||||
currentDeviceDetail.updated_at || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
@@ -385,12 +375,9 @@
|
||||
device_no: '',
|
||||
device_name: '',
|
||||
status: undefined as DeviceStatus | undefined,
|
||||
shop_id: undefined as number | undefined,
|
||||
batch_no: '',
|
||||
device_type: '',
|
||||
manufacturer: '',
|
||||
created_at_start: '',
|
||||
created_at_end: ''
|
||||
manufacturer: ''
|
||||
}
|
||||
|
||||
// 搜索表单
|
||||
@@ -478,9 +465,7 @@
|
||||
{ label: '最大插槽数', prop: 'max_sim_slots' },
|
||||
{ label: '已绑定卡数', prop: 'bound_card_count' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '店铺', prop: 'shop_name' },
|
||||
{ label: '批次号', prop: 'batch_no' },
|
||||
{ label: '激活时间', prop: 'activated_at' },
|
||||
{ label: '创建时间', prop: 'created_at' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
@@ -610,24 +595,12 @@
|
||||
return h(ElTag, { type: status.type }, () => status.text)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'shop_name',
|
||||
label: '店铺',
|
||||
minWidth: 120,
|
||||
formatter: (row: Device) => row.shop_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'batch_no',
|
||||
label: '批次号',
|
||||
minWidth: 120,
|
||||
minWidth: 160,
|
||||
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',
|
||||
label: '创建时间',
|
||||
@@ -637,14 +610,10 @@
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 150,
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
formatter: (row: Device) => {
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
||||
h(ArtButtonTable, {
|
||||
type: 'view',
|
||||
onClick: () => viewDeviceDetail(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => deleteDevice(row)
|
||||
@@ -681,16 +650,13 @@
|
||||
device_no: searchForm.device_no || undefined,
|
||||
device_name: searchForm.device_name || undefined,
|
||||
status: searchForm.status,
|
||||
shop_id: searchForm.shop_id,
|
||||
batch_no: searchForm.batch_no || undefined,
|
||||
device_type: searchForm.device_type || undefined,
|
||||
manufacturer: searchForm.manufacturer || undefined,
|
||||
created_at_start: searchForm.created_at_start || undefined,
|
||||
created_at_end: searchForm.created_at_end || undefined
|
||||
manufacturer: searchForm.manufacturer || undefined
|
||||
}
|
||||
const res = await DeviceService.getDevices(params)
|
||||
if (res.code === 0 && res.data) {
|
||||
deviceList.value = res.data.list || []
|
||||
deviceList.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -735,14 +701,6 @@
|
||||
selectedDevices.value = selection
|
||||
}
|
||||
|
||||
// 查看设备详情
|
||||
const viewDeviceDetail = (row: Device) => {
|
||||
router.push({
|
||||
path: '/asset-management/device-detail',
|
||||
query: { id: row.id }
|
||||
})
|
||||
}
|
||||
|
||||
// 删除设备
|
||||
const deleteDevice = (row: Device) => {
|
||||
ElMessageBox.confirm(`确定删除设备 ${row.device_no} 吗?删除后将自动解绑所有卡。`, '删除确认', {
|
||||
@@ -869,11 +827,6 @@
|
||||
recallForm.remark = ''
|
||||
}
|
||||
|
||||
// 导入设备
|
||||
const handleImportDevice = () => {
|
||||
router.push('/batch/device-import')
|
||||
}
|
||||
|
||||
// 批量设置套餐系列
|
||||
const handleBatchSetSeries = async () => {
|
||||
if (selectedDevices.value.length === 0) {
|
||||
|
||||
@@ -50,17 +50,11 @@
|
||||
<template #title>
|
||||
<div style="line-height: 1.8">
|
||||
<p><strong>导入说明:</strong></p>
|
||||
<p>1. 请先下载 CSV 模板文件,按照模板格式填写设备信息</p>
|
||||
<p>2. 支持 CSV 格式(.csv),单次最多导入 1000 条</p>
|
||||
<p>3. CSV 文件编码:UTF-8(推荐)或 GBK</p>
|
||||
<p
|
||||
>4.
|
||||
必填字段:device_no(设备号)、device_name(设备名称)、device_model(设备型号)</p
|
||||
>
|
||||
<p
|
||||
>5.
|
||||
可选字段:device_type(设备类型)、manufacturer(制造商)、max_sim_slots(最大插槽数,默认1)</p
|
||||
>
|
||||
<p>1. 请先下载 Excel 模板文件,按照模板格式填写设备信息</p>
|
||||
<p>2. 仅支持 Excel 格式(.xlsx),单次最多导入 1000 条</p>
|
||||
<p>3. 列格式请设置为文本格式,避免长数字被转为科学计数法</p>
|
||||
<p>4. 必填列:device_no(设备号)、device_name(设备名称)、device_model(设备型号)、device_type(设备类型)</p>
|
||||
<p>5. 可选列:manufacturer(制造商)、max_sim_slots(最大插槽数,默认4)、iccid_1 ~ iccid_4(绑定的卡ICCID)</p>
|
||||
</div>
|
||||
</template>
|
||||
</ElAlert>
|
||||
@@ -77,12 +71,12 @@
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:limit="1"
|
||||
accept=".csv"
|
||||
accept=".xlsx"
|
||||
>
|
||||
<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>
|
||||
<div class="el-upload__tip">只能上传 CSV 文件,且不超过 10MB</div>
|
||||
<div class="el-upload__tip">只能上传 .xlsx 格式的 Excel 文件,且不超过 10MB</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
|
||||
@@ -131,18 +125,51 @@
|
||||
}}</ElDescriptionsItem>
|
||||
</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>
|
||||
<div
|
||||
v-if="currentDetail.failed_items && currentDetail.failed_items.length"
|
||||
style="max-height: 300px; overflow-y: auto"
|
||||
>
|
||||
<ElTable :data="currentDetail.failed_items" border size="small">
|
||||
<ElTableColumn label="行号" type="index" width="80" :index="(index) => index + 1" />
|
||||
<ElTableColumn label="设备编号" prop="device_no" width="150" />
|
||||
<ElTableColumn label="ICCID" prop="iccid" width="200" />
|
||||
<ElTableColumn label="失败原因" prop="reason" min-width="200">
|
||||
<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.error || '未知错误' }}
|
||||
{{ row.reason || '未知错误' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
@@ -151,6 +178,22 @@
|
||||
|
||||
<template #footer>
|
||||
<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
|
||||
v-if="currentDetail.fail_count > 0"
|
||||
type="primary"
|
||||
@@ -475,29 +518,78 @@
|
||||
}
|
||||
|
||||
// 下载模板
|
||||
const downloadTemplate = () => {
|
||||
// 使用 \t 前缀防止 Excel 将长数字转换为科学计数法
|
||||
const csvContent = [
|
||||
'device_no,device_name,device_model,device_type,manufacturer,max_sim_slots',
|
||||
'\t862639070731999,智能水表01,WM-2000,智能水表,华为,1',
|
||||
'\t862639070750932,GPS定位器01,GPS-3000,定位设备,小米,2',
|
||||
'\t862639070801875,智能燃气表01,GM-1500,智能燃气表,海尔,1'
|
||||
].join('\n')
|
||||
const downloadTemplate = async () => {
|
||||
try {
|
||||
// 动态导入 xlsx 库
|
||||
const XLSX = await import('xlsx')
|
||||
|
||||
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)
|
||||
link.setAttribute('href', url)
|
||||
link.setAttribute('download', '设备导入模板.csv')
|
||||
link.style.visibility = 'hidden'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
// 创建工作簿
|
||||
const wb = XLSX.utils.book_new()
|
||||
const ws = XLSX.utils.json_to_sheet(templateData)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if (uploadFile.raw && !uploadFile.raw.name.endsWith('.csv')) {
|
||||
ElMessage.error('只能上传 CSV 文件')
|
||||
if (uploadFile.raw && !uploadFile.raw.name.endsWith('.xlsx')) {
|
||||
ElMessage.error('只能上传 .xlsx 格式的 Excel 文件')
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
return
|
||||
@@ -535,7 +627,7 @@
|
||||
// 提交上传
|
||||
const submitUpload = async () => {
|
||||
if (!fileList.value.length) {
|
||||
ElMessage.warning('请先选择CSV文件')
|
||||
ElMessage.warning('请先选择 Excel 文件')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -546,7 +638,7 @@
|
||||
ElMessage.info('正在准备上传...')
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: 'text/csv',
|
||||
content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
purpose: 'iot_import'
|
||||
})
|
||||
|
||||
@@ -557,7 +649,7 @@
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
|
||||
ElMessage.info('正在上传文件...')
|
||||
await StorageService.uploadFile(upload_url, file, 'text/csv')
|
||||
await StorageService.uploadFile(upload_url, file, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||
|
||||
ElMessage.info('正在创建导入任务...')
|
||||
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 = () => {
|
||||
downloadFailDataFromDetail(currentDetail.value, currentDetail.value.batch_no)
|
||||
@@ -609,11 +789,10 @@
|
||||
// 下载失败数据的通用方法
|
||||
const downloadFailDataFromDetail = (detail: any, batchNo: string) => {
|
||||
const failReasons =
|
||||
detail.failed_items?.map((item: any, index: number) => ({
|
||||
row: index + 1,
|
||||
deviceCode: item.device_no || '-',
|
||||
iccid: item.iccid || '-',
|
||||
message: item.reason || item.error || '未知错误'
|
||||
detail.failed_items?.map((item: any) => ({
|
||||
line: item.line || '-',
|
||||
deviceNo: item.device_no || '-',
|
||||
message: item.reason || '未知错误'
|
||||
})) || []
|
||||
|
||||
if (failReasons.length === 0) {
|
||||
@@ -621,11 +800,11 @@
|
||||
return
|
||||
}
|
||||
|
||||
const headers = ['行号', '设备编号', 'ICCID', '失败原因']
|
||||
const headers = ['行号', '设备编号', '失败原因']
|
||||
const csvRows = [
|
||||
headers.join(','),
|
||||
...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')
|
||||
|
||||
@@ -33,18 +33,7 @@
|
||||
>
|
||||
批量回收
|
||||
</ElButton>
|
||||
<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>
|
||||
<ElButton type="info" @contextmenu.prevent="showMoreMenu">更多操作</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
@@ -501,6 +490,14 @@
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 更多操作右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="moreMenuRef"
|
||||
:menu-items="moreMenuItems"
|
||||
:menu-width="180"
|
||||
@select="handleMoreMenuSelect"
|
||||
/>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
@@ -517,6 +514,8 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import type {
|
||||
StandaloneIotCard,
|
||||
StandaloneCardStatus,
|
||||
@@ -578,6 +577,9 @@
|
||||
const cardDetailLoading = ref(false)
|
||||
const currentCardDetail = ref<any>(null)
|
||||
|
||||
// 更多操作右键菜单
|
||||
const moreMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
status: undefined,
|
||||
@@ -1423,6 +1425,72 @@
|
||||
})
|
||||
}
|
||||
|
||||
// 更多操作菜单项配置
|
||||
const moreMenuItems = computed((): MenuItemType[] => [
|
||||
{
|
||||
key: 'seriesBinding',
|
||||
label: '批量设置套餐系列',
|
||||
icon: '',
|
||||
disabled: selectedCards.value.length === 0
|
||||
},
|
||||
{
|
||||
key: 'distribution',
|
||||
label: '网卡分销',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
key: 'recharge',
|
||||
label: '批量充值',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
key: 'recycle',
|
||||
label: '网卡回收',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
key: 'download',
|
||||
label: '批量下载',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
key: 'changePackage',
|
||||
label: '变更套餐',
|
||||
icon: ''
|
||||
}
|
||||
])
|
||||
|
||||
// 显示更多操作菜单
|
||||
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 = () => {
|
||||
ElMessage.info('功能正在开发中')
|
||||
|
||||
@@ -50,10 +50,10 @@
|
||||
<template #title>
|
||||
<div style="line-height: 1.8">
|
||||
<p><strong>导入说明:</strong></p>
|
||||
<p>1. 请先下载 CSV 模板文件,按照模板格式填写IoT卡信息</p>
|
||||
<p>2. 支持 CSV 格式(.csv),单次最多导入 1000 条</p>
|
||||
<p>3. CSV 文件编码:UTF-8(推荐)或 GBK</p>
|
||||
<p>4. 必填字段:iccid(ICCID)、msisdn(MSISDN/手机号)</p>
|
||||
<p>1. 请先下载 Excel 模板文件,按照模板格式填写IoT卡信息</p>
|
||||
<p>2. 仅支持 Excel 格式(.xlsx),单次最多导入 1000 条</p>
|
||||
<p>3. 列格式请设置为文本格式,避免长数字被转为科学计数法</p>
|
||||
<p>4. 必填字段:ICCID、MSISDN(手机号)</p>
|
||||
<p>5. 必须选择运营商</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -79,12 +79,12 @@
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:limit="1"
|
||||
accept=".csv"
|
||||
accept=".xlsx"
|
||||
>
|
||||
<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>
|
||||
<div class="el-upload__tip">只能上传 CSV 文件,且不超过 10MB</div>
|
||||
<div class="el-upload__tip">只能上传 .xlsx 格式的 Excel 文件,且不超过 10MB</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
|
||||
@@ -133,13 +133,31 @@
|
||||
}}</ElDescriptionsItem>
|
||||
</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>
|
||||
<div
|
||||
v-if="currentDetail.failed_items && currentDetail.failed_items.length"
|
||||
style="max-height: 300px; overflow-y: auto"
|
||||
>
|
||||
<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="MSISDN" prop="msisdn" width="150" />
|
||||
<ElTableColumn label="失败原因" prop="reason" min-width="200">
|
||||
@@ -153,6 +171,14 @@
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||||
<ElButton
|
||||
v-if="currentDetail.skip_count > 0"
|
||||
type="warning"
|
||||
:icon="Download"
|
||||
@click="downloadSkippedData"
|
||||
>
|
||||
下载跳过数据
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="currentDetail.fail_count > 0"
|
||||
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 = () => {
|
||||
downloadFailDataFromDetail(currentDetail.value, currentDetail.value.task_no)
|
||||
@@ -521,8 +592,8 @@
|
||||
// 下载失败数据的通用方法
|
||||
const downloadFailDataFromDetail = (detail: any, taskNo: string) => {
|
||||
const failReasons =
|
||||
detail.failed_items?.map((item: any, index: number) => ({
|
||||
row: index + 1,
|
||||
detail.failed_items?.map((item: any) => ({
|
||||
line: item.line || '-',
|
||||
iccid: item.iccid || '-',
|
||||
msisdn: item.msisdn || '-',
|
||||
message: item.reason || item.error || '未知错误'
|
||||
@@ -537,7 +608,7 @@
|
||||
const csvRows = [
|
||||
headers.join(','),
|
||||
...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')
|
||||
@@ -559,29 +630,58 @@
|
||||
}
|
||||
|
||||
// 下载模板
|
||||
const downloadTemplate = () => {
|
||||
// 使用 \t 前缀防止 Excel 将长数字转换为科学计数法
|
||||
const csvContent = [
|
||||
'iccid,msisdn',
|
||||
'\t89860123456789012345,\t13800138000',
|
||||
'\t89860123456789012346,\t13800138001',
|
||||
'\t89860123456789012347,\t13800138002'
|
||||
].join('\n')
|
||||
const downloadTemplate = async () => {
|
||||
try {
|
||||
// 动态导入 xlsx 库
|
||||
const XLSX = await import('xlsx')
|
||||
|
||||
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)
|
||||
link.setAttribute('href', url)
|
||||
link.setAttribute('download', 'IoT卡导入模板.csv')
|
||||
link.style.visibility = 'hidden'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
// 创建工作簿
|
||||
const wb = XLSX.utils.book_new()
|
||||
const ws = XLSX.utils.json_to_sheet(templateData)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if (uploadFile.raw && !uploadFile.raw.name.endsWith('.csv')) {
|
||||
ElMessage.error('只能上传 CSV 文件')
|
||||
if (uploadFile.raw && !uploadFile.raw.name.endsWith('.xlsx')) {
|
||||
ElMessage.error('只能上传 .xlsx 格式的 Excel 文件')
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
return
|
||||
@@ -625,7 +725,7 @@
|
||||
}
|
||||
|
||||
if (!fileList.value.length) {
|
||||
ElMessage.warning('请先选择CSV文件')
|
||||
ElMessage.warning('请先选择 Excel 文件')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -636,7 +736,7 @@
|
||||
ElMessage.info('正在准备上传...')
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: 'text/csv',
|
||||
content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
purpose: 'iot_import'
|
||||
})
|
||||
|
||||
@@ -647,7 +747,7 @@
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
|
||||
ElMessage.info('正在上传文件...')
|
||||
await StorageService.uploadFile(upload_url, file, 'text/csv')
|
||||
await StorageService.uploadFile(upload_url, file, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||
|
||||
ElMessage.info('正在创建导入任务...')
|
||||
const importRes = await CardService.importIotCards({
|
||||
|
||||
@@ -290,7 +290,6 @@
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '店铺ID', prop: 'shop_id' },
|
||||
{ label: '店铺编码', prop: 'shop_code' },
|
||||
{ label: '店铺名称', prop: 'shop_name' },
|
||||
{ label: '用户名', prop: 'username' },
|
||||
@@ -306,11 +305,6 @@
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'shop_id',
|
||||
label: '店铺ID',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'shop_code',
|
||||
label: '店铺编码',
|
||||
|
||||
@@ -74,11 +74,31 @@
|
||||
:label="t('orderManagement.createForm.iotCardId')"
|
||||
prop="iot_card_id"
|
||||
>
|
||||
<ElInputNumber
|
||||
<ElSelect
|
||||
v-model="createForm.iot_card_id"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:placeholder="t('orderManagement.createForm.iotCardIdPlaceholder')"
|
||||
:remote-method="searchIotCards"
|
||||
:loading="cardSearchLoading"
|
||||
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
|
||||
v-if="createForm.order_type === 'device'"
|
||||
@@ -200,7 +220,7 @@
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { OrderService } from '@/api/modules'
|
||||
import { OrderService, CardService } from '@/api/modules'
|
||||
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type {
|
||||
@@ -211,7 +231,8 @@
|
||||
OrderType,
|
||||
BuyerType,
|
||||
OrderPaymentMethod,
|
||||
OrderCommissionStatus
|
||||
OrderCommissionStatus,
|
||||
StandaloneIotCard
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
@@ -342,6 +363,35 @@
|
||||
|
||||
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 => {
|
||||
return `¥${(amount / 100).toFixed(2)}`
|
||||
@@ -554,8 +604,29 @@
|
||||
}
|
||||
|
||||
// 显示创建订单对话框
|
||||
const showCreateDialog = () => {
|
||||
const showCreateDialog = async () => {
|
||||
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.iot_card_id = null
|
||||
createForm.device_id = null
|
||||
|
||||
// 清空IoT卡搜索结果
|
||||
iotCardOptions.value = []
|
||||
}
|
||||
|
||||
// 创建订单
|
||||
|
||||
@@ -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 () => {
|
||||
try {
|
||||
const response = await PermissionService.getPermissions(searchForm)
|
||||
if (response.code === 0) {
|
||||
permissionList.value = response.data.items || []
|
||||
const flatData = response.data.items || []
|
||||
// 将扁平数据转换为树形结构
|
||||
permissionList.value = buildTreeData(flatData)
|
||||
// 构建权限树选项
|
||||
buildPermissionTreeOptions()
|
||||
}
|
||||
@@ -332,7 +371,7 @@
|
||||
return list.map((item) => ({
|
||||
value: item.ID,
|
||||
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)
|
||||
|
||||
@@ -89,24 +89,28 @@
|
||||
|
||||
<!-- 分配权限对话框 -->
|
||||
<ElDialog v-model="permissionDialogVisible" title="分配权限" width="500px">
|
||||
<ElCheckboxGroup v-model="selectedPermissions">
|
||||
<div
|
||||
v-for="permission in allPermissions"
|
||||
:key="permission.ID"
|
||||
style="margin-bottom: 12px"
|
||||
>
|
||||
<ElCheckbox :label="permission.ID">
|
||||
{{ permission.perm_name }}
|
||||
<ElTree
|
||||
ref="permissionTreeRef"
|
||||
:data="permissionTreeData"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:default-checked-keys="selectedPermissions"
|
||||
:props="{ children: 'children', label: 'label' }"
|
||||
: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
|
||||
:type="permission.perm_type === 1 ? 'info' : 'success'"
|
||||
:type="data.perm_type === 1 ? 'info' : 'success'"
|
||||
size="small"
|
||||
style="margin-left: 8px"
|
||||
>
|
||||
{{ permission.perm_type === 1 ? '菜单' : '按钮' }}
|
||||
{{ data.perm_type === 1 ? '菜单' : '按钮' }}
|
||||
</ElTag>
|
||||
</ElCheckbox>
|
||||
</div>
|
||||
</ElCheckboxGroup>
|
||||
</span>
|
||||
</template>
|
||||
</ElTree>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="permissionDialogVisible = false">取消</ElButton>
|
||||
@@ -132,30 +136,34 @@
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTag,
|
||||
ElCheckbox,
|
||||
ElCheckboxGroup,
|
||||
ElTree,
|
||||
ElSwitch
|
||||
} from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { PlatformRole, Permission } from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { CommonStatus, getStatusText } from '@/config/constants'
|
||||
|
||||
defineOptions({ name: 'Role' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const permissionDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const permissionSubmitLoading = ref(false)
|
||||
const tableRef = ref()
|
||||
const permissionTreeRef = ref()
|
||||
const currentRoleId = ref<number>(0)
|
||||
const selectedPermissions = ref<number[]>([])
|
||||
const originalPermissions = ref<number[]>([]) // 保存原始权限,用于对比
|
||||
const allPermissions = ref<Permission[]>([])
|
||||
const permissionTreeData = ref<any[]>([])
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
@@ -272,20 +280,39 @@
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
formatter: (row: any) => {
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
||||
h(ArtButtonTable, {
|
||||
icon: '',
|
||||
onClick: () => showPermissionDialog(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => deleteRole(row)
|
||||
})
|
||||
])
|
||||
const buttons = []
|
||||
|
||||
// 分配权限按钮
|
||||
if (hasAuth('role:permission')) {
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
icon: '',
|
||||
onClick: () => showPermissionDialog(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()
|
||||
})
|
||||
|
||||
// 将扁平数据转换为树形结构
|
||||
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 () => {
|
||||
try {
|
||||
const res = await PermissionService.getPermissions({ page: 1, page_size: 1000 })
|
||||
if (res.code === 0) {
|
||||
allPermissions.value = res.data.items || []
|
||||
// 构建树形数据
|
||||
permissionTreeData.value = buildTreeData(allPermissions.value)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取权限列表失败:', error)
|
||||
@@ -343,14 +417,21 @@
|
||||
|
||||
// 提交分配权限
|
||||
const handleAssignPermissions = async () => {
|
||||
if (!permissionTreeRef.value) return
|
||||
|
||||
permissionSubmitLoading.value = true
|
||||
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)
|
||||
)
|
||||
const removedPermissions = originalPermissions.value.filter(
|
||||
(id) => !selectedPermissions.value.includes(id)
|
||||
(id) => !currentPermissions.includes(id)
|
||||
)
|
||||
|
||||
// 使用 Promise.all 并发执行新增和移除操作
|
||||
@@ -532,3 +613,16 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user