feat: 添加Neo4j图数据库支持及前端代码重构

- 新增 Neo4j 图数据库 handler、service、model
- 后端添加 SaveGraph API 接口
- 前端 Database.vue 重构,拆分为独立组件
- 新增 web/src/views/database/ 组件目录
- 删除临时文件 (temp_*.go)
- 添加 Neo4j 相关 API 需求文档

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 09:11:08 +08:00
parent 20015dbd2a
commit c917d6b04c
41 changed files with 4453 additions and 1021 deletions

95
web/src/utils/format.ts Normal file
View File

@@ -0,0 +1,95 @@
/**
* 格式化工具函数
*/
/**
* 格式化日期
* @param date - 日期字符串或 Date 对象
* @param format - 格式化模板,默认 'YYYY-MM-DD'
*/
export function formatDate(date: string | Date, format: string = 'YYYY-MM-DD'): string {
if (!date) return ''
const d = typeof date === 'string' ? new Date(date) : date
if (isNaN(d.getTime())) return ''
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hours = String(d.getHours()).padStart(2, '0')
const minutes = String(d.getMinutes()).padStart(2, '0')
const seconds = String(d.getSeconds()).padStart(2, '0')
return format
.replace('YYYY', String(year))
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds)
}
/**
* 格式化数字(千分位)
* @param num - 数字
*/
export function formatNumber(num: number | string): string {
if (num === null || num === undefined) return ''
const n = typeof num === 'string' ? parseFloat(num) : num
if (isNaN(n)) return ''
return n.toLocaleString()
}
/**
* 格式化文件大小
* @param bytes - 字节数
*/
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B'
const units = ['B', 'KB', 'MB', 'GB', 'TB']
const k = 1024
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${units[i]}`
}
/**
* 截断文本
* @param text - 文本
* @param maxLength - 最大长度
* @param suffix - 后缀,默认 '...'
*/
export function truncate(text: string, maxLength: number, suffix: string = '...'): string {
if (!text || text.length <= maxLength) return text
return text.slice(0, maxLength) + suffix
}
/**
* 首字母大写
* @param text - 文本
*/
export function capitalize(text: string): string {
if (!text) return ''
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase()
}
/**
* 格式化时长(秒)
* @param seconds - 秒数
*/
export function formatDuration(seconds: number): string {
if (!seconds || seconds < 0) return '0s'
const h = Math.floor(seconds / 3600)
const m = Math.floor((seconds % 3600) / 60)
const s = Math.floor(seconds % 60)
const parts: string[] = []
if (h > 0) parts.push(`${h}h`)
if (m > 0) parts.push(`${m}m`)
if (s > 0 || parts.length === 0) parts.push(`${s}s`)
return parts.join(' ')
}

128
web/src/utils/validate.ts Normal file
View File

@@ -0,0 +1,128 @@
/**
* 校验工具函数
*/
/**
* 校验邮箱
*/
export function isEmail(value: string): boolean {
const reg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return reg.test(value)
}
/**
* 校验手机号(中国大陆)
*/
export function isPhone(value: string): boolean {
const reg = /^1[3-9]\d{9}$/
return reg.test(value)
}
/**
* 校验 URL
*/
export function isUrl(value: string): boolean {
try {
new URL(value)
return true
} catch {
return false
}
}
/**
* 校验 IP 地址
*/
export function isIP(value: string): boolean {
const reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
return reg.test(value)
}
/**
* 校验端口号
*/
export function isPort(value: string | number): boolean {
const port = typeof value === 'string' ? parseInt(value) : value
return !isNaN(port) && port >= 1 && port <= 65535
}
/**
* 校验必填
*/
export function isRequired(value: any): boolean {
if (value === null || value === undefined) return false
if (typeof value === 'string') return value.trim().length > 0
if (Array.isArray(value)) return value.length > 0
return true
}
/**
* 校验最小长度
*/
export function minLength(value: string, min: number): boolean {
return value.length >= min
}
/**
* 校验最大长度
*/
export function maxLength(value: string, max: number): boolean {
return value.length <= max
}
/**
* 校验数值范围
*/
export function inRange(value: number, min: number, max: number): boolean {
return value >= min && value <= max
}
/**
* 校验密码强度
* @returns 0-4, 0=无, 1=弱, 2=中, 3=强, 4=很强
*/
export function passwordStrength(password: string): number {
if (!password) return 0
let strength = 0
// 长度
if (password.length >= 8) strength++
if (password.length >= 12) strength++
// 字符类型
if (/[a-z]/.test(password)) strength++
if (/[A-Z]/.test(password)) strength++
if (/[0-9]/.test(password)) strength++
if (/[^a-zA-Z0-9]/.test(password)) strength++
return Math.min(strength, 4)
}
/**
* 校验对象
*/
export interface ValidationRule {
validator: (value: any) => boolean
message: string
}
export interface ValidationResult {
valid: boolean
errors: string[]
}
export function validate(value: any, rules: ValidationRule[]): ValidationResult {
const errors: string[] = []
for (const rule of rules) {
if (!rule.validator(value)) {
errors.push(rule.message)
}
}
return {
valid: errors.length === 0,
errors,
}
}