feat: 创建项目ID

This commit is contained in:
2026-02-25 11:00:31 +08:00
parent 37cedb5272
commit 9fbeb408fc
9 changed files with 288 additions and 173 deletions

View File

@@ -1,4 +1,5 @@
# VITE_APP_URL = http://192.168.31.82:8771 # VITE_APP_URL = http://192.168.31.82:8771
VITE_APP_URL = http://18.167.251.121:10015 # VITE_APP_URL = http://18.167.251.121:10015
# VITE_APP_URL = http://192.168.31.118:8080 # VITE_APP_URL = http://192.168.31.118:8080
VITE_APP_URL = http://192.168.31.82:8755
VITE_GOOGLE_CLIENT_ID = 216037134725-7q8vqp0ohtmohlosltkfg7bd2v29rm5a.apps.googleusercontent.com VITE_GOOGLE_CLIENT_ID = 216037134725-7q8vqp0ohtmohlosltkfg7bd2v29rm5a.apps.googleusercontent.com

View File

@@ -17,3 +17,13 @@ export const fetchAgentReply = (data: AgentParamsType): Promise<AgentResponse> =
meta: { responseAll: true } meta: { responseAll: true }
}) })
} }
export interface CreateProjectParamsType {
type: string
region: string
style: string
temperature: number | string
}
export const createProject = (data: CreateProjectParamsType): Promise<any> => {
return request({ url: '/api/project/init', method: 'post', data })
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 295 B

View File

@@ -8,3 +8,4 @@ export default store
export * from './global' export * from './global'
export * from './userInfo' export * from './userInfo'
export * from './projectData' export * from './projectData'
export * from './agent'

View File

@@ -5,25 +5,25 @@ import { ElMessage } from 'element-plus'
// 扩展 AxiosRequestConfig 接口 // 扩展 AxiosRequestConfig 接口
declare module 'axios' { declare module 'axios' {
interface AxiosRequestConfig { interface AxiosRequestConfig {
loading?: boolean loading?: boolean
loadingDom?: any loadingDom?: any
repeatRequest?: boolean repeatRequest?: boolean
meta?: { meta?: {
responseAll?: boolean responseAll?: boolean
} }
} }
} }
// 创建axios实例 // 创建axios实例
// console.log(import.meta.env,123) // console.log(import.meta.env,123)
const service = axios.create({ const service = axios.create({
// baseURL: import.meta.env.VITE_APP_URL, // api的base_url // baseURL: import.meta.env.VITE_APP_URL, // api的base_url
timeout: 60000 // 请求超时时间 timeout: 60000 // 请求超时时间
}) })
if (import.meta.env.MODE != 'development') { if (import.meta.env.MODE != 'development') {
service.defaults.baseURL = import.meta.env.VITE_APP_URL service.defaults.baseURL = import.meta.env.VITE_APP_URL
} }
axios.defaults.headers.post['Content-Type'] = 'application/json' axios.defaults.headers.post['Content-Type'] = 'application/json'
axios.defaults.headers.post['lang'] = 'en' //配置语言请求头 axios.defaults.headers.post['lang'] = 'en' //配置语言请求头
@@ -31,97 +31,98 @@ axios.defaults.withCredentials = true //跨域携带cookie
// request拦截器 // request拦截器
service.interceptors.request.use( service.interceptors.request.use(
(config: any) => { (config: any) => {
removePending(config) removePending(config)
// 如果repeatRequest不配置那么默认该请求就取消重复接口请求 // 如果repeatRequest不配置那么默认该请求就取消重复接口请求
!config.repeatRequest && addPending(config) !config.repeatRequest && addPending(config)
// 打开loading // 打开loading
if (config.loading) { if (config.loading) {
LoadingInstance._count++ LoadingInstance._count++
if (LoadingInstance._count === 1) { if (LoadingInstance._count === 1) {
openLoading(config.loadingDom) openLoading(config.loadingDom)
} }
} }
// 如果登录了有token则请求携带token // 如果登录了有token则请求携带token
// Do something before request is sent // Do something before request is sent
const token = useUserInfoStore().state.token const token = useUserInfoStore().state.token
if (token) { if (token) {
config.headers.Authorization = token // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改 config.headers.Authorization = 'Bearer ' + token // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
// config.headers['X-Token'] = getLocal('token') // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改 // config.headers['X-Token'] = getLocal('token') // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
} }
return config return config
}, },
(error) => { (error) => {
// Do something with request error // Do something with request error
console.log(error) // for debug console.log(error) // for debug
Promise.reject(error) Promise.reject(error)
} }
) )
// respone拦截器 // respone拦截器
service.interceptors.response.use( service.interceptors.response.use(
// response => response, // response => response,
/** /**
* 下面的注释为通过response自定义code来标示请求状态当code返回如下情况为权限有问题登出并返回到登录页 * 下面的注释为通过response自定义code来标示请求状态当code返回如下情况为权限有问题登出并返回到登录页
* 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中 * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
*/ */
(response: any) => { (response: any) => {
// 如果是llm/streamChat这样的流式接口,不走这样的处理 // 如果是llm/streamChat这样的流式接口,不走这样的处理
if (response.config.url.includes('llm/streamChat')) { if (response.config.url.includes('llm/streamChat')) {
return response return response
}
// 已完成请求的删除请求中数组
removePending(response.config)
// 关闭loading
if (response.config.loading) {
closeLoading()
}
const res = response.data
// 处理异常的情况
// console.log(res)
if (res.code != 200) {
ElMessage.error(res.message)
return Promise.reject(new Error(res.errMsg || res.message || 'error'))
} else {
// 默认只返回data不返回状态码和message
// 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码message和data)
const isbackAll = response.config.meta && response.config.meta.responseAll
if (isbackAll) {
return res
} else {
return res.data
}
}
},
(error) => {
if(error?.response){
if (error.config?.loading) closeLoading() // 关闭loading
if(error?.response?.status === 401){//如果是记录浏览器页面就不跳转login
// showConfirmDialog({
// title: '确定登出',
// message: '你已被登出,可以取消继续留在该页面,或者重新登录',
// confirmButtonText: '重新登录',
// cancelButtonText: '取消'
// }).then(() => {
// store.loginOut().then(() => {
// location.reload() // 为了重新实例化vue-router对象 避免bug
// })
// })
// showToast({
// message: 'Please log in and try again.',
// duration: 5000
// })
// router.push('/login')
// useGenerateStore().clearGenerateData()
return Promise.reject(false)
} }
error.config && removePending(error.config)
console.log('err' + error) // for debug // 已完成请求的删除请求中数组
ElMessage.error(error.message) removePending(response.config)
// 关闭loading
if (response.config.loading) {
closeLoading()
}
const res = response.data
// 处理异常的情况
// console.log(res)
if (res.code != 200) {
ElMessage.error(res.message)
return Promise.reject(new Error(res.errMsg || res.message || 'error'))
} else {
// 默认只返回data不返回状态码和message
// 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码message和data)
const isbackAll = response.config.meta && response.config.meta.responseAll
if (isbackAll) {
return res
} else {
return res.data
}
}
},
(error) => {
if (error?.response) {
if (error.config?.loading) closeLoading() // 关闭loading
if (error?.response?.status === 401) {
//如果是记录浏览器页面就不跳转login
// showConfirmDialog({
// title: '确定登出',
// message: '你已被登出,可以取消继续留在该页面,或者重新登录',
// confirmButtonText: '重新登录',
// cancelButtonText: '取消'
// }).then(() => {
// store.loginOut().then(() => {
// location.reload() // 为了重新实例化vue-router对象 避免bug
// })
// })
// showToast({
// message: 'Please log in and try again.',
// duration: 5000
// })
// router.push('/login')
// useGenerateStore().clearGenerateData()
return Promise.reject(false)
}
error.config && removePending(error.config)
console.log('err' + error) // for debug
ElMessage.error(error.message)
}
return Promise.reject(error)
} }
return Promise.reject(error)
}
) )
// --------------------------------取消接口重复请求的函数----------------------------------- // --------------------------------取消接口重复请求的函数-----------------------------------
@@ -133,10 +134,10 @@ const pendingMap = new Map()
* @returns string * @returns string
*/ */
function getPendingKey(config: any) { function getPendingKey(config: any) {
const { url, method, params } = config const { url, method, params } = config
let { data } = config let { data } = config
if (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象 if (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象
return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&') return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&')
} }
/** /**
@@ -144,39 +145,39 @@ function getPendingKey(config: any) {
* @param {*} config * @param {*} config
*/ */
function addPending(config: any) { function addPending(config: any) {
const pendingKey = getPendingKey(config) const pendingKey = getPendingKey(config)
config.cancelToken = config.cancelToken =
config.cancelToken || config.cancelToken ||
new axios.CancelToken((cancel) => { new axios.CancelToken((cancel) => {
if (!pendingMap.has(pendingKey)) { if (!pendingMap.has(pendingKey)) {
pendingMap.set(pendingKey, cancel) pendingMap.set(pendingKey, cancel)
} }
}) })
} }
/** /**
* 删除重复的请求 * 删除重复的请求
* @param {*} config * @param {*} config
*/ */
function removePending(config: any) { function removePending(config: any) {
const pendingKey = getPendingKey(config) const pendingKey = getPendingKey(config)
if (pendingMap.has(pendingKey)) { if (pendingMap.has(pendingKey)) {
const cancelToken = pendingMap.get(pendingKey) const cancelToken = pendingMap.get(pendingKey)
cancelToken(pendingKey) cancelToken(pendingKey)
pendingMap.delete(pendingKey) pendingMap.delete(pendingKey)
} }
} }
// ----------------------------------loading的函数------------------------------- // ----------------------------------loading的函数-------------------------------
const LoadingInstance: { _count: number } = { const LoadingInstance: { _count: number } = {
_count: 0 _count: 0
} }
function openLoading(loadingDom: any) { function openLoading(loadingDom: any) {
useGlobalStore().setLoading(true) useGlobalStore().setLoading(true)
} }
function closeLoading() { function closeLoading() {
if (LoadingInstance._count > 0) LoadingInstance._count-- if (LoadingInstance._count > 0) LoadingInstance._count--
if (LoadingInstance._count === 0) { if (LoadingInstance._count === 0) {
useGlobalStore().setLoading(false) useGlobalStore().setLoading(false)
} }
} }
export default service export default service

View File

@@ -8,8 +8,13 @@
<SvgIcon name="equal" color="#0d0d0d" size="24" /> <SvgIcon name="equal" color="#0d0d0d" size="24" />
</div> </div>
<div class="agent-body flex-1 flex flex-col"> <div class="agent-body flex-1 flex flex-col">
<List ref="listRef" :message-list="messageList" /> <List ref="listRef" :message-list="messageList" @regenerate="handleRegenerate" />
<Input is-agent-mode @send="handleSendMessage" /> <Input
is-agent-mode
:generating="isGenerating"
@send="handleSendMessage"
@pause="handlePause"
/>
</div> </div>
</div> </div>
</template> </template>
@@ -20,7 +25,7 @@
import Input from '../../components/Input.vue' import Input from '../../components/Input.vue'
import { fetchAgentReply } from '@/api/agent' import { fetchAgentReply } from '@/api/agent'
import type { AgentParamsType } from '@/api/agent' import type { AgentParamsType } from '@/api/agent'
import { useUserInfoStore,useProjectStore } from '@/stores' import { useUserInfoStore, useProjectStore } from '@/stores'
import { useAgentStore } from '@/stores/agent' import { useAgentStore } from '@/stores/agent'
const userStore = useUserInfoStore() const userStore = useUserInfoStore()
@@ -37,8 +42,10 @@
const messageList = ref([]) const messageList = ref([])
const listRef = ref() const listRef = ref()
const isGenerating = ref(false)
const isPaused = ref(false) // 标记是否为主动暂停
const params = reactive<AgentParamsType>({ const params = reactive<AgentParamsType>({
projectID: '1', projectID: projectStore.state.id,
message: '', message: '',
token: userStore.state.token, token: userStore.state.token,
versionID: '', versionID: '',
@@ -51,22 +58,32 @@
imageUrlList: [] imageUrlList: []
}) })
const abort = new AbortController() // 每次请求时创建新的 AbortController
let abort: AbortController
const createAbortController = () => {
// 如果已有未完成的中止,先中止它
if (abort) {
abort.abort()
}
abort = new AbortController()
return abort
}
onUnmounted(() => { onUnmounted(() => {
abort.abort() abort?.abort()
}) })
onMounted(() => { onMounted(() => {
// 检查 store 中是否有初始项目数据 // 检查 store 中是否有初始项目数据
projectStore.setId('1') // 临时设置项目ID为1实际应用中应根据上下文动态设置 // projectStore.setId('1') // 临时设置项目ID为1实际应用中应根据上下文动态设置
const initialData = agentStore.getInitialProjectData const initialData = agentStore.getInitialProjectData
if (initialData) { if (initialData) {
// 等待页面渲染完成后自动发送初始消息 // 等待页面渲染完成后自动发送初始消息
params.configParams = { params.configParams = {
type: initialData.type || 'Chair', type: initialData.type ,
region: initialData.area || 'China', region: initialData.area,
style: initialData.style || 'Transitional', style: initialData.style ,
temperature: 0.7 temperature: 0.7
} }
handleSendMessage({ handleSendMessage({
@@ -83,15 +100,21 @@
const handleSendMessage = async (message: { const handleSendMessage = async (message: {
text: string text: string
images: Array<{ url: string; name: string }> images: Array<{ url: string; name: string }>
}) => { }, skipUserMessage = false) => {
console.log('Message sent:', message) console.log('Message sent:', message)
isPaused.value = false
isGenerating.value = true
params.message = message.text params.message = message.text
params.imageUrlList = message.images || [] params.imageUrlList = message.images || []
messageList.value.push({
id: messageList.value.length + 1, // 如果不是重新生成模式,则添加用户消息到列表
text: message.text, if (!skipUserMessage) {
isUser: true messageList.value.push({
}) id: messageList.value.length + 1,
text: message.text,
isUser: true
})
}
// Add AI loading message // Add AI loading message
const aiMessage = reactive({ const aiMessage = reactive({
@@ -106,6 +129,9 @@
}) })
messageList.value.push(aiMessage) messageList.value.push(aiMessage)
// 创建新的 AbortController
const abortController = createAbortController()
// console.log('token---', params.token, '参数---', params) // console.log('token---', params.token, '参数---', params)
try { try {
@@ -116,7 +142,7 @@
const response = await fetch(`/api/ai-design/chat?${urlParams.toString()}`, { const response = await fetch(`/api/ai-design/chat?${urlParams.toString()}`, {
method: 'GET', method: 'GET',
signal: abort.signal signal: abortController.signal
}) })
// 检查响应内容类型,判断是否为流式响应 // 检查响应内容类型,判断是否为流式响应
@@ -128,9 +154,12 @@
// 非流式错误响应,使用 text() 读取错误信息 // 非流式错误响应,使用 text() 读取错误信息
const errorText = await response.text() const errorText = await response.text()
console.error('请求错误:', errorText) console.error('请求错误:', errorText)
aiMessage.text = '发送失败,请重试' if (!isPaused.value) {
aiMessage.text = '发送失败,请重试'
}
aiMessage.streaming = false aiMessage.streaming = false
aiMessage.loading = false aiMessage.loading = false
isGenerating.value = false
return return
} }
@@ -143,9 +172,12 @@
} catch (e) { } catch (e) {
console.error('非流式响应文本:', text) console.error('非流式响应文本:', text)
} }
aiMessage.text = '发送失败,请重试' if (!isPaused.value) {
aiMessage.text = '发送失败,请重试'
}
aiMessage.streaming = false aiMessage.streaming = false
aiMessage.loading = false aiMessage.loading = false
isGenerating.value = false
return return
} }
@@ -181,17 +213,17 @@
// 过滤掉 id: 等字段,只取 data: // 过滤掉 id: 等字段,只取 data:
let isNodeIdEvent = false let isNodeIdEvent = false
if(event.startsWith('event:')){ if (event.startsWith('event:')) {
isNodeIdEvent = true isNodeIdEvent = true
// continue // continue
} }
const dataLines = event const dataLines = event
.split(/\n/) .split(/\n/)
.filter((line) => line.startsWith('data:')) .filter((line) => line.startsWith('data:'))
.map((line) => line.replace(/^data:\s*/, '').trim()) .map((line) => line.replace(/^data:\s*/, '').trim())
console.log('dataLInes',dataLines); console.log('dataLInes', dataLines)
if(isNodeIdEvent){ if (isNodeIdEvent) {
params.versionID = dataLines[0] params.versionID = dataLines[0]
} }
@@ -215,6 +247,7 @@
if (jsonData.type === 'end') { if (jsonData.type === 'end') {
aiMessage.streaming = false aiMessage.streaming = false
aiMessage.loading = false aiMessage.loading = false
isGenerating.value = false
flag = false flag = false
break break
} }
@@ -224,6 +257,7 @@
console.log('结束-----------------------') console.log('结束-----------------------')
aiMessage.streaming = false aiMessage.streaming = false
aiMessage.loading = false aiMessage.loading = false
isGenerating.value = false
flag = false flag = false
break break
} }
@@ -239,19 +273,59 @@
} }
} catch (error) { } catch (error) {
console.error('流式传输错误:', error) console.error('流式传输错误:', error)
aiMessage.text = '发送失败,请重试' if (!isPaused.value) {
aiMessage.text = '发送失败,请重试'
}
aiMessage.streaming = false aiMessage.streaming = false
aiMessage.loading = false aiMessage.loading = false
isGenerating.value = false
} finally { } finally {
reader.releaseLock() reader.releaseLock()
} }
} catch (error) { } catch (error) {
console.error('fetch请求失败:', error) console.error('fetch请求失败:', error)
aiMessage.text = '发送失败,请重试' if (!isPaused.value) {
aiMessage.text = '发送失败,请重试'
}
aiMessage.streaming = false aiMessage.streaming = false
aiMessage.loading = false aiMessage.loading = false
isGenerating.value = false
} }
} }
const handlePause = () => {
isPaused.value = true
isGenerating.value = false
abort?.abort()
}
const handleRegenerate = async (aiMessage: any) => {
// 找到当前 AI 消息在列表中的索引
const aiIndex = messageList.value.findIndex((msg) => msg.id === aiMessage.id)
if (aiIndex === -1) return
// 找到对应的用户消息AI 消息前面的最近一条用户消息)
let userMessage = null
for (let i = aiIndex - 1; i >= 0; i--) {
if (messageList.value[i].isUser) {
userMessage = messageList.value[i]
break
}
}
if (!userMessage) return
// 删除当前的 AI 回复消息
messageList.value.splice(aiIndex, 1)
// 重新调用 API跳过用户消息添加因为用户消息已存在
await handleSendMessage(
{
text: userMessage.text,
images: userMessage.images || []
},
true
)
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@@ -81,6 +81,8 @@
return str return str
}) })
const emit = defineEmits(['regenerate'])
const operateList = ref([ const operateList = ref([
{ {
name: 'thumbUp', name: 'thumbUp',
@@ -97,7 +99,7 @@
{ {
name: 'refreshTransparent', name: 'refreshTransparent',
action: () => { action: () => {
console.log('refresh') emit('regenerate')
} }
}, },
{ {

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="agent-list flex flex-col flex-1" ref="listContainer"> <div class="agent-list flex flex-col flex-1" ref="listContainer">
<Item v-for="message in messageList" :key="message.id" :content="message" /> <Item v-for="message in messageList" :key="message.id" :content="message" @regenerate="$emit('regenerate', message)" />
</div> </div>
</template> </template>
@@ -12,6 +12,8 @@
messageList: Array<any> messageList: Array<any>
}>() }>()
const emit = defineEmits(['regenerate'])
const listContainer = ref<HTMLDivElement>() const listContainer = ref<HTMLDivElement>()
const scrollToBottom = () => { const scrollToBottom = () => {

View File

@@ -151,12 +151,16 @@
<img src="@/assets/images/shining.png" class="shining-icon" alt="" /> <img src="@/assets/images/shining.png" class="shining-icon" alt="" />
<span class="create-btn-text">{{ $t('Input.createProject') }}</span> <span class="create-btn-text">{{ $t('Input.createProject') }}</span>
</div> </div>
<img
v-else <div v-else class="sender-btn flex flex-center" @click="handleSendAgent">
src="@/assets/images/sender.png" <img
class="sender-icon" v-show="!generating"
@click="handleSendAgent" src="@/assets/images/sender.png"
/> alt=""
class="sender-icon"
/>
<div v-show="generating" class="sender-pause" />
</div>
</div> </div>
</div> </div>
<div v-if="!isAgentMode" class="report-btn flex flex-center" @click="toogltReportTag"> <div v-if="!isAgentMode" class="report-btn flex flex-center" @click="toogltReportTag">
@@ -171,24 +175,28 @@
import { areaList } from '@/utils/area' import { areaList } from '@/utils/area'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useAgentStore } from '@/stores/agent' import { useAgentStore, useProjectStore } from '@/stores'
import lightIcon from '@/assets/images/light-icon.png' import lightIcon from '@/assets/images/light-icon.png'
import closeIcon from '@/assets/images/close-icon.png' import closeIcon from '@/assets/images/close-icon.png'
import { createProject } from '@/api/agent'
// import Tag from './Tag.vue' // import Tag from './Tag.vue'
const router = useRouter() const router = useRouter()
const agentStore = useAgentStore() const agentStore = useAgentStore()
const projectStore = useProjectStore()
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
isAgentMode?: boolean isAgentMode?: boolean
generating?: boolean
}>(), }>(),
{ {
isAgentMode: false isAgentMode: false,
generating: false
} }
) )
const emits = defineEmits(['send']) const emits = defineEmits(['send', 'pause'])
const { t } = useI18n() const { t } = useI18n()
@@ -424,7 +432,11 @@
} }
} }
const handleSendAgent = () => { const handleSendAgent = async () => {
if (props.generating) {
emits('pause')
return
}
if (!inputValue.value.trim()) return if (!inputValue.value.trim()) return
emits('send', { text: inputValue.value.trim(), images: uploadedImages.value }) emits('send', { text: inputValue.value.trim(), images: uploadedImages.value })
// 发送后清空输入框 // 发送后清空输入框
@@ -499,21 +511,22 @@
})) }))
) )
const handleCreateProject = () => { const handleCreateProject = async () => {
// 这里可以添加创建项目的逻辑 // 这里可以添加创建项目的逻辑
const params = { const params = {
type: typeValue.value, type: typeValue.value,
area: areaValue.value, area: areaValue.value,
style: styleValue.value style: styleValue.value,
temperature: 0.7
} }
const projectres = await createProject(params)
console.log('projectres', projectres)
projectStore.setId(projectres)
// 保存初始数据到 store // 保存初始数据到 store
agentStore.setInitialProjectData({ agentStore.setInitialProjectData({
text: inputValue.value.trim(), text: inputValue.value.trim(),
images: uploadedImages.value, images: uploadedImages.value,
type: typeValue.value, ...params
area: areaValue.value,
style: styleValue.value
}) })
console.log('Create project with:', params) console.log('Create project with:', params)
@@ -736,10 +749,21 @@
.right { .right {
display: flex; display: flex;
align-items: center; align-items: center;
.sender-icon { .sender-btn {
width: 3.2rem; width: 3.2rem;
height: 3.2rem; height: 3.2rem;
cursor: pointer; cursor: pointer;
background-color: #ff7a51;
border-radius: 50%;
.sender-icon {
width: 1.3rem;
height: 1.3rem;
}
.sender-pause {
width: 1rem;
height: 1rem;
background-color: #fff;
}
} }
} }
} }