阿里云官网有很多有优秀的产品,我学校在做课设时用到过OSS对象存储、阿里云服务器、模型服务、短信服务、DNS等
这里主要介绍一下我使用的模型服务过程。
最终效果

实现过程
一、获取Apikey
大家首先要在阿里云官网注册账号,进入官网主页后,点击搜索‘模型服务灵积‘

在搜索结构中,点击‘产品控制台’

进入控制台后,点击API-KEY管理,创建自己的API-KEY,会生成一个API-KEY,我们必须要保存好!

注意创建后一定要保存好!!!

如果创建失败,大家可以自己去网上找一更详细的教程,我这里只是简单介绍一下。有志者,事竟成!!
二、学习文档开发
在控制台页面点击模型广场,这里有针对很多领域训练的模型,大家可以选择合适的模型开发。我这选择的是通义千问

可以看到这里有两种调用通义千问的方式(OpenAi兼容和DashScope)

我们要想通过OpenAi或DashScope调用的话,首先要获取API-KEY和配置环境变量(可选)。获取API-KEY我们在第一步已经完成,至于配置环境变量,大家可以选择性配置,这样做其实就是确保API-KEY的安全(我这里就没有配置了,直接在项目中使用)。然后还需安装SDK

我们点击安装SDK,然后会跳转到以下界面

大家可以看到Java SDK信息,我们可以先点击以下链接,查找到最新的SDK版本,复制到项目中
https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java

我们再次点击左侧的导航栏,点击‘模型调用’选择‘通义千问’

往下滑找到‘DashScope’,选择流式输出,点击Java

可以看到有示例代码、响应格式、请求体等(大家以后可以参考这些更深刻的完善自己的项目)。

三、简单实践
接下来我们分析一下示例代码,从中学习调用大模型的具体过程,最后简单实践测试一下。
我们先看main方法
public static void main(String[] args) {
try {
//生成式对象,封装了调用大模型接口,底层使用的是OkHttp库管理
Generation gen = new Generation();
//Message这是一个对话消息实体,包括角色和信息,这里是用户角色提问
Message userMsg = Message.builder().role(Role.USER.getValue()).content("你是谁?").build();
//将生成式对象和对话消息传入streamCallWithMessage,调用大模型
streamCallWithMessage(gen, userMsg);
} catch (ApiException | NoApiKeyException | InputRequiredException e) {
logger.error("An exception occurred: {}", e.getMessage());
}
System.exit(0);
}
Generation·这个对象并未在阿里云上找到详情说明,但我们通过查看源码,发现有很多类似于OkHttp中call方法,所以猜测它是封装了okhttp库的请求类。我们可以查看方法调用链,发现它最终使用的是OkHttp3,所以基本确定它就是封装好的调用大模型的请求类,我们只需配置好信息,然后使用它封装的方法即可完成调用。

