vue---day07(props,混入mixin,插件plugins,elementui样式库,Router,vuex,localStorage系列,)
昨日回顾
# 1 nodejs 后端语言---》js语法---》node,npm命令
-npm命令下载模块慢
-淘宝的 cnpm,以后使用npm的地方都可以使用cnpm
# 2 安装vue-cli创建项目
-vue项目的运行依赖于node环境---》后期如果上线,服务器上装node?
-创建vue项目,把vue的项目编译成纯html,css,js
- cnpm install -g @vue/cli
-释放出一个可执行文件 vue,已经在环境变量了
-vue create 项目名 #创建项目--》babel,vue-router,vuex ,vue2
-vue ui # node 启动一个服务,在浏览器中可以图形化界面创建
# 3 项目目录结构
-node_models # 项目第三方依赖 项目路径下:cnpm install
-public # index.html 小图标
-src # 代码存放路径
-router
-store
-放页面组件和小组件
-App.vue
-main.js
-package.json
# 5 vue 开发流程
- 以后只需要写组件 xx.vue,把组件导入使用即可
-组件中有的三部分
-<template>html内容,插值,方法,事件,指令</template>
-<script> js 代码 export default {之前学过的配置项}</script>
-<style scoped> </style>
# 6 导入导出语法 es6
-写了一些包,想在其他js,xx.vue文件中使用
-包下新建xx.js ,里面写js代码,只能在内部用,一定要导出外部才能用
- 默认导出
export default {}
-默认导入
import 别名 from '路径'
别名.xx
-命名导出 导出多个
export const name='lqz'
-命名导入
import {name,age} from '路径'
-包内如果有index.js,导入包的时候,不用写这个文件
# 7 登录小案例
-后端---》解决跨域--》一步一步操作---》注释掉csrf---》request.post 取不到
-前端:
-使用axios,安装
-导入用即可
今日内容
1 props 的使用
# 安装依赖
cnpm install
--------------------------------------
# 做成纯净的vue项目
在router 的index.js 中删除about的路由
删除所有小组件和about页面组件
App.vue 只留
<template>
<div id="app">
<router-view/>
</div>
</template>
--------------------------------------
# 自定义属性,在子组件中接收传入的数据
# 方式一:使用数组
props:['name']
#方式二:使用对象
props: {name: Number}
# 方式三:使用对象,默认值和必填
props: {
name: {
type: String, //规定传过来的数据类型
required: true, //规定是否必须要传值
default: '老王' //如果不传,使用的默认值
}
}
# 页面组件代码
<template>
<div class="home">
<h2>props其他使用</h2>
<hr>
<Hello :msg="name"></Hello>
<hr>
</div>
</template>
<script>
import Hello from "@/components/Hello";
export default {
name: 'HomeView',
data() {
return {
name: '刘清政'
}
},
components: {
// 注册局部组件
Hello
}
}
</script>
------------------------
# 小组件代码
<template>
<div class="hello">
<button>后退</button>
小组件首页----页面组件传的变量: {{ msg }}
<button>前进</button>
</div>
</template>
<script>
export default {
name: "Hello",
// props: ['msg']
// props:{msg:Number}
props: {
msg: {
type: String, //规定传过来的数据类型
required: true, //规定是否必须要传值
default: '老王' //如果不传,使用的默认值
}
}
}
</script>
<style scoped>
</style>
要想不每次创组件的时候都弹出该弹框,直接到项目目录里,删除.git文件夹
.
.
.
.
父组件中:
.
.
.
2 混入mixin
# 功能: 可以把多个组件共用的配置提取成一个混入对象
---------------------------------------------
# 使用步骤:
1 定义混入对象,新建mixin包,下新建index.js
-------------------
2 在 index.js中写 代码(组件中会用到的,data,methods等等 配置项)
export const lqz = {
methods: {
showName() {
alert(this.name); // 没有this.name
},
},
mounted() {
console.log("你好啊!,页面挂在执行");
},
}
-------------------
3 混入的局部使用(只在当前组件中使用)
在当前组件中先导入混入对象,然后再在export default里面 写个混入的配置项
import {lqz} from '@/mixin'
export default {
name: 'HomeView',
data() {
return {
name: '刘清政' } },
mixins: [teng],
}
-------------------
4 全局使用(所有组件都可以用) main.js中
import {lqz} from '@/mixin'
Vue.mixin(lqz)
// Vue.mixin(lqz2)
// Vue.mixin(lqz3)
-------------------
5 在组件中,直接使用即可
.
.
--------------------------------------------
# 在全局导入并注册混入对象 main.js 文件
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
// 当在全局导入混入对象,并注册一下混入对象后
// 所有的页面组件都可以不用导入混入对象就可以用到混入对象里面的属性与方法
import {teng} from "@/mixin";
Vue.mixin(teng)
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
--------------------------------------------
--------------------------------------------
# 混入对象文件 mixin文件夹下的---index.js文件
// 1 定义混入对象,新建mixin包,下新建index.js
// 2 在index.js中写代码(组件中会用到的,data,methods等等配置项)
export const teng = {
data() {
return { age: 199 } },
methods: {
showName() {
alert(this.name)
// 哪个组件对象运行的该混入里面的方法,this指代的就是该组件对象
}},
mounted() {console.log("你好啊!,页面挂载执行");},
}
--------------------------------------------
--------------------------------------------
# indexView.vue 代码 没有导入与注册混入对象
<template>
<div class="home">
<h2>混入的使用</h2>
<hr>
<button @click="showName">点我看名字</button>
<h2>混入里面传过来的变量age: {{age}}</h2>
<hr>
</div>
</template>
<script>
export default {
name: 'HomeView',
data() {
return {
name: '刘清政'
}},
components: {
// 注册局部组件
},
}
// 由于在全局已经导入并注册了混入对象,所以再该页面组件了,
// 不导入混入对象,依然可以使用混入对象里面的方法与属性
</script>
--------------------------------------------
--------------------------------------------
# indexView_局部混入.vue 代码 导入与注册混入对象
<template>
<div class="home">
<h2>混入的使用</h2>
<hr>
<button @click="showName">点我看名字222</button>
<h2>混入里面传过来的变量age: {{age}}</h2>
<hr>
</div>
</template>
<script>
// 3 导入混入对象
import {teng} from "@/mixin";
export default {
name: 'HomeView',
data() {
return {
name: '刘清政'
}},
// 3 配置项里面注册一下该混入对象
mixins: [teng],
components: {
// 注册局部组件
},
}
// 混入对象的作用:在组件里可以使用到混入对象里面的属性和方法
// 该组件里可以使用该混入对象,其他组件里也可以使用该混入对象,只要导入并写到配置项里面去即可
</script>
.
在index.js中写代码(组件中会用到的data, methods。。。等的配置项),命名导出
.
全局使用(所有组件都可以使用),在main.js中。命名导入,mixin注意没有s只能注册单个,当有多个时注册多个
.
局部使用(只在当前最组件中使用)
.
.
.
.
.
.
3 插件了解
--------------------------------------------
插件的功能非常强大
功能:用于增强Vue对象的功能
本质:包含install方法的一个对象,install的第一个参数是Vue,第二个以后的参数是插件使用者传递的数据
插件的常用功能:
在插件中定义自定义指令,在插件中定义全局变量,在插件中使用全局混入,
在插件中还可以自定义组件(elementui的组件就是这么来的)
--------------------------------------------
# 使用步骤
1 新建包plugins,新建index.js
import Vue from "vue";
import axios from "axios";
export default {
install(vue) {
console.log('执行了插件', vue)
# 可以做的事
# 1 了解,自定义指令(不了解没关系)
# 2 定义全局变量,以后在任何组件中都可以使用到,
# 借助于Vue.prototype.变量名=值,往Vue原型里放 ,
# 以后所有组件要拿值,用this.$变量名 就能拿到了
# 3 使用全局混入
# 4 自定义全局组件
}
}
注意:不一定只能在插件里面,定义全局变量,其他地方也能定义全局变量
----------------------------------------
2 在main.js 中配置
# 使用自定义插件
import plugin888 from '@/plugins'
Vue.use(plugin888)
.
# 代码参考 插件index.js 代码
import Vue from 'vue'
// 1 新建包plugins,新建index.js
import axios from "axios";
export default {
install(vue) {
console.log('插件执行了', vue)
// 可以做的事
// 1 自定义了一个v-fbind指令
Vue.directive("fbind", {
// 指令与元素绑定时
bind(element, binding) {
element.value = binding.value
},
// 指令与元素插入时
inserted(element, binding) {
element.focus()
},
// 指令所在的模板被重新解析时
update(element, binding) {
element.value = binding.value
},
})
// 2 定义全局变量,以后在任何组件中都可以使用到该变量
// 借助于Vue.prototype 往Vue类里面放属性,
// 往Vue类里面放了一个$name的属性,以后在任意组件中用this.$name都可以拿到
Vue.prototype.$name = '刘清政'
Vue.prototype.$add = (a, b) => {
return a + b }
Vue.prototype.$ajax = axios
// 3 使用全局混入
Vue.mixin(
{
data(){
return {
name:'彭于晏',
age:19 }
}
}
)
}
}
----------------------------------
----------------------------------
# main.js 里面代码
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
// 2 在main.js 中配置 先导入插件 再使用插件
// 只要使用了插件,就会自动去执行插件里面的代码!
// 现在在任意组件中,就可以使用自定义的指令了,因为它是在插件里面定义的
import plugin123 from '@/plugins'
Vue.use(plugin123)
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
----------------------------------
----------------------------------
# 页面组件HomeView.vue 里代码
<template>
<div class="home">
<h2>插件的使用一</h2>
<h2>使用自定义指令</h2>
<input type="text" v-fbind:value="vvv">
<hr>
<h2>插件的使用二</h2>
<h2>定义全局变量</h2>
<button @click="handClick">点我发生ajax请求</button>
<hr>
<h2>插件的使用三</h2>
<h2>在插件中使用全局混入</h2>
<button @click="handName">点我弹出名字</button>
</div>
</template>
<script>
export default {
name: 'HomeView',
data() {
return {
vvv: 'lqz'
}
},
methods: {
handClick() {
this.$ajax('127.0.0.1:8080/sss').then(res => {
})
console.log(this.$name)
console.log(this.$add(4,5))
},
handName(){
console.log(this.name)
}
},
}
</script>
.
.
.
.
.
4 elementui样式库使用(重点)
# 在vue上,css样式,用的最多的是elementui 样式库,但是还有其他的样式库
-elementui 做网页端 样式用的多 饿了么团队基于vue2的开发的
-elementui-plus 第三方团队继续基于vue3写的
-vant 做app的样式
-iview pc端用www.iviewui.com
---------------------------------------------
---------------------------------------------
# elementui的使用
1 安装
cnpm i element-ui -S
2 配置完整引入 在 main.js 中写入以下内容
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI) // 以后在咱们组件中直接使用elementui提供的全局组件即可
3 在组件中使用
-去官网看到好的,复制贴到你的项目中
---------------------------------------------
.
.
.
.
.
5 vuex第三方插件
.
.
# 在Vue中实现集中式状态(数据)管理的一个Vue插件,对vue应用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信
# 也就是说组件中有一些变量,变量可以存在这,适用于任意组件间的通信
----------------------------------------------
----------------------------------------------
# 使用步骤
1 安装,新建store/index.js
2 在index.js 中写
export default new Vuex.Store({
state: {
# 放数据
},
mutations: {
# 放方法,正常是让actions中来调用
# 组件也可以直接调用
},
actions: {
# 放方法,正常组件调用
}
})
3 在组件中
-显示state的变量
html中:
{{$store.state.变量名}}
js中:
this.$store.state.变量名
改state中的值
-推荐按正常步骤---》this.$store.dispatch('actions中的方法',参数)
---》actions中的方法调用 context.commit('mutations中的方法',参数)---》
在mutations中直接修改state的值
-可以跨过任何一步
this.$store.commit('mutations中的方法',参数)
this.$store.state.变量名 # 再继续改变量名对应的值
----------------------------------------
----------------------------------------
# 搞了三层的目的就是可以在actions:{} 里面发ajax请求
# 如果不和后端交互,那就直接 this.$store.state 再继续点state里面定义的变量直接改了
-----------------------------------------
-----------------------------------------
# 代码示范
# 页面组件代码
<template>
<div>
<h2>vuex的使用</h2>
<h3>因为在main.js里面,Vue实例化对象是传了store,所以可以this.$store拿到vuex对象</h3>
<h3>插值语法里不需要用this点 但是在js里面需要用this.store </h3>
<button @click="add111">点我自增1</button>
------{{ $store.state.num }}
<hr>
<ul>
<li v-for="item in good_list">
商品id:{{ item.id }}----商品名:{{ item.name }}-----商品价格:{{ item.price }}-----
<button @click="addcar1(item.id)">加入购物车</button>
</li>
</ul>
<hr>
<shoppingcar></shoppingcar>
</div>
</template>
<script>
import shoppingcar from "@/component/shoppingcar";
export default {
name: 'HomeView',
data() {
return {
good_list: [
{id: 1, name: '刚笔', price: 222},
{id: 2, name: '毛笔', price: 33},
{id: 3, name: '铅笔', price: 212},
{id: 4, name: '蜡笔', price: 66},
]
}
},
methods: {
add111() {
// this.$store.state.num++ // 这样可以直接实现自增,但是不建议
// 先触发action的执行----触发store中的actions中定义的add222,并把1传给value
this.$store.dispatch('add222', 1)
},
addcar1(id){
this.$store.dispatch('addcar2',id)
// this.$store.state.good_num +=1 如果不与后端交互,可以直接掉state了
// this.$store.commit('addcar3',id) 也可以直接去触发mutations里面的函数
}
},
components:{
shoppingcar
}
}
</script>
--------------------------------
# store文件里面的index.js 里面的代码
import { createStore } from 'vuex'
export default createStore({
state: {
num:10,
good_num:5
},
mutations: {
add333(state,value){
console.log(state) // state就是上面state键对应的字典{num:10}
console.log(value) // 传过来的值
state.num = state.num + value
},
addcar3(state,id){
state.good_num = state.good_num + 1
},
},
actions: {
add222(context,value){
console.log(context) // context是传过来的一个对象
console.log(value) // 传过来的值
context.commit('add333',value) // 它会触发mutations里面的add333的执行
},
addcar2(context,id){
context.commit('addcar3',id)
// 发送ajax请求,把id携带到后端,把商品加入到购物车数据库中,id实际上是给后端用的
}
},
})
--------------------------------
# 小组件代码
<template>
<div>购物车数量: {{ $store.state.good_num }}</div>
</template>
<script>
export default {
name: "shoppingcar"
}
</script>
<style scoped>
</style>
--------------------------------
.
.
.
.
.
.
6 vue Router 第三方插件
# 第三方插件,用来实现SPA单页应用程序 的vue 插件
vue Router就是 实现在一个index.html 中有页面跳转效果的 插件
其实就是做路由控制
<router-link> 跳转用
<router-view/> 替换页面组件用
------------------------------------------
------------------------------------------
#1 基本使用
-1 创建vue项目时加入了,直接用即可
-如果之前没装:先下载,在项目中创建router包,写个index.js,从之前的模板里面把代码copy过来,然后main.js 里面写一下
-2 配置路由的跳转(就是跳转页面组件),
只需要在router包下的index.js文件里的 routes数组中写对象即可
const routes = [
{
path: '/',
name: 'index',
component: Index },
{
path: '/home',
name: 'home',
component: Home }
]
-3 一定要写个视图组件 Home
.
.
#2 点击跳转路由两种方式
# js代码控制 路由跳转
this.$router.push('/login')
# router-link标签控制 路由跳转
<router-link to="/home">
<button>点我跳转到home页面</button>
</router-link>
------------------------------------------
# 代码示范
<template>
<div class="index">
<h3>我是index页面</h3>
<button @click="goLogin">点我跳转到登录页面</button>
<hr>
<router-link to="/home">
<button>点我跳转到home页面</button>
</router-link>
</div>
</template>
<script>
export default {
name:'index',
methods:{
goLogin(){
// js代码 控制的路由跳转到login页面
this.$router.push('/login')
// this.$router 的其他方法
// this.$router.replace('/login') 也能跳转但无非回退
// this.$router.back() 退回到上一个记录路由
// this.$router.go(-1) 退回到上一个记录路由
// this.$router.go(-1) 退回到下一个记录路由
},
}
};
</script>
<style scoped>
</style>
.
.
#3 路由跳转,携带数据的两种方式
-1 /course/?pk=1 带在路径中使用 ? 携带
-2 /course/1/ 带在路径中用斜杠分割的
-----------------------
# 路由跳转后,在跳转后的页面拿到路由中携带数据的方式
-1 第一种方式:/course/?pk=1
# 一般写在跳转后的页面代码的created钩子函数里面
export default {
name: "login",
created() {
console.log('route----', this.$route)
// 从当前路由对象中取出?后携带过来的值
this.$route.query.pk # 取pk对应的值
}
}
-2 第二种方式:/course/1/
需要在router包下的index.js文件中 改路径
{
// path: '/login',
path: '/login/:id',
name: 'login',
component: Login },
用 this.$route.params.id # 取路由斜杠后id对应的值
------------------------------------------
------------------------------------------
#4 区分this.$route this.$router
-this.$router
# 在router包下index.js文件里 new VueRouter生成的对象,可以实现路由的跳转
-this.$route
# 是当前路由对象,内部有传入的参数
区分this.
.
.
从当前路由对象中取出携带过来的值
.
.
从当前路由对象中取出路由斜杠后携带过来的值
.
.
.
# 5 两种跳转方式(目的就是通过传对象的方式比传路由的方式更方便的携带数据)
-使用js代码 跳转页面 传对象方式
this.$router.push({
name:'login', // 注意此处name对应的是配在index.js里面的路由对应的名字
query:{
name:'lqz',
age:19 },
params:{id:888}
})
-标签形式跳转,传对象形式
<router-link :to="{name: 'home', query: {name: 'lqz'}, params: {id: 999}}">
<button>点我跳转到home页面</button>
</router-link>
------------------------------------------------
.
.
# 6 路由守卫--->
全局路由守卫
前置路由守卫:在进路由前,执行代码
后置路由守卫:路由跳转走,执行代码
---------------------------------------------
# 如何用路由守卫? 就是在router包下index.js文件中,加入以下代码
// 全局前置路由守卫--->任意路由跳转都会触发它的执行
router.beforeEach((to, from, next) => {
// to 是去哪,to是个路由对象
// from 是来自哪,from也是个个路由对象 比如从 /根路径 跳转到 /login 根路径对象就是to /login就是from
// next 是函数,如果加括号执行,就会真正的调到去的路由
console.log('前置路由守卫', to, from, next)
// next()
if (to.name == 'login') {
console.log('允许跳转路由')
next()
} else {
var res = localStorage.getItem('userinfo')
if (res) {
next()
} else {
alert('您没有登录')
next(name:'login')
}
}
})
# 这样就实现如果去的路由对象对应的名字是'login',那么就正常跳转到去的路由对象对应的视图上去
# 否则去localStorage里面取userinfo键对应的值,如果取到了,也正常跳转到去的路由对象对应的视图上去
# 否则不允许跳转到想要跳转的页面去!!!
.
.
.
.
.
.
.
7 localStorage系列
# 都是在浏览器存储数据的----存数据有什么用?
# 3个主要功能
登录成功 token存在本地
不登录加入购物车功能,比如迪卡侬存在了localStorage中
跨组件间通信
# 本地存数据基本就存这3个地方
------------------------------------
# localStorage
-永久存储,除非清空缓存,手动删除,代码删除
注意只有打开原来往localStorage存数据的网页,才有对应的数据,
不是说打开浏览器就能找到存在localStorage里面对应的数据的!!!
-localStorage.setItem('userinfo', JSON.stringify(this.userInfo))
-localStorage.getItem('userinfo')
-localStorage.clear() // 清空全部
-localStorage.removeItem('userinfo')
-------------------------------------
# sessionStorage
-关闭浏览器,自动清理
sessionStorage.setItem('userinfo', JSON.stringify(this.userInfo))
sessionStorage.getItem('userinfo')
sessionStorage.clear() // 清空全部
sessionStorage.removeItem('userinfo') // 只删除里面某一个键对应的值
-------------------------------------
# cookie
-有过期时间,到过期时间自动清理
-借助于第三方先下载 cnpm install vue-cookies
cookies.set('userinfo', this.userInfo,3600) # 过期时间按秒算的
cookies.get('userinfo')
cookies.remove('userInfo') // 删除cookie
-------------------------------------
-------------------------------------
-------------------------------------
# 代码参考
<template>
<div>
<h2>localStorage使用</h2>
<button @click="handleinsert">写入localStorage使用</button>
<button @click="handleget">获取localStorage使用</button>
<button @click="handlegdel">删除localStorage使用</button>
<hr>
<h2>sessionStorage使用</h2>
<button @click="handleinsert2">写入sessionStorage使用</button>
<button @click="handleget2">获取sessionStorage使用</button>
<button @click="handlegdel2">删除sessionStorage使用</button>
<hr>
<h2>cookie使用</h2>
<button @click="handleinsert3">写入cookie使用</button>
<button @click="handleget3">获取cookie</button>
<button @click="handlegdel3">删除cookie</button>
</div>
</template>
<script>
import cookies from 'vue-cookies'
export default {
name: 'HomeView',
data() {
return {
userInfo: {name: 'lqz', age: 19}
}
},
methods: {
handleinsert() {
// 写入localStorage value 必须是字符串,如果是对象或者是数组,要转成json格式字符串
window.localStorage.setItem('userInfo', JSON.stringify(this.userInfo))
},
handleget() {
// 获取localStorage的值
var res = window.localStorage.getItem('userInfo')
console.log(res, typeof (res))
var res1 = JSON.parse(res)
console.log(res1, typeof (res1))
},
handlegdel() {
// localStorage.clear() // 清空全部
localStorage.removeItem('userInfo') // 只删除localStorage里面某一个键对应的值
},
handleinsert2() {
// 写入sessionStorage value 必须是字符串,如果是对象或者是数组,要转成json格式字符串
window.sessionStorage.setItem('userInfo', JSON.stringify(this.userInfo))
},
handleget2() {
// 获取sessionStorage的值,window可以直接省略
var res = sessionStorage.getItem('userInfo')
console.log(res, typeof (res))
var res1 = JSON.parse(res)
console.log(res1, typeof (res1))
},
handlegdel2() {
// sessionStorage.clear() // 清空全部
sessionStorage.removeItem('userInfo') // 只删除sessionStorage里面某一个键对应的值
},
handleinsert3() {
// vue 中操作cookie需要借助于第三方 vue-cookies
// 写入cookie vue-cookies 帮我们做了json格式的转化与解析,所以直接放对象进去
// 不写时间参数默认是1天,参数是按秒算的
cookies.set('userInfo', this.userInfo,3600)
},
handleget3() {
// 获取cookie的值
var res = cookies.get('userInfo')
console.log(res) // 可以正常拿到对象
},
handlegdel3() {
cookies.remove('userInfo') // 删除cookie
},
},
}
</script>
.
.
.
.
.
跨组件间通信的实现方法
1 父传子 通过自定义属性
2 子传父 通过自定义事件
3 父传子与子传父 通过ref属性
4 父传子与子传父 通过localStorage,sessionStorage,cookie
5 父传子与子传父 通过vuex
.
.
.
.
作业
1 自定义混入测试
2 自定义插件,把axios放入到vue的原型中
3 研究elementui,写个页面
4 整理vuex的使用
5 整理vueRouter的使用
6 整理localStorage。。。使用
----------------------
7 uni-app----》写个安卓app,有个按钮,点击随机切换图片
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY