374 lines
7.8 KiB
Vue
374 lines
7.8 KiB
Vue
|
|
<script>
|
||
|
|
import { mapActions } from 'vuex';
|
||
|
|
|
||
|
|
export default {
|
||
|
|
props: {
|
||
|
|
title: {
|
||
|
|
type: String,
|
||
|
|
default: '',
|
||
|
|
},
|
||
|
|
headers: {
|
||
|
|
type: Array,
|
||
|
|
default: () => [],
|
||
|
|
},
|
||
|
|
rows: {
|
||
|
|
type: Array,
|
||
|
|
default: () => [],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
computed: {
|
||
|
|
// Check if any row has a button
|
||
|
|
hasButtonColumn() {
|
||
|
|
return this.rows.some(row => {
|
||
|
|
const lastItem = row[row.length - 1];
|
||
|
|
return lastItem && typeof lastItem === 'object' && lastItem.button;
|
||
|
|
});
|
||
|
|
},
|
||
|
|
// Get the first row with button to determine column count
|
||
|
|
firstRowWithButton() {
|
||
|
|
return this.rows.find(row => {
|
||
|
|
const lastItem = row[row.length - 1];
|
||
|
|
return lastItem && typeof lastItem === 'object' && lastItem.button;
|
||
|
|
});
|
||
|
|
},
|
||
|
|
// Get number of data columns (excluding button)
|
||
|
|
dataColumnCount() {
|
||
|
|
if (this.firstRowWithButton) {
|
||
|
|
return this.firstRowWithButton.length - 1;
|
||
|
|
}
|
||
|
|
// If no button row, check first row
|
||
|
|
if (this.rows.length > 0) {
|
||
|
|
return this.rows[0].length;
|
||
|
|
}
|
||
|
|
return this.headers.length;
|
||
|
|
},
|
||
|
|
// Check if headers has button header column
|
||
|
|
hasButtonHeader() {
|
||
|
|
return this.headers.length > this.dataColumnCount;
|
||
|
|
},
|
||
|
|
// Get data headers (all headers if no button header, otherwise all except last)
|
||
|
|
displayHeaders() {
|
||
|
|
if (this.hasButtonHeader) {
|
||
|
|
return this.headers.slice(0, -1);
|
||
|
|
}
|
||
|
|
return this.headers;
|
||
|
|
},
|
||
|
|
// Get button header text
|
||
|
|
buttonHeaderText() {
|
||
|
|
if (this.hasButtonHeader) {
|
||
|
|
return this.headers[this.headers.length - 1];
|
||
|
|
}
|
||
|
|
return '';
|
||
|
|
},
|
||
|
|
// Check if button header should be shown
|
||
|
|
showButtonHeader() {
|
||
|
|
return this.hasButtonColumn && this.hasButtonHeader;
|
||
|
|
},
|
||
|
|
},
|
||
|
|
methods: {
|
||
|
|
...mapActions('conversation', ['sendMessage']),
|
||
|
|
onButtonClick(button) {
|
||
|
|
if (!button) return;
|
||
|
|
|
||
|
|
const { reply } = button;
|
||
|
|
if (reply) {
|
||
|
|
this.sendMessage({ content: reply });
|
||
|
|
}
|
||
|
|
},
|
||
|
|
// Get display cells for a row
|
||
|
|
getDisplayCells(row) {
|
||
|
|
const lastItem = row[row.length - 1];
|
||
|
|
const hasButton = lastItem && typeof lastItem === 'object' && lastItem.button;
|
||
|
|
|
||
|
|
if (hasButton) {
|
||
|
|
return row.slice(0, -1);
|
||
|
|
}
|
||
|
|
return row;
|
||
|
|
},
|
||
|
|
// Get button from row
|
||
|
|
getRowButton(row) {
|
||
|
|
const lastItem = row[row.length - 1];
|
||
|
|
if (lastItem && typeof lastItem === 'object' && lastItem.button) {
|
||
|
|
return lastItem.button;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<template>
|
||
|
|
<div class="table-message-container">
|
||
|
|
<div class="table-bubble">
|
||
|
|
<h4 v-if="title" class="table-title">
|
||
|
|
{{ title }}
|
||
|
|
</h4>
|
||
|
|
<div class="table-scroll">
|
||
|
|
<table class="table-content">
|
||
|
|
<thead>
|
||
|
|
<tr class="header-row">
|
||
|
|
<th
|
||
|
|
v-for="(header, index) in displayHeaders"
|
||
|
|
:key="'header-' + index"
|
||
|
|
class="header-cell"
|
||
|
|
>
|
||
|
|
{{ header }}
|
||
|
|
</th>
|
||
|
|
<th v-if="showButtonHeader" class="header-cell sticky-header">
|
||
|
|
{{ buttonHeaderText }}
|
||
|
|
</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<tr
|
||
|
|
v-for="(row, rowIndex) in rows"
|
||
|
|
:key="'row-' + rowIndex"
|
||
|
|
class="data-row"
|
||
|
|
>
|
||
|
|
<td
|
||
|
|
v-for="(cell, cellIndex) in getDisplayCells(row)"
|
||
|
|
:key="'cell-' + cellIndex"
|
||
|
|
class="data-cell"
|
||
|
|
>
|
||
|
|
{{ cell }}
|
||
|
|
</td>
|
||
|
|
<td v-if="hasButtonColumn" class="data-cell sticky-cell">
|
||
|
|
<button
|
||
|
|
v-if="getRowButton(row)"
|
||
|
|
class="table-action-button"
|
||
|
|
@click="onButtonClick(getRowButton(row))"
|
||
|
|
>
|
||
|
|
{{ getRowButton(row).text }}
|
||
|
|
</button>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.table-message-container {
|
||
|
|
display: flex;
|
||
|
|
width: 100%;
|
||
|
|
margin-bottom: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.table-bubble {
|
||
|
|
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||
|
|
border-radius: 12px;
|
||
|
|
padding: 16px;
|
||
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.05);
|
||
|
|
border: 1px solid #e2e8f0;
|
||
|
|
width: 100%;
|
||
|
|
max-width: 100%;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dark .table-bubble {
|
||
|
|
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
|
||
|
|
border-color: #475569;
|
||
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25), 0 1px 3px rgba(0, 0, 0, 0.15);
|
||
|
|
}
|
||
|
|
|
||
|
|
.table-title {
|
||
|
|
font-size: 15px;
|
||
|
|
font-weight: 600;
|
||
|
|
line-height: 1.4;
|
||
|
|
color: #1e293b;
|
||
|
|
margin: 0 0 12px 0;
|
||
|
|
padding-bottom: 10px;
|
||
|
|
border-bottom: 2px solid #e2e8f0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dark .table-title {
|
||
|
|
color: #f1f5f9;
|
||
|
|
border-bottom-color: #475569;
|
||
|
|
}
|
||
|
|
|
||
|
|
.table-scroll {
|
||
|
|
overflow-x: auto;
|
||
|
|
overflow-y: hidden;
|
||
|
|
border-radius: 8px;
|
||
|
|
max-width: 100%;
|
||
|
|
}
|
||
|
|
|
||
|
|
.table-content {
|
||
|
|
width: 100%;
|
||
|
|
border-collapse: separate;
|
||
|
|
border-spacing: 0;
|
||
|
|
font-size: 13px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.header-row {
|
||
|
|
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||
|
|
}
|
||
|
|
|
||
|
|
.header-cell {
|
||
|
|
padding: 10px 12px;
|
||
|
|
text-align: left;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #ffffff;
|
||
|
|
font-size: 12px;
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 0.03em;
|
||
|
|
}
|
||
|
|
|
||
|
|
.header-cell:first-child {
|
||
|
|
border-top-left-radius: 6px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.sticky-header {
|
||
|
|
position: sticky;
|
||
|
|
right: 0;
|
||
|
|
background: inherit;
|
||
|
|
z-index: 10;
|
||
|
|
min-width: 100px;
|
||
|
|
text-align: center;
|
||
|
|
box-shadow: -2px 0 4px rgba(0, 0, 0, 0.1);
|
||
|
|
}
|
||
|
|
|
||
|
|
.data-row {
|
||
|
|
background-color: #ffffff;
|
||
|
|
transition: all 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.data-row:hover {
|
||
|
|
background-color: #f1f5f9;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dark .data-row {
|
||
|
|
background-color: #1e293b;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dark .data-row:hover {
|
||
|
|
background-color: #334155;
|
||
|
|
}
|
||
|
|
|
||
|
|
.data-row:nth-child(even) {
|
||
|
|
background-color: #f8fafc;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dark .data-row:nth-child(even) {
|
||
|
|
background-color: #0f172a;
|
||
|
|
}
|
||
|
|
|
||
|
|
.data-cell {
|
||
|
|
padding: 10px 12px;
|
||
|
|
color: #475569;
|
||
|
|
border-bottom: 1px solid #e2e8f0;
|
||
|
|
border-right: 1px solid #f1f5f9;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dark .data-cell {
|
||
|
|
color: #cbd5e1;
|
||
|
|
border-bottom-color: #334155;
|
||
|
|
border-right-color: #1e293b;
|
||
|
|
}
|
||
|
|
|
||
|
|
.sticky-cell {
|
||
|
|
position: sticky;
|
||
|
|
right: 0;
|
||
|
|
background: inherit;
|
||
|
|
z-index: 10;
|
||
|
|
text-align: center;
|
||
|
|
padding: 8px;
|
||
|
|
box-shadow: -2px 0 4px rgba(0, 0, 0, 0.1);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dark .sticky-cell {
|
||
|
|
background-color: #1e293b;
|
||
|
|
box-shadow: -2px 0 4px rgba(0, 0, 0, 0.3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.data-row:hover .sticky-cell {
|
||
|
|
background-color: #f1f5f9;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dark .data-row:hover .sticky-cell {
|
||
|
|
background-color: #334155;
|
||
|
|
}
|
||
|
|
|
||
|
|
.data-row:nth-child(even) .sticky-cell {
|
||
|
|
background-color: #f8fafc;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dark .data-row:nth-child(even) .sticky-cell {
|
||
|
|
background-color: #0f172a;
|
||
|
|
}
|
||
|
|
|
||
|
|
.data-row:last-child .data-cell {
|
||
|
|
border-bottom: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.data-row:last-child .data-cell:first-child {
|
||
|
|
border-bottom-left-radius: 6px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.data-row:last-child .sticky-cell {
|
||
|
|
border-bottom-right-radius: 6px;
|
||
|
|
border-bottom: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.table-action-button {
|
||
|
|
padding: 6px 12px;
|
||
|
|
font-size: 12px;
|
||
|
|
font-weight: 500;
|
||
|
|
color: #3b82f6;
|
||
|
|
background: transparent;
|
||
|
|
border: 1px solid #3b82f6;
|
||
|
|
border-radius: 6px;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: all 0.2s ease;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.table-action-button:hover {
|
||
|
|
background: #3b82f6;
|
||
|
|
color: #ffffff;
|
||
|
|
transform: translateY(-1px);
|
||
|
|
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.table-action-button:active {
|
||
|
|
transform: translateY(0);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dark .table-action-button {
|
||
|
|
color: #60a5fa;
|
||
|
|
border-color: #60a5fa;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dark .table-action-button:hover {
|
||
|
|
background: #60a5fa;
|
||
|
|
color: #1e293b;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* 响应式设计 */
|
||
|
|
@media (max-width: 480px) {
|
||
|
|
.table-bubble {
|
||
|
|
padding: 12px;
|
||
|
|
border-radius: 10px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.table-title {
|
||
|
|
font-size: 14px;
|
||
|
|
margin-bottom: 10px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.header-cell,
|
||
|
|
.data-cell {
|
||
|
|
padding: 8px 10px;
|
||
|
|
font-size: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.table-action-button {
|
||
|
|
padding: 5px 10px;
|
||
|
|
font-size: 11px;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|