我们再看streamCallWithMessage方法
public static void streamCallWithMessage(Generation gen, Message userMsg) throws Exception {
//将对话消息实体传入buildGenerationParam方法获取参数对象
GenerationParam param = buildGenerationParam(userMsg);
//调用生成式对象中call方法,完成大模型调用,结果保存在result中
Flowable<GenerationResult> result = gen.streamCall(param);
//我们服务器与阿里大模型服务之间也采用了sse,所以大模型服务会将已经回答消息流式推送我们的服务器
result.blockingForEach(message -> handleGenerationResult(message));
}
result.blockingForEach这个方法会监听阿里大模型服务推送过来的消息,只要有新的消息就会调用handleGenerationResult方法,将大模型回答的消息打印出来
private static void handleGenerationResult(GenerationResult message) {
System.out.println(JsonUtils.toJson(message));
}
{"requestId":"xxx","usage":{"input_tokens":11,"output_tokens":1,"total_tokens":12},"output":{"choices":[{"finish_reason":"null","message":{"role":"assistant","content":"我是"}}]}}
{"requestId":"xxx","usage":{"input_tokens":11,"output_tokens":2,"total_tokens":13},"output":{"choices":[{"finish_reason":"null","message":{"role":"assistant","content":"通"}}]}}
{"requestId":"xxx","usage":{"input_tokens":11,"output_tokens":3,"total_tokens":14},"output":{"choices":[{"finish_reason":"null","message":{"role":"assistant","content":"义"}}]}}
{"requestId":"xxx","usage":{"input_tokens":11,"output_tokens":8,"total_tokens":19},"output":{"choices":[{"finish_reason":"null","message":{"role":"assistant","content":"千问,由阿里"}}]}}
{"requestId":"xxx","usage":{"input_tokens":11,"output_tokens":16,"total_tokens":27},"output":{"choices":[{"finish_reason":"null","message":{"role":"assistant","content":"云开发的AI助手。我被"}}]}}
{"requestId":"xxx","usage":{"input_tokens":11,"output_tokens":24,"total_tokens":35},"output":{"choices":[{"finish_reason":"null","message":{"role":"assistant","content":"设计用来回答各种问题、提供信息"}}]}}
{"requestId":"xxx","usage":{"input_tokens":11,"output_tokens":32,"total_tokens":43},"output":{"choices":[{"finish_reason":"null","message":{"role":"assistant","content":"和与用户进行对话。有什么我可以"}}]}}
{"requestId":"xxx","usage":{"input_tokens":11,"output_tokens":36,"total_tokens":47},"output":{"choices":[{"finish_reason":"stop","message":{"role":"assistant","content":"帮助你的吗?"}}]}}
我们刚刚讨论了调用大模型服务的过程,我们再来看看它需要的配置参数,我们可以先看它的方法
private static GenerationParam buildGenerationParam(Message userMsg) {
return GenerationParam.builder()
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.model("qwen-turbo")
.messages(Arrays.asList(userMsg))
.resultFormat(GenerationParam.ResultFormat.MESSAGE)
.incrementalOutput(true)
.build();
}
我们可以看到需要设置有:
-
apiKey(我们之前申请的秘钥)
-
model(调用的大模型名称)
-
messages(对话信息)
-
resultFormat(设置返回的消息格式)
-
incrementalOutput(流式增量推送,这里选择true表开启)
apiKey我们已经申请过了就不再追诉,resultFormat和incrementalOutput我们不需要更改(但是如果有需要的话我们可以像之前那样找到文档描述,结合实际情况去修改)

我们要注意的是调用的大模型名model和对话消息messages,大家知道通义千问系列的大模型有多种,比如
qwen-plus、qwen-turbo...大家可以在模型列表中自行查看

而对话消息messages,我们参考官方文档教程,重点关注以下三种对话信息类型

我们可以使用Json格式列表展示对话结构,让大家对此有更好的理解
#对话信息messages
messages = [
{'role': 'system', 'content': 'You are a helpful assistant.'}, --指定模型角色
{'role': 'user', 'content': '你是谁?'} --用户提问
{'role':'assistant','content': '我是智能助手...'} --Ai回答
{...} --用户提问
{...} --Ai回答
]
到这里相信大家已经对我们调用大模型的过程有了一定的了解,我们再回过头去看实例代码是不是清晰了许多。
import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.utils.JsonUtils;
import io.reactivex.Flowable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
public class Main {
private static final Logger logger = LoggerFactory.getLogger(Main.class);
private static void handleGenerationResult(GenerationResult message) {
System.out.println(JsonUtils.toJson(message));
}
public static void streamCallWithMessage(Generation gen, Message userMsg)throws Exception {
GenerationParam param = buildGenerationParam(userMsg);
Flowable<GenerationResult> result = gen.streamCall(param);
result.blockingForEach(message -> handleGenerationResult(message));
}
private static GenerationParam buildGenerationParam(Message userMsg) {
return GenerationParam.builder()
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.model("qwen-plus")
.messages(Arrays.asList(userMsg))
.resultFormat(GenerationParam.ResultFormat.MESSAGE)
.incrementalOutput(true)
.build();
}
public static void main(String[] args) {
try {
Generation gen = new Generation();
Message userMsg = Message.builder().role(Role.USER.getValue()).content("你是谁?").build();
streamCallWithMessage(gen, userMsg);
} catch (Exception e) {
logger.error("An exception occurred: {}", e.getMessage());
}
System.exit(0);
}
}
我们在项目pom文件中导入SDK,编写一个测试案例,只需将以上代码中的apiKey替换成你自己的,点击运行,查看结果


如图实现了简单调用
四、进阶文档参考
我们在模型服务灵积控制台页面,左侧导航栏中找到‘文档中心’,点击后会出现右侧弹窗,我们点击‘文档中心打开’

