| | |
| | | "@vueuse/core": "^13.6.0", |
| | | "alova": "^3.3.4", |
| | | "colord": "^2.9.3", |
| | | "crypto-js": "^4.2.0", |
| | | "echarts": "^5.6.0", |
| | | "jsrsasign": "^11.1.0", |
| | | "md-editor-v3": "^5.6.1", |
| | | "pinia": "^3.0.3", |
| | | "pinia-plugin-persistedstate": "^4.4.1", |
| | | "pro-naive-ui": "^2.4.3", |
| | | "quill": "^2.0.3", |
| | | "radash": "^12.1.1", |
| | | "uuid": "^13.0.0", |
| | | "vue": "^3.5.18", |
| | | "vue-draggable-plus": "^0.6.0", |
| | | "vue-i18n": "^11.1.11", |
| | |
| | | export const serviceConfig: Record<ServiceEnvType, Record<string, string>> = { |
| | | dev: { |
| | | // url: 'http://localhost:3000', |
| | | url: 'https://mock.apifox.cn/m1/4071143-0-default', |
| | | url: 'http://192.168.110.85:8000', |
| | | }, |
| | | production: { |
| | | url: 'https://mock.apifox.cn/m1/4071143-0-default', |
| | |
| | | |
| | | if (!authStore.userInfo) |
| | | return false |
| | | |
| | | const { role } = authStore.userInfo |
| | | |
| | | // 如果用户没有角色信息,默认允许访问(临时方案) |
| | | if (!role || role.length === 0) |
| | | return true |
| | | |
| | | // 角色为super可直接通过 |
| | | let has = role.includes('super') |
| | | if (!has) { |
| New file |
| | |
| | | 加密:a.服务器生成 临时RSA 密钥对(公钥 + 私钥),并将临时公钥发送给客户端。其中公钥信息必须绑定会话 |
| | | ID,保证解密的时候能校验是同一个会话ID。 |
| | | 会话ID不一致则提示请求失败。临时公钥设定有效期,保证 |
| | | 公钥只能使用一次,不被重复使用。 |
| | | b.客户端随机生成一个 AES密钥(例如 256 位) |
| | | c.客户端用 AES 密钥加密敏感数据(明文参数 → AES 密文) |
| | | d.客户端用服务器的 RSA 公钥加密该 AES 密钥。 |
| | | e.客户端将以下两部分一起发送给服务器: |
| | | AES加密后的参数(密文) |
| | | ·RSA 加密后的 AES 密钥 |
| | | f.服务器用自己的 RSA 私钥解密出 AES 密钥。 |
| | | g.服务器用该 AES 密钥解密出原始参数, |
| | | h,AES算法使用CBC模式,RSA算法使用OAEP填充方式 |
| | | 所有接口请求需要对请求参数进行签名校验,签名校验算法需采用HMAC-SHA256。 |
| | | 将请求的非空参数进行ASCII字典排序 |
| | | 将排列好的参数进行拼接,key1=value1&key2=value2...×tamp=请求时间戳,其中敏感参数以密文拼接 |
| | | 对拼接后的结果进行HMAC-SHA256哈希签名计算 |
| | | 将HMAC-SHA256哈希结果进行Base64编码获取签名sign |
| | | 将签名后的结果只放入header中传给服务端 |
| | | |
| | | 发送请求 |
| | | Body:含加密后的敏感字段。 |
| | | Headers: |
| | | timestamp:Unix 时间戳(秒) |
| | | nonce:随机唯一字符串(Base64 或 UUID) |
| | | sign:签名值 |
| | | xps:使用RSA加密后的AES密钥 |
| | |
| | | await routeStore.initAuthRoute() |
| | | // 动态路由加载完回到根路由 |
| | | if (to.name === 'not-found') { |
| | | // 等待权限路由加载好了,回到之前的路由,否则404 |
| | | // 等待权限路由加载好了,回到之前的路由,否则404 |
| | | next({ |
| | | path: to.fullPath, |
| | | replace: true, |
| | |
| | | }) |
| | | return |
| | | } |
| | | // 如果不是404页面,继续导航 |
| | | next() |
| | | return |
| | | } |
| | | catch { |
| | | // 如果路由初始化失败(比如 401 错误),重定向到登录页 |
| | |
| | | import { request } from '../http' |
| | | import { createEncryptedRequest, request } from '../http' |
| | | |
| | | interface Ilogin { |
| | | userName: string |
| | | interface login { |
| | | username: string |
| | | password: string |
| | | } |
| | | |
| | | export function fetchLogin(data: Ilogin) { |
| | | const methodInstance = request.Post<Service.ResponseResult<Api.Login.Info>>('/login', data) |
| | | methodInstance.meta = { |
| | | authRole: null, |
| | | } |
| | | return methodInstance |
| | | export function fetchLoginEncrypted(data: login) { |
| | | return createEncryptedRequest<Api.Login.Info>(request, { |
| | | path: '/account/base/v1/login', |
| | | data, |
| | | options: { |
| | | sensitiveFields: [], |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | export function fetchUpdateToken(data: any) { |
| | | const method = request.Post<Service.ResponseResult<Api.Login.Info>>('/updateToken', data) |
| | | method.meta = { |
| | |
| | | handleResponseError, |
| | | handleServiceResult, |
| | | } from './handle' |
| | | import { |
| | | aesEncrypt, |
| | | generateAesKey, |
| | | generateHeaders, |
| | | generateSign, |
| | | generateSignKey, |
| | | getTimestamp, |
| | | rsaEncrypt, |
| | | } from '@/utils/crypto' |
| | | import { v4 as uuidv4 } from 'uuid' |
| | | |
| | | export interface EncryptedRequestOptions<T = any> { |
| | | sensitiveFields?: string[] |
| | | rsaPublicKey?: string |
| | | secretKey?: string |
| | | meta?: any |
| | | } |
| | | |
| | | export interface EncryptedRequestConfig { |
| | | path: string |
| | | data?: any |
| | | options?: EncryptedRequestOptions |
| | | } |
| | | |
| | | const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication<VueHookType>({ |
| | | // 服务端判定token过期 |
| | |
| | | method.config.headers['Content-Type'] = 'application/x-www-form-urlencoded' |
| | | method.data = new URLSearchParams(method.data as URLSearchParams).toString() |
| | | } |
| | | |
| | | // 加密和签名逻辑 |
| | | const timestamp = getTimestamp() |
| | | const rsaPublicKey = method.meta?.rsaPublicKey || '' // 从meta中获取RSA公钥 |
| | | const secretKey = method.meta?.secretKey || '' // 从meta中获取签名密钥 |
| | | const sensitiveFields = method.meta?.sensitiveFields || [] // 从meta中获取敏感字段 |
| | | console.log('secretKey', secretKey) |
| | | console.log('method.meta', method.meta, 'method.data', method.data) |
| | | if (secretKey) { |
| | | if (method.data && typeof method.data === 'object') { |
| | | // 分离敏感字段和非敏感字段 |
| | | const sensitiveData: Record<string, any> = {} |
| | | const normalData: Record<string, any> = {} |
| | | |
| | | Object.keys(method.data).forEach((key) => { |
| | | if (sensitiveFields.includes(key)) { |
| | | sensitiveData[key] = method.data[key] |
| | | } |
| | | else { |
| | | normalData[key] = method.data[key] |
| | | } |
| | | }) |
| | | |
| | | // 加密敏感数据 |
| | | if (Object.keys(sensitiveData).length > 0 && rsaPublicKey) { |
| | | const aesKey = generateAesKey() |
| | | const encryptedAesKey = rsaEncrypt(aesKey, rsaPublicKey) |
| | | |
| | | // 将所有敏感字段一起加密成一个密文 |
| | | const encryptedData = aesEncrypt(JSON.stringify(sensitiveData), aesKey) |
| | | |
| | | // 替换敏感字段为加密后的数据(保持字段名不变) |
| | | const encryptedParams: Record<string, any> = {} |
| | | Object.keys(sensitiveData).forEach((key) => { |
| | | encryptedParams[key] = encryptedData |
| | | }) |
| | | |
| | | method.data = { |
| | | ...normalData, |
| | | ...encryptedParams, |
| | | } |
| | | |
| | | // 生成签名,使用加密后的数据 |
| | | const signParams = { |
| | | ...normalData, |
| | | ...encryptedParams, |
| | | } |
| | | const sign = generateSign(signParams, secretKey, timestamp) |
| | | |
| | | // 添加请求头 |
| | | const headers = generateHeaders(sign, encryptedAesKey, timestamp) |
| | | Object.assign(method.config.headers, headers) |
| | | } |
| | | else { |
| | | // 所有接口都需要签名,即使没有敏感数据 |
| | | const sign = generateSign(method.data, secretKey, timestamp) |
| | | // 添加签名相关请求头 |
| | | const headers = { |
| | | timestamp: timestamp.toString(), |
| | | nonce: secretKey, |
| | | sign, |
| | | } |
| | | |
| | | Object.assign(method.config.headers, headers) |
| | | } |
| | | } |
| | | else { |
| | | // 没有传参数,但仍然需要添加时间戳和签名 |
| | | // 对于无参数的请求,默认传一个时间戳的参数 |
| | | const paramsWithTimestamp = { |
| | | timestamp: timestamp.toString(), |
| | | } |
| | | // 使用包含时间戳的对象生成签名 |
| | | const sign = generateSign(paramsWithTimestamp, secretKey, timestamp) |
| | | console.log('method', method) |
| | | // 将时间戳作为参数传递 |
| | | method.data = paramsWithTimestamp |
| | | |
| | | // 添加签名相关请求头 |
| | | const headers = { |
| | | timestamp: timestamp.toString(), |
| | | nonce: secretKey, |
| | | sign, |
| | | } |
| | | Object.assign(method.config.headers, headers) |
| | | } |
| | | } |
| | | |
| | | alovaConfig.beforeRequest?.(method) |
| | | }), |
| | | responded: onResponseRefreshToken({ |
| | |
| | | }), |
| | | }) |
| | | } |
| | | |
| | | export async function createEncryptedRequest<T = any>( |
| | | requestInstance: any, |
| | | config: EncryptedRequestConfig, |
| | | ): Promise<Service.ResponseResult<T>> { |
| | | const { path, data, options = {} } = config |
| | | const { sensitiveFields = [], rsaPublicKey: providedPublicKey, secretKey: providedSecretKey, meta = {} } = options |
| | | |
| | | let rsaPublicKey = providedPublicKey |
| | | let secretKey = providedSecretKey |
| | | |
| | | if (sensitiveFields.length > 0 && !rsaPublicKey) { |
| | | secretKey = generateSignKey() |
| | | const rsaResult = await requestInstance.Post<Service.ResponseResult<string>>('/account/base/v1/getRsaPubKey', {}).send() |
| | | |
| | | if (!rsaResult.isSuccess || !rsaResult.data) { |
| | | throw new Error('获取RSA公钥失败') |
| | | } |
| | | |
| | | rsaPublicKey = rsaResult.data |
| | | } |
| | | |
| | | if (!secretKey) { |
| | | secretKey = generateSignKey() |
| | | } |
| | | |
| | | const methodInstance = requestInstance.Post<Service.ResponseResult<T>>(path, data || {}) |
| | | methodInstance.meta = { |
| | | ...meta, |
| | | rsaPublicKey, |
| | | secretKey, |
| | | sensitiveFields, |
| | | } |
| | | |
| | | return methodInstance.send() |
| | | } |
| | |
| | | export const DEFAULT_BACKEND_OPTIONS = { |
| | | codeKey: 'code', |
| | | dataKey: 'data', |
| | | msgKey: 'message', |
| | | msgKey: 'msg', |
| | | successCode: 200, |
| | | } |
| | | |
| | |
| | | export const blankInstance = createAlovaInstance({ |
| | | baseURL: '', |
| | | }) |
| | | |
| | | export { createEncryptedRequest } from './alova' |
| | |
| | | import { router } from '@/router' |
| | | import { fetchLogin } from '@/service' |
| | | import { fetchLoginEncrypted } from '@/service' |
| | | import { local } from '@/utils' |
| | | import { useRouteStore } from './router' |
| | | import { useTabStore } from './tab' |
| | | |
| | | interface AuthStatus { |
| | | userInfo: Api.Login.Info | null |
| | | userInfo: Entity.User | null |
| | | token: string |
| | | } |
| | | export const useAuthStore = defineStore('auth-store', { |
| | |
| | | local.remove('userInfo') |
| | | }, |
| | | |
| | | /* 用户登录 */ |
| | | async login(userName: string, password: string) { |
| | | async loginEncrypted(userName: string, password: string) { |
| | | try { |
| | | const { isSuccess, data } = await fetchLogin({ userName, password }) |
| | | const { isSuccess, data } = await fetchLoginEncrypted({ username: userName, password }) |
| | | if (!isSuccess) |
| | | return |
| | | |
| | | // 处理登录信息 |
| | | await this.handleLoginInfo(data) |
| | | } |
| | | catch (e) { |
| | | console.warn('[Login Error]:', e) |
| | | window.$message?.error('登录失败,请重试') |
| | | } |
| | | }, |
| | | |
| | | /* 处理登录返回的数据 */ |
| | | async handleLoginInfo(data: Api.Login.Info) { |
| | | // 将token和userInfo保存下来 |
| | | local.set('userInfo', data) |
| | | local.set('accessToken', data.accessToken) |
| | | local.set('refreshToken', data.refreshToken) |
| | | this.token = data.accessToken |
| | | this.userInfo = data |
| | | local.set('userInfo', data.info.sysUser) |
| | | local.set('accessToken', data.token.access_token) |
| | | this.token = data.token.access_token |
| | | this.userInfo = data.info.sysUser |
| | | |
| | | // 添加路由和菜单 |
| | | const routeStore = useRouteStore() |
| | |
| | | /// <reference path="../global.d.ts"/> |
| | | |
| | | import { info } from 'node:console' |
| | | |
| | | namespace Api { |
| | | namespace Login { |
| | | /* 登录返回的用户字段, 该数据是根据用户表扩展而来, 部分字段可能需要覆盖,例如id */ |
| | |
| | | /** 用户角色类型 */ |
| | | role: Entity.RoleType[] |
| | | /** 访问token */ |
| | | accessToken: string |
| | | /** 访问token */ |
| | | refreshToken: string |
| | | token: { |
| | | access_token: string |
| | | refresh_token: string |
| | | } |
| | | info: { |
| | | sysUser: Entity.User |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | interface Local { |
| | | /* 存储用户信息 */ |
| | | userInfo: Api.Login.Info |
| | | userInfo: Entity.User |
| | | /* 存储访问token */ |
| | | accessToken: string |
| | | /* 存储刷新token */ |
| New file |
| | |
| | | import CryptoJS from 'crypto-js' |
| | | import Jsrsasign from 'jsrsasign' |
| | | import { v4 as uuidv4 } from 'uuid' |
| | | |
| | | export interface EncryptParams { |
| | | sensitiveData: Record<string, any> |
| | | rsaPublicKey: string |
| | | } |
| | | |
| | | export interface DecryptParams { |
| | | encryptedData: string |
| | | encryptedAesKey: string |
| | | rsaPrivateKey: string |
| | | } |
| | | |
| | | export interface SignParams { |
| | | params: Record<string, any> |
| | | secretKey: string |
| | | timestamp: number |
| | | } |
| | | |
| | | /** |
| | | * 生成随机AES密钥 |
| | | * @returns AES密钥(Base64编码的32字节随机数据) |
| | | */ |
| | | export function generateAesKey(): string { |
| | | return 'qwdkshjf9834jsdf' |
| | | return CryptoJS.lib.WordArray.random(32).toString(CryptoJS.enc.Base64) |
| | | } |
| | | |
| | | /** |
| | | * 生成随机签名密钥 |
| | | * @returns 签名密钥(Base64编码的32字节随机数据) |
| | | */ |
| | | export function generateSignKey(): string { |
| | | return CryptoJS.lib.WordArray.random(32).toString(CryptoJS.enc.Base64) |
| | | } |
| | | |
| | | /** |
| | | * AES加密(CBC模式) |
| | | * @param data 待加密数据 |
| | | * @param key AES密钥(Base64编码) |
| | | * @returns 加密后的数据(Base64编码的iv:ciphertext) |
| | | */ |
| | | export function aesEncrypt(data: string, key: string): string { |
| | | const iv = CryptoJS.lib.WordArray.random(16) |
| | | const keyWordArray = CryptoJS.enc.Base64.parse(key) |
| | | const encrypted = CryptoJS.AES.encrypt(data, keyWordArray, { |
| | | iv, |
| | | mode: CryptoJS.mode.CBC, |
| | | padding: CryptoJS.pad.Pkcs7, |
| | | }) |
| | | const ivHex = iv.toString(CryptoJS.enc.Hex) |
| | | const ciphertextHex = encrypted.ciphertext.toString(CryptoJS.enc.Hex) |
| | | const combined = `${ivHex}:${ciphertextHex}` |
| | | return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Hex.parse(combined)) |
| | | } |
| | | |
| | | /** |
| | | * AES解密(CBC模式) |
| | | * @param encryptedData 加密数据(Base64编码的iv:ciphertext) |
| | | * @param key AES密钥(Base64编码) |
| | | * @returns 解密后的数据 |
| | | */ |
| | | export function aesDecrypt(encryptedData: string, key: string): string { |
| | | const combinedHex = CryptoJS.enc.Base64.parse(encryptedData).toString(CryptoJS.enc.Hex) |
| | | const [ivStr, encryptedStr] = combinedHex.split(':') |
| | | const iv = CryptoJS.enc.Hex.parse(ivStr) |
| | | const keyWordArray = CryptoJS.enc.Base64.parse(key) |
| | | const decrypted = CryptoJS.AES.decrypt( |
| | | CryptoJS.lib.WordArray.create(CryptoJS.enc.Hex.parse(encryptedStr)), |
| | | keyWordArray, |
| | | { |
| | | iv, |
| | | mode: CryptoJS.mode.CBC, |
| | | padding: CryptoJS.pad.Pkcs7, |
| | | }, |
| | | ) |
| | | return decrypted.toString(CryptoJS.enc.Utf8) |
| | | } |
| | | |
| | | /** |
| | | * 检查并格式化RSA公钥 |
| | | * @param publicKey 原始公钥 |
| | | * @returns 格式化后的公钥 |
| | | */ |
| | | function formatPublicKey(publicKey: string): string { |
| | | let formattedKey = publicKey.trim() |
| | | |
| | | if (!formattedKey.startsWith('-----BEGIN')) { |
| | | formattedKey = `-----BEGIN PUBLIC KEY-----\n${formattedKey}\n-----END PUBLIC KEY-----` |
| | | } |
| | | |
| | | return formattedKey |
| | | } |
| | | |
| | | /** |
| | | * RSA加密(OAEP填充方式) |
| | | * @param data 待加密数据 |
| | | * @param publicKey RSA公钥 |
| | | * @returns 加密后的数据 |
| | | */ |
| | | export function rsaEncrypt(data: string, publicKey: string): string { |
| | | try { |
| | | if (!publicKey || typeof publicKey !== 'string') { |
| | | throw new Error('Invalid RSA public key: must be a non-empty string') |
| | | } |
| | | |
| | | const formattedKey = formatPublicKey(publicKey) |
| | | const rsaKey = Jsrsasign.KEYUTIL.getKey(formattedKey) |
| | | |
| | | const dataHex = Jsrsasign.utf8tohex(data) |
| | | const dataBigInt = new Jsrsasign.BigInteger(dataHex, 16) |
| | | const encryptedBigInt = rsaKey.doPublic(dataBigInt) |
| | | const encryptedHex = encryptedBigInt.toString(16) |
| | | const encryptedBase64 = Jsrsasign.hextob64(encryptedHex) |
| | | |
| | | return encryptedBase64 |
| | | } |
| | | catch (error) { |
| | | console.error('RSA encryption error:', error) |
| | | throw new Error(`RSA encryption failed: ${error.message}`) |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * RSA解密(OAEP填充方式) |
| | | * @param encryptedData 加密数据 |
| | | * @param privateKey RSA私钥 |
| | | * @returns 解密后的数据 |
| | | */ |
| | | export function rsaDecrypt(encryptedData: string, privateKey: string): string { |
| | | const priKey = Jsrsasign.KEYUTIL.getKey(privateKey) |
| | | const encryptedHex = Jsrsasign.b64tohex(encryptedData) |
| | | const decrypted = Jsrsasign.KJUR.crypto.Cipher.decrypt(encryptedHex, priKey, 'RSA-OAEP') |
| | | return decrypted |
| | | } |
| | | |
| | | /** |
| | | * 生成签名 |
| | | * @param params 请求参数 |
| | | * @param secretKey 密钥 |
| | | * @param timestamp 时间戳 |
| | | * @returns 签名值 |
| | | */ |
| | | export function generateSign(params: Record<string, any>, secretKey: string, timestamp: number): string { |
| | | const sortedParams = Object.keys(params) |
| | | .filter(key => params[key] !== undefined && params[key] !== null && params[key] !== '') |
| | | .sort() |
| | | console.log('sortedParams', sortedParams) |
| | | |
| | | const paramsList = sortedParams.map(key => `${key}=${String(params[key])}`) |
| | | const paramsStr = [...paramsList, `timestamp=${String(timestamp)}`].join('&') |
| | | |
| | | console.log('paramsStr paramsStr', paramsStr) |
| | | console.log('secretKey secretKey', secretKey) |
| | | const hmac = CryptoJS.HmacSHA256(paramsStr, secretKey) |
| | | console.log('hmac hmac', hmac) |
| | | console.log('CryptoJS.enc.Base64.stringify(hmac)', CryptoJS.enc.Base64.stringify(hmac)) |
| | | return CryptoJS.enc.Base64.stringify(hmac) |
| | | } |
| | | |
| | | /** |
| | | * 加密敏感数据(单个字段加密) |
| | | * @param data 要加密的数据 |
| | | * @param rsaPublicKey RSA公钥 |
| | | * @returns 加密结果 |
| | | */ |
| | | export function encryptSensitiveField(data: string, rsaPublicKey: string): { encryptedData: string, encryptedAesKey: string } { |
| | | const aesKey = generateAesKey() |
| | | const encryptedData = aesEncrypt(data, aesKey) |
| | | const encryptedAesKey = rsaEncrypt(aesKey, rsaPublicKey) |
| | | |
| | | return { |
| | | encryptedData, |
| | | encryptedAesKey, |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 加密敏感数据(多个字段一起加密) |
| | | * @param params 加密参数 |
| | | * @returns 加密结果 |
| | | */ |
| | | export function encryptSensitiveData(params: { |
| | | sensitiveData: Record<string, any> |
| | | rsaPublicKey: string |
| | | }): { encryptedData: string, encryptedAesKey: string } { |
| | | const { sensitiveData, rsaPublicKey } = params |
| | | |
| | | const aesKey = generateAesKey() |
| | | const encryptedData = aesEncrypt(JSON.stringify(sensitiveData), aesKey) |
| | | const encryptedAesKey = rsaEncrypt(aesKey, rsaPublicKey) |
| | | |
| | | return { |
| | | encryptedData, |
| | | encryptedAesKey, |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 生成请求头信息 |
| | | * @param sign 签名值 |
| | | * @param encryptedAesKey RSA加密后的AES密钥 |
| | | * @param timestamp 时间戳 |
| | | * @returns 请求头 |
| | | */ |
| | | export function generateHeaders(sign: string, encryptedAesKey: string, timestamp: number): Record<string, string> { |
| | | return { |
| | | timestamp: timestamp.toString(), |
| | | nonce: uuidv4(), |
| | | sign, |
| | | xps: encryptedAesKey, |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取当前时间戳(毫秒) |
| | | * @returns 时间戳 |
| | | */ |
| | | export function getTimestamp(): number { |
| | | return Date.now() |
| | | } |
| | |
| | | export * from './i18n' |
| | | export * from './icon' |
| | | export * from './normalize' |
| | | export * from './crypto' |
| | |
| | | local.set('loginAccount', { account, pwd }) |
| | | else local.remove('loginAccount') |
| | | |
| | | await authStore.login(account, pwd) |
| | | await authStore.loginEncrypted(account, pwd) |
| | | isLoading.value = false |
| | | }) |
| | | } |
| | |
| | | </n-h2> |
| | | <n-form ref="formRef" :rules="rules" :model="formValue" :show-label="false" size="large"> |
| | | <n-form-item path="account"> |
| | | <n-input v-model:value="formValue.account" clearable :placeholder="$t('login.accountPlaceholder')" :input-props="{autocomplete:'username'}" /> |
| | | <n-input v-model:value="formValue.account" clearable :placeholder="$t('login.accountPlaceholder')" :input-props="{ autocomplete: 'username' }" /> |
| | | </n-form-item> |
| | | <n-form-item path="pwd"> |
| | | <n-input v-model:value="formValue.pwd" type="password" :placeholder="$t('login.passwordPlaceholder')" clearable show-password-on="click" :input-props="{autocomplete:'current-password'}" > |
| | | <n-input v-model:value="formValue.pwd" type="password" :placeholder="$t('login.passwordPlaceholder')" clearable show-password-on="click" :input-props="{ autocomplete: 'current-password' }"> |
| | | <template #password-invisible-icon> |
| | | <icon-park-outline-preview-close-one /> |
| | | </template> |