Browse Source

删除重复component,先删除button 的name

master
hejl 1 day ago
parent
commit
3af4231a6b
  1. 6
      gofaster/app/plugins/modules/route-analyzer.js
  2. 629
      gofaster/app/plugins/modules/trigger-analyzer.js
  3. 4
      gofaster/app/plugins/route-mapping-plugin.js
  4. 761
      gofaster/app/src/renderer/components/LoginModal.vue
  5. 1878
      gofaster/app/src/renderer/components/MainLayout.vue
  6. 967
      gofaster/app/src/renderer/components/PasswordChangeModal.vue
  7. 2
      gofaster/app/src/renderer/modules/core/components/MainLayout.vue
  8. 6
      gofaster/app/src/renderer/modules/role-management/components/PermissionManager.vue
  9. 10
      gofaster/app/src/renderer/modules/role-management/components/RolePermissionAssignment.vue
  10. 2
      gofaster/app/src/renderer/modules/role-management/components/UserRoleAssignment.vue
  11. 8
      gofaster/app/src/renderer/modules/role-management/views/RoleManagement.vue
  12. 145
      gofaster/app/src/renderer/modules/route-sync/direct-route-mappings.js
  13. 2
      gofaster/app/src/renderer/modules/user-management/components/LoginModal.vue
  14. 2
      gofaster/app/src/renderer/modules/user-management/components/PasswordChangeModal.vue
  15. 4
      gofaster/app/src/renderer/modules/user-management/views/UserManagement.vue
  16. 2
      gofaster/app/src/renderer/modules/user-management/views/UserProfile.vue

6
gofaster/app/plugins/modules/route-analyzer.js

