fetch: 页面移植

对话数据、账单统计移植
This commit is contained in:
杨谢雨 2025-03-06 14:07:05 +08:00
parent 373d8b771b
commit 1285218eea
17 changed files with 924 additions and 16 deletions

View File

@ -0,0 +1,23 @@
import request from '@/config/axios'
export function page(params: any) {
return request.get({
url: '/chat/aigc/conversation/page',
params,
});
}
export function del(id: string) {
return request.delete({
url: `/chat/aigc/conversation/${id}`,
});
}
export function getMessages(conversationId: string) {
return request.get({
url: `/chat/aigc/conversation/messages/${conversationId}`,
});
}

63
src/api/new-ai/message.ts Normal file
View File

@ -0,0 +1,63 @@
import request from '@/config/axios'
/**
*
*/
export interface MessageParams {
text?: string;
username?: string;
role?: string;
pageNum?: number;
pageSize?: number;
}
/**
*
*/
export function list(params: MessageParams) {
return request.get({
url: '/chat/aigc/message/list',
params
})
}
/**
*
*/
export function page(params: MessageParams) {
return request.get({
url: '/chat/aigc/message/page',
params
})
}
/**
*
*/
export function add(data: any) {
return request.post({
url: '/chat/aigc/message',
data
})
}
/**
*
*/
export function update(data: any) {
return request.put({
url: '/chat/aigc/message',
data
})
}
/**
*
*/
export function del(id: string) {
return request.delete({
url: `/chat/aigc/message/${id}`
})
}

View File

@ -0,0 +1,33 @@
import request from '@/config/axios'
export function getReqChartBy30() {
return request.get({
url: `/chat/aigc/statistic/requestBy30`,
});
}
export function getReqChart() {
return request.get({
url: `/chat/aigc/statistic/request`,
});
}
export function getTokenChartBy30() {
return request.get({
url: `/chat/aigc/statistic/tokenBy30`,
});
}
export function getTokenChart() {
return request.get({
url: `/chat/aigc/statistic/token`,
});
}
export function getHomeData() {
return request.get({
url: `/chat/aigc/statistic/home`,
});
}

View File

@ -1,18 +1,4 @@
<!--
- Copyright (c) 2024 LangChat. TyCoding All Rights Reserved.
-
- Licensed under the GNU Affero General Public License, Version 3 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- https://www.gnu.org/licenses/agpl-3.0.html
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue' import { computed, ref } from 'vue'

View File

@ -0,0 +1,107 @@
import { TableColumnCtx } from 'element-plus';
import { ElTag } from 'element-plus';
import { h } from 'vue';
import type { FormSchema } from '@/types/form';
export const columns: Partial<TableColumnCtx<any>>[] = [
{
label: '用户名',
prop: 'username',
align: 'center',
width: 120,
},
{
label: '请求ip',
prop: 'ip',
align: 'center',
width: 120,
},
{
label: '对话角色',
prop: 'role',
align: 'center',
width: 100,
formatter: (row: any) => {
return h(
ElTag,
{
type: row.role === 'user' ? 'success' : 'danger',
size: 'small',
},
{ default: () => row.role }
);
},
},
{
label: '模型名称',
prop: 'model',
align: 'center',
width: 100,
},
{
label: 'Token消耗',
prop: 'tokens',
align: 'center',
width: 100,
},
{
label: '提示词Token消耗',
prop: 'promptTokens',
align: 'center',
width: 120,
},
{
label: '消息内容',
prop: 'message',
formatter: (row: any) => {
return String(row.message).replace(/```|\n/g, '');
},
},
{
label: '会话时间',
prop: 'createTime',
width: 180,
align: 'center',
},
];
export const searchSchemas: FormSchema[] = [
{
field: 'text',
component: 'Input',
label: '内容',
componentProps: {
placeholder: '请输入内容查询',
},
},
{
field: 'username',
component: 'Input',
label: '用户名',
componentProps: {
placeholder: '请输入用户名查询',
},
},
{
field: 'role',
component: 'Select',
label: '对话角色',
componentProps: {
placeholder: '请选择对话角色查询',
style: {
width: '140px',
},
options: [
{
label: 'user',
value: 'user',
},
{
label: 'assistant',
value: 'assistant',
},
],
},
},
];

View File

@ -0,0 +1,165 @@
import { ref, computed } from 'vue'
import type { FormSchema } from '@/types/form'
import type { TableColumn } from '@/types/table'
import { useTable } from '@/hooks/web/useTable'
import { ElTag, dayjs } from 'element-plus'
import { h } from 'vue'
import * as MessageApi from '@/api/new-ai/message'
export default function () {
const searchParams = ref({})
const schema = ref<FormSchema[]>([
{
field: 'text',
component: 'Input',
label: '内容',
componentProps: {
placeholder: '请输入内容查询'
},
colProps: {
span: 6
}
},
{
field: 'username',
component: 'Input',
label: '用户名',
componentProps: {
placeholder: '请输入用户名查询'
},
colProps: {
span: 6
}
},
{
field: 'role',
component: 'Select',
label: '对话角色',
componentProps: {
placeholder: '请选择对话角色查询',
options: [
{
label: 'user',
value: 'user'
},
{
label: 'assistant',
value: 'assistant'
}
]
},
colProps: {
span: 6
}
}
])
const columns = ref<TableColumn[]>([
{
label: '用户名',
field: 'username',
align: 'center',
width: 120
},
{
label: '请求ip',
field: 'ip',
align: 'center',
width: 120
},
{
label: '对话角色',
field: 'role',
align: 'center',
width: 100,
formatter(row) {
return h(
ElTag,
{
type: row.role === 'user' ? 'success' : 'danger',
size: 'small'
},
{
default: () => row.role
}
)
}
},
{
label: '模型名称',
field: 'model',
align: 'center',
width: 100
},
{
label: 'Token消耗',
field: 'tokens',
align: 'center',
width: 100
},
{
label: '提示词Token消耗',
field: 'promptTokens',
align: 'center',
width: 120
},
{
label: '消息内容',
field: 'message',
formatter: (row: any) => {
return String(row.message).replace(/```|\n/g, '')
}
},
{
label: '会话时间',
field: 'createTime',
width: 180,
align: 'center',
formatter: (row: any) => {
return dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss')
}
},
])
const { register, tableObject, methods } = useTable({
getListApi: MessageApi.page,
defaultParams: searchParams.value,
delListApi: MessageApi.del
})
const handleSearch = (values: any) => {
methods.setSearchParams(values)
// methods.getList()
}
const pagination = computed(() => {
return {
total: tableObject.total,
pageSize: tableObject.pageSize,
currentPage: tableObject.currentPage
}
})
const handleDel = async (id: string | number) => {
try {
await methods.delList(id, false)
} catch (error) {
console.error('Failed to delete message:', error)
}
}
onMounted(() => {
methods.getList()
})
return {
schema,
columns,
register,
handleSearch,
methods,
tableObject,
pagination,
handleDel
}
}

View File

@ -0,0 +1,12 @@
import type { FormSchema } from '@/types/form'
export const searchSchemas: FormSchema[] = [
{
field: 'text',
component: 'Input',
label: '内容',
componentProps: {
placeholder: '请输入内容'
}
}
]

View File

@ -0,0 +1,68 @@
<script lang="ts" setup>
import { nextTick, ref } from 'vue';
import { getMessages } from '@/api/new-ai/conversation';
import Message from '@/views/ai/chat/new-chat/message/Message.vue';
const messageRef = ref();
const contentRef = ref();
const loading = ref(true);
const dialogVisible = ref(false);
const info = ref<any>({});
const messages = ref<any>([]);
async function show(row: any) {
dialogVisible.value = true;
await nextTick();
info.value = row;
messages.value = await getMessages(row.id);
loading.value = false;
}
async function handleDelete(row) {
console.log('del', row);
}
defineExpose({ show });
</script>
<template>
<el-drawer
v-model="dialogVisible"
:title="info.title"
size="1000px"
direction="rtl"
>
<template #default>
<div ref="contentRef" class="drawer-content">
<el-scrollbar ref="messageRef" height="calc(100vh - 180px)">
<Message
v-for="(item, index) of messages"
:key="index"
:date-time="item.createTime"
:error="false"
:inversion="item.role !== 'assistant'"
:loading="loading"
:text="item.message"
@delete="handleDelete(item)"
/>
</el-scrollbar>
</div>
<el-empty v-if="messages.length === 0" description="此会话还没有聊天信息" class="mt-5" />
</template>
<template #footer>
<div style="flex: auto">
<el-button @click="dialogVisible = false">关闭</el-button>
</div>
</template>
</el-drawer>
</template>
<style lang="scss" scoped>
.drawer-content {
height: 100%;
padding: 0 20px;
}
</style>

View File

@ -0,0 +1,107 @@
import { ref, computed, onMounted } from 'vue'
import type { FormSchema } from '@/types/form'
import type { TableColumn } from '@/types/table'
import { useTable } from '@/hooks/web/useTable'
import { ElTag, dayjs } from 'element-plus'
import { h } from 'vue'
import * as ConversationApi from '@/api/new-ai/conversation'
export default function () {
const searchParams = ref({})
const infoRef = ref()
const schema = ref<FormSchema[]>([
{
field: 'text',
component: 'Input',
label: '内容',
componentProps: {
placeholder: '请输入内容'
},
colProps: {
span: 6
}
}
])
const columns = ref<TableColumn[]>([
{
label: '用户名',
field: 'username',
align: 'center'
},
{
label: '窗口标题',
field: 'title',
align: 'center'
},
{
label: '对话次数',
field: 'chatTotal',
align: 'center',
width: 180
},
{
label: 'Token消耗量',
field: 'tokenUsed',
align: 'center',
width: 180
},
{
label: '最后一次对话时间',
field: 'endTime',
align: 'center',
width: 180,
formatter: (row: any) => {
return dayjs(row.endTime).format('YYYY-MM-DD HH:mm:ss')
}
},
{
label: '创建时间',
field: 'createTime',
width: 180,
align: 'center',
formatter: (row: any) => {
return dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss')
}
}
])
const { register, tableObject, methods } = useTable({
getListApi: ConversationApi.page,
defaultParams: searchParams.value,
delListApi: ConversationApi.del
})
const handleSearch = (values: any) => {
methods.setSearchParams(values)
}
const pagination = computed(() => {
return {
total: tableObject.total,
pageSize: tableObject.pageSize,
currentPage: tableObject.currentPage
}
})
const handleShowInfo = (row: any) => {
infoRef.value?.show(row)
}
onMounted(() => {
methods.getList()
})
return {
schema,
columns,
register,
handleSearch,
methods,
tableObject,
pagination,
handleShowInfo,
infoRef
}
}

View File

@ -0,0 +1,45 @@
<script lang="ts" setup>
import { Delete, View } from '@element-plus/icons-vue'
import { Table } from '@/components/Table'
import useConversation from './composables'
import InfoList from './components/InfoList.vue'
import { searchSchemas } from './columns'
const {
columns,
register,
handleSearch,
methods,
tableObject,
pagination,
handleShowInfo,
infoRef
} = useConversation()
const actionColumn = {
label: '操作',
field: 'action',
width: 80,
fixed: 'right',
align: 'center'
}
</script>
<template>
<div class="mt-2">
<Search :schema="searchSchemas" inline @search="handleSearch" />
<Table :columns="[...columns, actionColumn]" :data="tableObject.tableList" :pagination="pagination" @register="register">
<template #action="{ row }">
<el-button :icon="View" text type="primary" @click="handleShowInfo(row)" />
<el-button :icon="Delete" text type="danger" @click="methods.delList(row.id, false)" />
</template>
</Table>
<InfoList ref="infoRef" />
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,57 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { Delete } from '@element-plus/icons-vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import { Table } from '@/components/Table'
import useMessage from './composables'
import ConversationList from './conversation/index.vue'
import { searchSchemas } from './columns'
import { format } from 'path'
const activeName = ref('1')
const {
schema,
columns,
register,
handleSearch,
methods,
tableObject,
pagination,
handleDel
} = useMessage()
const actionColumn = {
label: '操作',
field: 'action',
width: 70,
fixed: 'right',
align: 'center',
}
</script>
<template>
<el-card>
<el-tabs v-model="activeName">
<el-tab-pane label="会话消息列表" name="1">
<div class="mt-2">
<Search :schema="searchSchemas" @search="handleSearch"/>
<Table
:columns="[...columns, actionColumn]"
:data="tableObject.tableList"
:pagination="pagination"
@register="register"
>
<template #action="{row}">
<el-button :icon="Delete" text type="danger" @click="methods.delList(row.id, false)" />
</template>
</Table>
</div>
</el-tab-pane>
<el-tab-pane label="会话窗口列表" name="2">
<ConversationList />
</el-tab-pane>
</el-tabs>
</el-card>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,79 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { Echart } from '@/components/Echart';
import { getReqChartBy30 } from '@/api/new-ai/statictic';
const options = ref({});
onMounted(async () => {
const data = await getReqChartBy30();
const xData: any = [];
const yData: any = [];
data.forEach((i: any) => {
xData.push(i.date);
yData.push(i.tokens);
});
options.value = {
tooltip: {
trigger: 'axis',
axisPointer: {
lineStyle: {
width: 1,
color: '#019680',
},
},
},
xAxis: {
type: 'category',
boundaryGap: false,
data: xData,
splitLine: {
show: true,
lineStyle: {
width: 1,
type: 'solid',
color: 'rgba(226,226,226,0.5)',
},
},
axisTick: {
show: false,
},
},
yAxis: [
{
type: 'value',
splitNumber: 4,
axisTick: {
show: false,
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(255,255,255,0.2)', 'rgba(226,226,226,0.2)'],
},
},
},
],
grid: { left: '1%', right: '1%', top: '2%', bottom: 0, containLabel: true },
series: [
{
smooth: true,
data: yData,
type: 'line',
areaStyle: {},
itemStyle: {
color: '#5ab1ef',
},
},
],
};
});
</script>
<template>
<div>
<h3 class="my-2 mb-6 text-lg">近30天请求汇总</h3>
<Echart :options="options" height="240px" />
</div>
</template>

View File

@ -0,0 +1,53 @@
<!--
- Copyright (c) 2024 LangChat. TyCoding All Rights Reserved.
-
- Licensed under the GNU Affero General Public License, Version 3 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- https://www.gnu.org/licenses/agpl-3.0.html
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<script lang="ts" setup>
import { Delete } from '@element-plus/icons-vue'
import { searchSchemas } from './columns'
import useOrder from '../composables'
import { columns } from './columns'
const { register, handleSearch, methods, tableObject, pagination } = useOrder()
const actionColumn = {
label: '操作',
field: 'action',
width: 70,
fixed: 'right',
align: 'center'
}
</script>
<template>
<div class="h-full">
<el-card>
<Search :schema="searchSchemas" inline @search="handleSearch" />
<Table
:columns="[...columns, actionColumn]"
:data="tableObject.tableList"
:pagination="pagination"
@register="register"
>
<template #action="{ row }">
<el-button :icon="Delete" text type="danger" @click="methods.delList(row.id, false)" />
</template>
</Table>
</el-card>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,56 @@
import type { FormSchema } from '@/types/form';
export const columns = [
{
label: '用户名',
field: 'username',
align: 'center',
},
{
label: '模型名称',
field: 'model',
align: 'center',
},
{
label: 'Tokens',
field: 'tokens',
align: 'center',
},
{
label: 'Prompt Tokens',
field: 'promptTokens',
align: 'center',
},
{
label: 'Prompt Tokens',
field: 'promptTokens',
align: 'center',
},
{
label: 'IP地址',
field: 'ip',
align: 'center',
},
{
label: '调用时间',
field: 'createTime',
align: 'center',
width: 180,
},
];
export const searchSchemas: FormSchema[] = [
{
field: 'name',
component: 'Input',
label: '用户名',
componentProps: {
placeholder: '请输入用户名查询'
},
colProps: {
span: 6
}
},
]

View File

@ -0,0 +1,39 @@
import { ref, computed, onMounted } from 'vue'
import type { TableColumn } from '@/types/table'
import { useTable } from '@/hooks/web/useTable'
import { dayjs } from 'element-plus'
import {page, del} from '@/api/new-ai/message'
export default function () {
const searchParams = ref({})
const { register, tableObject, methods } = useTable({
getListApi:page,
defaultParams: searchParams.value,
delListApi:del
})
const handleSearch = (values: any) => {
methods.setSearchParams(values)
}
const pagination = computed(() => {
return {
total: tableObject.total,
pageSize: tableObject.pageSize,
currentPage: tableObject.currentPage
}
})
onMounted(() => {
methods.getList()
})
return {
register,
handleSearch,
methods,
tableObject,
pagination
}
}

View File

@ -0,0 +1,15 @@
<script lang="ts" setup>
import Chart from './components/Chart.vue';
import List from './components/List.vue';
</script>
<template>
<div class="overflow-y-auto h-full">
<el-card>
<Chart />
<List />
</el-card>
</div>
</template>

View File

@ -114,4 +114,4 @@ defineExpose({ show })
</el-dialog> </el-dialog>
</template> </template>
<style lang="less" scoped></style> <style lang="scss" scoped></style>