feat: 【应用】支持自定义上传应用的logo #54

* feat: 【知识库】本地上传的文档内带的图片能同步到 maxkb 里 #69 
* feat: 【应用】支持自定义上传应用的logo #54
This commit is contained in:
shaohuzhang1 2024-04-23 19:03:34 +08:00 committed by GitHub
parent 73dfcb6fb0
commit b26265fefd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 557 additions and 44 deletions

View File

@ -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'),
),
]

View File

@ -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():

View File

@ -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':

View File

@ -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")
}
)

View File

@ -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',

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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})。')
}

View File

@ -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',
},
),
]

View File

@ -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"

View File

@ -0,0 +1,42 @@
# coding=utf-8
"""
@project: maxkb
@Author
@file image_serializers.py
@date2024/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'})

View File

@ -0,0 +1,22 @@
# coding=utf-8
"""
@project: maxkb
@Author
@file image_api.py
@date2024/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='上传图片文件')
]

View File

@ -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())
]

View File

@ -10,3 +10,4 @@ from .dataset import *
from .document import *
from .paragraph import *
from .problem import *
from .image import *

View File

@ -0,0 +1,43 @@
# coding=utf-8
"""
@project: maxkb
@Author
@file image.py
@date2024/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()

View File

@ -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
}

15
ui/src/api/image.ts Normal file
View File

@ -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
}

View File

@ -10,6 +10,7 @@ interface ApplicationFormType {
dataset_setting?: any
model_setting?: any
problem_optimization?: boolean
icon?: string | undefined
}
interface chatType {
id: string

View File

@ -18,6 +18,7 @@
class="mr-8"
:size="24"
/>
<AppAvatar
v-else-if="isDataset && currentType === '1'"
class="mr-8 avatar-purple"

View File

@ -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)
})
})
}
}
})

View File

@ -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);
}

View File

@ -0,0 +1,6 @@
export const defaultIcon = '/ui/favicon.ico'
// 是否显示字母 / icon
export function isAppIcon(url: string | undefined) {
return url === defaultIcon ? '' : url
}

View File

@ -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支持 icopng , 大小不超过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>

View File

@ -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>

View File

@ -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 {

View File

@ -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()

View File

@ -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 {

View File

@ -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 = {