fix: 设备分配代理,虚比例,sim顺序
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m15s

This commit is contained in:
sexygoat
2026-04-20 11:45:59 +08:00
parent c5ee690ccd
commit d721c4b8a9
8 changed files with 19 additions and 282 deletions

View File

@@ -5,26 +5,21 @@
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ElMessage, ElNotification } from 'element-plus'
import { ElNotification } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { useUserStore } from '@/store/modules/user'
import { HOME_PAGE } from '@/router/routesAlias'
import AppConfig from '@/config'
import { AuthService } from '@/api/modules'
import { ApiStatus } from '@/utils/http/status'
import { MOCK_ACCOUNTS, mockLogin, mockGetUserInfo, type MockAccount } from '@/mock/auth'
import { saveCredentials, getRememberedCredentials } from '@/utils/auth/rememberPassword'
import { usernameRules, passwordRules } from '@/utils/auth/loginValidation'
import { getRedirectPath } from '@/router/guards/permission'
// 开发模式:是否使用 Mock 数据
const USE_MOCK = false
/**
* 登录表单数据
*/
interface LoginForm {
account: string
username: string
password: string
rememberPassword: boolean
@@ -44,7 +39,6 @@ export function useLogin() {
// 表单数据
const formData = reactive<LoginForm>({
account: '',
username: '',
password: '',
rememberPassword: true
@@ -56,47 +50,19 @@ export function useLogin() {
password: passwordRules(t)
}))
// Mock 账号列表
const mockAccounts = computed<MockAccount[]>(() => MOCK_ACCOUNTS)
/**
* 初始化表单
*/
// 初始化表单
const initForm = () => {
// 尝试加载记住的密码
const remembered = getRememberedCredentials()
if (remembered) {
formData.username = remembered.username
formData.password = remembered.password
formData.rememberPassword = remembered.rememberPassword
// 如果使用 Mock尝试匹配 Mock 账号
if (USE_MOCK) {
const matchedAccount = MOCK_ACCOUNTS.find((acc) => acc.username === remembered.username)
if (matchedAccount) {
formData.account = matchedAccount.key
}
}
} else {
// 开发环境:使用真实 API 默认账号
if (import.meta.env.MODE === 'development') {
formData.username = 'admin'
formData.password = 'Admin@123456'
formData.rememberPassword = false
}
// 生产环境:不填充默认值
}
}
/**
* 设置 Mock 账号
*/
const setupAccount = (key: string) => {
const selectedAccount = MOCK_ACCOUNTS.find((account) => account.key === key)
if (selectedAccount) {
formData.account = key
formData.username = selectedAccount.username
formData.password = selectedAccount.password
}
}
@@ -112,13 +78,7 @@ export function useLogin() {
if (!valid) return
loading.value = true
// 判断使用 Mock 还是真实 API
if (USE_MOCK) {
await handleMockLogin()
} else {
await handleRealLogin()
}
await handleRealLogin()
} catch (error: any) {
console.error('登录失败:', error)
} finally {
@@ -127,44 +87,7 @@ export function useLogin() {
}
/**
* Mock 登录(开发测试用)
*/
const handleMockLogin = async () => {
const loginResult = mockLogin(formData.username, formData.password)
if (!loginResult) {
console.log(t('login.error.invalidCredentials'))
return
}
// 保存 Token
userStore.setToken(loginResult.access_token, loginResult.refresh_token)
// 获取用户信息
const userInfo = mockGetUserInfo(loginResult.access_token)
if (!userInfo) {
console.log(t('login.error.getUserInfoFailed'))
return
}
// 保存用户信息
userStore.setUserInfo(userInfo)
userStore.setLoginStatus(true)
// 保存记住密码
saveCredentials(formData.username, formData.password, formData.rememberPassword)
// 显示登录成功提示
showLoginSuccessNotice()
// 跳转到重定向页面或首页
const redirectPath = getRedirectPath(route)
await router.push(redirectPath || HOME_PAGE)
}
/**
* 真实 API 登录
* 登录
*/
const handleRealLogin = async () => {
// 调用登录接口
@@ -253,8 +176,6 @@ export function useLogin() {
formData,
rules,
loading,
mockAccounts,
setupAccount,
handleLogin
}
}

View File

@@ -1,132 +0,0 @@
/**
* 登录模块 Mock 数据
*/
import type { LoginData, UserInfo } from '@/types/api'
import { UserRole } from '@/types/api'
/**
* Mock 用户账号
*/
export interface MockAccount {
key: string
label: string
username: string
password: string
role: UserRole
userInfo: UserInfo
}
/**
* Mock 账号列表
*/
export const MOCK_ACCOUNTS: MockAccount[] = [
{
key: 'super',
label: '超级管理员',
username: 'super',
password: '123456',
role: UserRole.SUPER_ADMIN,
userInfo: {
id: 1,
username: 'Super',
phone: '13800138000',
user_type: 1,
user_type_name: '超级管理员'
}
},
{
key: 'admin',
label: '平台管理员',
username: 'admin',
password: '123456',
role: UserRole.ADMIN,
userInfo: {
id: 2,
username: 'admin',
phone: '13800138001',
user_type: 2,
user_type_name: '平台管理员'
}
},
{
key: 'agent',
label: '代理商',
username: 'agent',
password: '123456',
role: UserRole.AGENT,
userInfo: {
id: 3,
username: 'agent',
phone: '13800138002',
user_type: 3,
user_type_name: '代理商'
}
},
{
key: 'enterprise',
label: '企业客户',
username: 'enterprise',
password: '123456',
role: UserRole.ENTERPRISE,
userInfo: {
id: 4,
username: 'enterprise',
phone: '13800138003',
user_type: 4,
user_type_name: '企业客户'
}
}
]
/**
* Mock 登录响应
*/
export const mockLogin = (username: string, password: string): LoginData | null => {
const account = MOCK_ACCOUNTS.find(
(acc) => acc.username === username && acc.password === password
)
if (!account) {
return null
}
// 生成 Mock Token
const mockToken = `mock_token_${account.key}_${Date.now()}`
const mockRefreshToken = `mock_refresh_${account.key}_${Date.now()}`
return {
access_token: mockToken,
refresh_token: mockRefreshToken,
expires_in: 7200, // 2小时
user: account.userInfo
}
}
/**
* Mock 获取用户信息
*/
export const mockGetUserInfo = (token: string): UserInfo | null => {
// 从 token 中提取用户类型
const match = token.match(/mock_token_(\w+)_/)
if (!match) {
return null
}
const accountKey = match[1]
const account = MOCK_ACCOUNTS.find((acc) => acc.key === accountKey)
return account ? account.userInfo : null
}
/**
* 获取 Mock 账号选项(用于登录页下拉框)
*/
export const getMockAccountOptions = () => {
return MOCK_ACCOUNTS.map((acc) => ({
key: acc.key,
label: acc.label,
username: acc.username,
password: acc.password
}))
}

View File

@@ -171,23 +171,6 @@ function matchMenuPath(actualPath: string, menuPath: string): boolean {
*/
export const isTokenValid = (token: string): boolean => {
if (!token) return false
// Mock Token 格式: mock_token_{key}_{timestamp}
if (token.startsWith('mock_token_')) {
// Mock Token 永不过期(开发环境)
return true
}
// 真实 Token 可以在这里添加 JWT 解析和过期检查
// 例如:
// try {
// const decoded = jwt_decode(token)
// const isExpired = decoded.exp * 1000 < Date.now()
// return !isExpired
// } catch {
// return false
// }
return true
}

View File

@@ -22,7 +22,9 @@ export interface LoginData {
access_token: string
refresh_token: string
expires_in: number
permissions?: string[] | null
permissions: string[] | null
menus: any[] | null
buttons: string[] | null
user: UserInfo
}

View File

@@ -534,7 +534,7 @@
getSwitchModeName
} = useAssetFormatters(deviceRealtimeRef)
// 排序后的设备卡列表(当前卡排在最前面)
// 排序后的设备卡列表(按卡槽位置排序,当前卡排在最前面)
const sortedDeviceCards = computed(() => {
if (!props.cardInfo?.cards || props.cardInfo.cards.length === 0) {
return []
@@ -543,7 +543,7 @@
return cards.sort((a, b) => {
if (a.is_current && !b.is_current) return -1
if (!a.is_current && b.is_current) return 1
return 0
return (a.slot_position ?? 0) - (b.slot_position ?? 0)
})
})

View File

@@ -98,7 +98,7 @@
<ElDescriptionsItem label="套餐类型">
{{ getPackageTypeName(currentPackage.package_type) }}
</ElDescriptionsItem>
<ElDescriptionsItem v-if="currentPackage.enable_virtual_data" label="虚流量比例">
<ElDescriptionsItem v-if="currentPackage.enable_virtual_data && isSuperAdmin" label="虚流量比例">
{{ currentPackage.virtual_ratio || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem v-if="currentPackage.duration_days" label="套餐时长" :span="3">
@@ -134,6 +134,8 @@
ElAlert
} from 'element-plus'
import { useAssetFormatters } from '../composables/useAssetFormatters'
import { useUserStore } from '@/store/modules/user'
import { storeToRefs } from 'pinia'
import { formatDateTime } from '@/utils/business/format'
import type { PackageInfo } from '../types'
@@ -146,6 +148,10 @@
getProgressColorByPercentage
} = useAssetFormatters()
const userStore = useUserStore()
const { info: userInfo } = storeToRefs(userStore)
const isSuperAdmin = computed(() => userInfo.value.user_type === 1)
// Props
interface Props {
currentPackage: PackageInfo | null

View File

@@ -1365,7 +1365,7 @@
try {
const data = {
device_ids: selectedDevices.value.map((d) => d.id),
target_shop_id: allocateForm.target_shop_id!,
target_shop_id: allocateForm.target_shop_id![0],
remark: allocateForm.remark
}
const res = await DeviceService.allocateDevices(data)

View File

@@ -9,24 +9,6 @@
{{ isDark ? '&#xe6b5;' : '&#xe725;' }}
</i>
</div>
<ElDropdown @command="changeLanguage" popper-class="langDropDownStyle">
<!--<div class="btn language-btn">-->
<!-- <i class="iconfont-sys icon-language">&#xe611;</i>-->
<!--</div>-->
<!--<template #dropdown>-->
<!-- <ElDropdownMenu>-->
<!-- <div v-for="lang in languageOptions" :key="lang.value" class="lang-btn-item">-->
<!-- <ElDropdownItem-->
<!-- :command="lang.value"-->
<!-- :class="{ 'is-selected': locale === lang.value }"-->
<!-- >-->
<!-- <span class="menu-txt">{{ lang.label }}</span>-->
<!-- <i v-if="locale === lang.value" class="iconfont-sys icon-check">&#xe621;</i>-->
<!-- </ElDropdownItem>-->
<!-- </div>-->
<!-- </ElDropdownMenu>-->
<!--</template>-->
</ElDropdown>
</div>
<div class="header">
<ArtLogo class="icon" />
@@ -43,19 +25,6 @@
@keyup.enter="handleSubmit"
style="margin-top: 25px"
>
<!-- Mock 账号选择仅在开发模式且使用 Mock 时显示 -->
<ElFormItem prop="account" v-if="false">
<ElSelect v-model="formData.account" @change="setupAccount" class="account-select">
<ElOption
v-for="account in mockAccounts"
:key="account.key"
:label="account.label"
:value="account.key"
>
<span>{{ account.label }}</span>
</ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem prop="username">
<ElInput :placeholder="$t('login.placeholder[0]')" v-model.trim="formData.username" />
</ElFormItem>
@@ -74,7 +43,6 @@
<ElCheckbox v-model="formData.rememberPassword">{{
$t('login.rememberPwd')
}}</ElCheckbox>
<!--<RouterLink :to="RoutesAlias.ForgetPassword">{{ $t('login.forgetPwd') }}</RouterLink>-->
</div>
<div style="margin-top: 30px">
@@ -97,22 +65,18 @@
<script setup lang="ts">
import AppConfig from '@/config'
import { LanguageEnum, SystemThemeEnum } from '@/enums/appEnum'
import { useI18n } from 'vue-i18n'
import { SystemThemeEnum } from '@/enums/appEnum'
import { useSettingStore } from '@/store/modules/setting'
import { useUserStore } from '@/store/modules/user'
import { useTheme } from '@/composables/useTheme'
import { useLogin } from '@/composables/useLogin'
defineOptions({ name: 'Login' })
const { locale } = useI18n()
const settingStore = useSettingStore()
const userStore = useUserStore()
const { isDark, systemThemeType } = storeToRefs(settingStore)
// 使用登录 Composable
const { formRef, formData, rules, loading, mockAccounts, setupAccount, handleLogin } = useLogin()
const { formRef, formData, rules, loading, handleLogin } = useLogin()
const systemName = AppConfig.systemInfo.name
@@ -121,13 +85,6 @@
await handleLogin()
}
// 切换语言
const changeLanguage = (lang: LanguageEnum) => {
if (locale.value === lang) return
locale.value = lang
userStore.setLanguage(lang)
}
// 切换主题
const toggleTheme = () => {
let { LIGHT, DARK } = SystemThemeEnum