@ -48,6 +48,7 @@ class RouteAnalyzer { @@ -48,6 +48,7 @@ class RouteAnalyzer {
const componentMatch = match.match(/component:\s*([A-Za-z][A-Za-z0-9]*)/)
const nameMatch = match.match(/name:\s*['"]([^'"]+)['"]/)
const descriptionMatch = match.match(/description:\s*['"]([^'"]+)['"]/)
const authTypeMatch = match.match(/authType:\s*['"]([^'"]+)['"]/)
if (pathMatch && componentMatch) {
const route = {
@ -65,6 +66,11 @@ class RouteAnalyzer { @@ -65,6 +66,11 @@ class RouteAnalyzer {
route.description = descriptionMatch[1]
}
// 收集authType字段
if (authTypeMatch) {
route.authType = authTypeMatch[1]
}
routes.push(route)
}
})

629
gofaster/app/plugins/modules/trigger-analyzer.js

@ -1,7 +1,8 @@ @@ -1,7 +1,8 @@
const { readFileSync, existsSync, readdirSync } = require('fs')
const { readFileSync, writeFileSync, existsSync, readdirSync } = require('fs')
const { resolve } = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { parse } = require('@vue/compiler-sfc')
const AstAnalyzer = require('./ast-analyzer')
/**
@ -16,11 +17,15 @@ class TriggerAnalyzer { @@ -16,11 +17,15 @@ class TriggerAnalyzer {
/**
* 查找API方法的触发器源 - 第一步第一层触发源分析
* @param {Array} apiMappings - API映射数组
* @param {Array} routes - 路由配置数组
* @returns {Array} 增强的API映射数组
*/
findTriggerSourcesForApiMappings(apiMappings) {
findTriggerSourcesForApiMappings(apiMappings, routes) {
const enhancedApiMappings = []
// 创建组件到authType的映射
const componentAuthMap = this.createComponentAuthMap(routes)
apiMappings.forEach(moduleMapping => {
const enhancedModuleMapping = {
...moduleMapping,
@ -35,15 +40,25 @@ class TriggerAnalyzer { @@ -35,15 +40,25 @@ class TriggerAnalyzer {
moduleMapping.module
)
// 为所有API映射添加triggerSources字段(即使为空)
enhancedModuleMapping.apiMappings.push({
...apiMapping,
triggerSources: triggerSources || []
})
// 过滤掉authType为public的组件的触发源
const filteredTriggerSources = this.filterPublicComponentTriggers(
triggerSources,
componentAuthMap
)
// 只有当有触发源时才添加API映射
if (filteredTriggerSources.length > 0) {
enhancedModuleMapping.apiMappings.push({
...apiMapping,
triggerSources: filteredTriggerSources
})
}
})
// 返回所有模块映射(包括没有触发源的)
enhancedApiMappings.push(enhancedModuleMapping)
// 只有当模块有API映射时才添加模块映射
if (enhancedModuleMapping.apiMappings.length > 0) {
enhancedApiMappings.push(enhancedModuleMapping)
}
})
return enhancedApiMappings
@ -453,6 +468,115 @@ class TriggerAnalyzer { @@ -453,6 +468,115 @@ class TriggerAnalyzer {
})
}
/**
* 创建组件到authType的映射
* @param {Array} routes - 路由配置数组
* @returns {Map} 组件到authType的映射
*/
createComponentAuthMap(routes) {
const componentAuthMap = new Map()
// 从Vue组件文件中读取authType属性
routes.forEach(route => {
if (route.component) {
const authType = this.getComponentAuthType(route.component)
if (authType) {
componentAuthMap.set(route.component, authType)
}
}
})
// 扫描所有模块目录中的组件文件,查找authType属性
const moduleDirs = ['core', 'user-management', 'role-management', 'system-settings']
moduleDirs.forEach(moduleDir => {
this.scanComponentsForAuthType(moduleDir, componentAuthMap)
})
return componentAuthMap
}
/**
* 扫描指定模块目录中的组件文件查找authType属性
* @param {string} moduleDir - 模块目录名
* @param {Map} componentAuthMap - 组件到authType的映射
*/
scanComponentsForAuthType(moduleDir, componentAuthMap) {
try {
const modulePath = resolve(__dirname, `../../src/renderer/modules/${moduleDir}`)
if (existsSync(modulePath)) {
const files = readdirSync(modulePath, { recursive: true })
files.forEach(file => {
if (typeof file === 'string' && file.endsWith('.vue')) {
const filePath = resolve(modulePath, file)
const componentName = this.extractComponentNameFromPath(file)
// 如果还没有这个组件的authType信息,则尝试获取
if (!componentAuthMap.has(componentName)) {
const authType = this.getComponentAuthType(componentName)
if (authType) {
componentAuthMap.set(componentName, authType)
}
}
}
})
}
} catch (error) {
console.warn(`扫描模块 ${moduleDir} 的组件时出错:`, error.message)
}
}
/**
* 从Vue组件文件中获取authType属性
* @param {string} componentName - 组件名称
* @returns {string|null} authType值
*/
getComponentAuthType(componentName) {
try {
// 在modules目录中查找组件文件
const moduleDirs = ['core', 'user-management', 'role-management', 'system-settings']
for (const moduleDir of moduleDirs) {
const componentPath = resolve(__dirname, `../../src/renderer/modules/${moduleDir}/components/${componentName}.vue`)
if (existsSync(componentPath)) {
const content = readFileSync(componentPath, 'utf-8')
const authTypeMatch = content.match(/<template\s+authType\s*=\s*["']([^"']+)["']/)
if (authTypeMatch) {
return authTypeMatch[1]
}
}
// 也检查views目录
const viewPath = resolve(__dirname, `../../src/renderer/modules/${moduleDir}/views/${componentName}.vue`)
if (existsSync(viewPath)) {
const content = readFileSync(viewPath, 'utf-8')
const authTypeMatch = content.match(/<template\s+authType\s*=\s*["']([^"']+)["']/)
if (authTypeMatch) {
return authTypeMatch[1]
}
}
}
} catch (error) {
console.warn(`获取组件 ${componentName} 的authType时出错:`, error.message)
}
return null
}
/**
* 过滤掉authType为public的组件的触发源
* @param {Array} triggerSources - 触发源数组
* @param {Map} componentAuthMap - 组件到authType的映射
* @returns {Array} 过滤后的触发源数组
*/
filterPublicComponentTriggers(triggerSources, componentAuthMap) {
return triggerSources.filter(trigger => {
const authType = componentAuthMap.get(trigger.component)
// 如果组件没有定义authType,或者authType不是public,则保留
return !authType || authType !== 'public'
})
}
/**
* 从文件路径提取组件名称
* @param {string} filePath - 文件路径
@ -481,8 +605,8 @@ class TriggerAnalyzer { @@ -481,8 +605,8 @@ class TriggerAnalyzer {
const ast = this.parseVueComponent(content)
if (!ast) return triggerSources
// 查找API调用和函数调用关系
const apiCalls = this.findApiCallsWithContext(ast, serviceName, apiMethodName)
// 查找API调用和函数调用关系,传递文件路径用于追溯
const apiCalls = this.findApiCallsWithContext(ast, serviceName, apiMethodName, filePath, componentName)
// 为每个API调用创建触发源
apiCalls.forEach(call => {
@ -531,9 +655,11 @@ class TriggerAnalyzer { @@ -531,9 +655,11 @@ class TriggerAnalyzer {
* @param {Object} ast - AST对象
* @param {string} serviceName - 服务名称
* @param {string} apiMethodName - API方法名称
* @param {string} filePath - 组件文件路径
* @param {string} componentName - 组件名称
* @returns {Array} API调用信息数组
*/
findApiCallsWithContext(ast, serviceName, apiMethodName) {
findApiCallsWithContext(ast, serviceName, apiMethodName, filePath, componentName) {
const apiCalls = []
const self = this
const functionCallMap = new Map() // 存储函数调用关系
@ -592,7 +718,7 @@ class TriggerAnalyzer { @@ -592,7 +718,7 @@ class TriggerAnalyzer {
node.callee.property.name === apiMethodName) {
// 查找调用上下文
const context = self.findCallContextWithChain(path, functionCallMap)
const context = self.findCallContextWithChain(path, functionCallMap, filePath, componentName)
apiCalls.push({
triggerName: context.triggerName,
@ -638,9 +764,11 @@ class TriggerAnalyzer { @@ -638,9 +764,11 @@ class TriggerAnalyzer {
* 查找API调用的上下文包含调用链分析
* @param {Object} path - Babel路径对象
* @param {Map} functionCallMap - 函数调用关系映射
* @param {string} filePath - 组件文件路径
* @param {string} componentName - 组件名称
* @returns {Object} 上下文信息
*/
findCallContextWithChain(path, functionCallMap) {
findCallContextWithChain(path, functionCallMap, filePath, componentName) {
let currentPath = path
let triggerName = ''
let triggerType = 'function'
@ -721,9 +849,482 @@ class TriggerAnalyzer { @@ -721,9 +849,482 @@ class TriggerAnalyzer {
currentPath = currentPath.parentPath
}
// 如果找到的是method类型,继续向上追溯找到最终的可视组件
if (triggerType === 'method') {
const finalContext = this.traceToVisualComponent(path, triggerName, functionCallMap, filePath, componentName)
if (finalContext) {
return finalContext
}
}
return { triggerName, triggerType }
}
/**
* 追溯method类型的触发源到最终的可视组件
* @param {Object} path - Babel路径对象
* @param {string} methodName - 方法名称
* @param {Map} functionCallMap - 函数调用关系映射
* @param {string} filePath - 组件文件路径
* @returns {Object|null} 最终的可视组件上下文
*/
traceToVisualComponent(path, methodName, functionCallMap, filePath, componentName) {
try {
// 使用传入的文件路径
const componentPath = filePath
if (!componentPath) return null
// 读取组件文件内容
const content = readFileSync(componentPath, 'utf-8')
// 分析模板中的事件绑定
const visualContext = this.analyzeTemplateForMethod(content, methodName, componentName, filePath)
if (visualContext) {
return visualContext
}
// 如果模板中没有找到,检查是否有其他方法调用了这个方法
const callerContext = this.findMethodCaller(methodName, functionCallMap)
if (callerContext) {
// 检查调用者是否是生命周期钩子
const callerInfo = functionCallMap.get(callerContext)
if (callerInfo) {
// 检查调用者是否是生命周期钩子
if (this.isLifecycleHook(callerContext)) {
return {
triggerName: '',
triggerType: 'page'
}
}
}
// 继续递归追溯
return this.traceToVisualComponent(path, callerContext, functionCallMap, filePath, componentName)
}
} catch (error) {
console.warn(`追溯可视组件时出错:`, error.message)
}
return null
}
/**
* 检查方法名是否是生命周期钩子
* @param {string} methodName - 方法名称
* @returns {boolean} 是否是生命周期钩子
*/
isLifecycleHook(methodName) {
const lifecycleHooks = [
'setup',
'onMounted',
'onCreated',
'onBeforeMount',
'onBeforeCreate',
'onUpdated',
'onBeforeUpdate',
'onUnmounted',
'onBeforeUnmount',
'mounted',
'created',
'beforeMount',
'beforeCreate',
'updated',
'beforeUpdate',
'unmounted',
'beforeUnmount',
'watch'
]
return lifecycleHooks.includes(methodName)
}
/**
* 分析模板中与指定方法相关的事件绑定
* @param {string} content - 组件文件内容
* @param {string} methodName - 方法名称
* @param {string} componentName - 组件名称
* @param {string} filePath - 组件文件路径
* @returns {Object|null} 可视组件上下文
*/
analyzeTemplateForMethod(content, methodName, componentName, filePath) {
try {
// 使用Vue编译器解析单文件组件
const { descriptor } = parse(content)
if (!descriptor.template) {
return null
}
// 解析模板AST
const templateAst = descriptor.template.ast
if (!templateAst) {
return null
}
// 遍历模板AST,查找事件绑定
return this.findEventBindingInTemplate(templateAst, methodName, componentName, filePath)
} catch (error) {
console.warn(`解析Vue模板时出错:`, error.message)
return null
}
}
/**
* 在模板AST中查找事件绑定
* @param {Object} node - AST节点
* @param {string} methodName - 方法名称
* @param {string} componentName - 组件名称
* @param {string} filePath - 组件文件路径
* @returns {Object|null} 可视组件上下文
*/
findEventBindingInTemplate(node, methodName, componentName, filePath) {
if (!node) return null
// 检查当前节点是否有事件绑定
if (node.props) {
for (const prop of node.props) {
if (prop.type === 7) { // DIRECTIVE类型
const eventName = prop.name
if (eventName === 'on' && prop.arg) {
const eventType = prop.arg.content
if (['click', 'submit', 'change', 'input'].includes(eventType)) {
// 检查事件处理函数
if (prop.exp && this.isMethodCall(prop.exp, methodName)) {
const context = this.createVisualContext(node, eventType, componentName, methodName, filePath)
// 如果找到的是form,需要查找其中的submit按钮
if (context.triggerType === 'form') {
const submitButton = this.findSubmitButtonInForm(node, componentName, methodName)
if (submitButton) {
return submitButton
}
}
return context
}
}
}
}
}
}
// 递归检查子节点
if (node.children) {
for (const child of node.children) {
if (child.type === 1) { // ELEMENT类型
const result = this.findEventBindingInTemplate(child, methodName, componentName, filePath)
if (result) return result
}
}
}
return null
}
/**
* 在form中查找submit按钮
* @param {Object} formNode - form AST节点
* @param {string} componentName - 组件名称
* @param {string} methodName - 方法名称
* @returns {Object|null} submit按钮上下文
*/
findSubmitButtonInForm(formNode, componentName, methodName) {
if (!formNode || !formNode.children) return null
// 递归查找submit按钮
for (const child of formNode.children) {
if (child.type === 1) { // ELEMENT类型
if (child.tag === 'button') {
// 检查是否是submit按钮
const props = child.props || []
const attributes = {}
props.forEach(prop => {
if (prop.type === 6) { // ATTRIBUTE
attributes[prop.name] = prop.value ? prop.value.content : ''
}
})
if (attributes.type === 'submit') {
let triggerName = attributes.name
if (!triggerName) {
// 生成唯一的name属性:组件名+唯一尾缀
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}`
}
return {
triggerName: triggerName,
triggerType: 'button'
}
}
}
// 递归查找子元素
const result = this.findSubmitButtonInForm(child, componentName, methodName)
if (result) return result
}
}
return null
}
/**
* 检查表达式是否是方法调用
* @param {Object} exp - 表达式节点
* @param {string} methodName - 方法名称
* @returns {boolean} 是否是方法调用
*/
isMethodCall(exp, methodName) {
if (!exp) return false
// 简单的方法调用:methodName
if (exp.type === 4 && exp.content === methodName) { // SIMPLE_EXPRESSION
return true
}
// 带参数的方法调用:methodName(...) - 检查content是否以methodName开头
if (exp.type === 4 && exp.content && exp.content.startsWith(methodName + '(')) {
return true
}
// 带参数的方法调用:methodName(...)
if (exp.type === 8) { // COMPOUND_EXPRESSION
const children = exp.children
if (children && children.length >= 1) {
const firstChild = children[0]
if (firstChild.type === 4 && firstChild.content === methodName) {
return true
}
}
}
// 检查AST中的方法调用
if (exp.ast) {
return this.checkAstMethodCall(exp.ast, methodName)
}
return false
}
/**
* 检查AST中的方法调用
* @param {Object} ast - AST节点
* @param {string} methodName - 方法名称
* @returns {boolean} 是否是方法调用
*/
checkAstMethodCall(ast, methodName) {
if (!ast) return false
// 简单标识符:methodName
if (ast.type === 'Identifier' && ast.name === methodName) {
return true
}
// 方法调用:methodName(...)
if (ast.type === 'CallExpression') {
if (ast.callee && ast.callee.type === 'Identifier' && ast.callee.name === methodName) {
return true
}
}
return false
}
/**
* 创建可视组件上下文
* @param {Object} node - AST节点
* @param {string} eventType - 事件类型
* @param {string} componentName - 组件名称
* @param {string} methodName - 方法名称
* @param {string} filePath - 组件文件路径
* @returns {Object} 可视组件上下文
*/
createVisualContext(node, eventType, componentName, methodName, filePath) {
const tag = node.tag
const props = node.props || []
// 提取元素属性
const attributes = {}
props.forEach(prop => {
if (prop.type === 6) { // ATTRIBUTE
attributes[prop.name] = prop.value ? prop.value.content : ''
}
})
// 根据标签类型确定组件类型和名称
if (tag === 'button') {
let triggerName = attributes.name
if (!triggerName) {
// 生成唯一的name属性:组件名+唯一尾缀
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}`
// 自动为Vue文件添加name属性
this.addNameAttributeToVueFile(filePath, node, triggerName)
}
return {
triggerName: triggerName,
triggerType: 'button'
}
} else if (tag === 'form') {
let triggerName = attributes.name
if (!triggerName) {
// 生成唯一的name属性:组件名+唯一尾缀
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}`
}
return {
triggerName: triggerName,
triggerType: 'form'
}
} else if (tag === 'input') {
let triggerName = attributes.name
if (!triggerName) {
// 生成唯一的name属性:组件名+唯一尾缀
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}`
}
return {
triggerName: triggerName,
triggerType: 'input'
}
} else if (tag === 'a') {
let triggerName = attributes.name
if (!triggerName) {
// 生成唯一的name属性:组件名+唯一尾缀
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}`
}
return {
triggerName: triggerName,
triggerType: 'link'
}
} else if (tag === 'i') {
let triggerName = attributes.name
if (!triggerName) {
// 生成唯一的name属性:组件名+唯一尾缀
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}`
}
return {
triggerName: triggerName,
triggerType: 'icon'
}
} else {
let triggerName = attributes.name
if (!triggerName) {
// 生成唯一的name属性:组件名+唯一尾缀
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}`
}
return {
triggerName: triggerName,
triggerType: 'element'
}
}
}
/**
* 生成唯一的尾缀
* @returns {string} 唯一尾缀
*/
generateUniqueSuffix() {
// 生成6位随机字符串,类似 "2m2snp"
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
let result = ''
for (let i = 0; i < 6; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
return result
}
/**
* 为Vue文件中的button添加name属性
* @param {string} filePath - Vue文件路径
* @param {Object} node - AST节点
* @param {string} nameValue - name属性值
*/
addNameAttributeToVueFile(filePath, node, nameValue) {
try {
// 读取Vue文件内容
const content = readFileSync(filePath, 'utf-8')
// 获取节点的位置信息
const loc = node.loc
if (!loc) {
console.warn(`节点没有位置信息,无法添加name属性`)
return
}
// 找到button标签的开始位置
const startLine = loc.start.line
const startColumn = loc.start.column
console.log(`尝试为第${startLine}行第${startColumn}列的button添加name属性: ${nameValue}`)
// 将内容按行分割
const lines = content.split('\n')
// 找到对应的行
if (startLine > 0 && startLine <= lines.length) {
const lineIndex = startLine - 1
let line = lines[lineIndex]
console.log(`当前行内容: ${line}`)
// 在button标签中添加name属性
// 查找button标签的开始位置
const buttonStartMatch = line.match(/<button[^>]*>/)
if (buttonStartMatch) {
// 如果button标签在同一行结束
const buttonEndIndex = buttonStartMatch.index + buttonStartMatch[0].length
const beforeButton = line.substring(0, buttonEndIndex - 1) // 去掉最后的>
const afterButton = line.substring(buttonEndIndex - 1)
// 添加name属性
const newLine = `${beforeButton} name="${nameValue}">${afterButton}`
lines[lineIndex] = newLine
// 写回文件
const newContent = lines.join('\n')
writeFileSync(filePath, newContent, 'utf-8')
console.log(`已为 ${filePath} 中的button添加name属性: ${nameValue}`)
} else {
// 如果button标签跨行,查找button开始标签
const buttonStartMatch = line.match(/<button[^>]*$/)
if (buttonStartMatch) {
// 在button开始标签后添加name属性
const beforeButton = line.substring(0, buttonStartMatch.index + buttonStartMatch[0].length)
const afterButton = line.substring(buttonStartMatch.index + buttonStartMatch[0].length)
// 添加name属性
const newLine = `${beforeButton} name="${nameValue}"${afterButton}`
lines[lineIndex] = newLine
// 写回文件
const newContent = lines.join('\n')
writeFileSync(filePath, newContent, 'utf-8')
console.log(`已为 ${filePath} 中的跨行button添加name属性: ${nameValue}`)
} else {
console.warn(`未找到button标签`)
}
}
} else {
console.warn(`行号超出范围: ${startLine}`)
}
} catch (error) {
console.warn(`为Vue文件添加name属性时出错:`, error.message)
}
}
/**
* 查找方法的调用者
*/
findMethodCaller(methodName, functionCallMap) {
const funcInfo = functionCallMap.get(methodName)
if (funcInfo && funcInfo.callers && funcInfo.callers.length > 0) {
return funcInfo.callers[0] // 返回第一个调用者
}
return null
}
/**
* 查找API调用的上下文
* @param {Object} path - Babel路径对象

4
gofaster/app/plugins/route-mapping-plugin.js

@ -138,8 +138,8 @@ function routeMappingPlugin() { @@ -138,8 +138,8 @@ function routeMappingPlugin() {
const moduleDirs = this._routeAnalyzer.getModuleDirs()
const apiMappings = this._apiCollector.collectApiMappings(moduleDirs)
// 3. 关联页面与API(第三层)
const enhancedApiMappings = this._triggerAnalyzer.findTriggerSourcesForApiMappings(apiMappings)
// 3. 关联页面与API(第三层),传递路由信息用于过滤
const enhancedApiMappings = this._triggerAnalyzer.findTriggerSourcesForApiMappings(apiMappings, routes)
// 生成包含路由和API关联信息的文件
this._fileGenerator.generateRouteApiMappingFile(routes, enhancedApiMappings)

761
gofaster/app/src/renderer/components/LoginModal.vue

@ -1,761 +0,0 @@ @@ -1,761 +0,0 @@
<template authType="public">
<div v-if="visible" class="login-modal-overlay">
<div class="login-modal" @click.stop>
<div class="login-modal-header">
<h2>用户登录</h2>
<button class="close-btn" @click="closeModal">×</button>
</div>
<div class="login-modal-body">
<form @submit.prevent="handleLogin" class="login-form">
<!-- 用户名 -->
<div class="form-group">
<label for="username">用户名</label>
<input
id="username"
v-model="loginForm.username"
type="text"
placeholder="请输入用户名"
required
:disabled="loading"
/>
</div>
<!-- 密码 -->
<div class="form-group">
<label for="password">密码</label>
<div class="password-input-container">
<input
id="password"
v-model="loginForm.password"
:type="showPassword ? 'text' : 'password'"
placeholder="请输入密码"
required
:disabled="loading"
/>
<button
type="button"
class="password-toggle-btn"
@click="togglePassword"
:disabled="loading"
:title="showPassword ? '隐藏密码' : '显示密码'"
>
<span class="password-icon">{{ showPassword ? '👁' : '👁🗨' }}</span>
</button>
</div>
</div>
<!-- 验证码 -->
<div class="form-group">
<label for="captcha">验证码</label>
<div class="captcha-container">
<input
id="captcha"
v-model="loginForm.captcha"
type="text"
placeholder="请输入验证码"
required
:disabled="loading"
maxlength="4"
/>
<div class="captcha-image-container">
<img
v-if="captchaImage"
:src="captchaImage"
alt="验证码"
@click="refreshCaptcha"
class="captcha-image"
/>
<div v-else class="captcha-placeholder" @click="refreshCaptcha">
<span v-if="captchaLoading">验证码加载中...</span>
<span v-else>点击获取验证码</span>
<span v-if="captchaLoading" class="captcha-loading-spinner"></span>
</div>
</div>
</div>
</div>
<!-- 登录按钮 -->
<div class="form-actions">
<button
name="submit-login"
type="submit"
class="login-btn"
:disabled="loading || !isFormValid"
>
<span v-if="loading" class="loading-spinner"></span>
{{ loading ? '登录中...' : '登录' }}
</button>
</div>
<!-- 错误提示 -->
<div v-if="errorMessage" class="error-message" role="alert">
<span class="error-icon"></span>
{{ errorMessage }}
</div>
</form>
</div>
<div class="login-modal-footer">
<p class="login-tips">
<span>提示</span>
<span>默认管理员账号admin</span>
<span>默认密码password</span>
</p>
</div>
</div>
</div>
</template>
<script>
import { userService } from '@/modules/user-management'
import * as ipUtils from '@/modules/core/utils/ipUtils'
const { getClientIP } = ipUtils
export default {
name: 'LoginModal',
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {
loading: false,
captchaLoading: false,
showPassword: false,
errorMessage: '',
captchaImage: '',
captchaId: '',
clientIP: '',
loginForm: {
username: '',
password: '',
captcha: ''
}
}
},
computed: {
isFormValid() {
return this.loginForm.username.trim() &&
this.loginForm.password.trim() &&
this.loginForm.captcha.trim() &&
this.captchaImage //
},
},
watch: {
visible(newVal) {
if (newVal) {
this.resetForm()
this.getClientIP() // IP
this.refreshCaptcha()
this.loadSavedLoginInfo() //
}
}
},
methods: {
//
loadSavedLoginInfo() {
try {
const savedLoginInfo = localStorage.getItem('gofaster-login-info')
if (savedLoginInfo) {
const loginInfo = JSON.parse(savedLoginInfo)
if (loginInfo.username && loginInfo.password) {
this.loginForm.username = loginInfo.username
this.loginForm.password = loginInfo.password
console.log('已自动填充保存的登录信息')
}
}
} catch (error) {
console.error('加载保存的登录信息失败:', error)
}
},
//
saveLoginInfo() {
try {
// ""
const userSettings = localStorage.getItem('gofaster-settings')
if (userSettings) {
const settings = JSON.parse(userSettings)
if (settings.rememberPassword) {
const loginInfo = {
username: this.loginForm.username,
password: this.loginForm.password,
timestamp: Date.now()
}
localStorage.setItem('gofaster-login-info', JSON.stringify(loginInfo))
console.log('已保存登录信息到本地存储')
} else {
//
localStorage.removeItem('gofaster-login-info')
console.log('用户选择不记住密码,已清除保存的登录信息')
}
}
} catch (error) {
console.error('保存登录信息失败:', error)
}
},
async handleLogin() {
if (!this.isFormValid) {
this.errorMessage = '请填写完整的登录信息'
return
}
this.loading = true
this.errorMessage = ''
try {
//
const response = await userService.login({
username: this.loginForm.username,
password: this.loginForm.password,
captcha: this.loginForm.captcha,
captcha_id: this.captchaId, // snake_case
client_ip: this.clientIP // IP
})
//
this.saveLoginInfo()
//
this.$emit('login-success', response)
this.closeModal()
//
this.$emit('show-message', {
type: 'success',
title: '登录成功',
content: `欢迎回来,${response.data?.user?.username || response.user?.username || this.loginForm.username || '用户'}`
})
} catch (error) {
//
console.log('登录错误详情:', error)
console.log('错误响应状态:', error.response?.status)
console.log('错误响应数据:', error.response?.data)
let errorMsg = '登录失败,请重试'
if (error.response) {
//
const status = error.response.status
const data = error.response.data
//
if (data.message && data.message.includes('验证码')) {
errorMsg = '验证码错误,请重新输入'
} else if (data.error && data.error.includes('验证码')) {
errorMsg = '验证码错误,请重新输入'
} else if (status === 400) {
// 400
if (data.message) {
errorMsg = data.message
} else if (data.error) {
errorMsg = data.error
} else {
errorMsg = '请求参数错误,请检查输入信息'
}
} else if (status === 401) {
// 401
errorMsg = '用户名或密码错误'
} else if (status === 422) {
// 422
if (data.message) {
errorMsg = data.message
} else {
errorMsg = '验证失败,请检查输入信息'
}
} else if (status === 423) {
//
if (data.error) {
errorMsg = data.error
} else if (data.message) {
errorMsg = data.message
} else {
errorMsg = '账户被锁定,请稍后重试'
}
} else if (status === 500) {
errorMsg = '服务器内部错误,请稍后重试'
}
} else if (error.message) {
//
if (error.message.includes('验证码')) {
errorMsg = '验证码错误,请重新输入'
} else if (error.message.includes('用户名') || error.message.includes('密码')) {
//
errorMsg = '用户名或密码错误'
} else {
errorMsg = error.message
}
}
console.log('最终显示的错误信息:', errorMsg)
this.errorMessage = errorMsg
//
this.$nextTick(() => {
console.log('错误信息已设置到界面:', this.errorMessage)
})
//
this.refreshCaptchaWithoutClearError()
} finally {
this.loading = false
}
},
async refreshCaptcha() {
try {
this.captchaLoading = true
this.errorMessage = ''
//
const response = await userService.getCaptcha()
// snake_case使 camelCase
this.captchaImage = response.data.captcha_image
this.captchaId = response.data.captcha_id
this.loginForm.captcha = ''
console.log('验证码获取成功:', response)
} catch (error) {
console.error('获取验证码失败:', error)
this.errorMessage = '获取验证码失败,请重试'
this.captchaImage = ''
this.captchaId = ''
} finally {
this.captchaLoading = false
}
},
async refreshCaptchaWithoutClearError() {
try {
this.captchaLoading = true
//
//
const response = await userService.getCaptcha()
// snake_case使 camelCase
this.captchaImage = response.data.captcha_image
this.captchaId = response.data.captcha_id
this.loginForm.captcha = ''
console.log('验证码获取成功(不清空错误):', response)
} catch (error) {
console.error('获取验证码失败:', error)
//
this.captchaImage = ''
this.captchaId = ''
} finally {
this.captchaLoading = false
}
},
// IP
async getClientIP() {
try {
this.clientIP = await getClientIP()
console.log('获取到客户端IP:', this.clientIP)
} catch (error) {
console.error('获取客户端IP失败:', error)
this.clientIP = '127.0.0.1'
}
},
resetForm() {
this.loginForm = {
username: '',
password: '',
captcha: ''
}
this.showPassword = false
this.errorMessage = ''
this.captchaImage = ''
this.captchaId = ''
},
togglePassword() {
this.showPassword = !this.showPassword
},
closeModal() {
this.$emit('update:visible', false)
this.resetForm()
}
}
}
</script>
<style scoped>
.login-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(4px);
}
.login-modal {
background: var(--bg-primary);
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
width: 90%;
max-width: 400px;
max-height: 90vh;
overflow: hidden;
animation: modalSlideIn 0.3s ease-out;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.login-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid var(--border-color);
background: var(--header-bg);
}
.login-modal-header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
}
.close-btn {
background: none;
border: none;
font-size: 24px;
color: var(--text-secondary);
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
}
.close-btn:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
.login-modal-body {
padding: 24px;
}
.login-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-weight: 500;
color: var(--text-primary);
font-size: 14px;
}
.form-group input {
width: 100%;
padding: 12px 16px;
border: 2px solid var(--border-color);
border-radius: 8px;
font-size: 14px;
background: var(--bg-secondary);
color: var(--text-primary);
transition: all 0.2s;
box-sizing: border-box;
}
.form-group input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
}
.form-group input:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 密码输入框容器 */
.password-input-container {
position: relative;
display: flex;
align-items: center;
}
.password-input-container input {
padding-right: 50px; /* 为密码切换按钮留出空间 */
}
.password-toggle-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.password-toggle-btn:hover {
background: var(--bg-secondary);
}
.password-toggle-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.password-icon {
font-size: 16px;
line-height: 1;
}
.captcha-container {
display: flex;
gap: 12px;
align-items: stretch;
}
.captcha-container input {
flex: 1;
}
.captcha-image-container {
width: 100px;
height: 44px;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
border: 2px solid var(--border-color);
transition: all 0.2s;
}
.captcha-image-container:hover {
border-color: var(--primary-color);
}
.captcha-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.captcha-placeholder {
width: 100%;
height: 100%;
background: var(--bg-secondary);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--text-secondary);
font-size: 12px;
text-align: center;
padding: 8px;
cursor: pointer;
transition: all 0.2s;
}
.captcha-placeholder:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.captcha-hint {
font-size: 10px;
opacity: 0.7;
margin-top: 2px;
}
.captcha-loading-spinner {
width: 12px;
height: 12px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-top: 4px;
}
.form-actions {
margin-top: 8px;
}
.login-btn {
width: 100%;
padding: 14px 24px;
background: var(--primary-color);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
/* 深色主题下的登录按钮样式 */
.theme-dark .login-btn {
background: var(--primary-color);
color: #333333;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.theme-dark .login-btn:hover:not(:disabled) {
background: var(--primary-hover);
color: #222222;
}
.login-btn:hover:not(:disabled) {
background: var(--primary-hover);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3);
}
.login-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.error-message {
background: var(--error-bg);
color: var(--error-text);
padding: 12px 16px;
border-radius: 8px;
font-size: 14px;
text-align: center;
border: 1px solid var(--error-border);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 16px;
animation: errorShake 0.5s ease-in-out;
}
.error-icon {
font-size: 16px;
flex-shrink: 0;
}
@keyframes errorShake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
/* 深色主题下的错误提示样式优化 */
.theme-dark .error-message {
background: rgba(244, 67, 54, 0.2);
color: #ffab91;
border: 1px solid rgba(244, 67, 54, 0.5);
font-weight: 500;
}
.login-modal-footer {
padding: 16px 24px;
border-top: 1px solid var(--border-color);
background: var(--bg-secondary);
}
.login-tips {
margin: 0;
font-size: 12px;
color: var(--text-secondary);
text-align: center;
line-height: 1.5;
}
.login-tips span {
display: block;
margin-bottom: 4px;
}
.login-tips span:first-child {
font-weight: 600;
color: var(--text-primary);
}
/* 响应式设计 */
@media (max-width: 480px) {
.login-modal {
width: 95%;
margin: 20px;
}
.login-modal-header,
.login-modal-body,
.login-modal-footer {
padding: 16px 20px;
}
.captcha-container {
flex-direction: column;
gap: 8px;
}
.captcha-image-container {
width: 100%;
height: 40px;
}
}
</style>

1878
gofaster/app/src/renderer/components/MainLayout.vue

File diff suppressed because it is too large Load Diff

967
gofaster/app/src/renderer/components/PasswordChangeModal.vue

@ -1,967 +0,0 @@ @@ -1,967 +0,0 @@
<template>
<div v-if="visible" class="password-modal-overlay" @click="handleOverlayClick">
<div class="password-modal" @click.stop>
<div class="modal-header">
<h3>{{ title }}</h3>
<button class="close-btn" @click="handleClose">×</button>
</div>
<div class="modal-body">
<form @submit.prevent="handleSubmit">
<!-- 当前密码 -->
<div class="form-group" v-if="!isForceChange">
<label>当前密码 *</label>
<input
v-model="form.currentPassword"
type="password"
required
placeholder="请输入当前密码"
@input="clearCurrentPasswordError"
:class="{ 'error': errors.currentPassword }"
:disabled="loading"
ref="currentPasswordInput"
tabindex="1"
autocomplete="current-password"
/>
<div v-if="errors.currentPassword" class="error-message">
{{ errors.currentPassword }}
</div>
</div>
<!-- 新密码 -->
<div class="form-group">
<label>新密码 *</label>
<input
v-model="form.newPassword"
type="password"
required
placeholder="请输入新密码"
@input="handleNewPasswordInput"
@keyup="updateRequirements(form.newPassword)"
:class="{ 'error': errors.newPassword }"
:disabled="loading"
ref="newPasswordInput"
tabindex="2"
autocomplete="new-password"
/>
<div v-if="errors.newPassword" class="error-message">
{{ errors.newPassword }}
</div>
<!-- 密码强度指示器 -->
<div class="password-strength" >
<div class="strength-bar">
<div
class="strength-fill"
:class="strengthClass"
:style="{ width: strengthPercentage + '%' }"
></div>
</div>
<div class="strength-text">
强度: {{ strengthText }} ({{ passwordLevel }})
</div>
</div>
<!-- 密码要求提示 -->
<div class="password-requirements" >
<div class="requirement" :class="{ 'met': requirements.length }">
密码长度{{ form.newPassword.length }}
</div>
<div class="requirement" :class="{ 'met': requirements.uppercase }">
包含大写字母
</div>
<div class="requirement" :class="{ 'met': requirements.lowercase }">
包含小写字母
</div>
<div class="requirement" :class="{ 'met': requirements.numbers }">
包含数字
</div>
<div class="requirement" :class="{ 'met': requirements.special }">
包含特殊字符
</div>
</div>
</div>
<!-- 确认新密码 -->
<div class="form-group">
<label>确认新密码 *</label>
<input
v-model="form.confirmPassword"
type="password"
required
placeholder="请再次输入新密码"
@input="handleConfirmPasswordInput"
:class="{ 'error': errors.confirmPassword }"
:disabled="loading"
ref="confirmPasswordInput"
tabindex="3"
autocomplete="new-password"
/>
<div v-if="errors.confirmPassword" class="error-message">
{{ errors.confirmPassword }}
</div>
</div>
<!-- 操作按钮 -->
<div class="form-actions">
<button
type="button"
class="btn btn-secondary"
@click="handleClose"
:disabled="loading"
>
取消
</button>
<button
name="submit-password-change"
type="submit"
class="btn btn-primary"
:disabled="loading || !isFormValid"
>
<span v-if="loading" class="loading-spinner"></span>
{{ submitText }}
</button>
</div>
<!-- 强制修改密码提示 -->
<div v-if="isForceChange" class="force-change-notice">
<div class="notice-icon"></div>
<div class="notice-text">
<strong>重要提示</strong>您的密码已被重置必须立即修改密码才能继续使用系统
</div>
</div>
</form>
</div>
</div>
</div>
</template>
<script>
import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue'
import { userService } from '@/modules/user-management'
export default {
name: 'PasswordChangeModal',
props: {
visible: {
type: Boolean,
default: false
},
isForceChange: {
type: Boolean,
default: false
}
},
emits: ['close', 'success'],
setup(props, { emit }) {
const loading = ref(false)
const policy = ref({
minLength: 6,
minCharTypes: 1,
preventReuse: 3,
level: 1,
minRequiredLevel: 1,
requireUppercase: false,
requireLowercase: false,
requireNumbers: false,
requireSpecial: false
})
const form = reactive({
currentPassword: '',
newPassword: '',
confirmPassword: ''
})
const errors = reactive({
currentPassword: '',
newPassword: '',
confirmPassword: ''
})
const requirements = reactive({
length: false,
uppercase: false,
lowercase: false,
numbers: false,
special: false
})
const passwordStrength = ref(0)
const passwordLevel = ref(0)
//
const title = computed(() => {
return props.isForceChange ? '强制修改密码' : '修改密码'
})
const submitText = computed(() => {
return props.isForceChange ? '确认修改' : '修改密码'
})
const strengthClass = computed(() => {
if (passwordStrength.value >= 4) return 'strong'
if (passwordStrength.value >= 3) return 'medium'
if (passwordStrength.value >= 2) return 'weak'
return 'very-weak'
})
const strengthText = computed(() => {
if (passwordStrength.value >= 4) return '强'
if (passwordStrength.value >= 3) return '中'
if (passwordStrength.value >= 2) return '弱'
return '很弱'
})
const strengthPercentage = computed(() => {
// 0-50-100
return (passwordStrength.value / 5) * 100
})
const isFormValid = computed(() => {
if (props.isForceChange) {
return form.newPassword && form.confirmPassword &&
form.newPassword === form.confirmPassword &&
!errors.newPassword && !errors.confirmPassword
}
return form.currentPassword && form.newPassword && form.confirmPassword &&
form.newPassword === form.confirmPassword &&
!errors.currentPassword && !errors.newPassword && !errors.confirmPassword
})
//
const loadPasswordPolicy = async () => {
try {
const response = await userService.getPasswordPolicy()
// response.data
if (response.data && response.data.code === 200 && response.data.data) {
policy.value = response.data.data
} else if (response && response.code === 200 && response.data) {
// response.dataresponse
policy.value = response.data
} else {
console.warn('密码策略响应格式不正确:', response)
console.warn('尝试解析响应结构...')
if (response.data) {
console.warn('response.data.code:', response.data.code)
console.warn('response.data.data:', response.data.data)
}
if (response) {
console.warn('response.code:', response.code)
console.warn('response.data:', response.data)
}
// 使
policy.value = {
minLength: 6,
minCharTypes: 1,
preventReuse: 3,
level: 1,
minRequiredLevel: 1,
requireUppercase: false,
requireLowercase: false,
requireNumbers: false,
requireSpecial: false
}
}
} catch (error) {
console.error('加载密码策略失败:', error)
// 使
policy.value = {
minLength: 6,
minCharTypes: 1,
preventReuse: 3,
level: 1,
minRequiredLevel: 1,
requireUppercase: false,
requireLowercase: false,
requireNumbers: false,
requireSpecial: false
}
}
}
const validatePassword = async () => {
errors.newPassword = ''
if (!form.newPassword) {
//
updateRequirements('')
return
}
try {
const response = await userService.validatePassword(form.newPassword)
//
let result = null
if (response.data && response.data.code === 200 && response.data.data) {
result = response.data.data
} else if (response && response.code === 200 && response.data) {
result = response.data
}
if (result) {
if (!result.is_valid) {
//
if (Array.isArray(result.errors)) {
errors.newPassword = result.errors.join('; ')
} else if (typeof result.errors === 'string') {
errors.newPassword = result.errors
} else {
errors.newPassword = '密码验证失败'
}
}
passwordStrength.value = result.strength || 0
passwordLevel.value = result.level || 0
//
updateRequirements(form.newPassword)
} else {
console.warn('密码验证响应格式不正确:', response)
// 使
updateRequirements(form.newPassword)
}
} catch (error) {
console.error('密码验证失败:', error)
// 使
updateRequirements(form.newPassword)
}
}
const updateRequirements = (password) => {
//
requirements.length = false
requirements.uppercase = false
requirements.lowercase = false
requirements.numbers = false
requirements.special = false
//
if (!password || password.length === 0) {
return
}
let hasUppercase = false
let hasLowercase = false
let hasNumbers = false
let hasSpecial = false
for (const char of password) {
// 使
const charCode = char.charCodeAt(0)
if (charCode >= 65 && charCode <= 90) { // A-Z
hasUppercase = true
} else if (charCode >= 97 && charCode <= 122) { // a-z
hasLowercase = true
} else if (charCode >= 48 && charCode <= 57) { // 0-9
hasNumbers = true
} else {
//
hasSpecial = true
}
}
//
requirements.length = password.length >= 6 // 6
requirements.uppercase = hasUppercase
requirements.lowercase = hasLowercase
requirements.numbers = hasNumbers
requirements.special = hasSpecial
//
let charTypes = 0
if (hasUppercase) charTypes++
if (hasLowercase) charTypes++
if (hasNumbers) charTypes++
if (hasSpecial) charTypes++
//
const strength = calculatePasswordStrength(password, charTypes)
passwordStrength.value = strength
passwordLevel.value = strength
}
//
const calculatePasswordStrength = (password, charTypes) => {
//
// 5>=8>=4
if (password.length >= 8 && charTypes >= 4) {
return 5
}
// 4>=8>=3
if (password.length >= 8 && charTypes >= 3) {
return 4
}
// 3>=6>=3
if (password.length >= 6 && charTypes >= 3) {
return 3
}
// 2>=6>=2
if (password.length >= 6 && charTypes >= 2) {
return 2
}
// 1>=6>=1
if (password.length >= 6 && charTypes >= 1) {
return 1
}
// 0>=1>=1
if (password.length >= 1) {
return 0
}
// 00
return 0
}
const validateConfirmPassword = () => {
errors.confirmPassword = ''
if (form.newPassword !== form.confirmPassword) {
errors.confirmPassword = '两次输入的密码不一致'
}
}
const handleSubmit = async () => {
if (!isFormValid.value) return
loading.value = true
//
Object.keys(errors).forEach(key => {
errors[key] = ''
})
try {
const requestData = {
current_password: form.currentPassword,
new_password: form.newPassword,
confirm_password: form.confirmPassword
}
const response = await userService.changePassword(requestData)
//
//
if (window.showToast) {
window.showToast({
type: 'success',
title: '密码修改成功',
content: '您的密码已成功修改'
})
} else {
// toast使alert
alert('密码修改成功!')
}
//
setTimeout(() => {
emit('success')
handleClose()
}, 1500)
} catch (error) {
//
let errorMessage = '修改密码失败,请重试'
let targetField = 'newPassword'
if (error.response?.data) {
const responseData = error.response.data
//
if (responseData.error) {
// 使error
errorMessage = responseData.error
//
if (errorMessage.includes('当前密码') || errorMessage.includes('current password')) {
targetField = 'currentPassword'
} else if (errorMessage.includes('新密码') || errorMessage.includes('new password')) {
targetField = 'newPassword'
} else if (errorMessage.includes('确认密码') || errorMessage.includes('confirm password')) {
targetField = 'confirmPassword'
}
} else if (responseData.message) {
// error使message
errorMessage = responseData.message
//
if (errorMessage.includes('当前密码') || errorMessage.includes('current password')) {
targetField = 'currentPassword'
} else if (errorMessage.includes('新密码') || errorMessage.includes('new password')) {
targetField = 'newPassword'
} else if (errorMessage.includes('确认密码') || errorMessage.includes('confirm password')) {
targetField = 'confirmPassword'
}
}
} else if (error.message) {
errorMessage = error.message
}
//
errors[targetField] = errorMessage
// newPassword
if (props.isForceChange && targetField === 'currentPassword') {
errors.newPassword = errorMessage
}
} finally {
loading.value = false
}
}
const handleClose = () => {
if (loading.value) return
// 使
resetFormState()
emit('close')
}
const handleOverlayClick = () => {
//
//
//
return false
}
const clearCurrentPasswordError = () => {
errors.currentPassword = ''
}
const clearNewPasswordError = () => {
errors.newPassword = ''
}
const clearConfirmPasswordError = () => {
errors.confirmPassword = ''
}
const handleNewPasswordInput = () => {
clearNewPasswordError()
validatePassword()
}
const handleConfirmPasswordInput = () => {
clearConfirmPasswordError()
validateConfirmPassword()
}
//
watch(() => props.visible, (newVal) => {
if (newVal) {
//
resetFormState()
loadPasswordPolicy()
//
nextTick(() => {
if (!props.isForceChange && form.currentPassword === '') {
//
try {
const currentPasswordInput = document.querySelector('input[type="password"]')
if (currentPasswordInput) {
currentPasswordInput.focus()
}
} catch (error) {
console.error('聚焦失败:', error)
}
} else {
//
try {
const newPasswordInput = document.querySelectorAll('input[type="password"]')[1]
if (newPasswordInput) {
newPasswordInput.focus()
}
} catch (error) {
console.error('聚焦失败:', error)
}
}
})
}
})
//
const resetFormState = () => {
//
form.currentPassword = ''
form.newPassword = ''
form.confirmPassword = ''
//
Object.keys(errors).forEach(key => {
errors[key] = ''
})
//
Object.keys(requirements).forEach(key => {
requirements[key] = false
})
//
passwordStrength.value = 0
passwordLevel.value = 0
// -
loading.value = false
//
nextTick(() => {
})
}
//
watch(() => form.newPassword, (newPassword) => {
if (newPassword) {
updateRequirements(newPassword)
} else {
//
Object.keys(requirements).forEach(key => {
requirements[key] = false
})
//
passwordStrength.value = 0
passwordLevel.value = 0
}
})
//
onMounted(() => {
if (props.visible) {
loadPasswordPolicy()
}
//
if (form.newPassword) {
updateRequirements(form.newPassword)
}
})
return {
loading,
policy,
form,
errors,
requirements,
passwordStrength,
passwordLevel,
title,
submitText,
strengthClass,
strengthText,
strengthPercentage,
isFormValid,
validatePassword,
validateConfirmPassword,
updateRequirements,
handleSubmit,
handleClose,
handleOverlayClick,
clearCurrentPasswordError,
clearNewPasswordError,
clearConfirmPasswordError,
handleNewPasswordInput,
handleConfirmPasswordInput,
resetFormState //
}
}
}
</script>
<style scoped>
.password-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.password-modal {
background: var(--card-bg);
border-radius: 12px;
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 8px 32px var(--shadow-color);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid var(--border-color);
}
.modal-header h3 {
margin: 0;
color: var(--text-primary);
font-size: 18px;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--text-muted);
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
}
.close-btn:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
.modal-body {
padding: 24px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--text-primary);
}
.form-group input {
width: 100%;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 14px;
background: var(--input-bg);
color: var(--text-primary);
transition: all 0.2s;
box-sizing: border-box;
}
.form-group input:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.2);
}
.form-group input.error {
border-color: #f44336;
}
/* 确保禁用状态下的输入框仍然可见 */
.form-group input:disabled {
opacity: 0.7;
background: var(--bg-secondary);
cursor: not-allowed;
}
/* 测试焦点样式 */
.form-group input:focus {
outline: 2px solid #ff9800;
outline-offset: 2px;
}
.error-message {
color: #f44336;
font-size: 12px;
margin-top: 4px;
}
/* 密码强度指示器 */
.password-strength {
margin-top: 12px;
}
.strength-bar {
width: 100%;
height: 6px;
background: #e0e0e0;
border-radius: 3px;
overflow: hidden;
margin-bottom: 8px;
}
.strength-fill {
height: 100%;
transition: width 0.3s ease;
}
.strength-fill.very-weak {
background: #f44336;
}
.strength-fill.weak {
background: #ff9800;
}
.strength-fill.medium {
background: #ffc107;
}
.strength-fill.strong {
background: #4caf50;
}
.strength-text {
font-size: 12px;
color: var(--text-secondary);
text-align: center;
}
/* 密码要求提示 */
.password-requirements {
margin-top: 12px;
padding: 12px;
background: var(--bg-secondary);
border-radius: 6px;
font-size: 12px;
}
.requirement {
color: var(--text-muted);
margin-bottom: 4px;
transition: color 0.2s;
}
.requirement.met {
color: #4caf50;
}
.requirement:last-child {
margin-bottom: 0;
}
/* 操作按钮 */
.form-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 30px;
}
/* 强制修改密码提示 */
.force-change-notice {
margin-top: 20px;
padding: 16px;
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 8px;
display: flex;
align-items: flex-start;
gap: 12px;
}
.notice-icon {
font-size: 20px;
flex-shrink: 0;
}
.notice-text {
color: #856404;
font-size: 14px;
line-height: 1.5;
}
.notice-text strong {
color: #856404;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background: var(--accent-color);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: var(--accent-hover);
}
.btn-secondary {
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-secondary:hover:not(:disabled) {
background: var(--bg-tertiary);
}
/* 加载动画 */
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 768px) {
.password-modal {
width: 95%;
margin: 20px;
}
.modal-header,
.modal-body {
padding: 16px;
}
.form-actions {
flex-direction: column;
}
.btn {
width: 100%;
justify-content: center;
}
}
</style>

2
gofaster/app/src/renderer/modules/core/components/MainLayout.vue

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
<template>
<template authType="public">
<div class="main-layout">
<!-- 顶部导航栏 -->
<header class="header">

6
gofaster/app/src/renderer/modules/role-management/components/PermissionManager.vue

@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
<div class="modal" @click.stop>
<div class="modal-header">
<h3>权限管理</h3>
<button name="close-permission-manager" class="close-btn" @click="handleClose">
<button class="close-btn" @click="handleClose">
<i class="fas fa-times"></i>
</button>
</div>
@ -54,8 +54,8 @@ @@ -54,8 +54,8 @@
</div>
<div class="modal-footer">
<button name="cancel-permission-manager" class="btn btn-secondary" @click="handleClose">取消</button>
<button name="save-permissions" class="btn btn-primary" @click="savePermissions" :disabled="saving">
<button class="btn btn-secondary" @click="handleClose">取消</button>
<button class="btn btn-primary" @click="savePermissions" :disabled="saving">
{{ saving ? '保存中...' : '保存权限' }}
</button>
</div>

10
gofaster/app/src/renderer/modules/role-management/components/RolePermissionAssignment.vue

@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
<div class="modal large-modal" @click.stop>
<div class="modal-header">
<h3>角色权限分配 - {{ currentRole?.name }}</h3>
<button name="close-role-permission-assignment" class="close-btn" @click="handleClose">
<button class="close-btn" @click="handleClose">
<i class="fas fa-times"></i>
</button>
</div>
@ -38,7 +38,6 @@ @@ -38,7 +38,6 @@
<div class="permission-assignment">
<div class="assignment-tabs">
<button
name="tab-assigned-permissions"
class="tab-btn"
:class="{ active: activeTab === 'assigned' }"
@click="activeTab = 'assigned'"
@ -47,7 +46,6 @@ @@ -47,7 +46,6 @@
已分配权限 ({{ assignedPermissions.length }})
</button>
<button
name="tab-available-permissions"
class="tab-btn"
:class="{ active: activeTab === 'available' }"
@click="activeTab = 'available'"
@ -65,8 +63,7 @@ @@ -65,8 +63,7 @@
<button
class="btn btn-sm btn-danger"
@click="removeSelectedPermissions"
:disabled="selectedAssignedPermissions.length === 0"
name="rolepermissionassignment-cwppvx">
:disabled="selectedAssignedPermissions.length === 0">
<i class="fas fa-minus"></i>
移除选中 ({{ selectedAssignedPermissions.length }})
</button>
@ -128,8 +125,7 @@ @@ -128,8 +125,7 @@
<button
class="btn btn-sm btn-primary"
@click="assignSelectedPermissions"
:disabled="selectedAvailablePermissions.length === 0"
name="rolepermissionassignment-2m2snp">
:disabled="selectedAvailablePermissions.length === 0">
<i class="fas fa-plus"></i>
分配选中 ({{ selectedAvailablePermissions.length }})
</button>

2
gofaster/app/src/renderer/modules/role-management/components/UserRoleAssignment.vue

@ -82,7 +82,7 @@ @@ -82,7 +82,7 @@
<div class="modal-footer">
<button class="btn btn-secondary" @click="handleClose">取消</button>
<button class="btn btn-primary" @click="saveRoleAssignment" :disabled="saving" name="userroleassignment-xbmtac">
<button class="btn btn-primary" @click="saveRoleAssignment" :disabled="saving">
{{ saving ? '保存中...' : '保存分配' }}
</button>
</div>

8
gofaster/app/src/renderer/modules/role-management/views/RoleManagement.vue

@ -12,7 +12,7 @@ @@ -12,7 +12,7 @@
/>
</div>
<div class="filters">
<button name="create-role" class="btn btn-primary" @click="createNewRole">
<button class="btn btn-primary" @click="createNewRole">
<i class="fas fa-plus"></i> 新建角色
</button>
</div>
@ -27,7 +27,7 @@ @@ -27,7 +27,7 @@
<div v-else-if="roles.length === 0" class="empty-state">
<p><i class="fas fa-inbox"></i> 暂无角色数据</p>
<button name="create-first-role" class="btn btn-primary" @click="createNewRole">
<button class="btn btn-primary" @click="createNewRole">
<i class="fas fa-plus"></i> 创建第一个角色
</button>
</div>
@ -58,7 +58,7 @@ @@ -58,7 +58,7 @@
<button class="btn btn-sm btn-primary" @click="assignPermissions(role)" title="分配权限">
<i class="fas fa-shield-alt"></i>
</button>
<button name="delete-role" class="btn btn-sm btn-danger" @click="deleteRole(role)" title="删除角色">
<button class="btn btn-sm btn-danger" @click="deleteRole(role)" title="删除角色">
<i class="fas fa-trash"></i>
</button>
</div>
@ -177,7 +177,7 @@ @@ -177,7 +177,7 @@
<button type="button" class="btn btn-secondary" @click="showCreateDialog = false">
取消
</button>
<button name="save-role" type="submit" class="btn btn-primary" :disabled="saving">
<button type="submit" class="btn btn-primary" :disabled="saving">
{{ saving ? '保存中...' : (editingRole ? '更新' : '创建') }}
</button>
</div>

145
gofaster/app/src/renderer/modules/route-sync/direct-route-mappings.js

@ -48,8 +48,8 @@ export default { @@ -48,8 +48,8 @@ export default {
},
{
"component": "RoleManagement",
"triggerName": "loadRoles",
"triggerType": "method"
"triggerName": "save-role",
"triggerType": "button"
}
]
},
@ -72,8 +72,8 @@ export default { @@ -72,8 +72,8 @@ export default {
"triggerSources": [
{
"component": "RoleManagement",
"triggerName": "saveRole",
"triggerType": "method"
"triggerName": "save-role",
"triggerType": "button"
}
]
},
@ -84,13 +84,13 @@ export default { @@ -84,13 +84,13 @@ export default {
"triggerSources": [
{
"component": "PermissionManager",
"triggerName": "savePermissions",
"triggerType": "method"
"triggerName": "save-permissions",
"triggerType": "button"
},
{
"component": "RoleManagement",
"triggerName": "saveRole",
"triggerType": "method"
"triggerName": "save-role",
"triggerType": "button"
}
]
},
@ -101,8 +101,8 @@ export default { @@ -101,8 +101,8 @@ export default {
"triggerSources": [
{
"component": "RoleManagement",
"triggerName": "deleteRole",
"triggerType": "method"
"triggerName": "delete-role",
"triggerType": "button"
}
]
},
@ -130,8 +130,8 @@ export default { @@ -130,8 +130,8 @@ export default {
"triggerSources": [
{
"component": "UserRoleAssignment",
"triggerName": "saveRoleAssignment",
"triggerType": "method"
"triggerName": "userroleassignment-xbmtac",
"triggerType": "button"
}
]
},
@ -154,8 +154,8 @@ export default { @@ -154,8 +154,8 @@ export default {
"triggerSources": [
{
"component": "UserRoleAssignment",
"triggerName": "saveRoleAssignment",
"triggerType": "method"
"triggerName": "userroleassignment-xbmtac",
"triggerType": "button"
}
]
},
@ -166,8 +166,8 @@ export default { @@ -166,8 +166,8 @@ export default {
"triggerSources": [
{
"component": "RolePermissionAssignment",
"triggerName": "loadRolePermissions",
"triggerType": "method"
"triggerName": "rolepermissionassignment-krq0js",
"triggerType": "button"
}
]
},
@ -178,13 +178,13 @@ export default { @@ -178,13 +178,13 @@ export default {
"triggerSources": [
{
"component": "RolePermissionAssignment",
"triggerName": "assignPermission",
"triggerType": "method"
"triggerName": "rolepermissionassignment-krq0js",
"triggerType": "button"
},
{
"component": "RolePermissionAssignment",
"triggerName": "assignSelectedPermissions",
"triggerType": "method"
"triggerName": "rolepermissionassignment-2m2snp",
"triggerType": "button"
}
]
},
@ -195,13 +195,13 @@ export default { @@ -195,13 +195,13 @@ export default {
"triggerSources": [
{
"component": "RolePermissionAssignment",
"triggerName": "removePermission",
"triggerType": "method"
"triggerName": "rolepermissionassignment-qa8mqd",
"triggerType": "button"
},
{
"component": "RolePermissionAssignment",
"triggerName": "removeSelectedPermissions",
"triggerType": "method"
"triggerName": "rolepermissionassignment-cwppvx",
"triggerType": "button"
}
]
}
@ -218,17 +218,11 @@ export default { @@ -218,17 +218,11 @@ export default {
"triggerSources": [
{
"component": "UserManagement",
"triggerName": "loadUsers",
"triggerType": "method"
"triggerName": "usermanagement-9jq7qi",
"triggerType": "link"
}
]
},
{
"apiMethodName": "getUser",
"method": "GET",
"path": "/api/auth/admin/users/{id}",
"triggerSources": []
},
{
"apiMethodName": "createUser",
"method": "POST",
@ -236,8 +230,8 @@ export default { @@ -236,8 +230,8 @@ export default {
"triggerSources": [
{
"component": "UserManagement",
"triggerName": "submitUser",
"triggerType": "method"
"triggerName": "usermanagement-tcfjru",
"triggerType": "button"
}
]
},
@ -248,8 +242,8 @@ export default { @@ -248,8 +242,8 @@ export default {
"triggerSources": [
{
"component": "UserManagement",
"triggerName": "submitUser",
"triggerType": "method"
"triggerName": "usermanagement-tcfjru",
"triggerType": "button"
}
]
},
@ -260,17 +254,11 @@ export default { @@ -260,17 +254,11 @@ export default {
"triggerSources": [
{
"component": "UserManagement",
"triggerName": "deleteUser",
"triggerType": "method"
"triggerName": "usermanagement-djl8u7",
"triggerType": "button"
}
]
},
{
"apiMethodName": "getRoles",
"method": "GET",
"path": "/api/auth/admin/roles",
"triggerSources": []
},
{
"apiMethodName": "changePassword",
"method": "POST",
@ -278,8 +266,8 @@ export default { @@ -278,8 +266,8 @@ export default {
"triggerSources": [
{
"component": "PasswordChangeModal",
"triggerName": "handleSubmit",
"triggerType": "method"
"triggerName": "passwordchangemodal-krxyp6",
"triggerType": "button"
}
]
},
@ -302,58 +290,11 @@ export default { @@ -302,58 +290,11 @@ export default {
"triggerSources": [
{
"component": "PasswordChangeModal",
"triggerName": "validatePassword",
"triggerType": "method"
}
]
},
{
"apiMethodName": "checkPasswordStatus",
"method": "GET",
"path": "/api/auth/password-status",
"triggerSources": []
},
{
"apiMethodName": "getPermissions",
"method": "GET",
"path": "/api/permissions",
"triggerSources": []
},
{
"apiMethodName": "getCaptcha",
"method": "GET",
"path": "/api/auth/captcha",
"triggerSources": [
{
"component": "LoginModal",
"triggerName": "refreshCaptcha",
"triggerType": "method"
},
{
"component": "LoginModal",
"triggerName": "refreshCaptchaWithoutClearError",
"triggerType": "method"
"triggerName": "passwordchangemodal-sfn0g2",
"triggerType": "input"
}
]
},
{
"apiMethodName": "login",
"method": "POST",
"path": "/api/auth/login",
"triggerSources": [
{
"component": "LoginModal",
"triggerName": "handleLogin",
"triggerType": "method"
}
]
},
{
"apiMethodName": "logout",
"method": "POST",
"path": "/api/auth/logout",
"triggerSources": []
},
{
"apiMethodName": "getCurrentUser",
"method": "GET",
@ -361,8 +302,8 @@ export default { @@ -361,8 +302,8 @@ export default {
"triggerSources": [
{
"component": "UserProfile",
"triggerName": "loadUserProfile",
"triggerType": "method"
"triggerName": "userprofile-llgkyu",
"triggerType": "button"
}
]
},
@ -373,16 +314,10 @@ export default { @@ -373,16 +314,10 @@ export default {
"triggerSources": [
{
"component": "PasswordChangeModal",
"triggerName": "handleSubmit",
"triggerType": "method"
"triggerName": "passwordchangemodal-krxyp6",
"triggerType": "button"
}
]
},
{
"apiMethodName": "resetPassword",
"method": "POST",
"path": "/api/auth/reset-password",
"triggerSources": []
}
]
}

2
gofaster/app/src/renderer/modules/user-management/components/LoginModal.vue

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
<template>
<template authType="public">
<div v-if="visible" class="login-modal-overlay">
<div class="login-modal" @click.stop>
<div class="login-modal-header">

2
gofaster/app/src/renderer/modules/user-management/components/PasswordChangeModal.vue

@ -118,7 +118,7 @@ @@ -118,7 +118,7 @@
type="submit"
class="btn btn-primary"
:disabled="loading || !isFormValid"
name="passwordchangemodal-krxyp6">
>
<span v-if="loading" class="loading-spinner"></span>
{{ submitText }}
</button>

4
gofaster/app/src/renderer/modules/user-management/views/UserManagement.vue

@ -71,7 +71,7 @@ @@ -71,7 +71,7 @@
<button class="btn btn-sm btn-primary" @click="assignRoles(user)">
<i class="fas fa-user-shield"></i>
</button>
<button class="btn btn-sm btn-danger" @click="deleteUser(user.id)" name="usermanagement-djl8u7">
<button class="btn btn-sm btn-danger" @click="deleteUser(user.id)">
<i class="fas fa-trash"></i>
</button>
</div>
@ -200,7 +200,7 @@ @@ -200,7 +200,7 @@
<button type="button" class="btn btn-secondary" @click="closeModal">
取消
</button>
<button type="submit" class="btn btn-primary" name="usermanagement-tcfjru">
<button type="submit" class="btn btn-primary">
{{ showEditUserModal ? '更新' : '创建' }}
</button>
</div>

2
gofaster/app/src/renderer/modules/user-management/views/UserProfile.vue

@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
<div class="error-icon"><i class="fas fa-exclamation-triangle"></i></div>
<h3>加载失败</h3>
<p>{{ error }}</p>
<button @click="loadUserProfile" class="retry-btn" name="userprofile-llgkyu">
<button @click="loadUserProfile" class="retry-btn">
<i class="fas fa-redo"></i> 重试
</button>
</div>

Loading…
Cancel
Save