Commit 168490ae by yuzhenWang

Merge branch 'test' into 'feature-20260313wyz-修改产品'

Test

See merge request !13
parents 06f2713e 15b5f869
......@@ -21,17 +21,23 @@
"@element-plus/icons-vue": "2.3.1",
"@vueup/vue-quill": "1.2.0",
"@vueuse/core": "13.3.0",
"ali-oss": "^6.23.0",
"axios": "1.9.0",
"clipboard": "2.0.11",
"dompurify": "^3.3.3",
"echarts": "5.6.0",
"element-plus": "2.9.9",
"file-saver": "2.0.5",
"file-saver": "^2.0.5",
"fuse.js": "6.6.2",
"highlight.js": "^11.11.1",
"js-beautify": "1.14.11",
"js-cookie": "3.0.5",
"jsencrypt": "3.3.2",
"jszip": "^3.10.1",
"lodash-es": "^4.17.21",
"marked": "^4.3.0",
"nprogress": "0.2.0",
"p-limit": "^7.3.0",
"pinia": "3.0.2",
"splitpanes": "^4.0.4",
"vue": "3.5.16",
......
// src/api/ai/ai.js
import request from '@/utils/request'
import { getToken } from '@/utils/auth'
import useUserStore from '@/store/modules/user'
// 加载随机词条列表
export function randList(data) {
return request({
url: '/ai/api/entry/rand/list',
method: 'post',
data: data
})
}
/**
* 查询输出流信息(流式响应)
* 增强错误处理:当HTTP状态非2xx时,解析错误响应体并抛出包含code属性的错误
*/
export function getStream(question, timeout = 300000) {
const userStore = useUserStore()
const token = getToken()
const tenantId = userStore.currentTenant?.apiLoginTenantInfoResponse?.tenantBizId
const headers = {
'Accept': 'text/event-stream',
'Authorization': `Bearer ${token}`
}
if (tenantId) {
headers['X-Tenant-ID'] = tenantId
}
const baseUrl = import.meta.env.VITE_APP_BASE_API
const url = `${baseUrl}/ai/api/api/ai/stream?question=${encodeURIComponent(question)}`
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
return fetch(url, {
method: 'GET',
headers,
signal: controller.signal
}).then(async (response) => {
clearTimeout(timeoutId)
if (!response.ok) {
// 尝试解析错误响应体
let errorCode = null
let errorMessage = response.statusText
try {
const errorBody = await response.json()
if (errorBody && errorBody.code !== undefined) {
errorCode = errorBody.code
errorMessage = errorBody.msg || errorBody.message || errorMessage
}
} catch (e) {
// 无法解析JSON,使用默认错误信息
}
const error = new Error(errorMessage)
error.code = errorCode
error.status = response.status
throw error
}
return response
}).finally(() => {
clearTimeout(timeoutId)
})
}
/**
* 获取服务卡片列表(产品列表)
*/
export async function getServiceCardList() {
const userStore = useUserStore()
const token = getToken()
const userId = userStore.userInfo?.userId || userStore.userId || '30'
let cleanToken = token
if (token && token.startsWith('Bearer ')) {
cleanToken = token.slice(7)
}
const headers = {
'Content-Type': 'application/json',
'x-authorization': `sfpfamilyfinancialplanning ${cleanToken}`
}
const url = 'https://hoservice.ydhomeoffice.cn/hoserviceApi/ydServiceCard/serviceCardList'
const body = {
userId: String(userId),
cardType: '11'
}
try {
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(body)
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const data = await response.json()
return data
} catch (error) {
console.error('获取服务卡片列表失败:', error)
throw error
}
}
\ No newline at end of file
......@@ -37,6 +37,22 @@ export const constantRoutes = [
}
]
},
// {
// path: '/system',
// component: Layout,
// children: [
// {
// path: 'ai',
// name: 'Ai',
// component: Ai,
// meta: {
// title: '银盾AI',
// icon: 'message' // 【关键修改】补充图标名,避免 undefined
// // 如果项目里有其他 svg 图标,换成对应的文件名(不含.svg)
// }
// }
// ]
// },
{
path: '/login',
component: () => import('@/views/login'),
......
<template>
<div class="ai-container">
<div class="chat-header">
<div class="header-content">
<div class="logo-area">
<svg class="logo-icon" viewBox="0 0 32 32" fill="none">
<path d="M16 2L4 9v14l12 7 12-7V9L16 2z" stroke="#6c5ce7" stroke-width="1.5" fill="none"/>
<path d="M16 2v30M4 9l12 7 12-7" stroke="#6c5ce7" stroke-width="1.5" fill="none"/>
</svg>
<span class="logo-text">银盾AI</span>
</div>
<div class="header-actions">
<button class="action-btn" @click="clearChat" title="清空对话">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
</button>
</div>
</div>
</div>
<div class="chat-messages" ref="messagesContainer" @click="handleProductClick">
<div v-for="(msg, idx) in messages" :key="idx" :class="['message', msg.role]">
<div class="message-avatar">
<div v-if="msg.role === 'user'" class="user-avatar">U</div>
<div v-else class="ai-avatar">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
<path d="M12 2a10 10 0 1 0 10 10 10 10 0 0 0-10-10z"/>
<path d="M12 6v4M12 16h.01"/>
</svg>
</div>
</div>
<!-- 产品列表消息直接渲染HTML,其他消息使用markdown -->
<div class="message-content" v-if="msg.isProductList" v-html="msg.content"></div>
<div class="message-content" v-else v-html="formatMessage(msg.content)"></div>
</div>
<div v-if="isLoading" class="message assistant">
<div class="message-avatar">
<div class="ai-avatar typing">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
<path d="M12 2a10 10 0 1 0 10 10 10 10 0 0 0-10-10z"/>
<path d="M12 6v4M12 16h.01"/>
</svg>
</div>
</div>
<div class="message-content">
<div class="typing-indicator">
<span></span><span></span><span></span>
</div>
</div>
</div>
<div ref="messagesEnd"></div>
</div>
<div class="chat-input-area">
<div class="suggestions-area" v-if="suggestions.length || suggestionsLoading">
<div class="suggestions-header">
<span class="suggestions-title">💡 你可能想问:</span>
<button class="refresh-suggestions" @click="refreshSuggestions" :disabled="suggestionsLoading">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 2v6h-6M4 12a8 8 0 0 1 16 0 8 8 0 0 1-8 8 8 8 0 0 1-8-8z"/>
<path d="M12 8v4l3 3"/>
</svg>
换一批
</button>
</div>
<div class="suggestions-list">
<div v-if="suggestionsLoading" class="suggestions-loading">
<span class="loading-dot"></span><span class="loading-dot"></span><span class="loading-dot"></span>
</div>
<template v-else>
<button
v-for="(item, idx) in suggestions"
:key="idx"
class="suggestion-chip"
@click="handleSuggestionClick(item.content)"
:disabled="isLoading"
>
{{ item.content }}
</button>
<div v-if="!suggestions.length && !suggestionsLoading" class="suggestions-empty">
暂无推荐,点击换一批
</div>
</template>
</div>
</div>
<div class="input-wrapper">
<textarea
v-model="question"
:disabled="isLoading"
placeholder="请输入问题..."
@keydown.enter.exact.prevent="sendMessage"
@keydown.enter.shift.exact="question += '\n'"
rows="1"
ref="inputTextarea"
></textarea>
<button class="send-btn" @click="sendMessage" :disabled="isLoading || !question.trim()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13"/>
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
</svg>
</button>
</div>
<div class="input-hint">Shift + Enter 换行</div>
</div>
</div>
</template>
<script>
import { randList, getStream, getServiceCardList } from '@/api/ai/ai'
import { marked } from 'marked'
import hljs from 'highlight.js'
import 'highlight.js/styles/github-dark.css'
// 配置 marked,增强安全性
marked.setOptions({
highlight: function(code, lang) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value
}
return hljs.highlightAuto(code).value
},
breaks: true,
gfm: true,
mangle: false,
headerIds: false,
})
export default {
name: 'Ai',
data() {
return {
question: '',
messages: [],
isLoading: false,
suggestions: [],
suggestionsLoading: false,
}
},
mounted() {
this.messages.push({
role: 'assistant',
content: '你好!我是 银盾AI助手,有什么可以帮你的吗?'
})
this.loadSuggestions()
},
methods: {
scrollToBottom() {
this.$nextTick(() => {
const container = this.$refs.messagesContainer
if (container) {
container.scrollTop = container.scrollHeight
}
})
},
async loadSuggestions(randNum = 4) {
this.suggestionsLoading = true
try {
const response = await randList({ randNum })
if (response.code === 200 && Array.isArray(response.data)) {
this.suggestions = response.data.map(item => ({
content: item.content,
id: item.id,
}))
} else {
console.warn('词条接口返回异常:', response)
this.suggestions = []
}
} catch (error) {
console.error('加载词条失败:', error)
this.suggestions = []
} finally {
this.suggestionsLoading = false
}
},
refreshSuggestions() {
if (!this.suggestionsLoading) {
this.loadSuggestions(4)
}
},
handleSuggestionClick(content) {
if (this.isLoading) return
if (content && content.trim()) {
this.question = content.trim()
this.sendMessage()
}
},
formatMessage(content) {
if (!content) return ''
return marked.parse(content)
},
// 生成产品列表HTML
buildProductListHtml(productData) {
if (!productData || !productData.list || !productData.list.length) {
return '<div class="product-list-empty">暂无产品信息</div>'
}
let html = '<div class="product-list">'
productData.list.forEach(card => {
// 获取卡片主图(取第一个子项的图片)
const mainImage = card.list && card.list[0]?.itemImg || ''
// 获取卡片标识(用于购买跳转)
const cardCode = card.list && card.list[0]?.cardCode || ''
const cardName = card.cardName || ''
const price = card.price || '0'
html += `
<div class="product-card" data-product-code="${cardCode}" data-product-name="${cardName}">
${mainImage ? `<div class="product-image"><img src="${mainImage}" alt="${cardName}" loading="lazy"></div>` : ''}
<div class="product-info">
<div class="product-title">${escapeHtml(cardName)}</div>
<div class="product-price">¥${parseFloat(price).toFixed(2)}</div>
<button class="product-buy-btn" data-product-code="${cardCode}" data-product-name="${cardName}">立即购买</button>
</div>
</div>
`
})
html += '</div>'
return html
},
// 获取并展示产品列表
async fetchAndShowProducts(userQuestion) {
this.isLoading = true
try {
const response = await getServiceCardList()
if (response.success && response.data && response.data.list) {
const productHtml = this.buildProductListHtml(response.data)
const messageContent = `
<div class="product-intro">根据您的问题“${escapeHtml(userQuestion)}”,为您推荐以下产品:</div>
${productHtml}
`
this.messages.push({
role: 'assistant',
content: messageContent,
isProductList: true
})
} else {
this.messages.push({
role: 'assistant',
content: '抱歉,获取产品列表失败,请稍后重试。',
isProductList: false
})
}
this.scrollToBottom()
} catch (error) {
console.error('获取产品列表失败:', error)
this.messages.push({
role: 'assistant',
content: '获取产品列表失败,请检查网络后重试。',
isProductList: false
})
this.scrollToBottom()
} finally {
this.isLoading = false
}
},
// 处理产品卡片点击(事件委托)
handleProductClick(event) {
const target = event.target
// 查找实际触发的购买按钮或卡片本身
let productElement = target.closest('.product-buy-btn') || target.closest('.product-card')
if (!productElement) return
// 获取产品标识
let productCode = productElement.dataset.productCode
let productName = productElement.dataset.productName
// 如果是按钮,从按钮上获取;如果是卡片,从卡片上获取
if (!productCode) {
const card = productElement.closest('.product-card')
if (card) {
productCode = card.dataset.productCode
productName = card.dataset.productName
}
}
if (productCode) {
// 跳转到购买页面(根据实际商城地址调整)
const buyUrl = `https://hoservice.ydhomeoffice.cn/ydMall/productDetail?cardCode=${productCode}`
window.open(buyUrl, '_blank')
} else {
console.warn('缺少产品标识,无法跳转')
}
},
async sendMessage() {
if (!this.question.trim() || this.isLoading) return
const userQuestion = this.question.trim()
this.messages.push({ role: 'user', content: userQuestion })
this.question = ''
this.scrollToBottom()
// 正常流式回答
this.isLoading = true
const assistantMessageIndex = this.messages.length
this.messages.push({ role: 'assistant', content: '', isProductList: false })
try {
const response = await getStream(userQuestion)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')
let buffer = ''
let accumulatedContent = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() || ''
// 在 sendMessage 中,解析 SSE 时增加对 event 的处理
let currentEvent = null;
for (let line of lines) {
line = line.trim();
if (line === '') continue;
if (line.startsWith('event:')) {
currentEvent = line.slice(6).trim();
continue;
}
let content = '';
if (line.startsWith('data:')) {
content = line.slice(5).trim();
if (content === '[DONE]') continue;
} else if (line.startsWith('message:')) {
content = line.slice(8).trim();
} else {
content = line;
}
if (content) {
if (currentEvent === 'sensitive_notification') {
// 收到通知敏感词事件,移除刚添加的空消息,展示产品列表
this.messages.pop();
await this.fetchAndShowProducts(userQuestion);
currentEvent = null;
// 结束流式处理,不再继续
return;
} else {
accumulatedContent += content;
this.messages[assistantMessageIndex].content = accumulatedContent;
this.scrollToBottom();
}
}
}
}
if (buffer.trim()) {
let content = buffer
if (buffer.startsWith('data:')) content = buffer.slice(5).trim()
else if (buffer.startsWith('message:')) content = buffer.slice(8).trim()
if (content && content !== '[DONE]') {
accumulatedContent += content
this.messages[assistantMessageIndex].content = accumulatedContent
this.scrollToBottom()
}
}
} catch (error) {
console.error('流式请求失败:', error)
// 处理通知类型敏感词错误码 50002
if (error.code === 50002) {
// 移除刚添加的空助手消息
this.messages.pop()
// 调用产品列表展示
await this.fetchAndShowProducts(userQuestion)
} else {
// 其他错误正常显示错误消息
const errorMsg = error.name === 'AbortError' ? '请求超时,请稍后重试' : `错误:${error.message}`
this.messages[assistantMessageIndex].content = `❌ ${errorMsg}`
this.scrollToBottom()
}
} finally {
this.isLoading = false
}
},
clearChat() {
this.messages = []
this.messages.push({
role: 'assistant',
content: '对话已清空,有什么可以帮你的吗?'
})
this.scrollToBottom()
}
}
}
// HTML转义函数,防止XSS
function escapeHtml(str) {
if (!str) return ''
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
</script>
<style scoped>
.ai-container {
display: flex;
flex-direction: column;
height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #f0f2f5 100%);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
.chat-header {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
padding: 0 24px;
position: sticky;
top: 0;
z-index: 10;
}
.header-content {
max-width: 900px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
height: 60px;
}
.logo-area {
display: flex;
align-items: center;
gap: 10px;
}
.logo-icon {
width: 28px;
height: 28px;
}
.logo-text {
font-size: 18px;
font-weight: 600;
background: linear-gradient(135deg, #6c5ce7, #a855f7);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.action-btn {
background: transparent;
border: none;
cursor: pointer;
padding: 8px;
border-radius: 8px;
color: #666;
transition: all 0.2s;
}
.action-btn:hover {
background: #f0f0f0;
color: #6c5ce7;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 24px;
max-width: 900px;
margin: 0 auto;
width: 100%;
}
.message {
display: flex;
gap: 12px;
margin-bottom: 24px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.message.user {
flex-direction: row-reverse;
}
.message-avatar {
flex-shrink: 0;
}
.user-avatar {
width: 36px;
height: 36px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 14px;
}
.ai-avatar {
width: 36px;
height: 36px;
background: linear-gradient(135deg, #6c5ce7, #a855f7);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.message-content {
max-width: 70%;
padding: 12px 16px;
border-radius: 18px;
line-height: 1.6;
font-size: 15px;
}
.message.user .message-content {
background: linear-gradient(135deg, #6c5ce7, #7c3aed);
color: white;
border-bottom-right-radius: 4px;
}
.message.assistant .message-content {
background: white;
color: #2d2d2d;
border-bottom-left-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
/* 产品列表样式 */
.message-content :deep(.product-list) {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 16px;
margin-top: 12px;
}
.message-content :deep(.product-card) {
background: #fff;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
border: 1px solid #f0f0f0;
}
.message-content :deep(.product-card:hover) {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(108, 92, 231, 0.15);
}
.message-content :deep(.product-image) {
width: 100%;
height: 160px;
overflow: hidden;
background: #f5f5f5;
}
.message-content :deep(.product-image img) {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s;
}
.message-content :deep(.product-card:hover .product-image img) {
transform: scale(1.05);
}
.message-content :deep(.product-info) {
padding: 12px;
}
.message-content :deep(.product-title) {
font-size: 15px;
font-weight: 600;
color: #1a1a2e;
margin-bottom: 8px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.message-content :deep(.product-price) {
font-size: 18px;
font-weight: 700;
color: #6c5ce7;
margin-bottom: 12px;
}
.message-content :deep(.product-buy-btn) {
width: 100%;
padding: 8px 0;
background: linear-gradient(135deg, #6c5ce7, #a855f7);
border: none;
border-radius: 24px;
color: white;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s;
}
.message-content :deep(.product-buy-btn:hover) {
opacity: 0.9;
}
.message-content :deep(.product-intro) {
font-size: 14px;
color: #6c757d;
margin-bottom: 12px;
padding: 8px 0;
border-bottom: 1px solid #e9ecef;
}
.message-content :deep(.product-list-empty) {
text-align: center;
padding: 32px;
color: #94a3b8;
font-size: 14px;
}
.chat-input-area {
background: white;
border-top: 1px solid rgba(0, 0, 0, 0.05);
padding: 16px 24px;
position: sticky;
bottom: 0;
}
.suggestions-area {
max-width: 900px;
margin: 0 auto 12px auto;
background: #f8f9fa;
border-radius: 16px;
padding: 12px 16px;
border: 1px solid #e9ecef;
}
.suggestions-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
font-size: 13px;
color: #6c757d;
}
.suggestions-title {
font-weight: 500;
}
.refresh-suggestions {
display: flex;
align-items: center;
gap: 4px;
background: transparent;
border: none;
color: #6c5ce7;
font-size: 12px;
cursor: pointer;
padding: 4px 8px;
border-radius: 20px;
transition: all 0.2s;
}
.refresh-suggestions:hover:not(:disabled) {
background: rgba(108, 92, 231, 0.1);
}
.refresh-suggestions:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.suggestions-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
min-height: 36px;
}
.suggestion-chip {
background: white;
border: 1px solid #e2e8f0;
border-radius: 24px;
padding: 6px 14px;
font-size: 13px;
color: #2d3748;
cursor: pointer;
transition: all 0.2s;
max-width: 260px;
white-space: normal;
word-break: break-word;
text-align: left;
line-height: 1.4;
}
.suggestion-chip:hover:not(:disabled) {
background: #6c5ce7;
color: white;
border-color: #6c5ce7;
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(108, 92, 231, 0.2);
}
.suggestion-chip:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.suggestions-loading {
display: flex;
gap: 6px;
padding: 6px 0;
}
.loading-dot {
width: 6px;
height: 6px;
background: #6c5ce7;
border-radius: 50%;
animation: pulse 1.4s infinite ease-in-out;
}
.loading-dot:nth-child(2) { animation-delay: 0.2s; }
.loading-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes pulse {
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1); }
}
.suggestions-empty {
font-size: 12px;
color: #94a3b8;
padding: 6px 0;
}
.input-wrapper {
max-width: 900px;
margin: 0 auto;
display: flex;
align-items: flex-end;
gap: 12px;
background: #f8f9fa;
border-radius: 24px;
padding: 8px 16px;
border: 1px solid #e9ecef;
transition: all 0.2s;
}
.input-wrapper:focus-within {
border-color: #6c5ce7;
box-shadow: 0 0 0 3px rgba(108, 92, 231, 0.1);
}
.input-wrapper textarea {
flex: 1;
border: none;
background: transparent;
padding: 8px 0;
font-size: 15px;
resize: none;
outline: none;
font-family: inherit;
max-height: 120px;
line-height: 1.5;
}
.send-btn {
background: #6c5ce7;
border: none;
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: white;
transition: all 0.2s;
flex-shrink: 0;
}
.send-btn:hover:not(:disabled) {
background: #5b4cc4;
transform: scale(1.02);
}
.send-btn:disabled {
background: #cbd5e0;
cursor: not-allowed;
}
.input-hint {
max-width: 900px;
margin: 8px auto 0;
font-size: 12px;
color: #94a3b8;
text-align: center;
}
.typing-indicator {
display: flex;
gap: 4px;
padding: 4px 0;
}
.typing-indicator span {
width: 8px;
height: 8px;
background: #6c5ce7;
border-radius: 50%;
animation: typing 1.4s infinite;
}
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing {
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
30% { transform: translateY(-8px); opacity: 1; }
}
.message-content :deep(h1) {
font-size: 1.5em;
font-weight: 600;
margin: 0.8em 0 0.4em;
color: #1a1a2e;
}
.message-content :deep(h2) {
font-size: 1.3em;
font-weight: 600;
margin: 0.7em 0 0.35em;
color: #1a1a2e;
}
.message-content :deep(h3) {
font-size: 1.1em;
font-weight: 600;
margin: 0.6em 0 0.3em;
color: #1a1a2e;
}
.message-content :deep(p) {
margin: 0.6em 0;
}
.message-content :deep(code) {
background-color: #f1f3f5;
padding: 0.2em 0.4em;
border-radius: 4px;
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.9em;
color: #e83e8c;
}
.message-content :deep(pre) {
background-color: #1e1e1e;
padding: 16px;
border-radius: 12px;
overflow-x: auto;
margin: 0.8em 0;
}
.message-content :deep(pre code) {
background-color: transparent;
padding: 0;
color: #d4d4d4;
font-size: 13px;
line-height: 1.5;
}
.message-content :deep(ul), .message-content :deep(ol) {
padding-left: 1.5em;
margin: 0.6em 0;
}
.message-content :deep(li) {
margin: 0.25em 0;
}
.message-content :deep(blockquote) {
border-left: 3px solid #6c5ce7;
padding-left: 1em;
margin: 0.8em 0;
color: #6c757d;
font-style: italic;
}
.message-content :deep(table) {
border-collapse: collapse;
width: 100%;
margin: 0.8em 0;
font-size: 14px;
}
.message-content :deep(th), .message-content :deep(td) {
border: 1px solid #dee2e6;
padding: 8px 12px;
text-align: left;
}
.message-content :deep(th) {
background-color: #f8f9fa;
font-weight: 600;
}
.message-content :deep(a) {
color: #6c5ce7;
text-decoration: none;
}
.message-content :deep(a:hover) {
text-decoration: underline;
}
.message-content :deep(hr) {
border: none;
border-top: 1px solid #e9ecef;
margin: 1em 0;
}
.message.user .message-content :deep(code) {
background-color: rgba(255, 255, 255, 0.2);
color: #ffe0b5;
}
.message.user .message-content :deep(pre) {
background-color: rgba(0, 0, 0, 0.3);
}
.message.user .message-content :deep(a) {
color: #ffe0b5;
}
.chat-messages::-webkit-scrollbar {
width: 6px;
}
.chat-messages::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.chat-messages::-webkit-scrollbar-thumb {
background: #cbd5e0;
border-radius: 3px;
}
.chat-messages::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
</style>
\ No newline at end of file
......@@ -83,8 +83,8 @@ const router = useRouter()
const { proxy } = getCurrentInstance()
const loginForm = ref({
username: 'admin',
password: '12345',
username: '',
password: '',
rememberMe: false,
code: '',
uuid: ''
......
......@@ -10,6 +10,7 @@
</el-col>
</el-row>
<el-table
height="70%"
:data="tableData"
:span-method="objectSpanMethod"
border
......@@ -52,7 +53,7 @@
:close-on-click-modal="false"
>
<!-- 表格数据 -->
<el-table v-loading="settingLoading" :data="settingList" border ref="settingTableRef">
<el-table v-loading="settingLoading" :data="settingList" border ref="settingTableRef" height="75%">
<el-table-column
label="序号 "
width="55"
......
......@@ -16,7 +16,13 @@
<el-row>
<el-col :span="12">
<div class="commonHeader">产品标题</div>
<el-select
<el-input
v-model="form['apiProductLaunchDto'].title"
placeholder="请输入"
maxlength="20"
show-word-limit
/>
<!-- <el-select
v-model="form['apiProductLaunchDto'].title"
filterable
remote
......@@ -35,7 +41,7 @@
:label="item.productName"
:value="item.productBizId"
/>
</el-select>
</el-select> -->
</el-col>
<el-col :span="24" :class="showNameTip ? '' : 'colBottomGap'">
<div class="nameTip">
......
......@@ -7,7 +7,7 @@
</div>
</el-col>
</el-row>
<el-table :data="tableData" :span-method="objectSpanMethod" border style="width: 100%; margin-top: 20px"
<el-table :data="tableData" :span-method="objectSpanMethod" border style="width: 100%; margin-top: 20px" height="70%"
row-key="rowKey">
<!-- 动态生成所有列 -->
<template v-for="column in tableColumns" :key="column.prop">
......
......@@ -42,15 +42,15 @@
label="短标题"
prop="shortTitle"
:show-overflow-tooltip="true"
width="150"
width="200"
/>
<el-table-column label="状态" prop="status" align="left">
<el-table-column label="状态" prop="status" align="left" width="100">
<template #default="scope">
<dict-tag :options="product_launch_status" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime" align="left">
<el-table-column label="创建时间" prop="createTime" align="left" sortable >
<template #default="scope">
{{ formatIsoToDateTime(scope.row.createTime) }}
</template>
......
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 to comment