文本 Arduino C Python C++ Java C# Bash Go kotlin JavaScript HTML/XML CSS SQL <script setup lang="ts">
import {computed, nextTick, reactive, ref, watch} from "vue";
import avatar from "@/assets/girlfriend.jpg"
import {type ChatHistoryVO, type ChatMsgForm, ChatRoleEnum} from "@/types/chat.ts";
import {getPlayingGameInfo, startGame} from "@/api/game.ts";
import {type GameInfo, GameStatusEnum} from "@/types/game.ts";
import {findChatHistory, sendMessage} from "@/api/chat.ts";
import Dialog from "@/components/Dialog.vue";
/**
* 好感度文本展示
*/
const affectionText = computed(() => {
const value = gameInfo.value.affection
if (value === 100) return "满分心动"
if (value >= 90) return "甜蜜暴击"
if (value >= 80) return "非常开心"
if (value >= 70) return "心情不错"
if (value >= 60) return "还算满意"
if (value >= 50) return "情绪平稳"
if (value >= 40) return "有点生气"
if (value >= 30) return "明显不满"
if (value >= 20) return "非常生气"
if (value >= 10) return "气到不想说话"
return "我们分手吧"
})
// 游戏信息
const gameInfo = ref<GameInfo>({
conversationId: undefined,
affection: 0,
event: "",
name: "",
status: undefined,
thought: ""
})
const dialog = reactive({
// 是否显示弹窗
visible: false,
// 标题
title: "",
// 描述
description: "",
});
// 聊天记录
const chatHistory = ref<ChatHistoryVO[]>([]);
// 是否开始游戏
const gameStatus = ref<"idle" | "loading" | "playing">("idle")
/**
* 开始游戏
*/
const start = () => {
gameStatus.value = "loading"
startGame().then(response => {
if (response.code == 200) {
gameInfo.value = response.data
chatHistory.value.push({content: response.data.openingMessage, role: "assistant"})
gameStatus.value = "playing"
}
})
}
/**
* 重启游戏
*/
const resetStart = () => {
// 清空历史记录
chatHistory.value = []
// 关闭弹窗
dialog.visible = false
// 重现开始游戏
start();
}
/**
* 获取游戏信息
*/
const getGameInfo = () => {
getPlayingGameInfo().then(response => {
if (response.data) {
gameInfo.value = response.data
// 查询聊天记录
findChatHistory(gameInfo.value.conversationId!).then(response => {
console.log(response.data)
chatHistory.value = response.data
})
gameStatus.value = "playing"
} else {
gameStatus.value = "idle"
}
})
}
// 输入的内容
const content = ref<string>("");
// 发送中状态
const sending = ref<boolean>(false);
/**
* 聊天消息
*/
const send = async () => {
sending.value = true;
// 保存用户消息
const userMessage = content.value;
// 添加用户自己的消息
chatHistory.value.push({
role: ChatRoleEnum.USER,
content: userMessage,
});
// 添加 AI 消息
const history = reactive<ChatHistoryVO>({
role: ChatRoleEnum.ASSISTANT,
content: '',
});
chatHistory.value.push(history);
// 创建传输实体
const chatMsgForm: ChatMsgForm = {
id: gameInfo.value.conversationId!,
content: userMessage,
affection: gameInfo.value.affection,
};
// 清空输入框
content.value = "";
sendMessage(chatMsgForm).then(response => {
// 设置女友回复的内容
history.content = response.data.message
// 设置好感度变化
history.affectionChange = response.data.affectionChange;
// 修改好感度
gameInfo.value.affection += response.data.affectionChange;
// 修改想法
gameInfo.value.thought = response.data.thought;
checkGameOver();
sending.value = false;
})
};
/**
* 校验游戏是否结束
*/
const checkGameOver = () => {
if (gameInfo.value.affection >= 100) {
// 成功
gameInfo.value.status = GameStatusEnum.SUCCESS;
dialog.visible = true
dialog.title = "她开心了\n"
dialog.description=`好感度达到 100,她已经完全原谅你了 ❤️.`
}
if (gameInfo.value.affection <= 0) {
// 失败
gameInfo.value.status = GameStatusEnum.FAILED;
dialog.visible = true
dialog.title="哄人失败"
dialog.description=`好感度归零,${gameInfo.value.name}已经彻底心寒了.`
}
};
const chatContainerRef = ref<HTMLElement | null>(null)
// 监听自动跳转到底部
watch(() => chatHistory.value,
async () => {
// 等待 Vue 把新蹦出来的字渲染到 DOM 树中
await nextTick()
chatContainerRef.value!.scrollTo({top: chatContainerRef.value!.scrollHeight, behavior: "auto"});
},
{deep: true}
)
getGameInfo()
</script>
<template>
<div class="coax__box">
<!-- 导航栏部分 -->
<nav class="coax__nav">
<div class="coax__nav__title">
<span class="coax__nav__name">哄哄她</span>
<span class="coax__nav__desc">恋爱求生模拟器</span>
</div>
<div class="coax__nav__slogan">
哄人是一门艺术,也是一种爱。
</div>
</nav>
<div class="coax__content" v-if="gameStatus=='playing'">
<aside class="coax__aside">
<!-- 女友信息 -->
<div class="coax__girlfriend">
<img class="coax__girlfriend__avatar" :src="avatar" alt="女友头像">
<div class="coax__girlfriend__name">{{ gameInfo.name }}</div>
<div class="coax__girlfriend__mood">{{ affectionText }}</div>
<div class="coax__girlfriend__affection">
<div class="girlfriend__affection__info">
<span class="girlfriend__affection__label">好感度</span>
<span class="girlfriend__affection__value">{{ gameInfo.affection }}</span>
</div>
<div class="girlfriend__affection__progress">
<div class="girlfriend__affection__progress-inner"
:style="`width:${gameInfo.affection}%`"/>
</div>
</div>
</div>
<div class="coax__emotion">
<div class="emotion__item">
<span class="emotion__item-title">
本次事件
</span>
<div class="emotion__event__content">
{{ gameInfo.event }}
</div>
</div>
<div class="emotion__item">
<span class="emotion__item-title">
她在想什么
</span>
<div class="emotion__thought__content">
{{ gameInfo.thought }}
</div>
</div>
</div>
</aside>
<main class="coax__chat">
<div class="coax__chat__header">
<div class="chat__header__user">
<img :src="avatar" class="chat__header__avatar" alt="头像">
<div class="chat__header__info">
<span class="chat__header__nickname">{{ gameInfo.name }}</span>
<span class="chat__header__mood">{{ affectionText }}</span>
</div>
<div class="chat__header__affection">
<span class="chat__header__affection__label">好感度</span>
<span class="chat__header__affection__value">{{ gameInfo.affection }}</span>
</div>
</div>
</div>
<div class="chat__content" ref="chatContainerRef">
<div v-for="(item,index) in chatHistory" :key="index" :class="`chat__content__group
${item.role==ChatRoleEnum.USER?'chat__content--user':'chat__content--assistant'}`">
<img :src="avatar" class="chat__content__avatar" alt="头像">
<div class="chat__content__body">
<div class="chat__content__text">
{{ item.content }}
</div>
<div v-if="item.affectionChange" class="chat__body__affection-tag">
{{ item.affectionChange > 0 ? '+' + item.affectionChange : item.affectionChange }}
</div>
</div>
</div>
</div>
<div class="chat__footer chat__footer--playing" v-if="gameInfo.status== GameStatusEnum.PLAYING">
<span class="chat__footer__tip">不同的话会影响她的情绪,认真回答</span>
<div class="chat__input__wrapper">
<input v-model="content" class="chat__input-inner" placeholder="想一句话来哄哄她吧......" maxlength="100"
required/>
<button :class="['chat__send-btn','coax__button',{'chat__send-btn--disable':sending||!content}]"
@click="send">发送
</button>
</div>
</div>
<div class="chat__footer chat__footer--over" v-else-if="gameInfo.status== GameStatusEnum.SUCCESS">
<p class="chat__footer__desc">她已经完全原谅你了 ❤️</p>
<button class="chat__send-btn coax__button" @click="resetStart">再玩一局
</button>
</div>
<div class="chat__footer chat__footer--over" v-else-if="gameInfo.status== GameStatusEnum.FAILED">
<p class="chat__footer__desc">好感度归零,她已经心寒了。️</p>
<button class="chat__send-btn coax__button" @click="resetStart">再玩一局
</button>
</div>
</main>
</div>
<div v-else class="coax__ready">
<img class="coax__ready__avatar" :src="avatar" alt="哄哄她"/>
<span class="coax__ready__title">哄哄她</span>
<div class="coax__ready__info">
<span class="coax__ready__desc">每局剧情随机生成,好感度从 40 开始</span>
<span class="coax__ready__rules">降到 0 她就不想理你了,升到 100 她才原谅你</span>
</div>
<button class="coax__button coax__ready__button" v-if="gameStatus=='idle'" @click="start">
开始游戏
</button>
<div v-if="gameStatus=='loading'" class="coax__loading">
<div class="coax__loading__dots">
<span class="coax__loading__dot dot-1"></span>
<span class="coax__loading__dot dot-2"></span>
<span class="coax__loading__dot dot-3"></span>
</div>
<div class="coax__loading__text">
剧情生成中…
</div>
</div>
</div>
<Dialog v-model="dialog.visible" title="哄她失败">
<p class="coax__dialog__desc">好感度归零,{{ gameInfo.name }}已经彻底心寒了。</p>
<template #footer>
<button class="dialog__game-btn" @click="resetStart">
再玩一局
</button>
<button class="dialog__view-btn" @click="dialog.visible = false">
查看对话
</button>
</template>
</Dialog>
</div>
</template>
<style lang="scss" scoped>
// 页面整体部分
.coax__box {
height: 100vh;
background-color: #fff7f9;
display: flex;
flex-direction: column;
}
// 头部导航部分
.coax__nav {
background-color: white;
border-bottom: 1px solid #eee;
padding: 12px 32px;
display: flex;
align-items: center;
justify-content: space-between;
}
// 导航栏标题部分
.coax__nav__title {
display: flex;
align-items: center;
gap: 15px;
}
// 导航栏的标题名称
.coax__nav__name {
font-family: 'Ma Shan Zheng', cursive;
font-size: 18px;
color: #1a1a1a;
}
// 导航栏的标题描述
.coax__nav__desc {
font-size: 12px;
color: #c8a8b2;
}
// 导航栏的slogan
.coax__nav__slogan {
font-size: 12px;
color: #c8a8b2;
}
// 内容部分
.coax__content {
display: flex;
gap: 12px;
padding: 20px 26px;
min-height: 0;
flex: 1;
}
// 侧边栏部分
.coax__aside {
width: 240px;
display: flex;
flex-direction: column;
gap: 12px;
}
// 女友信息部分
.coax__girlfriend {
border: 1px solid #fce7ef;
background-color: white;
border-radius: 15px;
padding: 24px 20px 20px;
display: flex;
gap: 5px;
flex-direction: column;
align-items: center;
justify-content: center;
}
// 女友头像
.coax__girlfriend__avatar {
width: 85px;
object-fit: cover;
border-radius: 50%;
border: 1px solid #fce7ef;
}
// 女友名字
.coax__girlfriend__name {
font-family: 'Ma Shan Zheng', cursive;
font-size: 22px;
font-weight: 500;
}
// 女友情绪
.coax__girlfriend__mood {
font-size: 12px;
color: #c8a8b2;
}
// 女友好感度
.coax__girlfriend__affection {
margin-top: 10px;
width: 100%;
display: flex;
flex-direction: column;
gap: 5px;
}
// 好感度信息文本
.girlfriend__affection__info {
display: flex;
align-items: center;
justify-content: space-between;
}
// 好感度label
.girlfriend__affection__label {
color: #c8a8b2;
font-size: 12px;
}
// 好感度指数
.girlfriend__affection__value {
color: #e8708a;
font-size: 12px;
font-weight: 600;
}
// 好感度的进度条
.girlfriend__affection__progress {
border-radius: 25px;
background-color: #fce7ef;
height: 6px;
overflow: hidden;
}
// 好感度内层条样式
.girlfriend__affection__progress-inner {
background-color: #e8708a;
height: 100%;
transition: width .8s cubic-bezier(.25, .8, .25, 1);
}
// 情感样式
.coax__emotion {
flex: 1;
border: 1px solid #fce7ef;
background-color: white;
border-radius: 15px;
padding: 24px 20px 20px;
display: flex;
gap: 15px;
flex-direction: column;
align-items: center;
}
// 情感分析的每个项
.emotion__item {
width: 100%;
display: flex;
flex-direction: column;
gap: 5px;
}
// 情感信息每个项的标题
.emotion__item-title {
font-size: 12px;
font-weight: 500;
color: #c8a8b2;
}
// 情感事件内容
.emotion__event__content {
color: #333333;
font-size: 12px;
font-weight: 500;
}
// 情感想法内容
.emotion__thought__content {
color: #9b7b85;
font-size: 12px;
}
// 对话部分
.coax__chat {
flex: 1;
min-width: 0;
overflow: hidden;
background-color: white;
border: 1px solid #fce7ef;
border-radius: 20px;
display: flex;
flex-direction: column;
}
// 对话部分的头部
.coax__chat__header {
padding: 14px 24px;
border-bottom: 1px solid #fef0f5;
}
// 聊天头部的用户信息
.chat__header__user {
display: flex;
align-items: center;
gap: 12px;
}
// 聊天头部的用户头像
.chat__header__avatar {
width: 38px;
object-fit: cover;
border-radius: 50%;
border: 1px solid #fce7ef;
}
// 聊天头部的用户信息
.chat__header__info {
display: flex;
flex-direction: column;
gap: 4px;
}
// 聊天头部用户昵称
.chat__header__nickname {
color: #1a1a1a;
font-weight: 500;
font-size: 14px;
}
// 聊天头部用户情绪
.chat__header__mood {
font-size: 12px;
color: #c8a8b2;
}
// 聊天头部好感度部分
.chat__header__affection {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
// 聊天头部好感度label
.chat__header__affection__label {
color: #c8a8b2;
font-size: 12px;
}
// 聊天头部好感度度值
.chat__header__affection__value {
background: #fce7ef;
color: #e8708a;
font-weight: 600;
font-size: 12px;
border-radius: 25px;
padding: 2px 10px;
}
// 聊天内容部分
.chat__content {
overflow: auto;
min-height: 0;
flex: 1;
background: #fff7f9;
padding: 20px 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
// 聊天的具体内容部分
.chat__content__group {
position: relative;
align-items: first baseline;
width: 100%;
display: flex;
gap: 10px;
}
// 聊天的用户头像
.chat__content__avatar {
vertical-align: middle;
width: 34px;
height: 34px;
object-fit: cover;
border-radius: 50%;
border: 1px solid #fce7ef;
}
// 聊天的内容体
.chat__content__body {
position: relative;
max-width: 70%;
padding: 10px 16px;
box-shadow: rgba(232, 112, 138, 0.06) 0 1px 4px;
}
// 好感度tag
.chat__body__affection-tag {
position: absolute;
bottom: 0;
right: 0;
transform: translateY(30%) translateX(30%);
background-color: #fff1f1;
color: #f87171;
border: 1px solid #fee2e2;
font-size: 10px;
font-weight: 500;
padding: 2px 5px;
border-radius: 25px;
}
// ai的内容
.chat__content--assistant {
justify-content: flex-start;
// 聊天的内容体
.chat__content__body {
background-color: white;
border: 1px solid #fce7ef;
border-radius: 5px 15px 15px;
}
// 聊天的文字
.chat__content__text {
color: #1a1a1a;
// 为空显示
&:empty::after {
content: "女友正在思考...";
display: inline-block;
color: #c8a8b2;
font-style: italic;
font-size: 14px;
}
}
}
// 用户的聊天内容
.chat__content--user {
justify-content: flex-end;
.chat__content__avatar {
display: none;
}
// 聊天的内容体
.chat__content__body {
color: white;
background-color: #e8708a;
border-radius: 15px;
}
}
// 聊天的底部部分
.chat__footer {
padding: 16px 24px;
border-top: 1px solid #fef0f5;
background-color: white;
gap: 10px;
}
// 游戏中的底部样式
.chat__footer--playing{
display: flex;
flex-direction: column;
}
// 结束的底部样式
.chat__footer--over{
display: flex;
align-items: center;
justify-content: space-between;
}
// 聊天底部小提示
.chat__footer__tip {
color: #c8a8b2;
font-size: 12px;
}
// n内容部分
.chat__input__wrapper {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
// 描述部分
.chat__footer__desc {
color: #c8a8b2;
font-size: 12px;
}
// 输入框
.chat__input-inner {
flex: 1;
background-color: #fff7f9;
border: 1px solid #fce7ef;
padding: 10px 16px;
border-radius: 12px;
outline: none;
font-size: 14px;
color: #1a1a1a;
}
// 按钮样式
.coax__button {
background-color: #e8708a;
color: white;
font-weight: 500;
font-size: 14px;
border-radius: 12px;
border: none;
&:hover {
opacity: 0.85;
}
}
// 发送按钮
.chat__send-btn {
padding: 0 20px;
height: 38px;
}
// 发送按钮禁用样式
.chat__send-btn--disable {
opacity: 0.75;
cursor: not-allowed;
pointer-events: none;
}
// 准备游戏的样式
.coax__ready {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
}
// 准备游戏头像
.coax__ready__avatar {
width: 88px;
aspect-ratio: 1/1;
object-fit: cover;
border-radius: 50%;
border: 1px solid #fce7ef;
}
// 准备游戏的标题
.coax__ready__title {
font-family: 'Ma Shan Zheng', cursive;
color: #1a1a1a;
font-weight: 600;
font-size: 30px;
}
// 准备信息
.coax__ready__info {
display: flex;
flex-direction: column;
text-align: center;
gap: 5px;
}
// 准备游戏的描述
.coax__ready__desc {
color: #9b7b85;
font-size: 14px;
}
// 准备游戏的规则
.coax__ready__rules {
color: #c8a8b2;
font-size: 14px;
}
// 开始游戏按钮
.coax__ready__button {
padding: 10px 36px;
}
// 加载样式部分
.coax__loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
}
// 加载动画容器
.coax__loading__dots {
display: flex;
align-items: center;
gap: 6px;
}
// 加载圆点
.coax__loading__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgb(249, 168, 201);
animation: dot-bounce 1.2s infinite ease-in-out;
}
.dot-2 {
animation-delay: 0.15s;
}
.dot-3 {
animation-delay: 0.3s;
}
@keyframes dot-bounce {
0%,
60%,
100% {
transform: translateY(0);
}
30% {
transform: translateY(-4px);
}
}
// 加载的文本样式
.coax__loading__text {
font-size: 12px;
color: #c8a8b2;
}
// 结果描述
.coax__dialog__desc {
font-size: 14px;
color: #9b7b85;
text-align: center;
}
// 弹窗的游戏按钮
.dialog__game-btn {
width: 100%;
padding: 10px 5px;
background: #e8708a;
color: white;
font-weight: 500;
font-size: 14px;
border-radius: 12px;
outline: none;
border: none;
}
// 弹窗的查看按钮
.dialog__view-btn {
width: 100%;
padding: 10px 5px;
font-size: 14px;
border-radius: 12px;
outline: none;
border: none;
font-weight: 500;
background: #fce7ef;
color: #e8708a;
}
</style>