Merge pull request #21 from 1Panel-dev/pr@main@application_statistics

Pr@main@application statistics
This commit is contained in:
shaohuzhang1 2024-03-29 15:48:56 +08:00 committed by GitHub
commit 6736573dea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 1012 additions and 84 deletions

View File

@ -44,7 +44,7 @@ class PostResponseHandler:
@abstractmethod
def handler(self, chat_id, chat_record_id, paragraph_list: List[ParagraphPipelineModel], problem_text: str,
answer_text,
manage, step, padding_problem_text: str = None, **kwargs):
manage, step, padding_problem_text: str = None, client_id=None, **kwargs):
pass

View File

@ -67,7 +67,7 @@ def event_content(response,
manage.context['message_tokens'] = manage.context['message_tokens'] + request_token
manage.context['answer_tokens'] = manage.context['answer_tokens'] + response_token
post_response_handler.handler(chat_id, chat_record_id, paragraph_list, problem_text,
all_text, manage, step, padding_problem_text)
all_text, manage, step, padding_problem_text, client_id)
yield 'data: ' + json.dumps({'chat_id': str(chat_id), 'id': str(chat_record_id), 'operate': True,
'content': '', 'is_end': True}) + "\n\n"
add_access_num(client_id, client_type)
@ -172,7 +172,7 @@ class BaseChatStep(IChatStep):
manage.context['message_tokens'] = manage.context['message_tokens'] + request_token
manage.context['answer_tokens'] = manage.context['answer_tokens'] + response_token
post_response_handler.handler(chat_id, chat_record_id, paragraph_list, problem_text,
chat_result.content, manage, self, padding_problem_text)
chat_result.content, manage, self, padding_problem_text, client_id)
add_access_num(client_id, client_type)
return result.success({'chat_id': str(chat_id), 'id': str(chat_record_id), 'operate': True,
'content': chat_result.content, 'is_end': True})

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.13 on 2024-03-28 13:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('application', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='chat',
name='client_id',
field=models.UUIDField(default=None, null=True, verbose_name='客户端id'),
),
]

View File

@ -69,6 +69,7 @@ class Chat(AppModelMixin):
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid1, editable=False, verbose_name="主键id")
application = models.ForeignKey(Application, on_delete=models.CASCADE)
abstract = models.CharField(max_length=256, verbose_name="摘要")
client_id = models.UUIDField(verbose_name="客户端id", default=None, null=True)
class Meta:
db_table = "application_chat"

View File

