mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-31 14:08:06 +08:00
style: 增加应用商店列表和同步功能
This commit is contained in:
parent
12dc630c89
commit
367623293c
26
apps/list.json
Normal file
26
apps/list.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"version": "0.1",
|
||||
"tags": [
|
||||
{
|
||||
"key": "WebSite",
|
||||
"name": "网站"
|
||||
},
|
||||
{
|
||||
"key": "Datastore",
|
||||
"name": "数据库"
|
||||
}
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"key": "mysql",
|
||||
"name": "Mysql",
|
||||
"tags": ["Datastore"],
|
||||
"versions": ["5.7.39","8.0.30"],
|
||||
"short_desc": "常用关系型数据库",
|
||||
"icon": "mysql.png",
|
||||
"author": "Oracle",
|
||||
"type": "internal",
|
||||
"source": "https://www.mysql.com"
|
||||
}
|
||||
]
|
||||
}
|
6
apps/mysql/5.7.39/.env
Normal file
6
apps/mysql/5.7.39/.env
Normal file
@ -0,0 +1,6 @@
|
||||
TZ=Asia/Shanghai
|
||||
DATABASE=db
|
||||
USER=mysql
|
||||
PASSWORD=1qaz@WSX
|
||||
ROOT_PASSWORD=1panel@mysql
|
||||
PORT=3306
|
20
apps/mysql/5.7.39/README.md
Normal file
20
apps/mysql/5.7.39/README.md
Normal file
@ -0,0 +1,20 @@
|
||||
Copyright (c) 2000, 2022, Oracle and/or its affiliates.
|
||||
|
||||
This is a release of MySQL, an SQL database server.
|
||||
|
||||
License information can be found in the LICENSE file.
|
||||
|
||||
In test packages where this file is renamed README-test, the license
|
||||
file is renamed LICENSE-test.
|
||||
|
||||
This distribution may include materials developed by third parties.
|
||||
For license and attribution notices for these materials,
|
||||
please refer to the LICENSE file.
|
||||
|
||||
For further information on MySQL or additional documentation, visit
|
||||
http://dev.mysql.com/doc/
|
||||
|
||||
For additional downloads and the source of MySQL, visit
|
||||
http://dev.mysql.com/downloads/
|
||||
|
||||
MySQL is brought to you by the MySQL team at Oracle.
|
83
apps/mysql/5.7.39/conf/my.cnf
Normal file
83
apps/mysql/5.7.39/conf/my.cnf
Normal file
@ -0,0 +1,83 @@
|
||||
|
||||
[client]
|
||||
port = 3306
|
||||
socket = /var/run/mysqld/mysqld.sock
|
||||
|
||||
[mysqld_safe]
|
||||
socket = /var/run/mysqld/mysqld.sock
|
||||
nice = 0
|
||||
|
||||
[mysqld]
|
||||
user = mysql
|
||||
pid-file = /var/run/mysqld/mysqld.pid
|
||||
socket = /var/run/mysqld/mysqld.sock
|
||||
port = 3306
|
||||
basedir = /usr
|
||||
datadir = /var/lib/mysql
|
||||
tmpdir = /tmp
|
||||
lc-messages-dir = /usr/share/mysql
|
||||
skip-external-locking
|
||||
skip-character-set-client-handshake
|
||||
default-storage-engine = InnoDB
|
||||
character-set-server = utf8
|
||||
transaction-isolation = READ-COMMITTED
|
||||
|
||||
|
||||
bind-address = 127.0.0.1
|
||||
key_buffer = 16M
|
||||
max_allowed_packet = 16M
|
||||
thread_stack = 192K
|
||||
thread_cache_size = 16
|
||||
myisam-recover = BACKUP
|
||||
max_connections = 300
|
||||
table_open_cache = 64
|
||||
thread_concurrency = 10
|
||||
table_open_cache = 32
|
||||
thread_concurrency = 4
|
||||
|
||||
query_cache_type = 1
|
||||
query_cache_limit = 1M
|
||||
query_cache_size = 8M
|
||||
general_log_file = /var/log/mysql/mysql.log
|
||||
#general_log = 1
|
||||
log_error = /var/log/mysql/error.log
|
||||
|
||||
slow_query_log = 1
|
||||
slow_query_log_file = /var/log/mysql/mysql-slow.log
|
||||
long_query_time = 1
|
||||
#log-queries-not-using-indexes
|
||||
|
||||
|
||||
#server-id = 1
|
||||
#log_bin = /var/log/mysql/mysql-bin.log
|
||||
expire_logs_days = 14
|
||||
max_binlog_size = 1G
|
||||
#binlog_do_db = include_database_name
|
||||
#binlog_ignore_db = include_database_name
|
||||
|
||||
|
||||
# ssl-ca=/etc/mysql/cacert.pem
|
||||
# ssl-cert=/etc/mysql/server-cert.pem
|
||||
# ssl-key=/etc/mysql/server-key.pem
|
||||
innodb_data_file_path = ibdata1:128M:autoextend
|
||||
innodb_file_per_table = 1
|
||||
skip-innodb_doublewrite
|
||||
innodb_additional_mem_pool_size = 12M
|
||||
innodb_buffer_pool_size = 256M
|
||||
innodb_log_buffer_size = 8M
|
||||
innodb_log_file_size = 8M
|
||||
innodb_flush_log_at_trx_commit = 0
|
||||
innodb_flush_method = O_DIRECT
|
||||
innodb_support_xa = OFF
|
||||
|
||||
|
||||
[mysqldump]
|
||||
quick
|
||||
quote-names
|
||||
max_allowed_packet = 16M
|
||||
|
||||
[mysql]
|
||||
#no-auto-rehash # faster start of mysql but no tab completition
|
||||
|
||||
[isamchk]
|
||||
key_buffer = 16M
|
23
apps/mysql/5.7.39/docker-compose.yml
Normal file
23
apps/mysql/5.7.39/docker-compose.yml
Normal file
@ -0,0 +1,23 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
mysql5.7:
|
||||
image: mysql:5.7.39
|
||||
container_name: 1panel-mysql
|
||||
restart: always
|
||||
environment:
|
||||
TZ: ${TZ}
|
||||
MYSQL_DATABASE: ${DATABASE}
|
||||
MYSQL_USER: ${USER}
|
||||
MYSQL_PASSWORD: ${PASSWORD}
|
||||
MYSQL_ROOT_PASSWORD: ${ROOT_PASSWORD}
|
||||
ports:
|
||||
- ${PORT}:3306
|
||||
volumes:
|
||||
- ./data/:/var/lib/mysql
|
||||
- ./conf/my.cnf:/etc/mysql/my.cnf
|
||||
command:
|
||||
--character-set-server=utf8mb4
|
||||
--collation-server=utf8mb4_general_ci
|
||||
--explicit_defaults_for_timestamp=true
|
||||
--lower_case_table_names=1
|
52
apps/mysql/5.7.39/form.json
Normal file
52
apps/mysql/5.7.39/form.json
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
"form_fields": [
|
||||
{
|
||||
"type": "text",
|
||||
"label_zh": "时区",
|
||||
"label_en": "TimeZone",
|
||||
"required": true,
|
||||
"default": "Asia/Shanghai",
|
||||
"env_variable": "TZ"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label_zh": "数据库",
|
||||
"label_en": "Database",
|
||||
"required": true,
|
||||
"default": "db",
|
||||
"env_variable": "DATABASE"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label_zh": "普通用户",
|
||||
"label_en": "User",
|
||||
"required": true,
|
||||
"default": "mysql",
|
||||
"env_variable": "USER"
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"label_zh": "普通用户密码",
|
||||
"label_en": "Password",
|
||||
"required": true,
|
||||
"default": "1qaz@WSX",
|
||||
"env_variable": "PASSWORD"
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"label_zh": "Root用户密码",
|
||||
"label_en": "RootPassword",
|
||||
"required": true,
|
||||
"default": "1panel@mysql",
|
||||
"env_variable": "ROOT_PASSWORD"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"label_zh": "端口",
|
||||
"label_en": "Port",
|
||||
"required": true,
|
||||
"default": 3306,
|
||||
"env_variable": "PORT"
|
||||
}
|
||||
]
|
||||
}
|
@ -2,6 +2,10 @@ system:
|
||||
port: 9999
|
||||
db_type: sqlite
|
||||
level: debug
|
||||
data_dir: /opt/1Panel/data
|
||||
resource_dir: /opt/1Panel/data/resource
|
||||
app_dir: /opt/1Panel/data/apps
|
||||
app_oss:
|
||||
|
||||
mysql:
|
||||
path: localhost
|
||||
|
32
backend/app/api/v1/app.go
Normal file
32
backend/app/api/v1/app.go
Normal file
@ -0,0 +1,32 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/app/api/v1/helper"
|
||||
"github.com/1Panel-dev/1Panel/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/constant"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (b *BaseApi) AppSearch(c *gin.Context) {
|
||||
var req dto.AppRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||
return
|
||||
}
|
||||
|
||||
list, err := appService.Page(req)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.SuccessWithData(c, list)
|
||||
}
|
||||
|
||||
func (b *BaseApi) AppSync(c *gin.Context) {
|
||||
if err := appService.Sync(); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, "")
|
||||
}
|
@ -18,4 +18,5 @@ var (
|
||||
fileService = service.ServiceGroupApp.FileService
|
||||
cronjobService = service.ServiceGroupApp.CronjobService
|
||||
settingService = service.ServiceGroupApp.SettingService
|
||||
appService = service.ServiceGroupApp.AppService
|
||||
)
|
||||
|
58
backend/app/dto/app.go
Normal file
58
backend/app/dto/app.go
Normal file
@ -0,0 +1,58 @@
|
||||
package dto
|
||||
|
||||
import "github.com/1Panel-dev/1Panel/app/model"
|
||||
|
||||
type AppRes struct {
|
||||
Version string `json:"version"`
|
||||
CanUpdate bool `json:"canUpdate"`
|
||||
Items []*AppDTO `json:"items"`
|
||||
Tags []model.Tag `json:"tags"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type AppDTO struct {
|
||||
model.App
|
||||
Tags []model.Tag `json:"tags"`
|
||||
}
|
||||
|
||||
type AppList struct {
|
||||
Version string `json:"version"`
|
||||
Tags []Tag `json:"tags"`
|
||||
Items []AppDefine `json:"items"`
|
||||
}
|
||||
|
||||
type AppDefine struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
Tags []string `json:"tags"`
|
||||
Versions []string `json:"versions"`
|
||||
Icon string `json:"icon"`
|
||||
Author string `json:"author"`
|
||||
Source string `json:"source"`
|
||||
ShortDesc string `json:"short_desc"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type AppForm struct {
|
||||
FormFields []AppFormFields `json:"form_fields"`
|
||||
}
|
||||
|
||||
type AppFormFields struct {
|
||||
Type string `json:"type"`
|
||||
LabelZh string `json:"label_zh"`
|
||||
LabelEn string `json:"label_en"`
|
||||
Required string `json:"required"`
|
||||
Default string `json:"default"`
|
||||
EnvKey string `json:"env_variable"`
|
||||
}
|
||||
|
||||
type AppRequest struct {
|
||||
PageInfo
|
||||
Name string `json:"name"`
|
||||
Types []string `json:"types"`
|
||||
}
|
@ -92,7 +92,7 @@ type FileProcess struct {
|
||||
}
|
||||
|
||||
type FileProcessReq struct {
|
||||
Key string
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type FileProcessKeys struct {
|
||||
|
15
backend/app/model/app.go
Normal file
15
backend/app/model/app.go
Normal file
@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
type App struct {
|
||||
BaseModel
|
||||
Name string `json:"name" gorm:"type:varchar(64);not null"`
|
||||
Key string `json:"key" gorm:"type:varchar(64);not null;uniqueIndex"`
|
||||
ShortDesc string `json:"shortDesc" gorm:"type:longtext;"`
|
||||
Icon string `json:"icon" gorm:"type:longtext;"`
|
||||
Author string `json:"author" gorm:"type:varchar(64);not null"`
|
||||
Source string `json:"source" gorm:"type:varchar(64);not null"`
|
||||
Type string `json:"type" gorm:"type:varchar(64);not null" `
|
||||
Details []*AppDetail `json:"-"`
|
||||
TagsKey []string `json:"-" gorm:"-"`
|
||||
AppTags []AppTag `json:"-"`
|
||||
}
|
8
backend/app/model/app_config.go
Normal file
8
backend/app/model/app_config.go
Normal file
@ -0,0 +1,8 @@
|
||||
package model
|
||||
|
||||
type AppConfig struct {
|
||||
BaseModel
|
||||
Version string `json:"version"`
|
||||
OssPath string `json:"ossPath"`
|
||||
CanUpdate bool `json:"canUpdate"`
|
||||
}
|
10
backend/app/model/app_detail.go
Normal file
10
backend/app/model/app_detail.go
Normal file
@ -0,0 +1,10 @@
|
||||
package model
|
||||
|
||||
type AppDetail struct {
|
||||
BaseModel
|
||||
AppId uint `json:"appId" gorm:"type:integer;not null"`
|
||||
Version string `json:"version" gorm:"type:varchar(64);not null"`
|
||||
FormFields string `json:"formFields" gorm:"type:longtext;"`
|
||||
DockerCompose string `json:"dockerCompose" gorm:"type:longtext;not null"`
|
||||
Readme string `json:"readme" gorm:"type:longtext;not null"`
|
||||
}
|
7
backend/app/model/app_tag.go
Normal file
7
backend/app/model/app_tag.go
Normal file
@ -0,0 +1,7 @@
|
||||
package model
|
||||
|
||||
type AppTag struct {
|
||||
BaseModel
|
||||
AppId uint `json:"appId" gorm:"type:integer;not null"`
|
||||
TagId uint `json:"tagId" gorm:"type:integer;not null"`
|
||||
}
|
7
backend/app/model/tag.go
Normal file
7
backend/app/model/tag.go
Normal file
@ -0,0 +1,7 @@
|
||||
package model
|
||||
|
||||
type Tag struct {
|
||||
BaseModel
|
||||
Key string `json:"key" gorm:"type:varchar(64);not null"`
|
||||
Name string `json:"name" gorm:"type:varchar(64);not null"`
|
||||
}
|
61
backend/app/repo/app.go
Normal file
61
backend/app/repo/app.go
Normal file
@ -0,0 +1,61 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/1Panel-dev/1Panel/app/model"
|
||||
"github.com/1Panel-dev/1Panel/global"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type AppRepo struct {
|
||||
}
|
||||
|
||||
func (a AppRepo) WithInTypes(types []string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("type in (?)", types)
|
||||
}
|
||||
}
|
||||
|
||||
func (a AppRepo) Page(page, size int, opts ...DBOption) (int64, []model.App, error) {
|
||||
var apps []model.App
|
||||
db := global.DB.Model(&model.App{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
count := int64(0)
|
||||
db = db.Count(&count)
|
||||
err := db.Debug().Limit(size).Offset(size * (page - 1)).Preload("AppTags").Find(&apps).Error
|
||||
return count, apps, err
|
||||
}
|
||||
|
||||
func (a AppRepo) BatchCreate(ctx context.Context, apps []*model.App) error {
|
||||
db := ctx.Value("db").(*gorm.DB)
|
||||
return db.Omit(clause.Associations).Create(apps).Error
|
||||
}
|
||||
|
||||
func (a AppRepo) GetByKey(ctx context.Context, key string) (model.App, error) {
|
||||
db := ctx.Value("db").(*gorm.DB)
|
||||
var app model.App
|
||||
if err := db.Where("key = ?", key).First(&app).Error; err != nil {
|
||||
return app, err
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func (a AppRepo) Create(ctx context.Context, app *model.App) error {
|
||||
db := ctx.Value("db").(*gorm.DB)
|
||||
return db.Omit(clause.Associations).Create(app).Error
|
||||
}
|
||||
|
||||
func (a AppRepo) Save(ctx context.Context, app *model.App) error {
|
||||
db := ctx.Value("db").(*gorm.DB)
|
||||
return db.Omit(clause.Associations).Save(app).Error
|
||||
}
|
||||
|
||||
func (a AppRepo) UpdateAppConfig(ctx context.Context, app *model.AppConfig) error {
|
||||
db := ctx.Value("db").(*gorm.DB)
|
||||
return db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
}).Create(app).Error
|
||||
}
|
29
backend/app/repo/app_detail.go
Normal file
29
backend/app/repo/app_detail.go
Normal file
@ -0,0 +1,29 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/1Panel-dev/1Panel/app/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AppDetailRepo struct {
|
||||
}
|
||||
|
||||
func (a AppDetailRepo) BatchCreate(ctx context.Context, details []*model.AppDetail) error {
|
||||
db := ctx.Value("db").(*gorm.DB)
|
||||
return db.Model(&model.AppDetail{}).Create(&details).Error
|
||||
}
|
||||
|
||||
func (a AppDetailRepo) DeleteByAppIds(ctx context.Context, appIds []uint) error {
|
||||
db := ctx.Value("db").(*gorm.DB)
|
||||
return db.Where("app_id in (?)", appIds).Delete(&model.AppDetail{}).Error
|
||||
}
|
||||
|
||||
func (a AppDetailRepo) GetByAppId(ctx context.Context, appId string) ([]model.AppDetail, error) {
|
||||
db := ctx.Value("db").(*gorm.DB)
|
||||
var details []model.AppDetail
|
||||
if err := db.Where("app_id = ?", appId).Find(&details).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return details, nil
|
||||
}
|
29
backend/app/repo/app_tag.go
Normal file
29
backend/app/repo/app_tag.go
Normal file
@ -0,0 +1,29 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/1Panel-dev/1Panel/app/model"
|
||||
"github.com/1Panel-dev/1Panel/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AppTagRepo struct {
|
||||
}
|
||||
|
||||
func (a AppTagRepo) BatchCreate(ctx context.Context, tags []*model.AppTag) error {
|
||||
db := ctx.Value("db").(*gorm.DB)
|
||||
return db.Create(&tags).Error
|
||||
}
|
||||
|
||||
func (a AppTagRepo) DeleteByAppIds(ctx context.Context, appIds []uint) error {
|
||||
db := ctx.Value("db").(*gorm.DB)
|
||||
return db.Where("app_id in (?)", appIds).Delete(&model.AppTag{}).Error
|
||||
}
|
||||
|
||||
func (a AppTagRepo) GetByAppId(appId uint) ([]model.AppTag, error) {
|
||||
var appTags []model.AppTag
|
||||
if err := global.DB.Where("app_id = ?", appId).Find(&appTags).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return appTags, nil
|
||||
}
|
@ -9,6 +9,10 @@ type RepoGroup struct {
|
||||
CommonRepo
|
||||
CronjobRepo
|
||||
SettingRepo
|
||||
AppRepo
|
||||
AppTagRepo
|
||||
TagRepo
|
||||
AppDetailRepo
|
||||
}
|
||||
|
||||
var RepoGroupApp = new(RepoGroup)
|
||||
|
37
backend/app/repo/tag.go
Normal file
37
backend/app/repo/tag.go
Normal file
@ -0,0 +1,37 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/1Panel-dev/1Panel/app/model"
|
||||
"github.com/1Panel-dev/1Panel/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TagRepo struct {
|
||||
}
|
||||
|
||||
func (t TagRepo) BatchCreate(ctx context.Context, tags []*model.Tag) error {
|
||||
db := ctx.Value("db").(*gorm.DB)
|
||||
return db.Create(&tags).Error
|
||||
}
|
||||
|
||||
func (t TagRepo) DeleteAll(ctx context.Context) error {
|
||||
db := ctx.Value("db").(*gorm.DB)
|
||||
return db.Where("1 = 1 ").Delete(&model.Tag{}).Error
|
||||
}
|
||||
|
||||
func (t TagRepo) All() ([]model.Tag, error) {
|
||||
var tags []model.Tag
|
||||
if err := global.DB.Where("1 = 1 ").Find(&tags).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (t TagRepo) GetByIds(ids []uint) ([]model.Tag, error) {
|
||||
var tags []model.Tag
|
||||
if err := global.DB.Where("id in (?)", ids).Find(&tags).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tags, nil
|
||||
}
|
238
backend/app/service/app.go
Normal file
238
backend/app/service/app.go
Normal file
@ -0,0 +1,238 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"github.com/1Panel-dev/1Panel/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/app/model"
|
||||
"github.com/1Panel-dev/1Panel/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/global"
|
||||
"golang.org/x/net/context"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type AppService struct {
|
||||
}
|
||||
|
||||
func (a AppService) Page(req dto.AppRequest) (interface{}, error) {
|
||||
|
||||
var opts []repo.DBOption
|
||||
opts = append(opts, commonRepo.WithOrderBy("name"))
|
||||
if req.Name != "" {
|
||||
opts = append(opts, commonRepo.WithLikeName(req.Name))
|
||||
}
|
||||
if len(req.Types) != 0 {
|
||||
opts = append(opts, appRepo.WithInTypes(req.Types))
|
||||
}
|
||||
var res dto.AppRes
|
||||
total, apps, err := appRepo.Page(req.Page, req.PageSize, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var appDTOs []*dto.AppDTO
|
||||
for _, a := range apps {
|
||||
appDTO := &dto.AppDTO{
|
||||
App: a,
|
||||
}
|
||||
appDTOs = append(appDTOs, appDTO)
|
||||
appTags, err := appTagRepo.GetByAppId(a.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var tagIds []uint
|
||||
for _, at := range appTags {
|
||||
tagIds = append(tagIds, at.TagId)
|
||||
}
|
||||
tags, err := tagRepo.GetByIds(tagIds)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
appDTO.Tags = tags
|
||||
}
|
||||
res.Items = appDTOs
|
||||
res.Total = total
|
||||
tags, err := tagRepo.All()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.Tags = tags
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (a AppService) Sync() error {
|
||||
//TODO 从 oss 拉取最新列表
|
||||
var appConfig model.AppConfig
|
||||
appConfig.OssPath = global.CONF.System.AppOss
|
||||
|
||||
appDir := path.Join(global.CONF.System.ResourceDir, "apps")
|
||||
iconDir := path.Join(appDir, "icons")
|
||||
listFile := path.Join(appDir, "list.json")
|
||||
|
||||
content, err := os.ReadFile(listFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
list := &dto.AppList{}
|
||||
if err := json.Unmarshal(content, list); err != nil {
|
||||
return err
|
||||
}
|
||||
appConfig.Version = list.Version
|
||||
appConfig.CanUpdate = false
|
||||
|
||||
var (
|
||||
tags []*model.Tag
|
||||
addApps []*model.App
|
||||
updateApps []*model.App
|
||||
appTags []*model.AppTag
|
||||
)
|
||||
|
||||
for _, t := range list.Tags {
|
||||
tags = append(tags, &model.Tag{
|
||||
Key: t.Key,
|
||||
Name: t.Name,
|
||||
})
|
||||
}
|
||||
|
||||
db := global.DB
|
||||
dbCtx := context.WithValue(context.Background(), "db", db)
|
||||
for _, l := range list.Items {
|
||||
icon, err := os.ReadFile(path.Join(iconDir, l.Icon))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("get [%s] icon error: %s", l.Name, err.Error())
|
||||
continue
|
||||
}
|
||||
iconStr := base64.StdEncoding.EncodeToString(icon)
|
||||
app := &model.App{
|
||||
Name: l.Name,
|
||||
Key: l.Key,
|
||||
ShortDesc: l.ShortDesc,
|
||||
Author: l.Author,
|
||||
Source: l.Source,
|
||||
Icon: iconStr,
|
||||
Type: l.Type,
|
||||
}
|
||||
app.TagsKey = l.Tags
|
||||
old, _ := appRepo.GetByKey(dbCtx, l.Key)
|
||||
if reflect.DeepEqual(old, &model.App{}) {
|
||||
addApps = append(addApps, app)
|
||||
} else {
|
||||
app.ID = old.ID
|
||||
updateApps = append(updateApps, app)
|
||||
}
|
||||
|
||||
versions := l.Versions
|
||||
for _, v := range versions {
|
||||
detail := &model.AppDetail{
|
||||
Version: v,
|
||||
}
|
||||
detailPath := path.Join(appDir, l.Key, v)
|
||||
if _, err := os.Stat(detailPath); err != nil {
|
||||
global.LOG.Errorf("get [%s] folder error: %s", detailPath, err.Error())
|
||||
continue
|
||||
}
|
||||
readmeStr, err := os.ReadFile(path.Join(detailPath, "README.md"))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("get [%s] README error: %s", detailPath, err.Error())
|
||||
}
|
||||
detail.Readme = string(readmeStr)
|
||||
dockerComposeStr, err := os.ReadFile(path.Join(detailPath, "docker-compose.yml"))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("get [%s] docker-compose.yml error: %s", detailPath, err.Error())
|
||||
continue
|
||||
}
|
||||
detail.DockerCompose = string(dockerComposeStr)
|
||||
formStr, err := os.ReadFile(path.Join(detailPath, "form.json"))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("get [%s] form.json error: %s", detailPath, err.Error())
|
||||
}
|
||||
detail.FormFields = string(formStr)
|
||||
app.Details = append(app.Details, detail)
|
||||
}
|
||||
}
|
||||
tx := global.DB.Begin()
|
||||
ctx := context.WithValue(context.Background(), "db", tx)
|
||||
if len(addApps) > 0 {
|
||||
if err := appRepo.BatchCreate(ctx, addApps); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tagRepo.DeleteAll(ctx); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tags) > 0 {
|
||||
if err := tagRepo.BatchCreate(ctx, tags); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
tagMap := make(map[string]uint, len(tags))
|
||||
|
||||
for _, t := range tags {
|
||||
tagMap[t.Key] = t.ID
|
||||
}
|
||||
|
||||
for _, a := range updateApps {
|
||||
if err := appRepo.Save(ctx, a); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
apps := append(addApps, updateApps...)
|
||||
|
||||
var (
|
||||
appDetails []*model.AppDetail
|
||||
appIds []uint
|
||||
)
|
||||
for _, a := range apps {
|
||||
|
||||
for _, t := range a.TagsKey {
|
||||
tagId, ok := tagMap[t]
|
||||
if ok {
|
||||
appTags = append(appTags, &model.AppTag{
|
||||
AppId: a.ID,
|
||||
TagId: tagId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, d := range a.Details {
|
||||
d.AppId = a.ID
|
||||
appDetails = append(appDetails, d)
|
||||
}
|
||||
appIds = append(appIds, a.ID)
|
||||
}
|
||||
|
||||
if err := appDetailRepo.DeleteByAppIds(ctx, appIds); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
if len(appDetails) > 0 {
|
||||
if err := appDetailRepo.BatchCreate(ctx, appDetails); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := appTagRepo.DeleteByAppIds(ctx, appIds); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
if len(appTags) > 0 {
|
||||
if err := appTagRepo.BatchCreate(ctx, appTags); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
return nil
|
||||
}
|
@ -12,6 +12,7 @@ type ServiceGroup struct {
|
||||
FileService
|
||||
CronjobService
|
||||
SettingService
|
||||
AppService
|
||||
}
|
||||
|
||||
var ServiceGroupApp = new(ServiceGroup)
|
||||
@ -25,4 +26,8 @@ var (
|
||||
commonRepo = repo.RepoGroupApp.CommonRepo
|
||||
cronjobRepo = repo.RepoGroupApp.CronjobRepo
|
||||
settingRepo = repo.RepoGroupApp.SettingRepo
|
||||
appRepo = repo.RepoGroupApp.AppRepo
|
||||
appTagRepo = repo.RepoGroupApp.AppTagRepo
|
||||
appDetailRepo = repo.RepoGroupApp.AppDetailRepo
|
||||
tagRepo = repo.RepoGroupApp.TagRepo
|
||||
)
|
||||
|
@ -1,7 +1,11 @@
|
||||
package configs
|
||||
|
||||
type System struct {
|
||||
Port int `mapstructure:"port"`
|
||||
DbType string `mapstructure:"db_type"`
|
||||
Level string `mapstructure:"level"`
|
||||
Port int `mapstructure:"port"`
|
||||
DbType string `mapstructure:"db_type"`
|
||||
Level string `mapstructure:"level"`
|
||||
DataDir string `mapstructure:"data_dir"`
|
||||
ResourceDir string `mapstructure:"resource_dir"`
|
||||
AppDir string `mapstructure:"app_dir"`
|
||||
AppOss string `mapstructure:"app_oss"`
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ func Init() {
|
||||
migrations.AddTableSetting,
|
||||
migrations.AddTableBackupAccount,
|
||||
migrations.AddTableCronjob,
|
||||
migrations.AddTableApp,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
@ -146,3 +146,10 @@ var AddTableCronjob = &gormigrate.Migration{
|
||||
return tx.AutoMigrate(&model.Cronjob{}, &model.JobRecords{})
|
||||
},
|
||||
}
|
||||
|
||||
var AddTableApp = &gormigrate.Migration{
|
||||
ID: "20200921-add-table-app",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(&model.App{}, &model.AppDetail{}, &model.Tag{}, &model.AppTag{}, &model.AppConfig{})
|
||||
},
|
||||
}
|
||||
|
@ -54,6 +54,7 @@ func Routers() *gin.Engine {
|
||||
systemRouter.InitFileRouter(PrivateGroup)
|
||||
systemRouter.InitCronjobRouter(PrivateGroup)
|
||||
systemRouter.InitSettingRouter(PrivateGroup)
|
||||
systemRouter.InitAppRouter(PrivateGroup)
|
||||
}
|
||||
|
||||
return Router
|
||||
|
@ -12,6 +12,7 @@ type RouterGroup struct {
|
||||
TerminalRouter
|
||||
CronjobRouter
|
||||
SettingRouter
|
||||
AppRouter
|
||||
}
|
||||
|
||||
var RouterGroupApp = new(RouterGroup)
|
||||
|
21
backend/router/ro_app.go
Normal file
21
backend/router/ro_app.go
Normal file
@ -0,0 +1,21 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
v1 "github.com/1Panel-dev/1Panel/app/api/v1"
|
||||
"github.com/1Panel-dev/1Panel/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AppRouter struct {
|
||||
}
|
||||
|
||||
func (a *AppRouter) InitAppRouter(Router *gin.RouterGroup) {
|
||||
appRouter := Router.Group("apps")
|
||||
appRouter.Use(middleware.JwtAuth()).Use(middleware.SessionAuth())
|
||||
|
||||
baseApi := v1.ApiGroupApp.BaseApi
|
||||
{
|
||||
appRouter.POST("/sync", baseApi.AppSync)
|
||||
appRouter.POST("/search", baseApi.AppSearch)
|
||||
}
|
||||
}
|
30
backend/utils/compose/compose.go
Normal file
30
backend/utils/compose/compose.go
Normal file
@ -0,0 +1,30 @@
|
||||
package compose
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func Up(filePath string) (string, error) {
|
||||
cmd := exec.Command("docker-compose", "-f", filePath, "up", "-d")
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(stdout), nil
|
||||
}
|
||||
|
||||
func Down(filePath string) (string, error) {
|
||||
cmd := exec.Command("docker-compose", "-f", filePath, "down")
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(stdout), nil
|
||||
}
|
||||
|
||||
func Restart(filePath string) (string, error) {
|
||||
cmd := exec.Command("docker-compose", "-f", filePath, "restart")
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(stdout), nil
|
||||
}
|
15329
frontend/package-lock.json
generated
15329
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -21,13 +21,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^1.1.4",
|
||||
"@kangc/v-md-editor": "^2.3.15",
|
||||
"@vueuse/core": "^8.0.1",
|
||||
"axios": "^0.27.2",
|
||||
"echarts": "^5.3.0",
|
||||
"echarts-liquidfill": "^3.1.0",
|
||||
"js-base64": "^3.7.2",
|
||||
"element-plus": "^2.2.13",
|
||||
"fit2cloud-ui-plus": "^0.0.1-beta.15",
|
||||
"js-base64": "^3.7.2",
|
||||
"js-md5": "^0.7.3",
|
||||
"monaco-editor": "^0.34.0",
|
||||
"nprogress": "^0.2.0",
|
||||
|
42
frontend/src/api/interface/app.ts
Normal file
42
frontend/src/api/interface/app.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { ReqPage } from '.';
|
||||
|
||||
export namespace App {
|
||||
export interface App {
|
||||
name: string;
|
||||
icon: string;
|
||||
key: string;
|
||||
tags: Tag[];
|
||||
shortDesc: string;
|
||||
author: string;
|
||||
source: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
key: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface AppResPage {
|
||||
total: number;
|
||||
canUpdate: boolean;
|
||||
version: string;
|
||||
items: App.App[];
|
||||
tags: App.Tag[];
|
||||
}
|
||||
|
||||
export interface AppDetail {
|
||||
name: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
sourceLink: string;
|
||||
versions: string[];
|
||||
readme: string;
|
||||
athor: string;
|
||||
}
|
||||
|
||||
export interface AppReq extends ReqPage {
|
||||
name: string;
|
||||
types: string[];
|
||||
}
|
||||
}
|
11
frontend/src/api/modules/app.ts
Normal file
11
frontend/src/api/modules/app.ts
Normal file
@ -0,0 +1,11 @@
|
||||
// export const GetAppList = ()
|
||||
import http from '@/api';
|
||||
import { App } from '../interface/app';
|
||||
|
||||
export const SyncApp = () => {
|
||||
return http.post<any>('apps/sync', {});
|
||||
};
|
||||
|
||||
export const SearchApp = (req: App.AppReq) => {
|
||||
return http.post<App.AppResPage>('apps/search', req);
|
||||
};
|
BIN
frontend/src/assets/apps/halo.png
Normal file
BIN
frontend/src/assets/apps/halo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
BIN
frontend/src/assets/apps/mysql.png
Normal file
BIN
frontend/src/assets/apps/mysql.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
BIN
frontend/src/assets/apps/redis.png
Normal file
BIN
frontend/src/assets/apps/redis.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
BIN
frontend/src/assets/apps/wordpress.png
Normal file
BIN
frontend/src/assets/apps/wordpress.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
@ -396,4 +396,9 @@ export default {
|
||||
star: 'Star',
|
||||
description: 'A modern Linux panel tool',
|
||||
},
|
||||
app: {
|
||||
installed: 'Installed',
|
||||
all: 'All',
|
||||
version: 'Version',
|
||||
},
|
||||
};
|
||||
|
@ -388,4 +388,9 @@ export default {
|
||||
star: '点亮 Star',
|
||||
description: '一个现代化的 Linux 面板工具',
|
||||
},
|
||||
app: {
|
||||
installed: '已安装',
|
||||
all: '全部',
|
||||
version: '版本',
|
||||
},
|
||||
};
|
||||
|
@ -17,6 +17,16 @@ import router from '@/routers/index';
|
||||
import I18n from '@/lang/index';
|
||||
import pinia from '@/store/index';
|
||||
import SvgIcon from './components/svg-icon/svg-icon.vue';
|
||||
import VMdPreview from '@kangc/v-md-editor/lib/preview';
|
||||
import '@kangc/v-md-editor/lib/style/preview.css';
|
||||
import githubTheme from '@kangc/v-md-editor/lib/theme/github.js';
|
||||
import '@kangc/v-md-editor/lib/theme/style/github.css';
|
||||
import hljs from 'highlight.js';
|
||||
|
||||
VMdPreview.use(githubTheme, {
|
||||
hljs,
|
||||
});
|
||||
|
||||
const app = createApp(App);
|
||||
app.component('SvgIcon', SvgIcon);
|
||||
app.use(ElementPlus);
|
||||
@ -29,4 +39,5 @@ app.use(router);
|
||||
app.use(I18n);
|
||||
app.use(pinia);
|
||||
app.use(directives);
|
||||
app.use(VMdPreview);
|
||||
app.mount('#app');
|
||||
|
@ -16,6 +16,25 @@ const appStoreRouter = {
|
||||
component: () => import('@/views/app-store/index.vue'),
|
||||
meta: {},
|
||||
},
|
||||
// {
|
||||
// path: '/apps/detail/:name',
|
||||
// name: 'AppDetail',
|
||||
// component: () => import('@/views/app-store/detail/index.vue'),
|
||||
// meta: {
|
||||
// hidden: true,
|
||||
// title: 'menu.apps',
|
||||
// },
|
||||
// },
|
||||
{
|
||||
path: '/apps/detail/:name',
|
||||
name: 'AppDetail',
|
||||
props: true,
|
||||
hidden: true,
|
||||
component: () => import('@/views/app-store/detail/index.vue'),
|
||||
meta: {
|
||||
activeMenu: '/apps',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
2
frontend/src/shims-vue.d.ts
vendored
Normal file
2
frontend/src/shims-vue.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
declare module '@kangc/v-md-editor/lib/preview';
|
||||
declare module '@kangc/v-md-editor/lib/theme/github.js';
|
3
frontend/src/utils/image.ts
Normal file
3
frontend/src/utils/image.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function getAssetsFile(url: string) {
|
||||
return new URL(`../assets/apps/${url}`, import.meta.url).href;
|
||||
}
|
29
frontend/src/views/app-store/apps.json
Normal file
29
frontend/src/views/app-store/apps.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"name": "Mysql",
|
||||
"icon": "mysql.png",
|
||||
"description": "常用的关系型数据库",
|
||||
"tags": ["数据库"]
|
||||
},
|
||||
{
|
||||
"name": "Redis",
|
||||
"icon": "redis.png",
|
||||
"description": "缓存数据库",
|
||||
"tags": ["数据库"]
|
||||
},
|
||||
{
|
||||
"name": "Wordpress",
|
||||
"icon": "wordpress.png",
|
||||
"description": "老牌博客平台",
|
||||
"tags": ["网站"]
|
||||
},
|
||||
{
|
||||
"name": "Halo",
|
||||
"icon": "halo.png",
|
||||
"description": "现代化的博客平台",
|
||||
"tags": ["网站"]
|
||||
}
|
||||
],
|
||||
"tags": ["数据库", "网站", "测试", "开发"]
|
||||
}
|
10
frontend/src/views/app-store/detail/detail.json
Normal file
10
frontend/src/views/app-store/detail/detail.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "Halo",
|
||||
"description": "更好用的博客模版",
|
||||
"versions": ["0.0.1", "0.0.2"],
|
||||
"sourceLink": "",
|
||||
"athor": "halo",
|
||||
"status": "",
|
||||
"readme": "",
|
||||
"icon": "halo.png"
|
||||
}
|
148
frontend/src/views/app-store/detail/index.vue
Normal file
148
frontend/src/views/app-store/detail/index.vue
Normal file
@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<LayoutContent>
|
||||
<div class="brief">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="4">
|
||||
<div class="icon">
|
||||
<el-image class="image" :src="getImageUrl(appDetail.icon)"></el-image>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="20">
|
||||
<div class="a-detail">
|
||||
<div class="a-name">
|
||||
<font size="5" style="font-weight: 800">{{ appDetail.name }}</font>
|
||||
</div>
|
||||
<div class="a-description">
|
||||
<span>
|
||||
<font>
|
||||
{{ appDetail.description }}
|
||||
</font>
|
||||
</span>
|
||||
</div>
|
||||
<el-descriptions :column="1">
|
||||
<el-descriptions-item :label="$t('app.version')">
|
||||
<el-select v-model="appSelect.version">
|
||||
<el-option
|
||||
v-for="(v, index) in appDetail.versions"
|
||||
:key="index"
|
||||
:value="v"
|
||||
:label="v"
|
||||
>
|
||||
{{ v }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="'链接'">
|
||||
<el-link>source</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="'来源'">FIT2CLOUD</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div>
|
||||
<el-button type="primary">安装</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<el-divider border-style="double" />
|
||||
<div class="detail">
|
||||
<v-md-preview :text="readme"></v-md-preview>
|
||||
</div>
|
||||
</LayoutContent>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import LayoutContent from '@/layout/layout-content.vue';
|
||||
import { getAssetsFile } from '@/utils/image';
|
||||
import { reactive, ref } from 'vue';
|
||||
import detail from './detail.json';
|
||||
|
||||
let appDetail = ref<any>();
|
||||
appDetail.value = detail;
|
||||
|
||||
let appSelect = reactive({
|
||||
version: '',
|
||||
});
|
||||
appSelect.version = appDetail.value.versions[0];
|
||||
|
||||
let readme = ref<string>(`<p align="center">
|
||||
<a href="https://halo.run" target="_blank" rel="noopener noreferrer">
|
||||
<img width="100" src="https://halo.run/logo" alt="Halo logo" />
|
||||
</a>
|
||||
</p>
|
||||
<p align="center"><b>Halo</b> [ˈheɪloʊ],一款现代化的开源博客/CMS系统,值得一试。</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/halo-dev/halo/releases"><img alt="GitHub release" src="https://img.shields.io/github/release/halo-dev/halo.svg?style=flat-square" /></a>
|
||||
<a href="https://github.com/halo-dev/halo/releases"><img alt="GitHub All Releases" src="https://img.shields.io/github/downloads/halo-dev/halo/total.svg?style=flat-square" /></a>
|
||||
<a href="https://hub.docker.com/r/halohub/halo"><img alt="Docker pulls" src="https://img.shields.io/docker/pulls/halohub/halo?style=flat-square" /></a>
|
||||
<a href="https://github.com/halo-dev/halo/commits"><img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/halo-dev/halo.svg?style=flat-square" /></a>
|
||||
<a href="https://github.com/halo-dev/halo/actions"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/workflow/status/halo-dev/halo/Halo%20CI?style=flat-square" /></a>
|
||||
<br />
|
||||
<a href="https://halo.run">官网</a>
|
||||
<a href="https://docs.halo.run">文档</a>
|
||||
<a href="https://bbs.halo.run">社区</a>
|
||||
<a href="https://gitee.com/halo-dev">Gitee</a>
|
||||
<a href="https://t.me/halo_dev">Telegram 频道</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
详细部署文档请查阅:<https://docs.halo.run>
|
||||
|
||||
## 在线体验
|
||||
|
||||
- 环境地址:<https://demo.halo.run>
|
||||
- 后台地址:<https://demo.halo.run/admin>
|
||||
- 用户名:demo
|
||||
- 密码:P@ssw0rd123..
|
||||
- 使用前请阅读:<https://demo.halo.run/archives/tips>
|
||||
|
||||
## 生态
|
||||
|
||||
| 项目 | 状态 | 描述 |
|
||||
| --- | --- | --- |
|
||||
| [halo-admin](https://github.com/halo-dev/halo-admin) | <a href="https://github.com/halo-dev/halo-admin/releases"><img alt="GitHub release" src="https://img.shields.io/github/release/halo-dev/halo-admin.svg?style=flat-square" /></a> | Web 管理端 UI,已内置在主应用 |
|
||||
| [js-sdk](https://github.com/halo-dev/js-sdk) | <a href="https://github.com/halo-dev/js-sdk"><img alt="npm release" src="https://img.shields.io/npm/v/@halo-dev/content-api?style=flat-square"/></a> | JavaScript SDK |
|
||||
| [halo-comment](https://github.com/halo-dev/halo-comment) | <a href="https://www.npmjs.com/package/halo-comment"><img alt="npm release" src="https://img.shields.io/npm/v/halo-comment?style=flat-square"/></a> | 独立评论组件,可以非常方便的集成到主题中 |
|
||||
| [halo-comment-normal](https://github.com/halo-dev/halo-comment-normal) | <a href="https://www.npmjs.com/package/halo-comment-normal"><img alt="npm release" src="https://img.shields.io/npm/v/halo-comment-normal?style=flat-square"/></a> | 另外一款评论组件 |
|
||||
| [halo-mobile-app](https://github.com/halo-dev/halo-mobile-app) | 已停止维护 | 移动端管理 APP |
|
||||
| [tencent-cloudbase-halo](https://github.com/halo-dev/tencent-cloudbase-halo) | 无 | 腾讯云 CloudBase 一键部署配置 |
|
||||
| [halo-theme-\*](https://github.com/topics/halo-theme) | 无 | GitHub 上开源的 Halo 主题集合 |
|
||||
|
||||
## 许可证
|
||||
|
||||
[![license](https://img.shields.io/github/license/halo-dev/halo.svg?style=flat-square)](https://github.com/halo-dev/halo/blob/master/LICENSE)
|
||||
|
||||
Halo 使用 GPL-v3.0 协议开源,请遵守开源协议。
|
||||
|
||||
## 贡献
|
||||
|
||||
参考 [CONTRIBUTING](https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md)。
|
||||
|
||||
<a href="https://github.com/halo-dev/halo/graphs/contributors"><img src="https://opencollective.com/halo/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
## 状态
|
||||
|
||||
![Repobeats analytics](https://repobeats.axiom.co/api/embed/ad008b2151c22e7cf734d2688befaa795d593b95.svg 'Repobeats analytics image')
|
||||
|
||||
`);
|
||||
|
||||
const getImageUrl = (name: string) => {
|
||||
return getAssetsFile(name);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.brief {
|
||||
height: 30vh;
|
||||
.icon {
|
||||
.image {
|
||||
width: auto;
|
||||
height: 20vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,7 +1,155 @@
|
||||
<template>
|
||||
<LayoutContent></LayoutContent>
|
||||
<LayoutContent>
|
||||
<el-row :gutter="20">
|
||||
<!-- <el-col :span="24">
|
||||
<div class="header">
|
||||
<el-radio-group v-model="activeName">
|
||||
<el-radio-button label="all">
|
||||
{{ $t('app.all') }}
|
||||
</el-radio-button>
|
||||
<el-radio-button label="installed">
|
||||
{{ $t('app.installed') }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</el-col> -->
|
||||
<el-col :span="12">
|
||||
<el-input></el-input>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-select v-model="selectTags" multiple style="width: 100%">
|
||||
<el-option v-for="item in tags" :key="item.key" :label="item.name" :value="item.key"></el-option>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-button @click="sync">同步</el-button>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col v-for="(app, index) in apps" :key="index" :xs="8" :sm="8" :lg="4">
|
||||
<div @click="getAppDetail(app.name)">
|
||||
<el-card :body-style="{ padding: '0px' }" class="a-card">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<div class="icon">
|
||||
<el-image class="image" :src="'data:image/png;base64,' + app.icon"></el-image>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<div class="a-detail">
|
||||
<div class="d-name">
|
||||
<font size="3" style="font-weight: 700">{{ app.name }}</font>
|
||||
</div>
|
||||
<div class="d-description">
|
||||
<font size="1">
|
||||
<span>
|
||||
{{ app.shortDesc }}
|
||||
</span>
|
||||
</font>
|
||||
</div>
|
||||
<div class="d-tag">
|
||||
<el-tag v-for="(tag, ind) in app.tags" :key="ind" round :colr="getColor(ind)">
|
||||
{{ tag.name }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</LayoutContent>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { App } from '@/api/interface/app';
|
||||
import LayoutContent from '@/layout/layout-content.vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import router from '@/routers';
|
||||
import { SyncApp } from '@/api/modules/app';
|
||||
import { SearchApp } from '@/api/modules/app';
|
||||
|
||||
let req = ref<App.AppReq>({
|
||||
name: '',
|
||||
types: [],
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
});
|
||||
|
||||
let apps = ref<App.App[]>([]);
|
||||
let tags = ref<App.Tag[]>([]);
|
||||
let selectTags = ref<string[]>([]);
|
||||
const colorArr = ['#6495ED', '#54FF9F', '#BEBEBE', '#FFF68F', '#FFFF00', '#8B0000'];
|
||||
|
||||
const getColor = (index: number) => {
|
||||
return colorArr[index];
|
||||
};
|
||||
|
||||
const sync = () => {
|
||||
SyncApp().then((res) => {
|
||||
console.log(res);
|
||||
});
|
||||
};
|
||||
|
||||
const search = async (req: App.AppReq) => {
|
||||
await SearchApp(req).then((res) => {
|
||||
apps.value = res.data.items;
|
||||
tags.value = res.data.tags;
|
||||
});
|
||||
};
|
||||
|
||||
const getAppDetail = (name: string) => {
|
||||
let params: { [key: string]: any } = {
|
||||
name: name,
|
||||
};
|
||||
router.push({ name: 'AppDetail', params });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
search(req.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.header {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.a-card {
|
||||
height: 100px;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
padding: 1px;
|
||||
|
||||
.icon {
|
||||
width: 100%;
|
||||
height: 80%;
|
||||
padding: 10%;
|
||||
margin-top: 5px;
|
||||
.image {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.a-detail {
|
||||
margin-top: 10px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.d-name {
|
||||
height: 20%;
|
||||
}
|
||||
|
||||
.d-description {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.a-card:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
</style>
|
||||
|
BIN
frontend/src/views/app-store/mysql-3.png
Normal file
BIN
frontend/src/views/app-store/mysql-3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
Loading…
x
Reference in New Issue
Block a user