mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 08:19:15 +08:00
fix: 计划任务数据库选项增加前缀 (#1820)
This commit is contained in:
parent
df770460d6
commit
a031d3ba41
@ -216,11 +216,11 @@ func (b *BaseApi) SearchMysql(c *gin.Context) {
|
|||||||
// @Description 获取 mysql 数据库列表
|
// @Description 获取 mysql 数据库列表
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.PageInfo true "request"
|
// @Param request body dto.PageInfo true "request"
|
||||||
// @Success 200 {array} string
|
// @Success 200 {array} dto.MysqlOption
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Router /databases/options [get]
|
// @Router /databases/options [get]
|
||||||
func (b *BaseApi) ListDBName(c *gin.Context) {
|
func (b *BaseApi) ListDBName(c *gin.Context) {
|
||||||
list, err := mysqlService.ListDBName()
|
list, err := mysqlService.ListDBOption()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
return
|
return
|
||||||
|
@ -24,6 +24,12 @@ type MysqlDBInfo struct {
|
|||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MysqlOption struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
From string `json:"from"`
|
||||||
|
}
|
||||||
|
|
||||||
type MysqlDBCreate struct {
|
type MysqlDBCreate struct {
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
From string `json:"from" validate:"required"`
|
From string `json:"from" validate:"required"`
|
||||||
|
@ -110,7 +110,7 @@ func handleAppBackup(install *model.AppInstall, backupDir, fileName string) erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := handleMysqlBackup(db.Name, tmpDir, fmt.Sprintf("%s.sql.gz", install.Name)); err != nil {
|
if err := handleMysqlBackup(db.MysqlName, db.Name, tmpDir, fmt.Sprintf("%s.sql.gz", install.Name)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ func (u *BackupService) MysqlBackup(req dto.CommonBackup) error {
|
|||||||
targetDir := path.Join(localDir, fmt.Sprintf("database/mysql/%s/%s", req.Name, req.DetailName))
|
targetDir := path.Join(localDir, fmt.Sprintf("database/mysql/%s/%s", req.Name, req.DetailName))
|
||||||
fileName := fmt.Sprintf("%s_%s.sql.gz", req.DetailName, timeNow)
|
fileName := fmt.Sprintf("%s_%s.sql.gz", req.DetailName, timeNow)
|
||||||
|
|
||||||
if err := handleMysqlBackup(req.DetailName, targetDir, fileName); err != nil {
|
if err := handleMysqlBackup(req.Name, req.DetailName, targetDir, fileName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,8 +97,8 @@ func (u *BackupService) MysqlRecoverByUpload(req dto.CommonRecover) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMysqlBackup(dbName, targetDir, fileName string) error {
|
func handleMysqlBackup(name, dbName, targetDir, fileName string) error {
|
||||||
dbInfo, err := mysqlRepo.Get(commonRepo.WithByName(dbName))
|
dbInfo, err := mysqlRepo.Get(commonRepo.WithByName(dbName), mysqlRepo.WithByMysqlName(name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -127,7 +127,7 @@ func handleMysqlRecover(req dto.CommonRecover, isRollback bool) error {
|
|||||||
if !fileOp.Stat(req.File) {
|
if !fileOp.Stat(req.File) {
|
||||||
return errors.New(fmt.Sprintf("%s file is not exist", req.File))
|
return errors.New(fmt.Sprintf("%s file is not exist", req.File))
|
||||||
}
|
}
|
||||||
dbInfo, err := mysqlRepo.Get(commonRepo.WithByName(req.DetailName))
|
dbInfo, err := mysqlRepo.Get(commonRepo.WithByName(req.DetailName), mysqlRepo.WithByMysqlName(req.Name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -254,15 +255,18 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, backup model.Back
|
|||||||
return paths, err
|
return paths, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var dblist []string
|
var dbs []model.DatabaseMysql
|
||||||
if cronjob.DBName == "all" {
|
if cronjob.DBName == "all" {
|
||||||
mysqlService := NewIMysqlService()
|
dbs, err = mysqlRepo.List()
|
||||||
dblist, err = mysqlService.ListDBName()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return paths, err
|
return paths, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dblist = append(dblist, cronjob.DBName)
|
itemID, _ := (strconv.Atoi(cronjob.DBName))
|
||||||
|
dbs, err = mysqlRepo.List(commonRepo.WithByID(uint(itemID)))
|
||||||
|
if err != nil {
|
||||||
|
return paths, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var client cloud_storage.CloudStorageClient
|
var client cloud_storage.CloudStorageClient
|
||||||
@ -273,25 +277,21 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, backup model.Back
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dbName := range dblist {
|
for _, dbInfo := range dbs {
|
||||||
var record model.BackupRecord
|
var record model.BackupRecord
|
||||||
|
|
||||||
record.Type = "mysql"
|
record.Type = "mysql"
|
||||||
record.Source = "LOCAL"
|
record.Source = "LOCAL"
|
||||||
record.BackupType = backup.Type
|
record.BackupType = backup.Type
|
||||||
|
|
||||||
dbInfo, err := mysqlRepo.Get(commonRepo.WithByName(dbName))
|
|
||||||
if err != nil {
|
|
||||||
return paths, err
|
|
||||||
}
|
|
||||||
record.Name = dbInfo.MysqlName
|
record.Name = dbInfo.MysqlName
|
||||||
backupDir := path.Join(localDir, fmt.Sprintf("database/mysql/%s/%s", record.Name, dbName))
|
backupDir := path.Join(localDir, fmt.Sprintf("database/mysql/%s/%s", record.Name, dbInfo.Name))
|
||||||
record.FileName = fmt.Sprintf("db_%s_%s.sql.gz", dbName, startTime.Format("20060102150405"))
|
record.FileName = fmt.Sprintf("db_%s_%s.sql.gz", dbInfo.Name, startTime.Format("20060102150405"))
|
||||||
if err = handleMysqlBackup(dbName, backupDir, record.FileName); err != nil {
|
if err = handleMysqlBackup(dbInfo.MysqlName, dbInfo.Name, backupDir, record.FileName); err != nil {
|
||||||
return paths, err
|
return paths, err
|
||||||
}
|
}
|
||||||
|
|
||||||
record.DetailName = dbName
|
record.DetailName = dbInfo.Name
|
||||||
record.FileDir = backupDir
|
record.FileDir = backupDir
|
||||||
itemFileDir := strings.TrimPrefix(backupDir, localDir+"/")
|
itemFileDir := strings.TrimPrefix(backupDir, localDir+"/")
|
||||||
if !cronjob.KeepLocal && backup.Type != "LOCAL" {
|
if !cronjob.KeepLocal && backup.Type != "LOCAL" {
|
||||||
|
@ -32,7 +32,7 @@ type MysqlService struct{}
|
|||||||
|
|
||||||
type IMysqlService interface {
|
type IMysqlService interface {
|
||||||
SearchWithPage(search dto.MysqlDBSearch) (int64, interface{}, error)
|
SearchWithPage(search dto.MysqlDBSearch) (int64, interface{}, error)
|
||||||
ListDBName() ([]string, error)
|
ListDBOption() ([]dto.MysqlOption, error)
|
||||||
Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error)
|
Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error)
|
||||||
LoadFromRemote(from string) error
|
LoadFromRemote(from string) error
|
||||||
ChangeAccess(info dto.ChangeDBInfo) error
|
ChangeAccess(info dto.ChangeDBInfo) error
|
||||||
@ -71,13 +71,17 @@ func (u *MysqlService) SearchWithPage(search dto.MysqlDBSearch) (int64, interfac
|
|||||||
return total, dtoMysqls, err
|
return total, dtoMysqls, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *MysqlService) ListDBName() ([]string, error) {
|
func (u *MysqlService) ListDBOption() ([]dto.MysqlOption, error) {
|
||||||
mysqls, err := mysqlRepo.List()
|
mysqls, err := mysqlRepo.List()
|
||||||
var dbNames []string
|
var dbs []dto.MysqlOption
|
||||||
for _, mysql := range mysqls {
|
for _, mysql := range mysqls {
|
||||||
dbNames = append(dbNames, mysql.Name)
|
var item dto.MysqlOption
|
||||||
|
if err := copier.Copy(&item, &mysql); err != nil {
|
||||||
|
return nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||||
|
}
|
||||||
|
dbs = append(dbs, item)
|
||||||
}
|
}
|
||||||
return dbNames, err
|
return dbs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) {
|
func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) {
|
||||||
@ -85,6 +89,11 @@ func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*mode
|
|||||||
return nil, buserr.New(constant.ErrCmdIllegal)
|
return nil, buserr.New(constant.ErrCmdIllegal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mysql, _ := mysqlRepo.Get(commonRepo.WithByName(req.Name), remoteDBRepo.WithByFrom(req.From))
|
||||||
|
if mysql.ID != 0 {
|
||||||
|
return nil, constant.ErrRecordExist
|
||||||
|
}
|
||||||
|
|
||||||
var createItem model.DatabaseMysql
|
var createItem model.DatabaseMysql
|
||||||
if err := copier.Copy(&createItem, &req); err != nil {
|
if err := copier.Copy(&createItem, &req); err != nil {
|
||||||
return nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
|
return nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||||
|
@ -60,6 +60,9 @@ func (r *Local) CreateUser(info CreateInfo) error {
|
|||||||
|
|
||||||
for _, user := range userlist {
|
for _, user := range userlist {
|
||||||
if err := r.ExecSQL(fmt.Sprintf("create user %s identified by '%s';", user, info.Password), info.Timeout); err != nil {
|
if err := r.ExecSQL(fmt.Sprintf("create user %s identified by '%s';", user, info.Password), info.Timeout); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "ERROR 1396") {
|
||||||
|
return buserr.New(constant.ErrUserIsExist)
|
||||||
|
}
|
||||||
_ = r.Delete(DeleteInfo{
|
_ = r.Delete(DeleteInfo{
|
||||||
Name: info.Name,
|
Name: info.Name,
|
||||||
Version: info.Version,
|
Version: info.Version,
|
||||||
@ -67,9 +70,6 @@ func (r *Local) CreateUser(info CreateInfo) error {
|
|||||||
Permission: info.Permission,
|
Permission: info.Permission,
|
||||||
ForceDelete: true,
|
ForceDelete: true,
|
||||||
Timeout: 300})
|
Timeout: 300})
|
||||||
if strings.Contains(err.Error(), "ERROR 1396") {
|
|
||||||
return buserr.New(constant.ErrUserIsExist)
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
grantStr := fmt.Sprintf("grant all privileges on `%s`.* to %s", info.Name, user)
|
grantStr := fmt.Sprintf("grant all privileges on `%s`.* to %s", info.Name, user)
|
||||||
|
@ -242,6 +242,10 @@ func (r *Remote) Recover(info RecoverInfo) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("gunzip file %s failed, stdout: %v, err: %v", info.SourceFile, string(stdout), err)
|
return fmt.Errorf("gunzip file %s failed, stdout: %v, err: %v", info.SourceFile, string(stdout), err)
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
gzipCmd := exec.Command("gzip", fileName)
|
||||||
|
_, _ = gzipCmd.CombinedOutput()
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
dns := fmt.Sprintf("%s:%s@tcp(%s:%v)/%s?charset=%s&parseTime=true&loc=Asia%sShanghai", r.User, r.Password, r.Address, r.Port, info.Name, info.Format, "%2F")
|
dns := fmt.Sprintf("%s:%s@tcp(%s:%v)/%s?charset=%s&parseTime=true&loc=Asia%sShanghai", r.User, r.Password, r.Address, r.Port, info.Name, info.Format, "%2F")
|
||||||
f, err := os.Open(fileName)
|
f, err := os.Open(fileName)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// Package docs GENERATED BY SWAG; DO NOT EDIT
|
// Code generated by swaggo/swag. DO NOT EDIT.
|
||||||
// This file was generated by swaggo/swag
|
|
||||||
package docs
|
package docs
|
||||||
|
|
||||||
import "github.com/swaggo/swag"
|
import "github.com/swaggo/swag"
|
||||||
@ -4056,7 +4056,7 @@ const docTemplate = `{
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"$ref": "#/definitions/dto.MysqlOption"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -13147,6 +13147,20 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.MysqlOption": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"from": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.MysqlStatus": {
|
"dto.MysqlStatus": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -4049,7 +4049,7 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"$ref": "#/definitions/dto.MysqlOption"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -13140,6 +13140,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.MysqlOption": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"from": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.MysqlStatus": {
|
"dto.MysqlStatus": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -1149,6 +1149,15 @@ definitions:
|
|||||||
- page
|
- page
|
||||||
- pageSize
|
- pageSize
|
||||||
type: object
|
type: object
|
||||||
|
dto.MysqlOption:
|
||||||
|
properties:
|
||||||
|
from:
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
dto.MysqlStatus:
|
dto.MysqlStatus:
|
||||||
properties:
|
properties:
|
||||||
Aborted_clients:
|
Aborted_clients:
|
||||||
@ -6414,7 +6423,7 @@ paths:
|
|||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
items:
|
items:
|
||||||
type: string
|
$ref: '#/definitions/dto.MysqlOption'
|
||||||
type: array
|
type: array
|
||||||
security:
|
security:
|
||||||
- ApiKeyAuth: []
|
- ApiKeyAuth: []
|
||||||
|
@ -111,6 +111,11 @@ export namespace Database {
|
|||||||
File: string;
|
File: string;
|
||||||
Position: number;
|
Position: number;
|
||||||
}
|
}
|
||||||
|
export interface MysqlOption {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
from: string;
|
||||||
|
}
|
||||||
export interface ChangeInfo {
|
export interface ChangeInfo {
|
||||||
id: number;
|
id: number;
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -59,8 +59,8 @@ export const loadMysqlStatus = () => {
|
|||||||
export const loadRemoteAccess = () => {
|
export const loadRemoteAccess = () => {
|
||||||
return http.get<boolean>(`/databases/remote`);
|
return http.get<boolean>(`/databases/remote`);
|
||||||
};
|
};
|
||||||
export const loadDBNames = () => {
|
export const loadDBOptions = () => {
|
||||||
return http.get<Array<string>>(`/databases/options`);
|
return http.get<Array<Database.MysqlOption>>(`/databases/options`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// redis
|
// redis
|
||||||
|
@ -122,7 +122,18 @@
|
|||||||
<el-form-item :label="$t('cronjob.database')" prop="dbName">
|
<el-form-item :label="$t('cronjob.database')" prop="dbName">
|
||||||
<el-select class="selectClass" clearable v-model="dialogData.rowData!.dbName">
|
<el-select class="selectClass" clearable v-model="dialogData.rowData!.dbName">
|
||||||
<el-option :label="$t('commons.table.all')" value="all" />
|
<el-option :label="$t('commons.table.all')" value="all" />
|
||||||
<el-option v-for="item in mysqlInfo.dbNames" :key="item" :label="item" :value="item" />
|
<div v-for="item in mysqlInfo.dbs" :key="item.id">
|
||||||
|
<el-option
|
||||||
|
v-if="item.from === 'local'"
|
||||||
|
:label="$t('database.localDB') + ' [' + item.name + ']'"
|
||||||
|
:value="item.id + ''"
|
||||||
|
/>
|
||||||
|
<el-option
|
||||||
|
v-else
|
||||||
|
:label="item.from + ' [' + item.name + ']'"
|
||||||
|
:value="item.id + ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
@ -219,12 +230,13 @@ import i18n from '@/lang';
|
|||||||
import { ElForm } from 'element-plus';
|
import { ElForm } from 'element-plus';
|
||||||
import { Cronjob } from '@/api/interface/cronjob';
|
import { Cronjob } from '@/api/interface/cronjob';
|
||||||
import { addCronjob, editCronjob } from '@/api/modules/cronjob';
|
import { addCronjob, editCronjob } from '@/api/modules/cronjob';
|
||||||
import { loadDBNames } from '@/api/modules/database';
|
import { loadDBOptions } from '@/api/modules/database';
|
||||||
import { GetWebsiteOptions } from '@/api/modules/website';
|
import { GetWebsiteOptions } from '@/api/modules/website';
|
||||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { listContainer } from '@/api/modules/container';
|
import { listContainer } from '@/api/modules/container';
|
||||||
|
import { Database } from '@/api/interface/database';
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
interface DialogProps {
|
interface DialogProps {
|
||||||
@ -275,7 +287,7 @@ const mysqlInfo = reactive({
|
|||||||
isExist: false,
|
isExist: false,
|
||||||
name: '',
|
name: '',
|
||||||
version: '',
|
version: '',
|
||||||
dbNames: [] as Array<string>,
|
dbs: [] as Array<Database.MysqlOption>,
|
||||||
});
|
});
|
||||||
|
|
||||||
const varifySpec = (rule: any, value: any, callback: any) => {
|
const varifySpec = (rule: any, value: any, callback: any) => {
|
||||||
@ -458,8 +470,8 @@ const loadContainers = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const checkMysqlInstalled = async () => {
|
const checkMysqlInstalled = async () => {
|
||||||
const data = await loadDBNames();
|
const data = await loadDBOptions();
|
||||||
mysqlInfo.dbNames = data.data || [];
|
mysqlInfo.dbs = data.data || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
function isBackup() {
|
function isBackup() {
|
||||||
|
@ -190,6 +190,7 @@ const onSave = async (formEl: FormInstance | undefined) => {
|
|||||||
const onSubmitAccess = async () => {
|
const onSubmitAccess = async () => {
|
||||||
let param = {
|
let param = {
|
||||||
id: 0,
|
id: 0,
|
||||||
|
from: form.from,
|
||||||
value: form.privilege ? '%' : 'localhost',
|
value: form.privilege ? '%' : 'localhost',
|
||||||
};
|
};
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user