@ -0,0 +1,128 @@
# coding=utf-8
"""
@project: maxkb
@Author
@file application_statistics_serializers.py
@date2024/3/27 10:55
@desc:
"""
import datetime
import os
from typing import List, Dict
from django.db import models
from django.db.models.query import QuerySet
from rest_framework import serializers
from application.models.api_key_model import ApplicationPublicAccessClient
from common.db.search import native_search, get_dynamics_model
from common.util.field_message import ErrMessage
from common.util.file_util import get_file_content
from smartdoc.conf import PROJECT_DIR
class ApplicationStatisticsSerializer(serializers.Serializer):
application_id = serializers.UUIDField(required=True, error_messages=ErrMessage.char("应用id"))
start_time = serializers.DateField(format='%Y-%m-%d', error_messages=ErrMessage.date("开始时间"))
end_time = serializers.DateField(format='%Y-%m-%d', error_messages=ErrMessage.date("结束时间"))
def get_end_time(self):
return datetime.datetime.combine(
datetime.datetime.strptime(self.data.get('end_time'), '%Y-%m-%d'),
datetime.datetime.max.time())
def get_start_time(self):
return self.data.get('start_time')
def get_customer_count(self, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
start_time = self.get_start_time()
end_time = self.get_end_time()
return native_search(
QuerySet(ApplicationPublicAccessClient).filter(application_id=self.data.get('application_id'),
create_time__gte=start_time,
create_time__lte=end_time),
select_string=get_file_content(
os.path.join(PROJECT_DIR, "apps", "application", 'sql', 'customer_count.sql')),
with_search_one=True)
def get_customer_count_trend(self, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
start_time = self.get_start_time()
end_time = self.get_end_time()
return native_search(
{'default_sql': QuerySet(ApplicationPublicAccessClient).filter(
application_id=self.data.get('application_id'),
create_time__gte=start_time,
create_time__lte=end_time)},
select_string=get_file_content(
os.path.join(PROJECT_DIR, "apps", "application", 'sql', 'customer_count_trend.sql')))
def get_chat_record_aggregate(self, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
start_time = self.get_start_time()
end_time = self.get_end_time()
chat_record_aggregate = native_search(
QuerySet(model=get_dynamics_model(
{'application_chat.application_id': models.UUIDField(),
'application_chat_record.create_time': models.DateTimeField()})).filter(
**{'application_chat.application_id': self.data.get('application_id'),
'application_chat_record.create_time__gte': start_time,
'application_chat_record.create_time__lte': end_time}
),
select_string=get_file_content(
os.path.join(PROJECT_DIR, "apps", "application", 'sql', 'chat_record_count.sql')),
with_search_one=True)
customer = self.get_customer_count(with_valid=False)
return {**chat_record_aggregate, **customer}
def get_chat_record_aggregate_trend(self, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
start_time = self.get_start_time()
end_time = self.get_end_time()
chat_record_aggregate_trend = native_search(
{'default_sql': QuerySet(model=get_dynamics_model(
{'application_chat.application_id': models.UUIDField(),
'application_chat_record.create_time': models.DateTimeField()})).filter(
**{'application_chat.application_id': self.data.get('application_id'),
'application_chat_record.create_time__gte': start_time,
'application_chat_record.create_time__lte': end_time}
)},
select_string=get_file_content(
os.path.join(PROJECT_DIR, "apps", "application", 'sql', 'chat_record_count_trend.sql')))
customer_count_trend = self.get_customer_count_trend(with_valid=False)
return self.merge_customer_chat_record(chat_record_aggregate_trend, customer_count_trend)
def merge_customer_chat_record(self, chat_record_aggregate_trend: List[Dict], customer_count_trend: List[Dict]):
return [{**self.find(chat_record_aggregate_trend, lambda c: c.get('day').strftime('%Y-%m-%d') == day,
{'star_num': 0, 'trample_num': 0, 'tokens_num': 0, 'chat_record_count': 0,
'customer_num': 0,
'day': day}),
**self.find(customer_count_trend, lambda c: c.get('day').strftime('%Y-%m-%d') == day,
{'customer_added_count': 0})}
for
day in
self.get_days_between_dates(self.data.get('start_time'), self.data.get('end_time'))]
@staticmethod
def find(source_list, condition, default):
value_list = [row for row in source_list if condition(row)]
if len(value_list) > 0:
return value_list[0]
return default
@staticmethod
def get_days_between_dates(start_date, end_date):
start_date = datetime.datetime.strptime(start_date, '%Y-%m-%d')
end_date = datetime.datetime.strptime(end_date, '%Y-%m-%d')
days = []
current_date = start_date
while current_date <= end_date:
days.append(current_date.strftime('%Y-%m-%d'))
current_date += datetime.timedelta(days=1)
return days

View File

@ -87,13 +87,14 @@ class ChatInfo:
'exclude_paragraph_id_list': exclude_paragraph_id_list, 'stream': stream, 'client_id': client_id,
'client_type': client_type}
def append_chat_record(self, chat_record: ChatRecord):
def append_chat_record(self, chat_record: ChatRecord, client_id=None):
# 存入缓存中
self.chat_record_list.append(chat_record)
if self.application.id is not None:
# 插入数据库
if not QuerySet(Chat).filter(id=self.chat_id).exists():
Chat(id=self.chat_id, application_id=self.application.id, abstract=chat_record.problem_text).save()
Chat(id=self.chat_id, application_id=self.application.id, abstract=chat_record.problem_text,
client_id=client_id).save()
# 插入会话记录
chat_record.save()
@ -110,6 +111,7 @@ def get_post_handler(chat_info: ChatInfo):
manage: PiplineManage,
step: BaseChatStep,
padding_problem_text: str = None,
client_id=None,
**kwargs):
chat_record = ChatRecord(id=chat_record_id,
chat_id=chat_id,
@ -120,7 +122,7 @@ def get_post_handler(chat_info: ChatInfo):
answer_tokens=manage.context['answer_tokens'],
run_time=manage.context['run_time'],
index=len(chat_info.chat_record_list) + 1)
chat_info.append_chat_record(chat_record)
chat_info.append_chat_record(chat_record, client_id)
# 重新设置缓存
chat_cache.set(chat_id,
chat_info, timeout=60 * 30)

View File

@ -0,0 +1,9 @@
SELECT SUM
( CASE WHEN application_chat_record.vote_status = '0' THEN 1 ELSE 0 END ) AS "star_num",
SUM ( CASE WHEN application_chat_record.vote_status = '1' THEN 1 ELSE 0 END ) AS "trample_num",
SUM ( application_chat_record.message_tokens + application_chat_record.answer_tokens ) as "tokens_num",
"count"(DISTINCT application_chat.client_id) customer_num,
"count"(application_chat_record."id") as chat_record_count
FROM
application_chat_record application_chat_record
LEFT JOIN application_chat application_chat ON application_chat."id" = application_chat_record.chat_id

View File

@ -0,0 +1,12 @@
SELECT SUM
( CASE WHEN application_chat_record.vote_status = '0' THEN 1 ELSE 0 END ) AS "star_num",
SUM ( CASE WHEN application_chat_record.vote_status = '1' THEN 1 ELSE 0 END ) AS "trample_num",
SUM ( application_chat_record.message_tokens + application_chat_record.answer_tokens ) as "tokens_num",
"count"(application_chat_record."id") as chat_record_count,
"count"(DISTINCT application_chat.client_id) customer_num,
application_chat_record.create_time :: DATE as "day"
FROM
application_chat_record application_chat_record
LEFT JOIN application_chat application_chat ON application_chat."id" = application_chat_record.chat_id
${default_sql}
GROUP BY "day"

View File

@ -0,0 +1,5 @@
SELECT
( SUM ( CASE WHEN create_time :: DATE = CURRENT_DATE THEN 1 ELSE 0 END ) ) AS "customer_today_added_count",
COUNT ( "application_public_access_client"."id" ) AS "customer_added_count"
FROM
"application_public_access_client"

View File

@ -0,0 +1,7 @@
SELECT
COUNT ( "application_public_access_client"."id" ) AS "customer_added_count",
create_time :: DATE as "day"
FROM
"application_public_access_client"
${default_sql}
GROUP BY "day"

View File

@ -0,0 +1,86 @@
# coding=utf-8
"""
@project: maxkb
@Author
@file application_statistics_api.py
@date2024/3/27 15:09
@desc:
"""
from drf_yasg import openapi
from common.mixins.api_mixin import ApiMixin
class ApplicationStatisticsApi(ApiMixin):
@staticmethod
def get_request_params_api():
return [openapi.Parameter(name='application_id',
in_=openapi.IN_PATH,
type=openapi.TYPE_STRING,
required=True,
description='应用id'),
openapi.Parameter(name='start_time',
in_=openapi.IN_QUERY,
type=openapi.TYPE_STRING,
required=True,
description='开始时间'),
openapi.Parameter(name='end_time',
in_=openapi.IN_QUERY,
type=openapi.TYPE_STRING,
required=True,
description='结束时间'),
]
class ChatRecordAggregate(ApiMixin):
@staticmethod
def get_response_body_api():
return openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['star_num', 'trample_num', 'tokens_num', 'chat_record_count'],
properties={
'star_num': openapi.Schema(type=openapi.TYPE_NUMBER, title="点赞数量",
description="点赞数量"),
'trample_num': openapi.Schema(type=openapi.TYPE_NUMBER, title="点踩数量", description="点踩数量"),
'tokens_num': openapi.Schema(type=openapi.TYPE_NUMBER, title="token使用数量",
description="token使用数量"),
'chat_record_count': openapi.Schema(type=openapi.TYPE_NUMBER, title="对话次数",
description="对话次数"),
'customer_num': openapi.Schema(type=openapi.TYPE_NUMBER, title="客户数量",
description="客户数量"),
'customer_added_count': openapi.Schema(type=openapi.TYPE_NUMBER, title="客户新增数量",
description="客户新增数量"),
'day': openapi.Schema(type=openapi.TYPE_STRING,
title="日期",
description="日期,只有查询趋势的时候才有该字段"),
}
)
class CustomerCountTrend(ApiMixin):
@staticmethod
def get_response_body_api():
return openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['added_count'],
properties={
'added_count': openapi.Schema(type=openapi.TYPE_NUMBER, title="新增数量", description="新增数量"),
'day': openapi.Schema(type=openapi.TYPE_STRING,
title="时间",
description="时间"),
}
)
class CustomerCount(ApiMixin):
@staticmethod
def get_response_body_api():
return openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['added_count'],
properties={
'today_added_count': openapi.Schema(type=openapi.TYPE_NUMBER, title="今日新增数量",
description="今日新增数量"),
'added_count': openapi.Schema(type=openapi.TYPE_NUMBER, title="新增数量", description="新增数量"),
}
)

View File

@ -8,6 +8,14 @@ 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>/statistics/customer_count',
views.ApplicationStatistics.CustomerCount.as_view()),
path('application/<str:application_id>/statistics/customer_count_trend',
views.ApplicationStatistics.CustomerCountTrend.as_view()),
path('application/<str:application_id>/statistics/chat_record_aggregate',
views.ApplicationStatistics.ChatRecordAggregate.as_view()),
path('application/<str:application_id>/statistics/chat_record_aggregate_trend',
views.ApplicationStatistics.ChatRecordAggregateTrend.as_view()),
path('application/<str:application_id>/model', views.Application.Model.as_view()),
path('application/<str:application_id>/hit_test', views.Application.HitTest.as_view()),
path('application/<str:application_id>/api_key', views.Application.ApplicationKey.as_view()),

View File

@ -14,7 +14,9 @@ from rest_framework.request import Request
from rest_framework.views import APIView
from application.serializers.application_serializers import ApplicationSerializer
from application.serializers.application_statistics_serializers import ApplicationStatisticsSerializer
from application.swagger_api.application_api import ApplicationApi
from application.swagger_api.application_statistics_api import ApplicationStatisticsApi
from common.auth import TokenAuth, has_permissions
from common.constants.permission_constants import CompareConstants, PermissionConstants, Permission, Group, Operate, \
ViewPermission, RoleConstants
@ -25,6 +27,107 @@ from common.util.common import query_params_to_single_dict
from dataset.serializers.dataset_serializers import DataSetSerializers
class ApplicationStatistics(APIView):
class CustomerCount(APIView):
authentication_classes = [TokenAuth]
@action(methods=["GET"], detail=False)
@swagger_auto_schema(operation_summary="用户统计",
operation_id="用户统计",
tags=["应用/统计"],
manual_parameters=ApplicationStatisticsApi.get_request_params_api(),
responses=result.get_api_response(
ApplicationStatisticsApi.CustomerCount.get_response_body_api())
)
@has_permissions(ViewPermission(
[RoleConstants.ADMIN, RoleConstants.USER],
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.USE,
dynamic_tag=keywords.get('application_id'))],
compare=CompareConstants.AND))
def get(self, request: Request, application_id: str):
return result.success(
ApplicationStatisticsSerializer(data={'application_id': application_id,
'start_time': request.query_params.get(
'start_time'),
'end_time': request.query_params.get(
'end_time')
}).get_customer_count())
class CustomerCountTrend(APIView):
authentication_classes = [TokenAuth]
@action(methods=["GET"], detail=False)
@swagger_auto_schema(operation_summary="用户统计趋势",
operation_id="用户统计趋势",
tags=["应用/统计"],
manual_parameters=ApplicationStatisticsApi.get_request_params_api(),
responses=result.get_api_array_response(
ApplicationStatisticsApi.CustomerCountTrend.get_response_body_api()))
@has_permissions(ViewPermission(
[RoleConstants.ADMIN, RoleConstants.USER],
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.USE,
dynamic_tag=keywords.get('application_id'))],
compare=CompareConstants.AND))
def get(self, request: Request, application_id: str):
return result.success(
ApplicationStatisticsSerializer(data={'application_id': application_id,
'start_time': request.query_params.get(
'start_time'),
'end_time': request.query_params.get(
'end_time')
}).get_customer_count_trend())
class ChatRecordAggregate(APIView):
authentication_classes = [TokenAuth]
@action(methods=["GET"], detail=False)
@swagger_auto_schema(operation_summary="对话相关统计",
operation_id="对话相关统计",
tags=["应用/统计"],
manual_parameters=ApplicationStatisticsApi.get_request_params_api(),
responses=result.get_api_response(
ApplicationStatisticsApi.ChatRecordAggregate.get_response_body_api())
)
@has_permissions(ViewPermission(
[RoleConstants.ADMIN, RoleConstants.USER],
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.USE,
dynamic_tag=keywords.get('application_id'))],
compare=CompareConstants.AND))
def get(self, request: Request, application_id: str):
return result.success(
ApplicationStatisticsSerializer(data={'application_id': application_id,
'start_time': request.query_params.get(
'start_time'),
'end_time': request.query_params.get(
'end_time')
}).get_chat_record_aggregate())
class ChatRecordAggregateTrend(APIView):
authentication_classes = [TokenAuth]
@action(methods=["GET"], detail=False)
@swagger_auto_schema(operation_summary="对话相关统计趋势",
operation_id="对话相关统计趋势",
tags=["应用/统计"],
manual_parameters=ApplicationStatisticsApi.get_request_params_api(),
responses=result.get_api_array_response(
ApplicationStatisticsApi.ChatRecordAggregate.get_response_body_api())
)
@has_permissions(ViewPermission(
[RoleConstants.ADMIN, RoleConstants.USER],
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.USE,
dynamic_tag=keywords.get('application_id'))],
compare=CompareConstants.AND))
def get(self, request: Request, application_id: str):
return result.success(
ApplicationStatisticsSerializer(data={'application_id': application_id,
'start_time': request.query_params.get(
'start_time'),
'end_time': request.query_params.get(
'end_time')
}).get_chat_record_aggregate_trend())
class Application(APIView):
authentication_classes = [TokenAuth]

View File

@ -86,3 +86,12 @@ class ErrMessage:
'required': gettext_lazy('%s】此字段必填。' % field),
'null': gettext_lazy('%s】此字段不能为null。' % field),
}
@staticmethod
def date(field: str):
return {
'required': gettext_lazy('%s】此字段必填。' % field),
'null': gettext_lazy('%s】此字段不能为null。' % field),
'invalid': gettext_lazy('%s】日期格式错误。请改用以下格式之一: {format}'),
'datetime': gettext_lazy('%s】应为日期,但得到的是日期时间。')
}

View File

@ -14,6 +14,7 @@
},
"dependencies": {
"axios": "^0.28.0",
"echarts": "^5.5.0",
"element-plus": "^2.5.6",
"install": "^0.13.0",
"lodash": "^4.17.21",
@ -29,6 +30,7 @@
"md-editor-v3": "^4.12.1",
"medium-zoom": "^1.1.0",
"mitt": "^3.0.0",
"moment": "^2.30.1",
"npm": "^10.2.4",
"nprogress": "^0.2.0",
"pinia": "^2.1.6",

View File

@ -55,9 +55,22 @@ const putAPIKey: (
return put(`${prefix}/${applicaiton_id}/api_key/${api_key_id}`, data, undefined, loading)
}
/**
*
* @param applicaiton_id, data
*/
const getStatistics: (
applicaiton_id: string,
data: any,
loading?: Ref<boolean>
) => Promise<Result<any>> = (applicaiton_id, data, loading) => {
return get(`${prefix}/${applicaiton_id}/statistics/chat_record_aggregate_trend`, data, loading)
}
export default {
getAPIKey,
postAPIKey,
delAPIKey,
putAPIKey
putAPIKey,
getStatistics
}

View File

@ -0,0 +1,128 @@
<template>
<div :id="id" ref="PieChartRef" :style="{ height: height, width: width }" />
</template>
<script lang="ts" setup>
import { onMounted, nextTick, watch, onBeforeUnmount } from 'vue'
import * as echarts from 'echarts'
const props = defineProps({
id: {
type: String,
default: 'lineChartId'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '200px'
},
option: {
type: Object,
required: true
} // option: { title , data }
})
const color = ['rgba(82, 133, 255, 1)', 'rgba(255, 207, 47, 1)']
const areaColor = ['rgba(82, 133, 255, 0.2)', 'rgba(255, 207, 47, 0.2)']
function initChart() {
let myChart = echarts?.getInstanceByDom(document.getElementById(props.id)!)
if (myChart === null || myChart === undefined) {
myChart = echarts.init(document.getElementById(props.id))
}
const series: any = []
if (props.option?.yDatas?.length) {
props.option?.yDatas.forEach((item: any, index: number) => {
series.push({
itemStyle: {
color: color[index]
},
areaStyle: item.area
? {
color: areaColor[index]
}
: null,
...item
})
})
}
const option = {
title: {
text: props.option?.title,
textStyle: {
fontSize: '16px'
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
legend: {
right: 0,
itemWidth: 8,
textStyle: {
color: '#646A73'
},
icon: 'circle'
},
grid: {
left: '1%',
right: '1%',
bottom: '0',
top: '18%',
containLabel: true
},
xAxis: {
type: 'category',
data: props.option.xDatas
},
yAxis: {
type: 'value',
splitLine: {
lineStyle: {
color: '#EFF0F1'
}
}
},
series: series
}
//
myChart.setOption(option, true)
}
function changeChartSize() {
echarts.getInstanceByDom(document.getElementById(props.id)!)?.resize()
}
watch(
() => props.option,
(val) => {
if (val) {
nextTick(() => {
initChart()
})
}
}
)
onMounted(() => {
nextTick(() => {
initChart()
window.addEventListener('resize', changeChartSize)
})
})
onBeforeUnmount(() => {
echarts.getInstanceByDom(document.getElementById(props.id)!)?.dispose()
window.removeEventListener('resize', changeChartSize)
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,30 @@
<template>
<component
:is="typeComponentMap[type]"
:height="height"
:option="option"
:dataZoom="dataZoom"
class="v-charts"
/>
</template>
<script lang="ts" setup>
import line from './components/LineCharts.vue'
defineOptions({ name: 'AppCharts' })
defineProps({
type: {
type: String,
default: 'line'
},
height: {
type: String,
default: '200px'
},
dataZoom: Boolean,
option: {
type: Object,
required: true
} // { title , xDatas, yDatas, formatStr }
})
const typeComponentMap = { line } as any
</script>

View File

@ -640,5 +640,93 @@ export const iconMap: any = {
)
])
}
},
'app-user': {
iconReader: () => {
return h('i', [
h(
'svg',
{
style: { height: '100%', width: '100%' },
viewBox: '0 0 24 24',
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M15 13H9C6.23858 13 3 14.9314 3 18.4V21.1C3 21.597 3.44772 22 4 22H20C20.5523 22 21 21.597 21 21.1V18.4C21 14.9285 17.7614 13 15 13Z',
fill: 'currentColor'
}),
h('path', {
d: 'M7 6.99997C7 9.76139 9.23858 12 12 12C14.7614 12 17 9.76139 17 6.99997C17 4.23855 14.7614 1.99997 12 1.99997C9.23858 1.99997 7 4.23855 7 6.99997Z',
fill: 'currentColor'
})
]
)
])
}
},
'app-question': {
iconReader: () => {
return h('i', [
h(
'svg',
{
style: { height: '100%', width: '100%' },
viewBox: '0 0 24 24',
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M12.7071 22.2009L17 18.5111H21.5C22.0523 18.5111 22.5 18.0539 22.5 17.4899V2.52112C22.5 1.95715 22.0523 1.49997 21.5 1.49997H2C1.44772 1.49997 1 1.95715 1 2.52112V17.4899C1 18.0539 1.44772 18.5111 2 18.5111H7L11.2929 22.2009C11.6834 22.5997 12.3166 22.5997 12.7071 22.2009ZM6.5 8.49997H7.5C8.05228 8.49997 8.5 8.94768 8.5 9.49997V10.5C8.5 11.0523 8.05228 11.5 7.5 11.5H6.5C5.94772 11.5 5.5 11.0523 5.5 10.5V9.49997C5.5 8.94768 5.94772 8.49997 6.5 8.49997ZM10.5 9.49997C10.5 8.94768 10.9477 8.49997 11.5 8.49997H12.5C13.0523 8.49997 13.5 8.94768 13.5 9.49997V10.5C13.5 11.0523 13.0523 11.5 12.5 11.5H11.5C10.9477 11.5 10.5 11.0523 10.5 10.5V9.49997ZM16.5 8.49997H17.5C18.0523 8.49997 18.5 8.94768 18.5 9.49997V10.5C18.5 11.0523 18.0523 11.5 17.5 11.5H16.5C15.9477 11.5 15.5 11.0523 15.5 10.5V9.49997C15.5 8.94768 15.9477 8.49997 16.5 8.49997Z',
fill: 'currentColor'
})
]
)
])
}
},
'app-tokens': {
iconReader: () => {
return h('i', [
h(
'svg',
{
style: { height: '100%', width: '100%' },
viewBox: '0 0 24 24',
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M15.6 2.39996C12.288 2.39996 9.60002 5.08796 9.60002 8.39996C9.60002 9.11996 9.74402 9.79196 9.97202 10.428L2.47325 17.9267C2.42636 17.9736 2.40002 18.0372 2.40002 18.1035V21.1C2.40002 21.3761 2.62388 21.6 2.90002 21.6H4.30002C4.57617 21.6 4.80002 21.3761 4.80002 21.1V20.4H6.70003C6.97617 20.4 7.20002 20.1761 7.20002 19.9V18H8.40002L10.8 15.6H12L13.572 14.028C14.208 14.256 14.88 14.4 15.6 14.4C18.912 14.4 21.6 11.712 21.6 8.39996C21.6 5.08796 18.912 2.39996 15.6 2.39996ZM17.4 8.39996C16.404 8.39996 15.6 7.59596 15.6 6.59996C15.6 5.60396 16.404 4.79996 17.4 4.79996C18.396 4.79996 19.2 5.60396 19.2 6.59996C19.2 7.59596 18.396 8.39996 17.4 8.39996Z',
fill: 'currentColor'
})
]
)
])
}
},
'app-user-stars': {
iconReader: () => {
return h('i', [
h(
'svg',
{
style: { height: '100%', width: '100%' },
viewBox: '0 0 24 24',
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M12 23C18.0751 23 23 18.0751 23 12C23 5.92484 18.0751 0.999969 12 0.999969C5.92487 0.999969 1 5.92484 1 12C1 18.0751 5.92487 23 12 23ZM8.5 10.5C7.67157 10.5 7 9.8284 7 8.99997C7 8.17154 7.67157 7.49997 8.5 7.49997C9.32843 7.49997 10 8.17154 10 8.99997C10 9.8284 9.32843 10.5 8.5 10.5ZM17 8.99997C17 9.8284 16.3284 10.5 15.5 10.5C14.6716 10.5 14 9.8284 14 8.99997C14 8.17154 14.6716 7.49997 15.5 7.49997C16.3284 7.49997 17 8.17154 17 8.99997ZM16.9779 13.4994C16.7521 16.0264 14.8169 18 12 18C9.18312 18 7.24789 16.0264 7.02213 13.4994C6.99756 13.2244 7.22386 13 7.5 13H16.5C16.7761 13 17.0024 13.2244 16.9779 13.4994Z',
fill: 'currentColor'
})
]
)
])
}
}
}

View File

@ -105,6 +105,9 @@ h4 {
.h-full {
height: 100%;
}
.w-120 {
width: 120px;
}
.w-240 {
width: 240px;
}
@ -206,6 +209,10 @@ h4 {
align-items: center;
}
.align-baseline {
align-items: baseline;
}
.text-left {
text-align: left;
}
@ -424,9 +431,13 @@ h4 {
color: var(--el-color-info);
}
.color-secondary {
color: var(--app-text-color-secondary);
}
.app-warning-icon {
font-size: 16px;
color: #646a73;
color: var(--app-text-color-secondary);
}
.dotting {

View File

@ -1,3 +1,13 @@
import moment from 'moment'
// 当天日期 YYYY-MM-DD
export const nowDate = moment().format('YYYY-MM-DD')
// 当前时间的前n天
export function beforeDay(n) {
return moment().subtract(n, 'days').format('YYYY-MM-DD')
}
const getCheckDate = (timestamp: any) => {
if (!timestamp) return false
const dt = new Date(timestamp)

View File

@ -64,3 +64,15 @@ export function isAllPropertiesEmpty(obj: object) {
value === null || typeof value === 'undefined' || (typeof value === 'string' && !value)
)
}
// 数组对象中某一属性值的集合
export function getAttrsArray(array, attr) {
return array.map((item) => {
return item[attr]
})
}
// 求和
export function getSum(array) {
return array.reduce((totol, item) => totol + item, 0)
}

View File

@ -0,0 +1,161 @@
<template>
<el-row :gutter="16">
<el-col
:xs="12"
:sm="12"
:md="12"
:lg="6"
:xl="6"
v-for="(item, index) in statisticsType"
:key="index"
class="mb-16"
>
<el-card shadow="never">
<div class="flex align-center ml-8 mr-8">
<el-avatar :size="40" shape="square" :style="{ background: item.background }">
<appIcon :iconName="item.icon" :style="{ fontSize: '24px', color: item.color }" />
</el-avatar>
<div class="ml-12">
<p class="color-secondary lighter mb-4">{{ item.name }}</p>
<div v-if="item.id !== 'starCharts'" class="flex align-baseline">
<h2>{{ numberFormat(item.sum?.[0]) }}</h2>
<span v-if="item.sum.length > 1" class="ml-12" style="color: #f54a45"
>+{{ numberFormat(item.sum?.[1]) }}</span
>
</div>
<div v-else class="flex align-center mr-8">
<AppIcon iconName="app-like-color"></AppIcon>
<h2 class="ml-4">{{ item.sum?.[0] }}</h2>
<AppIcon class="ml-12" iconName="app-oppose-color"></AppIcon>
<h2 class="ml-4">{{ item.sum?.[1] }}</h2>
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col
:xs="24"
:sm="24"
:md="24"
:lg="12"
:xl="12"
v-for="(item, index) in statisticsType"
:key="index"
class="mb-16"
>
<el-card shadow="never">
<div class="p-8">
<AppCharts height="316px" :id="item.id" type="line" :option="item.option" />
</div>
</el-card>
</el-col>
</el-row>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import AppCharts from '@/components/app-charts/index.vue'
import { getAttrsArray, getSum, numberFormat } from '@/utils/utils'
const props = defineProps({
data: {
type: Array,
default: () => []
}
})
const statisticsType = computed(() => [
{
id: 'customerCharts',
name: '用户总数',
icon: 'app-user',
background: '#EBF1FF',
color: '#3370FF',
sum: [
getSum(getAttrsArray(props.data, 'customer_num') || 0),
getSum(getAttrsArray(props.data, 'customer_added_count') || 0)
],
option: {
title: '用户总数',
xDatas: getAttrsArray(props.data, 'day'),
yDatas: [
{
name: '用户总数',
type: 'line',
area: true,
data: getAttrsArray(props.data, 'customer_num')
},
{
name: '用户新增数',
type: 'line',
area: true,
data: getAttrsArray(props.data, 'customer_added_count')
}
]
}
},
{
id: 'chatRecordCharts',
name: '提问次数',
icon: 'app-question',
background: '#FFF3E5',
color: '#FF8800',
sum: [getSum(getAttrsArray(props.data, 'chat_record_count') || 0)],
option: {
title: '提问次数',
xDatas: getAttrsArray(props.data, 'day'),
yDatas: [
{
type: 'line',
data: getAttrsArray(props.data, 'chat_record_count')
}
]
}
},
{
id: 'tokensCharts',
name: 'Tokens 总数',
icon: 'app-tokens',
background: '#E5FBF8',
color: '#00D6B9',
sum: [getSum(getAttrsArray(props.data, 'tokens_num') || 0)],
option: {
title: 'Tokens 总数',
xDatas: getAttrsArray(props.data, 'day'),
yDatas: [
{
type: 'line',
data: getAttrsArray(props.data, 'tokens_num')
}
]
}
},
{
id: 'starCharts',
name: '用户满意度',
icon: 'app-user-stars',
background: '#FEEDEC',
color: '#F54A45',
sum: [
getSum(getAttrsArray(props.data, 'star_num') || 0),
getSum(getAttrsArray(props.data, 'trample_num') || 0)
],
option: {
title: '用户满意度',
xDatas: getAttrsArray(props.data, 'day'),
yDatas: [
{
name: '赞同',
type: 'line',
data: getAttrsArray(props.data, 'star_num')
},
{
name: '反对',
type: 'line',
data: getAttrsArray(props.data, 'trample_num')
}
]
}
}
])
</script>
<style lang="scss" scoped></style>

View File

@ -1,78 +1,104 @@
<template>
<LayoutContainer header="概览">
<div class="main-calc-height p-24" style="min-width: 600px">
<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"
<el-scrollbar>
<div class="main-calc-height p-24">
<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"
/>
<h4>{{ detail?.name }}</h4>
</div>
<el-row :gutter="12">
<el-col :span="12" class="mt-16">
<div class="flex">
<el-text type="info">公开访问链接</el-text>
<el-switch
v-model="accessToken.is_active"
class="ml-8"
size="small"
inline-prompt
active-text="开"
inactive-text="关"
@change="changeState($event)"
/>
</div>
<div class="mt-4 mb-16 url-height">
<span class="vertical-middle lighter break-all">
{{ shareUrl }}
</span>
<el-button type="primary" text @click="copyClick(shareUrl)">
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
<el-button @click="refreshAccessToken" type="primary" text style="margin-left: 1px">
<el-icon><RefreshRight /></el-icon>
</el-button>
</div>
<div>
<el-button :disabled="!accessToken?.is_active" type="primary">
<a v-if="accessToken?.is_active" :href="shareUrl" target="_blank"> 演示 </a>
<span v-else>演示</span>
</el-button>
<el-button :disabled="!accessToken?.is_active" @click="openDialog">
嵌入第三方
</el-button>
<el-button @click="openLimitDialog"> 访问限制 </el-button>
</div>
</el-col>
<el-col :span="12" class="mt-16">
<div class="flex">
<el-text type="info">API访问凭据</el-text>
</div>
<div class="mt-4 mb-16 url-height">
<span class="vertical-middle lighter break-all">
{{ apiUrl }}
</span>
<el-button type="primary" text @click="copyClick(apiUrl)">
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
</div>
<div>
<el-button @click="openAPIKeyDialog"> API Key </el-button>
</div>
</el-col>
</el-row>
</el-card>
<h4 class="title-decoration-1 mt-16 mb-16">监控统计</h4>
<div class="mb-16">
<el-select v-model="history_day" class="mr-12 w-120" @change="changeDayHandle">
<el-option
v-for="item in dayOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-date-picker
v-if="history_day === 'other'"
v-model="daterangeValue"
type="daterange"
start-placeholder="开始时间"
end-placeholder="结束时间"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
@change="changeDayRangeHandle"
/>
<h4>{{ detail?.name }}</h4>
</div>
<el-row :gutter="12">
<el-col :span="12" class="mt-16">
<div class="flex">
<el-text type="info">公开访问链接</el-text>
<el-switch
v-model="accessToken.is_active"
class="ml-8"
size="small"
inline-prompt
active-text="开"
inactive-text="关"
@change="changeState($event)"
/>
</div>
<div class="mt-4 mb-16 url-height">
<span class="vertical-middle lighter break-all">
{{ shareUrl }}
</span>
<el-button type="primary" text @click="copyClick(shareUrl)">
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
<el-button @click="refreshAccessToken" type="primary" text style="margin-left: 1px">
<el-icon><RefreshRight /></el-icon>
</el-button>
</div>
<div>
<el-button :disabled="!accessToken?.is_active" type="primary">
<a v-if="accessToken?.is_active" :href="shareUrl" target="_blank"> 演示 </a>
<span v-else>演示</span>
</el-button>
<el-button :disabled="!accessToken?.is_active" @click="openDialog">
嵌入第三方
</el-button>
<el-button @click="openLimitDialog"> 访问限制 </el-button>
</div>
</el-col>
<el-col :span="12" class="mt-16">
<div class="flex">
<el-text type="info">API访问凭据</el-text>
</div>
<div class="mt-4 mb-16 url-height">
<span class="vertical-middle lighter break-all">
{{ apiUrl }}
</span>
<el-button type="primary" text @click="copyClick(apiUrl)">
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
</div>
<div>
<el-button @click="openAPIKeyDialog"> API Key </el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
<div v-loading="statisticsLoading">
<StatisticsCharts :data="statisticsData" />
</div>
</div>
</el-scrollbar>
<EmbedDialog ref="EmbedDialogRef" />
<APIKeyDialog ref="APIKeyDialogRef" />
<LimitDialog ref="LimitDialogRef" @refresh="refresh" />
@ -80,16 +106,20 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useRoute } from 'vue-router'
import EmbedDialog from './component/EmbedDialog.vue'
import APIKeyDialog from './component/APIKeyDialog.vue'
import LimitDialog from './component/LimitDialog.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 useStore from '@/stores'
const { application } = useStore()
const router = useRouter()
const route = useRoute()
const {
params: { id }
@ -108,6 +138,63 @@ const loading = ref(false)
const shareUrl = computed(() => application.location + accessToken.value.access_token)
const dayOptions = [
{
value: 7,
label: '过去7天'
},
{
value: 30,
label: '过去30天'
},
{
value: 90,
label: '过去90天'
},
{
value: 183,
label: '过去半年'
},
{
value: 'other',
label: '自定义'
}
]
const history_day = ref<number | string>(7)
//
const daterangeValue = ref('')
//
const daterange = ref({
start_time: '',
end_time: ''
})
const statisticsLoading = ref(false)
const statisticsData = ref([])
function changeDayHandle(val: number | string) {
if (val !== 'other') {
daterange.value.start_time = beforeDay(val)
daterange.value.end_time = nowDate
getAppStatistics()
}
}
function changeDayRangeHandle(val: string) {
daterange.value.start_time = val[0]
daterange.value.end_time = val[1]
getAppStatistics()
}
function getAppStatistics() {
overviewApi.getStatistics(id, daterange.value, statisticsLoading).then((res: any) => {
statisticsData.value = res.data
})
}
function refreshAccessToken() {
const obj = {
access_token_reset: true
@ -158,6 +245,7 @@ function refresh() {
onMounted(() => {
getDetail()
getAccessToken()
changeDayHandle(history_day.value)
})
</script>
<style lang="scss" scoped>
@ -168,8 +256,5 @@ onMounted(() => {
right: 16px;
top: 21px;
}
.url-height {
// min-height: 50px;
}
}
</style>