Projects STRLCPY SeaMoon Commits cc04d380
🤬
Showing first 121 files as there are too many
  • ■ ■ ■ ■ ■
    .gitignore
    skipped 23 lines
    24 24  *.sql
    25 25   
    26 26  cmd/client/static/dist/*
    27  -web/
    28 27  *.log
    29 28  *.toml
    30 29  .config
    skipped 6 lines
  • ■ ■ ■ ■ ■
    Dockerfile
    1 1  # build stage
    2 2  FROM golang:alpine AS build
    3 3  ARG VERSION
     4 +ARG SHA
    4 5  COPY .. /src
    5 6  WORKDIR /src
    6 7  ENV CGO_ENABLED 0
    7 8  ENV VERSION=${VERSION}
    8 9  ENV SHA=${SHA}
    9  -RUN go build -ldflags "-X github.com/DVKunion/SeaMoon/system/xlog.Version=${VERSION} -X github.com/DVKunion/SeaMoon/system/xlog.Commit=${SHA}" -o /tmp/seamoon cmd/main.go
     10 +#COPY ./seamoon /tmp/seamoon
     11 +RUN go build -v -ldflags "-X github.com/DVKunion/SeaMoon/system/xlog.Version=${VERSION} -X github.com/DVKunion/SeaMoon/system/xlog.Commit=${SHA}" -o /tmp/seamoon cmd/main.go
    10 12  RUN chmod +x /tmp/seamoon
     13 +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \
     14 + apk add upx && upx -9 /tmp/seamoon
    11 15  # run stage
    12 16  FROM alpine:3.19
    13 17  LABEL maintainer="[email protected]"
    skipped 9 lines
  • ■ ■ ■ ■ ■ ■
    README.md
    skipped 3 lines
    4 4  <h1 align="center">Sea Moon</h1>
    5 5   
    6 6  <p align="center">
    7  -<img src="https://goreportcard.com/badge/github.com/DVKunion/SeaMoon" />
    8  -<img src="https://img.shields.io/github/stars/DVKunion/SeaMoon.svg" alt="stars"/>
    9  -<img src="https://img.shields.io/github/downloads/dvkunion/seamoon/total?color=orange" alt="downloads" />
    10  -<img src="https://img.shields.io/github/languages/top/DVKunion/SeaMoon.svg?&color=blueviolet" alt="languages">
    11  -<img src="https://img.shields.io/badge/LICENSE-MIT-777777.svg" alt="license"/>
     7 +<img src="https://goreportcard.com/badge/github.com/DVKunion/SeaMoon" alt="go-report"/>
     8 +<img src="https://img.shields.io/github/languages/top/DVKunion/SeaMoon.svg?&color=blueviolet"
     9 + alt="languages"/>
     10 +<img src="https://img.shields.io/badge/LICENSE-MIT-777777.svg" alt="license"/>
     11 +<img src="https://img.shields.io/github/downloads/dvkunion/seamoon/total?color=orange" alt="downloads"/>
     12 +<img src="https://img.shields.io/github/stars/DVKunion/SeaMoon.svg" alt="stars"/>
    12 13  </p>
    13 14   
    14 15  <p align="center">
    skipped 118 lines
  • ■ ■ ■ ■
    cmd/server/server.go
    skipped 5 lines
    6 6   "strings"
    7 7   
    8 8   net "github.com/DVKunion/SeaMoon/pkg/network"
    9  - "github.com/DVKunion/SeaMoon/pkg/service"
    10 9   "github.com/DVKunion/SeaMoon/pkg/system/errors"
    11 10   "github.com/DVKunion/SeaMoon/pkg/system/xlog"
     11 + "github.com/DVKunion/SeaMoon/pkg/tunnel/service"
    12 12  )
    13 13   
    14 14  type Server struct {
    skipped 61 lines
  • ■ ■ ■ ■
    pkg/api/controller/middleware/jwt.go
    skipped 8 lines
    9 9   "github.com/DVKunion/SeaMoon/pkg/api/controller/servant"
    10 10   "github.com/DVKunion/SeaMoon/pkg/api/enum"
    11 11   "github.com/DVKunion/SeaMoon/pkg/system/errors"
     12 + "github.com/DVKunion/SeaMoon/pkg/system/tools"
    12 13   "github.com/DVKunion/SeaMoon/pkg/system/xlog"
    13  - "github.com/DVKunion/SeaMoon/pkg/tools"
    14 14  )
    15 15   
    16 16  func JWTAuthMiddleware(c *gin.Context) {
    skipped 33 lines
  • ■ ■ ■ ■ ■ ■
    pkg/api/controller/v1/provider.go
    skipped 1 lines
    2 2   
    3 3  import (
    4 4   "net/http"
    5  - "sync"
    6 5   
    7 6   "github.com/gin-gonic/gin"
    8 7   
    skipped 63 lines
    72 71   if res, err := service.SVC.CreateProvider(c, obj.ToModel(true)); err != nil {
    73 72   servant.ErrorMsg(c, http.StatusInternalServerError, errors.ApiError(xlog.ApiServiceError, err))
    74 73   } else {
    75  - signal.Signal().SendProviderSignal(res.ID, enum.ProvStatusSync, nil)
     74 + signal.Signal().SendProviderSignal(res.ID, enum.ProvStatusSync)
    76 75   servant.SuccessMsg(c, 1, res.ToApi())
    77 76   }
    78 77  }
    skipped 16 lines
    95 94   if res, err := service.SVC.UpdateProvider(c, obj.ToModel(false)); err != nil {
    96 95   servant.ErrorMsg(c, http.StatusInternalServerError, errors.ApiError(xlog.ApiServiceError, err))
    97 96   } else {
    98  - signal.Signal().SendProviderSignal(res.ID, enum.ProvStatusSync, nil)
     97 + signal.Signal().SendProviderSignal(res.ID, enum.ProvStatusSync)
    99 98   servant.SuccessMsg(c, 1, res.ToApi())
    100 99   }
    101 100  }
    skipped 5 lines
    107 106   return
    108 107   }
    109 108   
    110  - wg := &sync.WaitGroup{}
    111  - wg.Add(1)
    112  - signal.Signal().SendProviderSignal(uint(id), enum.ProvStatusDelete, wg)
    113  - wg.Wait()
     109 + signal.Signal().SendProviderSignalSync(uint(id), enum.ProvStatusDelete)
    114 110   servant.SuccessMsg(c, 1, nil)
    115 111  }
    116 112   
    skipped 3 lines
    120 116   servant.ErrorMsg(c, http.StatusBadRequest, errors.ApiError(xlog.ApiParamsError, err))
    121 117   return
    122 118   }
    123  - wg := &sync.WaitGroup{}
    124  - wg.Add(1)
    125  - signal.Signal().SendProviderSignal(uint(id), enum.ProvStatusSync, wg)
    126  - wg.Wait()
     119 + signal.Signal().SendProviderSignalSync(uint(id), enum.ProvStatusSync)
    127 120   servant.SuccessMsg(c, 1, nil)
    128 121  }
    129 122   
  • ■ ■ ■ ■ ■ ■
    pkg/api/controller/v1/proxy.go
    skipped 1 lines
    2 2   
    3 3  import (
    4 4   "net/http"
    5  - "sync"
    6 5   
    7 6   "github.com/gin-gonic/gin"
    8 7   
    skipped 3 lines
    12 11   "github.com/DVKunion/SeaMoon/pkg/api/service"
    13 12   "github.com/DVKunion/SeaMoon/pkg/signal"
    14 13   "github.com/DVKunion/SeaMoon/pkg/system/errors"
     14 + "github.com/DVKunion/SeaMoon/pkg/system/tools"
    15 15   "github.com/DVKunion/SeaMoon/pkg/system/xlog"
    16  - "github.com/DVKunion/SeaMoon/pkg/tools"
    17 16  )
    18 17   
    19 18  func ListProxies(c *gin.Context) {
    skipped 42 lines
    62 61   *obj.Status = enum.ProxyStatusError
    63 62   *obj.StatusMessage = err.Error()
    64 63   } else {
    65  - signal.Signal().SendTunnelSignal(tun.ID, enum.TunnelActive, nil)
     64 + signal.Signal().SendTunnelSignal(tun.ID, enum.TunnelActive)
    66 65   obj.TunnelID = tun.ID
    67 66   }
    68 67   }
    skipped 4 lines
    73 72   return
    74 73   } else {
    75 74   // 发送启动通知
    76  - signal.Signal().SendProxySignal(res.ID, enum.ProxyStatusActive, nil)
     75 + signal.Signal().SendProxySignal(res.ID, enum.ProxyStatusActive)
    77 76   servant.SuccessMsg(c, 1, res.ToApi())
    78 77   }
    79 78  }
    skipped 12 lines
    92 91   
    93 92   m := obj.ToModel(false)
    94 93   if m.Status != nil {
    95  - signal.Signal().SendProxySignal(obj.ID, *obj.Status, nil)
     94 + signal.Signal().SendProxySignal(obj.ID, *obj.Status)
    96 95   // 这里愚蠢de做一个特殊处理: 当代理关闭时,自动将连接数清零
    97 96   if *m.Status == enum.ProxyStatusInactive {
    98 97   m.Conn = tools.IntPtr(0)
    skipped 13 lines
    112 111   return
    113 112   }
    114 113   
    115  - wg := &sync.WaitGroup{}
    116  - wg.Add(1)
    117  - signal.Signal().SendProxySignal(uint(id), enum.ProxyStatusDelete, wg)
    118  - wg.Wait()
     114 + signal.Signal().SendProxySignalSync(uint(id), enum.ProxyStatusDelete)
    119 115   servant.SuccessMsg(c, 1, nil)
    120 116  }
    121 117   
    skipped 8 lines
    130 126   if proxy, err := service.SVC.GetProxyById(c, uint(id)); err != nil || *proxy.Status != enum.ProxyStatusActive {
    131 127   servant.ErrorMsg(c, http.StatusInternalServerError, errors.ApiError(xlog.ApiServiceError, err))
    132 128   } else {
    133  - signal.Signal().SendProxySignal(proxy.ID, enum.ProxyStatusSpeeding, nil)
     129 + signal.Signal().SendProxySignal(proxy.ID, enum.ProxyStatusSpeeding)
    134 130   servant.SuccessMsg(c, 1, proxy.ToApi())
    135 131   }
    136 132  }
    skipped 1 lines
  • ■ ■ ■ ■ ■ ■
    pkg/api/controller/v1/tunnel.go
    skipped 3 lines
    4 4   "context"
    5 5   "net/http"
    6 6   "reflect"
    7  - "sync"
    8 7   
    9 8   "github.com/gin-gonic/gin"
    10 9   
    skipped 49 lines
    60 59   if res, err := service.SVC.CreateTunnel(c, obj.ToModel(true)); err != nil {
    61 60   servant.ErrorMsg(c, http.StatusInternalServerError, errors.ApiError(xlog.ApiServiceError, err))
    62 61   } else {
     62 + signal.Signal().SendTunnelSignalSync(res.ID, enum.TunnelActive)
    63 63   servant.SuccessMsg(c, 1, res.ToApi(extra()))
    64 64   }
    65 65  }
    skipped 12 lines
    78 78   }
    79 79   
    80 80   obj.ID = uint(id)
     81 + 
     82 + if obj.Status != nil {
     83 + signal.Signal().SendTunnelSignal(obj.ID, *obj.Status)
     84 + }
    81 85   
    82 86   if res, err := service.SVC.UpdateTunnel(c, obj.ToModel(false)); err != nil {
    83 87   servant.ErrorMsg(c, http.StatusInternalServerError, errors.ApiError(xlog.ApiServiceError, err))
    skipped 9 lines
    93 97   return
    94 98   }
    95 99   
    96  - wg := &sync.WaitGroup{}
    97  - wg.Add(1)
    98  - signal.Signal().SendTunnelSignal(uint(id), enum.TunnelDelete, wg)
    99  - wg.Wait()
     100 + signal.Signal().SendTunnelSignalSync(uint(id), enum.TunnelDelete)
    100 101   
    101 102   servant.SuccessMsg(c, 1, nil)
    102 103  }
    skipped 17 lines
  • ■ ■ ■ ■ ■ ■
    pkg/api/enum/proxy.go
    skipped 19 lines
    20 20   ProxyTypeAUTO ProxyType = "auto"
    21 21   ProxyTypeHTTP ProxyType = "http"
    22 22   ProxyTypeSOCKS5 ProxyType = "socks5"
    23  - ProxyTypeSOCKS5Ssr ProxyType = "socks5+ssr"
    24  - ProxyTypeSOCKS5Vmess ProxyType = "socks5+vmess"
    25  - ProxyTypeSOCKS5Vless ProxyType = "socks5+vless"
     23 + ProxyTypeShadowSocks ProxyType = "ss"
     24 + ProxyTypeVmess ProxyType = "vmess"
     25 + ProxyTypeVless ProxyType = "vless"
    26 26  )
    27 27   
    28 28  func (t ProxyType) String() string {
    skipped 15 lines
  • ■ ■ ■ ■ ■ ■
    pkg/api/models/config.go
    skipped 12 lines
    13 13   },
    14 14   {
    15 15   Key: "control_port",
    16  - Value: "7778",
     16 + Value: "7777",
    17 17   },
    18 18   {
    19 19   Key: "control_log",
    20  - Value: "seamoon-web.log",
     20 + Value: "seamoon.log",
    21 21   },
    22 22   {
    23 23   Key: "auto_start",
    24 24   Value: "true",
    25 25   },
    26 26   {
    27  - Key: "version",
    28  - Value: xlog.Version,
     27 + Key: "auto_sync",
     28 + Value: "true",
    29 29   },
    30 30  }
    31 31   
    skipped 14 lines
    46 46   ControlPort string `json:"control_port"`
    47 47   ControlLog string `json:"control_log"`
    48 48   AutoStart string `json:"auto_start"`
     49 + AutoSync string `json:"auto_sync"`
    49 50   
    50 51   Version string `json:"version"`
    51 52  }
    skipped 17 lines
    69 70   Key: "auto_start",
    70 71   Value: c.AutoStart,
    71 72   })
     73 + res = append(res, &Config{
     74 + Key: "auto_sync",
     75 + Value: c.AutoSync,
     76 + })
    72 77   
    73 78   return res
    74 79  }
    skipped 10 lines
    85 90   res.ControlLog = s.Value
    86 91   case "auto_start":
    87 92   res.AutoStart = s.Value
     93 + case "auto_sync":
     94 + res.AutoSync = s.Value
    88 95   case "version":
    89  - res.Version = s.Value
     96 + res.Version = xlog.Version
    90 97   }
    91 98   }
    92 99   return res
    skipped 2 lines
  • ■ ■ ■ ■
    pkg/api/models/proxy.go
    skipped 64 lines
    65 65  }
    66 66   
    67 67  func (p Proxy) ProtoAddr() string {
    68  - if *p.Type == enum.ProxyTypeAUTO || *p.Type == enum.ProxyTypeSOCKS5Vmess || *p.Type == enum.ProxyTypeSOCKS5Vless || *p.Type == enum.ProxyTypeSOCKS5Ssr {
     68 + if *p.Type == enum.ProxyTypeAUTO || *p.Type == enum.ProxyTypeVmess || *p.Type == enum.ProxyTypeVless || *p.Type == enum.ProxyTypeShadowSocks {
    69 69   // 随便选好了
    70 70   return fmt.Sprintf("%s://%s", enum.ProxyTypeSOCKS5, strings.Join([]string{*p.ListenAddr, *p.ListenPort}, ":"))
    71 71   }
    skipped 20 lines
  • ■ ■ ■ ■ ■ ■
    pkg/api/models/tunnel.go
    skipped 35 lines
    36 36   Memory int32 `json:"memory"` // 内存资源
    37 37   Instance int32 `json:"instance"` // 最大实例处理数
    38 38   FcAuthType enum.AuthType `json:"fc_auth_type"` // 函数认证方式
     39 + SSRCrypt string `json:"ssr_crypt"` // ssr 加密方式
     40 + SSRPass string `json:"ssr_pass"` // ssr 密码
     41 + V2rayUid string `json:"v2ray_uid"` // v2ray_uid
    39 42   
    40 43   TLS bool `json:"tls"` // 是否开启 TLS 传输, 开启后自动使用 wss 协议
    41 44   Tor bool `json:"tor"` // 是否开启 Tor 转发
    skipped 76 lines
  • ■ ■ ■ ■
    pkg/api/service/auth.go
    skipped 9 lines
    10 10   "github.com/DVKunion/SeaMoon/pkg/api/database/dao"
    11 11   "github.com/DVKunion/SeaMoon/pkg/api/enum"
    12 12   "github.com/DVKunion/SeaMoon/pkg/api/models"
    13  - "github.com/DVKunion/SeaMoon/pkg/tools"
     13 + "github.com/DVKunion/SeaMoon/pkg/system/tools"
    14 14  )
    15 15   
    16 16  var paramsMissingError = errors.New("missing important params")
    skipped 56 lines
  • ■ ■ ■ ■ ■
    pkg/api/service/provider.go
    skipped 53 lines
    54 54   
    55 55   query := dao.Q.Provider
    56 56   
    57  - if _, err := query.WithContext(ctx).Where(query.ID.Eq(obj.ID)).Updates(obj); err != nil {
     57 + if _, err := query.WithContext(ctx).Omit(query.Status).Where(query.ID.Eq(obj.ID)).Updates(obj); err != nil {
    58 58   return nil, err
    59 59   }
    60 60   
    skipped 14 lines
    75 75   
    76 76  func (p *provider) DeleteProvider(ctx context.Context, id uint) error {
    77 77   query := dao.Q.Provider
    78  - res, err := query.WithContext(ctx).Where(query.ID.Eq(id)).Delete()
     78 + res, err := query.WithContext(ctx).Unscoped().Where(query.ID.Eq(id)).Delete()
    79 79   if err != nil || res.Error != nil {
    80 80   return err
    81 81   }
    skipped 19 lines
    101 101   for _, tun := range tuns {
    102 102   // 检测是否存在
    103 103   if SVC.ExistTunnel(ctx, nil, tun.UniqID) {
     104 + // 存在的话,仅更新状态好了
     105 + SVC.UpdateTunnelStatusByUid(ctx, *tun.UniqID, *tun.Status, *tun.StatusMessage)
    104 106   continue
    105 107   }
    106 108   tun.ProviderId = prov.ID
    107 109   if _, err = SVC.CreateTunnel(ctx, tun.ToModel(true)); err != nil {
    108 110   return err
    109 111   }
     112 + }
     113 + 
     114 + // 这里的更新是为了更新 info 信息
     115 + if _, err = p.UpdateProvider(ctx, prov); err != nil {
     116 + return err
    110 117   }
    111 118   
    112 119   return nil
    skipped 10 lines
  • ■ ■ ■ ■ ■ ■
    pkg/api/service/proxy.go
    skipped 44 lines
    45 45  func (p *proxy) UpdateProxy(ctx context.Context, id uint, obj *models.Proxy) (*models.Proxy, error) {
    46 46   query := dao.Q.Proxy
    47 47   
    48  - if _, err := query.WithContext(ctx).Where(query.ID.Eq(id)).Updates(obj); err != nil {
     48 + if _, err := query.WithContext(ctx).Omit(query.Status).Where(query.ID.Eq(id)).Updates(obj); err != nil {
    49 49   return nil, err
    50 50   }
    51 51   
    skipped 39 lines
    91 91  func (p *proxy) UpdateProxyStatus(ctx context.Context, id uint, status enum.ProxyStatus, msg string) {
    92 92   query := dao.Q.Proxy
    93 93   
    94  - if _, err := query.WithContext(ctx).Where(query.ID.Eq(id)).Updates(&models.Proxy{
     94 + if _, err := query.WithContext(ctx).Where(query.ID.Eq(id)).Updates(models.Proxy{
    95 95   Status: &status,
    96 96   StatusMessage: &msg,
    97 97   }); err != nil {
    skipped 3 lines
    101 101   
    102 102  func (p *proxy) DeleteProxy(ctx context.Context, id uint) error {
    103 103   query := dao.Q.Proxy
    104  - res, err := query.WithContext(ctx).Where(query.ID.Eq(id)).Delete()
     104 + res, err := query.WithContext(ctx).Unscoped().Where(query.ID.Eq(id)).Delete()
    105 105   if err != nil || res.Error != nil {
    106 106   return err
    107 107   }
    skipped 45 lines
  • ■ ■ ■ ■ ■ ■
    pkg/api/service/tunnel.go
    skipped 7 lines
    8 8   "github.com/DVKunion/SeaMoon/pkg/api/models"
    9 9   "github.com/DVKunion/SeaMoon/pkg/sdk"
    10 10   "github.com/DVKunion/SeaMoon/pkg/system/errors"
     11 + "github.com/DVKunion/SeaMoon/pkg/system/tools"
    11 12   "github.com/DVKunion/SeaMoon/pkg/system/xlog"
    12 13  )
    13 14   
    skipped 39 lines
    53 54   }
    54 55   }
    55 56   
     57 + // 手动填充账户与密码
     58 + obj.Config.V2rayUid = tools.GenerateUUID()
     59 + obj.Config.SSRPass = tools.GenerateRandomString(12)
     60 + // todo: 开放认证
     61 + obj.Config.SSRCrypt = "aes-256-gcm"
     62 + 
    56 63   if err = dao.Q.Tunnel.WithContext(ctx).Create(obj); err != nil {
    57 64   return nil, err
    58 65   }
    skipped 4 lines
    63 70  func (t *tunnel) UpdateTunnel(ctx context.Context, obj *models.Tunnel) (*models.Tunnel, error) {
    64 71   query := dao.Q.Tunnel
    65 72   
    66  - if _, err := query.WithContext(ctx).Where(query.ID.Eq(obj.ID)).Updates(obj); err != nil {
     73 + if _, err := query.WithContext(ctx).Omit(query.Status).Where(query.ID.Eq(obj.ID)).Updates(obj); err != nil {
    67 74   return nil, err
    68 75   }
    69 76   
    skipped 3 lines
    73 80  func (t *tunnel) UpdateTunnelStatus(ctx context.Context, id uint, status enum.TunnelStatus, msg string) {
    74 81   query := dao.Q.Tunnel
    75 82   
    76  - if _, err := query.WithContext(ctx).Where(query.ID.Eq(id)).Updates(&models.Tunnel{
     83 + if _, err := query.WithContext(ctx).Where(query.ID.Eq(id)).Updates(models.Tunnel{
    77 84   Status: &status,
    78 85   StatusMessage: &msg,
    79 86   }); err != nil {
    skipped 1 lines
    81 88   }
    82 89  }
    83 90   
    84  -func (t *tunnel) UpdateTunnelAddr(ctx context.Context, id uint, addr string) {
     91 +func (t *tunnel) UpdateTunnelStatusByUid(ctx context.Context, uid string, status enum.TunnelStatus, msg string) {
     92 + query := dao.Q.Tunnel
     93 + 
     94 + if _, err := query.WithContext(ctx).Where(query.UniqID.Eq(uid)).Updates(models.Tunnel{
     95 + Status: &status,
     96 + StatusMessage: &msg,
     97 + }); err != nil {
     98 + xlog.Error(xlog.ServiceDBUpdateStatusError, "type", "tunnel_status_uid", "err", err)
     99 + }
     100 +}
     101 + 
     102 +func (t *tunnel) UpdateTunnelDetail(ctx context.Context, id uint, addr string, uid string) {
    85 103   query := dao.Q.Tunnel
    86 104   
    87  - if _, err := query.WithContext(ctx).Where(query.ID.Eq(id)).Updates(&models.Tunnel{
    88  - Addr: &addr,
     105 + if _, err := query.WithContext(ctx).Where(query.ID.Eq(id)).Updates(models.Tunnel{
     106 + UniqID: &uid,
     107 + Addr: &addr,
    89 108   }); err != nil {
    90 109   xlog.Error(xlog.ServiceDBUpdateFiledError, "type", "tunnel_addr", "err", err)
    91 110   }
    skipped 1 lines
    93 112   
    94 113  func (t *tunnel) DeleteTunnel(ctx context.Context, id uint) error {
    95 114   query := dao.Q.Tunnel
    96  - res, err := query.WithContext(ctx).Where(query.ID.Eq(id)).Delete()
     115 + res, err := query.WithContext(ctx).Unscoped().Where(query.ID.Eq(id)).Delete()
    97 116   if err != nil || res.Error != nil {
    98 117   return err
    99 118   }
    skipped 12 lines
    112 131   return false
    113 132  }
    114 133   
    115  -func (t *tunnel) DeployTunnel(ctx context.Context, tun *models.Tunnel) (string, error) {
     134 +func (t *tunnel) DeployTunnel(ctx context.Context, tun *models.Tunnel) (string, string, error) {
    116 135   
    117 136   prv, err := SVC.GetProviderById(ctx, tun.ProviderId)
    118 137   if err != nil {
    119  - return "", err
     138 + return "", "", err
    120 139   }
    121 140   
    122  - addr, err := sdk.GetSDK(*prv.Type).Deploy(prv.CloudAuth, tun)
    123  - if err != nil {
    124  - return "", err
    125  - }
    126  - return addr, nil
     141 + return sdk.GetSDK(*prv.Type).Deploy(prv.CloudAuth, tun)
    127 142  }
    128 143   
    129 144  func (t *tunnel) StopTunnel(ctx context.Context, tun *models.Tunnel) error {
    skipped 11 lines
  • ■ ■ ■ ■
    pkg/listener/tcp.go
    skipped 7 lines
    8 8   "github.com/DVKunion/SeaMoon/pkg/api/models"
    9 9   db_service "github.com/DVKunion/SeaMoon/pkg/api/service"
    10 10   "github.com/DVKunion/SeaMoon/pkg/network"
    11  - "github.com/DVKunion/SeaMoon/pkg/service"
    12 11   "github.com/DVKunion/SeaMoon/pkg/system/errors"
    13 12   "github.com/DVKunion/SeaMoon/pkg/system/xlog"
     13 + "github.com/DVKunion/SeaMoon/pkg/tunnel/service"
    14 14  )
    15 15   
    16 16  func TCPListen(ctx context.Context, py *models.Proxy) (net.Listener, error) {
    skipped 51 lines
  • ■ ■ ■ ■ ■ ■
    pkg/listener/v2ray.go
     1 +package listener
     2 + 
     3 +// 懒得写了,直接套一个 v2ray 的客户端去做 wrapper 原本的conn完事了
     4 + 
  • ■ ■ ■ ■ ■ ■
    pkg/sdk/aliyun/aliyun.go
    skipped 1 lines
    2 2   
    3 3  import (
    4 4   "github.com/DVKunion/SeaMoon/pkg/api/models"
    5  - "github.com/DVKunion/SeaMoon/pkg/tools"
     5 + "github.com/DVKunion/SeaMoon/pkg/system/tools"
    6 6  )
    7 7   
    8 8  var (
    skipped 30 lines
    39 39   }, nil
    40 40  }
    41 41   
    42  -func (a *SDK) Deploy(ca *models.CloudAuth, tun *models.Tunnel) (string, error) {
     42 +func (a *SDK) Deploy(ca *models.CloudAuth, tun *models.Tunnel) (string, string, error) {
    43 43   return deploy(ca, tun)
    44 44  }
    45 45   
    skipped 8 lines
  • ■ ■ ■ ■ ■ ■
    pkg/sdk/aliyun/aliyun_sdk.go
    skipped 73 lines
    74 74   return strconv.ParseFloat(strings.Replace(r.Body.Data["AvailableAmount"].(string), ",", "", -1), 64)
    75 75  }
    76 76   
    77  -func deploy(ca *models.CloudAuth, tun *models.Tunnel) (string, error) {
     77 +func deploy(ca *models.CloudAuth, tun *models.Tunnel) (string, string, error) {
     78 + uid := ""
    78 79   // 原生的库是真tm的难用,
    79 80   client, err := fc.NewClient(
    80 81   fmt.Sprintf("%s.%s.fc.aliyuncs.com", ca.AccessId, tun.Config.Region),
    81 82   "2016-08-15", ca.AccessKey, ca.AccessSecret)
    82 83   if err != nil {
    83  - return "", err
     84 + return "", "", err
    84 85   }
    85 86   // 先尝试是否已经存在了 svc
    86 87   _, err = client.GetService(fc.NewGetServiceInput(serviceName))
    skipped 5 lines
    92 93   WithServiceName(serviceName).
    93 94   WithDescription(serviceDesc))
    94 95   if err != nil {
    95  - return "", err
     96 + return "", "", err
    96 97   }
    97 98   }
    98 99   } else {
    99  - return "", err
     100 + return "", "", err
    100 101   }
    101 102   }
    102 103   
    103 104   funcName := *tun.Name
    104 105   // 有了服务了,现在来创建函数
    105  - if _, err = client.CreateFunction(fc.NewCreateFunctionInput(serviceName).
     106 + if res, err := client.CreateFunction(fc.NewCreateFunctionInput(serviceName).
    106 107   WithFunctionName(funcName).
    107 108   WithDescription(string(*tun.Type)).
    108 109   WithRuntime("custom-container").
    109 110   WithCPU(tun.Config.CPU).
    110 111   WithMemorySize(tun.Config.Memory).
    111 112   WithHandler("main").
     113 + WithEnvironmentVariables(map[string]string{
     114 + "SM_SS_PASS": tun.Config.SSRPass,
     115 + "SM_SS_CRYPT": tun.Config.SSRCrypt,
     116 + "SM_UID": tun.Config.V2rayUid,
     117 + }).
    112 118   WithDisk(512).
    113 119   WithInstanceConcurrency(tun.Config.Instance).
    114 120   WithCAPort(*tun.Port).
    skipped 3 lines
    118 124   WithImage(fmt.Sprintf("%s:%s", registryEndPoint[tun.Config.Region], xlog.Version)).
    119 125   WithCommand("[\"./seamoon\"]").
    120 126   WithArgs("[\"server\"]"))); err != nil {
    121  - return "", err
     127 + return "", "", err
     128 + } else {
     129 + uid = *res.FunctionID
    122 130   }
    123 131   // 有了函数了,接下来创建 trigger
    124 132   if _, err = client.CreateTrigger(fc.NewCreateTriggerInput(serviceName, funcName).
    skipped 4 lines
    129 137   AuthType: "anonymous",
    130 138   DisableURLInternet: false,
    131 139   })); err != nil {
    132  - return "", err
     140 + return "", "", err
    133 141   }
    134 142   // 创建成功了, 查一下
    135 143   respTS, err := client.GetTrigger(fc.NewGetTriggerInput(serviceName, funcName, string(*tun.Type)))
    136 144   if err != nil {
    137  - return "", err
     145 + return "", "", err
    138 146   }
    139 147   
    140  - return strings.Replace(respTS.UrlInternet, "https://", "", -1), nil
     148 + return strings.Replace(respTS.UrlInternet, "https://", "", -1), uid, nil
    141 149  }
    142 150   
    143 151  func destroy(ca *models.CloudAuth, tun *models.Tunnel) error {
    skipped 53 lines
    197 205   Memory: *c.MemorySize,
    198 206   Instance: *c.InstanceConcurrency,
    199 207   
    200  - // todo: 这里太糙了
    201 208   TLS: true, // 默认同步过来都打开
    202  - Tor: func() bool {
    203  - // 如果是 开启 Tor 的隧道,需要有环境变量
    204  - return len(c.EnvironmentVariables) > 0
    205  - }(),
     209 + Tor: false,
     210 + }
     211 + if len(c.EnvironmentVariables) > 0 {
     212 + for key, value := range c.EnvironmentVariables {
     213 + if key == "SEAMOON_TOR" {
     214 + tun.Config.Tor = true
     215 + }
     216 + if key == "SM_SS_CRYPT" {
     217 + tun.Config.SSRCrypt = value
     218 + }
     219 + if key == "SM_SS_PASS" {
     220 + tun.Config.SSRPass = value
     221 + }
     222 + if key == "SM_UID" {
     223 + tun.Config.V2rayUid = value
     224 + }
     225 + }
    206 226   }
    207 227   *tun.Type = enum.TransTunnelType(*c.Description)
    208 228   tun.Port = c.CAPort
    skipped 21 lines
  • ■ ■ ■ ■
    pkg/sdk/sdk.go
    skipped 12 lines
    13 13   // 返回认证后查询的账户信息
    14 14   Auth(ca *models.CloudAuth, region string) (*models.ProviderInfo, error)
    15 15   // Deploy 部署隧道函数
    16  - Deploy(ca *models.CloudAuth, tun *models.Tunnel) (string, error)
     16 + Deploy(ca *models.CloudAuth, tun *models.Tunnel) (string, string, error)
    17 17   // Destroy 删除隧道函数
    18 18   Destroy(ca *models.CloudAuth, tun *models.Tunnel) error
    19 19   // SyncFC 同步函数
    skipped 18 lines
  • ■ ■ ■ ■ ■
    pkg/sdk/sealos/sealos.go
    skipped 4 lines
    5 5   "strings"
    6 6   
    7 7   "github.com/DVKunion/SeaMoon/pkg/api/enum"
     8 + "github.com/DVKunion/SeaMoon/pkg/api/models"
     9 + "github.com/DVKunion/SeaMoon/pkg/system/tools"
    8 10   "github.com/DVKunion/SeaMoon/pkg/system/xlog"
    9  - "github.com/DVKunion/SeaMoon/pkg/tools"
    10  - 
    11  - "github.com/DVKunion/SeaMoon/pkg/api/models"
    12 11  )
    13 12   
    14 13  type SDK struct {
    skipped 10 lines
    25 24   }, nil
    26 25  }
    27 26   
    28  -func (s *SDK) Deploy(ca *models.CloudAuth, tun *models.Tunnel) (string, error) {
     27 +func (s *SDK) Deploy(ca *models.CloudAuth, tun *models.Tunnel) (string, string, error) {
    29 28   
    30 29   // 拼接规则 seamoon-NAME-TYPE
    31 30   svc := "seamoon-" + *tun.Name + "-" + string(*tun.Type)
    skipped 3 lines
    35 34   host := tools.GenerateRandomLetterString(12)
    36 35   
    37 36   addr := fmt.Sprintf("%s.%s", host, regionMap[tun.Config.Region])
    38  - 
    39  - return addr, deploy(ca.KubeConfig, svc, img, host, *tun.Port, tun.Config, tun.Type)
     37 + uid, err := deploy(ca.KubeConfig, svc, img, host, *tun.Port, tun.Config, tun.Type)
     38 + return addr, uid, err
    40 39  }
    41 40   
    42 41  func (s *SDK) Destroy(ca *models.CloudAuth, tun *models.Tunnel) error {
    skipped 42 lines
    85 84   Memory: int32(svc.Spec.Template.Spec.Containers[0].Resources.Limits.Memory().MilliValue()) / 1024 / 1024 / 1000,
    86 85   Instance: *svc.Spec.Replicas,
    87 86   FcAuthType: enum.AuthEmpty, // sealos暂不支持认证
     87 + Tor: false,
     88 + TLS: true,
     89 + }
     90 + for _, env := range svc.Spec.Template.Spec.Containers[0].Env {
     91 + if env.Name == "SEAMOON_TOR" {
     92 + tun.Config.Tor = true
     93 + }
     94 + if env.Name == "SM_SS_CRYPT" {
     95 + tun.Config.SSRCrypt = env.Value
     96 + }
     97 + if env.Name == "SM_SS_PASS" {
     98 + tun.Config.SSRPass = env.Value
     99 + }
     100 + if env.Name == "SM_UID" {
     101 + tun.Config.V2rayUid = env.Value
     102 + }
    88 103   }
    89 104   *tun.Type = func() enum.TunnelType {
    90 105   if strings.HasSuffix(svc.Name, "websocket") {
    skipped 19 lines
  • ■ ■ ■ ■ ■
    pkg/sdk/sealos/sealos_sdk.go
    skipped 21 lines
    22 22   
    23 23   "github.com/DVKunion/SeaMoon/pkg/api/enum"
    24 24   "github.com/DVKunion/SeaMoon/pkg/api/models"
     25 + "github.com/DVKunion/SeaMoon/pkg/system/xlog"
    25 26  )
    26 27   
    27 28  var (
    skipped 56 lines
    84 85   return float64(sa.Data.Balance-sa.Data.DeductionBalance) / 1000000, float64(sa.Data.DeductionBalance) / 1000000, nil
    85 86  }
    86 87   
    87  -func deploy(config, svcName, imgName, hostName string, port int32, tc *models.TunnelConfig, tp *enum.TunnelType) error {
     88 +func deploy(config, svcName, imgName, hostName string, port int32, tc *models.TunnelConfig, tp *enum.TunnelType) (string, error) {
    88 89   ctx := context.Background()
    89  - 
     90 + uid := ""
    90 91   ns, clientSet, err := parseKubeConfig(config)
    91 92   
    92 93   if err != nil {
    93  - return err
     94 + return "", err
    94 95   }
    95 96   
    96  - if _, err = clientSet.AppsV1().Deployments(ns).
     97 + if res, err := clientSet.AppsV1().Deployments(ns).
    97 98   Create(ctx, renderDeployment(svcName, imgName, port, tc, tp),
    98 99   metav1.CreateOptions{}); err != nil {
    99  - return err
     100 + return "", err
     101 + } else {
     102 + uid = string(res.ObjectMeta.UID)
    100 103   }
    101 104   
    102 105   if _, err = clientSet.CoreV1().Services(ns).
    103 106   Create(ctx, renderService(svcName, port), metav1.CreateOptions{}); err != nil {
    104  - return err
     107 + return "", err
    105 108   }
    106 109   
    107 110   // ingress
    108 111   if _, err = clientSet.NetworkingV1().Ingresses(ns).
    109 112   Create(ctx, renderIngress(svcName, hostName, tc, tp), metav1.CreateOptions{}); err != nil {
    110  - return err
     113 + return "", err
     114 + }
     115 + 
     116 + cnt := 0
     117 + status := enum.TunnelInitializing
     118 + message := ""
     119 + 
     120 + for cnt < 30 {
     121 + // 查看一下状态:
     122 + svcs, err := clientSet.AppsV1().Deployments(ns).List(ctx, metav1.ListOptions{
     123 + LabelSelector: "cloud.sealos.io/app-deploy-manager=" + svcName,
     124 + })
     125 + if err != nil {
     126 + return "", err
     127 + }
     128 + 
     129 + if len(svcs.Items) == 0 {
     130 + return "", errors.New(xlog.SDKFCCreateError)
     131 + }
     132 + 
     133 + for _, svc := range svcs.Items {
     134 + if svc.ObjectMeta.Name == svcName {
     135 + for _, condition := range svc.Status.Conditions {
     136 + xlog.Info(xlog.SDKWaitingFCStatus, "type", condition.Type, "status", condition.Status, "cnt", cnt)
     137 + if condition.Type == "Available" && condition.Status == "True" {
     138 + status = enum.TunnelActive
     139 + message = ""
     140 + cnt = 31
     141 + break
     142 + }
     143 + if condition.Type == "Progressing" && condition.Status == "True" {
     144 + message = condition.Message
     145 + }
     146 + if condition.Type == "Progressing" && condition.Status == "False" {
     147 + message = condition.Message
     148 + }
     149 + if condition.Type == "Available" && condition.Status == "False" && message == "" {
     150 + status = enum.TunnelError
     151 + message = condition.Message
     152 + }
     153 + }
     154 + }
     155 + }
     156 + 
     157 + cnt += 1
     158 + time.Sleep(2 * time.Second)
    111 159   }
    112  - return nil
     160 + 
     161 + if status != enum.TunnelActive {
     162 + return "", errors.New(message)
     163 + }
     164 + 
     165 + return uid, nil
    113 166  }
    114 167   
    115 168  func destroy(config, svcName string) error {
    skipped 83 lines
    199 252   Value: "true",
    200 253   })
    201 254   }
     255 + env = append(env, corev1.EnvVar{
     256 + Name: "SM_UID",
     257 + Value: config.V2rayUid,
     258 + })
     259 + env = append(env, corev1.EnvVar{
     260 + Name: "SM_SS_CRYPT",
     261 + Value: config.SSRCrypt,
     262 + })
     263 + env = append(env, corev1.EnvVar{
     264 + Name: "SM_SS_PASS",
     265 + Value: config.SSRPass,
     266 + })
    202 267   return env
    203 268   }(),
    204 269   Resources: corev1.ResourceRequirements{
    skipped 168 lines
  • ■ ■ ■ ■ ■ ■
    pkg/sdk/tencent/tencent.go
    skipped 4 lines
    5 5   
    6 6   "github.com/DVKunion/SeaMoon/pkg/api/enum"
    7 7   "github.com/DVKunion/SeaMoon/pkg/api/models"
    8  - "github.com/DVKunion/SeaMoon/pkg/tools"
     8 + "github.com/DVKunion/SeaMoon/pkg/system/tools"
    9 9  )
    10 10   
    11 11  var (
    skipped 22 lines
    34 34   }, nil
    35 35  }
    36 36   
    37  -func (t *SDK) Deploy(ca *models.CloudAuth, tun *models.Tunnel) (string, error) {
    38  - addr, err := deploy(ca, tun)
     37 +func (t *SDK) Deploy(ca *models.CloudAuth, tun *models.Tunnel) (string, string, error) {
     38 + addr, uid, err := deploy(ca, tun)
    39 39   if err != nil {
    40  - return "", err
     40 + return "", "", err
    41 41   }
    42  - return strings.Replace(addr, "https://", "", -1), nil
     42 + return strings.Replace(addr, "https://", "", -1), uid, nil
    43 43  }
    44 44   
    45 45  func (t *SDK) Destroy(ca *models.CloudAuth, tun *models.Tunnel) error {
    skipped 20 lines
    66 66   CPU: 0,
    67 67   Memory: tools.PtrInt32(fc.detail.MemorySize),
    68 68   Instance: 1, // 这个玩意tmd怎么也找不到,同步过来的就算他1好了。
    69  - 
    70  - TLS: true, // 默认同步过来都打开
    71  - Tor: func() bool {
    72  - if fc.detail.Environment != nil {
    73  - // 如果是 开启 Tor 的隧道,需要有环境变量
    74  - return len(fc.detail.Environment.Variables) > 0
     69 + Tor: false,
     70 + TLS: true, // 默认同步过来都打开
     71 + }
     72 + if fc.detail.Environment != nil {
     73 + for _, env := range fc.detail.Environment.Variables {
     74 + if *env.Key == "SEAMOON_TOR" {
     75 + tun.Config.Tor = true
     76 + }
     77 + if *env.Key == "SM_SS_CRYPT" {
     78 + tun.Config.SSRCrypt = *env.Value
     79 + }
     80 + if *env.Key == "SM_SS_PASS" {
     81 + tun.Config.SSRPass = *env.Value
     82 + }
     83 + if *env.Key == "SM_UID" {
     84 + tun.Config.V2rayUid = *env.Value
    75 85   }
    76  - return false
    77  - }(),
     86 + }
    78 87   }
    79 88   *tun.Type = enum.TransTunnelType(*fc.detail.Description)
    80 89   *tun.Port = int32(*fc.detail.ImageConfig.ImagePort)
    skipped 8 lines
  • ■ ■ ■ ■ ■ ■
    pkg/sdk/tencent/tencent_sdk.go
    skipped 154 lines
    155 155   return float64(balance) / 100, nil
    156 156  }
    157 157   
    158  -func deploy(ca *models.CloudAuth, tun *models.Tunnel) (string, error) {
     158 +func deploy(ca *models.CloudAuth, tun *models.Tunnel) (string, string, error) {
     159 + uid := ""
    159 160   credential := common.NewCredential(
    160 161   ca.AccessKey,
    161 162   ca.AccessSecret,
    skipped 6 lines
    168 169   client, err := scf.NewClient(credential, tun.Config.Region, cpf)
    169 170   
    170 171   if err != nil {
    171  - return "", err
     172 + return "", "", err
    172 173   }
    173 174   
    174 175   // SCF 需要一个 namespace
    skipped 6 lines
    181 182   if err != nil {
    182 183   // 如果错误是 ns 存在,则忽略。
    183 184   if err, ok := err.(*fcError.TencentCloudSDKError); !ok || err.Code != scf.RESOURCEINUSE_NAMESPACE {
    184  - return "", err
     185 + return "", "", err
    185 186   }
    186 187   }
    187 188   
    skipped 1 lines
    189 190   request := scf.NewCreateFunctionRequest()
    190 191   
    191 192   // 查询的时候只能用模糊匹配,sb, 得用个不会模糊的前缀区分
    192  - fcName := "a" + strconv.Itoa(int(tun.CreatedAt.Unix())) + "-" + *tun.Name
     193 + fcName := *tun.Name
    193 194   
    194 195   request.Namespace = common.StringPtr(serviceName)
    195 196   request.FunctionName = common.StringPtr(fcName)
    skipped 22 lines
    218 219   },
    219 220   }
    220 221   
    221  - if tun.Config.Tor {
    222  - request.Environment = &scf.Environment{
    223  - Variables: []*scf.Variable{
    224  - {
    225  - Key: common.StringPtr("SEAMOON_TOR"),
    226  - Value: common.StringPtr("true"),
    227  - },
     222 + request.Environment = &scf.Environment{
     223 + Variables: []*scf.Variable{
     224 + {
     225 + Key: common.StringPtr("SM_UID"),
     226 + Value: common.StringPtr(tun.Config.V2rayUid),
    228 227   },
    229  - }
     228 + {
     229 + Key: common.StringPtr("SM_SS_CRYPT"),
     230 + Value: common.StringPtr(tun.Config.SSRCrypt),
     231 + },
     232 + {
     233 + Key: common.StringPtr("SM_SS_PASS"),
     234 + Value: common.StringPtr(tun.Config.SSRPass),
     235 + },
     236 + },
     237 + }
     238 + 
     239 + if tun.Config.Tor {
     240 + request.Environment.Variables = append(request.Environment.Variables, &scf.Variable{
     241 + Key: common.StringPtr("SEAMOON_TOR"),
     242 + Value: common.StringPtr("true"),
     243 + })
    230 244   }
    231 245   
    232 246   request.PublicNetConfig = &scf.PublicNetConfigIn{
    skipped 11 lines
    244 258   _, err = client.CreateFunction(request)
    245 259   if err != nil {
    246 260   if err, ok := err.(*fcError.TencentCloudSDKError); !ok || err.Code != scf.RESOURCEINUSE_FUNCTION {
    247  - return "", err
     261 + return "", "", err
    248 262   }
    249 263   }
    250 264   
    skipped 7 lines
    258 272   
    259 273   fc, err := client.ListFunctions(eRequest)
    260 274   if err != nil {
    261  - return "", err
     275 + return "", "", err
    262 276   }
    263 277   if *fc.Response.TotalCount != 1 {
    264  - return "", errors.New(xlog.SDKFCInfoError)
     278 + return "", "", errors.New(xlog.SDKFCInfoError)
    265 279   }
    266 280   xlog.Info(xlog.SDKWaitingFCStatus, "status", *fc.Response.Functions[0].Status, "cnt", cnt)
    267 281   switch *fc.Response.Functions[0].Status {
    268 282   case "Active":
    269 283   cnt = 31
     284 + uid = *fc.Response.Functions[0].FunctionId
    270 285   case "Creating":
    271 286   time.Sleep(2 * time.Second)
    272 287   cnt++
    273 288   continue
    274 289   default:
    275  - return "", errors.New(*fc.Response.Functions[0].StatusDesc)
     290 + return "", "", errors.New(*fc.Response.Functions[0].StatusDesc)
    276 291   }
    277 292   }
    278 293   
    skipped 33 lines
    312 327   
    313 328   response, err := client.CreateTrigger(r)
    314 329   if err != nil {
    315  - return "", err
     330 + return "", "", err
    316 331   }
    317 332   
    318 333   extractor := &triggerResp{}
    319 334   desc := *response.Response.TriggerInfo.TriggerDesc
    320 335   if err := json.Unmarshal([]byte(desc), extractor); err != nil {
    321  - return "", err
     336 + return "", "", err
    322 337   }
    323 338   
    324  - return extractor.Service.SubDomain, nil
     339 + return extractor.Service.SubDomain, uid, nil
    325 340  }
    326 341   
    327 342  func destroy(ca *models.CloudAuth, tun *models.Tunnel) error {
    skipped 12 lines
    340 355   return err
    341 356   }
    342 357   
    343  - fcName := "a" + strconv.Itoa(int(tun.CreatedAt.Unix())) + "-" + *tun.Name
     358 + fcName := *tun.Name
    344 359   
    345 360   // 先删除触发器
    346 361   r := scf.NewDeleteTriggerRequest()
    347 362   r.TriggerName = common.StringPtr("apigw")
     363 + r.Type = common.StringPtr("apigw")
    348 364   r.FunctionName = common.StringPtr(fcName)
    349 365   r.Namespace = common.StringPtr(serviceName)
    350 366   if _, err = client.DeleteTrigger(r); err != nil {
    skipped 91 lines
  • ■ ■ ■ ■ ■ ■
    pkg/signal/handler_provider.go
    skipped 1 lines
    2 2   
    3 3  import (
    4 4   "context"
     5 + "sync"
    5 6   
    6 7   "github.com/DVKunion/SeaMoon/pkg/api/enum"
    7 8   "github.com/DVKunion/SeaMoon/pkg/api/service"
    8 9   "github.com/DVKunion/SeaMoon/pkg/system/xlog"
    9 10  )
     11 + 
     12 +func (sb *Bus) SendProviderSignal(p uint, tp enum.ProviderStatus) {
     13 + sb.providerChannel <- &providerSignal{
     14 + id: p,
     15 + next: tp,
     16 + wg: nil,
     17 + }
     18 +}
     19 + 
     20 +func (sb *Bus) SendProviderSignalSync(p uint, tp enum.ProviderStatus) {
     21 + wg := &sync.WaitGroup{}
     22 + wg.Add(1)
     23 + sb.providerChannel <- &providerSignal{
     24 + id: p,
     25 + next: tp,
     26 + wg: wg,
     27 + }
     28 + wg.Wait()
     29 +}
    10 30   
    11 31  func (sb *Bus) providerHandler(ctx context.Context, prs *providerSignal) {
    12 32   // proxy sync change task
    skipped 41 lines
  • ■ ■ ■ ■ ■ ■
    pkg/signal/handler_proxy.go
    skipped 1 lines
    2 2   
    3 3  import (
    4 4   "context"
     5 + "sync"
    5 6   
    6 7   "github.com/DVKunion/SeaMoon/pkg/api/enum"
    7 8   "github.com/DVKunion/SeaMoon/pkg/api/models"
    skipped 1 lines
    9 10   "github.com/DVKunion/SeaMoon/pkg/listener"
    10 11   "github.com/DVKunion/SeaMoon/pkg/system/xlog"
    11 12  )
     13 + 
     14 +func (sb *Bus) SendProxySignal(p uint, tp enum.ProxyStatus) {
     15 + sb.proxyChannel <- &proxySignal{
     16 + id: p,
     17 + next: tp,
     18 + wg: nil,
     19 + }
     20 +}
     21 + 
     22 +func (sb *Bus) SendProxySignalSync(p uint, tp enum.ProxyStatus) {
     23 + wg := &sync.WaitGroup{}
     24 + wg.Add(1)
     25 + sb.proxyChannel <- &proxySignal{
     26 + id: p,
     27 + next: tp,
     28 + wg: wg,
     29 + }
     30 + wg.Wait()
     31 +}
    12 32   
    13 33  func (sb *Bus) proxyHandler(ctx context.Context, pys *proxySignal) {
    14 34   // proxy sync change task
    skipped 75 lines
  • ■ ■ ■ ■ ■ ■
    pkg/signal/handler_tunnel.go
    skipped 1 lines
    2 2   
    3 3  import (
    4 4   "context"
     5 + "sync"
    5 6   
    6 7   "github.com/DVKunion/SeaMoon/pkg/api/enum"
    7 8   "github.com/DVKunion/SeaMoon/pkg/api/models"
    skipped 1 lines
    9 10   "github.com/DVKunion/SeaMoon/pkg/system/xlog"
    10 11  )
    11 12   
     13 +func (sb *Bus) SendTunnelSignal(p uint, tp enum.TunnelStatus) {
     14 + sb.tunnelChannel <- &tunnelSignal{
     15 + id: p,
     16 + next: tp,
     17 + wg: nil,
     18 + }
     19 +}
     20 + 
     21 +func (sb *Bus) SendTunnelSignalSync(p uint, tp enum.TunnelStatus) {
     22 + wg := &sync.WaitGroup{}
     23 + wg.Add(1)
     24 + sb.tunnelChannel <- &tunnelSignal{
     25 + id: p,
     26 + next: tp,
     27 + wg: wg,
     28 + }
     29 + wg.Wait()
     30 +}
     31 + 
    12 32  func (sb *Bus) tunnelHandler(ctx context.Context, ts *tunnelSignal) {
    13 33   // proxy sync change task
    14 34   // 如果是需要同步的,记得释放锁
    skipped 15 lines
    30 50   }
    31 51   service.SVC.UpdateTunnelStatus(ctx, tun.ID, ts.next, "")
    32 52   switch ts.next {
    33  - case enum.TunnelActive:
    34  - if addr, err := service.SVC.DeployTunnel(ctx, tun); err != nil {
     53 + case enum.TunnelInitializing, enum.TunnelActive:
     54 + if addr, uid, err := service.SVC.DeployTunnel(ctx, tun); err != nil {
    35 55   xlog.Error(xlog.SignalDeployTunError, "obj", "tunnel", "err", err)
    36 56   service.SVC.UpdateTunnelStatus(ctx, tun.ID, enum.TunnelError, err.Error())
    37 57   return
    38 58   } else {
    39  - service.SVC.UpdateTunnelAddr(ctx, tun.ID, addr)
     59 + service.SVC.UpdateTunnelDetail(ctx, tun.ID, addr, uid)
    40 60   }
    41 61   xlog.Info(xlog.SignalDeployTunnel, "id", tun.ID, "type", tun.Type)
     62 + service.SVC.UpdateTunnelStatus(ctx, tun.ID, enum.TunnelActive, "")
    42 63   case enum.TunnelInactive:
    43  - _ = sb.stopTunnel(ctx, tun)
     64 + sb.stopTunnel(ctx, tun)
    44 65   case enum.TunnelDelete:
    45 66   sb.deleteTunnel(ctx, tun)
    46 67   }
    47 68  }
    48 69   
    49  -func (sb *Bus) stopTunnel(ctx context.Context, tun *models.Tunnel) error {
     70 +func (sb *Bus) stopTunnel(ctx context.Context, tun *models.Tunnel) {
    50 71   if err := service.SVC.StopTunnel(ctx, tun); err != nil {
    51 72   xlog.Error(xlog.SignalStopTunError, "obj", "tunnel", "err", err)
    52 73   service.SVC.UpdateTunnelStatus(ctx, tun.ID, enum.TunnelError, err.Error())
    53  - return err
     74 + return
    54 75   }
    55 76   xlog.Info(xlog.SignalStopTunnel, "id", tun.ID, "type", tun.Type)
    56  - return nil
     77 + return
    57 78  }
    58 79   
    59 80  func (sb *Bus) deleteTunnel(ctx context.Context, tun *models.Tunnel) {
    skipped 1 lines
    61 82   for _, py := range tun.Proxies {
    62 83   sb.deleteProxy(ctx, &py)
    63 84   }
    64  - if err := sb.stopTunnel(ctx, tun); err != nil {
    65  - xlog.Error(xlog.SignalDeleteTunError, "obj", "tunnel", "err", err)
    66  - service.SVC.UpdateTunnelStatus(ctx, tun.ID, enum.TunnelError, err.Error())
    67  - return
    68  - }
     85 + 
     86 + sb.stopTunnel(ctx, tun)
     87 + 
    69 88   // 最后删除服务即可
    70 89   if err := service.SVC.DeleteTunnel(ctx, tun.ID); err != nil {
    71 90   xlog.Error(xlog.SignalDeleteTunError, "obj", "tunnel", "err", err)
    skipped 6 lines
  • ■ ■ ■ ■ ■
    pkg/signal/signal.go
    skipped 71 lines
    72 72   xlog.Error(xlog.SignalRecoverProxyError, "err", err)
    73 73   }
    74 74   for _, p := range proxies {
    75  - sb.SendProxySignal(p.ID, enum.ProxyStatusRecover, nil)
     75 + sb.SendProxySignal(p.ID, enum.ProxyStatusRecover)
    76 76   }
    77 77   }
    78 78  }
    79 79   
    80  -func (sb *Bus) SendProxySignal(p uint, tp enum.ProxyStatus, wg *sync.WaitGroup) {
    81  - sb.proxyChannel <- &proxySignal{
    82  - id: p,
    83  - next: tp,
    84  - wg: wg,
    85  - }
    86  -}
    87  - 
    88  -func (sb *Bus) SendProviderSignal(p uint, tp enum.ProviderStatus, wg *sync.WaitGroup) {
    89  - sb.providerChannel <- &providerSignal{
    90  - id: p,
    91  - next: tp,
    92  - wg: wg,
    93  - }
    94  -}
    95  - 
    96  -func (sb *Bus) SendTunnelSignal(p uint, tp enum.TunnelStatus, wg *sync.WaitGroup) {
    97  - sb.tunnelChannel <- &tunnelSignal{
    98  - id: p,
    99  - next: tp,
    100  - wg: wg,
    101  - }
    102  -}
    103  - 
  • pkg/tools/jwt.go pkg/system/tools/jwt.go
    Content is identical
  • pkg/tools/ptr.go pkg/system/tools/ptr.go
    Content is identical
  • ■ ■ ■ ■ ■ ■
    pkg/tools/random.go pkg/system/tools/random.go
    skipped 2 lines
    3 3  import (
    4 4   "math/rand"
    5 5   "strings"
     6 + 
     7 + "github.com/v2fly/v2ray-core/v5/common/uuid"
    6 8  )
    7 9   
    8 10  const randomList = "ASDFGHJKLZXCVBNMQWERTYUIOPasdfghjklzxcvbnmqwertyuiop1234567890"
    9 11  const randomLetterList = "asdfghjklzxcvbnmqwertyuiop"
     12 + 
     13 +func GenerateUUID() string {
     14 + u := uuid.New()
     15 + return u.String()
     16 +}
    10 17   
    11 18  func GenerateRandomString(length int) string {
    12 19   var sb strings.Builder
    skipped 20 lines
  • ■ ■ ■ ■ ■
    pkg/system/xlog/consts.go
    skipped 92 lines
    93 93   
    94 94  // SDK 相关错误
    95 95  const (
     96 + SDKFCCreateError = "sdk create function but not found error"
    96 97   SDKFCInfoError = "sdk get function info error"
    97 98   SDKFCDetailError = "sdk get function detail error"
    98 99   SDKTriggerError = "sdk get function trigger error"
    skipped 22 lines
  • ■ ■ ■ ■ ■ ■
    pkg/transfer/v2ray.go
    skipped 23 lines
    24 24   
    25 25  var v2ray *core.Instance
    26 26   
    27  -func InitV2ray(port uint32, id, pass, crypt string, tp enum.TunnelType, tor bool, tls bool) error {
     27 +func InitV2rayServer(port uint32, id, pass, crypt string, tp enum.TunnelType, tor bool, tls bool) error {
    28 28   config, err := renderConfig(port, id, pass, crypt, tp, tor, tls)
    29 29   if err != nil {
    30 30   return err
    skipped 5 lines
    36 36   return nil
    37 37  }
    38 38   
    39  -// V2rayTransport v2ray 相关协议支持: vmess / vless / vlite
     39 +// V2rayTransport v2ray 相关协议支持: vmess / vless
    40 40  // 这是一个偷懒的版本,并没有详细的研究对应协议的具体通信解析方案, 直接集成了 v2ray-core, 并且实现的相当的简陋。
    41 41  // 还是期望能够和 socks5 一样保持一致是最好的
    42 42  // proto 来自己做 dispatch
    skipped 159 lines
  • ■ ■ ■ ■
    pkg/transfer/v2ray_test.go
    skipped 13 lines
    14 14   "port": 10000,
    15 15   "tag": "xixixi",
    16 16   "listen":"127.0.0.1",
    17  - "protocol": "shadowsocks",
     17 + "protocol": "shadowsocks2022",
    18 18   "settings": {
    19 19   "method": "aes-256-gcm",
    20 20   "password": "123456",
    skipped 21 lines
  • ■ ■ ■ ■ ■ ■
    pkg/tunnel/grpc.go
    skipped 8 lines
    9 9   
    10 10   "google.golang.org/grpc"
    11 11   
    12  - "github.com/DVKunion/SeaMoon/pkg/service/proto"
     12 + proto2 "github.com/DVKunion/SeaMoon/pkg/tunnel/service/proto"
    13 13  )
    14 14   
    15 15  type grpcConn struct {
    skipped 31 lines
    47 47  }
    48 48   
    49 49  func (c *grpcConn) Write(b []byte) (n int, err error) {
    50  - chunk := &proto.Chunk{
     50 + chunk := &proto2.Chunk{
    51 51   Body: b,
    52 52   Size: int32(len(b)),
    53 53   }
    skipped 8 lines
    62 62   
    63 63  func (c *grpcConn) Close() error {
    64 64   switch cost := c.cc.(type) {
    65  - case proto.Tunnel_HttpClient:
    66  - case proto.Tunnel_Socks5Client:
     65 + case proto2.Tunnel_HttpClient:
     66 + case proto2.Tunnel_Socks5Client:
    67 67   return cost.CloseSend()
    68 68   }
    69 69   return nil
    skipped 26 lines
    96 96   return context.Background()
    97 97  }
    98 98   
    99  -func (c *grpcConn) send(data *proto.Chunk) error {
     99 +func (c *grpcConn) send(data *proto2.Chunk) error {
    100 100   sender, ok := c.cc.(interface {
    101  - Send(*proto.Chunk) error
     101 + Send(*proto2.Chunk) error
    102 102   })
    103 103   if !ok {
    104 104   // todo
    skipped 2 lines
    107 107   return sender.Send(data)
    108 108  }
    109 109   
    110  -func (c *grpcConn) recv() (*proto.Chunk, error) {
     110 +func (c *grpcConn) recv() (*proto2.Chunk, error) {
    111 111   receiver, ok := c.cc.(interface {
    112  - Recv() (*proto.Chunk, error)
     112 + Recv() (*proto2.Chunk, error)
    113 113   })
    114 114   if !ok {
    115 115   // todo
    skipped 5 lines
  • ■ ■ ■ ■ ■ ■
    pkg/service/grpc.go pkg/tunnel/service/grpc.go
    skipped 11 lines
    12 12   "google.golang.org/grpc/keepalive"
    13 13   
    14 14   "github.com/DVKunion/SeaMoon/pkg/api/enum"
    15  - pb "github.com/DVKunion/SeaMoon/pkg/service/proto"
    16  - "github.com/DVKunion/SeaMoon/pkg/service/proto/gost"
    17 15   "github.com/DVKunion/SeaMoon/pkg/system/xlog"
    18 16   "github.com/DVKunion/SeaMoon/pkg/transfer"
    19 17   "github.com/DVKunion/SeaMoon/pkg/tunnel"
     18 + "github.com/DVKunion/SeaMoon/pkg/tunnel/service/proto"
     19 + "github.com/DVKunion/SeaMoon/pkg/tunnel/service/proto/gost"
    20 20  )
    21 21   
    22 22  type GRPCService struct {
    skipped 1 lines
    24 24   cc *grpc.ClientConn
    25 25   server *grpc.Server
    26 26   startAt time.Time
    27  - pb.UnimplementedTunnelServer
     27 + proto.UnimplementedTunnelServer
    28 28   gost.UnimplementedGostTunelServer
    29 29  }
    30 30   
    skipped 53 lines
    84 84   }
    85 85   }
    86 86   
    87  - client := pb.NewTunnelClient(g.cc)
     87 + client := proto.NewTunnelClient(g.cc)
    88 88   
    89 89   switch t {
    90 90   case enum.ProxyTypeHTTP:
    skipped 39 lines
    130 130   
    131 131   server := grpc.NewServer(gRPCOpts...)
    132 132   
    133  - pb.RegisterTunnelServer(server, &g)
     133 + proto.RegisterTunnelServer(server, &g)
    134 134   gost.RegisterGostTunelServer(server, &g)
    135 135   
    136 136   g.startAt = time.Now()
    137 137   return server.Serve(ln)
    138 138  }
    139 139   
    140  -func (g GRPCService) Auto(server pb.Tunnel_AutoServer) error {
     140 +func (g GRPCService) Auto(server proto.Tunnel_AutoServer) error {
    141 141   gt := tunnel.GRPCWrapConn(g.addr, server)
    142 142   
    143 143   if err := transfer.AutoTransport(gt); err != nil {
    skipped 3 lines
    147 147   return nil
    148 148  }
    149 149   
    150  -func (g GRPCService) Http(server pb.Tunnel_HttpServer) error {
     150 +func (g GRPCService) Http(server proto.Tunnel_HttpServer) error {
    151 151   gt := tunnel.GRPCWrapConn(g.addr, server)
    152 152   
    153 153   if err := transfer.HttpTransport(gt); err != nil {
    skipped 4 lines
    158 158   return nil
    159 159  }
    160 160   
    161  -func (g GRPCService) Socks5(server pb.Tunnel_Socks5Server) error {
     161 +func (g GRPCService) Socks5(server proto.Tunnel_Socks5Server) error {
    162 162   gt := tunnel.GRPCWrapConn(g.addr, server)
    163 163   
    164 164   if err := transfer.Socks5Transport(gt, false); err != nil {
    skipped 3 lines
    168 168   return nil
    169 169  }
    170 170   
    171  -func (g GRPCService) V2RaySsr(server pb.Tunnel_V2RaySsrServer) error {
     171 +func (g GRPCService) V2RaySsr(server proto.Tunnel_V2RaySsrServer) error {
    172 172   gt := tunnel.GRPCWrapConn(g.addr, server)
    173 173   
    174 174   if err := transfer.V2rayTransport(gt, "shadowsocks"); err != nil {
    skipped 3 lines
    178 178   return nil
    179 179  }
    180 180   
    181  -func (g GRPCService) V2RayVmess(server pb.Tunnel_V2RayVmessServer) error {
     181 +func (g GRPCService) V2RayVmess(server proto.Tunnel_V2RayVmessServer) error {
    182 182   gt := tunnel.GRPCWrapConn(g.addr, server)
    183 183   
    184 184   if err := transfer.V2rayTransport(gt, "vmess"); err != nil {
    skipped 3 lines
    188 188   return nil
    189 189  }
    190 190   
    191  -func (g GRPCService) V2RayVless(server pb.Tunnel_V2RayVlessServer) error {
     191 +func (g GRPCService) V2RayVless(server proto.Tunnel_V2RayVlessServer) error {
    192 192   gt := tunnel.GRPCWrapConn(g.addr, server)
    193 193   
    194 194   if err := transfer.V2rayTransport(gt, "vless"); err != nil {
    skipped 14 lines
    209 209   return nil
    210 210  }
    211 211   
    212  -func (g GRPCService) Health(ctx context.Context, p *pb.Ping) (*pb.Pong, error) {
    213  - return &pb.Pong{
     212 +func (g GRPCService) Health(ctx context.Context, p *proto.Ping) (*proto.Pong, error) {
     213 + return &proto.Pong{
    214 214   Status: "OK",
    215 215   Time: g.startAt.Format("2006-01-02 15:04:05"),
    216 216   Version: xlog.Version,
    skipped 4 lines
  • pkg/service/options.go pkg/tunnel/service/options.go
    Content is identical
  • pkg/service/proto/Makefile pkg/tunnel/service/proto/Makefile
    Content is identical
  • ■ ■ ■ ■ ■
    pkg/service/proto/gost/gost.pb.go pkg/tunnel/service/proto/gost/gost.pb.go
    skipped 6 lines
    7 7  package gost
    8 8   
    9 9  import (
     10 + reflect "reflect"
     11 + sync "sync"
     12 + 
    10 13   protoreflect "google.golang.org/protobuf/reflect/protoreflect"
    11 14   protoimpl "google.golang.org/protobuf/runtime/protoimpl"
    12  - reflect "reflect"
    13  - sync "sync"
    14 15  )
    15 16   
    16 17  const (
    skipped 132 lines
  • pkg/service/proto/gost/gost_grpc.pb.go pkg/tunnel/service/proto/gost/gost_grpc.pb.go
    Content is identical
  • pkg/service/proto/gost.proto pkg/tunnel/service/proto/gost.proto
    Content is identical
  • ■ ■ ■ ■ ■
    pkg/service/proto/tunnel.pb.go pkg/tunnel/service/proto/tunnel.pb.go
    skipped 6 lines
    7 7  package proto
    8 8   
    9 9  import (
     10 + reflect "reflect"
     11 + sync "sync"
     12 + 
    10 13   protoreflect "google.golang.org/protobuf/reflect/protoreflect"
    11 14   protoimpl "google.golang.org/protobuf/runtime/protoimpl"
    12  - reflect "reflect"
    13  - sync "sync"
    14 15  )
    15 16   
    16 17  const (
    skipped 323 lines
  • pkg/service/proto/tunnel.proto pkg/tunnel/service/proto/tunnel.proto
    Content is identical
  • ■ ■ ■ ■ ■
    pkg/service/proto/tunnel_grpc.pb.go pkg/tunnel/service/proto/tunnel_grpc.pb.go
    skipped 7 lines
    8 8   
    9 9  import (
    10 10   context "context"
     11 + 
    11 12   grpc "google.golang.org/grpc"
    12 13   codes "google.golang.org/grpc/codes"
    13 14   status "google.golang.org/grpc/status"
    skipped 502 lines
  • pkg/service/service.go pkg/tunnel/service/service.go
    Content is identical
  • ■ ■ ■ ■
    pkg/service/websocket.go pkg/tunnel/service/websocket.go
    skipped 106 lines
    107 107   // websocket socks5 proxy handler
    108 108   mux.HandleFunc("/socks5", s.socks5)
    109 109   
    110  - if err := transfer.InitV2ray(uint32(port), srvOpts.uid, srvOpts.pass, srvOpts.crypt, enum.TunnelTypeWST, srvOpts.tor, srvOpts.tlsConf != nil); err == nil {
     110 + if err := transfer.InitV2rayServer(uint32(port), srvOpts.uid, srvOpts.pass, srvOpts.crypt, enum.TunnelTypeWST, srvOpts.tor, srvOpts.tlsConf != nil); err == nil {
    111 111   mux.HandleFunc("/vmess", s.v2ray("vmess"))
    112 112   mux.HandleFunc("/vless", s.v2ray("vless"))
    113 113   mux.HandleFunc("/v-shadowsocks", s.v2ray("shadowsocks"))
    skipped 89 lines
  • ■ ■ ■ ■ ■ ■
    web/config/config.dev.ts
     1 +// https://umijs.org/config/
     2 +import { defineConfig } from 'umi';
     3 + 
     4 +export default defineConfig({
     5 + plugins: [
     6 + // https://github.com/zthxxx/react-dev-inspector
     7 + 'react-dev-inspector/plugins/umi/react-inspector',
     8 + ],
     9 + // https://github.com/zthxxx/react-dev-inspector#inspector-loader-props
     10 + inspectorConfig: {
     11 + exclude: [],
     12 + babelPlugins: [],
     13 + babelOptions: {},
     14 + },
     15 +});
     16 + 
  • ■ ■ ■ ■ ■ ■
    web/config/config.ts
     1 +// https://umijs.org/config/
     2 +import { defineConfig } from 'umi';
     3 +import { join } from 'path';
     4 + 
     5 +import defaultSettings from './defaultSettings';
     6 +import proxy from './proxy';
     7 +import routes from './routes';
     8 + 
     9 +const { REACT_APP_ENV } = process.env;
     10 + 
     11 +export default defineConfig({
     12 + hash: true,
     13 + antd: {},
     14 + dva: {
     15 + hmr: true,
     16 + },
     17 + layout: {
     18 + // https://umijs.org/zh-CN/plugins/plugin-layout
     19 + locale: true,
     20 + siderWidth: 208,
     21 + ...defaultSettings,
     22 + },
     23 + // https://umijs.org/zh-CN/plugins/plugin-locale
     24 + locale: {
     25 + // default zh-CN
     26 + default: 'zh-CN',
     27 + antd: true,
     28 + // default true, when it is true, will use `navigator.language` overwrite default
     29 + baseNavigator: true,
     30 + },
     31 + dynamicImport: {
     32 + loading: '@ant-design/pro-layout/es/PageLoading',
     33 + },
     34 + targets: {
     35 + ie: 11,
     36 + },
     37 + // umi routes: https://umijs.org/docs/routing
     38 + routes,
     39 + access: {},
     40 + // Theme for antd: https://ant.design/docs/react/customize-theme-cn
     41 + theme: {
     42 + // 如果不想要 configProvide 动态设置主题需要把这个设置为 default
     43 + // 只有设置为 variable, 才能使用 configProvide 动态设置主色调
     44 + // https://ant.design/docs/react/customize-theme-variable-cn
     45 + 'root-entry-name': 'variable',
     46 + },
     47 + // esbuild is father build tools
     48 + // https://umijs.org/plugins/plugin-esbuild
     49 + esbuild: {},
     50 + title: false,
     51 + ignoreMomentLocale: true,
     52 + proxy: proxy[REACT_APP_ENV || 'dev'],
     53 + manifest: {
     54 + basePath: '/',
     55 + },
     56 + // Fast Refresh 热更新
     57 + fastRefresh: {},
     58 + openAPI: [
     59 + {
     60 + requestLibPath: "import { request } from 'umi'",
     61 + // 或者使用在线的版本
     62 + // schemaPath: "https://gw.alipayobjects.com/os/antfincdn/M%24jrzTTYJN/oneapi.json"
     63 + schemaPath: join(__dirname, 'oneapi.json'),
     64 + mock: false,
     65 + },
     66 + {
     67 + requestLibPath: "import { request } from 'umi'",
     68 + schemaPath: 'https://gw.alipayobjects.com/os/antfincdn/CA1dOm%2631B/openapi.json',
     69 + projectName: 'swagger',
     70 + },
     71 + ],
     72 + nodeModulesTransform: { type: 'none' },
     73 + mfsu: {},
     74 + webpack5: {},
     75 + exportStatic: {},
     76 +});
     77 + 
  • ■ ■ ■ ■ ■ ■
    web/config/defaultSettings.ts
     1 +import {Settings as LayoutSettings} from '@ant-design/pro-components';
     2 + 
     3 +const Settings: LayoutSettings & {
     4 + pwa?: boolean;
     5 + logo?: string;
     6 +} = {
     7 + navTheme: 'realDark',
     8 + // 拂晓蓝
     9 + primaryColor: '#76b39d',
     10 + layout: 'mix',
     11 + contentWidth: 'Fluid',
     12 + fixedHeader: false,
     13 + fixSiderbar: true,
     14 + colorWeak: false,
     15 + title: 'SeaMoon',
     16 + pwa: false,
     17 + logo: '/icon.svg',
     18 + iconfontUrl: '',
     19 +};
     20 + 
     21 +export default Settings;
     22 + 
  • ■ ■ ■ ■ ■ ■
    web/config/oneapi.json
     1 +{
     2 + "openapi": "3.0.1",
     3 + "info": {
     4 + "title": "Ant Design Pro",
     5 + "version": "1.0.0"
     6 + },
     7 + "servers": [
     8 + {
     9 + "url": "http://localhost:8000/"
     10 + },
     11 + {
     12 + "url": "https://localhost:8000/"
     13 + }
     14 + ],
     15 + "paths": {
     16 + "/api/currentUser": {
     17 + "get": {
     18 + "tags": ["api"],
     19 + "description": "获取当前的用户",
     20 + "operationId": "currentUser",
     21 + "responses": {
     22 + "200": {
     23 + "description": "Success",
     24 + "content": {
     25 + "application/json": {
     26 + "schema": {
     27 + "$ref": "#/components/schemas/CurrentUser"
     28 + }
     29 + }
     30 + }
     31 + },
     32 + "401": {
     33 + "description": "Error",
     34 + "content": {
     35 + "application/json": {
     36 + "schema": {
     37 + "$ref": "#/components/schemas/ErrorResponse"
     38 + }
     39 + }
     40 + }
     41 + }
     42 + }
     43 + },
     44 + "x-swagger-router-controller": "api"
     45 + },
     46 + "/api/login/captcha": {
     47 + "post": {
     48 + "description": "发送验证码",
     49 + "operationId": "getFakeCaptcha",
     50 + "tags": ["login"],
     51 + "parameters": [
     52 + {
     53 + "name": "phone",
     54 + "in": "query",
     55 + "description": "手机号",
     56 + "schema": {
     57 + "type": "string"
     58 + }
     59 + }
     60 + ],
     61 + "responses": {
     62 + "200": {
     63 + "description": "Success",
     64 + "content": {
     65 + "application/json": {
     66 + "schema": {
     67 + "$ref": "#/components/schemas/FakeCaptcha"
     68 + }
     69 + }
     70 + }
     71 + }
     72 + }
     73 + }
     74 + },
     75 + "/api/login/outLogin": {
     76 + "post": {
     77 + "description": "登录接口",
     78 + "operationId": "outLogin",
     79 + "tags": ["login"],
     80 + "responses": {
     81 + "200": {
     82 + "description": "Success",
     83 + "content": {
     84 + "application/json": {
     85 + "schema": {
     86 + "type": "object"
     87 + }
     88 + }
     89 + }
     90 + },
     91 + "401": {
     92 + "description": "Error",
     93 + "content": {
     94 + "application/json": {
     95 + "schema": {
     96 + "$ref": "#/components/schemas/ErrorResponse"
     97 + }
     98 + }
     99 + }
     100 + }
     101 + }
     102 + },
     103 + "x-swagger-router-controller": "api"
     104 + },
     105 + "/api/login/account": {
     106 + "post": {
     107 + "tags": ["login"],
     108 + "description": "登录接口",
     109 + "operationId": "login",
     110 + "requestBody": {
     111 + "description": "登录系统",
     112 + "content": {
     113 + "application/json": {
     114 + "schema": {
     115 + "$ref": "#/components/schemas/LoginParams"
     116 + }
     117 + }
     118 + },
     119 + "required": true
     120 + },
     121 + "responses": {
     122 + "200": {
     123 + "description": "Success",
     124 + "content": {
     125 + "application/json": {
     126 + "schema": {
     127 + "$ref": "#/components/schemas/LoginResult"
     128 + }
     129 + }
     130 + }
     131 + },
     132 + "401": {
     133 + "description": "Error",
     134 + "content": {
     135 + "application/json": {
     136 + "schema": {
     137 + "$ref": "#/components/schemas/ErrorResponse"
     138 + }
     139 + }
     140 + }
     141 + }
     142 + },
     143 + "x-codegen-request-body-name": "body"
     144 + },
     145 + "x-swagger-router-controller": "api"
     146 + },
     147 + "/api/notices": {
     148 + "summary": "getNotices",
     149 + "description": "NoticeIconItem",
     150 + "get": {
     151 + "tags": ["api"],
     152 + "operationId": "getNotices",
     153 + "responses": {
     154 + "200": {
     155 + "description": "Success",
     156 + "content": {
     157 + "application/json": {
     158 + "schema": {
     159 + "$ref": "#/components/schemas/NoticeIconList"
     160 + }
     161 + }
     162 + }
     163 + }
     164 + }
     165 + }
     166 + },
     167 + "/api/rule": {
     168 + "get": {
     169 + "tags": ["rule"],
     170 + "description": "获取规则列表",
     171 + "operationId": "rule",
     172 + "parameters": [
     173 + {
     174 + "name": "current",
     175 + "in": "query",
     176 + "description": "当前的页码",
     177 + "schema": {
     178 + "type": "number"
     179 + }
     180 + },
     181 + {
     182 + "name": "pageSize",
     183 + "in": "query",
     184 + "description": "页面的容量",
     185 + "schema": {
     186 + "type": "number"
     187 + }
     188 + }
     189 + ],
     190 + "responses": {
     191 + "200": {
     192 + "description": "Success",
     193 + "content": {
     194 + "application/json": {
     195 + "schema": {
     196 + "$ref": "#/components/schemas/RuleList"
     197 + }
     198 + }
     199 + }
     200 + },
     201 + "401": {
     202 + "description": "Error",
     203 + "content": {
     204 + "application/json": {
     205 + "schema": {
     206 + "$ref": "#/components/schemas/ErrorResponse"
     207 + }
     208 + }
     209 + }
     210 + }
     211 + }
     212 + },
     213 + "post": {
     214 + "tags": ["rule"],
     215 + "description": "新建规则",
     216 + "operationId": "addRule",
     217 + "responses": {
     218 + "200": {
     219 + "description": "Success",
     220 + "content": {
     221 + "application/json": {
     222 + "schema": {
     223 + "$ref": "#/components/schemas/RuleListItem"
     224 + }
     225 + }
     226 + }
     227 + },
     228 + "401": {
     229 + "description": "Error",
     230 + "content": {
     231 + "application/json": {
     232 + "schema": {
     233 + "$ref": "#/components/schemas/ErrorResponse"
     234 + }
     235 + }
     236 + }
     237 + }
     238 + }
     239 + },
     240 + "put": {
     241 + "tags": ["rule"],
     242 + "description": "新建规则",
     243 + "operationId": "updateRule",
     244 + "responses": {
     245 + "200": {
     246 + "description": "Success",
     247 + "content": {
     248 + "application/json": {
     249 + "schema": {
     250 + "$ref": "#/components/schemas/RuleListItem"
     251 + }
     252 + }
     253 + }
     254 + },
     255 + "401": {
     256 + "description": "Error",
     257 + "content": {
     258 + "application/json": {
     259 + "schema": {
     260 + "$ref": "#/components/schemas/ErrorResponse"
     261 + }
     262 + }
     263 + }
     264 + }
     265 + }
     266 + },
     267 + "delete": {
     268 + "tags": ["rule"],
     269 + "description": "删除规则",
     270 + "operationId": "removeRule",
     271 + "responses": {
     272 + "200": {
     273 + "description": "Success",
     274 + "content": {
     275 + "application/json": {
     276 + "schema": {
     277 + "type": "object"
     278 + }
     279 + }
     280 + }
     281 + },
     282 + "401": {
     283 + "description": "Error",
     284 + "content": {
     285 + "application/json": {
     286 + "schema": {
     287 + "$ref": "#/components/schemas/ErrorResponse"
     288 + }
     289 + }
     290 + }
     291 + }
     292 + }
     293 + },
     294 + "x-swagger-router-controller": "api"
     295 + },
     296 + "/swagger": {
     297 + "x-swagger-pipe": "swagger_raw"
     298 + }
     299 + },
     300 + "components": {
     301 + "schemas": {
     302 + "CurrentUser": {
     303 + "type": "object",
     304 + "properties": {
     305 + "name": {
     306 + "type": "string"
     307 + },
     308 + "avatar": {
     309 + "type": "string"
     310 + },
     311 + "userid": {
     312 + "type": "string"
     313 + },
     314 + "email": {
     315 + "type": "string"
     316 + },
     317 + "signature": {
     318 + "type": "string"
     319 + },
     320 + "title": {
     321 + "type": "string"
     322 + },
     323 + "group": {
     324 + "type": "string"
     325 + },
     326 + "tags": {
     327 + "type": "array",
     328 + "items": {
     329 + "type": "object",
     330 + "properties": {
     331 + "key": {
     332 + "type": "string"
     333 + },
     334 + "label": {
     335 + "type": "string"
     336 + }
     337 + }
     338 + }
     339 + },
     340 + "notifyCount": {
     341 + "type": "integer",
     342 + "format": "int32"
     343 + },
     344 + "unreadCount": {
     345 + "type": "integer",
     346 + "format": "int32"
     347 + },
     348 + "country": {
     349 + "type": "string"
     350 + },
     351 + "access": {
     352 + "type": "string"
     353 + },
     354 + "geographic": {
     355 + "type": "object",
     356 + "properties": {
     357 + "province": {
     358 + "type": "object",
     359 + "properties": {
     360 + "label": {
     361 + "type": "string"
     362 + },
     363 + "key": {
     364 + "type": "string"
     365 + }
     366 + }
     367 + },
     368 + "city": {
     369 + "type": "object",
     370 + "properties": {
     371 + "label": {
     372 + "type": "string"
     373 + },
     374 + "key": {
     375 + "type": "string"
     376 + }
     377 + }
     378 + }
     379 + }
     380 + },
     381 + "address": {
     382 + "type": "string"
     383 + },
     384 + "phone": {
     385 + "type": "string"
     386 + }
     387 + }
     388 + },
     389 + "LoginResult": {
     390 + "type": "object",
     391 + "properties": {
     392 + "status": {
     393 + "type": "string"
     394 + },
     395 + "type": {
     396 + "type": "string"
     397 + },
     398 + "currentAuthority": {
     399 + "type": "string"
     400 + }
     401 + }
     402 + },
     403 + "PageParams": {
     404 + "type": "object",
     405 + "properties": {
     406 + "current": {
     407 + "type": "number"
     408 + },
     409 + "pageSize": {
     410 + "type": "number"
     411 + }
     412 + }
     413 + },
     414 + "RuleListItem": {
     415 + "type": "object",
     416 + "properties": {
     417 + "key": {
     418 + "type": "integer",
     419 + "format": "int32"
     420 + },
     421 + "disabled": {
     422 + "type": "boolean"
     423 + },
     424 + "href": {
     425 + "type": "string"
     426 + },
     427 + "avatar": {
     428 + "type": "string"
     429 + },
     430 + "name": {
     431 + "type": "string"
     432 + },
     433 + "owner": {
     434 + "type": "string"
     435 + },
     436 + "desc": {
     437 + "type": "string"
     438 + },
     439 + "callNo": {
     440 + "type": "integer",
     441 + "format": "int32"
     442 + },
     443 + "status": {
     444 + "type": "integer",
     445 + "format": "int32"
     446 + },
     447 + "updatedAt": {
     448 + "type": "string",
     449 + "format": "datetime"
     450 + },
     451 + "createdAt": {
     452 + "type": "string",
     453 + "format": "datetime"
     454 + },
     455 + "progress": {
     456 + "type": "integer",
     457 + "format": "int32"
     458 + }
     459 + }
     460 + },
     461 + "RuleList": {
     462 + "type": "object",
     463 + "properties": {
     464 + "data": {
     465 + "type": "array",
     466 + "items": {
     467 + "$ref": "#/components/schemas/RuleListItem"
     468 + }
     469 + },
     470 + "total": {
     471 + "type": "integer",
     472 + "description": "列表的内容总数",
     473 + "format": "int32"
     474 + },
     475 + "success": {
     476 + "type": "boolean"
     477 + }
     478 + }
     479 + },
     480 + "FakeCaptcha": {
     481 + "type": "object",
     482 + "properties": {
     483 + "code": {
     484 + "type": "integer",
     485 + "format": "int32"
     486 + },
     487 + "status": {
     488 + "type": "string"
     489 + }
     490 + }
     491 + },
     492 + "LoginParams": {
     493 + "type": "object",
     494 + "properties": {
     495 + "username": {
     496 + "type": "string"
     497 + },
     498 + "password": {
     499 + "type": "string"
     500 + },
     501 + "autoLogin": {
     502 + "type": "boolean"
     503 + },
     504 + "type": {
     505 + "type": "string"
     506 + }
     507 + }
     508 + },
     509 + "ErrorResponse": {
     510 + "required": ["errorCode"],
     511 + "type": "object",
     512 + "properties": {
     513 + "errorCode": {
     514 + "type": "string",
     515 + "description": "业务约定的错误码"
     516 + },
     517 + "errorMessage": {
     518 + "type": "string",
     519 + "description": "业务上的错误信息"
     520 + },
     521 + "success": {
     522 + "type": "boolean",
     523 + "description": "业务上的请求是否成功"
     524 + }
     525 + }
     526 + },
     527 + "NoticeIconList": {
     528 + "type": "object",
     529 + "properties": {
     530 + "data": {
     531 + "type": "array",
     532 + "items": {
     533 + "$ref": "#/components/schemas/NoticeIconItem"
     534 + }
     535 + },
     536 + "total": {
     537 + "type": "integer",
     538 + "description": "列表的内容总数",
     539 + "format": "int32"
     540 + },
     541 + "success": {
     542 + "type": "boolean"
     543 + }
     544 + }
     545 + },
     546 + "NoticeIconItemType": {
     547 + "title": "NoticeIconItemType",
     548 + "description": "已读未读列表的枚举",
     549 + "type": "string",
     550 + "properties": {},
     551 + "enum": ["notification", "message", "event"]
     552 + },
     553 + "NoticeIconItem": {
     554 + "type": "object",
     555 + "properties": {
     556 + "id": {
     557 + "type": "string"
     558 + },
     559 + "extra": {
     560 + "type": "string",
     561 + "format": "any"
     562 + },
     563 + "key": { "type": "string" },
     564 + "read": {
     565 + "type": "boolean"
     566 + },
     567 + "avatar": {
     568 + "type": "string"
     569 + },
     570 + "title": {
     571 + "type": "string"
     572 + },
     573 + "status": {
     574 + "type": "string"
     575 + },
     576 + "datetime": {
     577 + "type": "string",
     578 + "format": "date"
     579 + },
     580 + "description": {
     581 + "type": "string"
     582 + },
     583 + "type": {
     584 + "extensions": {
     585 + "x-is-enum": true
     586 + },
     587 + "$ref": "#/components/schemas/NoticeIconItemType"
     588 + }
     589 + }
     590 + }
     591 + }
     592 + }
     593 +}
     594 + 
  • ■ ■ ■ ■ ■ ■
    web/config/proxy.ts
     1 +/**
     2 + * 在生产环境 代理是无法生效的,所以这里没有生产环境的配置
     3 + * -------------------------------
     4 + * The agent cannot take effect in the production environment
     5 + * so there is no configuration of the production environment
     6 + * For details, please see
     7 + * https://pro.ant.design/docs/deploy
     8 + */
     9 +export default {
     10 + dev: {
     11 + // localhost:8000/api/** -> https://preview.pro.ant.design/api/**
     12 + '/api/': {
     13 + // 要代理的地址
     14 + target: 'http://localhost:7778/',
     15 + // 配置了这个可以从 http 代理到 https
     16 + // 依赖 origin 的功能可能需要这个,比如 cookie
     17 + changeOrigin: true,
     18 + },
     19 + },
     20 + test: {
     21 + '/api/': {
     22 + target: 'https://proapi.azurewebsites.net',
     23 + changeOrigin: true,
     24 + pathRewrite: { '^': '' },
     25 + },
     26 + },
     27 + pre: {
     28 + '/api/': {
     29 + target: 'your pre url',
     30 + changeOrigin: true,
     31 + pathRewrite: { '^': '' },
     32 + },
     33 + },
     34 +};
     35 + 
  • ■ ■ ■ ■ ■ ■
    web/config/routes.ts
     1 +export default [
     2 + {
     3 + path: '/user',
     4 + layout: false,
     5 + routes: [
     6 + {
     7 + name: 'login',
     8 + path: '/user/login',
     9 + component: './user/',
     10 + },
     11 + {
     12 + component: './404',
     13 + },
     14 + ],
     15 + },
     16 + // {
     17 + // path: '/dashboard',
     18 + // name: 'dashboard',
     19 + // icon: 'dashboard',
     20 + // component: './dashboard/',
     21 + // },
     22 + {
     23 + path: '/service',
     24 + name: 'service',
     25 + icon: 'Thunderbolt',
     26 + component: './service/',
     27 + },
     28 + {
     29 + path: '/function',
     30 + name: 'function',
     31 + icon: 'cluster',
     32 + component: './function/',
     33 + },
     34 + {
     35 + path: 'provider',
     36 + name: 'cloud', // 云账户相关配置
     37 + icon: 'cloud',
     38 + component: './provider/',
     39 + },
     40 + {
     41 + path: '/setting',
     42 + name: 'setting',
     43 + icon: 'setting',
     44 + component: './setting/',
     45 + },
     46 + {
     47 + path: '/',
     48 + redirect: '/service',
     49 + // redirect: '/dashboard',
     50 + },
     51 + {
     52 + component: './404',
     53 + },
     54 +];
     55 + 
  • ■ ■ ■ ■ ■ ■
    web/jest.config.js
     1 +module.exports = {
     2 + testURL: 'http://localhost:8000',
     3 + verbose: false,
     4 + extraSetupFiles: ['./tests/setupTests.js'],
     5 + globals: {
     6 + ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false,
     7 + localStorage: null,
     8 + },
     9 +};
     10 + 
  • ■ ■ ■ ■ ■ ■
    web/jsconfig.json
     1 +{
     2 + "compilerOptions": {
     3 + "jsx": "react-jsx",
     4 + "emitDecoratorMetadata": true,
     5 + "experimentalDecorators": true,
     6 + "baseUrl": ".",
     7 + "paths": {
     8 + "@/*": ["./src/*"]
     9 + }
     10 + }
     11 +}
     12 + 
  • ■ ■ ■ ■ ■ ■
    web/mock/listTableList.ts
     1 +import { Request, Response } from 'express';
     2 +import moment from 'moment';
     3 +import { parse } from 'url';
     4 + 
     5 +// mock tableListDataSource
     6 +const genList = (current: number, pageSize: number) => {
     7 + const tableListDataSource: API.RuleListItem[] = [];
     8 + 
     9 + for (let i = 0; i < pageSize; i += 1) {
     10 + const index = (current - 1) * 10 + i;
     11 + tableListDataSource.push({
     12 + key: index,
     13 + disabled: i % 6 === 0,
     14 + href: 'https://ant.design',
     15 + avatar: [
     16 + 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
     17 + 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
     18 + ][i % 2],
     19 + name: `TradeCode ${index}`,
     20 + owner: '曲丽丽',
     21 + desc: '这是一段描述',
     22 + callNo: Math.floor(Math.random() * 1000),
     23 + status: Math.floor(Math.random() * 10) % 4,
     24 + updatedAt: moment().format('YYYY-MM-DD'),
     25 + createdAt: moment().format('YYYY-MM-DD'),
     26 + progress: Math.ceil(Math.random() * 100),
     27 + });
     28 + }
     29 + tableListDataSource.reverse();
     30 + return tableListDataSource;
     31 +};
     32 + 
     33 +let tableListDataSource = genList(1, 100);
     34 + 
     35 +function getRule(req: Request, res: Response, u: string) {
     36 + let realUrl = u;
     37 + if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
     38 + realUrl = req.url;
     39 + }
     40 + const { current = 1, pageSize = 10 } = req.query;
     41 + const params = parse(realUrl, true).query as unknown as API.PageParams &
     42 + API.RuleListItem & {
     43 + sorter: any;
     44 + filter: any;
     45 + };
     46 + 
     47 + let dataSource = [...tableListDataSource].slice(
     48 + ((current as number) - 1) * (pageSize as number),
     49 + (current as number) * (pageSize as number),
     50 + );
     51 + if (params.sorter) {
     52 + const sorter = JSON.parse(params.sorter);
     53 + dataSource = dataSource.sort((prev, next) => {
     54 + let sortNumber = 0;
     55 + Object.keys(sorter).forEach((key) => {
     56 + if (sorter[key] === 'descend') {
     57 + if (prev[key] - next[key] > 0) {
     58 + sortNumber += -1;
     59 + } else {
     60 + sortNumber += 1;
     61 + }
     62 + return;
     63 + }
     64 + if (prev[key] - next[key] > 0) {
     65 + sortNumber += 1;
     66 + } else {
     67 + sortNumber += -1;
     68 + }
     69 + });
     70 + return sortNumber;
     71 + });
     72 + }
     73 + if (params.filter) {
     74 + const filter = JSON.parse(params.filter as any) as {
     75 + [key: string]: string[];
     76 + };
     77 + if (Object.keys(filter).length > 0) {
     78 + dataSource = dataSource.filter((item) => {
     79 + return Object.keys(filter).some((key) => {
     80 + if (!filter[key]) {
     81 + return true;
     82 + }
     83 + if (filter[key].includes(`${item[key]}`)) {
     84 + return true;
     85 + }
     86 + return false;
     87 + });
     88 + });
     89 + }
     90 + }
     91 + 
     92 + if (params.name) {
     93 + dataSource = dataSource.filter((data) => data?.name?.includes(params.name || ''));
     94 + }
     95 + const result = {
     96 + data: dataSource,
     97 + total: tableListDataSource.length,
     98 + success: true,
     99 + pageSize,
     100 + current: parseInt(`${params.current}`, 10) || 1,
     101 + };
     102 + 
     103 + return res.json(result);
     104 +}
     105 + 
     106 +function postRule(req: Request, res: Response, u: string, b: Request) {
     107 + let realUrl = u;
     108 + if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
     109 + realUrl = req.url;
     110 + }
     111 + 
     112 + const body = (b && b.body) || req.body;
     113 + const { method, name, desc, key } = body;
     114 + 
     115 + switch (method) {
     116 + /* eslint no-case-declarations:0 */
     117 + case 'delete':
     118 + tableListDataSource = tableListDataSource.filter((item) => key.indexOf(item.key) === -1);
     119 + break;
     120 + case 'post':
     121 + (() => {
     122 + const i = Math.ceil(Math.random() * 10000);
     123 + const newRule: API.RuleListItem = {
     124 + key: tableListDataSource.length,
     125 + href: 'https://ant.design',
     126 + avatar: [
     127 + 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
     128 + 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
     129 + ][i % 2],
     130 + name,
     131 + owner: '曲丽丽',
     132 + desc,
     133 + callNo: Math.floor(Math.random() * 1000),
     134 + status: Math.floor(Math.random() * 10) % 2,
     135 + updatedAt: moment().format('YYYY-MM-DD'),
     136 + createdAt: moment().format('YYYY-MM-DD'),
     137 + progress: Math.ceil(Math.random() * 100),
     138 + };
     139 + tableListDataSource.unshift(newRule);
     140 + return res.json(newRule);
     141 + })();
     142 + return;
     143 + 
     144 + case 'update':
     145 + (() => {
     146 + let newRule = {};
     147 + tableListDataSource = tableListDataSource.map((item) => {
     148 + if (item.key === key) {
     149 + newRule = { ...item, desc, name };
     150 + return { ...item, desc, name };
     151 + }
     152 + return item;
     153 + });
     154 + return res.json(newRule);
     155 + })();
     156 + return;
     157 + default:
     158 + break;
     159 + }
     160 + 
     161 + const result = {
     162 + list: tableListDataSource,
     163 + pagination: {
     164 + total: tableListDataSource.length,
     165 + },
     166 + };
     167 + 
     168 + res.json(result);
     169 +}
     170 + 
     171 +export default {
     172 + 'GET /api/rule': getRule,
     173 + 'POST /api/rule': postRule,
     174 +};
     175 + 
  • ■ ■ ■ ■ ■ ■
    web/mock/notices.ts
     1 +import { Request, Response } from 'express';
     2 + 
     3 +const getNotices = (req: Request, res: Response) => {
     4 + res.json({
     5 + data: [
     6 + {
     7 + id: '000000001',
     8 + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
     9 + title: '你收到了 14 份新周报',
     10 + datetime: '2017-08-09',
     11 + type: 'notification',
     12 + },
     13 + {
     14 + id: '000000002',
     15 + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
     16 + title: '你推荐的 曲妮妮 已通过第三轮面试',
     17 + datetime: '2017-08-08',
     18 + type: 'notification',
     19 + },
     20 + {
     21 + id: '000000003',
     22 + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
     23 + title: '这种模板可以区分多种通知类型',
     24 + datetime: '2017-08-07',
     25 + read: true,
     26 + type: 'notification',
     27 + },
     28 + {
     29 + id: '000000004',
     30 + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
     31 + title: '左侧图标用于区分不同的类型',
     32 + datetime: '2017-08-07',
     33 + type: 'notification',
     34 + },
     35 + {
     36 + id: '000000005',
     37 + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
     38 + title: '内容不要超过两行字,超出时自动截断',
     39 + datetime: '2017-08-07',
     40 + type: 'notification',
     41 + },
     42 + {
     43 + id: '000000006',
     44 + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
     45 + title: '曲丽丽 评论了你',
     46 + description: '描述信息描述信息描述信息',
     47 + datetime: '2017-08-07',
     48 + type: 'message',
     49 + clickClose: true,
     50 + },
     51 + {
     52 + id: '000000007',
     53 + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
     54 + title: '朱偏右 回复了你',
     55 + description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
     56 + datetime: '2017-08-07',
     57 + type: 'message',
     58 + clickClose: true,
     59 + },
     60 + {
     61 + id: '000000008',
     62 + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
     63 + title: '标题',
     64 + description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
     65 + datetime: '2017-08-07',
     66 + type: 'message',
     67 + clickClose: true,
     68 + },
     69 + {
     70 + id: '000000009',
     71 + title: '任务名称',
     72 + description: '任务需要在 2017-01-12 20:00 前启动',
     73 + extra: '未开始',
     74 + status: 'todo',
     75 + type: 'event',
     76 + },
     77 + {
     78 + id: '000000010',
     79 + title: '第三方紧急代码变更',
     80 + description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
     81 + extra: '马上到期',
     82 + status: 'urgent',
     83 + type: 'event',
     84 + },
     85 + {
     86 + id: '000000011',
     87 + title: '信息安全考试',
     88 + description: '指派竹尔于 2017-01-09 前完成更新并发布',
     89 + extra: '已耗时 8 天',
     90 + status: 'doing',
     91 + type: 'event',
     92 + },
     93 + {
     94 + id: '000000012',
     95 + title: 'ABCD 版本发布',
     96 + description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
     97 + extra: '进行中',
     98 + status: 'processing',
     99 + type: 'event',
     100 + },
     101 + ],
     102 + });
     103 +};
     104 + 
     105 +export default {
     106 + 'GET /api/notices': getNotices,
     107 +};
     108 + 
  • ■ ■ ■ ■ ■ ■
    web/mock/route.ts
     1 +export default {
     2 + '/api/auth_routes': {
     3 + '/form/advanced-form': { authority: ['admin', 'user'] },
     4 + },
     5 +};
     6 + 
  • ■ ■ ■ ■ ■ ■
    web/mock/user.ts
     1 +import { Request, Response } from 'express';
     2 + 
     3 +const waitTime = (time: number = 100) => {
     4 + return new Promise((resolve) => {
     5 + setTimeout(() => {
     6 + resolve(true);
     7 + }, time);
     8 + });
     9 +};
     10 + 
     11 +async function getFakeCaptcha(req: Request, res: Response) {
     12 + await waitTime(2000);
     13 + return res.json('captcha-xxx');
     14 +}
     15 + 
     16 +const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION } = process.env;
     17 + 
     18 +/**
     19 + * 当前用户的权限,如果为空代表没登录
     20 + * current user access, if is '', user need login
     21 + * 如果是 pro 的预览,默认是有权限的
     22 + */
     23 +let access = ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site' ? 'admin' : '';
     24 + 
     25 +const getAccess = () => {
     26 + return access;
     27 +};
     28 + 
     29 +// 代码中会兼容本地 service mock 以及部署站点的静态数据
     30 +export default {
     31 + // 支持值为 Object 和 Array
     32 + 'GET /api/currentUser': (req: Request, res: Response) => {
     33 + if (!getAccess()) {
     34 + res.status(401).send({
     35 + data: {
     36 + isLogin: false,
     37 + },
     38 + errorCode: '401',
     39 + errorMessage: '请先登录!',
     40 + success: true,
     41 + });
     42 + return;
     43 + }
     44 + res.send({
     45 + success: true,
     46 + data: {
     47 + name: 'Serati Ma',
     48 + avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
     49 + userid: '00000001',
     50 + email: '[email protected]',
     51 + signature: '海纳百川,有容乃大',
     52 + title: '交互专家',
     53 + group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
     54 + tags: [
     55 + {
     56 + key: '0',
     57 + label: '很有想法的',
     58 + },
     59 + {
     60 + key: '1',
     61 + label: '专注设计',
     62 + },
     63 + {
     64 + key: '2',
     65 + label: '辣~',
     66 + },
     67 + {
     68 + key: '3',
     69 + label: '大长腿',
     70 + },
     71 + {
     72 + key: '4',
     73 + label: '川妹子',
     74 + },
     75 + {
     76 + key: '5',
     77 + label: '海纳百川',
     78 + },
     79 + ],
     80 + notifyCount: 12,
     81 + unreadCount: 11,
     82 + country: 'China',
     83 + access: getAccess(),
     84 + geographic: {
     85 + province: {
     86 + label: '浙江省',
     87 + key: '330000',
     88 + },
     89 + city: {
     90 + label: '杭州市',
     91 + key: '330100',
     92 + },
     93 + },
     94 + address: '西湖区工专路 77 号',
     95 + phone: '0752-268888888',
     96 + },
     97 + });
     98 + },
     99 + // GET POST 可省略
     100 + 'GET /api/users': [
     101 + {
     102 + key: '1',
     103 + name: 'John Brown',
     104 + age: 32,
     105 + address: 'New York No. 1 Lake Park',
     106 + },
     107 + {
     108 + key: '2',
     109 + name: 'Jim Green',
     110 + age: 42,
     111 + address: 'London No. 1 Lake Park',
     112 + },
     113 + {
     114 + key: '3',
     115 + name: 'Joe Black',
     116 + age: 32,
     117 + address: 'Sidney No. 1 Lake Park',
     118 + },
     119 + ],
     120 + 'POST /api/login/account': async (req: Request, res: Response) => {
     121 + const { password, username, type } = req.body;
     122 + await waitTime(2000);
     123 + if (password === 'ant.design' && username === 'admin') {
     124 + res.send({
     125 + status: 'ok',
     126 + type,
     127 + currentAuthority: 'admin',
     128 + });
     129 + access = 'admin';
     130 + return;
     131 + }
     132 + if (password === 'ant.design' && username === 'user') {
     133 + res.send({
     134 + status: 'ok',
     135 + type,
     136 + currentAuthority: 'user',
     137 + });
     138 + access = 'user';
     139 + return;
     140 + }
     141 + if (type === 'mobile') {
     142 + res.send({
     143 + status: 'ok',
     144 + type,
     145 + currentAuthority: 'admin',
     146 + });
     147 + access = 'admin';
     148 + return;
     149 + }
     150 + 
     151 + res.send({
     152 + status: 'error',
     153 + type,
     154 + currentAuthority: 'guest',
     155 + });
     156 + access = 'guest';
     157 + },
     158 + 'POST /api/login/outLogin': (req: Request, res: Response) => {
     159 + access = '';
     160 + res.send({ data: {}, success: true });
     161 + },
     162 + 'POST /api/register': (req: Request, res: Response) => {
     163 + res.send({ status: 'ok', currentAuthority: 'user', success: true });
     164 + },
     165 + 'GET /api/500': (req: Request, res: Response) => {
     166 + res.status(500).send({
     167 + timestamp: 1513932555104,
     168 + status: 500,
     169 + error: 'error',
     170 + message: 'error',
     171 + path: '/base/category/list',
     172 + });
     173 + },
     174 + 'GET /api/404': (req: Request, res: Response) => {
     175 + res.status(404).send({
     176 + timestamp: 1513932643431,
     177 + status: 404,
     178 + error: 'Not Found',
     179 + message: 'No message available',
     180 + path: '/base/category/list/2121212',
     181 + });
     182 + },
     183 + 'GET /api/403': (req: Request, res: Response) => {
     184 + res.status(403).send({
     185 + timestamp: 1513932555104,
     186 + status: 403,
     187 + error: 'Forbidden',
     188 + message: 'Forbidden',
     189 + path: '/base/category/list',
     190 + });
     191 + },
     192 + 'GET /api/401': (req: Request, res: Response) => {
     193 + res.status(401).send({
     194 + timestamp: 1513932555104,
     195 + status: 401,
     196 + error: 'Unauthorized',
     197 + message: 'Unauthorized',
     198 + path: '/base/category/list',
     199 + });
     200 + },
     201 + 
     202 + 'GET /api/login/captcha': getFakeCaptcha,
     203 +};
     204 + 
  • ■ ■ ■ ■ ■ ■
    web/package.json
     1 +{
     2 + "name": "ant-design-pro",
     3 + "version": "5.2.0",
     4 + "private": true,
     5 + "description": "An out-of-box UI solution for enterprise applications",
     6 + "scripts": {
     7 + "analyze": "cross-env ANALYZE=1 umi build",
     8 + "build": "NODE_OPTIONS=--openssl-legacy-provider umi build",
     9 + "deploy": "npm run build && npm run gh-pages",
     10 + "dev": "npm run start:dev",
     11 + "gh-pages": "gh-pages -d dist",
     12 + "i18n-remove": "pro i18n-remove --locale=zh-CN --write",
     13 + "postinstall": "umi g tmp",
     14 + "lint": "umi g tmp && npm run lint:js && npm run lint:style && npm run lint:prettier && npm run tsc",
     15 + "lint-staged": "lint-staged",
     16 + "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
     17 + "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src && npm run lint:style",
     18 + "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
     19 + "lint:prettier": "prettier -c --write \"src/**/*\" --end-of-line auto",
     20 + "lint:style": "stylelint --fix \"src/**/*.less\" --syntax less",
     21 + "openapi": "umi openapi",
     22 + "playwright": "playwright install && playwright test",
     23 + "prepare": "husky install",
     24 + "prettier": "prettier -c --write \"src/**/*\"",
     25 + "serve": "umi-serve",
     26 + "start": "NODE_OPTIONS=--openssl-legacy-provider cross-env UMI_ENV=dev umi dev",
     27 + "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev umi dev",
     28 + "start:no-mock": "cross-env MOCK=none UMI_ENV=dev umi dev",
     29 + "start:no-ui": "cross-env UMI_UI=none UMI_ENV=dev umi dev",
     30 + "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev umi dev",
     31 + "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev umi dev",
     32 + "test": "umi test",
     33 + "test:component": "umi test ./src/components",
     34 + "test:e2e": "node ./tests/run-tests.js",
     35 + "tsc": "tsc --noEmit"
     36 + },
     37 + "lint-staged": {
     38 + "**/*.less": "stylelint --syntax less",
     39 + "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
     40 + "**/*.{js,jsx,tsx,ts,less,md,json}": [
     41 + "prettier --write"
     42 + ]
     43 + },
     44 + "browserslist": [
     45 + "> 1%",
     46 + "last 2 versions",
     47 + "not ie <= 10"
     48 + ],
     49 + "dependencies": {
     50 + "@ant-design/icons": "^4.7.0",
     51 + "@ant-design/pro-components": "1.1.5",
     52 + "@umijs/route-utils": "^2.0.0",
     53 + "antd": "^4.20.0",
     54 + "classnames": "^2.3.0",
     55 + "lodash": "^4.17.0",
     56 + "moment": "^2.29.0",
     57 + "omit.js": "^2.0.2",
     58 + "rc-menu": "^9.1.0",
     59 + "rc-util": "^5.16.0",
     60 + "react": "^17.0.0",
     61 + "react-copy-to-clipboard": "^5.1.0",
     62 + "react-dev-inspector": "^1.7.0",
     63 + "react-dom": "^17.0.0",
     64 + "react-helmet-async": "^1.2.0",
     65 + "umi": "^3.5.0"
     66 + },
     67 + "devDependencies": {
     68 + "@ant-design/pro-cli": "^2.1.0",
     69 + "@playwright/test": "^1.17.0",
     70 + "@types/classnames": "^2.3.1",
     71 + "@types/express": "^4.17.0",
     72 + "@types/history": "^4.7.0",
     73 + "@types/jest": "^26.0.0",
     74 + "@types/lodash": "^4.14.0",
     75 + "@types/react": "^17.0.0",
     76 + "@types/react-dom": "^17.0.0",
     77 + "@types/react-helmet": "^6.1.0",
     78 + "@umijs/fabric": "^2.11.1",
     79 + "@umijs/openapi": "^1.6.0",
     80 + "@umijs/plugin-blocks": "^2.2.0",
     81 + "@umijs/plugin-esbuild": "^1.4.0",
     82 + "@umijs/plugin-openapi": "^1.3.3",
     83 + "@umijs/preset-ant-design-pro": "^1.3.0",
     84 + "@umijs/preset-dumi": "^1.1.0",
     85 + "@umijs/preset-react": "^2.1.0",
     86 + "cross-env": "^7.0.0",
     87 + "cross-port-killer": "^1.3.0",
     88 + "detect-installer": "^1.0.0",
     89 + "eslint": "^7.32.0",
     90 + "gh-pages": "^3.2.0",
     91 + "husky": "^7.0.4",
     92 + "jsdom-global": "^3.0.0",
     93 + "lint-staged": "^10.0.0",
     94 + "mockjs": "^1.1.0",
     95 + "prettier": "^2.5.0",
     96 + "stylelint": "^13.0.0",
     97 + "swagger-ui-dist": "^4.12.0",
     98 + "typescript": "^4.5.0",
     99 + "umi-serve": "^1.9.10"
     100 + },
     101 + "engines": {
     102 + "node": ">=12.0.0"
     103 + }
     104 +}
     105 + 
  • ■ ■ ■ ■ ■ ■
    web/playwright.config.ts
     1 +// playwright.config.ts
     2 +import type { PlaywrightTestConfig } from '@playwright/test';
     3 +import { devices } from '@playwright/test';
     4 + 
     5 +const config: PlaywrightTestConfig = {
     6 + forbidOnly: !!process.env.CI,
     7 + retries: process.env.CI ? 2 : 0,
     8 + use: {
     9 + trace: 'on-first-retry',
     10 + },
     11 + projects: [
     12 + {
     13 + name: 'chromium',
     14 + use: { ...devices['Desktop Chrome'] },
     15 + },
     16 + {
     17 + name: 'firefox',
     18 + use: { ...devices['Desktop Firefox'] },
     19 + },
     20 + ],
     21 +};
     22 +export default config;
     23 + 
  • web/public/favicon.ico
  • web/public/icon.svg
  • web/public/icon_black.svg
  • web/public/icons/icon-128x128.png
  • web/public/icons/icon-192x192.png
  • web/public/icons/icon-512x512.png
  • web/public/logo.png
  • ■ ■ ■ ■ ■ ■
    web/src/access.ts
     1 +/**
     2 + * @see https://umijs.org/zh-CN/plugins/plugin-access
     3 + * */
     4 +export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) {
     5 + const { currentUser } = initialState ?? {};
     6 + return {
     7 + canAdmin: currentUser && currentUser.access === 'admin',
     8 + };
     9 +}
     10 + 
     11 +declare namespace API {
     12 + type CurrentUser = {
     13 + name?: string;
     14 + avatar?: string;
     15 + userid?: string;
     16 + email?: string;
     17 + signature?: string;
     18 + title?: string;
     19 + group?: string;
     20 + tags?: { key?: string; label?: string }[];
     21 + notifyCount?: number;
     22 + unreadCount?: number;
     23 + country?: string;
     24 + access?: string;
     25 + geographic?: {
     26 + province?: { label?: string; key?: string };
     27 + city?: { label?: string; key?: string };
     28 + };
     29 + address?: string;
     30 + phone?: string;
     31 + };
     32 +}
     33 + 
  • ■ ■ ■ ■ ■ ■
    web/src/app.tsx
     1 +import Footer from '@/components/Footer';
     2 +import RightContent from '@/components/RightContent';
     3 +import type {Settings as LayoutSettings} from '@ant-design/pro-components';
     4 +import {PageLoading, SettingDrawer} from '@ant-design/pro-components';
     5 +import type {RunTimeLayoutConfig} from 'umi';
     6 +import {history} from 'umi';
     7 +import {ConfigProvider} from 'antd';
     8 +import defaultSettings from '../config/defaultSettings';
     9 + 
     10 +const loginPath = '/user/login';
     11 + 
     12 +ConfigProvider.config({
     13 + theme: {
     14 + primaryColor: '#76b39d',
     15 + }
     16 +});
     17 + 
     18 +/** 获取用户信息比较慢的时候会展示一个 loading */
     19 +export const initialStateConfig = {
     20 + loading: <PageLoading/>,
     21 +};
     22 + 
     23 +/**
     24 + * @see https://umijs.org/zh-CN/plugins/plugin-initial-state
     25 + * */
     26 +export async function getInitialState(): Promise<{
     27 + settings?: Partial<LayoutSettings>;
     28 + currentUser?: string | null;
     29 + loading?: boolean;
     30 + fetchUserInfo?: () => Promise<string | null>;
     31 +}> {
     32 + const fetchUserInfo = async () => {
     33 + const token = localStorage.getItem("token");
     34 + if (token !== "") {
     35 + return token;
     36 + }
     37 + else {
     38 + history.push(loginPath);
     39 + }
     40 + return "";
     41 + };
     42 + // 如果不是登录页面,执行
     43 + if (history.location.pathname !== loginPath) {
     44 + const currentUser = await fetchUserInfo();
     45 + return {
     46 + fetchUserInfo,
     47 + currentUser,
     48 + settings: defaultSettings,
     49 + };
     50 + }
     51 + return {
     52 + fetchUserInfo,
     53 + settings: defaultSettings,
     54 + };
     55 +}
     56 + 
     57 +// ProLayout 支持的api https://procomponents.ant.design/components/layout
     58 +export const layout: RunTimeLayoutConfig = ({initialState, setInitialState}) => {
     59 + return {
     60 + rightContentRender: () => <RightContent/>,
     61 + disableContentMargin: false,
     62 + waterMarkProps: false,
     63 + // content: initialState?.currentUser?.name,
     64 + // },
     65 + footerRender: () => <Footer/>,
     66 + onPageChange: () => {
     67 + const {location} = history;
     68 + // 如果没有登录,重定向到 login
     69 + if (!initialState?.currentUser && location.pathname !== loginPath) {
     70 + history.push(loginPath);
     71 + }
     72 + },
     73 + links: [],
     74 + menuHeaderRender: undefined,
     75 + // 自定义 403 页面
     76 + // unAccessible: <div>unAccessible</div>,
     77 + // 增加一个 loading 的状态
     78 + childrenRender: (children: any, props: { location: { pathname: string | string[]; }; }) => {
     79 + // if (initialState?.loading) return <PageLoading />;
     80 + return (
     81 + <>
     82 + {children}
     83 + {!props.location?.pathname?.includes('/user/login') && (
     84 + <SettingDrawer
     85 + disableUrlParams
     86 + enableDarkTheme
     87 + settings={initialState?.settings}
     88 + onSettingChange={(settings) => {
     89 + setInitialState((preInitialState) => ({
     90 + ...preInitialState,
     91 + settings,
     92 + }));
     93 + }}
     94 + />
     95 + )}
     96 + </>
     97 + );
     98 + },
     99 + ...initialState?.settings,
     100 + };
     101 +};
     102 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/CodePreview/index.less
     1 +@import (reference) '~antd/es/style/themes/index';
     2 + 
     3 +.pre {
     4 + margin: 12px 0;
     5 + padding: 12px 20px;
     6 + background: @input-bg;
     7 + box-shadow: @card-shadow;
     8 +}
     9 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/CodePreview/index.tsx
     1 +import React from "react";
     2 +import styles from "./index.less";
     3 +import {Typography} from "antd";
     4 + 
     5 +const CodePreview: React.FC = ({children}) => (
     6 + <pre className={styles.pre}>
     7 + <code>
     8 + <Typography.Text copyable>{children}</Typography.Text>
     9 + </code>
     10 + </pre>
     11 +);
     12 + 
     13 +export default CodePreview
     14 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/HeaderDropdown/index.less
     1 +@import (reference) '~antd/es/style/themes/index';
     2 + 
     3 +.container > * {
     4 + background-color: @popover-bg;
     5 + border-radius: 4px;
     6 + box-shadow: @shadow-1-down;
     7 +}
     8 + 
     9 +@media screen and (max-width: @screen-xs) {
     10 + .container {
     11 + width: 100% !important;
     12 + }
     13 + .container > * {
     14 + border-radius: 0 !important;
     15 + }
     16 +}
     17 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/HeaderDropdown/index.tsx
     1 +import { Dropdown } from 'antd';
     2 +import type { DropDownProps } from 'antd/es/dropdown';
     3 +import classNames from 'classnames';
     4 +import React from 'react';
     5 +import styles from './index.less';
     6 + 
     7 +export type HeaderDropdownProps = {
     8 + overlayClassName?: string;
     9 + overlay: React.ReactNode | (() => React.ReactNode) | any;
     10 + placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
     11 +} & Omit<DropDownProps, 'overlay'>;
     12 + 
     13 +const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => (
     14 + <Dropdown overlayClassName={classNames(styles.container, cls)} {...restProps} />
     15 +);
     16 + 
     17 +export default HeaderDropdown;
     18 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/HeaderSearch/index.less
     1 +@import (reference) '~antd/es/style/themes/index';
     2 + 
     3 +.headerSearch {
     4 + display: inline-flex;
     5 + align-items: center;
     6 + .input {
     7 + width: 0;
     8 + min-width: 0;
     9 + overflow: hidden;
     10 + background: transparent;
     11 + border-radius: 0;
     12 + transition: width 0.3s, margin-left 0.3s;
     13 + :global(.ant-select-selection) {
     14 + background: transparent;
     15 + }
     16 + input {
     17 + box-shadow: none !important;
     18 + }
     19 + 
     20 + &.show {
     21 + width: 210px;
     22 + margin-left: 8px;
     23 + }
     24 + }
     25 +}
     26 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/HeaderSearch/index.tsx
     1 +import { SearchOutlined } from '@ant-design/icons';
     2 +import type { InputRef } from 'antd';
     3 +import { AutoComplete, Input } from 'antd';
     4 +import type { AutoCompleteProps } from 'antd/es/auto-complete';
     5 +import classNames from 'classnames';
     6 +import useMergedState from 'rc-util/es/hooks/useMergedState';
     7 +import React, { useRef } from 'react';
     8 +import styles from './index.less';
     9 + 
     10 +export type HeaderSearchProps = {
     11 + onSearch?: (value?: string) => void;
     12 + onChange?: (value?: string) => void;
     13 + onVisibleChange?: (b: boolean) => void;
     14 + className?: string;
     15 + placeholder?: string;
     16 + options: AutoCompleteProps['options'];
     17 + defaultVisible?: boolean;
     18 + visible?: boolean;
     19 + defaultValue?: string;
     20 + value?: string;
     21 +};
     22 + 
     23 +const HeaderSearch: React.FC<HeaderSearchProps> = (props) => {
     24 + const {
     25 + className,
     26 + defaultValue,
     27 + onVisibleChange,
     28 + placeholder,
     29 + visible,
     30 + defaultVisible,
     31 + ...restProps
     32 + } = props;
     33 + 
     34 + const inputRef = useRef<InputRef | null>(null);
     35 + 
     36 + const [value, setValue] = useMergedState<string | undefined>(defaultValue, {
     37 + value: props.value,
     38 + onChange: props.onChange,
     39 + });
     40 + 
     41 + const [searchMode, setSearchMode] = useMergedState(defaultVisible ?? false, {
     42 + value: props.visible,
     43 + onChange: onVisibleChange,
     44 + });
     45 + 
     46 + const inputClass = classNames(styles.input, {
     47 + [styles.show]: searchMode,
     48 + });
     49 + return (
     50 + <div
     51 + className={classNames(className, styles.headerSearch)}
     52 + onClick={() => {
     53 + setSearchMode(true);
     54 + if (searchMode && inputRef.current) {
     55 + inputRef.current.focus();
     56 + }
     57 + }}
     58 + onTransitionEnd={({ propertyName }) => {
     59 + if (propertyName === 'width' && !searchMode) {
     60 + if (onVisibleChange) {
     61 + onVisibleChange(searchMode);
     62 + }
     63 + }
     64 + }}
     65 + >
     66 + <SearchOutlined
     67 + key="Icon"
     68 + style={{
     69 + cursor: 'pointer',
     70 + }}
     71 + />
     72 + <AutoComplete
     73 + key="AutoComplete"
     74 + className={inputClass}
     75 + value={value}
     76 + options={restProps.options}
     77 + onChange={(completeValue) => setValue(completeValue)}
     78 + >
     79 + <Input
     80 + size="small"
     81 + ref={inputRef}
     82 + defaultValue={defaultValue}
     83 + aria-label={placeholder}
     84 + placeholder={placeholder}
     85 + onKeyDown={(e) => {
     86 + if (e.key === 'Enter') {
     87 + if (restProps.onSearch) {
     88 + restProps.onSearch(value);
     89 + }
     90 + }
     91 + }}
     92 + onBlur={() => {
     93 + setSearchMode(false);
     94 + }}
     95 + />
     96 + </AutoComplete>
     97 + </div>
     98 + );
     99 +};
     100 + 
     101 +export default HeaderSearch;
     102 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/IconFont/index.tsx
     1 +import {createFromIconfontCN} from '@ant-design/icons';
     2 + 
     3 +const IconFont = createFromIconfontCN({
     4 + scriptUrl: '//at.alicdn.com/t/c/font_4440295_2omb3a1s336.js',
     5 +});
     6 + 
     7 +export default IconFont
     8 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/NoticeIcon/NoticeIcon.tsx
     1 +import { BellOutlined } from '@ant-design/icons';
     2 +import { Badge, Spin, Tabs } from 'antd';
     3 +import classNames from 'classnames';
     4 +import useMergedState from 'rc-util/es/hooks/useMergedState';
     5 +import React from 'react';
     6 +import HeaderDropdown from '../HeaderDropdown';
     7 +import styles from './index.less';
     8 +import type { NoticeIconTabProps } from './NoticeList';
     9 +import NoticeList from './NoticeList';
     10 + 
     11 +const { TabPane } = Tabs;
     12 + 
     13 +export type NoticeIconProps = {
     14 + count?: number;
     15 + bell?: React.ReactNode;
     16 + className?: string;
     17 + loading?: boolean;
     18 + onClear?: (tabName: string, tabKey: string) => void;
     19 + onItemClick?: (item: API.NoticeIconItem, tabProps: NoticeIconTabProps) => void;
     20 + onViewMore?: (tabProps: NoticeIconTabProps, e: MouseEvent) => void;
     21 + onTabChange?: (tabTile: string) => void;
     22 + style?: React.CSSProperties;
     23 + onPopupVisibleChange?: (visible: boolean) => void;
     24 + popupVisible?: boolean;
     25 + clearText?: string;
     26 + viewMoreText?: string;
     27 + clearClose?: boolean;
     28 + emptyImage?: string;
     29 + children?: React.ReactElement<NoticeIconTabProps>[];
     30 +};
     31 + 
     32 +const NoticeIcon: React.FC<NoticeIconProps> & {
     33 + Tab: typeof NoticeList;
     34 +} = (props) => {
     35 + const getNotificationBox = (): React.ReactNode => {
     36 + const {
     37 + children,
     38 + loading,
     39 + onClear,
     40 + onTabChange,
     41 + onItemClick,
     42 + onViewMore,
     43 + clearText,
     44 + viewMoreText,
     45 + } = props;
     46 + if (!children) {
     47 + return null;
     48 + }
     49 + const panes: React.ReactNode[] = [];
     50 + React.Children.forEach(children, (child: React.ReactElement<NoticeIconTabProps>): void => {
     51 + if (!child) {
     52 + return;
     53 + }
     54 + const { list, title, count, tabKey, showClear, showViewMore } = child.props;
     55 + const len = list && list.length ? list.length : 0;
     56 + const msgCount = count || count === 0 ? count : len;
     57 + const tabTitle: string = msgCount > 0 ? `${title} (${msgCount})` : title;
     58 + panes.push(
     59 + <TabPane tab={tabTitle} key={tabKey}>
     60 + <NoticeList
     61 + clearText={clearText}
     62 + viewMoreText={viewMoreText}
     63 + list={list}
     64 + tabKey={tabKey}
     65 + onClear={(): void => onClear && onClear(title, tabKey)}
     66 + onClick={(item): void => onItemClick && onItemClick(item, child.props)}
     67 + onViewMore={(event): void => onViewMore && onViewMore(child.props, event)}
     68 + showClear={showClear}
     69 + showViewMore={showViewMore}
     70 + title={title}
     71 + />
     72 + </TabPane>,
     73 + );
     74 + });
     75 + return (
     76 + <>
     77 + <Spin spinning={loading} delay={300}>
     78 + <Tabs className={styles.tabs} onChange={onTabChange}>
     79 + {panes}
     80 + </Tabs>
     81 + </Spin>
     82 + </>
     83 + );
     84 + };
     85 + 
     86 + const { className, count, bell } = props;
     87 + 
     88 + const [visible, setVisible] = useMergedState<boolean>(false, {
     89 + value: props.popupVisible,
     90 + onChange: props.onPopupVisibleChange,
     91 + });
     92 + const noticeButtonClass = classNames(className, styles.noticeButton);
     93 + const notificationBox = getNotificationBox();
     94 + const NoticeBellIcon = bell || <BellOutlined className={styles.icon} />;
     95 + const trigger = (
     96 + <span className={classNames(noticeButtonClass, { opened: visible })}>
     97 + <Badge count={count} style={{ boxShadow: 'none' }} className={styles.badge}>
     98 + {NoticeBellIcon}
     99 + </Badge>
     100 + </span>
     101 + );
     102 + if (!notificationBox) {
     103 + return trigger;
     104 + }
     105 + 
     106 + return (
     107 + <HeaderDropdown
     108 + placement="bottomRight"
     109 + overlay={notificationBox}
     110 + overlayClassName={styles.popover}
     111 + trigger={['click']}
     112 + visible={visible}
     113 + onVisibleChange={setVisible}
     114 + >
     115 + {trigger}
     116 + </HeaderDropdown>
     117 + );
     118 +};
     119 + 
     120 +NoticeIcon.defaultProps = {
     121 + emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
     122 +};
     123 + 
     124 +NoticeIcon.Tab = NoticeList;
     125 + 
     126 +export default NoticeIcon;
     127 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/NoticeIcon/NoticeList.less
     1 +@import (reference) '~antd/es/style/themes/index';
     2 + 
     3 +.list {
     4 + max-height: 400px;
     5 + overflow: auto;
     6 + &::-webkit-scrollbar {
     7 + display: none;
     8 + }
     9 + .item {
     10 + padding-right: 24px;
     11 + padding-left: 24px;
     12 + overflow: hidden;
     13 + cursor: pointer;
     14 + transition: all 0.3s;
     15 + 
     16 + .meta {
     17 + width: 100%;
     18 + }
     19 + 
     20 + .avatar {
     21 + margin-top: 4px;
     22 + background: @component-background;
     23 + }
     24 + .iconElement {
     25 + font-size: 32px;
     26 + }
     27 + 
     28 + &.read {
     29 + opacity: 0.4;
     30 + }
     31 + &:last-child {
     32 + border-bottom: 0;
     33 + }
     34 + &:hover {
     35 + background: @primary-1;
     36 + }
     37 + .title {
     38 + margin-bottom: 8px;
     39 + font-weight: normal;
     40 + }
     41 + .description {
     42 + font-size: 12px;
     43 + line-height: @line-height-base;
     44 + }
     45 + .datetime {
     46 + margin-top: 4px;
     47 + font-size: 12px;
     48 + line-height: @line-height-base;
     49 + }
     50 + .extra {
     51 + float: right;
     52 + margin-top: -1.5px;
     53 + margin-right: 0;
     54 + color: @text-color-secondary;
     55 + font-weight: normal;
     56 + }
     57 + }
     58 + .loadMore {
     59 + padding: 8px 0;
     60 + color: @primary-6;
     61 + text-align: center;
     62 + cursor: pointer;
     63 + &.loadedAll {
     64 + color: rgba(0, 0, 0, 0.25);
     65 + cursor: unset;
     66 + }
     67 + }
     68 +}
     69 + 
     70 +.notFound {
     71 + padding: 73px 0 88px;
     72 + color: @text-color-secondary;
     73 + text-align: center;
     74 + img {
     75 + display: inline-block;
     76 + height: 76px;
     77 + margin-bottom: 16px;
     78 + }
     79 +}
     80 + 
     81 +.bottomBar {
     82 + height: 46px;
     83 + color: @text-color;
     84 + line-height: 46px;
     85 + text-align: center;
     86 + border-top: 1px solid @border-color-split;
     87 + border-radius: 0 0 @border-radius-base @border-radius-base;
     88 + transition: all 0.3s;
     89 + div {
     90 + display: inline-block;
     91 + width: 50%;
     92 + cursor: pointer;
     93 + transition: all 0.3s;
     94 + user-select: none;
     95 + 
     96 + &:only-child {
     97 + width: 100%;
     98 + }
     99 + &:not(:only-child):last-child {
     100 + border-left: 1px solid @border-color-split;
     101 + }
     102 + }
     103 +}
     104 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/NoticeIcon/NoticeList.tsx
     1 +import { Avatar, List } from 'antd';
     2 +import classNames from 'classnames';
     3 +import React from 'react';
     4 +import styles from './NoticeList.less';
     5 + 
     6 +export type NoticeIconTabProps = {
     7 + count?: number;
     8 + showClear?: boolean;
     9 + showViewMore?: boolean;
     10 + style?: React.CSSProperties;
     11 + title: string;
     12 + tabKey: API.NoticeIconItemType;
     13 + onClick?: (item: API.NoticeIconItem) => void;
     14 + onClear?: () => void;
     15 + emptyText?: string;
     16 + clearText?: string;
     17 + viewMoreText?: string;
     18 + list: API.NoticeIconItem[];
     19 + onViewMore?: (e: any) => void;
     20 +};
     21 +const NoticeList: React.FC<NoticeIconTabProps> = ({
     22 + list = [],
     23 + onClick,
     24 + onClear,
     25 + title,
     26 + onViewMore,
     27 + emptyText,
     28 + showClear = true,
     29 + clearText,
     30 + viewMoreText,
     31 + showViewMore = false,
     32 +}) => {
     33 + if (!list || list.length === 0) {
     34 + return (
     35 + <div className={styles.notFound}>
     36 + <img
     37 + src="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
     38 + alt="not found"
     39 + />
     40 + <div>{emptyText}</div>
     41 + </div>
     42 + );
     43 + }
     44 + return (
     45 + <div>
     46 + <List<API.NoticeIconItem>
     47 + className={styles.list}
     48 + dataSource={list}
     49 + renderItem={(item, i) => {
     50 + const itemCls = classNames(styles.item, {
     51 + [styles.read]: item.read,
     52 + });
     53 + // eslint-disable-next-line no-nested-ternary
     54 + const leftIcon = item.avatar ? (
     55 + typeof item.avatar === 'string' ? (
     56 + <Avatar className={styles.avatar} src={item.avatar} />
     57 + ) : (
     58 + <span className={styles.iconElement}>{item.avatar}</span>
     59 + )
     60 + ) : null;
     61 + 
     62 + return (
     63 + <div
     64 + onClick={() => {
     65 + onClick?.(item);
     66 + }}
     67 + >
     68 + <List.Item className={itemCls} key={item.key || i}>
     69 + <List.Item.Meta
     70 + className={styles.meta}
     71 + avatar={leftIcon}
     72 + title={
     73 + <div className={styles.title}>
     74 + {item.title}
     75 + <div className={styles.extra}>{item.extra}</div>
     76 + </div>
     77 + }
     78 + description={
     79 + <div>
     80 + <div className={styles.description}>{item.description}</div>
     81 + <div className={styles.datetime}>{item.datetime}</div>
     82 + </div>
     83 + }
     84 + />
     85 + </List.Item>
     86 + </div>
     87 + );
     88 + }}
     89 + />
     90 + <div className={styles.bottomBar}>
     91 + {showClear ? (
     92 + <div onClick={onClear}>
     93 + {clearText} {title}
     94 + </div>
     95 + ) : null}
     96 + {showViewMore ? (
     97 + <div
     98 + onClick={(e) => {
     99 + if (onViewMore) {
     100 + onViewMore(e);
     101 + }
     102 + }}
     103 + >
     104 + {viewMoreText}
     105 + </div>
     106 + ) : null}
     107 + </div>
     108 + </div>
     109 + );
     110 +};
     111 + 
     112 +export default NoticeList;
     113 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/NoticeIcon/index.less
     1 +@import (reference) '~antd/es/style/themes/index';
     2 + 
     3 +.popover {
     4 + position: relative;
     5 + width: 336px;
     6 +}
     7 + 
     8 +.noticeButton {
     9 + display: inline-block;
     10 + cursor: pointer;
     11 + transition: all 0.3s;
     12 +}
     13 +.icon {
     14 + padding: 4px;
     15 + vertical-align: middle;
     16 +}
     17 + 
     18 +.badge {
     19 + font-size: 16px;
     20 +}
     21 + 
     22 +.tabs {
     23 + :global {
     24 + .ant-tabs-nav-list {
     25 + margin: auto;
     26 + }
     27 + 
     28 + .ant-tabs-nav-scroll {
     29 + text-align: center;
     30 + }
     31 + .ant-tabs-nav {
     32 + margin-bottom: 0;
     33 + }
     34 + }
     35 +}
     36 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/NoticeIcon/index.tsx
     1 +import { getNotices } from '@/services/ant-design-pro/api';
     2 +import { message, Tag } from 'antd';
     3 +import { groupBy } from 'lodash';
     4 +import moment from 'moment';
     5 +import { useEffect, useState } from 'react';
     6 +import { useModel, useRequest } from 'umi';
     7 +import styles from './index.less';
     8 +import NoticeIcon from './NoticeIcon';
     9 + 
     10 +export type GlobalHeaderRightProps = {
     11 + fetchingNotices?: boolean;
     12 + onNoticeVisibleChange?: (visible: boolean) => void;
     13 + onNoticeClear?: (tabName?: string) => void;
     14 +};
     15 + 
     16 +const getNoticeData = (notices: API.NoticeIconItem[]): Record<string, API.NoticeIconItem[]> => {
     17 + if (!notices || notices.length === 0 || !Array.isArray(notices)) {
     18 + return {};
     19 + }
     20 + 
     21 + const newNotices = notices.map((notice) => {
     22 + const newNotice = { ...notice };
     23 + 
     24 + if (newNotice.datetime) {
     25 + newNotice.datetime = moment(notice.datetime as string).fromNow();
     26 + }
     27 + 
     28 + if (newNotice.id) {
     29 + newNotice.key = newNotice.id;
     30 + }
     31 + 
     32 + if (newNotice.extra && newNotice.status) {
     33 + const color = {
     34 + todo: '',
     35 + processing: 'blue',
     36 + urgent: 'red',
     37 + doing: 'gold',
     38 + }[newNotice.status];
     39 + newNotice.extra = (
     40 + <Tag
     41 + color={color}
     42 + style={{
     43 + marginRight: 0,
     44 + }}
     45 + >
     46 + {newNotice.extra}
     47 + </Tag>
     48 + ) as any;
     49 + }
     50 + 
     51 + return newNotice;
     52 + });
     53 + return groupBy(newNotices, 'type');
     54 +};
     55 + 
     56 +const getUnreadData = (noticeData: Record<string, API.NoticeIconItem[]>) => {
     57 + const unreadMsg: Record<string, number> = {};
     58 + Object.keys(noticeData).forEach((key) => {
     59 + const value = noticeData[key];
     60 + 
     61 + if (!unreadMsg[key]) {
     62 + unreadMsg[key] = 0;
     63 + }
     64 + 
     65 + if (Array.isArray(value)) {
     66 + unreadMsg[key] = value.filter((item) => !item.read).length;
     67 + }
     68 + });
     69 + return unreadMsg;
     70 +};
     71 + 
     72 +const NoticeIconView: React.FC = () => {
     73 + const { initialState } = useModel('@@initialState');
     74 + const { currentUser } = initialState || {};
     75 + const [notices, setNotices] = useState<API.NoticeIconItem[]>([]);
     76 + const { data } = useRequest(getNotices);
     77 + 
     78 + useEffect(() => {
     79 + setNotices(data || []);
     80 + }, [data]);
     81 + 
     82 + const noticeData = getNoticeData(notices);
     83 + const unreadMsg = getUnreadData(noticeData || {});
     84 + 
     85 + const changeReadState = (id: string) => {
     86 + setNotices(
     87 + notices.map((item) => {
     88 + const notice = { ...item };
     89 + if (notice.id === id) {
     90 + notice.read = true;
     91 + }
     92 + return notice;
     93 + }),
     94 + );
     95 + };
     96 + 
     97 + const clearReadState = (title: string, key: string) => {
     98 + setNotices(
     99 + notices.map((item) => {
     100 + const notice = { ...item };
     101 + if (notice.type === key) {
     102 + notice.read = true;
     103 + }
     104 + return notice;
     105 + }),
     106 + );
     107 + message.success(`${'清空了'} ${title}`);
     108 + };
     109 + 
     110 + return (
     111 + <NoticeIcon
     112 + className={styles.action}
     113 + count={currentUser && currentUser.unreadCount}
     114 + onItemClick={(item) => {
     115 + changeReadState(item.id!);
     116 + }}
     117 + onClear={(title: string, key: string) => clearReadState(title, key)}
     118 + loading={false}
     119 + clearText="清空"
     120 + viewMoreText="查看更多"
     121 + onViewMore={() => message.info('Click on view more')}
     122 + clearClose
     123 + >
     124 + <NoticeIcon.Tab
     125 + tabKey="notification"
     126 + count={unreadMsg.notification}
     127 + list={noticeData.notification}
     128 + title="通知"
     129 + emptyText="你已查看所有通知"
     130 + showViewMore
     131 + />
     132 + <NoticeIcon.Tab
     133 + tabKey="message"
     134 + count={unreadMsg.message}
     135 + list={noticeData.message}
     136 + title="消息"
     137 + emptyText="您已读完所有消息"
     138 + showViewMore
     139 + />
     140 + <NoticeIcon.Tab
     141 + tabKey="event"
     142 + title="待办"
     143 + emptyText="你已完成所有待办"
     144 + count={unreadMsg.event}
     145 + list={noticeData.event}
     146 + showViewMore
     147 + />
     148 + </NoticeIcon>
     149 + );
     150 +};
     151 + 
     152 +export default NoticeIconView;
     153 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/RightContent/index.less
     1 +@import (reference) '~antd/es/style/themes/index';
     2 + 
     3 +@pro-header-hover-bg: rgba(0, 0, 0, 0.025);
     4 + 
     5 +.menu {
     6 + :global(.anticon) {
     7 + margin-right: 8px;
     8 + }
     9 + :global(.ant-dropdown-menu-item) {
     10 + min-width: 160px;
     11 + }
     12 +}
     13 + 
     14 +.right {
     15 + display: flex;
     16 + float: right;
     17 + height: 48px;
     18 + margin-left: auto;
     19 + overflow: hidden;
     20 + .action {
     21 + display: flex;
     22 + align-items: center;
     23 + height: 48px;
     24 + padding: 0 12px;
     25 + cursor: pointer;
     26 + transition: all 0.3s;
     27 + > span {
     28 + vertical-align: middle;
     29 + }
     30 + &:hover {
     31 + background: @pro-header-hover-bg;
     32 + }
     33 + &:global(.opened) {
     34 + background: @pro-header-hover-bg;
     35 + }
     36 + }
     37 + .search {
     38 + padding: 0 12px;
     39 + &:hover {
     40 + background: transparent;
     41 + }
     42 + }
     43 + .account {
     44 + .avatar {
     45 + margin-right: 8px;
     46 + color: @primary-color;
     47 + vertical-align: top;
     48 + background: rgba(255, 255, 255, 0.85);
     49 + }
     50 + }
     51 +}
     52 + 
     53 +.dark {
     54 + .action {
     55 + &:hover {
     56 + background: #252a3d;
     57 + }
     58 + &:global(.opened) {
     59 + background: #252a3d;
     60 + }
     61 + }
     62 +}
     63 + 
     64 +@media only screen and (max-width: @screen-md) {
     65 + :global(.ant-divider-vertical) {
     66 + vertical-align: unset;
     67 + }
     68 + .name {
     69 + display: none;
     70 + }
     71 + .right {
     72 + position: absolute;
     73 + top: 0;
     74 + right: 12px;
     75 + .account {
     76 + .avatar {
     77 + margin-right: 0;
     78 + }
     79 + }
     80 + .search {
     81 + display: none;
     82 + }
     83 + }
     84 +}
     85 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/RightContent/index.tsx
     1 +import {BookOutlined, GithubOutlined} from '@ant-design/icons';
     2 +import { Space } from 'antd';
     3 +import React from 'react';
     4 +import { useModel } from 'umi';
     5 +import styles from './index.less';
     6 + 
     7 +export type SiderTheme = 'light' | 'dark';
     8 + 
     9 +const GlobalHeaderRight: React.FC = () => {
     10 + const { initialState } = useModel('@@initialState');
     11 + 
     12 + if (!initialState || !initialState.settings) {
     13 + return null;
     14 + }
     15 + 
     16 + const { navTheme, layout } = initialState.settings;
     17 + let className = styles.right;
     18 + 
     19 + if ((navTheme === 'dark' && layout === 'top') || layout === 'mix') {
     20 + className = `${styles.right} ${styles.dark}`;
     21 + }
     22 + return (
     23 + <Space className={className}>
     24 + <span
     25 + className={styles.action}
     26 + onClick={() => {
     27 + window.open('https://seamoon.dvkunion.cn');
     28 + }}
     29 + >
     30 + <BookOutlined />
     31 + </span>
     32 + <span
     33 + className={styles.action}
     34 + onClick={() => {
     35 + window.open('https://www.github.com/DVKunion/Seamoon');
     36 + }}>
     37 + <GithubOutlined />
     38 + </span>
     39 + {/*<Avatar />*/}
     40 + </Space>
     41 + );
     42 +};
     43 +export default GlobalHeaderRight;
     44 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/ShieldList/index.tsx
     1 +import React from 'react';
     2 +import {Space} from "antd";
     3 + 
     4 +const ShieldList: React.FC = ({children}) => (
     5 + <Space>
     6 + <img src="https://goreportcard.com/badge/github.com/DVKunion/SeaMoon" alt="go-report"/>
     7 + <img src="https://img.shields.io/github/languages/top/DVKunion/SeaMoon.svg?&color=blueviolet"
     8 + alt="languages"/>
     9 + <img src="https://img.shields.io/badge/LICENSE-MIT-777777.svg" alt="license"/>
     10 + <img src="https://img.shields.io/github/downloads/dvkunion/seamoon/total?color=orange" alt="downloads"/>
     11 + <img src="https://img.shields.io/github/stars/DVKunion/SeaMoon.svg" alt="stars"/>
     12 + </Space>
     13 +)
     14 + 
     15 +export default ShieldList;
     16 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/SpeedTransfer/index.tsx
     1 +export type SpeedTransferProps = {
     2 + bytes: number
     3 + decimals?: number
     4 +}
     5 + 
     6 +export const SpeedTransfer: (props: SpeedTransferProps) => (string | string) = (props: SpeedTransferProps) => {
     7 + if (props.decimals === undefined || props.decimals === 0) {
     8 + props.decimals = 2
     9 + }
     10 + if (props.bytes === 0) return '0 B';
     11 + 
     12 + const k = 1024;
     13 + const dm = props.decimals < 0 ? 0 : props.decimals;
     14 + const sizes = ["B", 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
     15 + 
     16 + const i = Math.floor(Math.log(props.bytes) / Math.log(k));
     17 + 
     18 + return parseFloat((props.bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
     19 +}
     20 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/StepForm/ProviderSelect/index.tsx
     1 +import React, {useState} from "react";
     2 +import {ProFormSelect} from "@ant-design/pro-components";
     3 +import {getActiveProvider} from "@/services/cloud/api";
     4 +import {Space, Tag } from "antd";
     5 +import {CloudProvideTypeIcon, RegionEnum} from "@/enum/cloud";
     6 + 
     7 +export type ProviderProps = {
     8 + onChange: (values: number) => void;
     9 +};
     10 + 
     11 +export const ProviderSelect: React.FC<ProviderProps> = (props: ProviderProps) => {
     12 + const [cloud, setCloud] = useState<Partial<Cloud.Provider>>({});
     13 + 
     14 + return <><ProFormSelect
     15 + name="provider_id"
     16 + label="选择关联云账户"
     17 + width={"xl"}
     18 + tooltip={"仅允许正常状态的账户"}
     19 + showSearch={true}
     20 + request={async () => {
     21 + const res: { key: number; label: JSX.Element; value: number; obj: Cloud.Provider; }[] = [];
     22 + const {data} = await getActiveProvider();
     23 + data.forEach((item) => {
     24 + res.push(
     25 + {
     26 + key: item.id,
     27 + label: <Space>{CloudProvideTypeIcon[item.type]}{item.name}</Space>,
     28 + value: item.id,
     29 + obj: item
     30 + }
     31 + )
     32 + })
     33 + return res
     34 + }}
     35 + rules={[
     36 + {
     37 + required: true,
     38 + message: "请选择关联云账户!",
     39 + },
     40 + ]}
     41 + fieldProps={
     42 + {
     43 + onSelect: (value, option) => {
     44 + setCloud(option["data-item"].obj);
     45 + props.onChange(option["data-item"].obj.type)
     46 + }
     47 + }
     48 + }
     49 + />
     50 + {cloud.id !== undefined && cloud.id !== 0 ?
     51 + <>
     52 + <Space size={120}>
     53 + {cloud.info?.amount !== undefined ? <div><p>账户余额: </p><Tag
     54 + color={cloud.info.amount > 0 ? "volcano" : "green"}>{"¥" + cloud.info?.amount}</Tag></div> : <></>}
     55 + {cloud.count !== undefined && cloud.max_limit !== undefined ?
     56 + <div><p>已部署函数限制: </p><Tag
     57 + color={cloud.max_limit === 0 ? "volcano" : cloud.count <= cloud.max_limit ? "volcano" : "green"}>{cloud.count + " / " + (cloud.max_limit === 0 ? "∞" : cloud.max_limit)}</Tag></div> : <></>
     58 + }
     59 + </Space>
     60 + <p style={{marginTop: "20px"}}>允许部署区域:</p>
     61 + <Space>
     62 + {cloud.regions?.map((region, index) => (
     63 + <Tag key={index}>{RegionEnum[region]}</Tag>
     64 + ))}
     65 + </Space></> : <></>
     66 + }
     67 + </>
     68 +}
     69 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/StepForm/ProxyForm/index.tsx
     1 +import React from "react";
     2 +import {ProForm, ProFormText, ProFormSelect, ProFormSwitch} from "@ant-design/pro-components";
     3 +import {toNumber} from "lodash";
     4 +import {ProxyTypeValueEnum} from "@/enum/service";
     5 + 
     6 +export const ProxyForm: React.FC = (props) => {
     7 + return <> <ProForm.Group
     8 + title={"服务参数"}
     9 + >
     10 + <ProFormText
     11 + name="name"
     12 + label="代理名称"
     13 + placeholder={""}
     14 + colProps={{span: 8}}
     15 + rules={[
     16 + {
     17 + required: true,
     18 + message: "请输入代理服务名称!",
     19 + }
     20 + ]}
     21 + />
     22 + <ProFormText
     23 + name="listen_address"
     24 + label="监听地址"
     25 + placeholder={""}
     26 + colProps={{span: 8,offset:4}}
     27 + rules={[
     28 + {
     29 + required: true,
     30 + message: "请输入合法的监听地址!",
     31 + pattern: RegExp(""),
     32 + },
     33 + {
     34 + pattern: /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
     35 + message: "请输入合理的IP地址",
     36 + }
     37 + ]}
     38 + />
     39 + <ProFormText
     40 + name="listen_port"
     41 + label="监听端口"
     42 + placeholder={""}
     43 + colProps={{span: 8}}
     44 + rules={[
     45 + {
     46 + required: true,
     47 + message: "请输入监听端口!",
     48 + },
     49 + {
     50 + validator: (rule, value) => {
     51 + const v = toNumber(value);
     52 + if (v >= 65535 ||v <= 0 || isNaN(v) ) {
     53 + return Promise.reject(new Error('请输入合法端口号(1-65535)'));
     54 + }
     55 + return Promise.resolve();
     56 + },
     57 + },
     58 + ]}
     59 + />
     60 + <ProFormSelect
     61 + name="type"
     62 + colProps={{span: 8,offset:4}}
     63 + label="监听协议"
     64 + placeholder={""}
     65 + valueEnum={ProxyTypeValueEnum}
     66 + rules={[
     67 + {
     68 + required: true,
     69 + message: "请选择监听的服务类型!",
     70 + },
     71 + ]}
     72 + />
     73 + </ProForm.Group>
     74 + <ProForm.Group
     75 + title={"高级选项"}
     76 + >
     77 + <ProFormSwitch
     78 + name="tor"
     79 + label={"开启 Tor 网桥"}
     80 + tooltip={"选择开启 Tor 网桥, 创建或选择的函数必须也对应开启 Tor 标识"}
     81 + checkedChildren={"开启"}
     82 + unCheckedChildren={"关闭"}
     83 + colProps={{
     84 + span: 12,
     85 + }}
     86 + />
     87 + </ProForm.Group>
     88 + </>
     89 +}
     90 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/StepForm/TunnelForm/index.tsx
     1 +import React from "react";
     2 +import {ProForm, ProFormSwitch, ProFormText, ProFormSelect} from "@ant-design/pro-components";
     3 +import {toNumber} from "lodash";
     4 +import {TunnelAuthFCTypeEnum, TunnelTypeValueEnum} from "@/enum/tunnel";
     5 +import {CloudRegionOneSelector} from "@/pages/provider/components/AuthForm";
     6 + 
     7 +export type TunnelFormProps = {
     8 + type: number
     9 +}
     10 + 
     11 +export const TunnelForm: React.FC<TunnelFormProps> = (props) => {
     12 + return <> <ProForm.Group
     13 + title={"基础信息"}
     14 + >
     15 + <ProFormText
     16 + name="tunnel_name"
     17 + label={"函数名称"}
     18 + colProps={{span: 8}}
     19 + rules={[
     20 + {
     21 + required: true,
     22 + message: "请填写函数名称!",
     23 + },
     24 + {
     25 + pattern: /^[a-zA-Z0-9_]+$/,
     26 + message: "代理服务只能为英文/数字/下划线!",
     27 + },
     28 + {
     29 + max: 24,
     30 + message: "代理名称不要超过60个字符",
     31 + }
     32 + ]
     33 + }
     34 + />
     35 + <CloudRegionOneSelector type={props.type} />
     36 + </ProForm.Group>
     37 + <ProForm.Group
     38 + title={"函数规格"}
     39 + >
     40 + <ProFormText
     41 + name="cpu"
     42 + label={"CPU限制"}
     43 + colProps={{span: 8}}
     44 + tooltip={"必须为 0.05 的倍数"}
     45 + placeholder={""}
     46 + rules={[
     47 + {
     48 + required: true,
     49 + message: "请填写cpu资源限制!",
     50 + },
     51 + {
     52 + validator: (rule, value) => {
     53 + if (toNumber(value) < 0.05) {
     54 + return Promise.reject(new Error('最低 cpu 数值为 0.05'));
     55 + }
     56 + if (toNumber(value) * 100 % 5 !== 0) {
     57 + return Promise.reject(new Error('必须是 0.05 的倍数'));
     58 + }
     59 + return Promise.resolve();
     60 + }
     61 + }
     62 + ]}
     63 + />
     64 + <ProFormText
     65 + name="memory"
     66 + label={"内存限制"}
     67 + colProps={{span: 8, offset: 4}}
     68 + placeholder={""}
     69 + rules={[
     70 + {
     71 + required: true,
     72 + message: "请填写内存资源限制!",
     73 + },
     74 + {
     75 + validator: (rule, value) => {
     76 + if (toNumber(value) < 64) {
     77 + return Promise.reject(new Error('最低内存数值为64'));
     78 + }
     79 + return Promise.resolve();
     80 + }
     81 + }
     82 + ]}
     83 + />
     84 + <ProFormText
     85 + name="instance"
     86 + label={"最大处理数"}
     87 + tooltip={"表示一个实例最大能够并发处理的请求数"}
     88 + colProps={{span: 8}}
     89 + placeholder={""}
     90 + rules={[
     91 + {
     92 + required: true,
     93 + message: "请填写最大实例处理数!",
     94 + },
     95 + {
     96 + validator: (rule, value) => {
     97 + if (toNumber(value) < 1) {
     98 + return Promise.reject(new Error('处理数最低为1'));
     99 + }
     100 + return Promise.resolve();
     101 + }
     102 + }
     103 + ]}
     104 + />
     105 + <ProFormText
     106 + name="port"
     107 + label={"端口号配置"}
     108 + tooltip={"自定义配置实例服务端口号"}
     109 + colProps={{span: 8, offset: 4}}
     110 + width={"md"}
     111 + placeholder={""}
     112 + rules={[
     113 + {
     114 + required: true,
     115 + message: "请填写正确的端口号!",
     116 + },
     117 + {
     118 + validator: (rule, value) => {
     119 + if (toNumber(value) >= 65535 || toNumber(value) <= 0) {
     120 + return Promise.reject(new Error('请输入合法端口'));
     121 + }
     122 + return Promise.resolve();
     123 + },
     124 + }
     125 + ]}
     126 + />
     127 + <ProFormSelect
     128 + name="tunnel_auth_type"
     129 + label={"函数认证方式"}
     130 + tooltip={"云函数自身提供认证方式,配置该项可防止被刷"}
     131 + colProps={{span: 8}}
     132 + placeholder={""}
     133 + valueEnum={TunnelAuthFCTypeEnum}
     134 + showSearch={true}
     135 + rules={[
     136 + {
     137 + required: true,
     138 + message: "请选择函数的认证方式!",
     139 + },
     140 + ]}
     141 + />
     142 + <ProFormSelect
     143 + name="tunnel_type"
     144 + label={"隧道协议类型"}
     145 + colProps={{span: 8, offset: 4}}
     146 + placeholder={""}
     147 + valueEnum={TunnelTypeValueEnum}
     148 + rules={[
     149 + {
     150 + required: true,
     151 + message: "请选择隧道协议类型!",
     152 + },
     153 + ]}
     154 + />
     155 + </ProForm.Group>
     156 + <ProForm.Group title={"高级选项"} grid={true} rowProps={{
     157 + gutter: [16, 16],
     158 + }}>
     159 + <ProFormSwitch
     160 + name="tls"
     161 + label={"开启 TLS"}
     162 + checkedChildren={"开启"}
     163 + unCheckedChildren={"关闭"}
     164 + colProps={{
     165 + span: 12,
     166 + }}
     167 + />
     168 + <ProFormSwitch
     169 + name="tor"
     170 + label={"开启 Tor 网桥"}
     171 + tooltip={"开启 Tor 模式会导致内存资源使用增多"}
     172 + checkedChildren={"开启"}
     173 + unCheckedChildren={"关闭"}
     174 + colProps={{
     175 + span: 12,
     176 + }}
     177 + />
     178 + </ProForm.Group>
     179 + </>
     180 +}
     181 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/StepForm/TunnelSelect/index.tsx
     1 +import React, {useState} from "react";
     2 +import {ProFormSelect} from "@ant-design/pro-components";
     3 +import {Space} from "antd";
     4 +import {getServerlessTunnel} from "@/services/function/api";
     5 +import {TunnelTypeValueEnum} from "@/enum/tunnel";
     6 +import {CloudProvideTypeValueEnum} from "@/enum/cloud";
     7 + 
     8 + 
     9 +export type TunnelProps = {
     10 + tor: boolean,
     11 + values: Partial<Serverless.Tunnel>
     12 +};
     13 + 
     14 +export const TunnelSelect: React.FC<TunnelProps> = (props: TunnelProps) => {
     15 + const [tunnel, setTunnel] = useState<Partial<Serverless.Tunnel>>({});
     16 + 
     17 + return <><ProFormSelect
     18 + dependencies={[props.tor]}
     19 + name="tunnel_id"
     20 + label="选择关联函数实例"
     21 + width={"xl"}
     22 + tooltip={"仅允许正常状态的实例,如果开启了 tor, 则会自动筛选 tor 标签的实例"}
     23 + showSearch={true}
     24 + placeholder={""}
     25 + request={async () => {
     26 + const res: { key: number; label: JSX.Element; value: number; obj: Serverless.Tunnel; }[] = [];
     27 + const {data} = await getServerlessTunnel(0, 999999);
     28 + data.forEach((item) => {
     29 + if(!props.tor || (props.tor && item.tunnel_config.tor)) {
     30 + res.push({
     31 + key: item.id,
     32 + label: <Space>{CloudProvideTypeValueEnum[item.provider_type || 0]} - {TunnelTypeValueEnum[item.type]} - {item.name}</Space>,
     33 + value: item.id,
     34 + obj: item
     35 + });
     36 + }
     37 + })
     38 + return res
     39 + }}
     40 + rules={[
     41 + {
     42 + required: true,
     43 + message: "请选择关联函数实例!",
     44 + },
     45 + ]}
     46 + fieldProps={
     47 + {
     48 + onSelect: (value, option) => {
     49 + setTunnel(option["data-item"].obj);
     50 + }
     51 + }
     52 + }
     53 + />
     54 + {tunnel !== undefined && tunnel.id !== 0 ?
     55 + <Space>
     56 + {/*{cloud.amount !== undefined ? <>账户余额: <Tag*/}
     57 + {/* color={cloud.amount > 0 ? "volcano" : "green"}>{"¥" + cloud.amount}</Tag></> : <></>}*/}
     58 + {/*{cloud.count !== undefined && cloud.max_limit !== undefined ?*/}
     59 + {/* <>已部署函数限制: <Tag*/}
     60 + {/* color={cloud.max_limit === 0 ? "volcano" : cloud.count <= cloud.max_limit ? "volcano" : "green"}>{cloud.count + " / " + (cloud.max_limit === 0 ? "∞" : cloud.max_limit)}</Tag></> : <></>*/}
     61 + {/*}*/}
     62 + </Space> : <></>
     63 + }
     64 + </>
     65 +}
     66 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/StepForm/index.md
     1 +# StepForm
     2 + 
     3 +一些结构通用的表单组件
     4 + 
  • ■ ■ ■ ■ ■ ■
    web/src/components/index.md
     1 +---
     2 +title: 业务组件
     3 +sidemenu: false
     4 +---
     5 + 
     6 +> 此功能由[dumi](https://d.umijs.org/zh-CN/guide/advanced#umi-%E9%A1%B9%E7%9B%AE%E9%9B%86%E6%88%90%E6%A8%A1%E5%BC%8F)提供,dumi 是一个 📖 为组件开发场景而生的文档工具,用过的都说好。
     7 + 
     8 +# 业务组件
     9 + 
     10 +这里列举了 Pro 中所有用到的组件,这些组件不适合作为组件库,但是在业务中却真实需要。所以我们准备了这个文档,来指导大家是否需要使用这个组件。
     11 + 
     12 +## Footer 页脚组件
     13 + 
     14 +这个组件自带了一些 Pro 的配置,你一般都需要改掉它的信息。
     15 + 
     16 +```tsx
     17 +/**
     18 + * background: '#f0f2f5'
     19 + */
     20 +import Footer from '@/components/Footer';
     21 +import React from 'react';
     22 + 
     23 +export default () => <Footer />;
     24 +```
     25 + 
     26 +## HeaderDropdown 头部下拉列表
     27 + 
     28 +HeaderDropdown 是 antd Dropdown 的封装,但是增加了移动端的特殊处理,用法也是相同的。
     29 + 
     30 +```tsx
     31 +/**
     32 + * background: '#f0f2f5'
     33 + */
     34 +import HeaderDropdown from '@/components/HeaderDropdown';
     35 +import { Button, Menu } from 'antd';
     36 +import React from 'react';
     37 + 
     38 +export default () => {
     39 + const menuHeaderDropdown = (
     40 + <Menu selectedKeys={[]}>
     41 + <Menu.Item key="center">个人中心</Menu.Item>
     42 + <Menu.Item key="settings">个人设置</Menu.Item>
     43 + <Menu.Divider />
     44 + <Menu.Item key="logout">退出登录</Menu.Item>
     45 + </Menu>
     46 + );
     47 + return (
     48 + <HeaderDropdown overlay={menuHeaderDropdown}>
     49 + <Button>hover 展示菜单</Button>
     50 + </HeaderDropdown>
     51 + );
     52 +};
     53 +```
     54 + 
     55 +## HeaderSearch 头部搜索框
     56 + 
     57 +一个带补全数据的输入框,支持收起和展开 Input
     58 + 
     59 +```tsx
     60 +/**
     61 + * background: '#f0f2f5'
     62 + */
     63 +import HeaderSearch from '@/components/HeaderSearch';
     64 +import React from 'react';
     65 + 
     66 +export default () => {
     67 + return (
     68 + <HeaderSearch
     69 + placeholder="站内搜索"
     70 + defaultValue="umi ui"
     71 + options={[
     72 + { label: 'Ant Design Pro', value: 'Ant Design Pro' },
     73 + {
     74 + label: 'Ant Design',
     75 + value: 'Ant Design',
     76 + },
     77 + {
     78 + label: 'Pro Table',
     79 + value: 'Pro Table',
     80 + },
     81 + {
     82 + label: 'Pro Layout',
     83 + value: 'Pro Layout',
     84 + },
     85 + ]}
     86 + onSearch={(value) => {
     87 + console.log('input', value);
     88 + }}
     89 + />
     90 + );
     91 +};
     92 +```
     93 + 
     94 +### API
     95 + 
     96 +| 参数 | 说明 | 类型 | 默认值 |
     97 +| --------------- | ---------------------------------- | ---------------------------- | ------ |
     98 +| value | 输入框的值 | `string` | - |
     99 +| onChange | 值修改后触发 | `(value?: string) => void` | - |
     100 +| onSearch | 查询后触发 | `(value?: string) => void` | - |
     101 +| options | 选项菜单的的列表 | `{label,value}[]` | - |
     102 +| defaultVisible | 输入框默认是否显示,只有第一次生效 | `boolean` | - |
     103 +| visible | 输入框是否显示 | `boolean` | - |
     104 +| onVisibleChange | 输入框显示隐藏的回调函数 | `(visible: boolean) => void` | - |
     105 + 
     106 +## NoticeIcon 通知工具
     107 + 
     108 +通知工具提供一个展示多种通知信息的界面。
     109 + 
     110 +```tsx
     111 +/**
     112 + * background: '#f0f2f5'
     113 + */
     114 +import NoticeIcon from '@/components/NoticeIcon/NoticeIcon';
     115 +import { message } from 'antd';
     116 +import React from 'react';
     117 + 
     118 +export default () => {
     119 + const list = [
     120 + {
     121 + id: '000000001',
     122 + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
     123 + title: '你收到了 14 份新周报',
     124 + datetime: '2017-08-09',
     125 + type: 'notification',
     126 + },
     127 + {
     128 + id: '000000002',
     129 + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
     130 + title: '你推荐的 曲妮妮 已通过第三轮面试',
     131 + datetime: '2017-08-08',
     132 + type: 'notification',
     133 + },
     134 + ];
     135 + return (
     136 + <NoticeIcon
     137 + count={10}
     138 + onItemClick={(item) => {
     139 + message.info(`${item.title} 被点击了`);
     140 + }}
     141 + onClear={(title: string, key: string) => message.info('点击了清空更多')}
     142 + loading={false}
     143 + clearText="清空"
     144 + viewMoreText="查看更多"
     145 + onViewMore={() => message.info('点击了查看更多')}
     146 + clearClose
     147 + >
     148 + <NoticeIcon.Tab
     149 + tabKey="notification"
     150 + count={2}
     151 + list={list}
     152 + title="通知"
     153 + emptyText="你已查看所有通知"
     154 + showViewMore
     155 + />
     156 + <NoticeIcon.Tab
     157 + tabKey="message"
     158 + count={2}
     159 + list={list}
     160 + title="消息"
     161 + emptyText="您已读完所有消息"
     162 + showViewMore
     163 + />
     164 + <NoticeIcon.Tab
     165 + tabKey="event"
     166 + title="待办"
     167 + emptyText="你已完成所有待办"
     168 + count={2}
     169 + list={list}
     170 + showViewMore
     171 + />
     172 + </NoticeIcon>
     173 + );
     174 +};
     175 +```
     176 + 
     177 +### NoticeIcon API
     178 + 
     179 +| 参数 | 说明 | 类型 | 默认值 |
     180 +| --- | --- | --- | --- |
     181 +| count | 有多少未读通知 | `number` | - |
     182 +| bell | 铃铛的图表 | `ReactNode` | - |
     183 +| onClear | 点击清空数据按钮 | `(tabName: string, tabKey: string) => void` | - |
     184 +| onItemClick | 未读消息列被点击 | `(item: API.NoticeIconData, tabProps: NoticeIconTabProps) => void` | - |
     185 +| onViewMore | 查看更多的按钮点击 | `(tabProps: NoticeIconTabProps, e: MouseEvent) => void` | - |
     186 +| onTabChange | 通知 Tab 的切换 | `(tabTile: string) => void;` | - |
     187 +| popupVisible | 通知显示是否展示 | `boolean` | - |
     188 +| onPopupVisibleChange | 通知信息显示隐藏的回调函数 | `(visible: boolean) => void` | - |
     189 +| clearText | 清空按钮的文字 | `string` | - |
     190 +| viewMoreText | 查看更多的按钮文字 | `string` | - |
     191 +| clearClose | 展示清空按钮 | `boolean` | - |
     192 +| emptyImage | 列表为空时的兜底展示 | `ReactNode` | - |
     193 + 
     194 +### NoticeIcon.Tab API
     195 + 
     196 +| 参数 | 说明 | 类型 | 默认值 |
     197 +| ------------ | ------------------ | ------------------------------------ | ------ |
     198 +| count | 有多少未读通知 | `number` | - |
     199 +| title | 通知 Tab 的标题 | `ReactNode` | - |
     200 +| showClear | 展示清除按钮 | `boolean` | `true` |
     201 +| showViewMore | 展示加载更 | `boolean` | `true` |
     202 +| tabKey | Tab 的唯一 key | `string` | - |
     203 +| onClick | 子项的单击事件 | `(item: API.NoticeIconData) => void` | - |
     204 +| onClear | 清楚按钮的点击 | `()=>void` | - |
     205 +| emptyText | 为空的时候测试 | `()=>void` | - |
     206 +| viewMoreText | 查看更多的按钮文字 | `string` | - |
     207 +| onViewMore | 查看更多的按钮点击 | `( e: MouseEvent) => void` | - |
     208 +| list | 通知信息的列表 | `API.NoticeIconData` | - |
     209 + 
     210 +### NoticeIconData
     211 + 
     212 +```tsx | pure
     213 +export interface NoticeIconData {
     214 + id: string;
     215 + key: string;
     216 + avatar: string;
     217 + title: string;
     218 + datetime: string;
     219 + type: string;
     220 + read?: boolean;
     221 + description: string;
     222 + clickClose?: boolean;
     223 + extra: any;
     224 + status: string;
     225 +}
     226 +```
     227 + 
     228 +## RightContent
     229 + 
     230 +RightContent 是以上几个组件的组合,同时新增了 plugins 的 `SelectLang` 插件。
     231 + 
     232 +```tsx | pure
     233 +<Space>
     234 + <HeaderSearch
     235 + placeholder="站内搜索"
     236 + defaultValue="umi ui"
     237 + options={[
     238 + { label: <a href="https://umijs.org/zh/guide/umi-ui.html">umi ui</a>, value: 'umi ui' },
     239 + {
     240 + label: <a href="next.ant.design">Ant Design</a>,
     241 + value: 'Ant Design',
     242 + },
     243 + {
     244 + label: <a href="https://protable.ant.design/">Pro Table</a>,
     245 + value: 'Pro Table',
     246 + },
     247 + {
     248 + label: <a href="https://prolayout.ant.design/">Pro Layout</a>,
     249 + value: 'Pro Layout',
     250 + },
     251 + ]}
     252 + />
     253 + <Tooltip title="使用文档">
     254 + <span
     255 + className={styles.action}
     256 + onClick={() => {
     257 + window.location.href = 'https://pro.ant.design/docs/getting-started';
     258 + }}
     259 + >
     260 + <QuestionCircleOutlined />
     261 + </span>
     262 + </Tooltip>
     263 + <Avatar />
     264 + {REACT_APP_ENV && (
     265 + <span>
     266 + <Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag>
     267 + </span>
     268 + )}
     269 + <SelectLang className={styles.action} />
     270 +</Space>
     271 +```
     272 + 
  • ■ ■ ■ ■ ■ ■
    web/src/e2e/baseLayout.e2e.spec.ts
     1 +import type { Page } from '@playwright/test';
     2 +import { expect, test } from '@playwright/test';
     3 +const { uniq } = require('lodash');
     4 +const RouterConfig = require('../../config/routes').default;
     5 + 
     6 +const BASE_URL = `http://localhost:${process.env.PORT || 8001}`;
     7 + 
     8 +function formatter(routes: any, parentPath = ''): string[] {
     9 + const fixedParentPath = parentPath.replace(/\/{1,}/g, '/');
     10 + let result: string[] = [];
     11 + routes.forEach((item: { path: string; routes: string }) => {
     12 + if (item.path && !item.path.startsWith('/')) {
     13 + result.push(`${fixedParentPath}/${item.path}`.replace(/\/{1,}/g, '/'));
     14 + }
     15 + if (item.path && item.path.startsWith('/')) {
     16 + result.push(`${item.path}`.replace(/\/{1,}/g, '/'));
     17 + }
     18 + if (item.routes) {
     19 + result = result.concat(
     20 + formatter(item.routes, item.path ? `${fixedParentPath}/${item.path}` : parentPath),
     21 + );
     22 + }
     23 + });
     24 + return uniq(result.filter((item) => !!item));
     25 +}
     26 + 
     27 +const testPage = (path: string, page: Page) => async () => {
     28 + await page.evaluate(() => {
     29 + localStorage.setItem('antd-pro-authority', '["admin"]');
     30 + });
     31 + await page.goto(`${BASE_URL}${path}`);
     32 + await page.waitForSelector('footer', {
     33 + timeout: 2000,
     34 + });
     35 + const haveFooter = await page.evaluate(() => document.getElementsByTagName('footer').length > 0);
     36 + expect(haveFooter).toBeTruthy();
     37 +};
     38 + 
     39 +const routers = formatter(RouterConfig);
     40 + 
     41 +routers.forEach((route) => {
     42 + test(`test route page ${route}`, async ({ page }) => {
     43 + await testPage(route, page);
     44 + });
     45 +});
     46 + 
  • ■ ■ ■ ■ ■ ■
    web/src/enum/cloud.tsx
     1 +import {Space} from "antd";
     2 +import IconFont from "@/components/IconFont";
     3 + 
     4 +export const CloudProvideTypeIcon = [
     5 + <></>,
     6 + <IconFont type={"icon-aliyun"}/>,
     7 + <IconFont type={"icon-tengxunyun1"}/>,
     8 + <IconFont type={"icon-huaweiyun1"}/>,
     9 + <IconFont type={"icon-baiduyun"}/>,
     10 + <IconFont type={"icon-sealos"}/>
     11 +]
     12 + 
     13 +export const CloudProvideTypeValueEnum = {
     14 + 1: <Space><IconFont type={"icon-aliyun"}/>阿里云</Space>,
     15 + 2: <Space><IconFont type={"icon-tengxunyun1"}/>腾讯云</Space>,
     16 + // 3: <Space><IconFont type={"icon-huaweiyun1"}/>华为云</Space>,
     17 + // 4: <Space><IconFont type={"icon-baiduyun"}/>百度云</Space>,
     18 + 5: <Space><IconFont type={"icon-sealos"}/>Sealos</Space>,
     19 +}
     20 + 
     21 + 
     22 +export const CloudProvideTypeEnum = {
     23 + 1: {
     24 + text: <Space><IconFont type={"icon-aliyun"}/>阿里云</Space>,
     25 + },
     26 + 2: {
     27 + text: <Space><IconFont type={"icon-tengxunyun1"}/>腾讯云</Space>,
     28 + },
     29 + // 3: {
     30 + // text: <Space><IconFont type={"icon-huaweiyun1"}/>华为云</Space>,
     31 + // },
     32 + // 4: {
     33 + // text: <Space><IconFont type={"icon-baiduyun"}/>百度云</Space>,
     34 + // },
     35 + 5: {
     36 + text: <Space><IconFont type={"icon-sealos"}/>Sealos</Space>,
     37 + },
     38 +}
     39 + 
     40 +export const CloudProviderStatusEnum = {
     41 + 1: {
     42 + text: '创建中',
     43 + status: 'processing',
     44 + },
     45 + 2: {
     46 + text: '正常',
     47 + status: 'success',
     48 + },
     49 + 3: {
     50 + text: '异常',
     51 + status: 'error',
     52 + },
     53 + 4: {
     54 + text: '同步中',
     55 + status: 'default',
     56 + },
     57 + 5: {
     58 + text: '已禁用',
     59 + status: 'error',
     60 + },
     61 + 6: {
     62 + text: '同步失败',
     63 + status: 'error',
     64 + },
     65 + 7: {
     66 + text: '删除中',
     67 + status: 'warning',
     68 + }
     69 +}
     70 + 
     71 +export const ALiYunRegionEnum = {
     72 + // 阿里云
     73 + "cn-hangzhou": "华东1(杭州)",
     74 + "cn-shanghai": "华东2(上海)",
     75 + "cn-qingdao": "华北1(青岛)",
     76 + "cn-beijing": "华北2(北京)",
     77 + "cn-zhangjiakou": "华北3(张家口)",
     78 + "cn-huhehaote": "华北5(呼和浩特)",
     79 + "cn-shenzhen": "华南1(深圳)",
     80 + "cn-chengdu": "西南1(成都)",
     81 + "cn-hongkong": "中国香港",
     82 + "ap-northeast-1": "日本(东京)",
     83 + // "ap-northeast-2": "韩国(首尔)",
     84 + "ap-southeast-1": "新加坡(新加坡)",
     85 + "ap-southeast-2": "澳大利亚(悉尼)",
     86 + "ap-southeast-3": "马来西亚(吉隆坡)",
     87 + "ap-southeast-5": "印尼(雅加达)",
     88 + // "ap-southeast-7": "泰国(曼谷)",
     89 + "ap-south-1": "印度(孟买)",
     90 + "eu-central-1": "德国(法兰克福)",
     91 + "eu-west-1": "英国(伦敦)",
     92 + "us-west-1": "美国(硅谷)",
     93 + "us-east-1": "美国(弗吉尼亚)",
     94 +}
     95 + 
     96 +export const SealosRegionEnum = {
     97 + "beijing-a": "北京 A",
     98 + "singapore-b": "新加坡 B",
     99 + "guangzhou-g": "广州 G",
     100 + "hangzhou-h": "杭州 H",
     101 +}
     102 + 
     103 +export const TencentRegionEnum = {
     104 + "ap-beijing": "华北(北京)",
     105 + "ap-chengdu": "西南(成都)",
     106 + "ap-guangzhou": "华南(广州)",
     107 + "ap-shanghai": "华东(上海)",
     108 + "ap-nanjing": "华东(南京)",
     109 + "ap-hongkong": "中国香港",
     110 + "ap-mumbai": "亚太南部(孟买)",
     111 + "ap-singapore": "亚太东南(新加坡)",
     112 + "ap-bangkok": "亚太东南(曼谷)",
     113 + "ap-seoul": "亚太东北(首尔)",
     114 + "ap-tokyo": "亚太东北(东京)",
     115 + "eu-frankfurt": "欧洲(法兰克福)",
     116 + "na-ashburn": "美国东部(弗吉尼亚)",
     117 + // "na-toronto": "北美(多伦多)",
     118 + "na-siliconvalley": "美国西部(硅谷)",
     119 +}
     120 + 
     121 +export const RegionEnum = {
     122 + ...ALiYunRegionEnum,
     123 + ...TencentRegionEnum,
     124 + ...SealosRegionEnum,
     125 +}
     126 + 
  • ■ ■ ■ ■ ■ ■
    web/src/enum/service.tsx
     1 +import {Tag, Space, Tooltip} from "antd";
     2 +import {
     3 + SyncOutlined,
     4 + MinusCircleOutlined,
     5 + CloseCircleOutlined,
     6 + // ExclamationCircleOutlined,
     7 + ClockCircleOutlined
     8 +} from "@ant-design/icons";
     9 +import IconFont from "@/components/IconFont";
     10 +import React from "react";
     11 +import ThunderboltOutlined from "@ant-design/icons/ThunderboltOutlined";
     12 + 
     13 +export type DynamicProps = {
     14 + status: number
     15 + spin: boolean
     16 + msg?: string
     17 +}
     18 + 
     19 +export const ProxyDynamicTagList: React.FC<DynamicProps> = (props) => {
     20 + switch (props.status) {
     21 + case 1:
     22 + return <Tag icon={<ClockCircleOutlined spin={props.spin}/>} color={"processing"}>初始化</Tag>
     23 + case 2:
     24 + return <Tag icon={<SyncOutlined spin={props.spin}/>} color="cyan">运行中</Tag>
     25 + case 3:
     26 + return <Tag icon={<MinusCircleOutlined/>} color="geekblue">已停止</Tag>
     27 + case 4:
     28 + return <Tooltip title={props.msg}>
     29 + <Tag icon={<CloseCircleOutlined/>} color="red">服务错误</Tag>
     30 + </Tooltip>
     31 + case 5:
     32 + return <Tag icon={<ThunderboltOutlined/>} color="blue">测速中</Tag>
     33 + case 6:
     34 + return <Tag icon={<ClockCircleOutlined spin={props.spin}/>} color="yellow">恢复中</Tag>
     35 + case 7:
     36 + return <Tag icon={<ClockCircleOutlined spin={props.spin}/>} color="yellow">删除中</Tag>
     37 + }
     38 + return <></>
     39 +}
     40 + 
     41 +export const ProxyTypeTagColor = {
     42 + "default": "#666666",
     43 + "auto": "#61C8C6",
     44 + "socks5": "#E2003B",
     45 + "http": "#1296DB",
     46 +}
     47 + 
     48 +export const ProxyTypeIcon = {
     49 + "": <IconFont type={"icon-proxy-default"}/>,
     50 + "auto": <IconFont type={"icon-proxy-auto"}/>,
     51 + "socks5": <IconFont type={"icon-proxy-socks5"}/>,
     52 + "http": <IconFont type={"icon-proxy-http"}/>,
     53 +}
     54 + 
     55 + 
     56 +export const ProxyTypeValueEnum = {
     57 + "auto": <Space><IconFont type={"icon-proxy-auto"}/>auto</Space>,
     58 + "socks5": <Space><IconFont type={"icon-proxy-socks5"}/>socks5</Space>,
     59 + "http": <Space><IconFont type={"icon-proxy-http"}/>http</Space>,
     60 +}
     61 + 
  • ■ ■ ■ ■ ■ ■
    web/src/enum/tunnel.tsx
     1 +import {Space} from "antd";
     2 +import IconFont from "@/components/IconFont";
     3 +import {Tag} from "antd";
     4 +import {
     5 + ClockCircleOutlined,
     6 + CloseCircleOutlined, ExclamationCircleOutlined,
     7 + MinusCircleOutlined,
     8 + SyncOutlined
     9 +} from "@ant-design/icons";
     10 + 
     11 +export const TunnelTypeValueEnum = {
     12 + "websocket": <Space><IconFont type={"icon-web-socket"}/>WebSockets</Space>,
     13 + "grpc": <Space><IconFont type={"icon-gRPC-red-copy"}/>GRPC</Space>,
     14 +}
     15 + 
     16 +export const TunnelTypeIcon = {
     17 + "unknown": <IconFont type={"icon-svc_node"}/>,
     18 + "websocket": <IconFont type={"icon-web-socket"}/>,
     19 + "grpc": <IconFont type={"icon-gRPC-red-copy"}/>
     20 +}
     21 + 
     22 +export const TunnelStatusEnum = {
     23 + 1: {
     24 + text: '创建中',
     25 + status: 'processing',
     26 + },
     27 + 2: {
     28 + text: '运行中',
     29 + status: 'success',
     30 + },
     31 + 3: {
     32 + text: '已停用',
     33 + status: 'default',
     34 + },
     35 + 4: {
     36 + text: '异常',
     37 + status: 'error',
     38 + },
     39 + 5: {
     40 + text: '正在部署',
     41 + status: 'warning',
     42 + },
     43 + 6: {
     44 + text: '删除中',
     45 + status: 'warning',
     46 + },
     47 +}
     48 + 
     49 + 
     50 +export const TunnelStatusTag = [
     51 + <></>,
     52 + <Tag icon={<ClockCircleOutlined spin/>} color={"processing"}>创建中</Tag>,
     53 + <Tag icon={<SyncOutlined spin/>} color="cyan">运行中</Tag>,
     54 + <Tag icon={<MinusCircleOutlined/>} color="geekblue">已停用</Tag>,
     55 + <Tag icon={<CloseCircleOutlined/>} color="red">异常</Tag>,
     56 + <Tag icon={<SyncOutlined spin/>} color="gold">正在部署</Tag>,
     57 + <Tag icon={<ExclamationCircleOutlined/>} color="gold">删除中</Tag>,
     58 +]
     59 + 
     60 +export const TunnelAuthFCTypeEnum = {
     61 + 1: '无认证',
     62 + 5: '签名认证',
     63 + 6: 'jwt'
     64 +}
     65 + 
  • ■ ■ ■ ■ ■ ■
    web/src/global.less
     1 +@import '~antd/es/style/variable.less';
     2 + 
     3 +html,
     4 +body,
     5 +#root {
     6 + height: 100%;
     7 + .ant-pro-global-header-layout-mix {
     8 + background-color: rgb(36, 37, 37);
     9 + }
     10 +}
     11 + 
     12 +.colorWeak {
     13 + filter: invert(80%);
     14 +}
     15 + 
     16 +.ant-layout {
     17 + min-height: 100vh;
     18 +}
     19 +.ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {
     20 + left: unset;
     21 +}
     22 + 
     23 +.ant-pro-list .ant-pro-list-row-card .ant-list-item-meta-title {
     24 + margin-left: 7px;
     25 +}
     26 + 
     27 +.ant-pro-card-header {
     28 + padding: 16px 16px;
     29 +}
     30 + 
     31 +canvas {
     32 + display: block;
     33 +}
     34 + 
     35 +body {
     36 + text-rendering: optimizeLegibility;
     37 + -webkit-font-smoothing: antialiased;
     38 + -moz-osx-font-smoothing: grayscale;
     39 +}
     40 + 
     41 +ul,
     42 +ol {
     43 + list-style: none;
     44 +}
     45 + 
     46 +@media (max-width: @screen-xs) {
     47 + .ant-table {
     48 + width: 100%;
     49 + overflow-x: auto;
     50 + &-thead > tr,
     51 + &-tbody > tr {
     52 + > th,
     53 + > td {
     54 + white-space: pre;
     55 + > span {
     56 + display: block;
     57 + }
     58 + }
     59 + }
     60 + }
     61 +}
     62 + 
     63 +// Compatible with IE11
     64 +@media screen and(-ms-high-contrast: active), (-ms-high-contrast: none) {
     65 + body .ant-design-pro > .ant-layout {
     66 + min-height: 100vh;
     67 + }
     68 +}
     69 + 
  • ■ ■ ■ ■ ■ ■
    web/src/global.tsx
     1 +import { Button, message, notification } from 'antd';
     2 +import { useIntl } from 'umi';
     3 +import defaultSettings from '../config/defaultSettings';
     4 + 
     5 +const { pwa } = defaultSettings;
     6 +const isHttps = document.location.protocol === 'https:';
     7 + 
     8 +const clearCache = () => {
     9 + // remove all caches
     10 + if (window.caches) {
     11 + caches
     12 + .keys()
     13 + .then((keys) => {
     14 + keys.forEach((key) => {
     15 + caches.delete(key);
     16 + });
     17 + })
     18 + .catch((e) => console.log(e));
     19 + }
     20 +};
     21 + 
     22 +// if pwa is true
     23 +if (pwa) {
     24 + // Notify user if offline now
     25 + window.addEventListener('sw.offline', () => {
     26 + message.warning(useIntl().formatMessage({ id: 'app.pwa.offline' }));
     27 + });
     28 + 
     29 + // Pop up a prompt on the page asking the user if they want to use the latest version
     30 + window.addEventListener('sw.updated', (event: Event) => {
     31 + const e = event as CustomEvent;
     32 + const reloadSW = async () => {
     33 + // Check if there is sw whose state is waiting in ServiceWorkerRegistration
     34 + // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
     35 + const worker = e.detail && e.detail.waiting;
     36 + if (!worker) {
     37 + return true;
     38 + }
     39 + // Send skip-waiting event to waiting SW with MessageChannel
     40 + await new Promise((resolve, reject) => {
     41 + const channel = new MessageChannel();
     42 + channel.port1.onmessage = (msgEvent) => {
     43 + if (msgEvent.data.error) {
     44 + reject(msgEvent.data.error);
     45 + } else {
     46 + resolve(msgEvent.data);
     47 + }
     48 + };
     49 + worker.postMessage({ type: 'skip-waiting' }, [channel.port2]);
     50 + });
     51 + 
     52 + clearCache();
     53 + window.location.reload();
     54 + return true;
     55 + };
     56 + const key = `open${Date.now()}`;
     57 + const btn = (
     58 + <Button
     59 + type="primary"
     60 + onClick={() => {
     61 + notification.close(key);
     62 + reloadSW();
     63 + }}
     64 + >
     65 + {useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.ok' })}
     66 + </Button>
     67 + );
     68 + notification.open({
     69 + message: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated' }),
     70 + description: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }),
     71 + btn,
     72 + key,
     73 + onClose: async () => null,
     74 + });
     75 + });
     76 +} else if ('serviceWorker' in navigator && isHttps) {
     77 + // unregister service worker
     78 + const { serviceWorker } = navigator;
     79 + if (serviceWorker.getRegistrations) {
     80 + serviceWorker.getRegistrations().then((sws) => {
     81 + sws.forEach((sw) => {
     82 + sw.unregister();
     83 + });
     84 + });
     85 + }
     86 + serviceWorker.getRegistration().then((sw) => {
     87 + if (sw) sw.unregister();
     88 + });
     89 + 
     90 + clearCache();
     91 +}
     92 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/bn-BD/component.ts
     1 +export default {
     2 + 'component.tagSelect.expand': 'বিস্তৃত',
     3 + 'component.tagSelect.collapse': 'সঙ্কুচিত',
     4 + 'component.tagSelect.all': 'সব',
     5 +};
     6 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/bn-BD/globalHeader.ts
     1 +export default {
     2 + 'component.globalHeader.search': 'অনুসন্ধান করুন',
     3 + 'component.globalHeader.search.example1': 'অনুসন্ধান উদাহরণ ১',
     4 + 'component.globalHeader.search.example2': 'অনুসন্ধান উদাহরণ ২',
     5 + 'component.globalHeader.search.example3': 'অনুসন্ধান উদাহরণ ৩',
     6 + 'component.globalHeader.help': 'সহায়তা',
     7 + 'component.globalHeader.notification': 'বিজ্ঞপ্তি',
     8 + 'component.globalHeader.notification.empty': 'আপনি সমস্ত বিজ্ঞপ্তি দেখেছেন।',
     9 + 'component.globalHeader.message': 'বার্তা',
     10 + 'component.globalHeader.message.empty': 'আপনি সমস্ত বার্তা দেখেছেন।',
     11 + 'component.globalHeader.event': 'ঘটনা',
     12 + 'component.globalHeader.event.empty': 'আপনি সমস্ত ইভেন্ট দেখেছেন।',
     13 + 'component.noticeIcon.clear': 'সাফ',
     14 + 'component.noticeIcon.cleared': 'সাফ করা হয়েছে',
     15 + 'component.noticeIcon.empty': 'বিজ্ঞপ্তি নেই',
     16 + 'component.noticeIcon.view-more': 'আরো দেখুন',
     17 +};
     18 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/bn-BD/menu.ts
     1 +export default {
     2 + 'menu.welcome': 'স্বাগতম',
     3 + 'menu.more-blocks': 'আরও ব্লক',
     4 + 'menu.home': 'নীড়',
     5 + 'menu.admin': 'অ্যাডমিন',
     6 + 'menu.admin.sub-page': 'উপ-পৃষ্ঠা',
     7 + 'menu.login': 'প্রবেশ',
     8 + 'menu.register': 'নিবন্ধন',
     9 + 'menu.register-result': 'নিবন্ধনে ফলাফল',
     10 + 'menu.dashboard': 'ড্যাশবোর্ড',
     11 + 'menu.dashboard.analysis': 'বিশ্লেষণ',
     12 + 'menu.dashboard.monitor': 'নিরীক্ষণ',
     13 + 'menu.dashboard.workplace': 'কর্মক্ষেত্র',
     14 + 'menu.exception.403': '403',
     15 + 'menu.exception.404': '404',
     16 + 'menu.exception.500': '500',
     17 + 'menu.form': 'ফর্ম',
     18 + 'menu.form.basic-form': 'বেসিক ফর্ম',
     19 + 'menu.form.step-form': 'পদক্ষেপ ফর্ম',
     20 + 'menu.form.step-form.info': 'পদক্ষেপ ফর্ম (স্থানান্তর তথ্য লিখুন)',
     21 + 'menu.form.step-form.confirm': 'পদক্ষেপ ফর্ম (স্থানান্তর তথ্য নিশ্চিত করুন)',
     22 + 'menu.form.step-form.result': 'পদক্ষেপ ফর্ম (সমাপ্ত)',
     23 + 'menu.form.advanced-form': 'উন্নত ফর্ম',
     24 + 'menu.list': 'তালিকা',
     25 + 'menu.list.table-list': 'অনুসন্ধানের টেবিল',
     26 + 'menu.list.basic-list': 'বেসিক তালিকা',
     27 + 'menu.list.card-list': 'কার্ডের তালিকা',
     28 + 'menu.list.search-list': 'অনুসন্ধানের তালিকা',
     29 + 'menu.list.search-list.articles': 'অনুসন্ধানের তালিকা (নিবন্ধসমূহ)',
     30 + 'menu.list.search-list.projects': 'অনুসন্ধানের তালিকা (প্রকল্পগুলি)',
     31 + 'menu.list.search-list.applications': 'অনুসন্ধানের তালিকা (অ্যাপ্লিকেশন)',
     32 + 'menu.profile': 'প্রোফাইল',
     33 + 'menu.profile.basic': 'বেসিক প্রোফাইল',
     34 + 'menu.profile.advanced': 'উন্নত প্রোফাইল',
     35 + 'menu.result': 'ফলাফল',
     36 + 'menu.result.success': 'সাফল্য',
     37 + 'menu.result.fail': 'ব্যর্থ',
     38 + 'menu.exception': 'ব্যতিক্রম',
     39 + 'menu.exception.not-permission': '403',
     40 + 'menu.exception.not-find': '404',
     41 + 'menu.exception.server-error': '500',
     42 + 'menu.exception.trigger': 'ট্রিগার',
     43 + 'menu.account': 'হিসাব',
     44 + 'menu.account.center': 'অ্যাকাউন্ট কেন্দ্র',
     45 + 'menu.account.settings': 'অ্যাকাউন্ট সেটিংস',
     46 + 'menu.account.trigger': 'ট্রিগার ত্রুটি',
     47 + 'menu.account.logout': 'প্রস্থান',
     48 + 'menu.editor': 'গ্রাফিক সম্পাদক',
     49 + 'menu.editor.flow': 'ফ্লো এডিটর',
     50 + 'menu.editor.mind': 'মাইন্ড এডিটর',
     51 + 'menu.editor.koni': 'কোনি সম্পাদক',
     52 +};
     53 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/bn-BD/pages.ts
     1 +export default {
     2 + 'pages.layouts.userLayout.title':
     3 + 'পিঁপড়া ডিজাইন হচ্ছে সিহু জেলার সবচেয়ে প্রভাবশালী ওয়েব ডিজাইনের স্পেসিফিকেশন',
     4 + 'pages.login.accountLogin.tab': 'অ্যাকাউন্টে লগইন',
     5 + 'pages.login.accountLogin.errorMessage': 'ভুল ব্যবহারকারীর নাম/পাসওয়ার্ড(admin/ant.design)',
     6 + 'pages.login.failure': 'লগইন ব্যর্থ হয়েছে। আবার চেষ্টা করুন!',
     7 + 'pages.login.success': 'সফল লগইন!',
     8 + 'pages.login.username.placeholder': 'ব্যবহারকারীর নাম: admin or user',
     9 + 'pages.login.username.required': 'আপনার ব্যবহারকারীর নাম ইনপুট করুন!',
     10 + 'pages.login.password.placeholder': 'পাসওয়ার্ড: ant.design',
     11 + 'pages.login.password.required': 'আপনার পাসওয়ার্ড ইনপুট করুন!',
     12 + 'pages.login.phoneLogin.tab': 'ফোন লগইন',
     13 + 'pages.login.phoneLogin.errorMessage': 'যাচাইকরণ কোড ত্রুটি',
     14 + 'pages.login.phoneNumber.placeholder': 'ফোন নম্বর',
     15 + 'pages.login.phoneNumber.required': 'আপনার ফোন নম্বর ইনপুট করুন!',
     16 + 'pages.login.phoneNumber.invalid': 'ফোন নম্বরটি সঠিক নয়!',
     17 + 'pages.login.captcha.placeholder': 'যাচাইকরণের কোড',
     18 + 'pages.login.captcha.required': 'দয়া করে ভেরিফিকেশন কোডটি ইনপুট করুন!',
     19 + 'pages.login.phoneLogin.getVerificationCode': 'কোড পান',
     20 + 'pages.getCaptchaSecondText': 'সেকেন্ড',
     21 + 'pages.login.rememberMe': 'আমাকে মনে রাখুন',
     22 + 'pages.login.forgotPassword': 'পাসওয়ার্ড ভুলে গেছেন?',
     23 + 'pages.login.submit': 'প্রবেশ করুন',
     24 + 'pages.login.loginWith': 'লগইন করতে পারেন:',
     25 + 'pages.login.registerAccount': 'অ্যাকাউন্ট নিবন্ধন করুন',
     26 + 'pages.welcome.link': 'স্বাগতম',
     27 + 'pages.welcome.alertMessage': 'দ্রুত এবং শক্তিশালী ভারী শুল্ক উপাদান প্রকাশ করা হয়েছে।',
     28 + 'pages.admin.subPage.title': 'এই পৃষ্ঠাটি কেবল অ্যাডমিন দ্বারা দেখা যাবে',
     29 + 'pages.admin.subPage.alertMessage':
     30 + 'UMI UI এখন প্রকাশিত হয়েছে, অভিজ্ঞতা শুরু করতে npm run ui ব্যবহার করতে স্বাগতম।',
     31 + 'pages.searchTable.createForm.newRule': 'নতুন বিধি',
     32 + 'pages.searchTable.updateForm.ruleConfig': 'বিধি কনফিগারেশন',
     33 + 'pages.searchTable.updateForm.basicConfig': 'মৌলিক তথ্য',
     34 + 'pages.searchTable.updateForm.ruleName.nameLabel': 'বিধি নাম',
     35 + 'pages.searchTable.updateForm.ruleName.nameRules': 'বিধির নাম লিখুন!',
     36 + 'pages.searchTable.updateForm.ruleDesc.descLabel': 'বিধির বিবরণ',
     37 + 'pages.searchTable.updateForm.ruleDesc.descPlaceholder': 'কমপক্ষে পাঁচটি অক্ষর লিখুন',
     38 + 'pages.searchTable.updateForm.ruleDesc.descRules':
     39 + 'কমপক্ষে পাঁচটি অক্ষরের একটি বিধান বিবরণ লিখুন!',
     40 + 'pages.searchTable.updateForm.ruleProps.title': 'বৈশিষ্ট্য কনফিগার করুন',
     41 + 'pages.searchTable.updateForm.object': 'নিরীক্ষণ অবজেক্ট',
     42 + 'pages.searchTable.updateForm.ruleProps.templateLabel': 'বিধি টেম্পলেট',
     43 + 'pages.searchTable.updateForm.ruleProps.typeLabel': 'বিধি প্রকার',
     44 + 'pages.searchTable.updateForm.schedulingPeriod.title': 'সময়সূচী নির্ধারণ করুন',
     45 + 'pages.searchTable.updateForm.schedulingPeriod.timeLabel': 'শুরুর সময়',
     46 + 'pages.searchTable.updateForm.schedulingPeriod.timeRules': 'একটি শুরুর সময় চয়ন করুন!',
     47 + 'pages.searchTable.titleDesc': 'বর্ণনা',
     48 + 'pages.searchTable.ruleName': 'বিধি নাম প্রয়োজন',
     49 + 'pages.searchTable.titleCallNo': 'পরিষেবা কল সংখ্যা',
     50 + 'pages.searchTable.titleStatus': 'অবস্থা',
     51 + 'pages.searchTable.nameStatus.default': 'ডিফল্ট',
     52 + 'pages.searchTable.nameStatus.running': 'চলমান',
     53 + 'pages.searchTable.nameStatus.online': 'অনলাইন',
     54 + 'pages.searchTable.nameStatus.abnormal': 'অস্বাভাবিক',
     55 + 'pages.searchTable.titleUpdatedAt': 'সর্বশেষ নির্ধারিত',
     56 + 'pages.searchTable.exception': 'ব্যতিক্রম জন্য কারণ লিখুন!',
     57 + 'pages.searchTable.titleOption': 'অপশন',
     58 + 'pages.searchTable.config': 'কনফিগারেশন',
     59 + 'pages.searchTable.subscribeAlert': 'সতর্কতা সাবস্ক্রাইব করুন',
     60 + 'pages.searchTable.title': 'ইনকয়েরি ফরম',
     61 + 'pages.searchTable.new': 'নতুন',
     62 + 'pages.searchTable.chosen': 'নির্বাচিত',
     63 + 'pages.searchTable.item': 'আইটেম',
     64 + 'pages.searchTable.totalServiceCalls': 'পরিষেবা কলগুলির মোট সংখ্যা',
     65 + 'pages.searchTable.tenThousand': '000',
     66 + 'pages.searchTable.batchDeletion': 'একসাখে ডিলিট',
     67 + 'pages.searchTable.batchApproval': 'একসাখে অনুমোদন',
     68 +};
     69 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/bn-BD/pwa.ts
     1 +export default {
     2 + 'app.pwa.offline': 'আপনি এখন অফলাইন',
     3 + 'app.pwa.serviceworker.updated': 'নতুন সামগ্রী উপলব্ধ',
     4 + 'app.pwa.serviceworker.updated.hint':
     5 + 'বর্তমান পৃষ্ঠাটি পুনরায় লোড করতে দয়া করে "রিফ্রেশ" বোতাম টিপুন',
     6 + 'app.pwa.serviceworker.updated.ok': 'রিফ্রেশ',
     7 +};
     8 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/bn-BD/settingDrawer.ts
     1 +export default {
     2 + 'app.setting.pagestyle': 'পৃষ্ঠা স্টাইল সেটিং',
     3 + 'app.setting.pagestyle.dark': 'ডার্ক স্টাইল',
     4 + 'app.setting.pagestyle.light': 'লাইট স্টাইল',
     5 + 'app.setting.content-width': 'সামগ্রীর প্রস্থ',
     6 + 'app.setting.content-width.fixed': 'স্থির',
     7 + 'app.setting.content-width.fluid': 'প্রবাহী',
     8 + 'app.setting.themecolor': 'থিম রঙ',
     9 + 'app.setting.themecolor.dust': 'ডাস্ট রেড',
     10 + 'app.setting.themecolor.volcano': 'আগ্নেয়গিরি',
     11 + 'app.setting.themecolor.sunset': 'সানসেট কমলা',
     12 + 'app.setting.themecolor.cyan': 'সবুজাভ নীল',
     13 + 'app.setting.themecolor.green': 'পোলার সবুজ',
     14 + 'app.setting.themecolor.daybreak': 'দিবস ব্রেক ব্লু (ডিফল্ট)',
     15 + 'app.setting.themecolor.geekblue': 'গিক আঠালো',
     16 + 'app.setting.themecolor.purple': 'গোল্ডেন বেগুনি',
     17 + 'app.setting.navigationmode': 'নেভিগেশন মোড',
     18 + 'app.setting.sidemenu': 'সাইড মেনু লেআউট',
     19 + 'app.setting.topmenu': 'টপ মেনু লেআউট',
     20 + 'app.setting.fixedheader': 'স্থির হেডার',
     21 + 'app.setting.fixedsidebar': 'স্থির সাইডবার',
     22 + 'app.setting.fixedsidebar.hint': 'সাইড মেনু বিন্যাসে কাজ করে',
     23 + 'app.setting.hideheader': 'স্ক্রোল করার সময় হেডার লুকানো',
     24 + 'app.setting.hideheader.hint': 'লুকানো হেডার সক্ষম থাকলে কাজ করে',
     25 + 'app.setting.othersettings': 'অন্যান্য সেটিংস্',
     26 + 'app.setting.weakmode': 'দুর্বল মোড',
     27 + 'app.setting.copy': 'সেটিং কপি করুন',
     28 + 'app.setting.copyinfo': 'সাফল্যের অনুলিপি করুন - প্রতিস্থাপন করুন: src/models/setting.js',
     29 + 'app.setting.production.hint':
     30 + 'কেবল বিকাশের পরিবেশে প্যানেল শো সেট করা হচ্ছে, দয়া করে ম্যানুয়ালি সংশোধন করুন',
     31 +};
     32 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/bn-BD/settings.ts
     1 +export default {
     2 + 'app.settings.menuMap.basic': 'মৌলিক বৈশিষ্ট্যসহ',
     3 + 'app.settings.menuMap.security': 'নিরাপত্তা বিন্যাস',
     4 + 'app.settings.menuMap.binding': 'অ্যাকাউন্ট বাঁধাই',
     5 + 'app.settings.menuMap.notification': 'নতুন বার্তা বিজ্ঞপ্তি',
     6 + 'app.settings.basic.avatar': 'অবতার',
     7 + 'app.settings.basic.change-avatar': 'অবতার পরিবর্তন করুন',
     8 + 'app.settings.basic.email': 'ইমেইল',
     9 + 'app.settings.basic.email-message': 'আপনার ইমেইল ইনপুট করুন!',
     10 + 'app.settings.basic.nickname': 'ডাক নাম',
     11 + 'app.settings.basic.nickname-message': 'আপনার ডাকনামটি ইনপুট করুন!',
     12 + 'app.settings.basic.profile': 'ব্যক্তিগত প্রোফাইল',
     13 + 'app.settings.basic.profile-message': 'আপনার ব্যক্তিগত প্রোফাইল ইনপুট করুন!',
     14 + 'app.settings.basic.profile-placeholder': 'নিজের সাথে সংক্ষিপ্ত পরিচয়',
     15 + 'app.settings.basic.country': 'দেশ/অঞ্চল',
     16 + 'app.settings.basic.country-message': 'আপনার দেশ ইনপুট করুন!',
     17 + 'app.settings.basic.geographic': 'প্রদেশ বা শহর',
     18 + 'app.settings.basic.geographic-message': 'আপনার ভৌগলিক তথ্য ইনপুট করুন!',
     19 + 'app.settings.basic.address': 'রাস্তার ঠিকানা',
     20 + 'app.settings.basic.address-message': 'দয়া করে আপনার ঠিকানা ইনপুট করুন!',
     21 + 'app.settings.basic.phone': 'ফোন নম্বর',
     22 + 'app.settings.basic.phone-message': 'আপনার ফোন ইনপুট করুন!',
     23 + 'app.settings.basic.update': 'তথ্য হালনাগাদ',
     24 + 'app.settings.security.strong': 'শক্তিশালী',
     25 + 'app.settings.security.medium': 'মধ্যম',
     26 + 'app.settings.security.weak': 'দুর্বল',
     27 + 'app.settings.security.password': 'অ্যাকাউন্টের পাসওয়ার্ড',
     28 + 'app.settings.security.password-description': 'বর্তমান পাসওয়ার্ড শক্তি',
     29 + 'app.settings.security.phone': 'সুরক্ষা ফোন',
     30 + 'app.settings.security.phone-description': 'আবদ্ধ ফোন',
     31 + 'app.settings.security.question': 'নিরাপত্তা প্রশ্ন',
     32 + 'app.settings.security.question-description':
     33 + 'সুরক্ষা প্রশ্ন সেট করা নেই, এবং সুরক্ষা নীতি কার্যকরভাবে অ্যাকাউন্ট সুরক্ষা রক্ষা করতে পারে',
     34 + 'app.settings.security.email': 'ব্যাকআপ ইমেইল',
     35 + 'app.settings.security.email-description': 'বাউন্ড ইমেইল',
     36 + 'app.settings.security.mfa': 'MFA ডিভাইস',
     37 + 'app.settings.security.mfa-description':
     38 + "আনবাউন্ড এমএফএ ডিভাইস, বাঁধাইয়ের পরে, দু'বার নিশ্চিত করা যায়",
     39 + 'app.settings.security.modify': 'পরিবর্তন করুন',
     40 + 'app.settings.security.set': 'সেট',
     41 + 'app.settings.security.bind': 'বাঁধাই',
     42 + 'app.settings.binding.taobao': 'বাঁধাই তাওবাও',
     43 + 'app.settings.binding.taobao-description': 'বর্তমানে আনবাউন্ড তাওবাও অ্যাকাউন্ট',
     44 + 'app.settings.binding.alipay': 'বাইন্ডিং আলিপে',
     45 + 'app.settings.binding.alipay-description': 'বর্তমানে আনবাউন্ড আলিপে অ্যাকাউন্ট',
     46 + 'app.settings.binding.dingding': 'বাঁধাই ডিঙ্গটালক',
     47 + 'app.settings.binding.dingding-description': 'বর্তমানে আনবাউন্ড ডিঙ্গটাল অ্যাকাউন্ট',
     48 + 'app.settings.binding.bind': 'বাঁধাই',
     49 + 'app.settings.notification.password': 'অ্যাকাউন্টের পাসওয়ার্ড',
     50 + 'app.settings.notification.password-description':
     51 + 'অন্যান্য ব্যবহারকারীর বার্তাগুলি স্টেশন চিঠি আকারে জানানো হবে',
     52 + 'app.settings.notification.messages': 'সিস্টেম বার্তা',
     53 + 'app.settings.notification.messages-description':
     54 + 'সিস্টেম বার্তাগুলি স্টেশন চিঠির আকারে জানানো হবে',
     55 + 'app.settings.notification.todo': 'করণীয় বিজ্ঞপ্তি',
     56 + 'app.settings.notification.todo-description': 'করণীয় তালিকাটি স্টেশন থেকে চিঠি আকারে জানানো হবে',
     57 + 'app.settings.open': 'খোলা',
     58 + 'app.settings.close': 'বন্ধ',
     59 +};
     60 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/bn-BD.ts
     1 +import component from './bn-BD/component';
     2 +import globalHeader from './bn-BD/globalHeader';
     3 +import menu from './bn-BD/menu';
     4 +import pages from './bn-BD/pages';
     5 +import pwa from './bn-BD/pwa';
     6 +import settingDrawer from './bn-BD/settingDrawer';
     7 +import settings from './bn-BD/settings';
     8 + 
     9 +export default {
     10 + 'navBar.lang': 'ভাষা',
     11 + 'layout.user.link.help': 'সহায়তা',
     12 + 'layout.user.link.privacy': 'গোপনীয়তা',
     13 + 'layout.user.link.terms': 'শর্তাদি',
     14 + 'app.copyright.produced': 'প্রযোজনা করেছেন অ্যান্ট ফিনান্সিয়াল এক্সপেরিয়েন্স ডিপার্টমেন্ট',
     15 + 'app.preview.down.block': 'আপনার স্থানীয় প্রকল্পে এই পৃষ্ঠাটি ডাউনলোড করুন',
     16 + 'app.welcome.link.fetch-blocks': 'সমস্ত ব্লক পান',
     17 + 'app.welcome.link.block-list':
     18 + '`block` ডেভেলপমেন্ট এর উপর ভিত্তি করে দ্রুত স্ট্যান্ডার্ড, পৃষ্ঠাসমূহ তৈরি করুন।',
     19 + ...globalHeader,
     20 + ...menu,
     21 + ...settingDrawer,
     22 + ...settings,
     23 + ...pwa,
     24 + ...component,
     25 + ...pages,
     26 +};
     27 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/en-US/component.ts
     1 +export default {
     2 + 'component.tagSelect.expand': 'Expand',
     3 + 'component.tagSelect.collapse': 'Collapse',
     4 + 'component.tagSelect.all': 'All',
     5 +};
     6 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/en-US/globalHeader.ts
     1 +export default {
     2 + 'component.globalHeader.search': 'Search',
     3 + 'component.globalHeader.search.example1': 'Search example 1',
     4 + 'component.globalHeader.search.example2': 'Search example 2',
     5 + 'component.globalHeader.search.example3': 'Search example 3',
     6 + 'component.globalHeader.help': 'Help',
     7 + 'component.globalHeader.notification': 'Notification',
     8 + 'component.globalHeader.notification.empty': 'You have viewed all notifications.',
     9 + 'component.globalHeader.message': 'Message',
     10 + 'component.globalHeader.message.empty': 'You have viewed all messsages.',
     11 + 'component.globalHeader.event': 'Event',
     12 + 'component.globalHeader.event.empty': 'You have viewed all events.',
     13 + 'component.noticeIcon.clear': 'Clear',
     14 + 'component.noticeIcon.cleared': 'Cleared',
     15 + 'component.noticeIcon.empty': 'No notifications',
     16 + 'component.noticeIcon.view-more': 'View more',
     17 +};
     18 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/en-US/menu.ts
     1 +export default {
     2 + 'menu.welcome': 'Welcome',
     3 + 'menu.more-blocks': 'More Blocks',
     4 + 'menu.home': 'Home',
     5 + 'menu.admin': 'Admin',
     6 + 'menu.admin.sub-page': 'Sub-Page',
     7 + 'menu.login': 'Login',
     8 + 'menu.register': 'Register',
     9 + 'menu.register-result': 'Register Result',
     10 + 'menu.dashboard': 'Dashboard',
     11 + 'menu.dashboard.analysis': 'Analysis',
     12 + 'menu.dashboard.monitor': 'Monitor',
     13 + 'menu.dashboard.workplace': 'Workplace',
     14 + 'menu.exception.403': '403',
     15 + 'menu.exception.404': '404',
     16 + 'menu.exception.500': '500',
     17 + 'menu.form': 'Form',
     18 + 'menu.form.basic-form': 'Basic Form',
     19 + 'menu.form.step-form': 'Step Form',
     20 + 'menu.form.step-form.info': 'Step Form(write transfer information)',
     21 + 'menu.form.step-form.confirm': 'Step Form(confirm transfer information)',
     22 + 'menu.form.step-form.result': 'Step Form(finished)',
     23 + 'menu.form.advanced-form': 'Advanced Form',
     24 + 'menu.list': 'List',
     25 + 'menu.list.table-list': 'Search Table',
     26 + 'menu.list.basic-list': 'Basic List',
     27 + 'menu.list.card-list': 'Card List',
     28 + 'menu.list.search-list': 'Search List',
     29 + 'menu.list.search-list.articles': 'Search List(articles)',
     30 + 'menu.list.search-list.projects': 'Search List(projects)',
     31 + 'menu.list.search-list.applications': 'Search List(applications)',
     32 + 'menu.profile': 'Profile',
     33 + 'menu.profile.basic': 'Basic Profile',
     34 + 'menu.profile.advanced': 'Advanced Profile',
     35 + 'menu.result': 'Result',
     36 + 'menu.result.success': 'Success',
     37 + 'menu.result.fail': 'Fail',
     38 + 'menu.exception': 'Exception',
     39 + 'menu.exception.not-permission': '403',
     40 + 'menu.exception.not-find': '404',
     41 + 'menu.exception.server-error': '500',
     42 + 'menu.exception.trigger': 'Trigger',
     43 + 'menu.account': 'Account',
     44 + 'menu.account.center': 'Account Center',
     45 + 'menu.account.settings': 'Account Settings',
     46 + 'menu.account.trigger': 'Trigger Error',
     47 + 'menu.account.logout': 'Logout',
     48 + 'menu.editor': 'Graphic Editor',
     49 + 'menu.editor.flow': 'Flow Editor',
     50 + 'menu.editor.mind': 'Mind Editor',
     51 + 'menu.editor.koni': 'Koni Editor',
     52 +};
     53 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/en-US/pages.ts
     1 +export default {
     2 + 'pages.layouts.userLayout.title':
     3 + 'Ant Design is the most influential web design specification in Xihu district',
     4 + 'pages.login.accountLogin.tab': 'Account Login',
     5 + 'pages.login.accountLogin.errorMessage': 'Incorrect username/password(admin/ant.design)',
     6 + 'pages.login.failure': 'Login failed, please try again!',
     7 + 'pages.login.success': 'Login successful!',
     8 + 'pages.login.username.placeholder': 'Username: admin or user',
     9 + 'pages.login.username.required': 'Please input your username!',
     10 + 'pages.login.password.placeholder': 'Password: ant.design',
     11 + 'pages.login.password.required': 'Please input your password!',
     12 + 'pages.login.phoneLogin.tab': 'Phone Login',
     13 + 'pages.login.phoneLogin.errorMessage': 'Verification Code Error',
     14 + 'pages.login.phoneNumber.placeholder': 'Phone Number',
     15 + 'pages.login.phoneNumber.required': 'Please input your phone number!',
     16 + 'pages.login.phoneNumber.invalid': 'Phone number is invalid!',
     17 + 'pages.login.captcha.placeholder': 'Verification Code',
     18 + 'pages.login.captcha.required': 'Please input verification code!',
     19 + 'pages.login.phoneLogin.getVerificationCode': 'Get Code',
     20 + 'pages.getCaptchaSecondText': 'sec(s)',
     21 + 'pages.login.rememberMe': 'Remember me',
     22 + 'pages.login.forgotPassword': 'Forgot Password ?',
     23 + 'pages.login.submit': 'Login',
     24 + 'pages.login.loginWith': 'Login with :',
     25 + 'pages.login.registerAccount': 'Register Account',
     26 + 'pages.welcome.link': 'Welcome',
     27 + 'pages.welcome.alertMessage': 'Faster and stronger heavy-duty components have been released.',
     28 + 'pages.admin.subPage.title': 'This page can only be viewed by Admin',
     29 + 'pages.admin.subPage.alertMessage':
     30 + 'Umi ui is now released, welcome to use npm run ui to start the experience.',
     31 + 'pages.searchTable.createForm.newRule': 'New Rule',
     32 + 'pages.searchTable.updateForm.ruleConfig': 'Rule configuration',
     33 + 'pages.searchTable.updateForm.basicConfig': 'Basic Information',
     34 + 'pages.searchTable.updateForm.ruleName.nameLabel': 'Rule Name',
     35 + 'pages.searchTable.updateForm.ruleName.nameRules': 'Please enter the rule name!',
     36 + 'pages.searchTable.updateForm.ruleDesc.descLabel': 'Rule Description',
     37 + 'pages.searchTable.updateForm.ruleDesc.descPlaceholder': 'Please enter at least five characters',
     38 + 'pages.searchTable.updateForm.ruleDesc.descRules':
     39 + 'Please enter a rule description of at least five characters!',
     40 + 'pages.searchTable.updateForm.ruleProps.title': 'Configure Properties',
     41 + 'pages.searchTable.updateForm.object': 'Monitoring Object',
     42 + 'pages.searchTable.updateForm.ruleProps.templateLabel': 'Rule Template',
     43 + 'pages.searchTable.updateForm.ruleProps.typeLabel': 'Rule Type',
     44 + 'pages.searchTable.updateForm.schedulingPeriod.title': 'Set Scheduling Period',
     45 + 'pages.searchTable.updateForm.schedulingPeriod.timeLabel': 'Starting Time',
     46 + 'pages.searchTable.updateForm.schedulingPeriod.timeRules': 'Please choose a start time!',
     47 + 'pages.searchTable.titleDesc': 'Description',
     48 + 'pages.searchTable.ruleName': 'Rule name is required',
     49 + 'pages.searchTable.titleCallNo': 'Number of Service Calls',
     50 + 'pages.searchTable.titleStatus': 'Status',
     51 + 'pages.searchTable.nameStatus.default': 'default',
     52 + 'pages.searchTable.nameStatus.running': 'running',
     53 + 'pages.searchTable.nameStatus.online': 'online',
     54 + 'pages.searchTable.nameStatus.abnormal': 'abnormal',
     55 + 'pages.searchTable.titleUpdatedAt': 'Last Scheduled at',
     56 + 'pages.searchTable.exception': 'Please enter the reason for the exception!',
     57 + 'pages.searchTable.titleOption': 'Option',
     58 + 'pages.searchTable.config': 'Configuration',
     59 + 'pages.searchTable.subscribeAlert': 'Subscribe to alerts',
     60 + 'pages.searchTable.title': 'Enquiry Form',
     61 + 'pages.searchTable.new': 'New',
     62 + 'pages.searchTable.chosen': 'chosen',
     63 + 'pages.searchTable.item': 'item',
     64 + 'pages.searchTable.totalServiceCalls': 'Total Number of Service Calls',
     65 + 'pages.searchTable.tenThousand': '0000',
     66 + 'pages.searchTable.batchDeletion': 'bacth deletion',
     67 + 'pages.searchTable.batchApproval': 'batch approval',
     68 +};
     69 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/en-US/pwa.ts
     1 +export default {
     2 + 'app.pwa.offline': 'You are offline now',
     3 + 'app.pwa.serviceworker.updated': 'New content is available',
     4 + 'app.pwa.serviceworker.updated.hint': 'Please press the "Refresh" button to reload current page',
     5 + 'app.pwa.serviceworker.updated.ok': 'Refresh',
     6 +};
     7 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/en-US/settingDrawer.ts
     1 +export default {
     2 + 'app.setting.pagestyle': 'Page style setting',
     3 + 'app.setting.pagestyle.dark': 'Dark style',
     4 + 'app.setting.pagestyle.light': 'Light style',
     5 + 'app.setting.content-width': 'Content Width',
     6 + 'app.setting.content-width.fixed': 'Fixed',
     7 + 'app.setting.content-width.fluid': 'Fluid',
     8 + 'app.setting.themecolor': 'Theme Color',
     9 + 'app.setting.themecolor.dust': 'Dust Red',
     10 + 'app.setting.themecolor.volcano': 'Volcano',
     11 + 'app.setting.themecolor.sunset': 'Sunset Orange',
     12 + 'app.setting.themecolor.cyan': 'Cyan',
     13 + 'app.setting.themecolor.green': 'Polar Green',
     14 + 'app.setting.themecolor.daybreak': 'Daybreak Blue (default)',
     15 + 'app.setting.themecolor.geekblue': 'Geek Glue',
     16 + 'app.setting.themecolor.purple': 'Golden Purple',
     17 + 'app.setting.navigationmode': 'Navigation Mode',
     18 + 'app.setting.sidemenu': 'Side Menu Layout',
     19 + 'app.setting.topmenu': 'Top Menu Layout',
     20 + 'app.setting.fixedheader': 'Fixed Header',
     21 + 'app.setting.fixedsidebar': 'Fixed Sidebar',
     22 + 'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout',
     23 + 'app.setting.hideheader': 'Hidden Header when scrolling',
     24 + 'app.setting.hideheader.hint': 'Works when Hidden Header is enabled',
     25 + 'app.setting.othersettings': 'Other Settings',
     26 + 'app.setting.weakmode': 'Weak Mode',
     27 + 'app.setting.copy': 'Copy Setting',
     28 + 'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/models/setting.js',
     29 + 'app.setting.production.hint':
     30 + 'Setting panel shows in development environment only, please manually modify',
     31 +};
     32 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/en-US/settings.ts
     1 +export default {
     2 + 'app.settings.menuMap.basic': 'Basic Settings',
     3 + 'app.settings.menuMap.security': 'Security Settings',
     4 + 'app.settings.menuMap.binding': 'Account Binding',
     5 + 'app.settings.menuMap.notification': 'New Message Notification',
     6 + 'app.settings.basic.avatar': 'Avatar',
     7 + 'app.settings.basic.change-avatar': 'Change avatar',
     8 + 'app.settings.basic.email': 'Email',
     9 + 'app.settings.basic.email-message': 'Please input your email!',
     10 + 'app.settings.basic.nickname': 'Nickname',
     11 + 'app.settings.basic.nickname-message': 'Please input your Nickname!',
     12 + 'app.settings.basic.profile': 'Personal profile',
     13 + 'app.settings.basic.profile-message': 'Please input your personal profile!',
     14 + 'app.settings.basic.profile-placeholder': 'Brief introduction to yourself',
     15 + 'app.settings.basic.country': 'Country/Region',
     16 + 'app.settings.basic.country-message': 'Please input your country!',
     17 + 'app.settings.basic.geographic': 'Province or city',
     18 + 'app.settings.basic.geographic-message': 'Please input your geographic info!',
     19 + 'app.settings.basic.address': 'Street Address',
     20 + 'app.settings.basic.address-message': 'Please input your address!',
     21 + 'app.settings.basic.phone': 'Phone Number',
     22 + 'app.settings.basic.phone-message': 'Please input your phone!',
     23 + 'app.settings.basic.update': 'Update Information',
     24 + 'app.settings.security.strong': 'Strong',
     25 + 'app.settings.security.medium': 'Medium',
     26 + 'app.settings.security.weak': 'Weak',
     27 + 'app.settings.security.password': 'Account Password',
     28 + 'app.settings.security.password-description': 'Current password strength',
     29 + 'app.settings.security.phone': 'Security Phone',
     30 + 'app.settings.security.phone-description': 'Bound phone',
     31 + 'app.settings.security.question': 'Security Question',
     32 + 'app.settings.security.question-description':
     33 + 'The security question is not set, and the security policy can effectively protect the account security',
     34 + 'app.settings.security.email': 'Backup Email',
     35 + 'app.settings.security.email-description': 'Bound Email',
     36 + 'app.settings.security.mfa': 'MFA Device',
     37 + 'app.settings.security.mfa-description':
     38 + 'Unbound MFA device, after binding, can be confirmed twice',
     39 + 'app.settings.security.modify': 'Modify',
     40 + 'app.settings.security.set': 'Set',
     41 + 'app.settings.security.bind': 'Bind',
     42 + 'app.settings.binding.taobao': 'Binding Taobao',
     43 + 'app.settings.binding.taobao-description': 'Currently unbound Taobao account',
     44 + 'app.settings.binding.alipay': 'Binding Alipay',
     45 + 'app.settings.binding.alipay-description': 'Currently unbound Alipay account',
     46 + 'app.settings.binding.dingding': 'Binding DingTalk',
     47 + 'app.settings.binding.dingding-description': 'Currently unbound DingTalk account',
     48 + 'app.settings.binding.bind': 'Bind',
     49 + 'app.settings.notification.password': 'Account Password',
     50 + 'app.settings.notification.password-description':
     51 + 'Messages from other users will be notified in the form of a station letter',
     52 + 'app.settings.notification.messages': 'System Messages',
     53 + 'app.settings.notification.messages-description':
     54 + 'System messages will be notified in the form of a station letter',
     55 + 'app.settings.notification.todo': 'To-do Notification',
     56 + 'app.settings.notification.todo-description':
     57 + 'The to-do list will be notified in the form of a letter from the station',
     58 + 'app.settings.open': 'Open',
     59 + 'app.settings.close': 'Close',
     60 +};
     61 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/en-US.ts
     1 +import component from './en-US/component';
     2 +import globalHeader from './en-US/globalHeader';
     3 +import menu from './en-US/menu';
     4 +import pages from './en-US/pages';
     5 +import pwa from './en-US/pwa';
     6 +import settingDrawer from './en-US/settingDrawer';
     7 +import settings from './en-US/settings';
     8 + 
     9 +export default {
     10 + 'navBar.lang': 'Languages',
     11 + 'layout.user.link.help': 'Help',
     12 + 'layout.user.link.privacy': 'Privacy',
     13 + 'layout.user.link.terms': 'Terms',
     14 + 'app.copyright.produced': 'Produced by Ant Financial Experience Department',
     15 + 'app.preview.down.block': 'Download this page to your local project',
     16 + 'app.welcome.link.fetch-blocks': 'Get all block',
     17 + 'app.welcome.link.block-list': 'Quickly build standard, pages based on `block` development',
     18 + ...globalHeader,
     19 + ...menu,
     20 + ...settingDrawer,
     21 + ...settings,
     22 + ...pwa,
     23 + ...component,
     24 + ...pages,
     25 +};
     26 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/fa-IR/component.ts
     1 +export default {
     2 + 'component.tagSelect.expand': 'باز',
     3 + 'component.tagSelect.collapse': 'بسته ',
     4 + 'component.tagSelect.all': 'همه',
     5 +};
     6 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/fa-IR/globalHeader.ts
     1 +export default {
     2 + 'component.globalHeader.search': 'جستجو ',
     3 + 'component.globalHeader.search.example1': 'مثال 1 را جستجو کنید',
     4 + 'component.globalHeader.search.example2': 'مثال 2 را جستجو کنید',
     5 + 'component.globalHeader.search.example3': 'مثال 3 را جستجو کنید',
     6 + 'component.globalHeader.help': 'کمک',
     7 + 'component.globalHeader.notification': 'اعلان',
     8 + 'component.globalHeader.notification.empty': 'شما همه اعلان ها را مشاهده کرده اید.',
     9 + 'component.globalHeader.message': 'پیام',
     10 + 'component.globalHeader.message.empty': 'شما همه پیام ها را مشاهده کرده اید.',
     11 + 'component.globalHeader.event': 'رویداد',
     12 + 'component.globalHeader.event.empty': 'شما همه رویدادها را مشاهده کرده اید.',
     13 + 'component.noticeIcon.clear': 'پاک کردن',
     14 + 'component.noticeIcon.cleared': 'پاک شد',
     15 + 'component.noticeIcon.empty': 'بدون اعلان',
     16 + 'component.noticeIcon.view-more': 'نمایش بیشتر',
     17 +};
     18 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/fa-IR/menu.ts
     1 +export default {
     2 + 'menu.welcome': 'خوش آمدید',
     3 + 'menu.more-blocks': 'بلوک های بیشتر',
     4 + 'menu.home': 'خانه',
     5 + 'menu.admin': 'مدیر',
     6 + 'menu.admin.sub-page': 'زیر صفحه',
     7 + 'menu.login': 'ورود',
     8 + 'menu.register': 'ثبت نام',
     9 + 'menu.register-result': 'ثبت نام نتیجه',
     10 + 'menu.dashboard': 'داشبورد',
     11 + 'menu.dashboard.analysis': 'تحلیل و بررسی',
     12 + 'menu.dashboard.monitor': 'نظارت',
     13 + 'menu.dashboard.workplace': 'محل کار',
     14 + 'menu.exception.403': '403',
     15 + 'menu.exception.404': '404',
     16 + 'menu.exception.500': '500',
     17 + 'menu.form': 'فرم',
     18 + 'menu.form.basic-form': 'فرم اساسی',
     19 + 'menu.form.step-form': 'فرم مرحله',
     20 + 'menu.form.step-form.info': 'فرم مرحله (نوشتن اطلاعات انتقال)',
     21 + 'menu.form.step-form.confirm': 'فرم مرحله (تأیید اطلاعات انتقال)',
     22 + 'menu.form.step-form.result': 'فرم مرحله (تمام شده)',
     23 + 'menu.form.advanced-form': 'فرم پیشرفته',
     24 + 'menu.list': 'لیست',
     25 + 'menu.list.table-list': 'جدول جستجو',
     26 + 'menu.list.basic-list': 'لیست اصلی',
     27 + 'menu.list.card-list': 'لیست کارت',
     28 + 'menu.list.search-list': 'لیست جستجو',
     29 + 'menu.list.search-list.articles': 'لیست جستجو (مقالات)',
     30 + 'menu.list.search-list.projects': 'لیست جستجو (پروژه ها)',
     31 + 'menu.list.search-list.applications': 'لیست جستجو (برنامه ها)',
     32 + 'menu.profile': 'مشخصات',
     33 + 'menu.profile.basic': 'مشخصات عمومی',
     34 + 'menu.profile.advanced': 'مشخصات پیشرفته',
     35 + 'menu.result': 'نتیجه',
     36 + 'menu.result.success': 'موفق',
     37 + 'menu.result.fail': 'ناموفق',
     38 + 'menu.exception': 'استثنا',
     39 + 'menu.exception.not-permission': '403',
     40 + 'menu.exception.not-find': '404',
     41 + 'menu.exception.server-error': '500',
     42 + 'menu.exception.trigger': 'راه اندازی',
     43 + 'menu.account': 'حساب',
     44 + 'menu.account.center': 'مرکز حساب',
     45 + 'menu.account.settings': 'تنظیمات حساب',
     46 + 'menu.account.trigger': 'خطای راه اندازی',
     47 + 'menu.account.logout': 'خروج',
     48 + 'menu.editor': 'ویرایشگر گرافیک',
     49 + 'menu.editor.flow': 'ویرایشگر جریان',
     50 + 'menu.editor.mind': 'ویرایشگر ذهن',
     51 + 'menu.editor.koni': 'ویرایشگر Koni',
     52 +};
     53 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/fa-IR/pages.ts
     1 +export default {
     2 + 'pages.layouts.userLayout.title': 'طراحی مورچه تأثیرگذارترین مشخصات طراحی وب در منطقه Xihu است',
     3 + 'pages.login.accountLogin.tab': 'ورود به حساب کاربری',
     4 + 'pages.login.accountLogin.errorMessage': 'نام کاربری / رمزعبور نادرست (مدیر / ant.design)',
     5 + 'pages.login.username.placeholder': 'نام کاربری: مدیر یا کاربر',
     6 + 'pages.login.username.required': 'لطفا نام کاربری خود را وارد کنید!',
     7 + 'pages.login.password.placeholder': 'رمز عبور: ant.design',
     8 + 'pages.login.password.required': 'لطفاً رمز ورود خود را وارد کنید!',
     9 + 'pages.login.phoneLogin.tab': 'ورود به سیستم تلفن',
     10 + 'pages.login.phoneLogin.errorMessage': 'خطای کد تأیید',
     11 + 'pages.login.phoneNumber.placeholder': 'شماره تلفن',
     12 + 'pages.login.phoneNumber.required': 'لطفاً شماره تلفن خود را وارد کنید!',
     13 + 'pages.login.phoneNumber.invalid': 'شماره تلفن نامعتبر است!',
     14 + 'pages.login.captcha.placeholder': 'کد تایید',
     15 + 'pages.login.captcha.required': 'لطفا کد تأیید را وارد کنید!',
     16 + 'pages.login.phoneLogin.getVerificationCode': 'دریافت کد',
     17 + 'pages.getCaptchaSecondText': 'ثانیه',
     18 + 'pages.login.rememberMe': 'مرا به خاطر بسپار',
     19 + 'pages.login.forgotPassword': 'رمز عبور را فراموش کرده اید ?',
     20 + 'pages.login.submit': 'ارسال',
     21 + 'pages.login.loginWith': 'وارد شوید با :',
     22 + 'pages.login.registerAccount': 'ثبت نام',
     23 + 'pages.welcome.link': 'خوش آمدید',
     24 + 'pages.welcome.alertMessage': 'اجزای سنگین تر سریعتر و قوی تر آزاد شده اند.',
     25 + 'pages.admin.subPage.title': 'این صفحه فقط توسط مدیر قابل مشاهده است',
     26 + 'pages.admin.subPage.alertMessage':
     27 + 'رابط کاربری Umi اکنون منتشر شده است ، برای شروع تجربه استفاده از npm run ui خوش آمدید.',
     28 + 'pages.searchTable.createForm.newRule': 'قانون جدید',
     29 + 'pages.searchTable.updateForm.ruleConfig': 'پیکربندی قانون',
     30 + 'pages.searchTable.updateForm.basicConfig': 'اطلاعات اولیه',
     31 + 'pages.searchTable.updateForm.ruleName.nameLabel': ' نام قانون',
     32 + 'pages.searchTable.updateForm.ruleName.nameRules': 'لطفاً نام قانون را وارد کنید!',
     33 + 'pages.searchTable.updateForm.ruleDesc.descLabel': 'شرح قانون',
     34 + 'pages.searchTable.updateForm.ruleDesc.descPlaceholder': 'لطفاً حداقل پنج حرف وارد کنید',
     35 + 'pages.searchTable.updateForm.ruleDesc.descRules':
     36 + 'لطفاً حداقل یک قانون حاوی پنج کاراکتر شرح دهید!',
     37 + 'pages.searchTable.updateForm.ruleProps.title': 'پیکربندی خصوصیات',
     38 + 'pages.searchTable.updateForm.object': 'نظارت بر شی',
     39 + 'pages.searchTable.updateForm.ruleProps.templateLabel': 'الگوی قانون',
     40 + 'pages.searchTable.updateForm.ruleProps.typeLabel': 'نوع قانون',
     41 + 'pages.searchTable.updateForm.schedulingPeriod.title': 'تنظیم دوره زمان بندی',
     42 + 'pages.searchTable.updateForm.schedulingPeriod.timeLabel': 'زمان شروع',
     43 + 'pages.searchTable.updateForm.schedulingPeriod.timeRules': 'لطفاً زمان شروع را انتخاب کنید!',
     44 + 'pages.searchTable.titleDesc': 'شرح',
     45 + 'pages.searchTable.ruleName': 'نام قانون لازم است',
     46 + 'pages.searchTable.titleCallNo': 'تعداد تماس های خدماتی',
     47 + 'pages.searchTable.titleStatus': 'وضعیت',
     48 + 'pages.searchTable.nameStatus.default': 'پیش فرض',
     49 + 'pages.searchTable.nameStatus.running': 'در حال دویدن',
     50 + 'pages.searchTable.nameStatus.online': 'برخط',
     51 + 'pages.searchTable.nameStatus.abnormal': 'غیرطبیعی',
     52 + 'pages.searchTable.titleUpdatedAt': 'آخرین برنامه ریزی در',
     53 + 'pages.searchTable.exception': 'لطفا دلیل استثنا را وارد کنید!',
     54 + 'pages.searchTable.titleOption': 'گزینه',
     55 + 'pages.searchTable.config': 'پیکربندی',
     56 + 'pages.searchTable.subscribeAlert': 'مشترک شدن در هشدارها',
     57 + 'pages.searchTable.title': 'فرم درخواست',
     58 + 'pages.searchTable.new': 'جدید',
     59 + 'pages.searchTable.chosen': 'انتخاب شده',
     60 + 'pages.searchTable.item': 'مورد',
     61 + 'pages.searchTable.totalServiceCalls': 'تعداد کل تماس های خدماتی',
     62 + 'pages.searchTable.tenThousand': '0000',
     63 + 'pages.searchTable.batchDeletion': 'حذف دسته ای',
     64 + 'pages.searchTable.batchApproval': 'تصویب دسته ای',
     65 +};
     66 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/fa-IR/pwa.ts
     1 +export default {
     2 + 'app.pwa.offline': 'شما اکنون آفلاین هستید',
     3 + 'app.pwa.serviceworker.updated': 'مطالب جدید در دسترس است',
     4 + 'app.pwa.serviceworker.updated.hint':
     5 + 'لطفاً برای بارگیری مجدد صفحه فعلی ، دکمه "تازه سازی" را فشار دهید',
     6 + 'app.pwa.serviceworker.updated.ok': 'تازه سازی',
     7 +};
     8 + 
  • ■ ■ ■ ■ ■ ■
    web/src/locales/fa-IR/settingDrawer.ts
     1 +export default {
     2 + 'app.setting.pagestyle': 'تنظیم نوع صفحه',
     3 + 'app.setting.pagestyle.dark': 'سبک تیره',
     4 + 'app.setting.pagestyle.light': 'سبک سبک',
     5 + 'app.setting.content-width': 'عرض محتوا',
     6 + 'app.setting.content-width.fixed': 'ثابت',
     7 + 'app.setting.content-width.fluid': 'شناور',
     8 + 'app.setting.themecolor': 'رنگ تم',
     9 + 'app.setting.themecolor.dust': 'گرد و غبار قرمز',
     10 + 'app.setting.themecolor.volcano': 'آتشفشان',
     11 + 'app.setting.themecolor.sunset': 'غروب نارنجی',
     12 + 'app.setting.themecolor.cyan': 'فیروزه ای',
     13 + 'app.setting.themecolor.green': 'سبز قطبی',
     14 + 'app.setting.themecolor.daybreak': 'آبی روشن(پیشفرض)',
     15 + 'app.setting.themecolor.geekblue': 'چسب گیک',
     16 + 'app.setting.themecolor.purple': 'بنفش طلایی',
     17 + 'app.setting.navigationmode': 'حالت پیمایش',
     18 + 'app.setting.sidemenu': 'طرح منوی کناری',
     19 + 'app.setting.topmenu': 'طرح منوی بالایی',
     20 + 'app.setting.fixedheader': 'سرصفحه ثابت',
     21 + 'app.setting.fixedsidebar': 'نوار کناری ثابت',
     22 + 'app.setting.fixedsidebar.hint': 'کار بر روی منوی کناری',
     23 + 'app.setting.hideheader': 'هدر پنهان هنگام پیمایش',
     24 + 'app.setting.hideheader.hint': 'وقتی Hidden Header فعال باشد کار می کند',
     25 + 'app.setting.othersettings': 'تنظیمات دیگر',
     26 + 'app.setting.weakmode': 'حالت ضعیف',
     27 + 'app.setting.copy': 'تنظیمات کپی',
     28 + 'app.setting.copyinfo':
     29 + 'موفقیت در کپی کردن , لطفا defaultSettings را در src / models / setting.js جایگزین کنید',
     30 + 'app.setting.production.hint':
     31 + 'صفحه تنظیم فقط در محیط توسعه نمایش داده می شود ، لطفاً دستی تغییر دهید',
     32 +};
     33 + 
Please wait...
Page is in error, reload to recover