完成后台修改

dev
linyongLynn 4 months ago
parent 1d134c6ec0
commit cb47d97c32

@ -126,3 +126,12 @@ export function supplyDemandChart(params) {
}) })
} }
// 获取消息列表
export function getMessageList(params) {
return request({
method: 'get',
url: '/api/admin/supply-demand/message-list',
params
})
}

@ -294,7 +294,7 @@
style="width: 100%;"> style="width: 100%;">
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item v-if="sendForm.testEmail"> <el-form-item v-if="sendForm.testEmail">
<el-button type="warning" @click="sendTestEmail" :loading="testSending"> <el-button type="warning" @click="sendTestEmail" :loading="testSending">
<i class="el-icon-s-promotion"></i> <i class="el-icon-s-promotion"></i>
@ -624,8 +624,8 @@
:before-close="() => showVarDataModal = false"> :before-close="() => showVarDataModal = false">
<div v-if="currentVarData" class="var-data-content"> <div v-if="currentVarData" class="var-data-content">
<el-descriptions :column="2" border> <el-descriptions :column="2" border>
<el-descriptions-item <el-descriptions-item
v-for="(value, key) in currentVarData" v-for="(value, key) in currentVarData"
:key="key" :key="key"
:label="key"> :label="key">
{{ value }} {{ value }}
@ -642,22 +642,26 @@
:before-close="() => showSystemDataModal = false"> :before-close="() => showSystemDataModal = false">
<div class="system-data-content"> <div class="system-data-content">
<div class="search-section"> <div class="search-section">
<el-input <el-input
v-model="systemStudentSearch" v-model="systemStudentSearch"
placeholder="搜索学员姓名、邮箱或公司" placeholder="搜索学员姓名、邮箱或公司"
prefix-icon="el-icon-search" prefix-icon="el-icon-search"
clearable clearable
@keyup.enter="searchStudents"> @keyup.enter="searchStudents">
</el-input> </el-input>
<el-button <el-select v-model="selectedCourseType" placeholder="请选择课程" clearable style="width: 200px;" @change="handleCourseTypeChange">
type="primary" <el-option v-for="item in courseTypesList" :key="item.id" :label="item.name" :value="item.id">
icon="el-icon-search" </el-option>
</el-select>
<el-button
type="primary"
icon="el-icon-search"
@click="searchStudents" @click="searchStudents"
:loading="studentsLoading"> :loading="studentsLoading">
搜索 搜索
</el-button> </el-button>
</div> </div>
<el-table <el-table
ref="systemStudentTable" ref="systemStudentTable"
:data="students" :data="students"
@ -714,6 +718,7 @@
import { saveEmailTemplate, getEmailTemplateList, deleteEmailTemplate } from '@/api/email/index' import { saveEmailTemplate, getEmailTemplateList, deleteEmailTemplate } from '@/api/email/index'
import { uploadEmailRecord } from '@/api/email/index' import { uploadEmailRecord } from '@/api/email/index'
import { saveEmailRecord, getEmailRecordList, sendEmail } from '@/api/email/index' import { saveEmailRecord, getEmailRecordList, sendEmail } from '@/api/email/index'
import { index as getCourseList } from '@/api/course/index'
export default { export default {
name: 'EmailManagement', name: 'EmailManagement',
@ -769,6 +774,8 @@ export default {
// //
systemStudentSearch: '', systemStudentSearch: '',
selectedCourseType: '', //
courseTypesList: [], //
selectedSystemStudents: [], // selectedSystemStudents: [], //
allSelectedStudents: [], // allSelectedStudents: [], //
studentsLoading: false, studentsLoading: false,
@ -1432,6 +1439,8 @@ export default {
this.selectedSystemStudents = [] // this.selectedSystemStudents = [] //
this.allSelectedStudents = [] // this.allSelectedStudents = [] //
this.systemStudentSearch = '' // this.systemStudentSearch = '' //
this.selectedCourseType = '' //
this.loadCourseTypes() //
this.loadStudents() // this.loadStudents() //
}, },
@ -1572,6 +1581,47 @@ export default {
this.$message.success(`已在控制台打印 ${uniqueSelections.length} 条数据(去重后,包含所有分页选择)`) this.$message.success(`已在控制台打印 ${uniqueSelections.length} 条数据(去重后,包含所有分页选择)`)
}, },
//
loadCourseTypes() {
getCourseList({
page: 1,
page_size: 999,
sort_name: 'id',
sort_type: 'ASC'
}).then(res => {
this.courseTypesList = res.data
}).catch(error => {
console.error('加载课程列表失败:', error)
})
},
//
handleCourseTypeChange(courseId) {
if (courseId) {
//
const selectedCourse = this.courseTypesList.find(course => course.id === courseId)
if (selectedCourse) {
//
this.systemStudentSearch = selectedCourse.name
//
this.selectedSystemStudents = []
//
this.currentPage = 1
// loadStudents
this.loadStudents()
}
} else {
//
this.systemStudentSearch = ''
//
this.selectedSystemStudents = []
//
this.currentPage = 1
//
this.loadStudents()
}
},
// //
loadStudents() { loadStudents() {
this.studentsLoading = true this.studentsLoading = true
@ -1589,6 +1639,11 @@ export default {
params.keyword = this.systemStudentSearch.trim() params.keyword = this.systemStudentSearch.trim()
} }
//
if (this.selectedCourseType) {
params.course_id = this.selectedCourseType
}
index(params, false).then(res => { index(params, false).then(res => {
// //
const currentSelection = [...this.selectedSystemStudents] const currentSelection = [...this.selectedSystemStudents]

@ -12,17 +12,42 @@
<div class="filter-section"> <div class="filter-section">
<el-form :model="filter" label-position="top"> <el-form :model="filter" label-position="top">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="6"> <el-col :span="4">
<el-form-item label="时间范围" prop="timeRange"> <el-form-item label="时间范围" prop="timeRange">
<el-select v-model="filter.timeRange" placeholder="请选择时间范围" style="width: 100%;"> <el-select v-model="filter.timeRange" placeholder="请选择时间范围" style="width: 100%;" @change="handleTimeRangeChange">
<el-option label="最近7天" :value="7"></el-option> <el-option label="最近7天" :value="7"></el-option>
<el-option label="最近30天" :value="30"></el-option> <el-option label="最近30天" :value="30"></el-option>
<el-option label="最近90天" :value="90"></el-option> <el-option label="最近90天" :value="90"></el-option>
<el-option label="最近一年" :value="365"></el-option> <el-option label="最近一年" :value="365"></el-option>
<el-option label="自定义时间段" :value="'custom'"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="4" v-if="filter.timeRange === 'custom'">
<el-form-item label="开始日期" prop="startDate">
<el-date-picker
v-model="filter.startDate"
type="date"
placeholder="选择开始日期"
style="width: 100%;"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="4" v-if="filter.timeRange === 'custom'">
<el-form-item label="结束日期" prop="endDate">
<el-date-picker
v-model="filter.endDate"
type="date"
placeholder="选择结束日期"
style="width: 100%;"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="供需类型" prop="supplyType"> <el-form-item label="供需类型" prop="supplyType">
<el-select v-model="filter.supplyType" placeholder="请选择供需类型" clearable style="width: 100%;"> <el-select v-model="filter.supplyType" placeholder="请选择供需类型" clearable style="width: 100%;">
<el-option label="全部" value=""></el-option> <el-option label="全部" value=""></el-option>
@ -31,7 +56,7 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="4">
<el-form-item label="操作"> <el-form-item label="操作">
<el-button type="primary" @click="updateStats" :loading="loading"> <el-button type="primary" @click="updateStats" :loading="loading">
{{ loading ? '更新中...' : '更新统计' }} {{ loading ? '更新中...' : '更新统计' }}
@ -52,7 +77,7 @@
</div> </div>
</div> </div>
<div class="stat-value">{{ chartData.totalCount || 0 }}</div> <div class="stat-value">{{ chartData.totalCount || 0 }}</div>
<div class="stat-change" :class="getChangeClass(chartData.publishChange)"> <div class="stat-change" v-if="filter.timeRange !== 'custom'" :class="getChangeClass(chartData.publishChange)">
<i :class="getChangeIcon(chartData.publishChange)"></i> <i :class="getChangeIcon(chartData.publishChange)"></i>
<span>{{ formatChange(chartData.publishChange) }} 较上期</span> <span>{{ formatChange(chartData.publishChange) }} 较上期</span>
</div> </div>
@ -66,7 +91,7 @@
</div> </div>
</div> </div>
<div class="stat-value">{{ chartData.messageCount || 0 }}</div> <div class="stat-value">{{ chartData.messageCount || 0 }}</div>
<div class="stat-change" :class="getChangeClass(chartData.messageChange)"> <div class="stat-change" v-if="filter.timeRange !== 'custom'" :class="getChangeClass(chartData.messageChange)">
<i :class="getChangeIcon(chartData.messageChange)"></i> <i :class="getChangeIcon(chartData.messageChange)"></i>
<span>{{ formatChange(chartData.messageChange) }} 较上期</span> <span>{{ formatChange(chartData.messageChange) }} 较上期</span>
</div> </div>
@ -80,10 +105,11 @@
</div> </div>
</div> </div>
<div class="stat-value">{{ chartData.interactionCount || 0 }}</div> <div class="stat-value">{{ chartData.interactionCount || 0 }}</div>
<div class="stat-change" :class="getChangeClass(chartData.interactionChange)"> <div class="stat-change" v-if="filter.timeRange !== 'custom'" :class="getChangeClass(chartData.interactionChange)">
<i :class="getChangeIcon(chartData.interactionChange)"></i> <i :class="getChangeIcon(chartData.interactionChange)"></i>
<span>{{ formatChange(chartData.interactionChange) }} 较上期</span> <span>{{ formatChange(chartData.interactionChange) }} 较上期</span>
</div> </div>
<div class="stat-description">周期内有私信来回计一次</div>
</div> </div>
</div> </div>
@ -94,26 +120,26 @@
</div> </div>
<div class="data-table"> <div class="data-table">
<el-table :data="interactionList" style="width: 100%" v-loading="tableLoading"> <el-table :data="interactionList" style="width: 100%" v-loading="tableLoading">
<el-table-column label="供需信息" min-width="200"> <el-table-column label="供需信息" min-width="200">
<template slot-scope="scope"> <template slot-scope="scope">
<div> <div>
<div style="font-weight: 600; font-size: 14px; margin-bottom: 5px;">{{ scope.row.title || '-' }}</div> <div style="font-weight: 600; font-size: 14px; margin-bottom: 5px;">{{ scope.row.title || '-' }}</div>
<div style="font-size: 12px; color: #666; margin-bottom: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" :title="scope.row.content || '-'">{{ scope.row.content || '-' }}</div> <div style="font-size: 12px; color: #666; margin-bottom: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" :title="scope.row.content || '-'">{{ scope.row.content || '-' }}</div>
<el-tag :type="getTypeTagType(scope.row.type)" size="small"> <el-tag :type="getTypeTagType(scope.row.type)" size="small">
{{ getTypeDisplayValue(scope.row.type) }} {{ getTypeDisplayValue(scope.row.type) }}
</el-tag> </el-tag>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="发布者" min-width="150"> <el-table-column label="发布者" min-width="150">
<template slot-scope="scope"> <template slot-scope="scope">
<div class="user-info"> <div class="user-info">
<div class="user-avatar">{{ scope.row.user && scope.row.user.name ? scope.row.user.name.charAt(0) : '-' }}</div> <div class="user-avatar">{{ scope.row.user && scope.row.user.name ? scope.row.user.name.charAt(0) : '-' }}</div>
<div> <div>
<div style="font-weight: 600; font-size: 14px;">{{ scope.row.user && scope.row.user.name ? scope.row.user.name : '-' }}</div> <div style="font-weight: 600; font-size: 14px;">{{ scope.row.user && scope.row.user.name ? scope.row.user.name : '-' }}</div>
<div style="font-size: 12px; color: #666;">{{ getPublisherInfo(scope.row.user) }}</div> <div style="font-size: 12px; color: #666;">{{ getPublisherInfo(scope.row.user) }}</div>
</div> </div>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="发布时间" min-width="120"> <el-table-column label="发布时间" min-width="120">
@ -125,11 +151,17 @@
<template slot-scope="scope"> <template slot-scope="scope">
<div> <div>
<div class="interaction-detail"> <div class="interaction-detail">
<div v-if="scope.row.messages && scope.row.messages.length > 0"> <div v-if="scope.row.messages && scope.row.messages.length > 0">
<div v-for="(message, index) in scope.row.messages" :key="index" style="margin-bottom: 8px;"> <div v-for="(message, index) in scope.row.messages" :key="index" style="margin-bottom: 8px;">
<span style="font-size: 12px;">{{ message.to_user.name || '-' }}({{ message.to_user.year || '-' }}) · {{ formatDateTime(message.created_at) || '-' }}</span> <span
</div> class="clickable-user"
</div> @click="showInteractionDetail(scope.row, message.to_user)"
style="font-size: 12px; cursor: pointer; color: #409EFF; text-decoration: underline;">
{{ (message.to_user && message.to_user.name) || '-' }}({{ (message.to_user && message.to_user.year) || '-' }})
</span>
<span style="font-size: 12px; color: #666;"> · {{ formatDateTime(message.created_at) || '-' }}</span>
</div>
</div>
<div v-else style="color: #999; font-size: 12px;">暂无交互记录</div> <div v-else style="color: #999; font-size: 12px;">暂无交互记录</div>
</div> </div>
</div> </div>
@ -160,11 +192,54 @@
</el-pagination> </el-pagination>
</div> </div>
</div> </div>
<!-- 交互详情对话框 -->
<el-dialog
:title="getDialogTitle()"
:visible.sync="interactionDetailVisible"
width="800px"
:before-close="handleCloseDialog">
<div class="interaction-detail-dialog">
<div v-if="currentSupplyDemand" class="supply-demand-info">
<h4>{{ currentSupplyDemand.title || '供需信息' }}</h4>
<p class="supply-demand-content">{{ currentSupplyDemand.content || '暂无内容' }}</p>
</div>
<div class="conversation-users" v-if="currentSupplyDemand && currentSupplyDemand.user">
<div class="user-indicator left">
<div class="user-avatar-small">{{ (currentTargetUser && currentTargetUser.name) ? currentTargetUser.name.charAt(0) : 'U' }}</div>
<span class="user-name">{{ (currentTargetUser && currentTargetUser.name) || '用户' }}</span>
</div>
<div class="user-indicator right">
<div class="user-avatar-small">{{ (currentSupplyDemand.user && currentSupplyDemand.user.name) ? currentSupplyDemand.user.name.charAt(0) : 'P' }}</div>
<span class="user-name">{{ (currentSupplyDemand.user && currentSupplyDemand.user.name) || '发布者' }}</span>
</div>
</div>
<div class="message-list-container" v-if="currentSupplyDemand && currentSupplyDemand.user">
<div v-if="messageList.length > 0" class="message-list">
<div v-for="(message, index) in messageList" :key="index" class="message-item" :class="getMessageItemClass(message)">
<div class="message-bubble" :class="getMessageBubbleClass(message)">
<div class="message-content">{{ message.content || '暂无内容' }}</div>
<div class="message-time">{{ formatDateTime(message.created_at) }}</div>
</div>
</div>
</div>
<div v-else class="empty-messages">
<i class="el-icon-chat-dot-round" style="font-size: 48px; color: #909399;"></i>
<p style="margin-top: 16px; color: #909399;">暂无消息记录</p>
</div>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="interactionDetailVisible = false">关闭</el-button>
</span>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import { supplyDemandChart } from '@/api/student' import { supplyDemandChart, getMessageList } from '@/api/student'
export default { export default {
name: 'InteractionStats', name: 'InteractionStats',
@ -172,13 +247,19 @@ export default {
return { return {
filter: { filter: {
timeRange: 30, timeRange: 30,
supplyType: '' supplyType: '',
startDate: '',
endDate: ''
}, },
currentPage: 1, currentPage: 1,
pageSize: 10, pageSize: 10,
totalCount: 0, totalCount: 0,
loading: false, loading: false,
tableLoading: false, tableLoading: false,
interactionDetailVisible: false,
messageList: [],
currentSupplyDemand: null,
currentTargetUser: null,
chartData: { chartData: {
totalCount: 0, totalCount: 0,
messageCount: 0, messageCount: 0,
@ -194,15 +275,41 @@ export default {
this.fetchChartData() this.fetchChartData()
}, },
methods: { methods: {
//
handleTimeRangeChange(value) {
if (value === 'custom') {
// 30
const endDate = new Date()
const startDate = new Date()
startDate.setDate(endDate.getDate() - 30)
this.filter.startDate = startDate.toISOString().split('T')[0]
this.filter.endDate = endDate.toISOString().split('T')[0]
} else {
//
this.filter.startDate = ''
this.filter.endDate = ''
}
},
// //
async fetchChartData() { async fetchChartData() {
try { try {
this.loading = true this.loading = true
// timeRange // timeRange
const endDate = new Date() let startDate, endDate
const startDate = new Date()
startDate.setDate(endDate.getDate() - this.filter.timeRange) if (this.filter.timeRange === 'custom') {
// 使
startDate = new Date(this.filter.startDate)
endDate = new Date(this.filter.endDate)
} else {
// 使
endDate = new Date()
startDate = new Date()
startDate.setDate(endDate.getDate() - this.filter.timeRange)
}
const params = { const params = {
start_date: startDate.toISOString().split('T')[0], // YYYY-MM-DD start_date: startDate.toISOString().split('T')[0], // YYYY-MM-DD
@ -222,9 +329,9 @@ export default {
totalCount: response.supply_demand_count || 0, totalCount: response.supply_demand_count || 0,
messageCount: response.message_count || 0, messageCount: response.message_count || 0,
interactionCount: response.interaction_count || 0, interactionCount: response.interaction_count || 0,
publishChange: response.supply_demand_growth_rate?.rate || 0, publishChange: (response.supply_demand_growth_rate && response.supply_demand_growth_rate.rate) || 0,
messageChange: response.message_growth_rate?.rate || 0, messageChange: (response.message_growth_rate && response.message_growth_rate.rate) || 0,
interactionChange: response.interaction_growth_rate?.rate || 0 interactionChange: (response.interaction_growth_rate && response.interaction_growth_rate.rate) || 0
} }
// //
@ -252,6 +359,22 @@ export default {
// //
async updateStats() { async updateStats() {
//
if (this.filter.timeRange === 'custom') {
if (!this.filter.startDate || !this.filter.endDate) {
this.$message.warning('请选择开始和结束日期')
return
}
const startDate = new Date(this.filter.startDate)
const endDate = new Date(this.filter.endDate)
if (startDate > endDate) {
this.$message.error('开始日期不能晚于结束日期')
return
}
}
await this.fetchChartData() await this.fetchChartData()
this.$message.success('统计数据已更新') this.$message.success('统计数据已更新')
}, },
@ -347,6 +470,83 @@ export default {
return 'info' return 'info'
}, },
//
async showInteractionDetail(row, targetUser) {
//
if (!row || !row.user) {
this.$message.error('无效的供需信息或用户信息')
return
}
this.currentSupplyDemand = row
this.currentTargetUser = targetUser
this.interactionDetailVisible = true
try {
//
const params = {
to_user_id: (row.user && row.user.id) || '',
user_id: (row.user && row.user.id) || '',
page_size: 50,
page: 1
}
const response = await getMessageList(params)
if (response && response.data) {
this.messageList = response.data
} else {
this.messageList = []
}
} catch (error) {
console.error('获取消息列表失败:', error)
// this.$message.warning('')
// mock
this.messageList = this.generateMockMessages(row, targetUser)
}
},
//
handleCloseDialog(done) {
this.interactionDetailVisible = false
this.messageList = []
this.currentSupplyDemand = null
this.currentTargetUser = null
done()
},
//
getDialogTitle() {
if (!this.currentSupplyDemand || !this.currentTargetUser) {
return '交互详情'
}
const publisherName = (this.currentSupplyDemand.user && this.currentSupplyDemand.user.name) || '发布者'
const targetUserName = (this.currentTargetUser && this.currentTargetUser.name) || '用户'
return `${publisherName}${targetUserName} 的交互详情`
},
//
getMessageBubbleClass(message) {
//
const isPublisher = this.currentSupplyDemand &&
this.currentSupplyDemand.user &&
message.from_user &&
message.from_user.id === this.currentSupplyDemand.user.id
return isPublisher ? 'message-right' : 'message-left'
},
//
getMessageItemClass(message) {
//
const isPublisher = this.currentSupplyDemand &&
this.currentSupplyDemand.user &&
message.from_user &&
message.from_user.id === this.currentSupplyDemand.user.id
return isPublisher ? 'message-right' : 'message-left'
},
// //
formatDateTime(dateTime) { formatDateTime(dateTime) {
if (!dateTime) return '-' if (!dateTime) return '-'
@ -380,6 +580,71 @@ export default {
console.error('格式化日期时间失败:', error, dateTime) console.error('格式化日期时间失败:', error, dateTime)
return '-' return '-'
} }
},
//
generateMockMessages(supplyDemand, targetUser) {
const mockMessages = []
const now = new Date()
//
const publisherName = (supplyDemand.user && supplyDemand.user.name) || '发布者'
//
mockMessages.push({
id: 'mock_1',
from_user: {
id: supplyDemand.user ? supplyDemand.user.id : 'mock_publisher',
name: publisherName
},
to_user: targetUser,
content: `您好!看到您对我们发布的"${supplyDemand.title || '供需信息'}"感兴趣,请问有什么具体需求吗?`,
created_at: new Date(now.getTime() - 2 * 60 * 60 * 1000).toISOString() // 2
})
//
mockMessages.push({
id: 'mock_2',
from_user: targetUser,
to_user: supplyDemand.user,
content: '您好!我对这个很感兴趣,想了解更多详细信息,可以详细介绍一下吗?',
created_at: new Date(now.getTime() - 1.5 * 60 * 60 * 1000).toISOString() // 1.5
})
//
mockMessages.push({
id: 'mock_3',
from_user: {
id: supplyDemand.user ? supplyDemand.user.id : 'mock_publisher',
name: publisherName
},
to_user: targetUser,
content: '当然可以!具体情况是这样的:' + (supplyDemand.content ? supplyDemand.content.substring(0, 100) + '...' : '这是一个很好的机会,我们提供专业的服务和优质的产品。'),
created_at: new Date(now.getTime() - 1 * 60 * 60 * 1000).toISOString() // 1
})
//
mockMessages.push({
id: 'mock_4',
from_user: targetUser,
to_user: supplyDemand.user,
content: '听起来很不错!我想进一步了解合作细节,方便电话沟通吗?',
created_at: new Date(now.getTime() - 30 * 60 * 1000).toISOString() // 30
})
//
mockMessages.push({
id: 'mock_5',
from_user: {
id: supplyDemand.user ? supplyDemand.user.id : 'mock_publisher',
name: publisherName
},
to_user: targetUser,
content: '好的我的联系电话是138****8888微信wxid_123欢迎随时联系',
created_at: new Date(now.getTime() - 15 * 60 * 1000).toISOString() // 15
})
return mockMessages
} }
}, },
@ -506,6 +771,13 @@ export default {
margin-bottom: 5px; margin-bottom: 5px;
} }
.stat-description {
font-size: 12px;
color: #909399;
margin-bottom: 8px;
line-height: 1.4;
}
.stat-change { .stat-change {
font-size: 12px; font-size: 12px;
display: flex; display: flex;
@ -658,4 +930,164 @@ export default {
} }
} }
} }
//
.interaction-detail-dialog {
min-height: 400px;
}
.supply-demand-info {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 4px solid #409EFF;
h4 {
margin: 0 0 8px 0;
color: #2c3e50;
font-size: 16px;
}
.supply-demand-content {
margin: 0;
color: #666;
font-size: 14px;
line-height: 1.5;
}
}
.conversation-users {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.user-indicator {
display: flex;
align-items: center;
gap: 8px;
&.left {
.user-avatar-small {
background: #f0f0f0;
color: #333;
}
}
&.right {
.user-avatar-small {
background: #67C23A;
color: white;
}
}
}
.user-avatar-small {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
}
.user-name {
font-size: 14px;
font-weight: 500;
color: #2c3e50;
}
.message-list-container {
max-height: 400px;
overflow-y: auto;
}
.message-list {
padding: 0;
}
.message-item {
margin-bottom: 16px;
display: flex;
justify-content: flex-start;
&.message-right {
justify-content: flex-end;
}
&.message-left {
justify-content: flex-start;
}
}
.message-bubble {
max-width: 70%;
padding: 12px 16px;
border-radius: 18px;
position: relative;
word-wrap: break-word;
&.message-left {
background: #f0f0f0;
color: #333;
margin-right: auto;
border-bottom-left-radius: 4px;
margin-left: 0;
}
&.message-right {
background: #67C23A;
color: white;
margin-left: auto;
border-bottom-right-radius: 4px;
margin-right: 0;
}
}
.message-time {
font-size: 11px;
color: #999;
margin-top: 6px;
text-align: right;
opacity: 0.8;
}
.message-content {
font-size: 14px;
line-height: 1.4;
word-break: break-word;
}
.empty-messages {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 0;
}
.empty-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
}
.clickable-user {
transition: all 0.2s ease;
&:hover {
color: #66b1ff !important;
text-decoration: none !important;
}
}
</style> </style>

Loading…
Cancel
Save