Commit 60dd1279 by pengshun

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

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