我们在文档中心页面中,在左侧导航栏位置找到‘实践教程’,点击‘java sdk最佳实践’,这里可以看到官方对java sdk的最佳使用方式

官方对我们生成式对象Generation最佳使用介绍

对象池示例(org.apache.commons:commons-pool2)
public class PooledDashScopeObjectFactory extends BasePooledObjectFactory<Generation> {
//创建生成式对象
@Override
public Generation create() throws Exception {
return new Generation();
}
//将创建好的Generation对象包装成DefaultPooledObject<Generation>对象
//包装后的对象可以被对象池管理和维护,包括对象的借用、归还等操作。
@Override
public PooledObject<Generation> wrap(Generation obj) {
return new DefaultPooledObject<>(obj);
}
}
对象池使用
public class PooledDashScopeObjectUsage {
public static void main(String[] args) throws Exception {
//创建对象池实例
PooledDashScopeObjectFactory pooledDashScopeObjectFactory =
new PooledDashScopeObjectFactory();
//对象池配置实例
GenericObjectPoolConfig<Generation> config = new GenericObjectPoolConfig<>();
// 对于语音服务,websocket协议,保持下面值相同
config.setMaxTotal(32);
config.setMaxIdle(32);
config.setMinIdle(32);
//对象池装配该配置
GenericObjectPool<Generation> generationPool =
new GenericObjectPool<>(pooledDashScopeObjectFactory, config);
//挂一个空的Generation,便于后面从对象池中取实例
Generation gen = null;
//设置apiKey
Constants.apiKey="你的apiKey";
try {
//创建系统对话消息对象
Message systemMsg = Message.builder().role(Role.SYSTEM.getValue())
.content("You are a helpful assistant.").build();
//创建用户对话消息对象
Message userMsg = Message.builder().role(Role.USER.getValue()).content("你好").build();
//调用大模型时需要的配置参数对象(这里我们之前介绍过)
GenerationParam param = GenerationParam.builder()
.model("qwen-plus")
.messages(Arrays.asList(systemMsg, userMsg))
.resultFormat(GenerationParam.ResultFormat.MESSAGE).topP(0.8).enableSearch(true)
.build();
//从对象池中取出一个生成式对象实例
gen = generationPool.borrowObject();
//调用大模型回答
GenerationResult result = gen.call(param);
//打印
System.out.println(result);
} finally {
//调用结束归还到对象池
if (gen != null) {
generationPool.returnObject(gen);
}
}
System.out.println("completed");
generationPool.close();
}
}
将以上代码复制到项目中测试时,代码中替换你自己的apiKey,即可完成简单调用。
我们已经确定Generation底层使用的就是OkHttp库管理连接,所以我也可以修改连接配置,我们参考官方案例

Constants.connectionConfigurations = ConnectionConfigurations.builder()
.connectTimeout(Duration.ofSeconds(120)) // set connection timeout, default 120s
.readTimeout(Duration.ofSeconds(300)) // set read timeout, default 300s
.writeTimeout(Duration.ofSeconds(60)) // set read timeout, default 60s
.connectionIdleTimeout(Duration.ofSeconds(300)) // connection pool idle timeout, default 300s
.connectionPoolSize(32) // idle connections in the okhttp connection pool.
.maximumAsyncRequests(32) // async requests limit.
.maximumAsyncRequestsPerHost(32) // async request host limit.
.proxyHost("The http proxy host") // set proxy host, if set will use proxy. default null.
.proxyPort(443) // set proxy port, default 443
.proxyAuthenticator(null) // you can customize you proxy authenticator. default null.
.build();
// 更多 connectionPoolSize and connectionIdleTimeout, 可以参考
// ref: https://square.github.io/okhttp/3.x/okhttp/okhttp3/ConnectionPool.html
// maximumAsyncRequests and maximumAsyncRequestsPerHost 只对streamCall, audio相关服务,以及您自行设置
// 的使用websocket连接对象,详细参考:https://square.github.io/okhttp/3.x/okhttp/okhttp3/Dispatcher.html
五、项目实践
接下来我们创建一个项目,在pom文件中引入SDK

<!--阿里云模型服务SDK-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.16.3</version>
</dependency>
<!--对象池管理依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.8.0</version>
</dependency>
我们新建config包,创建MyCofig类如图

代码:
@Configuration
public class MyConfig {
@Value("${aliyunInfo.apikey}")
private String apiKey;
@PostConstruct
public void config(){
//设置APIKEY
Constants.apiKey=apiKey;
}
}
我们继续创建一个vo包,在该包下创建QianWenModelType枚举类

@Getter
public enum QianWenModelType {
/**
* 模型名和描述
*/
QWEN_TOURBO("qwen-turbo", "千问Touber"),
QWEN_PLUS("qwen-plus", "千问Plus");
private final String modelName;
private final String modelDescription;
QianWenModelType(String modelName, String modelDescription) {
this.modelName = modelName;
this.modelDescription = modelDescription;
}
}
然后我们创建一个entity包,并创建ChatClientEntity、MessageEntity两个类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChatClientEntity {
private String uuid;
private List<MessageEntity> messages;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class MessageEntity {
private String role;
private String content;
}
接下来我们还需创建utils包,里面有两个类
-
PooledDashScopeObjectFactory类,这个类是官网案例,也就是我们前面的讨论的对象池

-
QianWenReplyUtil类
@Slf4j public class QianWenReplyUtil { /** * 对象池 */ private static GenericObjectPool<Generation> generationPool; /** * 设置参数(这个方法我们之前已经讨论过) * @param messages 消息 * @return 参数实例 */ public static GenerationParam createGenerationParam(List<Message> messages) { return GenerationParam.builder() .model(QianWenModelType.QWEN_TOURBO.getModelName()) .messages(messages) .resultFormat(GenerationParam.ResultFormat.MESSAGE) .topP(0.8) .incrementalOutput(true) .build(); } /** * 消息转换(我们前后端传输使用的是自定义的MessageEntity类,需要将其转换成Message,即规定的类) */ public static List<Message> createMessage(List<MessageEntity> messageEntities) { List<Message> messages = new ArrayList<>(); for (MessageEntity messageEntity : messageEntities) { Message message = Message.builder().role(messageEntity.getRole()).content(messageEntity.getContent()).build(); messages.add(message); } return messages; } /** * 开启连接池(这里我们使用的是单例模式) */ public static GenericObjectPool<Generation> getGenerationPool() { if (Objects.isNull(generationPool)) { //开启连接池 PooledDashScopeObjectFactory pooledDashScopeObjectFactory = new PooledDashScopeObjectFactory(); GenericObjectPoolConfig<Generation> config = new GenericObjectPoolConfig<>(); // 对于语音服务,websocket协议,保持下面值相同 config.setMaxTotal(32); config.setMaxIdle(32); config.setMinIdle(32); generationPool = new GenericObjectPool<>(pooledDashScopeObjectFactory, config); } return generationPool; } /** * 生成式文本敏感词过滤(大家根据自己实际情况去编写) */ public static String filterSensitiveWords(String text) { // TODO return text; } }
最后创建一个Controller包,并创建QianWenAiController类
如果说大家对sse技术不是很了解,这推荐参考:
@RestController
@Slf4j
@CrossOrigin
public class QianWenAiController {
/**
* 客户端sse连接池
*/
public static Map<String, SseEmitter> sseEmitterMap = new HashMap<>();
/**
* 开启sse连接
*/
@GetMapping("/open")
public SseEmitter startSse(@RequestParam String uuid) {
// 判断sse连接池中是否存在该连接
SseEmitter sseEmitter = sseEmitterMap.get(uuid);
if (Objects.isNull(sseEmitter)) {
sseEmitter = new SseEmitter(0L);
sseEmitter.onCompletion(() -> sseEmitterMap.remove(uuid));
sseEmitter.onTimeout(() -> sseEmitterMap.remove(uuid));
sseEmitterMap.put(uuid, sseEmitter);
}
log.info("客户端{}开启了sse连接", uuid);
return sseEmitter;
}
/**
* 关闭sse连接
*/
@GetMapping("/close")
public void closeSse(@RequestBody String uuid) {
// 判断sse连接池中是否存在该连接
SseEmitter sseEmitter = sseEmitterMap.get(uuid);
if (Objects.nonNull(sseEmitter)) {
sseEmitter.complete();
sseEmitterMap.remove(uuid);
}
}
/**
* 推送消息,调用大模型流式回答接口,将返回的消息推送
*/
@PostMapping("/push")
public void pushMessage(@RequestBody ChatClientEntity chatClientEntity, HttpServletResponse response) {
// 关闭Nginx缓存,直接推流
response.setHeader("Cache-Control", "no-cache");
response.setHeader("X-Accel-Buffering", "no");
// 流式事件流响应
response.setContentType(MediaType.TEXT_EVENT_STREAM_VALUE);
// 判断sse连接池中是否存在该连接
SseEmitter sseEmitter = sseEmitterMap.get(chatClientEntity.getUuid());
Generation gen = null;
//异步回答
if (Objects.nonNull(sseEmitter)) {
try {
// 从对象池中获取一个连接
gen = QianWenReplyUtil.getGenerationPool().borrowObject();
//semaphore用于标记异步调用是否完成或出错
Semaphore semaphore = new Semaphore(0);
//保存完整的回答信息
StringBuilder fullContent = new StringBuilder();
// 创建参数实例
GenerationParam param = QianWenReplyUtil.createGenerationParam(QianWenReplyUtil.createMessage(chatClientEntity.getMessages()));
// 调用大模型
gen.streamCall(param, new ResultCallback<>() {
@Override
public void onEvent(GenerationResult message) {
fullContent.append(message.getOutput().getChoices().get(0).getMessage().getContent());
log.info("生成信息:{}", JsonUtils.toJson(message));
// 推送消息
// 生成式文本敏感词过滤(此处代码省略)
try {
sseEmitter.send(JsonUtils.toJson(message));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void onError(Exception err) {
log.error("发生错误{}", err.getMessage());
try {
sseEmitter.send("服务器正在升级中,请稍后再试!");
} catch (IOException e) {
throw new RuntimeException(e);
}
semaphore.release();
}
@Override
public void onComplete() {
log.info("回答完成");
semaphore.release();
}
});
log.info("Full content: \n{}", fullContent);
}catch (Exception e){
log.error("发生错误{}", e.getMessage());
}finally {
//异步线程调用完成,执行主线程,确保gen对象归还到对象池
semaphore.acquire();
QianWenReplyUtil.getGenerationPool().returnObject(gen);
}
}
}
}
这就是我们后端项目的实现代码,下面是前端实现代码,使用的是vue3,大家可以自行复制到项目中直接使用
<template>
<transition name="el-zoom-in-center">
<div class="robot-container" v-show="show">
<div class="window-container" ref="scrollDiv">
<div v-if="chatData.length !== 0" v-for="(item, index) in chatData ">
<div :class="{
'info-card-left': item.role === 'assistant',
'info-card-right': item.role === 'user'
}">
<!--头像 -->
<div class="user-avatar" v-if="item.role === 'assistant'">
<el-avatar shape="square" :size="50"
src="https://i03piccdn.sogoucdn.com/c477d031f53cfe05" />
</div>
<!-- 内容 -->
<div class="user-content">
<div v-html="markdown.render(item.content)"></div>
</div>
<!-- 头像 -->
<div class="user-avatar" v-if="item.role !== 'assistant'">
<el-avatar shape="square" :size="50" :src="userAvatar" />
</div>
</div>
</div>
</div>
<div class="bottom">
<el-input :disabled="isReplying" v-model="content" style="width: 98%;margin-left: 10px;" type="textarea"
placeholder="输入提问信息" />
<div class="send">
<el-button :disabled="isReplying" type="primary" style="width: 150px;margin-top: 10px;"
:icon="Promotion" @click="handleSend">提问</el-button>
</div>
</div>
</div>
</transition>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { Promotion } from '@element-plus/icons-vue'
import axios from 'axios'
import { v4 as uuid4 } from 'uuid'
import store from '@/store';
import MarkdownIt from 'markdown-it';
const markdown = new MarkdownIt()
const userAvatar = ref('https://pic4.zhimg.com/80/v2-5287de47e7e537125b7200fb99b84d53_720w.webp')
const uuid = ref('')
const source = ref(null)
const content = ref('')
const isReplying = ref(false)
const replyContent = ref('')
const show = ref(false)
const chatData = ref([])
const scrollDiv = ref(null)
//滚动到底部
const scrollToBottom = () => {
//滚动到底部
let scrollElem = scrollDiv.value;
scrollElem.scrollTo({ top: scrollElem.scrollHeight, behavior: 'smooth' });
}
/** 生成一个以字母开始的唯一标识符 */
const generateLetterPrefixedId = () => {
let id = '';
do {
// 生成一个UUID并取其前几个字符
id = uuid4().substring(0, 8).toUpperCase();
// 如果第一个字符不是字母,则继续循环
}
while (!/^[a-zA-Z]/.test(id));
return id;
}
//发送前处理数据
const handleData = () => {
isReplying.value = true
chatData.value.push({
role: 'user',
content: content.value
})
chatData.value.push({
role: 'assistant',
content: '正在思考...'
})
content.value = ''
replyContent.value = ''
}
//sse 连接
const SSE = () => {
if (window.EventSource) {
// 建立连接
source.value = new EventSource('http://localhost:8080/open?uuid=' + uuid.value)
/**
* 连接一旦建立,就会触发open事件
*/
source.value.onopen = function (e) {
console.log('建立连接', e)
}
/**
* 客户端收到服务器发来的数据
*/
source.value.onmessage = function (e) {
// 滚动到底部
scrollToBottom()
console.log(JSON.parse(e.data).output.choices[0].message.content)
replyContent.value = replyContent.value + JSON.parse(e.data).output.choices[0].message.content
chatData.value[chatData.value.length - 1].content = replyContent.value
store.commit('setMessages', chatData.value)
isReplying.value = false
}
/**
* 如果发生通信错误(比如连接中断),就会触发error事件
*/
source.value.onerror = function (e) {
if (e.readyState === EventSource.CLOSED) {
console.log('连接关闭')
} else {
console.log(e)
}
}
} else {
console.log('浏览器不支持SSE')
}
}
// 发送消息
const handleSend = async () => {
// 处理数据
await handleData()
// 开启sse连接
SSE()
// 滚动到底部
scrollToBottom()
await axios({
url: 'http://localhost:8080/push',
headers: {
'Content-Type': 'application/json'
},
data: {
'uuid': uuid.value,
'messages': chatData.value
},
method: 'post',
})
}
onMounted(() => {
//模仿登录用户的唯一标识
uuid.value = generateLetterPrefixedId()
chatData.value = store.getters.getMessages
show.value = true
})
</script>
<style scoped>
/* 隐藏滚动条 */
::-webkit-scrollbar {
width: 0;
}
.robot-container {
margin: 30px auto;
width: 900px;
height: 660px;
border-radius: 10px;
background-color: rgb(184, 203, 239);
overflow: hidden;
box-shadow: 0 2px 3px rgb(0 0 0 / 10%);
transition: all 0.5s;
padding: 5px;
.window-container {
width: 99%;
margin: 0 auto;
margin-top: 10px;
height: 440px;
padding: 10px;
background-color: rgb(245, 245, 245);
overflow: scroll;
.info-card-left {
width: 70%;
height: 20%;
display: flex;
justify-content: left;
margin-bottom: 10px;
.user-avatar {
display: flex;
align-items: center;
margin-left: 10px;
height: 50px;
box-shadow: 0 2px 3px rgb(0 0 0 / 10%);
}
.user-content {
background-color: white;
border-radius: 10px;
box-shadow: 0 2px 3px rgb(0 0 0 / 10%);
padding: 10px;
margin-left: 10px;
font-size: 16px;
align-items: center;
display: flex;
}
.user-content>>>h1 {
font-weight: bold;
}
.user-content>>>h2 {
font-weight: bold;
}
.user-content>>>h3 {
font-weight: bold;
}
.user-content>>>h4 {
font-weight: bold;
}
}
.info-card-right {
width: 70%;
height: 20%;
display: flex;
justify-content: right;
margin-bottom: 10px;
margin-left: auto;
.user-avatar {
display: flex;
align-items: center;
height: 50px;
margin-right: 10px;
box-shadow: 0 2px 3px rgb(0 0 0 / 10%);
}
.user-content {
background-color: white;
border-radius: 10px;
box-shadow: 0 2px 3px rgb(0 0 0 / 10%);
padding: 10px;
margin-right: 10px;
font-size: 16px;
background-color: rgb(71, 225, 99);
align-items: center;
display: flex;
text-align: right;
}
}
}
.bottom {
width: 99%;
margin: 0 auto;
padding-top: 10px;
height: 180px;
background-color: rgb(250, 250, 250);
.send {
width: 100%;
height: 50px;
display: flex;
justify-content: right;
align-items: center;
padding-right: 10px;
}
}
::v-deep .el-textarea__inner {
height: 120px;
font-size: 18px;
}
}
</style>
七、源码地址
有用的话,朋友,请点个赞或关注,谢谢!
335






