提交 0d942f87 authored 作者: 王鹏飞's avatar 王鹏飞

Merge branch 'master' into gdrtvu

......@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@microsoft/fetch-event-source": "^2.0.1",
"@tinymce/tinymce-vue": "^5.0.0",
"@vant/area-data": "^1.5.1",
"@vueuse/core": "^9.13.0",
......@@ -1108,6 +1109,11 @@
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
"node_modules/@microsoft/fetch-event-source": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
......@@ -7655,6 +7661,11 @@
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
"@microsoft/fetch-event-source": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
......
......@@ -15,6 +15,7 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@microsoft/fetch-event-source": "^2.0.1",
"@tinymce/tinymce-vue": "^5.0.0",
"@vant/area-data": "^1.5.1",
"@vueuse/core": "^9.13.0",
......
......@@ -7,6 +7,7 @@ const appConfigList = [
dmlURL: import.meta.env.VITE_DML_PRO_URL
},
{
system: 'default',
title: '商业数据分析实验室',
logo: 'https://zws-imgs-pub.ezijing.com/pc/base/ezijing-logo.svg',
hosts: ['saas-lab']
......
import httpRequest from '@/utils/axios'
// 获取实验列表
export function getExperimentList(params?: { page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/experiment/monitor-experiments', { params })
}
// 获取实时监控实验列表
export function getCurrentExperimentList(params?: { hour?: number; limit?: number }) {
return httpRequest.get('/api/resource/v1/backend/experiment/current-monitor-experiments', { params })
}
<script setup lang="ts">
import type { ExperimentItem } from '../types'
import AppList from '@/components/base/AppList.vue'
import { getExperimentList } from '../api'
import { useAppConfig } from '@/composables/useAppConfig'
const appConfig = useAppConfig()
const appList = ref<InstanceType<typeof AppList> | null>(null)
const list = ref<ExperimentItem[]>([])
async function fetchInfo() {
const res = await getExperimentList()
list.value = res.data.items
}
onMounted(() => {
fetchInfo()
})
const dmlURL = computed(() => {
return appConfig.dmlURL || import.meta.env.VITE_DML_URL
})
// 实验列表
const listOptions = computed(() => {
return {
remote: {
httpRequest: getExperimentList,
callback(data: any) {
return { total: data.total, list: data.items }
}
},
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '所属机构', prop: 'org.department_name' },
{ label: '实验课程名称', prop: 'course_name' },
{ label: '实验名称', prop: 'name' },
{
label: '指导老师',
prop: 'teachers',
computed({ row }: { row: ExperimentItem }) {
return row.teachers.map(item => item.name).join(',')
}
},
{
label: '班级名称',
prop: 'student.specialty_name',
computed({ row }: { row: ExperimentItem }) {
return row.classes.map(item => item.name).join(',')
}
},
{
label: '班级人数',
prop: 'student.class_name',
computed({ row }: { row: ExperimentItem }) {
return row.classes.map(item => item.student_total).join(',')
}
},
{ label: '使用人数', prop: 'current_use_user_count' },
{ label: '用户数据量', prop: 'current_member_count' },
{ label: '标签数据量', prop: 'current_tag_count' },
{ label: '群组数据量', prop: 'current_group_count' },
{ label: '实验使用时间', prop: 'time' },
{ label: '操作', slots: 'table-x' }
]
}
})
</script>
<template>
<AppCard>
<ul class="statistics">
<template v-for="(item, index) in list" :key="item.id">
<el-popover :width="340" trigger="hover" v-if="index < 5">
<el-form label-suffix=":" class="statistics-form">
<el-form-item label="实验名称">{{ item.name }}</el-form-item>
<el-form-item label="班级人数">{{ item.classes.map(item => item.student_total).join(',') }}</el-form-item>
<el-form-item label="使用人数">{{ item.current_use_user_count }}</el-form-item>
</el-form>
<template #reference>
<li>
<h6>
<span>Top{{ index + 1 }}</span>
</h6>
<p>{{ item.name }}</p>
</li>
</template>
</el-popover>
</template>
</ul>
<h2 class="h2-title">实验列表</h2>
<AppList border v-bind="listOptions" ref="appList">
<template #table-x="{ row }">
<el-button type="primary"><a :href="`${dmlURL}?experiment_id=${row.id}`" target="_blank">查看</a></el-button>
</template>
</AppList>
</AppCard>
</template>
<style lang="scss" scoped>
.statistics {
display: flex;
align-items: center;
justify-content: space-evenly;
margin: 90px 0;
&:empty {
display: none;
}
li {
width: 212px;
height: 212px;
background-color: rgba(247, 247, 247, 1);
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
h6 {
color: rgba(178, 15, 60, 1);
span {
font-size: 18px;
}
}
p {
margin-top: 20px;
font-size: 18px;
color: rgba(96, 96, 96, 1);
text-align: center;
}
}
}
.statistics-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
:deep(.el-form-item__content) {
font-size: 18px;
color: var(--main-color);
}
}
.h2-title {
padding-left: 5px;
font-size: 18px;
font-weight: 500;
line-height: 1;
margin: 20px 0;
border-left: 3px solid #aa1941;
}
</style>
<script setup lang="ts">
import type { ExperimentItem } from '../types'
import AppList from '@/components/base/AppList.vue'
import { getCurrentExperimentList } from '../api'
import { useAppConfig } from '@/composables/useAppConfig'
const appConfig = useAppConfig()
const appList = ref<InstanceType<typeof AppList> | null>(null)
const list = ref<ExperimentItem[]>([])
async function fetchInfo() {
const res = await getCurrentExperimentList()
list.value = res.data.items
}
let timer: null | number = null
onMounted(() => {
fetchInfo()
timer = setInterval(() => {
fetchInfo()
}, 10000)
})
onUnmounted(() => {
timer && clearInterval(timer)
})
const dmlURL = computed(() => {
return appConfig.dmlURL || import.meta.env.VITE_DML_URL
})
// 实验列表
const listOptions = computed(() => {
return {
data: list.value,
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '所属机构', prop: 'org.department_name' },
{ label: '实验课程名称', prop: 'course_name' },
{ label: '实验名称', prop: 'name' },
{
label: '指导老师',
prop: 'teachers',
computed({ row }: { row: ExperimentItem }) {
return row.teachers.map(item => item.name).join(',')
}
},
{
label: '班级名称',
prop: 'student.specialty_name',
computed({ row }: { row: ExperimentItem }) {
return row.classes.map(item => item.name).join(',')
}
},
{
label: '班级人数',
prop: 'student.class_name',
computed({ row }: { row: ExperimentItem }) {
return row.classes.map(item => item.student_total).join(',')
}
},
{ label: '正在使用人数', prop: 'current_use_user_count' },
{ label: '用户数据量', prop: 'current_member_count' },
{ label: '标签数据量', prop: 'current_tag_count' },
{ label: '群组数据量', prop: 'current_group_count' },
{ label: '实验使用时间', prop: 'time' },
{ label: '操作', slots: 'table-x' }
]
}
})
</script>
<template>
<AppCard>
<ul class="statistics">
<template v-for="(item, index) in list" :key="item.id">
<el-popover :width="340" trigger="hover" v-if="index < 5">
<el-form label-suffix=":" class="statistics-form">
<el-form-item label="实验名称">{{ item.name }}</el-form-item>
<el-form-item label="班级人数">{{ item.classes.map(item => item.student_total).join(',') }}</el-form-item>
<el-form-item label="正在使用人数">{{ item.current_use_user_count }}</el-form-item>
</el-form>
<template #reference>
<li>
<h6>
<span>Top{{ index + 1 }}</span>
</h6>
<p>{{ item.name }}</p>
</li>
</template>
</el-popover>
</template>
</ul>
<h2 class="h2-title">实验列表</h2>
<AppList border v-bind="listOptions" ref="appList">
<template #table-x="{ row }">
<el-button type="primary"><a :href="`${dmlURL}?experiment_id=${row.id}`" target="_blank">查看</a></el-button>
</template>
</AppList>
</AppCard>
</template>
<style lang="scss" scoped>
.statistics {
display: flex;
align-items: center;
justify-content: space-evenly;
margin: 90px 0;
&:empty {
display: none;
}
li {
width: 212px;
height: 212px;
background-color: rgba(247, 247, 247, 1);
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
h6 {
color: rgba(178, 15, 60, 1);
span {
font-size: 18px;
}
}
p {
margin-top: 20px;
font-size: 18px;
color: rgba(96, 96, 96, 1);
text-align: center;
}
}
}
.statistics-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
:deep(.el-form-item__content) {
font-size: 18px;
color: var(--main-color);
}
}
.h2-title {
padding-left: 5px;
font-size: 18px;
font-weight: 500;
line-height: 1;
margin: 20px 0;
border-left: 3px solid #aa1941;
}
</style>
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/admin/lab/dashboard',
component: AppLayout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
}
]
export interface OrgItem {
department_name: string
project_id: string
project_name: string
}
export interface ClassItem {
id: string
name: string
student_total: string
}
export interface TeacherItem {
id: string
name: string
}
export interface ExperimentItem {
id: string
name: string
time: string
org: OrgItem
classes: ClassItem[]
course_name: string
teachers: TeacherItem[]
current_use_user_count: string
current_member_count: string
current_tag_count: string
current_group_count: string
}
<script setup lang="ts">
import Live from '../components/Live.vue'
import History from '../components/History.vue'
const activeRadio = ref('1')
</script>
<template>
<el-radio-group v-model="activeRadio" style="margin-bottom: 10px">
<el-radio-button label="1">实时监控</el-radio-button>
<el-radio-button label="2">历史监控</el-radio-button>
</el-radio-group>
<Live v-if="activeRadio === '1'"></Live>
<History v-else></History>
</template>
......@@ -25,8 +25,8 @@ const form = reactive({
onMounted(() => {
form.organ_id = props.data.organ_id
form.experiment_name = props.data.name + '(copy)'
const [teacher] = props.data.teacher
form.sso_id = teacher.id
// const [teacher] = props.data.teacher
// form.sso_id = teacher.sso_id
})
// 机构列表
......@@ -34,9 +34,12 @@ const { organizations } = useGetProjectList()
// 指导教师列表
const { teachers, updateTeachers } = useGetTeacherList()
watchEffect(() => {
updateTeachers(form.organ_id)
})
watch(
() => form.organ_id,
() => {
updateTeachers(form.organ_id)
}
)
function handleOrgChange() {
form.sso_id = ''
......@@ -66,7 +69,7 @@ async function handleSubmit() {
</el-form-item>
<el-form-item label="指导老师" prop="sso_id">
<el-select v-model="form.sso_id" style="width: 100%" clearable>
<el-option v-for="item in teachers" :key="item.id" :label="item.name" :value="item.id"></el-option>
<el-option v-for="item in teachers" :key="item.id" :label="item.name" :value="item.sso_id"></el-option>
</el-select>
</el-form-item>
</el-form>
......
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
import type { ExperimentItem } from '../types'
import { Plus } from '@element-plus/icons-vue'
import { Plus, Minus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { gradeRule, gradeRuleList } from '@/utils/dictionary'
import { getExperimentGradeRule, updateExperimentGradeRule, getScoreExamList } from '../api'
......@@ -40,6 +40,12 @@ function fetchInfo() {
} catch (error) {
console.log(error)
}
if (!!detail.marketing_planning_rule) {
marketingForm = detail.marketing_planning_rule
if (detail.marketing_planning_rule.percent > 0) {
marketingShow = true
}
}
Object.assign(form, detail, { rule_list: ruleList })
})
}
......@@ -53,11 +59,6 @@ async function fetchExamList() {
})
}
onMounted(() => {
fetchInfo()
fetchExamList()
})
// 合计
const total = $computed(() => {
const examTotal = form.exam_rules.reduce((result: number, item: any) => {
......@@ -66,7 +67,9 @@ const total = $computed(() => {
return (
form.rule_list.reduce((result: number, item: any) => {
return result + (item.percent || 0)
}, 0) + examTotal
}, 0) +
examTotal +
marketingForm.percent
)
})
......@@ -103,7 +106,9 @@ function handleSubmit(call?: any) {
return
}
if (item.type === 5 && /实验报告|实验准备|实验结果|课堂活跃度/.test(item.name)) {
ElMessage.error(`第${i + 1}行规则配置错误,自定义的名称不能包含“实验报告”、“实验准备”、“实验结果”和“课堂活跃度”`)
ElMessage.error(
`第${i + 1}行规则配置错误,自定义的名称不能包含“实验报告”、“实验准备”、“实验结果”和“课堂活跃度”`
)
return
}
}
......@@ -112,12 +117,14 @@ function handleSubmit(call?: any) {
ElMessage.error('实验成绩规则项权重之和必须为100%!')
return
}
formRef?.validate().then(() => {
const params = { ...form, rule_list: JSON.stringify(form.rule_list) }
if (marketingShow) {
params.marketing_planning_rule = JSON.stringify(marketingForm)
} else {
delete params.marketing_planning_rule
}
updateExperimentGradeRule(params).then(() => {
// call()
console.log(call, 'call()')
if (!call) {
ElMessage({ message: '保存成功', type: 'success' })
emit('update')
......@@ -191,10 +198,73 @@ function handleAddExamRule() {
function handleRemoveExamRule(index: number) {
form.exam_rules.splice(index, 1)
}
// 营销策划
const marketingTotal = ref(100)
const marketingType = {
'1': '1、营销背景分析',
'2': '2、营销渠道选择',
'3': '3、用户分析',
'4': '4、用户标签体系',
'5': '5、用户精准分群',
'6': '6、自动化营销旅程',
'7': '7、营销物料设计',
'8': '8、营销策划报告'
}
let marketingForm = $ref({
percent: 0,
rule_mode: 1,
details: [
{ type: '1', percent: 5 },
{ type: '2', percent: 5 },
{ type: '3', percent: 15 },
{ type: '4', percent: 25 },
{ type: '5', percent: 15 },
{ type: '6', percent: 15 },
{ type: '7', percent: 15 },
{ type: '8', percent: 5 }
]
})
let marketingShow = $ref(false)
const handleMarketingAdd = function () {
marketingShow = !marketingShow
if (!marketingShow) {
marketingForm.percent = 0
}
}
const handleMarketingChildChange = function (row: any, index: number) {
const otherTotal = marketingForm.details.reduce((result: number, item: any, i: number) => {
if (index !== i) {
result += item.percent || 0
}
return result
}, 0)
// 最大可输入占比
nextTick(() => {
row.percent = Math.min(100 - otherTotal, row.percent)
})
const t = marketingForm.details.reduce((a: any, b: any) => {
a += b.percent
return a
}, 0)
marketingTotal.value = t > 100 ? 100 : t
}
onMounted(() => {
fetchInfo()
fetchExamList()
})
</script>
<template>
<el-dialog title="编辑实验成绩规则" :close-on-click-modal="false" width="800px" @update:modelValue="value => $emit('update:modelValue', value)">
<el-dialog
title="编辑实验成绩规则"
:close-on-click-modal="false"
width="800px"
@update:modelValue="value => $emit('update:modelValue', value)"
>
<el-form ref="formRef" :model="form" label-suffix=":">
<el-form-item label="实验名称">{{ data?.name }}</el-form-item>
<el-row>
......@@ -215,7 +285,8 @@ function handleRemoveExamRule(index: number) {
<el-form-item>
<el-row justify="space-between" style="width: 100%">
<p>理论考试:</p>
<el-button type="primary" :icon="Plus" @click="handleAddExamRule" :disabled="form.exam_rules.length >= 1"> </el-button>
<el-button type="primary" :icon="Plus" @click="handleAddExamRule" :disabled="form.exam_rules.length >= 1">
</el-button>
</el-row>
<el-table :data="form.exam_rules" row-key="id">
<el-table-column prop="name" width="170">
......@@ -226,7 +297,9 @@ function handleRemoveExamRule(index: number) {
</template>
</el-table-column>
<el-table-column prop="percent" align="center" width="200">
<template #default="{ row, $index }"> <el-input-number v-model="row.percent" :min="0" @change="handlePercentChange(row, $index, 2)" /> % </template>
<template #default="{ row, $index }">
<el-input-number v-model="row.percent" :min="0" @change="handlePercentChange(row, $index, 2)" /> %
</template>
</el-table-column>
<el-table-column prop="is_auto_scoring" width="200">
<template #default="{ row }">
......@@ -241,7 +314,14 @@ function handleRemoveExamRule(index: number) {
</el-table-column>
<el-table-column align="right">
<template #default="{ $index, row }">
<el-button style="padding: 0" text type="primary" @click="handleRemoveExamRule($index)" v-if="row.type !== 1">删除</el-button>
<el-button
style="padding: 0"
text
type="primary"
@click="handleRemoveExamRule($index)"
v-if="row.type !== 1"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
......@@ -255,13 +335,21 @@ function handleRemoveExamRule(index: number) {
<el-table-column prop="name" width="170">
<template #default="{ row }">
<el-input v-model="row.name" :maxlength="20" style="width: 100%" v-if="row.type === 5" />
<el-select v-model="row.type" :disabled="row.type === 1" style="width: 100%" @change="handleTypeChange(row)" v-else>
<el-select
v-model="row.type"
:disabled="row.type === 1"
style="width: 100%"
@change="handleTypeChange(row)"
v-else
>
<el-option v-for="item in currentRuleNames(row.type)" :key="item.value" v-bind="item"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column prop="percent" align="center" width="200">
<template #default="{ row, $index }"> <el-input-number v-model="row.percent" :min="0" @change="handlePercentChange(row, $index, 1)" /> % </template>
<template #default="{ row, $index }">
<el-input-number v-model="row.percent" :min="0" @change="handlePercentChange(row, $index, 1)" /> %
</template>
</el-table-column>
<el-table-column prop="rule_mode" width="200">
<template #default="{ row }">
......@@ -279,7 +367,13 @@ function handleRemoveExamRule(index: number) {
<template #default="{ $index, row }">
<div class="btn-box">
<!-- || row.type === 8 -->
<el-button :disabled="row.type === 1" style="padding: 0" text type="primary" @click="handleEdit(row.type)" v-if="row.type !== 1"
<el-button
:disabled="row.type === 1"
style="padding: 0"
text
type="primary"
@click="handleEdit(row.type)"
v-if="row.type !== 1"
>编辑</el-button
>
<el-button style="padding: 0" text type="primary" @click="handleRemove($index)">删除</el-button>
......@@ -287,7 +381,45 @@ function handleRemoveExamRule(index: number) {
</template>
</el-table-column>
</el-table>
<div class="total" v-if="form.rule_list.length">
<!-- <div class="total" v-if="form.rule_list.length">
<p>合计:{{ total }}%</p>
</div> -->
</el-form-item>
<el-form-item>
<el-row justify="space-between" style="width: 100%">
<p>营销策划:</p>
<div style="display: flex">
<el-form-item label="营销策划所占整体比例" v-if="marketingShow">
<el-input-number
@change="handlePercentChange(marketingForm, 1, 3)"
v-model="marketingForm.percent"
:min="0"
/>
%
</el-form-item>
<el-button
style="margin-left: 20px"
type="primary"
:icon="!marketingShow ? Plus : Minus"
@click="handleMarketingAdd"
></el-button>
</div>
<el-divider></el-divider>
<el-form-item
v-if="marketingShow"
style="margin-bottom: 10px"
label-width="150px"
:label="marketingType[item.type as '1']"
v-for="(item, index) in marketingForm.details"
>
<el-input-number @change="handleMarketingChildChange(item, index)" v-model="item.percent" :min="0" /> %
</el-form-item>
<div class="total-c" v-if="marketingShow">
<p>营销策划小计:{{ marketingTotal }}%</p>
</div>
</el-row>
<el-divider v-if="marketingShow"></el-divider>
<div class="total">
<p>合计:{{ total }}%</p>
</div>
</el-form-item>
......@@ -313,4 +445,18 @@ function handleRemoveExamRule(index: number) {
background-color: #ededed;
}
}
.total-c {
width: 100%;
padding: 10px 0;
box-sizing: border-box;
p {
margin-left: 16px;
display: inline-block;
min-width: 150px;
text-align: center;
color: #016fa0;
background-color: #ededed;
float: right;
}
}
</style>
......@@ -3,6 +3,7 @@ import { getExperimentTeacherList } from '../api'
interface TeacherType {
id: string
name: string
sso_id: string
}
export function useGetTeacherList() {
......
import httpRequest from '@/utils/axios'
// 聊天(流式响应)
export function qwenChat(data: any) {
return httpRequest.post('/api/lab/v1/experiment/qwen/chat', data, { headers: { 'Content-Type': 'application/json' } })
}
<script setup>
const emit = defineEmits(['success'])
const file = ref()
const onSuccess = res => {
file.value = res.data.detail
emit('success', file.value)
}
</script>
<template>
<el-upload
class="ai-upload"
drag
action="/api/lab/v1/experiment/qwen/upload-file"
accept=".csv, .xls, .xlsx, text/csv, application/csv,text/comma-separated-values, application/csv, application/excel,application/vnd.msexcel, text/anytext, application/vnd. ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
:data="{ purpose: 'file-extract' }"
:show-file-list="false"
:on-success="onSuccess">
<ul class="ai-upload-list" v-if="file">
<li>{{ file.filename }}</li>
</ul>
<div class="ai-upload-box">
<img src="@/assets/images/ai_plus.png" height="40" />
<div class="el-upload__text"><em>点击</em>上传数据文件</div>
</div>
</el-upload>
</template>
<style lang="scss">
.ai-upload {
.el-upload-dragger {
padding: 20px;
}
.ai-upload-list {
margin-bottom: 20px;
text-align: left;
li {
padding: 0 10px;
display: flex;
align-items: center;
min-height: 40px;
background: #ffffff;
box-shadow: 0px 3px 6px 1px rgba(0, 0, 0, 0.16);
}
}
.el-upload__text {
margin-top: 20px;
em {
text-decoration: underline;
}
}
}
</style>
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { ElMessage } from 'element-plus'
export function useChat() {
const messages = ref([])
const isLoading = ref(false)
async function post() {
isLoading.value = true
await fetchEventSource('/api/lab/v1/experiment/qwen/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'qwen-long', messages: messages.value }),
async onopen(response) {
if (response.ok) {
return response
} else {
throw response
}
},
onmessage(res) {
console.log(res.data)
try {
const message = JSON.parse(res.data)
if (message.error) {
ElMessage.error(message.error.message)
return
}
const id = message.id
const messageIndex = messages.value.findIndex(session => session.id === id)
let content = message?.choices[0]?.delta.content || ''
content = content.replaceAll('\n', '<br/>')
if (messageIndex === -1) {
messages.value.push({ id, role: 'assistant', content })
} else {
messages.value[messageIndex].content = messages.value[messageIndex].content + content
}
isLoading.value = false
} catch (error) {
console.log(error)
isLoading.value = false
}
},
onerror(err) {
isLoading.value = false
throw err
}
})
}
return { messages, post, isLoading }
}
import type { RouteRecordRaw } from 'vue-router'
export const routes: Array<RouteRecordRaw> = [
{
path: '/ai',
component: () => import('./views/Index.vue')
}
]
<script setup>
import Upload from '../components/Upload.vue'
import { useChat } from '../composabels/useChat'
const { messages, post, isLoading } = useChat()
const chatInput = ref('')
const onUploadSuccess = res => {
const message = { role: 'system', content: `fileid://${res.id}` }
messages.value.push(message)
}
async function postMessage() {
if (!chatInput.value) return
const message = { role: 'user', content: chatInput.value }
messages.value.push(message)
post(message)
chatInput.value = ''
}
const chatRef = ref()
function scrollToBottom() {
if (!chatRef.value) return
chatRef.value.scrollTo(0, chatRef.value.scrollHeight)
}
watch(messages.value, () => nextTick(() => scrollToBottom()))
</script>
<template>
<div class="ai-wrapper" :class="{ 'is-center': !messages.length }">
<header class="ai-header">
<div class="ai-header-inner">
<div class="ai-header-left">
<img src="https://zws-imgs-pub.ezijing.com/pc/base/ezijing-logo.svg" width="174" />
<div class="ai-header__title">AI商业数据分析</div>
</div>
<div class="ai-header-right">感知AI数据分析,让数据一触即知</div>
</div>
</header>
<main class="ai-main">
<Upload @success="onUploadSuccess" />
<div class="ai-message" ref="chatRef">
<template v-for="(item, index) in messages" :key="index">
<div class="ai-message-item" :class="item.role" v-if="item.role !== 'system'">
<div class="ai-message__avatar"><img :src="item.role === 'assistant' ? '/images/ai_avatar_bot.png' : '/images/ai_avatar_user.png'" /></div>
<div class="ai-message__content" v-html="item.content"></div>
</div>
</template>
<div class="ai-message-item" v-if="isLoading">
<div class="dot-flashing"></div>
</div>
</div>
<footer class="ai-footer">
<el-input placeholder="输入你想提问的问题" v-model="chatInput" @keyup.enter="postMessage">
<template #suffix>
<img src="@/assets/images/ai_send.png" class="ai-footer__button" @click="postMessage" />
</template>
</el-input>
</footer>
</main>
</div>
</template>
<style lang="scss">
.ai-wrapper {
height: 100vh;
display: flex;
align-items: center;
flex-direction: column;
&.is-center {
justify-content: space-evenly;
.ai-header {
box-shadow: none;
}
.ai-main {
flex: none;
}
.el-upload-dragger {
padding: 80px 0;
}
}
}
.ai-main {
display: flex;
flex-direction: column;
flex: 1;
width: 1000px;
overflow: hidden;
.ai-upload {
margin: 40px 0;
}
}
.ai-header {
width: 100%;
box-shadow: 0px 3px 6px 1px rgba(0, 0, 0, 0.16);
}
.ai-header-inner {
max-width: 1000px;
padding: 18px 0;
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 auto;
}
.ai-header__title {
margin-top: 8px;
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 16px;
color: #2c2c2c;
letter-spacing: 7px;
text-align: right;
}
.ai-header-right {
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 24px;
color: #2c2c2c;
letter-spacing: 5px;
}
.ai-footer {
margin: 40px 10px;
.el-input__wrapper {
height: 60px;
font-size: 16px;
border-radius: 33px;
box-shadow: 0px 3px 12px 1px rgba(0, 0, 0, 0.12);
}
}
.ai-footer__button {
cursor: pointer;
}
.ai-message {
min-height: 100px;
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
overflow-x: hidden;
overflow-y: auto;
}
.ai-message-item {
margin-bottom: 30px;
padding: 10px;
border-radius: 12px;
background-color: #f5edef;
display: flex;
color: #000;
gap: 10px;
}
.ai-message-item.user {
align-self: flex-end;
color: #fff;
background-color: #ab2940;
flex-direction: row-reverse;
}
.ai-message__avatar {
width: 48px;
height: 48px;
background-color: #fff;
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.ai-message__content {
flex: 1;
max-width: 100%;
font-size: 16px;
line-height: 24px;
word-break: break-word;
align-self: center;
}
.dot-flashing {
animation: dot-flashing 0.8s infinite alternate;
animation-delay: -0.2s;
animation-timing-function: ease;
margin: 7px 18px;
overflow: visible !important;
position: relative;
}
.dot-flashing,
.dot-flashing:after,
.dot-flashing:before {
background-color: rgba(0, 0, 0, 0.1);
border-radius: 4px;
color: rgba(0, 0, 0, 0.1);
height: 8px;
width: 8px;
}
.dot-flashing:after,
.dot-flashing:before {
animation: dot-flashing 0.8s infinite alternate;
animation-timing-function: ease;
content: '';
display: inline-block;
position: absolute;
top: 0;
}
.dot-flashing:before {
left: -15px;
animation-delay: -0.4s;
}
.dot-flashing:after {
left: 15px;
animation-delay: 0s;
}
@keyframes dot-flashing {
0% {
background-color: #000;
}
50% {
background-color: rgba(0, 0, 0, 0.1);
}
to {
background-color: #000;
}
}
</style>
......@@ -275,6 +275,9 @@ function handleReportPreviewReady() {
<el-button type="primary" :disabled="disabled" @click="handleSubmit">提交实验</el-button>
</div>
<div>
<el-button type="primary" v-if="appConfig.system == 'default'">
<router-link to="/ai" target="_blank">AI数据分析</router-link>
</el-button>
<el-button type="primary" :disabled="disabled" :loading="screenshotLoading" @click="handleCapture">截图</el-button>
<el-button type="primary" :disabled="disabled" @click="prepareDialogVisible = true">实验准备</el-button>
<el-button type="primary" :disabled="disabled" @click="resultDialogVisible = true">实验结果</el-button>
......
......@@ -29,7 +29,8 @@ const adminMenus: IMenuItem[] = [
{ name: '实验指导书管理', path: '/admin/lab/book', tag: 'v1-teacher-book' },
{ name: '实验操作视频管理', path: '/admin/lab/video', tag: 'v1-teacher-video' },
{ name: '实验讨论交流', path: '/admin/lab/discuss', tag: 'v1-teacher-discussion' },
{ name: '实验成绩管理', path: '/admin/lab/score', tag: 'v1-teacher-record' }
{ name: '实验成绩管理', path: '/admin/lab/score', tag: 'v1-teacher-record' },
{ name: '实验监控', path: '/admin/lab/dashboard' }
]
},
// {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论