统一暗色模式页面/顶栏/Handsontable 选中色,调整下单到仓方式文案与提示,CustomLeftMenu 卡片统一 60px;补充揽收单、问题件处理与 H5 相关页面。 Co-authored-by: Cursor <cursoragent@cursor.com>
155 lines
3.7 KiB
Vue
155 lines
3.7 KiB
Vue
<template>
|
||
<div class="xhs-page-panel ai-chat-panel">
|
||
<div class="table-toolbar">
|
||
<div class="toolbar-left">
|
||
<div>
|
||
<p class="xhs-panel-title">AI 助手 · 对话测试</p>
|
||
<p class="xhs-panel-desc">选择已配置密钥的提供商,发送消息验证连通性(Enter 发送)。</p>
|
||
</div>
|
||
</div>
|
||
<div class="toolbar-right">
|
||
<a-select
|
||
v-model="providerKeyProxy"
|
||
placeholder="选择 AI 提供商"
|
||
style="width: 220px"
|
||
allow-clear
|
||
>
|
||
<a-option
|
||
v-for="p in enabledProviders"
|
||
:key="p.providerKey"
|
||
:value="p.providerKey"
|
||
:label="p.displayName || p.providerKey"
|
||
/>
|
||
</a-select>
|
||
<a-button :disabled="loading" @click="emit('clear')">清空</a-button>
|
||
<a-button type="primary" :loading="loading" :disabled="!canSend" @click="emit('send')">
|
||
发送
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel-body">
|
||
<div ref="chatBoxRef" class="chat-box">
|
||
<div v-for="(msg, idx) in messages" :key="idx" :class="['chat-msg', msg.role]">
|
||
<span class="role">{{ msg.role === 'user' ? '我' : 'AI' }}</span>
|
||
<pre class="content">{{ msg.content }}</pre>
|
||
</div>
|
||
<div v-if="!messages.length" class="empty-chat">选择提供商后开始对话</div>
|
||
</div>
|
||
<a-textarea
|
||
:model-value="inputText"
|
||
placeholder="输入问题,Enter 发送"
|
||
:auto-size="{ minRows: 3, maxRows: 8 }"
|
||
@update:model-value="(v: string) => emit('update:inputText', v)"
|
||
@keydown.enter.exact.prevent="emit('send')"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, nextTick, ref, watch } from 'vue';
|
||
import type { AiProviderItem } from './providerCatalog';
|
||
|
||
const props = defineProps<{
|
||
providers: AiProviderItem[];
|
||
providerKey: string;
|
||
inputText: string;
|
||
messages: Array<{ role: string; content: string }>;
|
||
loading?: boolean;
|
||
}>();
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update:providerKey', v: string): void;
|
||
(e: 'update:inputText', v: string): void;
|
||
(e: 'send'): void;
|
||
(e: 'clear'): void;
|
||
}>();
|
||
|
||
const chatBoxRef = ref<HTMLElement>();
|
||
|
||
const enabledProviders = computed(() =>
|
||
props.providers.filter((p) => p.enabled && Boolean(p.apiKey)),
|
||
);
|
||
|
||
const providerKeyProxy = computed({
|
||
get: () => props.providerKey,
|
||
set: (v: string) => emit('update:providerKey', v),
|
||
});
|
||
|
||
const canSend = computed(
|
||
() => Boolean(props.providerKey && props.inputText.trim() && !props.loading),
|
||
);
|
||
|
||
watch(
|
||
() => props.messages.length,
|
||
async () => {
|
||
await nextTick();
|
||
if (chatBoxRef.value) {
|
||
chatBoxRef.value.scrollTop = chatBoxRef.value.scrollHeight;
|
||
}
|
||
},
|
||
);
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
@import '@/views/xhs/_shared/xhs-page.less';
|
||
|
||
.ai-chat-panel {
|
||
padding: 16px;
|
||
}
|
||
|
||
.toolbar-left {
|
||
flex-direction: column;
|
||
align-items: flex-start !important;
|
||
gap: 0 !important;
|
||
}
|
||
|
||
.panel-body {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.chat-box {
|
||
flex: 1;
|
||
min-height: 280px;
|
||
overflow-y: auto;
|
||
padding: 12px;
|
||
background: var(--app-card-bg);
|
||
border-radius: 8px;
|
||
border: 1px solid var(--color-border-2);
|
||
}
|
||
|
||
.chat-msg {
|
||
margin-bottom: 12px;
|
||
|
||
.role {
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
}
|
||
|
||
.content {
|
||
margin: 4px 0 0;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
font-family: inherit;
|
||
font-size: 14px;
|
||
color: var(--color-text-2);
|
||
}
|
||
|
||
&.user .content {
|
||
color: var(--color-text-1);
|
||
}
|
||
}
|
||
|
||
.empty-chat {
|
||
padding: 40px 0;
|
||
text-align: center;
|
||
font-size: 13px;
|
||
color: var(--color-text-3);
|
||
}
|
||
</style>
|