Add Vue user H5 client

This commit is contained in:
2026-07-06 17:34:37 +08:00
parent 17cba7cf61
commit e9f3db9ab2
20 changed files with 2433 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
<script setup lang="ts">
import { ref } from "vue";
defineProps<{
loading: boolean;
disabled: boolean;
}>();
const emit = defineEmits<{
send: [message: string];
stop: [];
}>();
const input = ref("");
function send() {
const text = input.value.trim();
if (!text) return;
emit("send", text);
input.value = "";
}
function onKeydown(event: KeyboardEvent) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
send();
}
}
</script>
<template>
<form class="composer" @submit.prevent="send">
<textarea
v-model="input"
rows="1"
placeholder="请输入你的问题"
:disabled="disabled"
@keydown="onKeydown"
/>
<button v-if="loading" type="button" class="stop-btn" @click="emit('stop')">停止</button>
<button v-else type="submit" class="send-btn" :disabled="disabled || !input.trim()">发送</button>
</form>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
defineProps<{
role: "user" | "assistant";
content: string;
streaming?: boolean;
}>();
</script>
<template>
<article class="message" :class="role">
<div class="avatar">{{ role === "assistant" ? "AI" : "我" }}</div>
<div class="bubble">
<div class="content">{{ content }}</div>
<span v-if="streaming" class="typing">正在生成</span>
</div>
</article>
</template>

View File

@@ -0,0 +1,69 @@
<script setup lang="ts">
import { ref } from "vue";
import { api, saveToken } from "../services/api";
import type { UserProfile } from "../types/api";
const emit = defineEmits<{
loggedIn: [user: UserProfile];
}>();
const phone = ref("");
const code = ref("123456");
const loading = ref(false);
const message = ref("开发阶段验证码默认 123456");
async function sendCode() {
loading.value = true;
message.value = "";
try {
await api.sendSms(phone.value);
message.value = "验证码已发送,开发阶段默认 123456";
} catch (error) {
message.value = error instanceof Error ? error.message : "发送失败";
} finally {
loading.value = false;
}
}
async function login() {
loading.value = true;
message.value = "";
try {
const result = await api.login(phone.value, code.value);
saveToken(result.token);
emit("loggedIn", result.user);
} catch (error) {
message.value = error instanceof Error ? error.message : "登录失败";
} finally {
loading.value = false;
}
}
</script>
<template>
<section class="login-panel">
<div class="brand-mark">AI</div>
<h1>AI知识库助手</h1>
<p>企业知识统一问答入口</p>
<label>
手机号
<input v-model="phone" inputmode="numeric" maxlength="11" placeholder="请输入手机号" />
</label>
<label>
验证码
<div class="code-row">
<input v-model="code" inputmode="numeric" maxlength="8" placeholder="验证码" />
<button type="button" class="secondary" :disabled="loading || phone.length < 11" @click="sendCode">
发送
</button>
</div>
</label>
<button type="button" class="primary" :disabled="loading || phone.length < 11 || code.length < 4" @click="login">
{{ loading ? "处理中..." : "立即进入" }}
</button>
<div class="form-tip">{{ message }}</div>
</section>
</template>

View File

@@ -0,0 +1,40 @@
<script setup lang="ts">
import type { ChatSession } from "../types/api";
defineProps<{
open: boolean;
sessions: ChatSession[];
activeSessionId: number | null;
}>();
const emit = defineEmits<{
close: [];
select: [sessionId: number];
create: [];
delete: [sessionId: number];
}>();
</script>
<template>
<div class="drawer-mask" :class="{ show: open }" @click="emit('close')"></div>
<aside class="session-drawer" :class="{ open }">
<div class="drawer-head">
<strong>历史会话</strong>
<button type="button" class="icon-btn" @click="emit('close')">×</button>
</div>
<button type="button" class="new-session-btn" @click="emit('create')"> 新聊天</button>
<div class="session-list">
<button
v-for="session in sessions"
:key="session.id"
type="button"
class="session-item"
:class="{ active: session.id === activeSessionId }"
@click="emit('select', session.id)"
>
<span>{{ session.title }}</span>
<small>{{ session.messageCount }} 条消息</small>
</button>
</div>
</aside>
</template>