UNPKG

metadesktop

Version:

js元桌面 基于vue的框架,实现web桌面系统

810 lines (725 loc) 26.5 kB
# 更新 #### 1. 引入文件 import { metadesktop } from 'metadesktop' 修改为 import { metadesktop } from MetaDesktop' # 介绍 **metadesktop**是一个基于vue3的框架,中文名称**云桌面**,一个用于在web端实现与window系统类似的桌面管理系统 ## 开始使用 ### 安装 **metadesktop**框架依赖于**vue3**构建工具,在使用之前,确保自己的vue版本为3及以上版本 ``` npm install metadesktop ``` ### 在vue3项目中引入 在vue项目的入口文件main.js中引入两个相关依赖文件,metadesktop.js和metadesktop.css ```javascript import { MetaDesktop } from 'metadesktop' import 'metadesktop/dist/metadesktop.css' ``` 使用 Vue.use 进行安装 ```javascript createApp(App).use(MetaDesktop).mount('#app') ``` 如果你使用了其他第三方插件,并不会对系统产生影响,可以放心使用,比如下面使用了vuex和vue-router ```javascript createApp(App).use(metadesktop).use(store).use(router).mount('#app') ``` main.js 完整代码: ```javascript import { createApp } from 'vue' import App from './App.vue' import router from './router' import store from './store' import { metadesktop } from 'metadesktop' import 'metadesktop/dist/metadesktop.css' createApp(App).use(metadesktop).use(store).use(router).mount('#app') ``` ### 创建桌面 **metadesktop**内置了一个MetaDesktop组件,他是**metadesktop**系统的入口组件,我们在App.vue中使用它 ```html <template> <div id="app"> <MetaDesktop/> </div> </template> ``` 我们使用浏览器查看当前项目,会发现桌面上可能什么也没有,这是因为MetaDesktop组件的大小依赖于父元素的大小,我们给 id="app"的div设置css属性,让他铺满整个浏览器窗口 ```css <style lang="less"> #app{ width: 100vw; height: 100vh; } </style> ``` 我们重新打开浏览器,如果看到左下脚的window图标,就证明创建桌面成功了。此时的桌面一片空白,是因为我们还没有加入我们自己的桌面图标,后面会详细介绍。 App.vue 完整代码: ```html <template> <div id="app"> <MetaDesktop/> </div> </template> <script> export default { } </script> <style lang="less"> #app{ width: 100vw; height: 100vh; } </style> ``` ### Application应用类、ApplicationManage管理类和 MetaWindow 窗口类 我们先来看一个例子,以此来了解这两个类。 在app.vue组件的生命周期函数 mounted 中加入如下代码。 ```javascript let appManege = this.$System.getApplicationManage(); appManage.addApplicationWeb({ title:"哔哩哔哩", icon: "https://www.bilibili.com/favicon.ico?v=1", href :"https://www.bilibili.com/" }) ``` 如果你是国内的话,你可以看到,我们创建的桌面上出现了哔哩哔哩的图标,点击图标,弹出了一个窗口,内容为哔哩哔哩网站的内容。如果是在国外,你可以随意找一个网站。 上面的代码,通过$System的getApplicationManage函数获取了一个ApplicationManage对象,不难猜出ApplicationManage对象是用来管理应用的。 Application对象是用来生成 MetaWindow 窗口的,所以需要创建一个MetaWindow 窗口之前,就需要创建一个Application对象。Application对象生成窗口需要依赖一个vue的组件,以组件内容生成窗口,而上面可以直接使用网站生成窗口,是因为内部已经默认使用了一个Web组件,通过iframe来加载网页。 Application对象生成窗口会返回一个MetaWindow 对象,通过MetaWindow 对象可以操作生成的MetaWindow窗口,在组件中,可以通过props获取到对应的MetaWindow 对象。 #### 1.ApplicationManage ApplicationManage里需要关注的方法只有三个:addApplication,addApplicationWeb,remove。 addApplication和addApplicationWeb都是用来创建应用,而remove是用来移除应用,addApplication需要传递一个组件对象,而addApplicationWeb不需要,因为addApplicationWeb是addApplication的一个特例,内部已经使用了Web组件,Web组件是内部的一个用来加载网页组件。 addApplication ```javascript // addApplication(Object component,Bool showDesktop) // component: vue组件 // showDesktop:可选, 应用图标是否显示在桌面上 // return: Application 对象 ``` addApplicationWeb ```javascript // addApplicationWeb(Object value,Bool showDesktop) // value: 传递值对象,title:标题,icon:图标,href:网址 // showDesktop: 应用图标是否显示在桌面上 // return: Application 对象 ``` remove ```javascript // remove(Application application) // application: 需要移除的应用对象 ``` #### 2.Application Application对象里,需要现在关注的只有一个:open,用来启动一个新窗口,一个应用可以启动多个窗口,不过因为默认只能启动一个窗口,所以现在只能启动一个,想要启动多个,后面会提到,open按需要可以传递一个值对象。 open ```javascript // open(Object value) // value:可选,传递值对象 // return: MetaWindow 对象 ``` #### 3.MetaWindow MetaWindow 里需要关注的东西有点多,有窗口配置WindowConfig,关闭窗口、隐藏窗口、全屏窗口函数,事件Event。 (1). WindowConfig对象里有很多属性,用来设置窗口,所有属性看下面的表。设置WindowConfig推荐在组件中设置。 ```javascript export default { windowConfig:{ windowWidth:"600px", windowHeight: "90%", showBottomBar: false, showTitleBar: false, isBoxShadow: false, isOverflow: false, showAddmin: false, showDesktop: false, }, data() { return { } }, mounted(){}, } ``` ##### 属性 | 属性名 | 描述 | 类型| 默认值| |--|--|--|-- | icon | 窗口的图标,是一个资源地址或svg代码 | string | 默认图标 | icon_svg | 是否启用svg图标,如果启用,icon的内容应该是svg代码而非资源地址 |bool|false |showTitleBar |是否显示tar栏|bool|true | title | 窗口名称 |string|null | titleBarBackground | 标题栏背景 |string|null |titleBarFontColor|标题字体颜色|string|null | titleBarFontSize | 标题字体大小,必须带单位 |string|null | titleBarFontWeight | 标题字体粗细 |string|null |titleBarFontFamily |标题字体|string|null | titleBarCancelBackground | 标题栏 关闭窗口按钮 背景色 |string|null | titleBarFullScreenBackground | 标题栏 全屏窗口按钮 背景色 |string|null |titleBarHideBackground|标题栏 隐藏窗口按钮 背景色|string|null | windowTop | 窗口距离桌面顶部位置,必须带单位 |string|null | windowLeft | 窗口距离桌面左边位置,必须带单位 |string|null |windowBottom |窗口距离桌面低部位置,必须带单位 |string|null | windowRight | 窗口距离桌面右边位置,必须带单位 |string|null | windowWidth | 窗口宽度,必须带单位 |string|null |windowHeight | 窗口高度,必须带单位 |string|null | windowMaxWidth | 窗口最大宽度,必须带单位 |string|null | windowMaxHeight | 窗口最大高度,必须带单位 |string|null |windowMinWidth | 窗口最小宽度,必须带单位|string|null | windowMinHeight | 窗口最小高度,必须带单位 |string|null |maxCountWindow|应用最大可打开窗口数量 |number|1 | fullScreen | 窗口是否全屏 |bool|false |isBoxShadow|是否使用用窗口阴影|bool|true |isBoxShadow|是否对超出窗口部分进行裁剪 |bool|true |isReSetSize|是否可以更改窗口大小 |bool|true |isSizeZero|窗口大小可以为0 |bool|false |menu|窗口启动时底部导航栏显示菜单 |array|[] |appMenu|桌面应用图标显示菜单 |array|[] |showAddmin|应用图标是否显示到应用管理界面 |bool|true |showDesktop|应用图标是否显示到桌面 |bool|true |showBottomBar|应用图标是否显示到底部导航栏 |bool|true |isCloseDestroy|是否在应用应窗口全部关闭后销毁图标 |bool|false (2). 关闭窗口close、隐藏窗口hide、全屏窗口fullScreen ```javascript let appManege = this.$System.getApplicationManage(); let app = appManage.addApplicationWeb({ title:"哔哩哔哩", icon: "https://www.bilibili.com/favicon.ico?v=1", href :"https://www.bilibili.com/" }) let metaWindow = app.open(); // 关闭窗口 metaWindow.close(); // 隐藏窗口 metaWindow.hide(); // 显示窗口 metaWindow.hide(false); // 全屏窗口 metaWindow.fullScreen(); // 取消全屏窗口 metaWindow.fullScreen(false); ``` 上面代码中的例子可以一个一个的试一下,暂时可以以定时器来进行调用。 ```javascript let appManege = this.$System.getApplicationManage(); let app = appManage.addApplicationWeb({ title:"哔哩哔哩", icon: "https://www.bilibili.com/favicon.ico?v=1", href :"https://www.bilibili.com/" }) let metaWindow = app.open(); // 隔2秒后关闭窗口 setTimeout(()=>{ metaWindow.close(); },2000) ``` (3.) 事件Event 事件用于对窗口的一些特定的行为进行监听,如关闭窗口close、隐藏窗口hide、全屏窗口fullScreen,监听的是执行动作之前,所以可以利用这对默认行为进行拦截。 ```javascript let appManege = this.$System.getApplicationManage(); let app = appManage.addApplicationWeb({ title:"哔哩哔哩", icon: "https://www.bilibili.com/favicon.ico?v=1", href :"https://www.bilibili.com/" }) let metaWindow = app.open(); metaWindow.on("onClose",()=>{ console.log("检查到窗口即将关闭"); }) metaWindow.on("onHide",({newV})=>{ if(!newV) console.log("检查到窗口即将隐藏"); else console.log("检查到窗口即将显示"); }) metaWindow.on("onFullScreen",({newV})=>{ if(newV) console.log("检查到窗口即将全屏"); else console.log("检查到窗口即将取消全屏"); }) ``` onClose回调函数不会传递任何值,而onHide和onFullScreen会传递一个值对象,可以根据值对象里的属性newV和oldV,判断隐藏和全屏的状态。 自定义事件 如果需要自定义一个事件,可以用 emit 方法,如果你对vue熟悉,应该对emit 很熟悉。 ```javascript let appManege = this.$System.getApplicationManage(); let app = appManage.addApplicationWeb({ title:"哔哩哔哩", icon: "https://www.bilibili.com/favicon.ico?v=1", href :"https://www.bilibili.com/" }) let metaWindow = app.open(); // 自定义 onCloseTime 事件,事件名称可以根据自己需要定义 metaWindow.on("onCloseTime",(res)=>{ console.log("关闭时间",res); }) metaWindow.on("onClose",()=>{ // 触发 onCloseTime 事件,并传递一个值 metaWindow.emit("onCloseTime",new Date().getTime()); }) ``` 下面,我们通过一个例子来看看创建窗口的完整流程,以及metaWindow的使用。 首先,创建一个vue组件Test.vue,放到component文件夹下面,内容如下: ```html <!-- Test.vue --> <template> <div class="test"> <button @click="metaWindow.close()">关闭窗口</button> <button @click="metaWindow.fullScreen(true)">全屏</button> <button @click="metaWindow.fullScreen(false)">取消全屏</button> <button @click="metaWindow.hide(false)">隐藏</button> <button @click="metaWindow.hide(true)">取消隐藏</button> <button @click="metaWindow.emit('message','傻子')">自定义事件message</button> </div> </template> <script> export default { // 在此处配置窗口属性 windowConfig:{ windowWidth : "600px", windowHeight : "600px", windowTop : "100px", windowLeft : "100px", title:"测试", icon: require("@/assets/test.png") }, props:["metaWindow"],// 通过props 获取到 metaWindow对象 mounted() { this.metaWindow.on("onClose",()=>{ console.log("检查到窗口即将关闭"); }) this.metaWindow.on("onHide",({newV})=>{ if(!newV) console.log("检查到窗口即将隐藏"); else console.log("检查到窗口即将显示"); }) this.metaWindow.on("onFullScreen",({newV})=>{ if(newV) console.log("检查到窗口即将全屏"); else console.log("检查到窗口即将取消全屏"); }) // 自定义事件 message this.metaWindow.on("message",(msg)=>{ console.log(msg); }) }, } </script> <style> .test{ width: 100%; height: 100%; background: white; } </style> ``` 在app.vue中引入Test组件,并且使用Test组件 ```html <template> <div id="nav"> <MetaDesktop/> </div> </template> <script> import Test from "@/components/Test.vue" export default { data(){ return {} }, mounted(){ let app = this.$System.getApplicationManage(); app.addApplication(Test); } </script> <style> body{ position: relative; width: 100vw; height: 100vh; } #nav{ position: relative; width: 100vw; height: 100vh; } </style> ``` 然后启动项目,点击桌面上的测试图标,打开一个窗口,内容为 Test.vue 的内容,点击几个按钮,感受一下吧。 ### 子窗口ChildMetaWindow 在浏览器里,很多时候我们页面切换的方式都是通过地址栏,然而,在Metadesktop的系统里,我们可以打开很多个功能不同的应用,它们各自有着不同的页面需要切换,这个时候,一个地址栏就不够用了。 那我们应该如何解决呢? Metadesktop的系统提供了两种方法,子窗口和页面路由,两个可以单独使用也可以混合使用,我们先来看看如何编写子窗口。 首先需要清楚的一点是,子窗口是存在于MetaWindow窗口中,我们先创建一个MetaWindow窗口。 在components文件夹下创建两个文件childWindowHome.vue和selectAddress.vue 在selectAddress.vue写入如下代码: ```html <template> <div class="select-address"> <ul> <li v-for="item in addressList" :key="item" @click="select(item)">{{item}} </li> </ul> </div> </template> <script> export default { childWindowConfig:{ title:"选择地址", titleBarTitlePosition:"center" }, inject:["childMetaWindow"], data() { return { addressList:["云南","贵州","广州","上海","重庆"] } }, methods: { select(address){ this.childMetaWindow.value.address = address; this.childMetaWindow.emit("close"); } }, } </script> <style scoped> .select-address{ width: 100%; height: 100%; background: white; } .select-address ul li{ padding: 10px 20px; transition: 0.25s; cursor: pointer; } .select-address ul li:hover{ background: whitesmoke; } </style> ``` 在MetaWindow窗口中,使用了windowConfig来进行设置窗口属性,而在子窗口中,通过childWindowConfig来设置。 在childWindowHome.vue中写入如下代码 ```html .<template> <div class="child-window-home"> <ul> <li><h3>下单确认</h3></li> <li><button @click="toSelectAddress">选择地址</button><p>{{address}}</p> </li> <li><h5>商品列表</h5></li> <li><span>草莓</span><span>x1</span></li> <li><span>苹果</span><span>x1</span></li> <li><span>梨</span><span>x1</span></li> </ul> </div> </template> <script> import selectAddress from "@/components/selectAddress.vue"; export default { windowConfig:{ windowWidth: "300px", windowHeight: "500px", title:"子窗口测试", icon: "",//写入自己的图片路径 }, data() { return { address:"" } }, inject:["System"], mounted() { }, methods: { toSelectAddress(){ let w = this.System.push(selectAddress,{ address: this.address }).addEvent("onClose",()=> { this.address = w.value.address; }) } }, } </script> <style scoped> .child-window-home{ width: 100%; height: 100%; background: whitesmoke; } .child-window-home ul li{ display: flex; padding: 5px; } .child-window-home ul li h3{ text-align: center; } .child-window-home ul li span{ width: 50%; } .child-window-home ul li button{ margin-right: 80px; } .child-window-home ul li >span:last-child{ text-align: right; } </style> ``` 写好了这两个文件,然后我们把 childWindowHome.vue 使用桌面图标来启动,这里就不介绍如何使用桌面图标来启动,记不得可以回去看看如何创建桌面图标的教程。 然后我们点击图标,打开 childWindowHome 组件的窗口页面,点击选择地址按钮,弹出了一个子窗口,内容是我们 selectAddress 组件中的内容,点击任意一个地址,就回到了主窗口的位置,我们可以看到,在选择按钮的右边出现了我们刚刚点击的地址,这个例子展示了如何创建子窗口、主窗口如何与子窗口通信以及如何关闭子窗口。 有了上面的例子,对子窗口就有了一定的认识,接下来我们来仔细的认识子窗口。 操作子窗口,并不需要像主窗口一样,需要获取一个管理类,而是在主窗口生成时就会自动创建一个ChildSystem 对象,这个对象通过vue的inject注入功能获取,属性名为System,要和全局的$System区分开。 ```javascript export default { inject:["System"], } ``` ChildSystem 对象里面有五个方法push,replace,pop,back,remove用来对子窗口进行生成和删除。 ### 应用功能管理类--ApplicationManage 我们一开始的时候已经用过该类,创建桌面图标的时候。现在我们来详细讲解一下该类。 我们知道,通过$System我们可以拿到该类的实例 ```javascript const appManage = this.$System.getApplicationManage(); ``` 我们来看看它具体有哪些方法供我们使用 1. add ```javascript // add(name,icon,model, component) // name(必须): 应用名称 // icon(必须): 应用图标 // model(必须): 模式,0-组件模式,1-web模式,2-自定义模式。组件模式可以把传入的组件渲染成窗 口,web模式把网络地址对应页面渲染成窗口,自定义模式根据自己需求进行定制 // component(可选): vue组件 // 返回值: Application对象 import Test from "@/components/Test.vue" appManage.add('Test',require("@/assets/test.png"),Test); ``` 如果模式为web模式或者自定义模式,请使用返回的Application对象更改。 如果是web模式,调用setValue方法设置地址、图标、标题,这几个属性是MetaWindow窗口的图标、标题,上面设置的是桌面的图标和标题 ```javascript Application.setValue({ href:'https://git-scm.com/', icon:'https://git-scm.com/images/logo@2x.png', title:'git' }); ``` 如果是自定义模式,调用setAction,指定点击图标的回调函数 ```javascript Application.setAction(()=>{ // 点击图标需要执行的代码 }); ``` 2. addComponent ```javascript // addComponent(component) // component(必须): vue组件 // 返回值: Application 对象 import Test from "@/components/Test.vue" appManage.addComponent(Test); ``` 该方法的默认模式为0。只需要传入一个组件,所有的配置通过组件内部的windowConfg进行配置。 ```javascript export default{ name:"Test", windowConfig:{ title: "Test", icon: require'@/assets/test.png') } } ``` 3. addWeb ```javascript // addWeb(value) // value(必须): 配置:地址、图标、标题 // 返回值: Application 对象 appManage.addWeb({ href:'https://git-scm.com/', icon:'https://git-scm.com/images/logo@2x.png', title:'git' }); ``` 该方法的默认模式为1 4. addCustom ```javascript // addCustom(object value,function action) // value:一个js的数据对象,用于指定图标和名称 // action:当我们点击桌面图标时触发的函数 // 返回值: Application 对象 appManage.addCustom({ title:"测试", icon: require("@/assets/01.png") // 图标换成你自己的即可 },function(){ console.log("我被点击了"); }); ``` 该方法的默认模式为2### 桌面背景管理类--DesktopBackgroundManage 这个类用于对桌面的背景进行设置,可以设置成纯色背景、渐变背景、图片背景、动态背景(视频背景),四种方式。 下面,我们来一一介绍这四种方式。 1. 纯色背景 首先,通过$System拿到DesktopBackgroundManage管理类 ```javascript const dBManage = this.$System.DesktopBackgroundManage(); ``` 然后通过管理类里的方法setColor来设置背景色 ```javascript // setColor(color) // color: 颜色值 dBManage.setColor("pink"); // 定义自己的颜色就行 ``` 设置完颜色,我们还需要设置背景模式为纯色模式 ```javascript // setModel(model) // model: 背景模式,0:纯色模式,1:渐变模式,2:图片模式,3:动态模式 dBManage.setModel(0); ``` 如果需要把颜色重置为默认值,可以使用defaultColor方法 ```javascript // defaultColor() dBManage.defaultColor(); ``` 2. 渐变模式 通过管理类里的方法setRradientColor设置渐变色 ```javascript // setRradientColor(angle,...color) // angle: 渐变的角度 // ...color: 渐变的颜色,可以是一个或多个值 dBManage.setRradientColor(45,"rgb(204, 251, 255)","rgb(239, 150, 197)"); ``` 然后设置背景模式为渐变模式 ```javascript // setModel(model) // model: 背景模式,0:纯色模式,1:渐变模式,2:图片模式,3:动态模式 dBManage.setModel(1); ``` 如果需要单独设置渐变角度,可以使用setRradientAngle方法 ```javascript // setRradientAngle(angle) // angle: 渐变的角度 dBManage.setRradientAngle(50); ``` 如果需要把渐变色重置为默认值,可以使用defaultRradientColor方法 ```javascript // defaultRradientColor() dBManage.defaultRradientColor(); ``` 3. 图片背景 需要使用pushImage方法将图片路径加入图片管理数组中 ```javascript // pushImage(src) // src: 图片路径 dBManage.pushImage(require'@/assets/bg.jpg')) ``` 加入后把背景模式设置为图片模式 ```javascript // setModel(model) // model: 背景模式,0:纯色模式,1:渐变模式,2:图片模式,3:动态模式 dBManage.setModel(2); ``` 因为图片可能有很多张,可以使用setImageIndex方法进行图片切换 ```javascript // setImageIndex(index) // index: 图片在数组中的索引 dBManage.setImageIndex(1); ``` 有的时候,需要清空图片数组,可以使用clearImage方法 ```javascript // clearImage() dBManage.clearImage() ``` 如果需要把重置图片设置,可以使用defaultImage方法 ```javascript // defaultImage() dBManage.defaultImage(); ``` 4. 动态背景 需要使用pushVideo方法将图片路径加入图片管理数组中 ```javascript // pushVideo(src) // src: 视频路径 dBManage.pushVideo(require'@/assets/bg.MP4')) ``` 加入后把背景模式设置为动态模式 ```javascript // setModel(model) // model: 背景模式,0:纯色模式,1:渐变模式,2:图片模式,3:动态模式 dBManage.setModel(3); ``` 因为视频可能有很多,可以使用setVideoIndex方法进行图片切换 ```javascript // setVideoIndex(index) // index: 视频在数组中的索引 dBManage.setVideoIndex(1); ``` 有的时候,需要清空视频数组,可以使用clearVideo方法 ```javascript // clearVideo() dBManage.clearVideo() ``` 如果需要把重置动态设置,可以使用defaultVideo方法 ```javascript // defaultVideo() dBManage.defaultVideo(); ``` ### 底部导航栏管理类--BottomBarManage 需要通过$system中的getBottomBarManage方法获取该类。这个类可以更改底部导航栏的背景色、激活图标背景色。 1. setBackground ```javascript // 修改背景色 // setBackground(color) // color: 需要设置的颜色 const bottomBarManage = this.$System.getBottomBarManage(); bottomBarManage.setBackground("#fff"); ``` 2. setIconBackground ```javascript // 修改激活图标背景色 // setIconBackground(color) // color: 需要设置的颜色 const bottomBarManage = this.$System.getBottomBarManage(); bottomBarManage.setIconBackground("#fff"); ``` 如果需要对设置进行恢复默认值,可以使用defaultSetting方法 ```javascript // 恢复默认值 // defaultSetting() const bottomBarManage = this.$System.getBottomBarManage(); bottomBarManage.defaultSetting(); ``` ### 系统通知管理类--NoticeManage 在MetaDesktop的右下角,有一个留言样式的图标,我们点击它,会弹出一个窗口,而NoticeManage类就是用来操作该窗口。这个类能操作的有三个方法setBackground、add和close。setBackground用来改变窗口的背景色,而add是用来添加一条通知,添加通知需要一个通知组件模板,close用来移除通。通过$System 的getNoticeManage方法获取该管理类 1. setBackground ```javascript // setBackground(color) // color: 颜色 const noticeManage= this.$System.getNoticeManage(); noticeManage.setBackground("#00ff00"); ``` 2. add ```javascript // add(value,component) // value: 需要传递到模板中的值 // component: 组件模板,默认值为内置的一个通知模板,需要传递title和msg // 返回值:id,通知唯一标识id const noticeManage= this.$System.getNoticeManage(); noticeManage.add({ title:"重要提醒!", msg:"数据库v那就是看到女" }); ``` 在模板组件内使用props来获取传递的value值 ```javascript export default { props:["value"] } ``` 3. close 添加了通知,close函数把通知从通知列表里移除,close需要传递一个id值,这个id是add的时候返回的。 ```javascript // close(id) // id: 通知唯一标识id const noticeManage= this.$System.getNoticeManage(); let id = noticeManage.add({ title:"重要提醒!", msg:"数据库v那就是看到女" }); //隔5s关闭该条通知 setTimeout(()=>{ noticeManage.close(id); },5000) ``` 在模板组件内部,可以通过props和inject获取id和close函数 ```javascript export default { props:["value","id"], inject:["close"], } ``` # 联系方式 #### 邮件:webappmall@163.com