mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-25 17:22:55 +00:00
feat: 【应用】支持自定义上传应用的logo #54
* feat: 【知识库】本地上传的文档内带的图片能同步到 maxkb 里 #69 * feat: 【应用】支持自定义上传应用的logo #54
This commit is contained in:
parent
73dfcb6fb0
commit
b26265fefd
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.1.13 on 2024-04-23 11:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('application', '0002_chat_client_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='application',
|
||||
name='icon',
|
||||
field=models.CharField(default='/ui/favicon.ico', max_length=256, verbose_name='应用icon'),
|
||||
),
|
||||
]
|
||||
|
|
@ -37,6 +37,7 @@ class Application(AppModelMixin):
|
|||
dataset_setting = models.JSONField(verbose_name="数据集参数设置", default=get_dataset_setting_dict)
|
||||
model_setting = models.JSONField(verbose_name="模型参数相关设置", default=get_model_setting_dict)
|
||||
problem_optimization = models.BooleanField(verbose_name="问题优化", default=False)
|
||||
icon = models.CharField(max_length=256, verbose_name="应用icon", default="/ui/favicon.ico")
|
||||
|
||||
@staticmethod
|
||||
def get_default_model_prompt():
|
||||
|
|
|
|||
|
|
@ -29,9 +29,10 @@ from common.constants.authentication_type import AuthenticationType
|
|||
from common.db.search import get_dynamics_model, native_search, native_page_search
|
||||
from common.db.sql_execute import select_list
|
||||
from common.exception.app_exception import AppApiException, NotFound404
|
||||
from common.field.common import UploadedImageField
|
||||
from common.util.field_message import ErrMessage
|
||||
from common.util.file_util import get_file_content
|
||||
from dataset.models import DataSet, Document
|
||||
from dataset.models import DataSet, Document, Image
|
||||
from dataset.serializers.common_serializers import list_paragraph
|
||||
from embedding.models import SearchMode
|
||||
from setting.models import AuthOperate
|
||||
|
|
@ -251,6 +252,7 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
# 问题补全
|
||||
problem_optimization = serializers.BooleanField(required=False, allow_null=True,
|
||||
error_messages=ErrMessage.boolean("问题补全"))
|
||||
icon = serializers.CharField(required=False, allow_null=True, error_messages=ErrMessage.char("icon图标"))
|
||||
|
||||
class Create(serializers.Serializer):
|
||||
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("用户id"))
|
||||
|
|
@ -380,7 +382,8 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
def reset_application(application: Dict):
|
||||
application['multiple_rounds_dialogue'] = True if application.get('dialogue_number') > 0 else False
|
||||
del application['dialogue_number']
|
||||
application['dataset_setting'] = {**application['dataset_setting'], 'search_mode': 'embedding'}
|
||||
if 'dataset_setting' in application:
|
||||
application['dataset_setting'] = {**application['dataset_setting'], 'search_mode': 'embedding'}
|
||||
return application
|
||||
|
||||
def page(self, current_page: int, page_size: int, with_valid=True):
|
||||
|
|
@ -393,7 +396,25 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
class ApplicationModel(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Application
|
||||
fields = ['id', 'name', 'desc', 'prologue', 'dialogue_number']
|
||||
fields = ['id', 'name', 'desc', 'prologue', 'dialogue_number', 'icon']
|
||||
|
||||
class IconOperate(serializers.Serializer):
|
||||
application_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("应用id"))
|
||||
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("用户id"))
|
||||
image = UploadedImageField(required=True, error_messages=ErrMessage.image("图片"))
|
||||
|
||||
def edit(self, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
application = QuerySet(Application).filter(id=self.data.get('application_id')).first()
|
||||
if application is None:
|
||||
raise AppApiException(500, '不存在的应用id')
|
||||
image_id = uuid.uuid1()
|
||||
image = Image(id=image_id, image=self.data.get('image').read(), image_name=self.data.get('image').name)
|
||||
image.save()
|
||||
application.icon = f'/api/image/{image_id}'
|
||||
application.save()
|
||||
return {**ApplicationSerializer.Query.reset_application(ApplicationSerializerModel(application).data)}
|
||||
|
||||
class Operate(serializers.Serializer):
|
||||
application_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("应用id"))
|
||||
|
|
@ -457,7 +478,7 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
|
||||
update_keys = ['name', 'desc', 'model_id', 'multiple_rounds_dialogue', 'prologue', 'status',
|
||||
'dataset_setting', 'model_setting', 'problem_optimization',
|
||||
'api_key_is_active']
|
||||
'api_key_is_active', 'icon']
|
||||
for update_key in update_keys:
|
||||
if update_key in instance and instance.get(update_key) is not None:
|
||||
if update_key == 'multiple_rounds_dialogue':
|
||||
|
|
|
|||
|
|
@ -12,6 +12,17 @@ from common.mixins.api_mixin import ApiMixin
|
|||
|
||||
|
||||
class ApplicationApi(ApiMixin):
|
||||
class EditApplicationIcon(ApiMixin):
|
||||
@staticmethod
|
||||
def get_request_params_api():
|
||||
return [
|
||||
openapi.Parameter(name='file',
|
||||
in_=openapi.IN_FORM,
|
||||
type=openapi.TYPE_FILE,
|
||||
required=True,
|
||||
description='上传文件')
|
||||
]
|
||||
|
||||
class Authentication(ApiMixin):
|
||||
@staticmethod
|
||||
def get_request_body_api():
|
||||
|
|
@ -143,7 +154,9 @@ class ApplicationApi(ApiMixin):
|
|||
'dataset_setting': ApplicationApi.DatasetSetting.get_request_body_api(),
|
||||
'model_setting': ApplicationApi.ModelSetting.get_request_body_api(),
|
||||
'problem_optimization': openapi.Schema(type=openapi.TYPE_BOOLEAN, title="问题优化",
|
||||
description="是否开启问题优化", default=True)
|
||||
description="是否开启问题优化", default=True),
|
||||
'icon': openapi.Schema(type=openapi.TYPE_STRING, title="icon",
|
||||
description="icon", default="/ui/favicon.ico")
|
||||
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ urlpatterns = [
|
|||
path('application/profile', views.Application.Profile.as_view()),
|
||||
path('application/embed', views.Application.Embed.as_view()),
|
||||
path('application/authentication', views.Application.Authentication.as_view()),
|
||||
path('application/<str:application_id>/edit_icon', views.Application.EditIcon.as_view()),
|
||||
path('application/<str:application_id>/statistics/customer_count',
|
||||
views.ApplicationStatistics.CustomerCount.as_view()),
|
||||
path('application/<str:application_id>/statistics/customer_count_trend',
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
from django.http import HttpResponse
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
|
@ -131,6 +132,28 @@ class ApplicationStatistics(APIView):
|
|||
class Application(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
class EditIcon(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
parser_classes = [MultiPartParser]
|
||||
|
||||
@action(methods=['PUT'], detail=False)
|
||||
@swagger_auto_schema(operation_summary="修改应用icon",
|
||||
operation_id="修改应用icon",
|
||||
tags=['应用'],
|
||||
manual_parameters=ApplicationApi.EditApplicationIcon.get_request_params_api(),
|
||||
request_body=ApplicationApi.Operate.get_request_body_api())
|
||||
@has_permissions(ViewPermission(
|
||||
[RoleConstants.ADMIN, RoleConstants.USER],
|
||||
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.MANAGE,
|
||||
dynamic_tag=keywords.get('application_id'))],
|
||||
compare=CompareConstants.AND), PermissionConstants.APPLICATION_EDIT,
|
||||
compare=CompareConstants.AND)
|
||||
def put(self, request: Request, application_id: str):
|
||||
return result.success(
|
||||
ApplicationSerializer.IconOperate(
|
||||
data={'application_id': application_id, 'user_id': request.user.id,
|
||||
'image': request.FILES.get('file')}).edit(request.data))
|
||||
|
||||
class Embed(APIView):
|
||||
@action(methods=["GET"], detail=False)
|
||||
@swagger_auto_schema(operation_summary="获取嵌入js",
|
||||
|
|
|
|||
|
|
@ -32,3 +32,11 @@ class FunctionField(serializers.Field):
|
|||
|
||||
def to_representation(self, value):
|
||||
return value
|
||||
|
||||
|
||||
class UploadedImageField(serializers.ImageField):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_representation(self, value):
|
||||
return value
|
||||
|
|
|
|||
|
|
@ -17,7 +17,14 @@ class StaticHeadersMiddleware(MiddlewareMixin):
|
|||
if request.path.startswith('/ui/chat/'):
|
||||
access_token = request.path.replace('/ui/chat/', '')
|
||||
application_access_token = QuerySet(ApplicationAccessToken).filter(access_token=access_token).first()
|
||||
if application_access_token is not None and application_access_token.white_active:
|
||||
# 添加自定义的响应头
|
||||
response['Content-Security-Policy'] = f'frame-ancestors {" ".join(application_access_token.white_list)}'
|
||||
if application_access_token is not None:
|
||||
if application_access_token.white_active:
|
||||
# 添加自定义的响应头
|
||||
response[
|
||||
'Content-Security-Policy'] = f'frame-ancestors {" ".join(application_access_token.white_list)}'
|
||||
response.content = (response.content.decode('utf-8').replace(
|
||||
'<link rel="icon" href="/ui/favicon.ico" />',
|
||||
f'<link rel="icon" href="{application_access_token.application.icon}" />')
|
||||
.replace('<title>MaxKB</title>', f'<title>{application_access_token.application.name}</title>').encode(
|
||||
"utf-8"))
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -95,3 +95,12 @@ class ErrMessage:
|
|||
'invalid': gettext_lazy('【%s】日期格式错误。请改用以下格式之一: {format}。'),
|
||||
'datetime': gettext_lazy('【%s】应为日期,但得到的是日期时间。')
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def image(field: str):
|
||||
return {
|
||||
'required': gettext_lazy('【%s】此字段必填。' % field),
|
||||
'null': gettext_lazy('【%s】此字段不能为null。' % field),
|
||||
'invalid_image': gettext_lazy('【%s】上载有效的图像。您上载的文件不是图像或图像已损坏。' % field),
|
||||
'max_length': gettext_lazy('请确保此文件名最多包含 {max_length} 个字符(长度为 {length})。')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 4.1.13 on 2024-04-22 19:31
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dataset', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Image',
|
||||
fields=[
|
||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
|
||||
('id', models.UUIDField(default=uuid.uuid1, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
|
||||
('image', models.BinaryField(verbose_name='图片数据')),
|
||||
('image_name', models.CharField(default='', max_length=256, verbose_name='图片名称')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'image',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -105,3 +105,12 @@ class ProblemParagraphMapping(AppModelMixin):
|
|||
|
||||
class Meta:
|
||||
db_table = "problem_paragraph_mapping"
|
||||
|
||||
|
||||
class Image(AppModelMixin):
|
||||
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid1, editable=False, verbose_name="主键id")
|
||||
image = models.BinaryField(verbose_name="图片数据")
|
||||
image_name = models.CharField(max_length=256, verbose_name="图片名称", default="")
|
||||
|
||||
class Meta:
|
||||
db_table = "image"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: maxkb
|
||||
@Author:虎
|
||||
@file: image_serializers.py
|
||||
@date:2024/4/22 16:36
|
||||
@desc:
|
||||
"""
|
||||
import uuid
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from django.http import HttpResponse
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.exception.app_exception import NotFound404
|
||||
from common.field.common import UploadedImageField
|
||||
from common.util.field_message import ErrMessage
|
||||
from dataset.models import Image
|
||||
|
||||
|
||||
class ImageSerializer(serializers.Serializer):
|
||||
image = UploadedImageField(required=True, error_messages=ErrMessage.image("图片"))
|
||||
|
||||
def upload(self, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
image_id = uuid.uuid1()
|
||||
image = Image(id=image_id, image=self.data.get('image').read(), image_name=self.data.get('image').name)
|
||||
image.save()
|
||||
return f'/api/image/{image_id}'
|
||||
|
||||
class Operate(serializers.Serializer):
|
||||
id = serializers.UUIDField(required=True)
|
||||
|
||||
def get(self, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
image_id = self.data.get('id')
|
||||
image = QuerySet(Image).filter(id=image_id).first()
|
||||
if image is None:
|
||||
raise NotFound404(404, "不存在的图片")
|
||||
return HttpResponse(image.image, status=200, headers={'Content-Type': 'image/png'})
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: maxkb
|
||||
@Author:虎
|
||||
@file: image_api.py
|
||||
@date:2024/4/23 11:23
|
||||
@desc:
|
||||
"""
|
||||
from drf_yasg import openapi
|
||||
|
||||
from common.mixins.api_mixin import ApiMixin
|
||||
|
||||
|
||||
class ImageApi(ApiMixin):
|
||||
@staticmethod
|
||||
def get_request_params_api():
|
||||
return [openapi.Parameter(name='file',
|
||||
in_=openapi.IN_FORM,
|
||||
type=openapi.TYPE_FILE,
|
||||
required=True,
|
||||
description='上传图片文件')
|
||||
]
|
||||
|
|
@ -40,5 +40,6 @@ urlpatterns = [
|
|||
path('dataset/<str:dataset_id>/problem/<int:current_page>/<int:page_size>', views.Problem.Page.as_view()),
|
||||
path('dataset/<str:dataset_id>/problem/<str:problem_id>', views.Problem.Operate.as_view()),
|
||||
path('dataset/<str:dataset_id>/problem/<str:problem_id>/paragraph', views.Problem.Paragraph.as_view()),
|
||||
|
||||
path('image/<str:image_id>', views.Image.Operate.as_view()),
|
||||
path('image', views.Image.as_view())
|
||||
]
|
||||
|
|
|
|||
|
|
@ -10,3 +10,4 @@ from .dataset import *
|
|||
from .document import *
|
||||
from .paragraph import *
|
||||
from .problem import *
|
||||
from .image import *
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: maxkb
|
||||
@Author:虎
|
||||
@file: image.py
|
||||
@date:2024/4/22 16:23
|
||||
@desc:
|
||||
"""
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.views import Request
|
||||
|
||||
from common.auth import TokenAuth
|
||||
from common.response import result
|
||||
from dataset.serializers.image_serializers import ImageSerializer
|
||||
|
||||
|
||||
class Image(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
parser_classes = [MultiPartParser]
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
@swagger_auto_schema(operation_summary="上传图片",
|
||||
operation_id="上传图片",
|
||||
manual_parameters=[openapi.Parameter(name='file',
|
||||
in_=openapi.IN_FORM,
|
||||
type=openapi.TYPE_FILE,
|
||||
required=True,
|
||||
description='上传文件')],
|
||||
tags=["图片"])
|
||||
def post(self, request: Request):
|
||||
return result.success(ImageSerializer(data={'image': request.FILES.get('file')}).upload())
|
||||
|
||||
class Operate(APIView):
|
||||
@action(methods=['GET'], detail=False)
|
||||
@swagger_auto_schema(operation_summary="获取图片",
|
||||
operation_id="获取图片",
|
||||
tags=["图片"])
|
||||
def get(self, request: Request, image_id: str):
|
||||
return ImageSerializer.Operate(data={'id': image_id}).get()
|
||||
|
|
@ -67,10 +67,24 @@ const getStatistics: (
|
|||
return get(`${prefix}/${application_id}/statistics/chat_record_aggregate_trend`, data, loading)
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改应用icon
|
||||
* @param 参数 application_id
|
||||
* data: file
|
||||
*/
|
||||
const putAppIcon: (
|
||||
application_id: string,
|
||||
data: any,
|
||||
loading?: Ref<boolean>
|
||||
) => Promise<Result<any>> = (application_id, data, loading) => {
|
||||
return put(`${prefix}/${application_id}/edit_icon`, data, undefined, loading)
|
||||
}
|
||||
|
||||
export default {
|
||||
getAPIKey,
|
||||
postAPIKey,
|
||||
delAPIKey,
|
||||
putAPIKey,
|
||||
getStatistics
|
||||
getStatistics,
|
||||
putAppIcon
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import { Result } from '@/request/Result'
|
||||
import { get, post, del, put } from '@/request/index'
|
||||
|
||||
const prefix = '/image'
|
||||
/**
|
||||
* 上传图片
|
||||
* @param 参数 file:file
|
||||
*/
|
||||
const postImage: (data: any) => Promise<Result<any>> = (data) => {
|
||||
return post(`${prefix}`, data)
|
||||
}
|
||||
|
||||
export default {
|
||||
postImage
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ interface ApplicationFormType {
|
|||
dataset_setting?: any
|
||||
model_setting?: any
|
||||
problem_optimization?: boolean
|
||||
icon?: string | undefined
|
||||
}
|
||||
interface chatType {
|
||||
id: string
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
class="mr-8"
|
||||
:size="24"
|
||||
/>
|
||||
|
||||
<AppAvatar
|
||||
v-else-if="isDataset && currentType === '1'"
|
||||
class="mr-8 avatar-purple"
|
||||
|
|
|
|||
|
|
@ -75,6 +75,19 @@ const useApplicationStore = defineStore({
|
|||
},
|
||||
async refreshAccessToken(token: string) {
|
||||
this.asyncAppAuthentication(token)
|
||||
},
|
||||
// 修改应用
|
||||
async asyncPutApplication(id: string, data: any, loading?: Ref<boolean>) {
|
||||
return new Promise((resolve, reject) => {
|
||||
applicationApi
|
||||
.putApplication(id, data, loading)
|
||||
.then((data) => {
|
||||
resolve(data)
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -306,6 +306,10 @@
|
|||
.el-popover {
|
||||
--el-popover-padding: 16px;
|
||||
}
|
||||
|
||||
.el-radio {
|
||||
--el-radio-font-weight: 400;
|
||||
}
|
||||
.el-radio__input.is-checked + .el-radio__label {
|
||||
color: var(--app-text-color);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
export const defaultIcon = '/ui/favicon.ico'
|
||||
|
||||
// 是否显示字母 / icon
|
||||
export function isAppIcon(url: string | undefined) {
|
||||
return url === defaultIcon ? '' : url
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
<template>
|
||||
<el-dialog title="设置 Logo" v-model="dialogVisible">
|
||||
<el-radio-group v-model="radioType" class="card__block mb-16">
|
||||
<div>
|
||||
<el-radio value="default">
|
||||
<p>默认 Logo</p>
|
||||
<AppAvatar
|
||||
v-if="detail?.name"
|
||||
:name="detail?.name"
|
||||
pinyinColor
|
||||
class="mt-8 mb-8"
|
||||
shape="square"
|
||||
:size="32"
|
||||
/>
|
||||
</el-radio>
|
||||
</div>
|
||||
<div>
|
||||
<el-radio value="custom">
|
||||
<p>自定义上传</p>
|
||||
<div class="flex mt-8">
|
||||
<AppAvatar
|
||||
v-if="fileURL"
|
||||
shape="square"
|
||||
:size="32"
|
||||
style="background: none"
|
||||
class="mr-16"
|
||||
>
|
||||
<img :src="fileURL" alt="" />
|
||||
</AppAvatar>
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:limit="1"
|
||||
accept="image/*"
|
||||
:on-change="onChange"
|
||||
>
|
||||
<el-button icon="Upload">上传</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
<div class="el-upload__tip info mt-16">
|
||||
建议尺寸 32*32,支持 ico、png , 大小不超过200KB
|
||||
</div>
|
||||
</el-radio>
|
||||
</div>
|
||||
</el-radio-group>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false"> 取消 </el-button>
|
||||
<el-button type="primary" @click="submit" :loading="loading"> 保存 </el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import overviewApi from '@/api/application-overview'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { MsgSuccess, MsgError } from '@/utils/message'
|
||||
import { defaultIcon, isAppIcon } from '@/utils/application'
|
||||
import useStore from '@/stores'
|
||||
|
||||
const { application } = useStore()
|
||||
|
||||
const route = useRoute()
|
||||
const {
|
||||
params: { id } //应用id
|
||||
} = route
|
||||
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const iconFile = ref<any>(null)
|
||||
const fileURL = ref<any>(null)
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const loading = ref(false)
|
||||
const detail = ref<any>(null)
|
||||
const radioType = ref('default')
|
||||
|
||||
watch(dialogVisible, (bool) => {
|
||||
if (!bool) {
|
||||
iconFile.value = null
|
||||
fileURL.value = null
|
||||
}
|
||||
})
|
||||
|
||||
const open = (data: any) => {
|
||||
radioType.value = isAppIcon(data.icon) ? 'custom' : 'default'
|
||||
fileURL.value = isAppIcon(data.icon) ? data.icon : null
|
||||
detail.value = cloneDeep(data)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const onChange = (file: any) => {
|
||||
//1、判断文件大小是否合法,文件限制不能大于 200KB
|
||||
const isLimit = file?.size / 1024 < 200
|
||||
if (!isLimit) {
|
||||
MsgError('文件大小超过 200KB')
|
||||
return false
|
||||
}
|
||||
iconFile.value = file
|
||||
fileURL.value = URL.createObjectURL(file.raw)
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (radioType.value === 'default') {
|
||||
application.asyncPutApplication(id as string, { icon: defaultIcon }, loading).then((res) => {
|
||||
emit('refresh')
|
||||
MsgSuccess('设置成功')
|
||||
dialogVisible.value = false
|
||||
})
|
||||
} else if (radioType.value === 'custom' && iconFile.value) {
|
||||
let fd = new FormData()
|
||||
fd.append('file', iconFile.value.raw)
|
||||
overviewApi.putAppIcon(id as string, fd, loading).then((res: any) => {
|
||||
emit('refresh')
|
||||
MsgSuccess('设置成功')
|
||||
dialogVisible.value = false
|
||||
})
|
||||
} else {
|
||||
MsgError('请上传一张图片')
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss" scope>
|
||||
.card__block {
|
||||
width: 100%;
|
||||
display: block;
|
||||
.el-radio {
|
||||
align-items: flex-start;
|
||||
height: 100%;
|
||||
}
|
||||
.el-radio__inner {
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -5,14 +5,37 @@
|
|||
<h4 class="title-decoration-1 mb-16">应用信息</h4>
|
||||
<el-card shadow="never" class="overview-card" v-loading="loading">
|
||||
<div class="title flex align-center">
|
||||
<AppAvatar
|
||||
v-if="detail?.name"
|
||||
:name="detail?.name"
|
||||
pinyinColor
|
||||
class="mr-12"
|
||||
shape="square"
|
||||
:size="32"
|
||||
/>
|
||||
<div
|
||||
class="edit-avatar mr-12"
|
||||
@mouseenter="showEditIcon = true"
|
||||
@mouseleave="showEditIcon = false"
|
||||
>
|
||||
<AppAvatar
|
||||
v-if="isAppIcon(detail?.icon)"
|
||||
shape="square"
|
||||
:size="32"
|
||||
style="background: none"
|
||||
>
|
||||
<img :src="detail?.icon" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar
|
||||
v-else-if="detail?.name"
|
||||
:name="detail?.name"
|
||||
pinyinColor
|
||||
shape="square"
|
||||
:size="32"
|
||||
/>
|
||||
<AppAvatar
|
||||
v-if="showEditIcon"
|
||||
shape="square"
|
||||
class="edit-mask"
|
||||
:size="32"
|
||||
@click="openEditAvatar"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>
|
||||
</AppAvatar>
|
||||
</div>
|
||||
|
||||
<h4>{{ detail?.name }}</h4>
|
||||
</div>
|
||||
|
||||
|
|
@ -102,6 +125,7 @@
|
|||
<EmbedDialog ref="EmbedDialogRef" />
|
||||
<APIKeyDialog ref="APIKeyDialogRef" />
|
||||
<LimitDialog ref="LimitDialogRef" @refresh="refresh" />
|
||||
<EditAvatarDialog ref="EditAvatarDialogRef" @refresh="refreshIcon" />
|
||||
</LayoutContainer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
|
@ -110,14 +134,14 @@ import { useRoute } from 'vue-router'
|
|||
import EmbedDialog from './component/EmbedDialog.vue'
|
||||
import APIKeyDialog from './component/APIKeyDialog.vue'
|
||||
import LimitDialog from './component/LimitDialog.vue'
|
||||
import EditAvatarDialog from './component/EditAvatarDialog.vue'
|
||||
import StatisticsCharts from './component/StatisticsCharts.vue'
|
||||
import applicationApi from '@/api/application'
|
||||
import overviewApi from '@/api/application-overview'
|
||||
import { nowDate, beforeDay } from '@/utils/time'
|
||||
|
||||
import { MsgSuccess, MsgConfirm } from '@/utils/message'
|
||||
|
||||
import { copyClick } from '@/utils/clipboard'
|
||||
import { isAppIcon } from '@/utils/application'
|
||||
import useStore from '@/stores'
|
||||
const { application } = useStore()
|
||||
const route = useRoute()
|
||||
|
|
@ -127,6 +151,7 @@ const {
|
|||
|
||||
const apiUrl = window.location.origin + '/doc'
|
||||
|
||||
const EditAvatarDialogRef = ref()
|
||||
const LimitDialogRef = ref()
|
||||
const APIKeyDialogRef = ref()
|
||||
const EmbedDialogRef = ref()
|
||||
|
|
@ -175,6 +200,12 @@ const daterange = ref({
|
|||
const statisticsLoading = ref(false)
|
||||
const statisticsData = ref([])
|
||||
|
||||
const showEditIcon = ref(false)
|
||||
|
||||
function openEditAvatar() {
|
||||
EditAvatarDialogRef.value.open(detail.value)
|
||||
}
|
||||
|
||||
function changeDayHandle(val: number | string) {
|
||||
if (val !== 'other') {
|
||||
daterange.value.start_time = beforeDay(val)
|
||||
|
|
@ -252,6 +283,11 @@ function getDetail() {
|
|||
function refresh() {
|
||||
getAccessToken()
|
||||
}
|
||||
|
||||
function refreshIcon() {
|
||||
getDetail()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
getAccessToken()
|
||||
|
|
@ -266,5 +302,14 @@ onMounted(() => {
|
|||
right: 16px;
|
||||
top: 21px;
|
||||
}
|
||||
|
||||
.edit-avatar {
|
||||
position: relative;
|
||||
.edit-mask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -350,7 +350,7 @@ const submit = async (formEl: FormInstance | undefined) => {
|
|||
await formEl.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
if (id) {
|
||||
applicationApi.putApplication(id, applicationForm.value, loading).then((res) => {
|
||||
application.asyncPutApplication(id, applicationForm.value, loading).then((res) => {
|
||||
MsgSuccess('保存成功')
|
||||
})
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -41,12 +41,21 @@
|
|||
>
|
||||
<template #icon>
|
||||
<AppAvatar
|
||||
v-if="item.name"
|
||||
:name="item.name"
|
||||
pinyinColor
|
||||
class="mr-12"
|
||||
v-if="isAppIcon(item?.icon)"
|
||||
shape="square"
|
||||
:size="32"
|
||||
style="background: none"
|
||||
class="mr-8"
|
||||
>
|
||||
<img :src="item?.icon" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar
|
||||
v-else-if="item?.name"
|
||||
:name="item?.name"
|
||||
pinyinColor
|
||||
shape="square"
|
||||
:size="32"
|
||||
class="mr-8"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
@ -85,6 +94,7 @@
|
|||
import { ref, onMounted, reactive, computed } from 'vue'
|
||||
import applicationApi from '@/api/application'
|
||||
import { MsgSuccess, MsgConfirm } from '@/utils/message'
|
||||
import { isAppIcon } from '@/utils/application'
|
||||
import { useRouter } from 'vue-router'
|
||||
import useStore from '@/stores'
|
||||
const { application } = useStore()
|
||||
|
|
|
|||
|
|
@ -59,12 +59,21 @@
|
|||
<CardCheckbox value-field="id" :data="item" v-model="application_id_list">
|
||||
<template #icon>
|
||||
<AppAvatar
|
||||
v-if="item.name"
|
||||
:name="item.name"
|
||||
pinyinColor
|
||||
class="mr-12"
|
||||
v-if="isAppIcon(item?.icon)"
|
||||
shape="square"
|
||||
:size="32"
|
||||
style="background: none"
|
||||
class="mr-12"
|
||||
>
|
||||
<img :src="item?.icon" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar
|
||||
v-else-if="item?.name"
|
||||
:name="item?.name"
|
||||
pinyinColor
|
||||
shape="square"
|
||||
:size="32"
|
||||
class="mr-12"
|
||||
/>
|
||||
</template>
|
||||
{{ item.name }}
|
||||
|
|
@ -87,6 +96,7 @@ import BaseForm from '@/views/dataset/component/BaseForm.vue'
|
|||
import datasetApi from '@/api/dataset'
|
||||
import type { ApplicationFormType } from '@/api/type/application'
|
||||
import { MsgSuccess } from '@/utils/message'
|
||||
import { isAppIcon } from '@/utils/application'
|
||||
import useStore from '@/stores'
|
||||
const route = useRoute()
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
:preview="false"
|
||||
:toolbars="toolbars"
|
||||
style="height: 300px"
|
||||
@onUploadImg="onUploadImg"
|
||||
/>
|
||||
<MdPreview
|
||||
v-else
|
||||
|
|
@ -46,6 +47,7 @@
|
|||
import { ref, reactive, onUnmounted, watch, nextTick } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { MdEditor, MdPreview } from 'md-editor-v3'
|
||||
import imageApi from '@/api/image'
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
|
|
@ -81,7 +83,7 @@ const toolbars = [
|
|||
'=',
|
||||
'pageFullscreen',
|
||||
'preview',
|
||||
'htmlPreview',
|
||||
'htmlPreview'
|
||||
] as any[]
|
||||
|
||||
const editorRef = ref()
|
||||
|
|
@ -134,20 +136,25 @@ function validate() {
|
|||
})
|
||||
}
|
||||
|
||||
// const onHtmlChanged = () => {
|
||||
// appendTarget()
|
||||
// }
|
||||
// const appendTarget = () => {
|
||||
// nextTick(() => {
|
||||
// var item = document.getElementsByClassName('maxkb-md')
|
||||
// for (var j = 0; j < item.length; j++) {
|
||||
// var aTags = item[j].getElementsByTagName('a')
|
||||
// for (var i = 0; i < aTags.length; i++) {
|
||||
// aTags[i].setAttribute('target', '_blank')
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
const onUploadImg = async (files: any, callback: any) => {
|
||||
const res = await Promise.all(
|
||||
files.map((file: any) => {
|
||||
return new Promise((rev, rej) => {
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
|
||||
imageApi
|
||||
.postImage(fd)
|
||||
.then((res: any) => {
|
||||
rev(res)
|
||||
})
|
||||
.catch((error) => rej(error))
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
callback(res.map((item) => item.data))
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
form.value = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue