Commit 60dd1279 by pengshun

feat: 添加公租房和长租房的区分

parent da7791dc
No preview for this file type
......@@ -49,4 +49,7 @@ public class AiChatAnalysisRespVO implements VO {
@Schema(description = "是否转人工")
private Boolean isTransferHuman;
@Schema(description = "是否公租房")
private Boolean isPublicRental;
}
......@@ -20,4 +20,7 @@ public class AiChatAnalysisStatisticsReqVO extends PageParam {
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] chatTime;
@Schema(description = "是否公租房")
private Boolean isPublicRental;
}
......@@ -58,4 +58,7 @@ public class AiChatAnalysysCreateMyReqVO {
@Schema(description = "是否转人工")
private Boolean isTransferHuman;
@Schema(description = "是否公租房")
private Boolean isPublicRental;
}
......@@ -17,4 +17,7 @@ public class AiChatKeywordsPageReqVO extends PageParam {
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] chatTime;
@Schema(description = "是否公租房")
private Boolean isPublicRental;
}
......@@ -84,4 +84,6 @@ public class AiChatAnalysisDO extends BaseDO {
private Boolean isTransferHuman;
private Boolean isPublicRental;
}
......@@ -60,18 +60,22 @@ public interface AiChatAnalysisMapper extends BaseMapperX<AiChatAnalysisDO> {
/**
* 统计指定时间范围内的问答总数
*/
default Long selectCountByTimeRange(LocalDateTime[] timeRange) {
default Long selectCountByTimeRange(LocalDateTime[] timeRange, Boolean isPublic) {
return selectCount(new LambdaQueryWrapperX<AiChatAnalysisDO>()
.betweenIfPresent(AiChatAnalysisDO::getChatTime, timeRange));
.betweenIfPresent(AiChatAnalysisDO::getChatTime, timeRange)
.eqIfPresent(AiChatAnalysisDO::getIsPublicRental, isPublic)
);
}
/**
* 统计指定时间范围内的转人工总数
*/
default Long selectTransferCountByTimeRange(LocalDateTime[] timeRange) {
default Long selectTransferCountByTimeRange(LocalDateTime[] timeRange, Boolean isPublic) {
return selectCount(new LambdaQueryWrapperX<AiChatAnalysisDO>()
.betweenIfPresent(AiChatAnalysisDO::getChatTime, timeRange)
.eq(AiChatAnalysisDO::getIsTransferHuman, true));
.eq(AiChatAnalysisDO::getIsTransferHuman, true)
.eqIfPresent(AiChatAnalysisDO::getIsPublicRental, isPublic)
);
}
/**
......@@ -81,6 +85,7 @@ public interface AiChatAnalysisMapper extends BaseMapperX<AiChatAnalysisDO> {
// 只查询 question_keywords 字段
return selectObjs(new LambdaQueryWrapperX<AiChatAnalysisDO>()
.betweenIfPresent(AiChatAnalysisDO::getChatTime, reqVo.getChatTime())
.eqIfPresent(AiChatAnalysisDO::getIsPublicRental, reqVo.getIsPublicRental())
.select(AiChatAnalysisDO::getQuestionKeywords));
}
}
......@@ -112,13 +112,13 @@ public class AiChatAnalysisServiceImpl implements AiChatAnalysisService {
// 1. 计算时间范围
// 2. 查询当前周期数据
Long currentTotal = chatAnalysisMapper.selectCountByTimeRange(pageReqVO.getChatTime());
Long currentTransfer = chatAnalysisMapper.selectTransferCountByTimeRange(pageReqVO.getChatTime());
Long currentTotal = chatAnalysisMapper.selectCountByTimeRange(pageReqVO.getChatTime(), pageReqVO.getIsPublicRental());
Long currentTransfer = chatAnalysisMapper.selectTransferCountByTimeRange(pageReqVO.getChatTime(), pageReqVO.getIsPublicRental());
LocalDateTime[] previousRange = computePreviousRange(pageReqVO.getChatTime());
// 3. 查询上一周期数据
Long previousTotal = chatAnalysisMapper.selectCountByTimeRange(previousRange);
Long previousTransfer = chatAnalysisMapper.selectTransferCountByTimeRange(previousRange);
Long previousTotal = chatAnalysisMapper.selectCountByTimeRange(previousRange, pageReqVO.getIsPublicRental());
Long previousTransfer = chatAnalysisMapper.selectTransferCountByTimeRange(previousRange, pageReqVO.getIsPublicRental());
// 4. 计算百分比(与上周期对比)
Double percentQAPrevious = calculatePercent(currentTotal, previousTotal);
......
......@@ -19,6 +19,6 @@ const title = computed(() => appStore.getTitle)
:class="prefixCls"
class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)] overflow-hidden"
>
<span class="text-14px">Copyright ©2022-{{ title }}</span>
<span class="text-14px">Copyright ©2026-{{ title }}</span>
</div>
</template>
<template>
<ContentWrap>
<!-- 新增外层 Tabs:公租房 / 长租房 -->
<el-tabs v-model="rentalType" @tab-change="handleRentalTabChange">
<el-tab-pane label="公租房" name="public" />
<el-tab-pane label="长租房" name="long" />
</el-tabs>
<!-- 原有周期 Tabs -->
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<el-tab-pane
v-for="(config, key) in periodConfigs"
:key="key"
:label="config.label + '数据'"
<el-tab-pane
v-for="(config, key) in periodConfigs"
:key="key"
:label="config.label + '数据'"
:name="key"
>
<div class="analysis-container p-20px min-h-500px">
<el-card shadow="never" class="period-card" v-loading="loading">
<div class="stats-container">
<div class="stat-item">
<div class="stat-label">问答总数</div>
<div class="stat-main">
<span class="num">{{ statsData?.totalCount || 0 }}</span>
<span :class="['growth', (statsData?.percentQAPrevious || 0) >= 0 ? 'up' : 'down']">
<span
:class="[
'growth',
(statsData?.percentQAPrevious || 0) >= 0 ? 'up' : 'down'
]"
>
{{ (statsData?.percentQAPrevious || 100) >= 0 ? '↑' : '↓' }}
{{ Math.abs(statsData?.percentQAPrevious || 100).toFixed(2) }}%
</span>
......@@ -29,7 +40,12 @@
<div class="stat-label">转人工总数</div>
<div class="stat-main">
<span class="num">{{ statsData?.transferCount || 0 }}</span>
<span :class="['growth', (statsData?.percentTransPrevious || 0) >= 0 ? 'up' : 'down']">
<span
:class="[
'growth',
(statsData?.percentTransPrevious || 0) >= 0 ? 'up' : 'down'
]"
>
{{ (statsData?.percentTransPrevious || 0) >= 0 ? '↑' : '↓' }}
{{ Math.abs(statsData?.percentTransPrevious || 0).toFixed(2) }}%
</span>
......@@ -40,7 +56,7 @@
<div class="word-section mt-30px">
<div class="flex justify-between items-center mb-10px">
<span class="text-16px font-bold">高频问答</span>
<span class="text-16px font-bold">高频问答</span>
<el-button link type="primary" @click="isExpanded = !isExpanded">
{{ isExpanded ? '收起列表' : '展开列表' }}
<Icon :icon="isExpanded ? 'ep:arrow-up' : 'ep:arrow-down'" />
......@@ -49,11 +65,25 @@
<el-collapse-transition>
<div v-show="isExpanded">
<el-table :data="displayWords" size="default" stripe v-loading="tableLoading">
<el-table-column label="关键词/问题" prop="keyword" show-overflow-tooltip />
<el-table-column label="出现频次" prop="count" width="120" align="center" />
<el-table
:data="displayWords"
size="default"
stripe
v-loading="tableLoading"
>
<el-table-column
label=""
prop="keyword"
show-overflow-tooltip
/>
<el-table-column
label="出现频次"
prop="count"
width="120"
align="center"
/>
</el-table>
<div class="flex justify-center mt-20px">
<el-pagination
background
......@@ -75,17 +105,16 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { ChatAnalysisApi } from '@/api/ai/chat/analysis'
import dayjs from 'dayjs'
import isoWeek from 'dayjs/plugin/isoWeek' // 用于处理周一为一周开始
dayjs.extend(isoWeek)
import isoWeek from 'dayjs/plugin/isoWeek'
dayjs.extend(isoWeek)
/** AI 聊天对话 管理 */
defineOptions({ name: 'AiChatAnalysis' })
// 默认选中“today”标签
// 原周期相关
const activeTab = ref('today')
const loading = ref(false)
const tableLoading = ref(false)
......@@ -95,12 +124,11 @@ const isExpanded = ref(true)
const currentPage = ref(1)
const pageSize = ref(10)
const totalWords = ref(0)
const wordList = ref([])
const statsData = ref<any>(null)
const allWords = ref<any[]>([])
const allWords = ref<AiChatKeywordCountVO[]>([]) // 存储全量关键词列表
const statsData = ref<any>(null)
// 计算属性:当前页显示的关键词
// 计算属性:当前页显示的关键词
const displayWords = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
......@@ -114,6 +142,14 @@ const periodConfigs: Record<string, any> = {
month: { label: '本月', compareUnit: '月', tagType: 'warning' }
}
// ================= 新增:公租房 / 长租房 =================
const rentalType = ref<'public' | 'long'>('public') // 默认公租房
/** 将 rentalType 转换为接口需要的 isPublicRental 布尔值 */
const getIsPublicRental = (): boolean => {
return rentalType.value === 'public' // 公租房 => true,长租房 => false
}
/**
* 根据周期获取起止时间范围
* @param period today / week / month
......@@ -126,18 +162,16 @@ const getTimeRangeByPeriod = (period: string): [string, string] => {
switch (period) {
case 'today':
// start = now.startOf('day') // 当天 00:00:00
// end = now.endOf('day') // 当天 23:59:59
start = now.subtract(1, 'day').startOf('day')
end = now.subtract(1, 'day').endOf('day')
break
case 'week':
start = now.startOf('isoWeek') // 本周一 00:00:00
end = now.endOf('isoWeek') // 本周日 23:59:59
start = now.startOf('isoWeek')
end = now.endOf('isoWeek')
break
case 'month':
start = now.startOf('month') // 本月第一天 00:00:00
end = now.endOf('month') // 本月最后一天 23:59:59
start = now.startOf('month')
end = now.endOf('month')
break
default:
start = now.startOf('day')
......@@ -147,7 +181,7 @@ const getTimeRangeByPeriod = (period: string): [string, string] => {
return [start.format('YYYY-MM-DD HH:mm:ss'), end.format('YYYY-MM-DD HH:mm:ss')]
}
/** 统一加载数据方法 */
/** 统一加载数据方法(含外层 rentalType 和内层周期) */
const loadAllData = async () => {
await Promise.all([fetchStats(), fetchWordList()])
}
......@@ -158,25 +192,26 @@ const fetchStats = async () => {
try {
const [start, end] = getTimeRangeByPeriod(activeTab.value)
statsData.value = await ChatAnalysisApi.getChatStatistics({
chatTime: [start, end] // 传入时间范围
chatTime: [start, end],
isPublicRental: getIsPublicRental() // 新增参数
})
} finally {
loading.value = false
}
}
/** 加载高频词分页列表 */
/** 加载高频词全量列表(前端分页) */
const fetchWordList = async () => {
tableLoading.value = true
try {
const [start, end] = getTimeRangeByPeriod(activeTab.value)
const res = await ChatAnalysisApi.getChatKeywords({
chatTime: [start, end]
chatTime: [start, end],
isPublicRental: getIsPublicRental() // 新增参数
})
allWords.value = res || []
totalWords.value = allWords.value.length
// 如果当前页码超出总页数,重置为最后一页或第一页
const maxPage = Math.ceil(totalWords.value / pageSize.value) || 1
if (currentPage.value > maxPage) {
currentPage.value = maxPage
......@@ -186,16 +221,22 @@ const fetchWordList = async () => {
}
}
/** 切换 Tab 时的处理 */
/** 内层周期 Tab 切换 */
const handleTabChange = (name: any) => {
currentPage.value = 1 // 切换周期时页码重置
currentPage.value = 1
loadAllData()
}
/** 外层公租房/长租房 Tab 切换 */
const handleRentalTabChange = () => {
currentPage.value = 1 // 重置分页
loadAllData()
}
/** 分页切换 */
const handlePageChange = (page: number) => {
currentPage.value = page
fetchWordList()
// 注意:这里不需要重新请求接口,因为全量数据已存在,仅重新切片展示
}
onMounted(() => {
......@@ -243,12 +284,12 @@ onMounted(() => {
font-weight: bold;
padding: 2px 8px;
border-radius: 4px;
&.up {
color: #f56c6c;
&.up {
color: #f56c6c;
background-color: #fef0f0;
}
&.down {
color: #67c23a;
&.down {
color: #67c23a;
background-color: #f0f9eb;
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment