326 lines
6.3 KiB
Vue
326 lines
6.3 KiB
Vue
<script setup lang="ts">
|
|
import {
|
|
ArrowUp,
|
|
Bot,
|
|
Box,
|
|
Building2,
|
|
Camera,
|
|
ClipboardCheck,
|
|
FileSearch,
|
|
Mic,
|
|
Search,
|
|
} from "@lucide/vue";
|
|
|
|
import { featureFlags } from "@/config/features";
|
|
|
|
defineProps<{ modelValue: string; loading: boolean; canUpload: boolean }>();
|
|
|
|
const emit = defineEmits<{
|
|
"update:modelValue": [value: string];
|
|
submit: [];
|
|
select: [value: string];
|
|
tool: [
|
|
value:
|
|
| "assessment"
|
|
| "supplier"
|
|
| "material"
|
|
| "cases"
|
|
| "solution"
|
|
| "review"
|
|
| "ocr"
|
|
| "voice",
|
|
];
|
|
}>();
|
|
|
|
const functions: Array<{
|
|
title: string;
|
|
description: string;
|
|
icon: typeof Search;
|
|
tone: string;
|
|
prompt?: string;
|
|
tool:
|
|
"assessment" | "supplier" | "material" | "cases" | "solution" | "review";
|
|
}> = [
|
|
{
|
|
title: "评估新品",
|
|
description: "含品类+频次",
|
|
icon: Search,
|
|
tone: "blue",
|
|
prompt: "帮我评估一款新品的综合风险",
|
|
tool: "assessment",
|
|
},
|
|
{
|
|
title: "查供应商",
|
|
description: "深度画像+分层",
|
|
icon: Building2,
|
|
tone: "green",
|
|
prompt: "帮我查询供应商风险画像",
|
|
tool: "supplier",
|
|
},
|
|
{
|
|
title: "查物料",
|
|
description: "品类常见问题",
|
|
icon: Box,
|
|
tone: "amber",
|
|
prompt: "帮我查询物料常见风险",
|
|
tool: "material",
|
|
},
|
|
{
|
|
title: "搜案例",
|
|
description: "语义检索",
|
|
icon: FileSearch,
|
|
tone: "red",
|
|
prompt: "搜索相似质量异常案例",
|
|
tool: "cases",
|
|
},
|
|
{
|
|
title: "AI方案",
|
|
description: "推理生成",
|
|
icon: Bot,
|
|
tone: "purple",
|
|
prompt: "帮我生成一个新品风险预防方案",
|
|
tool: "solution",
|
|
},
|
|
{
|
|
title: "审核队列",
|
|
description: "查看待审核风险项",
|
|
icon: ClipboardCheck,
|
|
tone: "red",
|
|
tool: "review",
|
|
},
|
|
];
|
|
</script>
|
|
|
|
<template>
|
|
<div class="composer-wrap">
|
|
<form class="composer" @submit.prevent="emit('submit')">
|
|
<button
|
|
v-if="featureFlags.voiceEntry"
|
|
class="composer-icon"
|
|
type="button"
|
|
title="语音录入"
|
|
aria-label="语音录入"
|
|
@click="emit('tool', 'voice')"
|
|
>
|
|
<Mic :size="16" />
|
|
</button>
|
|
<button
|
|
v-if="canUpload"
|
|
class="composer-icon"
|
|
type="button"
|
|
title="拍照识别"
|
|
aria-label="拍照识别"
|
|
@click="emit('tool', 'ocr')"
|
|
>
|
|
<Camera :size="16" />
|
|
</button>
|
|
<textarea
|
|
:value="modelValue"
|
|
rows="1"
|
|
placeholder="输入产品参数或问题…"
|
|
aria-label="输入产品参数或问题"
|
|
maxlength="2000"
|
|
@input="
|
|
emit(
|
|
'update:modelValue',
|
|
($event.target as HTMLTextAreaElement).value,
|
|
)
|
|
"
|
|
@keydown.enter.exact.prevent="emit('submit')"
|
|
></textarea>
|
|
<button
|
|
class="send-button"
|
|
type="submit"
|
|
:disabled="loading || !modelValue.trim()"
|
|
title="发送"
|
|
aria-label="发送"
|
|
>
|
|
<ArrowUp :size="17" />
|
|
</button>
|
|
</form>
|
|
|
|
<div class="function-grid">
|
|
<button
|
|
v-for="item in functions"
|
|
:key="item.title"
|
|
class="function-card"
|
|
type="button"
|
|
@click="emit('tool', item.tool)"
|
|
>
|
|
<span class="function-dot" :class="`function-dot--${item.tone}`"></span>
|
|
<component :is="item.icon" :size="13" />
|
|
<span
|
|
><strong>{{ item.title }}</strong
|
|
><small>{{ item.description }}</small></span
|
|
>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.composer-wrap {
|
|
padding: 12px 24px 14px;
|
|
background: linear-gradient(
|
|
to bottom,
|
|
rgb(250 250 250 / 88%),
|
|
var(--color-bg) 18%
|
|
);
|
|
}
|
|
|
|
.composer {
|
|
display: flex;
|
|
max-width: var(--content-width);
|
|
min-height: 52px;
|
|
align-items: flex-end;
|
|
gap: 7px;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 20px;
|
|
margin: 0 auto;
|
|
padding: 8px 10px;
|
|
background: var(--color-surface);
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.composer:focus-within {
|
|
border-color: var(--color-border-strong);
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
.composer textarea {
|
|
min-width: 0;
|
|
min-height: 32px;
|
|
max-height: 92px;
|
|
flex: 1;
|
|
resize: none;
|
|
border: 0;
|
|
padding: 6px 3px;
|
|
background: transparent;
|
|
color: var(--color-text);
|
|
font: inherit;
|
|
line-height: 1.45;
|
|
outline: 0;
|
|
}
|
|
|
|
.composer textarea::placeholder {
|
|
color: var(--color-text-faint);
|
|
}
|
|
|
|
.composer-icon,
|
|
.send-button {
|
|
display: grid;
|
|
width: 34px;
|
|
height: 34px;
|
|
flex: 0 0 34px;
|
|
place-items: center;
|
|
border: 0;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.composer-icon {
|
|
background: var(--color-bg-warm);
|
|
color: var(--color-text-muted);
|
|
}
|
|
|
|
.composer-icon:hover {
|
|
background: var(--color-border);
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.send-button {
|
|
background: var(--color-text);
|
|
color: white;
|
|
}
|
|
|
|
.send-button:disabled {
|
|
opacity: 0.35;
|
|
}
|
|
|
|
.function-grid {
|
|
display: grid;
|
|
max-width: var(--content-width);
|
|
margin: 8px auto 0;
|
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
gap: 6px;
|
|
}
|
|
|
|
.function-card {
|
|
display: flex;
|
|
min-width: 0;
|
|
min-height: 44px;
|
|
align-items: center;
|
|
gap: 6px;
|
|
overflow: hidden;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 9px;
|
|
padding: 7px 8px;
|
|
background: var(--color-surface);
|
|
color: var(--color-text-secondary);
|
|
text-align: left;
|
|
}
|
|
|
|
.function-card:hover {
|
|
border-color: var(--color-border-strong);
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.function-card > span:last-child {
|
|
display: grid;
|
|
min-width: 0;
|
|
}
|
|
|
|
.function-card strong,
|
|
.function-card small {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.function-card strong {
|
|
color: var(--color-text);
|
|
font-size: 10px;
|
|
}
|
|
|
|
.function-card small {
|
|
color: var(--color-text-faint);
|
|
font-size: 8px;
|
|
}
|
|
|
|
.function-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
flex: 0 0 6px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.function-dot--blue {
|
|
background: var(--color-accent);
|
|
}
|
|
.function-dot--green {
|
|
background: var(--color-success);
|
|
}
|
|
.function-dot--amber {
|
|
background: var(--color-warning);
|
|
}
|
|
.function-dot--red {
|
|
background: var(--color-danger);
|
|
}
|
|
.function-dot--purple {
|
|
background: var(--color-purple);
|
|
}
|
|
|
|
@media (max-width: 760px) {
|
|
.composer-wrap {
|
|
padding-inline: 10px;
|
|
}
|
|
|
|
.function-grid {
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
}
|
|
|
|
.composer-icon:nth-child(2) {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|