diff --git a/README.md b/README.md
new file mode 100644
index 0000000..63db575
--- /dev/null
+++ b/README.md
@@ -0,0 +1,53 @@
+# Memora
+
+一个背单词应用(Web + Go API + 记忆引擎)。
+
+## 目录
+
+- `memora-web`:前端(Vue3 + Vite + Element Plus)
+- `memora-api`:后端(Go + Gin + GORM + MySQL)
+- `memora-engine`:记忆引擎(后续可抽成独立服务/库)
+
+## 快速启动(推荐 Docker)
+
+> 由于当前机器可能未安装 Go,本项目提供 Docker 方式启动。
+
+1. 进入项目根目录:
+
+```bash
+cd /home/wsy182/Documents/code/memora
+```
+
+2. 复制并编辑后端配置:
+
+```bash
+cp memora-api/config.yaml memora-api/config.local.yaml
+```
+
+3. 启动:
+
+```bash
+docker compose up -d --build
+```
+
+- API: http://localhost:8080
+- Web: http://localhost:3000
+
+## 本地启动(不使用 Docker)
+
+### 启动 API
+
+```bash
+cd memora-api
+# 确保已安装 Go 1.21+
+go mod tidy
+go run main.go
+```
+
+### 启动 Web
+
+```bash
+cd memora-web
+npm install
+npm run dev
+```
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..c9f6b47
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,33 @@
+services:
+ mysql:
+ image: mysql:8.0
+ environment:
+ MYSQL_ROOT_PASSWORD: root
+ MYSQL_DATABASE: memora
+ ports:
+ - "3306:3306"
+ command: ["--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"]
+ volumes:
+ - memora_mysql:/var/lib/mysql
+
+ api:
+ build:
+ context: ./memora-api
+ depends_on:
+ - mysql
+ ports:
+ - "8080:8080"
+ volumes:
+ - ./memora-api/audio:/app/audio
+ - ./memora-api/config.yaml:/app/config.yaml:ro
+
+ web:
+ build:
+ context: ./memora-web
+ depends_on:
+ - api
+ ports:
+ - "3000:3000"
+
+volumes:
+ memora_mysql:
diff --git a/memora-api/Dockerfile b/memora-api/Dockerfile
new file mode 100644
index 0000000..e4a847a
--- /dev/null
+++ b/memora-api/Dockerfile
@@ -0,0 +1,15 @@
+FROM golang:1.21-alpine AS build
+WORKDIR /src
+RUN apk add --no-cache git
+COPY go.mod ./
+RUN go mod download
+COPY . .
+RUN go build -o /out/memora-api ./main.go
+
+FROM alpine:3.19
+WORKDIR /app
+COPY --from=build /out/memora-api /app/memora-api
+COPY config.yaml /app/config.yaml
+RUN mkdir -p /app/audio
+EXPOSE 8080
+CMD ["/app/memora-api"]
diff --git a/memora-api/config.yaml b/memora-api/config.yaml
new file mode 100644
index 0000000..805e7cf
--- /dev/null
+++ b/memora-api/config.yaml
@@ -0,0 +1,18 @@
+server:
+ host: 0.0.0.0
+ port: 8080
+
+database:
+ host: localhost
+ port: 3306
+ user: root
+ password: root
+ name: memora
+ charset: utf8mb4
+
+youdao:
+ app_id: "29a8d1df97e9e709"
+ app_secret: "o3bC1llh9XQoVPuWuE3fbbqYU58zpTTn"
+
+audio:
+ path: ./audio
diff --git a/memora-api/go.mod b/memora-api/go.mod
new file mode 100644
index 0000000..fdcea9e
--- /dev/null
+++ b/memora-api/go.mod
@@ -0,0 +1,39 @@
+module memora-api
+
+go 1.21
+
+require (
+ github.com/gin-gonic/gin v1.9.1
+ gopkg.in/yaml.v3 v3.0.1
+ gorm.io/driver/mysql v1.5.2
+ gorm.io/gorm v1.25.5
+)
+
+require (
+ github.com/bytedance/sonic v1.9.1 // indirect
+ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.2 // indirect
+ github.com/gin-contrib/sse v0.1.0 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-playground/validator/v10 v10.14.0 // indirect
+ github.com/go-sql-driver/mysql v1.7.0 // indirect
+ github.com/goccy/go-json v0.10.2 // indirect
+ github.com/jinzhu/inflection v1.0.0 // indirect
+ github.com/jinzhu/now v1.1.5 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.4 // indirect
+ github.com/leodido/go-urn v1.2.4 // indirect
+ github.com/mattn/go-isatty v0.0.19 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/pelletier/go-toml/v2 v2.0.8 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.2.11 // indirect
+ golang.org/x/arch v0.3.0 // indirect
+ golang.org/x/crypto v0.9.0 // indirect
+ golang.org/x/net v0.10.0 // indirect
+ golang.org/x/sys v0.8.0 // indirect
+ golang.org/x/text v0.9.0 // indirect
+ google.golang.org/protobuf v1.30.0 // indirect
+)
diff --git a/memora-api/internal/config/config.go b/memora-api/internal/config/config.go
new file mode 100644
index 0000000..1c63117
--- /dev/null
+++ b/memora-api/internal/config/config.go
@@ -0,0 +1,61 @@
+package config
+
+import (
+ "fmt"
+ "os"
+
+ "gopkg.in/yaml.v3"
+)
+
+type Config struct {
+ Server ServerConfig `yaml:"server"`
+ Database DatabaseConfig `yaml:"database"`
+ Youdao YoudaoConfig `yaml:"youdao"`
+ Audio AudioConfig `yaml:"audio"`
+}
+
+type ServerConfig struct {
+ Host string `yaml:"host"`
+ Port int `yaml:"port"`
+}
+
+type DatabaseConfig struct {
+ Host string `yaml:"host"`
+ Port int `yaml:"port"`
+ User string `yaml:"user"`
+ Password string `yaml:"password"`
+ Name string `yaml:"name"`
+ Charset string `yaml:"charset"`
+}
+
+type YoudaoConfig struct {
+ AppID string `yaml:"app_id"`
+ AppKey string `yaml:"app_key"` // 兼容旧字段
+ AppSecret string `yaml:"app_secret"`
+}
+
+type AudioConfig struct {
+ Path string `yaml:"path"`
+}
+
+func (d *DatabaseConfig) DSN() string {
+ return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
+ d.User, d.Password, d.Host, d.Port, d.Name, d.Charset)
+}
+
+var AppConfig *Config
+
+func LoadConfig(path string) (*Config, error) {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+
+ var cfg Config
+ if err := yaml.Unmarshal(data, &cfg); err != nil {
+ return nil, err
+ }
+
+ AppConfig = &cfg
+ return &cfg, nil
+}
diff --git a/memora-api/internal/handler/word.go b/memora-api/internal/handler/word.go
new file mode 100644
index 0000000..4696235
--- /dev/null
+++ b/memora-api/internal/handler/word.go
@@ -0,0 +1,140 @@
+package handler
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "memora-api/internal/model"
+ "memora-api/internal/service"
+
+ "github.com/gin-gonic/gin"
+)
+
+type WordHandler struct {
+ wordService *service.WordService
+}
+
+func NewWordHandler(wordService *service.WordService) *WordHandler {
+ return &WordHandler{wordService: wordService}
+}
+
+// 添加单词(记忆模式)
+func (h *WordHandler) AddWord(c *gin.Context) {
+ var req model.AddWordRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ // 调用有道API查询
+ youdaoResp, err := h.wordService.QueryWord(req.Word)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败: " + err.Error()})
+ return
+ }
+
+ if youdaoResp.ErrorCode != "0" && youdaoResp.ErrorCode != "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("有道API错误: %s", youdaoResp.ErrorCode)})
+ return
+ }
+
+ // 保存到数据库
+ word, err := h.wordService.SaveWord(req.Word, youdaoResp)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "保存失败: " + err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": "单词添加成功",
+ "data": word,
+ })
+}
+
+// 获取复习单词列表
+func (h *WordHandler) GetReviewWords(c *gin.Context) {
+ mode := c.DefaultQuery("mode", "spelling")
+ limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
+
+ words, err := h.wordService.GetReviewWords(mode, limit)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": words})
+}
+
+// 提交复习答案
+func (h *WordHandler) SubmitReview(c *gin.Context) {
+ var req model.ReviewAnswerRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ result, err := h.wordService.SubmitReviewAnswer(req)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": result})
+}
+
+// 获取所有单词
+func (h *WordHandler) GetWords(c *gin.Context) {
+ limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
+ offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
+
+ words, total, err := h.wordService.GetAllWords(limit, offset)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "data": words,
+ "total": total,
+ })
+}
+
+// 获取统计信息
+func (h *WordHandler) GetStatistics(c *gin.Context) {
+ stats, err := h.wordService.GetStatistics()
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": stats})
+}
+
+// 获取音频文件
+func (h *WordHandler) GetAudio(c *gin.Context) {
+ word := c.Query("word")
+ audioType := c.DefaultQuery("type", "uk")
+
+ if word == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "缺少word参数"})
+ return
+ }
+
+ // very small sanitization
+ word = strings.TrimSpace(word)
+ if word == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "word为空"})
+ return
+ }
+ for _, r := range word {
+ if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z') {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "word包含非法字符"})
+ return
+ }
+ }
+ filename := word + "_" + audioType + ".mp3"
+ path := "./audio/" + filename
+ c.File(path)
+}
diff --git a/memora-api/internal/model/model.go b/memora-api/internal/model/model.go
new file mode 100644
index 0000000..790c71f
--- /dev/null
+++ b/memora-api/internal/model/model.go
@@ -0,0 +1,86 @@
+package model
+
+import (
+ "time"
+)
+
+type Word struct {
+ ID int64 `json:"id" gorm:"primaryKey"`
+ Word string `json:"word" gorm:"size:100;uniqueIndex;not null"`
+ PhoneticUK string `json:"phonetic_uk" gorm:"size:255"`
+ PhoneticUS string `json:"phonetic_us" gorm:"size:255"`
+ AudioUK string `json:"audio_uk" gorm:"size:500"`
+ AudioUS string `json:"audio_us" gorm:"size:500"`
+ PartOfSpeech string `json:"part_of_speech" gorm:"size:50"`
+ Definition string `json:"definition" gorm:"type:text"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
+func (Word) TableName() string {
+ return "words"
+}
+
+type MemoryRecord struct {
+ ID int64 `json:"id" gorm:"primaryKey"`
+ WordID int64 `json:"word_id" gorm:"index;not null"`
+ UserID int64 `json:"user_id" gorm:"index;default:1"`
+ CorrectCount int `json:"correct_count" gorm:"default:0"`
+ TotalCount int `json:"total_count" gorm:"default:0"`
+ MasteryLevel int `json:"mastery_level" gorm:"default:0"`
+ LastReviewedAt *time.Time `json:"last_reviewed_at"`
+ NextReviewAt *time.Time `json:"next_review_at" gorm:"index"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+ Word *Word `json:"word,omitempty" gorm:"foreignKey:WordID"`
+}
+
+func (MemoryRecord) TableName() string {
+ return "memory_records"
+}
+
+// 请求响应结构
+type AddWordRequest struct {
+ Word string `json:"word" binding:"required"`
+}
+
+type ReviewAnswerRequest struct {
+ RecordID int64 `json:"record_id" binding:"required"`
+ Answer string `json:"answer" binding:"required"`
+ Mode string `json:"mode" binding:"required"` // spelling, en2cn, cn2en
+}
+
+type ReviewResult struct {
+ Word *Word `json:"word"`
+ Correct bool `json:"correct"`
+ Answer string `json:"answer"`
+ CorrectAns string `json:"correct_ans,omitempty"`
+}
+
+// 有道API响应 (标准API)
+type YoudaoResponse struct {
+ Query string `json:"query"`
+ Translation []string `json:"translation"`
+ Basic struct {
+ Phonetic string `json:"phonetic"`
+ UkPhonetic string `json:"uk-phonetic"`
+ UsPhonetic string `json:"us-phonetic"`
+ ExamType []string `json:"exam_type"`
+ Wfs []struct {
+ Wf struct {
+ Name string `json:"name"`
+ } `json:"wf"`
+ Means []struct {
+ Mean struct {
+ Text string `json:"text"`
+ } `json:"mean"`
+ } `json:"means"`
+ } `json:"wfs"`
+ } `json:"basic"`
+ SpeakUrl string `json:"speakUrl"`
+ SpeakFile string `json:"speakFile"`
+ Web []struct {
+ Value []string `json:"value"`
+ } `json:"web"`
+ ErrorCode string `json:"errorCode"`
+}
diff --git a/memora-api/internal/service/word.go b/memora-api/internal/service/word.go
new file mode 100644
index 0000000..9a1925a
--- /dev/null
+++ b/memora-api/internal/service/word.go
@@ -0,0 +1,354 @@
+package service
+
+import (
+ "crypto/md5"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "regexp"
+ "strings"
+ "time"
+
+ "memora-api/internal/config"
+ "memora-api/internal/model"
+
+ "gorm.io/gorm"
+)
+
+type WordService struct {
+ db *gorm.DB
+}
+
+func NewWordService(db *gorm.DB) *WordService {
+ return &WordService{db: db}
+}
+
+// 查询单词(调用有道API)
+func (s *WordService) QueryWord(word string) (*model.YoudaoResponse, error) {
+ // 优先用 app_id,兼容 app_key
+ appID := config.AppConfig.Youdao.AppID
+ if appID == "" {
+ appID = config.AppConfig.Youdao.AppKey
+ }
+ appSecret := config.AppConfig.Youdao.AppSecret
+
+ // 未配置有道密钥时,先走 mock(保证流程可跑通)
+ if strings.TrimSpace(appID) == "" || strings.TrimSpace(appSecret) == "" {
+ return &model.YoudaoResponse{ErrorCode: "0"}, nil
+ }
+
+ // 有道 API 签名算法: sign = MD5(appKey + q + salt + appSecret)
+ salt := fmt.Sprintf("%d", time.Now().UnixMilli())
+ q := strings.ToLower(word)
+ rawStr := appID + q + salt + appSecret
+ hash := md5.Sum([]byte(rawStr))
+ sign := hex.EncodeToString(hash[:])
+
+ // 调用有道 API - 智能词典查询
+ url := fmt.Sprintf("https://openapi.youdao.com/api?q=%s&from=en&to=zh_CHS&appKey=%s&salt=%s&sign=%s",
+ word, appID, salt, sign)
+
+ resp, err := http.Get(url)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ var result model.YoudaoResponse
+ if err := json.Unmarshal(body, &result); err != nil {
+ return nil, err
+ }
+
+ // Debug: 打印响应
+ fmt.Printf("[Youdao API] word=%s, errorCode=%s, translation=%v\n", word, result.ErrorCode, result.Translation)
+
+ return &result, nil
+}
+
+// 下载音频文件
+func (s *WordService) DownloadAudio(url, filePath string) error {
+ if url == "" {
+ return nil
+ }
+
+ resp, err := http.Get(url)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("download failed: %d", resp.StatusCode)
+ }
+
+ // 创建目录
+ dir := filePath[:strings.LastIndex(filePath, "/")]
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return err
+ }
+
+ file, err := os.Create(filePath)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ _, err = io.Copy(file, resp.Body)
+ return err
+}
+
+// 保存单词到数据库
+func (s *WordService) SaveWord(word string, youdaoResp *model.YoudaoResponse) (*model.Word, error) {
+ var existingWord model.Word
+
+ // 检查单词是否已存在
+ if err := s.db.Where("word = ?", word).First(&existingWord).Error; err == nil {
+ // 单词已存在,更新记忆记录
+ s.updateMemoryRecord(existingWord.ID, true)
+ return &existingWord, nil
+ }
+
+ // 解析有道API响应
+ var phoneticUK, phoneticUS, partOfSpeech, definition string
+
+ // 解析音标
+ if youdaoResp.Basic.UkPhonetic != "" {
+ phoneticUK = "[" + youdaoResp.Basic.UkPhonetic + "]"
+ } else if youdaoResp.Basic.Phonetic != "" {
+ phoneticUK = "[" + youdaoResp.Basic.Phonetic + "]"
+ }
+ if youdaoResp.Basic.UsPhonetic != "" {
+ phoneticUS = "[" + youdaoResp.Basic.UsPhonetic + "]"
+ } else if youdaoResp.Basic.Phonetic != "" {
+ phoneticUS = "[" + youdaoResp.Basic.Phonetic + "]"
+ }
+
+ // 解析释义 - 优先用 translation(更准确)
+ if len(youdaoResp.Translation) > 0 {
+ definition = strings.Join(youdaoResp.Translation, "; ")
+ }
+
+ // 如果 basic 有 wfs,也加到释义里
+ if len(youdaoResp.Basic.Wfs) > 0 {
+ var meanings []string
+ for _, wf := range youdaoResp.Basic.Wfs {
+ if len(wf.Means) > 0 {
+ meanings = append(meanings, wf.Wf.Name+": "+wf.Means[0].Mean.Text)
+ }
+ }
+ if len(meanings) > 0 {
+ if definition != "" {
+ definition += " | "
+ }
+ definition += strings.Join(meanings, "; ")
+ }
+ partOfSpeech = "basic"
+ }
+ if partOfSpeech == "" {
+ partOfSpeech = "n./v./adj."
+ }
+
+ // 音频 URL - 有道返回的 speakUrl 示例: https://dict.youdao.com/dictvoice?audio=hello
+ audioPath := config.AppConfig.Audio.Path
+ var audioUK, audioUS string
+ if youdaoResp.SpeakUrl != "" {
+ // 默认是美音,加 _uk 是英音
+ audioUK = strings.Replace(youdaoResp.SpeakUrl, "audio=", "audio=word_uk_", 1)
+ audioUS = youdaoResp.SpeakUrl
+
+ // 异步下载音频文件
+ go func() {
+ if audioPath != "" {
+ ukPath := fmt.Sprintf("%s/%s_uk.mp3", audioPath, word)
+ usPath := fmt.Sprintf("%s/%s_us.mp3", audioPath, word)
+ s.DownloadAudio(audioUK, ukPath)
+ s.DownloadAudio(audioUS, usPath)
+ }
+ }()
+ audioUK = fmt.Sprintf("%s/%s_uk.mp3", audioPath, word)
+ audioUS = fmt.Sprintf("%s/%s_us.mp3", audioPath, word)
+ }
+
+ // 创建新单词
+ newWord := model.Word{
+ Word: word,
+ PhoneticUK: phoneticUK,
+ PhoneticUS: phoneticUS,
+ AudioUK: audioUK,
+ AudioUS: audioUS,
+ PartOfSpeech: partOfSpeech,
+ Definition: definition,
+ }
+
+ if err := s.db.Create(&newWord).Error; err != nil {
+ return nil, err
+ }
+
+ // 创建记忆记录 + 记一次"背过"
+ _ = s.createMemoryRecord(newWord.ID)
+ _ = s.updateMemoryRecord(newWord.ID, true)
+
+ return &newWord, nil
+}
+
+// 创建记忆记录
+func (s *WordService) createMemoryRecord(wordID int64) error {
+ record := model.MemoryRecord{
+ WordID: wordID,
+ UserID: 1,
+ CorrectCount: 0,
+ TotalCount: 0,
+ MasteryLevel: 0,
+ }
+ return s.db.Create(&record).Error
+}
+
+// 更新记忆记录
+func (s *WordService) updateMemoryRecord(wordID int64, correct bool) error {
+ var record model.MemoryRecord
+ if err := s.db.Where("word_id = ?", wordID).First(&record).Error; err != nil {
+ return err
+ }
+
+ record.TotalCount++
+ if correct {
+ record.CorrectCount++
+ // 计算掌握程度
+ record.MasteryLevel = (record.CorrectCount * 5) / (record.TotalCount + 1)
+ if record.MasteryLevel > 5 {
+ record.MasteryLevel = 5
+ }
+ }
+
+ now := time.Now()
+ record.LastReviewedAt = &now
+ // 根据掌握程度计算下次复习时间
+ reviewInterval := time.Hour * 24 * time.Duration(record.MasteryLevel+1)
+ nextReview := now.Add(reviewInterval)
+ record.NextReviewAt = &nextReview
+
+ return s.db.Save(&record).Error
+}
+
+// 获取待复习单词
+func (s *WordService) GetReviewWords(mode string, limit int) ([]model.MemoryRecord, error) {
+ var records []model.MemoryRecord
+
+ query := s.db.Preload("Word").Where("next_review_at <= ? OR next_review_at IS NULL", time.Now())
+
+ if err := query.Limit(limit).Find(&records).Error; err != nil {
+ return nil, err
+ }
+
+ // 如果没有需要复习的,随机获取一些
+ if len(records) == 0 {
+ if err := s.db.Preload("Word").Limit(limit).Find(&records).Error; err != nil {
+ return nil, err
+ }
+ }
+
+ return records, nil
+}
+
+func normalizeText(s string) string {
+ s = strings.ToLower(strings.TrimSpace(s))
+ re := regexp.MustCompile(`[^\p{Han}a-z0-9]+`)
+ s = re.ReplaceAllString(s, "")
+ return s
+}
+
+func containsAny(def string, ans string) bool {
+ nDef := normalizeText(def)
+ nAns := normalizeText(ans)
+ if nDef == "" || nAns == "" {
+ return false
+ }
+ if strings.Contains(nDef, nAns) {
+ return true
+ }
+ for _, token := range strings.Split(def, "|") {
+ if strings.Contains(normalizeText(token), nAns) {
+ return true
+ }
+ }
+ return false
+}
+
+// 提交复习答案
+func (s *WordService) SubmitReviewAnswer(req model.ReviewAnswerRequest) (*model.ReviewResult, error) {
+ var record model.MemoryRecord
+ if err := s.db.Preload("Word").Where("id = ?", req.RecordID).First(&record).Error; err != nil {
+ return nil, err
+ }
+
+ word := record.Word
+ correct := false
+ correctAns := ""
+
+ switch req.Mode {
+ case "spelling": // 读音拼单词
+ correct = normalizeText(req.Answer) == normalizeText(word.Word)
+ correctAns = word.Word
+ case "en2cn": // 英文写中文
+ correct = containsAny(word.Definition, req.Answer)
+ correctAns = word.Definition
+ case "cn2en": // 中文写英文
+ correct = normalizeText(req.Answer) == normalizeText(word.Word)
+ correctAns = word.Word
+ default:
+ correct = false
+ correctAns = "模式错误"
+ }
+
+ // 更新记忆记录
+ s.updateMemoryRecord(record.WordID, correct)
+
+ return &model.ReviewResult{
+ Word: word,
+ Correct: correct,
+ Answer: req.Answer,
+ CorrectAns: correctAns,
+ }, nil
+}
+
+// 获取所有单词
+func (s *WordService) GetAllWords(limit, offset int) ([]model.Word, int64, error) {
+ var words []model.Word
+ var total int64
+
+ s.db.Model(&model.Word{}).Count(&total)
+
+ if err := s.db.Limit(limit).Offset(offset).Order("created_at DESC").Find(&words).Error; err != nil {
+ return nil, 0, err
+ }
+
+ return words, total, nil
+}
+
+// 获取记忆统计
+func (s *WordService) GetStatistics() (map[string]interface{}, error) {
+ var totalWords int64
+ var masteredWords int64
+ var needReview int64
+ var todayReviewed int64
+
+ s.db.Model(&model.Word{}).Count(&totalWords)
+ s.db.Model(&model.MemoryRecord{}).Where("mastery_level >= 4").Count(&masteredWords)
+ s.db.Model(&model.MemoryRecord{}).Where("next_review_at <= ?", time.Now()).Count(&needReview)
+ s.db.Model(&model.MemoryRecord{}).Where("last_reviewed_at >= ?", time.Now().Format("2006-01-02")).Count(&todayReviewed)
+
+ return map[string]interface{}{
+ "total_words": totalWords,
+ "mastered_words": masteredWords,
+ "need_review": needReview,
+ "today_reviewed": todayReviewed,
+ }, nil
+}
diff --git a/memora-api/main.go b/memora-api/main.go
new file mode 100644
index 0000000..cb96041
--- /dev/null
+++ b/memora-api/main.go
@@ -0,0 +1,78 @@
+package main
+
+import (
+ "fmt"
+ "log"
+
+ "memora-api/internal/config"
+ "memora-api/internal/handler"
+ "memora-api/internal/model"
+ "memora-api/internal/service"
+
+ "github.com/gin-gonic/gin"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+)
+
+func main() {
+ // 加载配置
+ cfg, err := config.LoadConfig("config.yaml")
+ if err != nil {
+ log.Fatalf("加载配置失败: %v", err)
+ }
+
+ // 连接数据库
+ db, err := gorm.Open(mysql.Open(cfg.Database.DSN()), &gorm.Config{})
+ if err != nil {
+ log.Fatalf("连接数据库失败: %v", err)
+ }
+
+ // 自动迁移表
+ if err := db.AutoMigrate(&model.Word{}, &model.MemoryRecord{}); err != nil {
+ log.Fatalf("数据库迁移失败: %v", err)
+ }
+
+ // 初始化服务
+ wordService := service.NewWordService(db)
+ wordHandler := handler.NewWordHandler(wordService)
+
+ // 启动 Gin
+ r := gin.Default()
+
+ // CORS 中间件
+ r.Use(func(c *gin.Context) {
+ c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
+ c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
+ c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
+ if c.Request.Method == "OPTIONS" {
+ c.AbortWithStatus(204)
+ return
+ }
+ c.Next()
+ })
+
+ // 路由
+ api := r.Group("/api")
+ {
+ // 记忆模式
+ api.POST("/words", wordHandler.AddWord)
+ api.GET("/words", wordHandler.GetWords)
+
+ // 复习模式
+ api.GET("/review", wordHandler.GetReviewWords)
+ api.POST("/review", wordHandler.SubmitReview)
+
+ // 统计
+ api.GET("/stats", wordHandler.GetStatistics)
+
+ // 音频
+ api.GET("/audio", wordHandler.GetAudio)
+ }
+
+ // 启动服务器
+ addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
+ log.Printf("服务器启动在: %s", addr)
+ if err := r.Run(addr); err != nil {
+ log.Fatalf("启动服务器失败: %v", err)
+ }
+}
diff --git a/memora-api/sql/init.sql b/memora-api/sql/init.sql
new file mode 100644
index 0000000..414ca94
--- /dev/null
+++ b/memora-api/sql/init.sql
@@ -0,0 +1,36 @@
+-- 初始化数据库
+CREATE DATABASE IF NOT EXISTS memora DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+USE memora;
+
+-- 标准单词表
+CREATE TABLE IF NOT EXISTS words (
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
+ word VARCHAR(100) NOT NULL UNIQUE COMMENT '单词',
+ phonetic_uk VARCHAR(255) COMMENT '英式音标',
+ phonetic_us VARCHAR(255) COMMENT '美式音标',
+ audio_uk VARCHAR(500) COMMENT '英式音频文件路径',
+ audio_us VARCHAR(500) COMMENT '美式音频文件路径',
+ part_of_speech VARCHAR(50) COMMENT '词性',
+ definition TEXT COMMENT '标准释义',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX idx_word (word)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- 记忆记录表
+CREATE TABLE IF NOT EXISTS memory_records (
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
+ word_id BIGINT NOT NULL COMMENT '关联words表',
+ user_id BIGINT DEFAULT 1 COMMENT '用户ID',
+ correct_count INT DEFAULT 0 COMMENT '正确次数',
+ total_count INT DEFAULT 0 COMMENT '总复习次数',
+ mastery_level INT DEFAULT 0 COMMENT '掌握程度 0-5',
+ last_reviewed_at TIMESTAMP NULL COMMENT '上次复习时间',
+ next_review_at TIMESTAMP NULL COMMENT '下次复习时间',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX idx_word_id (word_id),
+ INDEX idx_user_id (user_id),
+ INDEX idx_next_review (next_review_at),
+ FOREIGN KEY (word_id) REFERENCES words(id) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/memora-engine/internal/engine/memory.go b/memora-engine/internal/engine/memory.go
new file mode 100644
index 0000000..71f8e88
--- /dev/null
+++ b/memora-engine/internal/engine/memory.go
@@ -0,0 +1,119 @@
+package engine
+
+import (
+ "math"
+ "time"
+)
+
+// 记忆引擎 - 基于艾宾浩斯遗忘曲线
+type MemoryEngine struct {
+ // 基础间隔(小时)
+ baseInterval float64
+ // 最大间隔(天)
+ maxInterval float64
+}
+
+// 记忆级别
+const (
+ Level0 = iota // 完全忘记
+ Level1
+ Level2
+ Level3
+ Level4
+ Level5 // 永久记住
+)
+
+func NewMemoryEngine() *MemoryEngine {
+ return &MemoryEngine{
+ baseInterval: 1, // 1小时
+ maxInterval: 30 * 24, // 30天
+ }
+}
+
+// CalculateNextReview 计算下次复习时间
+// correct: 回答是否正确
+// currentLevel: 当前掌握程度 0-5
+func (e *MemoryEngine) CalculateNextReview(correct bool, currentLevel int) time.Duration {
+ var interval float64
+
+ if !correct {
+ // 回答错误,重置到Level1,1小时后复习
+ interval = e.baseInterval
+ } else {
+ // 根据当前级别计算间隔
+ switch currentLevel {
+ case 0:
+ interval = 1 // 1小时后
+ case 1:
+ interval = 12 // 12小时后
+ case 2:
+ interval = 24 // 1天后
+ case 3:
+ interval = 72 // 3天后
+ case 4:
+ interval = 168 // 7天后
+ case 5:
+ interval = 360 // 15天后
+ default:
+ interval = e.baseInterval
+ }
+
+ // 增加难度系数
+ interval = interval * 1.2
+ }
+
+ // 限制最大间隔
+ interval = math.Min(interval, e.maxInterval)
+
+ return time.Duration(interval) * time.Hour
+}
+
+// GetMasteryLevel 计算掌握程度
+// correctCount: 正确次数
+// totalCount: 总次数
+func (e *MemoryEngine) GetMasteryLevel(correctCount, totalCount int) int {
+ if totalCount == 0 {
+ return 0
+ }
+
+ // 正确率
+ rate := float64(correctCount) / float64(totalCount)
+
+ // 根据正确率和次数计算级别
+ if totalCount < 3 {
+ if rate >= 0.8 {
+ return 1
+ }
+ return 0
+ }
+
+ if rate >= 0.9 {
+ return 5
+ } else if rate >= 0.8 {
+ return 4
+ } else if rate >= 0.7 {
+ return 3
+ } else if rate >= 0.5 {
+ return 2
+ } else {
+ return 1
+ }
+}
+
+// IsDueForReview 是否应该复习
+func (e *MemoryEngine) IsDueForReview(lastReview *time.Time, nextReview *time.Time) bool {
+ now := time.Now()
+
+ // 没有复习记录,需要复习
+ if lastReview == nil {
+ return true
+ }
+
+ // 有计划复习时间
+ if nextReview != nil {
+ return now.After(*nextReview)
+ }
+
+ // 默认:超过1天没复习需要复习
+ return now.Sub(*lastReview) > 24*time.Hour
+}
diff --git a/memora-web/Dockerfile b/memora-web/Dockerfile
new file mode 100644
index 0000000..aad2002
--- /dev/null
+++ b/memora-web/Dockerfile
@@ -0,0 +1,7 @@
+FROM node:20-alpine
+WORKDIR /app
+COPY package.json package-lock.json* ./
+RUN npm install
+COPY . .
+EXPOSE 3000
+CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "3000"]
diff --git a/memora-web/index.html b/memora-web/index.html
new file mode 100644
index 0000000..363126e
--- /dev/null
+++ b/memora-web/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Memora 背单词
+
+
+
+
+
+
+
diff --git a/memora-web/package-lock.json b/memora-web/package-lock.json
new file mode 100644
index 0000000..f985401
--- /dev/null
+++ b/memora-web/package-lock.json
@@ -0,0 +1,1745 @@
+{
+ "name": "memora-web",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "memora-web",
+ "version": "1.0.0",
+ "dependencies": {
+ "axios": "^1.6.8",
+ "element-plus": "^2.7.2",
+ "vue": "^3.4.21",
+ "vue-router": "^4.3.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.0.4",
+ "vite": "^5.1.6"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@ctrl/tinycolor": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
+ "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@element-plus/icons-vue": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
+ "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "vue": "^3.2.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz",
+ "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz",
+ "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.4",
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@popperjs/core": {
+ "name": "@sxzz/popperjs-es",
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz",
+ "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
+ "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
+ "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
+ "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
+ "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
+ "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
+ "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
+ "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
+ "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
+ "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
+ "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
+ "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
+ "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
+ "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
+ "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
+ "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
+ "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
+ "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
+ "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
+ "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
+ "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
+ "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
+ "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/lodash": {
+ "version": "4.17.24",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
+ "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/lodash-es": {
+ "version": "4.17.12",
+ "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+ "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/lodash": "*"
+ }
+ },
+ "node_modules/@types/web-bluetooth": {
+ "version": "0.0.20",
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+ "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
+ "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz",
+ "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@vue/shared": "3.5.29",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz",
+ "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.29",
+ "@vue/shared": "3.5.29"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz",
+ "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@vue/compiler-core": "3.5.29",
+ "@vue/compiler-dom": "3.5.29",
+ "@vue/compiler-ssr": "3.5.29",
+ "@vue/shared": "3.5.29",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.6",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz",
+ "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.29",
+ "@vue/shared": "3.5.29"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz",
+ "integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.29"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz",
+ "integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.29",
+ "@vue/shared": "3.5.29"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz",
+ "integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.29",
+ "@vue/runtime-core": "3.5.29",
+ "@vue/shared": "3.5.29",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz",
+ "integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.29",
+ "@vue/shared": "3.5.29"
+ },
+ "peerDependencies": {
+ "vue": "3.5.29"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz",
+ "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==",
+ "license": "MIT"
+ },
+ "node_modules/@vueuse/core": {
+ "version": "10.11.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
+ "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.20",
+ "@vueuse/metadata": "10.11.1",
+ "@vueuse/shared": "10.11.1",
+ "vue-demi": ">=0.14.8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/core/node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vueuse/metadata": {
+ "version": "10.11.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
+ "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "10.11.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz",
+ "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
+ "license": "MIT",
+ "dependencies": {
+ "vue-demi": ">=0.14.8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared/node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/async-validator": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
+ "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
+ "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.19",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
+ "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/element-plus": {
+ "version": "2.13.2",
+ "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.2.tgz",
+ "integrity": "sha512-Zjzm1NnFXGhV4LYZ6Ze9skPlYi2B4KAmN18FL63A3PZcjhDfroHwhtM6RE8BonlOPHXUnPQynH0BgaoEfvhrGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@ctrl/tinycolor": "^3.4.1",
+ "@element-plus/icons-vue": "^2.3.2",
+ "@floating-ui/dom": "^1.0.1",
+ "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
+ "@types/lodash": "^4.17.20",
+ "@types/lodash-es": "^4.17.12",
+ "@vueuse/core": "^10.11.0",
+ "async-validator": "^4.2.5",
+ "dayjs": "^1.11.19",
+ "lodash": "^4.17.23",
+ "lodash-es": "^4.17.23",
+ "lodash-unified": "^1.0.3",
+ "memoize-one": "^6.0.0",
+ "normalize-wheel-es": "^1.2.0"
+ },
+ "peerDependencies": {
+ "vue": "^3.3.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash-es": {
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
+ "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash-unified": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
+ "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/lodash-es": "*",
+ "lodash": "*",
+ "lodash-es": "*"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/memoize-one": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+ "license": "MIT"
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/normalize-wheel-es": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
+ "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/rollup": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
+ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.59.0",
+ "@rollup/rollup-android-arm64": "4.59.0",
+ "@rollup/rollup-darwin-arm64": "4.59.0",
+ "@rollup/rollup-darwin-x64": "4.59.0",
+ "@rollup/rollup-freebsd-arm64": "4.59.0",
+ "@rollup/rollup-freebsd-x64": "4.59.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.59.0",
+ "@rollup/rollup-linux-arm64-musl": "4.59.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.59.0",
+ "@rollup/rollup-linux-loong64-musl": "4.59.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.59.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.59.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-musl": "4.59.0",
+ "@rollup/rollup-openbsd-x64": "4.59.0",
+ "@rollup/rollup-openharmony-arm64": "4.59.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.59.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.59.0",
+ "@rollup/rollup-win32-x64-gnu": "4.59.0",
+ "@rollup/rollup-win32-x64-msvc": "4.59.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz",
+ "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.29",
+ "@vue/compiler-sfc": "3.5.29",
+ "@vue/runtime-dom": "3.5.29",
+ "@vue/server-renderer": "3.5.29",
+ "@vue/shared": "3.5.29"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-router": {
+ "version": "4.6.4",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
+ "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ }
+ }
+}
diff --git a/memora-web/package.json b/memora-web/package.json
new file mode 100644
index 0000000..3bd08d5
--- /dev/null
+++ b/memora-web/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "memora-web",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "axios": "^1.6.8",
+ "element-plus": "^2.7.2",
+ "vue": "^3.4.21",
+ "vue-router": "^4.3.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.0.4",
+ "vite": "^5.1.6"
+ }
+}
diff --git a/memora-web/src/App.vue b/memora-web/src/App.vue
new file mode 100644
index 0000000..57e9168
--- /dev/null
+++ b/memora-web/src/App.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/memora-web/src/app/index.js b/memora-web/src/app/index.js
new file mode 100644
index 0000000..8986f7a
--- /dev/null
+++ b/memora-web/src/app/index.js
@@ -0,0 +1,11 @@
+import { createApp } from 'vue'
+import App from '../App.vue'
+import router from '../router'
+import { registerPlugins } from './plugins'
+
+export function bootstrap() {
+ const app = createApp(App)
+ app.use(router)
+ registerPlugins(app)
+ app.mount('#app')
+}
diff --git a/memora-web/src/app/plugins.js b/memora-web/src/app/plugins.js
new file mode 100644
index 0000000..1ec2ff1
--- /dev/null
+++ b/memora-web/src/app/plugins.js
@@ -0,0 +1,6 @@
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+
+export function registerPlugins(app) {
+ app.use(ElementPlus)
+}
diff --git a/memora-web/src/app/routes.js b/memora-web/src/app/routes.js
new file mode 100644
index 0000000..141f2f7
--- /dev/null
+++ b/memora-web/src/app/routes.js
@@ -0,0 +1,15 @@
+import Dashboard from '../views/Home.vue'
+import Memory from '../views/Memory.vue'
+import Review from '../views/Review.vue'
+import Statistics from '../views/Statistics.vue'
+import Words from '../views/Words.vue'
+import Settings from '../views/Settings.vue'
+
+export const routes = [
+ { path: '/', name: 'dashboard', component: Dashboard },
+ { path: '/memory', name: 'memory', component: Memory },
+ { path: '/review', name: 'review', component: Review },
+ { path: '/statistics', name: 'statistics', component: Statistics },
+ { path: '/words', name: 'words', component: Words },
+ { path: '/settings', name: 'settings', component: Settings }
+]
diff --git a/memora-web/src/main.js b/memora-web/src/main.js
new file mode 100644
index 0000000..ff93c60
--- /dev/null
+++ b/memora-web/src/main.js
@@ -0,0 +1,3 @@
+import { bootstrap } from './app'
+
+bootstrap()
diff --git a/memora-web/src/router/index.js b/memora-web/src/router/index.js
new file mode 100644
index 0000000..80e543d
--- /dev/null
+++ b/memora-web/src/router/index.js
@@ -0,0 +1,9 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import { routes } from '../app/routes'
+
+export const router = createRouter({
+ history: createWebHistory(),
+ routes
+})
+
+export default router
diff --git a/memora-web/src/services/api.js b/memora-web/src/services/api.js
new file mode 100644
index 0000000..5a7e5cc
--- /dev/null
+++ b/memora-web/src/services/api.js
@@ -0,0 +1,36 @@
+import axios from 'axios'
+
+export const api = axios.create({
+ baseURL: '/api',
+ timeout: 15000
+})
+
+export async function addWord(word) {
+ const res = await api.post('/words', { word })
+ return res.data
+}
+
+export async function getWords({ limit = 20, offset = 0 } = {}) {
+ const res = await api.get('/words', { params: { limit, offset } })
+ return res.data
+}
+
+export async function getReviewWords({ mode = 'spelling', limit = 10 } = {}) {
+ const res = await api.get('/review', { params: { mode, limit } })
+ return res.data
+}
+
+export async function submitReview({ recordId, answer, mode }) {
+ const res = await api.post('/review', { record_id: recordId, answer, mode })
+ return res.data
+}
+
+export async function getStatistics() {
+ const res = await api.get('/stats')
+ return res.data
+}
+
+export function audioUrl({ word, type = 'uk' }) {
+ const q = new URLSearchParams({ word, type })
+ return `/api/audio?${q.toString()}`
+}
diff --git a/memora-web/src/views/Home.vue b/memora-web/src/views/Home.vue
new file mode 100644
index 0000000..f6fe914
--- /dev/null
+++ b/memora-web/src/views/Home.vue
@@ -0,0 +1,57 @@
+
+
+
仪表盘
+
欢迎回来,继续你的学习之旅
+
+
+
+
+
+
+
+
+
+ 开始复习
+ 添加单词
+
+
+
+
+
+
+
diff --git a/memora-web/src/views/Memory.vue b/memora-web/src/views/Memory.vue
new file mode 100644
index 0000000..2df5865
--- /dev/null
+++ b/memora-web/src/views/Memory.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
记忆模式
+
输入单词并回车,会调用后端(后端再调用有道)校验并入库
+
+
+
+
+
+
+
+
+
+ 确认
+
+
+
+
+
+
+ {{ saved.word }}
+ {{ saved.part_of_speech }}
+ {{ saved.definition }}
+ {{ saved.phonetic_uk }}
+ {{ saved.phonetic_us }}
+
+
+
+
+
+
+
+
diff --git a/memora-web/src/views/Review.vue b/memora-web/src/views/Review.vue
new file mode 100644
index 0000000..8956079
--- /dev/null
+++ b/memora-web/src/views/Review.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 播放读音(uk)
+
+
+ {{ record.word.word }}
+
+
+ {{ record.word.definition }}
+
+
+
+
+
+ 提交
+ 换一个
+
+
+
+
+ 单词:{{ result.word.word }} / 释义:{{ result.word.definition }}
+ 你的答案:{{ result.answer }};正确:{{ result.correct_ans }}
+
+
+
+
+
+
+
+
+
+
diff --git a/memora-web/src/views/Settings.vue b/memora-web/src/views/Settings.vue
new file mode 100644
index 0000000..3dc8aa9
--- /dev/null
+++ b/memora-web/src/views/Settings.vue
@@ -0,0 +1,13 @@
+
+
+ 设置
+
+ 已在后端 config.yaml 配置
+ memora-api/audio
+ Vue3 + Element Plus
+
+
+
+
+
diff --git a/memora-web/src/views/Statistics.vue b/memora-web/src/views/Statistics.vue
new file mode 100644
index 0000000..3a43839
--- /dev/null
+++ b/memora-web/src/views/Statistics.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/memora-web/src/views/Words.vue b/memora-web/src/views/Words.vue
new file mode 100644
index 0000000..8eb5aba
--- /dev/null
+++ b/memora-web/src/views/Words.vue
@@ -0,0 +1,30 @@
+
+
+ 单词列表
+
+
+
+
+
+
+
+
+
+
diff --git a/memora-web/vite.config.js b/memora-web/vite.config.js
new file mode 100644
index 0000000..d2cd794
--- /dev/null
+++ b/memora-web/vite.config.js
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+export default defineConfig({
+ plugins: [vue()],
+ server: {
+ port: 3000,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8080',
+ changeOrigin: true
+ }
+ }
+ }
+})