diff --git a/internal/apps/init.go b/internal/apps/init.go
index e481ccb7..f9cca706 100644
--- a/internal/apps/init.go
+++ b/internal/apps/init.go
@@ -8,6 +8,7 @@ import (
_ "github.com/TheTNB/panel/internal/apps/fail2ban"
_ "github.com/TheTNB/panel/internal/apps/frp"
_ "github.com/TheTNB/panel/internal/apps/gitea"
+ _ "github.com/TheTNB/panel/internal/apps/memcached"
_ "github.com/TheTNB/panel/internal/apps/mysql"
_ "github.com/TheTNB/panel/internal/apps/nginx"
_ "github.com/TheTNB/panel/internal/apps/php"
diff --git a/internal/apps/memcached/init.go b/internal/apps/memcached/init.go
new file mode 100644
index 00000000..dc1ecc06
--- /dev/null
+++ b/internal/apps/memcached/init.go
@@ -0,0 +1,20 @@
+package memcached
+
+import (
+ "github.com/go-chi/chi/v5"
+
+ "github.com/TheTNB/panel/pkg/apploader"
+ "github.com/TheTNB/panel/pkg/types"
+)
+
+func init() {
+ apploader.Register(&types.App{
+ Slug: "memcached",
+ Route: func(r chi.Router) {
+ service := NewService()
+ r.Get("/load", service.Load)
+ r.Get("/config", service.GetConfig)
+ r.Post("/config", service.UpdateConfig)
+ },
+ })
+}
diff --git a/internal/apps/memcached/request.go b/internal/apps/memcached/request.go
new file mode 100644
index 00000000..150b2ca8
--- /dev/null
+++ b/internal/apps/memcached/request.go
@@ -0,0 +1,5 @@
+package memcached
+
+type UpdateConfig struct {
+ Config string `form:"config" json:"config" validate:"required"`
+}
diff --git a/internal/apps/memcached/service.go b/internal/apps/memcached/service.go
new file mode 100644
index 00000000..6e6d7290
--- /dev/null
+++ b/internal/apps/memcached/service.go
@@ -0,0 +1,97 @@
+package memcached
+
+import (
+ "bufio"
+ "net"
+ "net/http"
+ "regexp"
+
+ "github.com/TheTNB/panel/internal/service"
+ "github.com/TheTNB/panel/pkg/io"
+ "github.com/TheTNB/panel/pkg/systemctl"
+ "github.com/TheTNB/panel/pkg/types"
+)
+
+type Service struct{}
+
+func NewService() *Service {
+ return &Service{}
+}
+
+func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
+ status, err := systemctl.Status("memcached")
+ if err != nil {
+ service.Error(w, http.StatusInternalServerError, "failed to get Memcached status: %v", err)
+ return
+ }
+ if !status {
+ service.Success(w, []types.NV{})
+ return
+ }
+
+ conn, err := net.Dial("tcp", "127.0.0.1:11211")
+ if err != nil {
+ service.Success(w, []types.NV{})
+ return
+ }
+ defer conn.Close()
+
+ _, err = conn.Write([]byte("stats\nquit\n"))
+ if err != nil {
+ service.Error(w, http.StatusInternalServerError, "failed to write to Memcached: %v", err)
+ return
+ }
+
+ data := make([]types.NV, 0)
+ re := regexp.MustCompile(`STAT\s(\S+)\s(\S+)`)
+ scanner := bufio.NewScanner(conn)
+ for scanner.Scan() {
+ line := scanner.Text()
+ if matches := re.FindStringSubmatch(line); matches != nil && len(matches) == 3 {
+ data = append(data, types.NV{
+ Name: matches[1],
+ Value: matches[2],
+ })
+ }
+ if line == "END" {
+ break
+ }
+ }
+
+ if err = scanner.Err(); err != nil {
+ service.Error(w, http.StatusInternalServerError, "failed to read from Memcached: %v", err)
+ return
+ }
+
+ service.Success(w, data)
+}
+
+func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) {
+ config, err := io.Read("/etc/systemd/system/memcached.service")
+ if err != nil {
+ service.Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ service.Success(w, config)
+}
+
+func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) {
+ req, err := service.Bind[UpdateConfig](r)
+ if err != nil {
+ service.Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+
+ if err = io.Write("/etc/systemd/system/memcached.service", req.Config, 0644); err != nil {
+ service.Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ if err = systemctl.Restart("memcached"); err != nil {
+ service.Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ service.Success(w, nil)
+}
diff --git a/web/src/api/apps/memcached/index.ts b/web/src/api/apps/memcached/index.ts
new file mode 100644
index 00000000..209f6370
--- /dev/null
+++ b/web/src/api/apps/memcached/index.ts
@@ -0,0 +1,8 @@
+import { http } from '@/utils'
+
+// 负载状态
+export const getLoad = () => http.Get('/apps/memcached/load')
+// 获取配置
+export const getConfig = () => http.Get('/apps/memcached/config')
+// 保存配置
+export const updateConfig = (config: string) => http.Post('/apps/memcached/config', { config })
diff --git a/web/src/views/app/IndexView.vue b/web/src/views/app/IndexView.vue
index f2433070..27e82dfe 100644
--- a/web/src/views/app/IndexView.vue
+++ b/web/src/views/app/IndexView.vue
@@ -79,94 +79,96 @@ const columns: any = [
{
justify: 'center'
},
- [
- row.installed && row.update_exist
- ? h(
- NPopconfirm,
- {
- onPositiveClick: () => handleUpdate(row.slug)
- },
- {
- default: () => {
- return t('appIndex.confirm.update', { app: row.name })
+ {
+ default: () => [
+ row.installed && row.update_exist
+ ? h(
+ NPopconfirm,
+ {
+ onPositiveClick: () => handleUpdate(row.slug)
},
- trigger: () => {
- return h(
- NButton,
- {
- size: 'small',
- type: 'warning'
- },
- {
- default: () => t('appIndex.buttons.update'),
- icon: renderIcon('material-symbols:arrow-circle-up-outline-rounded', {
- size: 14
- })
- }
- )
+ {
+ default: () => {
+ return t('appIndex.confirm.update', { app: row.name })
+ },
+ trigger: () => {
+ return h(
+ NButton,
+ {
+ size: 'small',
+ type: 'warning'
+ },
+ {
+ default: () => t('appIndex.buttons.update'),
+ icon: renderIcon('material-symbols:arrow-circle-up-outline-rounded', {
+ size: 14
+ })
+ }
+ )
+ }
}
- }
- )
- : null,
- row.installed
- ? h(
- NButton,
- {
- size: 'small',
- type: 'success',
- onClick: () => handleManage(row.slug)
- },
- {
- default: () => t('appIndex.buttons.manage'),
- icon: renderIcon('material-symbols:settings-outline', { size: 14 })
- }
- )
- : null,
- row.installed
- ? h(
- NPopconfirm,
- {
- onPositiveClick: () => handleUninstall(row.slug)
- },
- {
- default: () => {
- return t('appIndex.confirm.uninstall', { app: row.name })
+ )
+ : null,
+ row.installed
+ ? h(
+ NButton,
+ {
+ size: 'small',
+ type: 'success',
+ onClick: () => handleManage(row.slug)
},
- trigger: () => {
- return h(
- NButton,
- {
- size: 'small',
- type: 'error'
- },
- {
- default: () => t('appIndex.buttons.uninstall'),
- icon: renderIcon('material-symbols:delete-outline', { size: 14 })
- }
- )
+ {
+ default: () => t('appIndex.buttons.manage'),
+ icon: renderIcon('material-symbols:settings-outline', { size: 14 })
}
- }
- )
- : null,
- !row.installed
- ? h(
- NButton,
- {
- size: 'small',
- type: 'info',
- onClick: () => {
- versionModalShow.value = true
- versionModalOperation.value = '安装'
- versionModalInfo.value = row
+ )
+ : null,
+ row.installed
+ ? h(
+ NPopconfirm,
+ {
+ onPositiveClick: () => handleUninstall(row.slug)
+ },
+ {
+ default: () => {
+ return t('appIndex.confirm.uninstall', { app: row.name })
+ },
+ trigger: () => {
+ return h(
+ NButton,
+ {
+ size: 'small',
+ type: 'error'
+ },
+ {
+ default: () => t('appIndex.buttons.uninstall'),
+ icon: renderIcon('material-symbols:delete-outline', { size: 14 })
+ }
+ )
+ }
}
- },
- {
- default: () => t('appIndex.buttons.install'),
- icon: renderIcon('material-symbols:download-rounded', { size: 14 })
- }
- )
- : null
- ]
+ )
+ : null,
+ !row.installed
+ ? h(
+ NButton,
+ {
+ size: 'small',
+ type: 'info',
+ onClick: () => {
+ versionModalShow.value = true
+ versionModalOperation.value = '安装'
+ versionModalInfo.value = row
+ }
+ },
+ {
+ default: () => t('appIndex.buttons.install'),
+ icon: renderIcon('material-symbols:download-rounded', { size: 14 })
+ }
+ )
+ : null
+ ]
+ }
)
}
}
diff --git a/web/src/views/apps/memcached/IndexView.vue b/web/src/views/apps/memcached/IndexView.vue
new file mode 100644
index 00000000..64f20f22
--- /dev/null
+++ b/web/src/views/apps/memcached/IndexView.vue
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+ 保存
+
+
+
+
+
+
+
+
+ 自启动开
+ 自启动关
+
+
+
+
+ {{ statusStr }}
+
+
+
+
+ 启动
+
+
+
+
+
+ 停止
+
+
+ 停止 Memcached 会导致使用 Memcached 的网站无法访问,确定要停止吗?
+
+
+
+ 重启
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/apps/memcached/route.ts b/web/src/views/apps/memcached/route.ts
new file mode 100644
index 00000000..5e19e24f
--- /dev/null
+++ b/web/src/views/apps/memcached/route.ts
@@ -0,0 +1,23 @@
+import type { RouteType } from '~/types/router'
+
+const Layout = () => import('@/layout/IndexView.vue')
+
+export default {
+ name: 'memcached',
+ path: '/apps/memcached',
+ component: Layout,
+ isHidden: true,
+ children: [
+ {
+ name: 'apps-memcached-index',
+ path: '',
+ component: () => import('./IndexView.vue'),
+ meta: {
+ title: 'Memcached',
+ icon: 'logos:memcached',
+ role: ['admin'],
+ requireAuth: true
+ }
+ }
+ ]
+} as RouteType