181 Commits
Author SHA1 Message Date
芋道源码 fa6ec8f07f !338 修复公众号-素材管理 上传时缺少accountId参数
Merge pull request !338 from Evol郑天/N/A
2023-11-30 12:38:24 +00:00
芋道源码 c322959c40 !337 crm联系人review修改
Merge pull request !337 from zyna/master
2023-11-29 05:54:39 +00:00
Evol郑天 fd266516fe 修复公众号-素材管理 上传时缺少accountId参数
Signed-off-by: Evol郑天 <jpevol@163.com>
2023-11-29 01:24:42 +00:00
zyna ed22e02b5b crm联系人review修改 2023-11-27 23:44:13 +08:00
芋道源码 57b20f1810 !336 同步公众号粉丝接口调用错误
Merge pull request !336 from Evol郑天/N/A
2023-11-27 12:35:25 +00:00
芋道源码 038f3d6ab0 !335 crm联系人review修改
Merge pull request !335 from zyna/master
2023-11-27 12:30:20 +00:00
Evol郑天 633965b07d 同步公众号粉丝接口调用错误
Signed-off-by: Evol郑天 <jpevol@163.com>
2023-11-27 07:07:32 +00:00
zyna 46a8755c93 联系人review修改 2023-11-26 21:54:50 +08:00
芋道源码 8aa17b673b !331 菜单右上角加一个搜索图标
Merge pull request !331 from boide/search-icon
2023-11-25 10:50:20 +00:00
芋道源码 54897b6d1b !332 同步最新的代码到 dev
Merge pull request !332 from 芋道源码/master
2023-11-25 02:27:11 +00:00
芋道源码 c6192a1918 !329 完善团队成员公共组件,crm-客户:集成团队成员
Merge pull request !329 from puhui999/dev-crm
2023-11-25 02:24:45 +00:00
Marvin b95b8008f7 feat: 菜单右上角加一个搜索图标 2023-11-25 03:52:59 +08:00
芋道源码 8f97155b43 !328 商城装修
Merge pull request !328 from 疯狂的世界/dev
2023-11-24 14:41:12 +00:00
owen c3cad3f3ee 商城:修复会员统计时间范围选择昨天时, 会强制查询两天的问题 2023-11-24 20:54:47 +08:00
YunaiV 1744c6ec6f websocket 重新封装 2023-11-24 00:33:05 +08:00
puhui999 f8b56a8c46 crm-客户:公海数据区分 2023-11-23 09:35:59 +08:00
owen 253401ace3 营销:适配商城装修组件【优惠券】 2023-11-22 16:58:00 +08:00
puhui999 12b59200c1 crm-客户:集成团队成员 2023-11-22 15:45:10 +08:00
puhui999 19a0a83054 crm-数据权限:完善团队成员公共组件 2023-11-22 15:44:42 +08:00
YunaiV f74ddf46ae sms:移除 sendCode、sendMsg 字段 2023-11-21 22:53:49 +08:00
owen 1bb9df7b3c 商品中心:修复商品表格选择对话框,在多选时表头内的多选框不显示选中效果的问题 2023-11-21 10:27:29 +08:00
owen e0a731dd86 商品评论、优惠券模板复用组件【商品橱窗】 2023-11-20 10:53:02 +08:00
owen 3198688eb5 营销:适配商城装修组件【商品栏】 2023-11-19 18:27:25 +08:00
芋道源码 b0b669129b !314 会员中心商品收藏
Merge pull request !314 from niou233/featrue/mall_org
2023-11-18 16:32:02 +00:00
芋道源码 4423e9b804 !326 CRM 客户最新的代码
Merge pull request !326 from 芋道源码/dev
2023-11-18 16:30:42 +00:00
YunaiV ebb19cfe8c Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into dev 2023-11-19 00:30:02 +08:00
YunaiV b1f5ad55b8 crm:code review 权限列表 2023-11-19 00:28:22 +08:00
芋道源码 2e01944df9 !322 crm-团队成员:完善团队成员通用组件封装
Merge pull request !322 from puhui999/dev-to-dev
2023-11-18 16:13:06 +00:00
芋道源码 5f0b0bd21d !324 商城装修
Merge pull request !324 from 疯狂的世界/dev
2023-11-18 14:18:12 +00:00
芋道源码 9d7c578d48 !323 update src/views/Profile/components/UserSocial.vue.
Merge pull request !323 from 口口口/N/A
2023-11-18 14:17:55 +00:00
YunaiV 526172af60 crm:code review 客户配置管理 2023-11-18 22:11:42 +08:00
芋道源码 c06223decd !316 feat: CRM 客户公海配置 + CRM 客户限制 + review 修改
Merge pull request !316 from wanwan/dev
2023-11-18 13:45:48 +00:00
芋道源码 f07d37438d !321 修复滑块验证码无任何滑动直接点击验证时,x坐标空,导致后端NPE的问题
Merge pull request !321 from 柯杨/master
2023-11-18 13:43:22 +00:00
YunaiV 85e953b920 crm:code review 联系人管理 2023-11-18 21:08:00 +08:00
YunaiV 7c5ae17d04 Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into dev
# Conflicts:
#	src/components/DiyEditor/components/ComponentContainer.vue
2023-11-18 20:39:31 +08:00
芋道源码 538783ce2d !317 crm联系人前端
Merge pull request !317 from zyna/master
2023-11-18 12:35:34 +00:00
YunaiV f1e3eccc8a Merge branch 'feature/sub-table' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 2023-11-18 18:00:41 +08:00
YunaiV 652698b871 代码生成:增加说明文档 2023-11-18 18:00:19 +08:00
芋道源码 26dba8b457 !325 新增主子表、树表的代码生成
Merge pull request !325 from 芋道源码/feature/sub-table
2023-11-18 09:57:33 +00:00
owen f5d611968f Merge remote-tracking branch 'origin/dev' into dev 2023-11-17 21:51:42 +08:00
YunaiV a42d0695fe 代码生成:增加主子表 erp 模式的示例 2023-11-17 13:21:17 +08:00
口口口 68f6683e52 update src/views/Profile/components/UserSocial.vue.
在这里绑定,绑定成功后,列表重复

Signed-off-by: 口口口 <17975121@qq.com>
2023-11-17 05:17:17 +00:00
owen b4a1772584 营销:适配商城装修组件【商品卡片】 2023-11-17 10:14:26 +08:00
YunaiV 6c405ba420 代码生成:增加主子表 inner 模式的示例 2023-11-16 23:48:20 +08:00
YunaiV 9aca49c1ff 代码生成:增加主子表 normal 模式的示例 2023-11-16 23:24:03 +08:00
YunaiV bbc37613b6 代码生成:增加 tree 树形的示例 2023-11-16 20:39:12 +08:00
YunaiV 3d0c4f4422 代码生成:支持树形表结构的配置 2023-11-16 00:11:23 +08:00
YunaiV 40b1522ed0 代码生成:增加 crud 单表示例 2023-11-15 23:48:00 +08:00
Wanwan 22206b25e1 feat: CRM 客户限制 2023-11-15 21:55:44 +08:00
xingyu 821c804189 fix: stylelint 2023-11-14 21:56:54 +08:00
xingyu 249e0d21a3 fix: eslint 2023-11-14 21:56:02 +08:00
xingyu 8a74b954d0 docs: update deps 2023-11-14 21:53:30 +08:00
xingyu e56afbf80f chore: update deps 2023-11-14 21:49:49 +08:00
puhui999 8e0cf5c1e5 crm-团队成员:新增获得权限级别名称函数 getLevelName 2023-11-14 12:06:42 +08:00
puhui999 ba702d03bc Merge remote-tracking branch 'yudao/dev' into dev-to-dev 2023-11-14 11:26:41 +08:00
puhui999 015a1640c1 crm-团队成员:完善团队成员通用组件封装 2023-11-14 11:26:07 +08:00
YunaiV 45656562a8 代码生成:支持设置主子表的信息 2023-11-14 09:50:54 +08:00
柯杨 8bb9dec328 【bugfix】:修复滑块验证码无任何滑动直接点击验证时,x坐标空,导致后端NPE的问题
Signed-off-by: 柯杨 <key5230@163.com>
2023-11-14 01:43:59 +00:00
芋道源码 c1a92ba22a !319 商城装修
Merge pull request !319 from 疯狂的世界/dev
2023-11-14 00:31:55 +00:00
puhui999 27c9a0f32d crm-团队成员: 先提交一波 2023-11-13 17:49:27 +08:00
puhui999 2385cd9b9a fix:如果已经到重新登录页面则不进行弹窗提示 2023-11-13 15:48:21 +08:00
YunaiV 999d8de9e9 代码生成:增加主子表(内嵌)的示例 2023-11-13 13:57:15 +08:00
YunaiV f0f7886025 代码生成:增加主子表(普通)的示例 2023-11-12 22:33:38 +08:00
YunaiV 72c5eaae31 代码生成:增加 one 情况下的示例代码 2023-11-12 19:50:38 +08:00
owen 49ebadd748 营销:适配商城装修组件【广告魔方】 2023-11-12 19:29:24 +08:00
Wanwan 702f23e672 feat: CRM 客户限制 2023-11-12 14:34:25 +08:00
zyna ed26142bb0 crm联系人init 2023-11-11 23:35:18 +08:00
Wanwan a7f86dfd8b feat: CRM 客户限制 2023-11-11 21:06:11 +08:00
Wanwan 6369b334e3 feat: CRM 客户限制 2023-11-11 20:50:33 +08:00
YunaiV 55f6e4ca25 主子表:确认列表编辑的支持组件情况 2023-11-10 23:01:36 +08:00
Wanwan b63e2a9efb Merge remote-tracking branch 'origin/dev' into dev 2023-11-10 22:47:34 +08:00
YunaiV 9f9e0f8bda 主子表:同步三种模式的代码 2023-11-10 19:54:55 +08:00
niou233 af5d1cf25e update src/locales/zh-CN.ts.
Signed-off-by: niou233 <2922564446@qq.com>
2023-11-10 11:36:11 +00:00
niou233 1ab5b44254 update src/locales/en.ts.
Signed-off-by: niou233 <2922564446@qq.com>
2023-11-10 11:35:54 +00:00
niou233 2ff97758fd update src/hooks/web/useMessage.ts.
Signed-off-by: niou233 <2922564446@qq.com>
2023-11-10 11:34:32 +00:00
YunaiV 86d9a97ccb 主子表:暂时存储,参数校验-表单数据跑通 2023-11-10 08:51:01 +08:00
YunaiV d45ebd4fcf 主子表:暂时存储,样式跑通 2023-11-10 00:20:00 +08:00
YunaiV 4e413a10e6 主子表:暂时存储,准备重构 2023-11-09 23:10:54 +08:00
owen 55b477acdb unplugin-auto-import:排除自动导入店铺装修组件 2023-11-09 15:48:20 +08:00
芋道源码 6160c2fa8a !315 商城装修
Merge pull request !315 from 疯狂的世界/dev
2023-11-09 01:30:21 +00:00
YunaiV e9299c6710 多租户:登录界面,根据 host 域名获取对应的租户编号 2023-11-09 09:20:21 +08:00
YunaiV a08e9bea7c 多租户:调整 tenant 表的 domain 字段为 website,避免 dm 数据库的关键字冲突 2023-11-09 09:20:21 +08:00
owen 82aed175ab 营销:适配商城装修组件【菜单导航】 2023-11-09 09:18:46 +08:00
Wanwan 59b2d89a18 feat: CRM 客户公海配置 2023-11-09 00:08:09 +08:00
Wanwan 288aa72cdd feat: CRM 客户公海配置 2023-11-09 00:05:10 +08:00
owen ac42493659 营销:适配商城装修组件【宫格导航】 2023-11-08 22:22:35 +08:00
owen 325e2e4ff2 营销:适配商城装修组件【列表导航】 2023-11-08 21:10:05 +08:00
niou233 084009f7f2 Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 2023-11-08 17:30:04 +08:00
niou233 f2c71d5836 refactor: 会员商品收藏 2023-11-08 17:29:57 +08:00
Wanwan 57474c0644 Merge remote-tracking branch 'origin/dev' into dev 2023-11-07 22:54:50 +08:00
Wanwan 7146fdc9b9 style: 客户 review 修改 2023-11-06 22:00:40 +08:00
YunaiV eb9f8c9f6f Merge branch 'dev' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 2023-11-06 21:43:55 +08:00
YunaiV 85ee558bef 多租户:登录界面,根据 host 域名获取对应的租户编号 2023-11-06 19:42:28 +08:00
YunaiV c0d5c76a06 多租户:调整 tenant 表的 domain 字段为 website,避免 dm 数据库的关键字冲突 2023-11-06 19:09:20 +08:00
芋道源码 821217501c !310 fix: 修复关闭拼团活动请求URL错误的问题
Merge pull request !310 from VicoHu/N/A
2023-11-06 07:03:30 +00:00
芋道源码 ead30e8cd9 !312 商城装修
Merge pull request !312 from 疯狂的世界/dev
2023-11-06 07:02:18 +00:00
owen 4253173a2a 营销:适配商城装修组件【轮播图】 2023-11-06 09:31:42 +08:00
owen 6e9e1d0888 Merge remote-tracking branch 'origin/dev' into dev 2023-11-06 08:33:16 +08:00
owen bb2b7ffd03 营销:增加商城装修组件【视频播放】 2023-11-05 22:53:30 +08:00
VicoHu 3f803767c4 fix: 修复关闭拼团活动请求URL错误的问题
Signed-off-by: VicoHu <vicohu@163.com>
2023-11-05 13:59:28 +00:00
YunaiV 47bb88d892 code review:产品分类 2023-11-05 20:50:13 +08:00
YunaiV e1cff74f41 Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into dev 2023-11-05 20:48:12 +08:00
owen fa710b560d 营销:增加商城装修组件【图片展示】 2023-11-05 20:42:50 +08:00
芋道源码 596ad888e1 !308 【增加CRM:产品以及产品分类】
Merge pull request !308 from ZanGe丶/master
2023-11-05 12:28:31 +00:00
YunaiV 17742988c9 code review:回款 2023-11-05 20:25:52 +08:00
YunaiV 65d85e30ca Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into dev 2023-11-05 20:23:56 +08:00
芋道源码 f0c45103e1 !309 完善回款管理和回款计划功能
Merge pull request !309 from souvc/master
2023-11-05 12:20:46 +00:00
owen 434aa864da 营销:优化装修编辑器 2023-11-05 12:40:32 +08:00
owen 0b0ba1f313 营销:优化装修编辑器样式 2023-11-05 10:08:03 +08:00
owen 984351fbf0 修复页面中心内容区会出现滚动条的问题: 底部版权栏的P标签有默认的margin导致 2023-11-05 09:47:54 +08:00
owen aedf014407 营销:装修页面适配搜索框 2023-11-04 22:06:40 +08:00
YunaiV 4f2f4f72b5 优化:社交登录、用户的管理 2023-11-04 20:43:36 +08:00
ZanGe丶 ff6f05df84 【crm:产品及产品分类】 2023-11-04 18:28:59 +08:00
YunaiV a9b91e7436 优化:社交登录、用户的管理 2023-11-04 14:37:23 +08:00
YunaiV ac7f1e29fb Merge branch 'dev_tmp' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 2023-11-04 10:50:04 +08:00
芋道源码 df646175dc !296 新增社交客户端 CRUD,新增社交用户 RUD
Merge pull request !296 from puhui999/dev_tmp
2023-11-04 02:49:27 +00:00
芋道源码 71e6d51c12 !299 update src/layout/components/Message/src/Message.vue.
Merge pull request !299 from lzy/N/A
2023-11-04 02:44:57 +00:00
YunaiV f79d31a64a code review:满减送活动 2023-11-04 10:39:39 +08:00
芋道源码 27defa7268 !291 满减页面布局
Merge pull request !291 from Bluemark/mall-backed
2023-11-04 02:30:50 +00:00
YunaiV a9faeb1bdb code review:客户管理 2023-11-04 10:12:40 +08:00
芋道源码 09c44cd4fe !306 feat: 客户详情 + review 修改
Merge pull request !306 from wanwan/dev
2023-11-04 01:50:33 +00:00
Wanwan 23f483ac45 feat: 客户详情 + review 修改 2023-11-04 03:21:01 +08:00
liuhongfeng 8ef5227f88 Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 2023-11-04 00:48:51 +08:00
芋道源码 188de1031b !302 update src/views/mall/trade/delivery/expressTemplate/ExpressTemplateForm.vue.
Merge pull request !302 from 风狗/N/A
2023-11-03 16:28:27 +00:00
芋道源码 f6adb4ebaf !303 商城装修
Merge pull request !303 from 疯狂的世界/dev
2023-11-03 15:07:04 +00:00
YunaiV de62a7a5b7 bugfix:注释掉 uno 的 transformerVariantGroup,解决 vite 监控到 css 变化,导致 Login.vue 登录时的重加载问题 2023-11-03 18:44:56 +08:00
jason fcc748dc63 Merge remote-tracking branch 'origin/dev' into dev 2023-11-03 08:47:57 +08:00
jason 753f5de81e 转账 - 新增转账订单查询 2023-11-03 08:46:53 +08:00
owen 3c0f3ceed5 营销:装修模板,底部导航适配APP 2023-11-03 06:47:46 +08:00
风狗 bf418653e4 update src/views/mall/trade/delivery/expressTemplate/ExpressTemplateForm.vue.
地区选择框placeholder 修改

Signed-off-by: 风狗 <909275705@qq.com>
2023-11-02 02:08:26 +00:00
芋道源码 20c59b8122 !301 营销:完善商城装修
Merge pull request !301 from 疯狂的世界/dev
2023-11-01 00:48:16 +00:00
liuhongfeng 3d54ad3a6a 【新增】负责人
【修改】规范命名:期数:indexNo-》period
2023-10-31 23:32:35 +08:00
owen 9bd33355e2 营销:装修编辑器样式优化 2023-10-31 17:17:46 +08:00
dongshanshan 18e2c33cef Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 2023-10-31 16:53:48 +08:00
dongshanshan 9a3253ad20 feat: 会员商品收藏 2023-10-31 16:53:44 +08:00
owen 881c4f3a29 营销:完善商城装修
1. 拆分页面设计用的属性修改接口
2. 增加模板的页面保存
3. 增加唯一名称校验
4. 增加页面编辑器,默认选中组件判断
2023-10-31 16:01:22 +08:00
xingyu 10483577b7 fix: types 2023-10-31 14:26:17 +08:00
xingyu 52755b38a9 fix: props 2023-10-31 14:25:49 +08:00
xingyu 13f31c19f5 chore: words warn 2023-10-31 14:25:41 +08:00
xingyu 4365ca6177 fix: title name 2023-10-31 14:10:05 +08:00
xingyu ce93c8d4e9 fix: name warn 2023-10-31 14:04:43 +08:00
xingyu 7321ff2136 chore: wh-full 2023-10-31 10:20:39 +08:00
xingyu af7c6f3b2c fix: url error 2023-10-31 10:17:00 +08:00
xingyu 80e72fd924 feat: coding 2023-10-31 10:13:22 +08:00
xingyu 2a029ba35b feat: add useTagsView hooks 2023-10-31 10:12:04 +08:00
xingyu 57e8256e9a feat: add useNow hooks 2023-10-31 10:11:55 +08:00
xingyu bdcef303ff perf: useValidator hooks 2023-10-31 10:11:43 +08:00
xingyu f8fdebffe6 feat: add useNetwork hooks 2023-10-31 10:11:34 +08:00
xingyu 4ba264e00d perf: use driver.js 2023-10-31 10:07:21 +08:00
xingyu f26a4cae10 feat: unocss add shortcuts 2023-10-31 10:04:31 +08:00
xingyu 899880bfe1 chore: update deps 2023-10-31 09:52:08 +08:00
liuhongfeng 2b84de7e09 【新增】负责人
【修改】规范命名:期数:indexNo-》period
2023-10-30 21:32:50 +08:00
lzy c269cab6b2 update src/layout/components/Message/src/Message.vue.
fix:  处理站内信内容过多回显问题

Signed-off-by: lzy <7783739+lzyyd@user.noreply.gitee.com>
2023-10-30 06:20:16 +00:00
jason c1534bf168 Merge remote-tracking branch 'origin/dev' into dev 2023-10-29 23:48:20 +08:00
jason 55a961e862 转账 - 管理后台新增转账示例 2023-10-29 23:48:10 +08:00
YunaiV 0c81a0d96a code review:客户表的实现 2023-10-29 22:29:03 +08:00
芋道源码 4fba270f1c !298 商城装修
Merge pull request !298 from 疯狂的世界/feature/mall_diy
2023-10-29 14:16:58 +00:00
owen e9ff8d3237 商城装修 2023-10-29 22:03:24 +08:00
YunaiV dc5cf9f24c code review:客户表的实现 2023-10-29 17:55:39 +08:00
YunaiV 690b208a4e Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into dev 2023-10-29 10:38:48 +08:00
芋道源码 5d760d5450 !297 feat: 调整 crm 客户表的字段
Merge pull request !297 from wanwan/master
2023-10-29 02:37:30 +00:00
Wanwan 60d53126c4 feat: 调整客户表的字段 2023-10-29 00:33:38 +08:00
Wanwan 59cf0c29f0 feat: 调整客户表的字段 2023-10-29 00:03:11 +08:00
Wanwan 4cdab6fb3e feat: 调整客户表的字段 2023-10-28 19:50:42 +08:00
芋道源码 cca34e0acb !295 订单:解决列数据中的 table#header 数据不刷新的问题
Merge pull request !295 from puhui999/dev-to-dev
2023-10-27 15:03:42 +00:00
YunaiV b9ab22dad1 code review:客户表的实现 2023-10-27 23:01:00 +08:00
芋道源码 32b3c874f5 !294 feat: 客户表的 crud
Merge pull request !294 from wanwan/master
2023-10-27 14:41:47 +00:00
puhui999 a624db04e9 三方应用新增授权方的网页应用 agentId 属性 2023-10-26 17:00:13 +08:00
puhui999 88a22d90e5 解决钉钉绑定失败的问题 2023-10-26 16:19:22 +08:00
puhui999 6ee2492ff1 新增社交用户 RUD 2023-10-25 17:02:11 +08:00
puhui999 8935188a82 新增社交客户端 CRUD 2023-10-25 16:07:58 +08:00
puhui999 d060d27ab2 Merge remote-tracking branch 'yudao/dev' into dev-to-dev 2023-10-25 11:14:26 +08:00
puhui999 ca385bc08e 解决列数据中的 table#header 数据不刷新的问题 2023-10-25 11:14:00 +08:00
xingyu 309c3f30d1 chore: remove vue-macros/volar 2023-10-25 10:43:03 +08:00
xingyu 238537df83 fix: ElLoading need import 2023-10-25 10:36:06 +08:00
xingyu bf2c59c6e0 chore: stylelint config 2023-10-25 10:35:30 +08:00
xingyu 50f1ae9524 chore: eslint config 2023-10-25 10:35:20 +08:00
xingyu 96db6ab538 chore: vscode setting 2023-10-25 10:35:10 +08:00
xingyu b8403c1b90 chore: update vue v3.3.7 2023-10-25 10:34:54 +08:00
Wanwan 044754cdb4 feat: 客户表的 crud 2023-10-25 00:24:20 +08:00
KONGDY-PC\Kongdy 7c8f7d570f 满减页面布局 2023-10-23 10:37:39 +08:00
234 changed files with 17058 additions and 779 deletions

No files matched your search

+2 -2
View File
@@ -4,10 +4,10 @@ NODE_ENV=development
VITE_DEV=true
# 请求路径
VITE_BASE_URL='http://localhost:48080'
VITE_BASE_URL='http://127.0.0.1:48080'
# 上传路径
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
VITE_UPLOAD_URL='http://127.0.0.1:48080/admin-api/infra/file/upload'
# 接口前缀
VITE_API_BASEPATH=/dev-api
+5 -4
View File
@@ -8,7 +8,6 @@ module.exports = defineConfig({
es6: true
},
parser: 'vue-eslint-parser',
plugins: ['vue'],
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 2020,
@@ -22,13 +21,13 @@ module.exports = defineConfig({
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended',
'plugin:prettier/recommended',
'@unocss'
],
rules: {
'vue/no-setup-props-destructure': 'off',
'vue/script-setup-uses-vars': 'error',
'vue/no-reserved-component-names': 'off',
'vue/no-setup-props-destructure': 'off',
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
@@ -54,6 +53,7 @@ module.exports = defineConfig({
'vue/attribute-hyphenation': 'off',
'vue/require-default-prop': 'off',
'vue/require-explicit-emits': 'off',
'vue/require-toggle-inside-transition': 'off',
'vue/html-self-closing': [
'error',
{
@@ -67,6 +67,7 @@ module.exports = defineConfig({
}
],
'vue/multi-word-component-names': 'off',
'vue/no-v-html': 'off'
'vue/no-v-html': 'off',
'prettier/prettier': 'off' // 芋艿:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件
}
})
+6 -6
View File
@@ -1,5 +1,5 @@
{
"typescript.tsdk": "./node_modules/typescript/lib",
"typescript.tsdk": "node_modules/typescript/lib",
"npm.packageManager": "pnpm",
"editor.tabSize": 2,
"prettier.printWidth": 100, // 超过最大值换行
@@ -86,15 +86,12 @@
"source.fixAll.eslint": true
},
"[vue]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true
}
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
},
"i18n-ally.localesPaths": ["src/locales"],
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
"i18n-ally.namespace": true,
"i18n-ally.namespace": false,
"i18n-ally.enabledParsers": ["ts"],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "zh-CN",
@@ -105,6 +102,7 @@
"codemirror",
"commitlint",
"cropperjs",
"echart",
"echarts",
"esnext",
"esno",
@@ -119,10 +117,12 @@
"sider",
"sortablejs",
"stylelint",
"svgs",
"unocss",
"unplugin",
"unref",
"videojs",
"VITE",
"vitejs",
"vueuse",
"wangeditor",
+6 -6
View File
@@ -38,15 +38,15 @@
| 框架 | 说明 | 版本 |
|----------------------------------------------------------------------|------------------|--------|
| [Vue](https://staging-cn.vuejs.org/) | Vue 框架 | 3.3.4 |
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 4.4.11 |
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.4.0 |
| [Vue](https://staging-cn.vuejs.org/) | Vue 框架 | 3.3.8 |
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 4.5.0 |
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.4.2 |
| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 的超集 | 5.2.2 |
| [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.1.7 |
| [vueuse](https://vueuse.org/) | 常用工具集 | 10.5.0 |
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.5.0 |
| [vueuse](https://vueuse.org/) | 常用工具集 | 10.6.1 |
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.6.5 |
| [vue-router](https://router.vuejs.org/) | Vue 路由 | 4.2.5 |
| [unocss](https://uno.antfu.me/) | 原子 css | 0.56.5 |
| [unocss](https://uno.antfu.me/) | 原子 css | 0.57.4 |
| [iconify](https://icon-sets.iconify.design/) | 在线图标库 | 3.1.1 |
| [wangeditor](https://www.wangeditor.com/) | 富文本编辑器 | 5.1.23 |
+1 -8
View File
@@ -60,18 +60,11 @@ export function createVitePlugins() {
}
}),
Components({
// 要搜索组件的目录的相对路径
dirs: ['src/components'],
// 组件的有效文件扩展名
extensions: ['vue', 'md'],
// 搜索子目录
deep: true,
include: [/\.vue$/, /\.vue\?vue/],
// 生成自定义 `auto-components.d.ts` 全局声明
dts: 'src/types/auto-components.d.ts',
// 自定义组件的解析器
resolvers: [ElementPlusResolver()],
exclude: [/[\\/]node_modules[\\/]/]
globs: ["src/components/**/**.{vue, md}", '!src/components/DiyEditor/components/mobile/**']
}),
EslintPlugin({
cache: false,
+35 -37
View File
@@ -31,25 +31,25 @@
"@form-create/element-ui": "^3.1.24",
"@iconify/iconify": "^3.1.1",
"@videojs-player/vue": "^1.0.0",
"@vueuse/core": "^10.5.0",
"@vueuse/core": "^10.6.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^3.0.4",
"animate.css": "^4.1.1",
"axios": "^1.5.1",
"axios": "^1.6.1",
"benz-amr-recorder": "^1.1.5",
"bpmn-js-token-simulation": "^0.10.0",
"camunda-bpmn-moddle": "^7.0.1",
"cropperjs": "^1.6.1",
"crypto-js": "^4.1.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10",
"diagram-js": "^12.4.0",
"diagram-js": "^12.8.0",
"driver.js": "^1.3.1",
"echarts": "^5.4.3",
"echarts-wordcloud": "^2.1.0",
"element-plus": "2.4.0",
"element-plus": "2.4.2",
"fast-xml-parser": "^4.3.2",
"highlight.js": "^11.9.0",
"intro.js": "^7.2.0",
"jsencrypt": "^3.3.2",
"lodash-es": "^4.17.21",
"min-dash": "^4.1.1",
@@ -62,9 +62,9 @@
"steady-xml": "^0.1.0",
"url": "^0.11.3",
"video.js": "^7.21.5",
"vue": "^3.3.4",
"vue": "^3.3.8",
"vue-dompurify-html": "^4.1.4",
"vue-i18n": "^9.5.0",
"vue-i18n": "^9.6.5",
"vue-router": "^4.2.5",
"vue-types": "^5.1.1",
"vuedraggable": "^4.1.0",
@@ -72,55 +72,53 @@
"xml-js": "^1.6.11"
},
"devDependencies": {
"@commitlint/cli": "^17.8.0",
"@commitlint/config-conventional": "^17.8.0",
"@iconify/json": "^2.2.129",
"@intlify/unplugin-vue-i18n": "^1.4.0",
"@commitlint/cli": "^18.4.1",
"@commitlint/config-conventional": "^18.4.0",
"@iconify/json": "^2.2.142",
"@intlify/unplugin-vue-i18n": "^1.5.0",
"@purge-icons/generated": "^0.9.0",
"@types/intro.js": "^5.1.2",
"@types/lodash-es": "^4.17.9",
"@types/node": "^20.8.6",
"@types/nprogress": "^0.2.1",
"@types/qrcode": "^1.5.2",
"@types/qs": "^6.9.8",
"@types/sortablejs": "^1.15.4",
"@typescript-eslint/eslint-plugin": "^6.7.5",
"@typescript-eslint/parser": "^6.7.5",
"@unocss/transformer-variant-group": "^0.56.5",
"@unocss/eslint-config": "^0.56.5",
"@types/lodash-es": "^4.17.11",
"@types/node": "^20.9.0",
"@types/nprogress": "^0.2.3",
"@types/qrcode": "^1.5.5",
"@types/qs": "^6.9.10",
"@types/sortablejs": "^1.15.5",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"@unocss/transformer-variant-group": "^0.57.4",
"@unocss/eslint-config": "^0.57.4",
"@vitejs/plugin-legacy": "^4.1.1",
"@vitejs/plugin-vue": "^4.4.0",
"@vitejs/plugin-vue": "^4.4.1",
"@vitejs/plugin-vue-jsx": "^3.0.2",
"@vue-macros/volar": "^0.17.0",
"autoprefixer": "^10.4.16",
"bpmn-js": "8.9.0",
"bpmn-js-properties-panel": "0.46.0",
"consola": "^3.2.3",
"eslint": "^8.51.0",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-define-config": "^1.24.1",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-vue": "^9.17.0",
"lint-staged": "^15.0.1",
"eslint-plugin-vue": "^9.18.1",
"lint-staged": "^15.1.0",
"postcss": "^8.4.31",
"postcss-html": "^1.5.0",
"postcss-scss": "^4.0.9",
"prettier": "^3.0.3",
"prettier": "^3.1.0",
"rimraf": "^5.0.5",
"rollup": "^4.1.4",
"sass": "^1.69.3",
"stylelint": "^15.10.3",
"rollup": "^4.4.1",
"sass": "^1.69.5",
"stylelint": "^15.11.0",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recommended": "^13.0.0",
"stylelint-config-standard": "^34.0.0",
"stylelint-order": "^6.0.3",
"terser": "^5.21.0",
"terser": "^5.24.0",
"typescript": "5.2.2",
"unocss": "^0.56.5",
"unplugin-auto-import": "^0.16.6",
"unocss": "^0.57.4",
"unplugin-auto-import": "^0.16.7",
"unplugin-element-plus": "^0.8.0",
"unplugin-vue-components": "^0.25.2",
"vite": "4.4.11",
"vite": "4.5.0",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-ejs": "^1.6.4",
"vite-plugin-eslint": "^1.8.1",
@@ -129,7 +127,7 @@
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-top-level-await": "^1.3.1",
"vue-eslint-parser": "^9.3.2",
"vue-tsc": "^1.8.19"
"vue-tsc": "^1.8.22"
},
"license": "MIT",
"repository": {
+67
View File
@@ -0,0 +1,67 @@
/*
* @Author: zyna
* @Date: 2023-11-05 13:34:41
* @LastEditTime: 2023-11-26 20:47:04
* @FilePath: \yudao-ui-admin-vue3\src\api\crm\contact\index.ts
* @Description:
*/
import request from '@/config/axios'
export interface ContactVO {
name: string
nextTime: Date
mobile: string
telephone: string
email: string
post: string
customerId: number
address: string
remark: string
ownerUserId: string
lastTime: Date
id: number
parentId: number
qq: number
wechat: string
sex: number
master: boolean
creatorName: string
updateTime?: Date
createTime?: Date
customerName: string,
areaName: string,
ownerUserName: string
}
// 查询crm联系人列表
export const getContactPage = async (params) => {
return await request.get({ url: `/crm/contact/page`, params })
}
// 查询crm联系人详情
export const getContact = async (id: number) => {
return await request.get({ url: `/crm/contact/get?id=` + id })
}
// 新增crm联系人
export const createContact = async (data: ContactVO) => {
return await request.post({ url: `/crm/contact/create`, data })
}
// 修改crm联系人
export const updateContact = async (data: ContactVO) => {
return await request.put({ url: `/crm/contact/update`, data })
}
// 删除crm联系人
export const deleteContact = async (id: number) => {
return await request.delete({ url: `/crm/contact/delete?id=` + id })
}
// 导出crm联系人 Excel
export const exportContact = async (params) => {
return await request.download({ url: `/crm/contact/export-excel`, params })
}
export const simpleAllList = async () => {
return await request.get({ url: `/crm/contact/simple-all-list` })
}
+68
View File
@@ -0,0 +1,68 @@
import request from '@/config/axios'
export interface CustomerVO {
id?: number
name: string
industryId: number
level: number
source: number
followUpStatus?: boolean
lockStatus?: boolean
dealStatus?: boolean
mobile: string
telephone: string
website: string
qq: string
wechat: string
email: string
description: string
remark: string
ownerUserId?: number
ownerUserName?: string
ownerUserDept?: string
roUserIds?: string
rwUserIds?: string
areaId?: number
areaName?: string
detailAddress: string
contactLastTime?: Date
contactNextTime: Date
createTime?: Date
updateTime?: Date
creator?: string
creatorName?: string
}
// 查询客户列表
export const getCustomerPage = async (params) => {
return await request.get({ url: `/crm/customer/page`, params })
}
// 查询客户详情
export const getCustomer = async (id: number) => {
return await request.get({ url: `/crm/customer/get?id=` + id })
}
// 新增客户
export const createCustomer = async (data: CustomerVO) => {
return await request.post({ url: `/crm/customer/create`, data })
}
// 修改客户
export const updateCustomer = async (data: CustomerVO) => {
return await request.put({ url: `/crm/customer/update`, data })
}
// 删除客户
export const deleteCustomer = async (id: number) => {
return await request.delete({ url: `/crm/customer/delete?id=` + id })
}
// 导出客户 Excel
export const exportCustomer = async (params) => {
return await request.download({ url: `/crm/customer/export-excel`, params })
}
//客户列表
export const queryAllList = async () => {
return await request.get({ url: `/crm/customer/query-all-list` })
}
+35
View File
@@ -0,0 +1,35 @@
import request from '@/config/axios'
export interface CustomerLimitConfigVO {
id?: number
type?: number
userIds?: string
deptIds?: string
maxCount?: number
dealCountEnabled?: boolean
}
// 查询客户限制配置列表
export const getCustomerLimitConfigPage = async (params) => {
return await request.get({ url: `/crm/customer-limit-config/page`, params })
}
// 查询客户限制配置详情
export const getCustomerLimitConfig = async (id: number) => {
return await request.get({ url: `/crm/customer-limit-config/get?id=` + id })
}
// 新增客户限制配置
export const createCustomerLimitConfig = async (data: CustomerLimitConfigVO) => {
return await request.post({ url: `/crm/customer-limit-config/create`, data })
}
// 修改客户限制配置
export const updateCustomerLimitConfig = async (data: CustomerLimitConfigVO) => {
return await request.put({ url: `/crm/customer-limit-config/update`, data })
}
// 删除客户限制配置
export const deleteCustomerLimitConfig = async (id: number) => {
return await request.delete({ url: `/crm/customer-limit-config/delete?id=` + id })
}
+19
View File
@@ -0,0 +1,19 @@
import request from '@/config/axios'
export interface CustomerPoolConfigVO {
enabled?: boolean
contactExpireDays?: number
dealExpireDays?: number
notifyEnabled?: boolean
notifyDays: number
}
// 获取客户公海规则设置
export const getCustomerPoolConfig = async () => {
return await request.get({ url: `/crm/customer-pool-config/get` })
}
// 更新客户公海规则设置
export const updateCustomerPoolConfig = async (data: ConfigVO) => {
return await request.put({ url: `/crm/customer-pool-config/update`, data })
}
+48
View File
@@ -0,0 +1,48 @@
import request from '@/config/axios'
export interface PermissionVO {
id?: number // 数据权限编号
userId: number | undefined // 用户编号
bizType: number | undefined // Crm 类型
bizId: number | undefined // Crm 类型数据编号
level: number | undefined // 权限级别
deptName?: string // 部门名称
nickname?: string // 用户昵称
postNames?: string[] // 岗位名称数组
createTime?: Date
}
// 查询团队成员列表
export const getPermissionList = async (params) => {
return await request.get({ url: `/crm/permission/list`, params })
}
// 新增团队成员
export const createPermission = async (data: PermissionVO) => {
return await request.post({ url: `/crm/permission/create`, data })
}
// 修改团队成员权限级别
export const updatePermission = async (data) => {
return await request.put({ url: `/crm/permission/update`, data })
}
// 删除团队成员
export const deletePermission = async (params) => {
return await request.delete({ url: '/crm/permission/delete', params })
}
// 退出团队
export const quitTeam = async (id) => {
return await request.delete({ url: '/crm/permission/quit-team?id=' + id })
}
// 领取公海数据
export const receive = async (data: { bizType: number; bizId: number }) => {
return await request.put({ url: `/crm/permission/receive`, data })
}
// 数据放入公海
export const putPool = async (data: { bizType: number; bizId: number }) => {
return await request.put({ url: `/crm/permission/put-pool`, data })
}
+43
View File
@@ -0,0 +1,43 @@
import request from '@/config/axios'
export interface ProductVO {
id: number
name: string
no: string
unit: string
price: number
status: number
categoryId: number
description: string
ownerUserId: number
}
// 查询产品列表
export const getProductPage = async (params) => {
return await request.get({ url: `/crm/product/page`, params })
}
// 查询产品详情
export const getProduct = async (id: number) => {
return await request.get({ url: `/crm/product/get?id=` + id })
}
// 新增产品
export const createProduct = async (data: ProductVO) => {
return await request.post({ url: `/crm/product/create`, data })
}
// 修改产品
export const updateProduct = async (data: ProductVO) => {
return await request.put({ url: `/crm/product/update`, data })
}
// 删除产品
export const deleteProduct = async (id: number) => {
return await request.delete({ url: `/crm/product/delete?id=` + id })
}
// 导出产品 Excel
export const exportProduct = async (params) => {
return await request.download({ url: `/crm/product/export-excel`, params })
}
+33
View File
@@ -0,0 +1,33 @@
import request from '@/config/axios'
// TODO @zange:挪到 product 下,建个 category 包,挪进去哈;
export interface ProductCategoryVO {
id: number
name: string
parentId: number
}
// 查询产品分类详情
export const getProductCategory = async (id: number) => {
return await request.get({ url: `/crm/product-category/get?id=` + id })
}
// 新增产品分类
export const createProductCategory = async (data: ProductCategoryVO) => {
return await request.post({ url: `/crm/product-category/create`, data })
}
// 修改产品分类
export const updateProductCategory = async (data: ProductCategoryVO) => {
return await request.put({ url: `/crm/product-category/update`, data })
}
// 删除产品分类
export const deleteProductCategory = async (id: number) => {
return await request.delete({ url: `/crm/product-category/delete?id=` + id })
}
// 产品分类列表
export const getProductCategoryList = async (params) => {
return await request.get({ url: `/crm/product-category/list`, params })
}
+1 -1
View File
@@ -2,7 +2,7 @@ import request from '@/config/axios'
export interface ReceivablePlanVO {
id: number
indexNo: number
period: number
receivableId: number
status: number
checkStatus: string
+5 -5
View File
@@ -67,6 +67,11 @@ export type CodegenCreateListReqVO = {
tableNames: string[]
}
// 查询列表代码生成表定义
export const getCodegenTableList = (dataSourceConfigId: number) => {
return request.get({ url: '/infra/codegen/table/list?dataSourceConfigId=' + dataSourceConfigId })
}
// 查询列表代码生成表定义
export const getCodegenTablePage = (params: PageParam) => {
return request.get({ url: '/infra/codegen/table/page', params })
@@ -92,11 +97,6 @@ export const syncCodegenFromDB = (id: number) => {
return request.put({ url: '/infra/codegen/sync-from-db?tableId=' + id })
}
// 基于 SQL 建表语句,同步数据库的表和字段定义
export const syncCodegenFromSQL = (id: number, sql: string) => {
return request.put({ url: '/infra/codegen/sync-from-sql?tableId=' + id + '&sql=' + sql })
}
// 预览生成代码
export const previewCodegen = (id: number) => {
return request.get({ url: '/infra/codegen/preview?tableId=' + id })
+40
View File
@@ -0,0 +1,40 @@
import request from '@/config/axios'
export interface Demo01ContactVO {
id: number
name: string
sex: number
birthday: Date
description: string
avatar: string
}
// 查询示例联系人分页
export const getDemo01ContactPage = async (params) => {
return await request.get({ url: `/infra/demo01-contact/page`, params })
}
// 查询示例联系人详情
export const getDemo01Contact = async (id: number) => {
return await request.get({ url: `/infra/demo01-contact/get?id=` + id })
}
// 新增示例联系人
export const createDemo01Contact = async (data: Demo01ContactVO) => {
return await request.post({ url: `/infra/demo01-contact/create`, data })
}
// 修改示例联系人
export const updateDemo01Contact = async (data: Demo01ContactVO) => {
return await request.put({ url: `/infra/demo01-contact/update`, data })
}
// 删除示例联系人
export const deleteDemo01Contact = async (id: number) => {
return await request.delete({ url: `/infra/demo01-contact/delete?id=` + id })
}
// 导出示例联系人 Excel
export const exportDemo01Contact = async (params) => {
return await request.download({ url: `/infra/demo01-contact/export-excel`, params })
}
+37
View File
@@ -0,0 +1,37 @@
import request from '@/config/axios'
export interface Demo02CategoryVO {
id: number
name: string
parentId: number
}
// 查询示例分类列表
export const getDemo02CategoryList = async (params) => {
return await request.get({ url: `/infra/demo02-category/list`, params })
}
// 查询示例分类详情
export const getDemo02Category = async (id: number) => {
return await request.get({ url: `/infra/demo02-category/get?id=` + id })
}
// 新增示例分类
export const createDemo02Category = async (data: Demo02CategoryVO) => {
return await request.post({ url: `/infra/demo02-category/create`, data })
}
// 修改示例分类
export const updateDemo02Category = async (data: Demo02CategoryVO) => {
return await request.put({ url: `/infra/demo02-category/update`, data })
}
// 删除示例分类
export const deleteDemo02Category = async (id: number) => {
return await request.delete({ url: `/infra/demo02-category/delete?id=` + id })
}
// 导出示例分类 Excel
export const exportDemo02Category = async (params) => {
return await request.download({ url: `/infra/demo02-category/export-excel`, params })
}
+91
View File
@@ -0,0 +1,91 @@
import request from '@/config/axios'
export interface Demo03StudentVO {
id: number
name: string
sex: number
birthday: Date
description: string
}
// 查询学生分页
export const getDemo03StudentPage = async (params) => {
return await request.get({ url: `/infra/demo03-student/page`, params })
}
// 查询学生详情
export const getDemo03Student = async (id: number) => {
return await request.get({ url: `/infra/demo03-student/get?id=` + id })
}
// 新增学生
export const createDemo03Student = async (data: Demo03StudentVO) => {
return await request.post({ url: `/infra/demo03-student/create`, data })
}
// 修改学生
export const updateDemo03Student = async (data: Demo03StudentVO) => {
return await request.put({ url: `/infra/demo03-student/update`, data })
}
// 删除学生
export const deleteDemo03Student = async (id: number) => {
return await request.delete({ url: `/infra/demo03-student/delete?id=` + id })
}
// 导出学生 Excel
export const exportDemo03Student = async (params) => {
return await request.download({ url: `/infra/demo03-student/export-excel`, params })
}
// ==================== 子表(学生课程) ====================
// 获得学生课程分页
export const getDemo03CoursePage = async (params) => {
return await request.get({ url: `/infra/demo03-student/demo03-course/page`, params })
}
// 新增学生课程
export const createDemo03Course = async (data) => {
return await request.post({ url: `/infra/demo03-student/demo03-course/create`, data })
}
// 修改学生课程
export const updateDemo03Course = async (data) => {
return await request.put({ url: `/infra/demo03-student/demo03-course/update`, data })
}
// 删除学生课程
export const deleteDemo03Course = async (id: number) => {
return await request.delete({ url: `/infra/demo03-student/demo03-course/delete?id=` + id })
}
// 获得学生课程
export const getDemo03Course = async (id: number) => {
return await request.get({ url: `/infra/demo03-student/demo03-course/get?id=` + id })
}
// ==================== 子表(学生班级) ====================
// 获得学生班级分页
export const getDemo03GradePage = async (params) => {
return await request.get({ url: `/infra/demo03-student/demo03-grade/page`, params })
}
// 新增学生班级
export const createDemo03Grade = async (data) => {
return await request.post({ url: `/infra/demo03-student/demo03-grade/create`, data })
}
// 修改学生班级
export const updateDemo03Grade = async (data) => {
return await request.put({ url: `/infra/demo03-student/demo03-grade/update`, data })
}
// 删除学生班级
export const deleteDemo03Grade = async (id: number) => {
return await request.delete({ url: `/infra/demo03-student/demo03-grade/delete?id=` + id })
}
// 获得学生班级
export const getDemo03Grade = async (id: number) => {
return await request.get({ url: `/infra/demo03-student/demo03-grade/get?id=` + id })
}
+53
View File
@@ -0,0 +1,53 @@
import request from '@/config/axios'
export interface Demo03StudentVO {
id: number
name: string
sex: number
birthday: Date
description: string
}
// 查询学生分页
export const getDemo03StudentPage = async (params) => {
return await request.get({ url: `/infra/demo03-student/page`, params })
}
// 查询学生详情
export const getDemo03Student = async (id: number) => {
return await request.get({ url: `/infra/demo03-student/get?id=` + id })
}
// 新增学生
export const createDemo03Student = async (data: Demo03StudentVO) => {
return await request.post({ url: `/infra/demo03-student/create`, data })
}
// 修改学生
export const updateDemo03Student = async (data: Demo03StudentVO) => {
return await request.put({ url: `/infra/demo03-student/update`, data })
}
// 删除学生
export const deleteDemo03Student = async (id: number) => {
return await request.delete({ url: `/infra/demo03-student/delete?id=` + id })
}
// 导出学生 Excel
export const exportDemo03Student = async (params) => {
return await request.download({ url: `/infra/demo03-student/export-excel`, params })
}
// ==================== 子表(学生课程) ====================
// 获得学生课程列表
export const getDemo03CourseListByStudentId = async (studentId) => {
return await request.get({ url: `/infra/demo03-student/demo03-course/list-by-student-id?studentId=` + studentId })
}
// ==================== 子表(学生班级) ====================
// 获得学生班级
export const getDemo03GradeByStudentId = async (studentId) => {
return await request.get({ url: `/infra/demo03-student/demo03-grade/get-by-student-id?studentId=` + studentId })
}
+53
View File
@@ -0,0 +1,53 @@
import request from '@/config/axios'
export interface Demo03StudentVO {
id: number
name: string
sex: number
birthday: Date
description: string
}
// 查询学生分页
export const getDemo03StudentPage = async (params) => {
return await request.get({ url: `/infra/demo03-student/page`, params })
}
// 查询学生详情
export const getDemo03Student = async (id: number) => {
return await request.get({ url: `/infra/demo03-student/get?id=` + id })
}
// 新增学生
export const createDemo03Student = async (data: Demo03StudentVO) => {
return await request.post({ url: `/infra/demo03-student/create`, data })
}
// 修改学生
export const updateDemo03Student = async (data: Demo03StudentVO) => {
return await request.put({ url: `/infra/demo03-student/update`, data })
}
// 删除学生
export const deleteDemo03Student = async (id: number) => {
return await request.delete({ url: `/infra/demo03-student/delete?id=` + id })
}
// 导出学生 Excel
export const exportDemo03Student = async (params) => {
return await request.download({ url: `/infra/demo03-student/export-excel`, params })
}
// ==================== 子表(学生课程) ====================
// 获得学生课程列表
export const getDemo03CourseListByStudentId = async (studentId) => {
return await request.get({ url: `/infra/demo03-student/demo03-course/list-by-student-id?studentId=` + studentId })
}
// ==================== 子表(学生班级) ====================
// 获得学生班级
export const getDemo03GradeByStudentId = async (studentId) => {
return await request.get({ url: `/infra/demo03-student/demo03-grade/get-by-student-id?studentId=` + studentId })
}
+5
View File
@@ -27,6 +27,11 @@ export const getTenantIdByName = (name: string) => {
return request.get({ url: '/system/tenant/get-id-by-name?name=' + name })
}
// 使用租户域名,获得租户信息
export const getTenantByWebsite = (website: string) => {
return request.get({ url: '/system/tenant/get-by-website?website=' + website })
}
// 登出
export const loginOut = () => {
return request.post({ url: '/system/auth/logout' })
+12
View File
@@ -0,0 +1,12 @@
import request from '@/config/axios'
export interface Favorite {
id?: number
userId?: string // 用户编号
spuId?: number | null // 商品 SPU 编号
}
// 获得 ProductFavorite 列表
export const getFavoritePage = (params: PageParam) => {
return request.get({ url: '/product/favorite/page', params })
}
@@ -57,7 +57,7 @@ export const updateCombinationActivity = async (data: CombinationActivityVO) =>
// 关闭拼团活动
export const closeCombinationActivity = async (id: number) => {
return await request.put({ url: '/promotion/bargain-combination/close?id=' + id })
return await request.put({ url: '/promotion/combination-activity/close?id=' + id })
}
// 删除拼团活动
@@ -73,6 +73,13 @@ export function getCouponTemplatePage(params: PageParam) {
})
}
// 获得优惠劵模板分页
export function getCouponTemplateList(ids: number[]) {
return request.get({
url: `/promotion/coupon-template/list?ids=${ids}`
})
}
// 导出优惠劵模板 Excel
export function exportCouponTemplateExcel(params: PageParam) {
return request.get({
+45
View File
@@ -0,0 +1,45 @@
import request from '@/config/axios'
export interface DiyPageVO {
id?: number
templateId?: number
name: string
remark: string
previewImageUrls: string[]
property: string
}
// 查询装修页面列表
export const getDiyPagePage = async (params: any) => {
return await request.get({ url: `/promotion/diy-page/page`, params })
}
// 查询装修页面详情
export const getDiyPage = async (id: number) => {
return await request.get({ url: `/promotion/diy-page/get?id=` + id })
}
// 新增装修页面
export const createDiyPage = async (data: DiyPageVO) => {
return await request.post({ url: `/promotion/diy-page/create`, data })
}
// 修改装修页面
export const updateDiyPage = async (data: DiyPageVO) => {
return await request.put({ url: `/promotion/diy-page/update`, data })
}
// 删除装修页面
export const deleteDiyPage = async (id: number) => {
return await request.delete({ url: `/promotion/diy-page/delete?id=` + id })
}
// 获得装修页面属性
export const getDiyPageProperty = async (id: number) => {
return await request.get({ url: `/promotion/diy-page/get-property?id=` + id })
}
// 更新装修页面属性
export const updateDiyPageProperty = async (data: DiyPageVO) => {
return await request.put({ url: `/promotion/diy-page/update-property`, data })
}
+58
View File
@@ -0,0 +1,58 @@
import request from '@/config/axios'
import { DiyPageVO } from '@/api/mall/promotion/diy/page'
export interface DiyTemplateVO {
id?: number
name: string
used: boolean
usedTime?: Date
remark: string
previewImageUrls: string[]
property: string
}
export interface DiyTemplatePropertyVO extends DiyTemplateVO {
pages: DiyPageVO[]
}
// 查询装修模板列表
export const getDiyTemplatePage = async (params: any) => {
return await request.get({ url: `/promotion/diy-template/page`, params })
}
// 查询装修模板详情
export const getDiyTemplate = async (id: number) => {
return await request.get({ url: `/promotion/diy-template/get?id=` + id })
}
// 新增装修模板
export const createDiyTemplate = async (data: DiyTemplateVO) => {
return await request.post({ url: `/promotion/diy-template/create`, data })
}
// 修改装修模板
export const updateDiyTemplate = async (data: DiyTemplateVO) => {
return await request.put({ url: `/promotion/diy-template/update`, data })
}
// 删除装修模板
export const deleteDiyTemplate = async (id: number) => {
return await request.delete({ url: `/promotion/diy-template/delete?id=` + id })
}
// 使用装修模板
export const useDiyTemplate = async (id: number) => {
return await request.put({ url: `/promotion/diy-template/use?id=` + id })
}
// 获得装修模板属性
export const getDiyTemplateProperty = async (id: number) => {
return await request.get<DiyTemplatePropertyVO>({
url: `/promotion/diy-template/get-property?id=` + id
})
}
// 更新装修模板属性
export const updateDiyTemplateProperty = async (data: DiyTemplateVO) => {
return await request.put({ url: `/promotion/diy-template/update-property`, data })
}
+1 -1
View File
@@ -26,6 +26,6 @@ export const getUserPage = (query) => {
// 同步公众号粉丝
export const syncUser = (accountId) => {
return request.post({
url: '/mp/tag/sync?accountId=' + accountId
url: '/mp/user/sync?accountId=' + accountId
})
}
+25
View File
@@ -0,0 +1,25 @@
import request from '@/config/axios'
export interface DemoTransferVO {
price: number
type: number
userName: string
alipayLogonId: string
openid: string
}
// 创建示例转账单
export function createDemoTransfer(data: DemoTransferVO) {
return request.post({
url: '/pay/demo-transfer/create',
data: data
})
}
// 获得示例订单分页
export function getDemoTransferPage(query: PageParam) {
return request.get({
url: '/pay/demo-transfer/page',
params: query
})
}
+27
View File
@@ -0,0 +1,27 @@
import request from '@/config/axios'
export interface TransferVO {
appId: number
channelCode: string
merchantTransferId: string
type: number
price: number
subject: string
userName: string
alipayLogonId: string
openid: string
}
// 新增转账单
export const createTransfer = async (data: TransferVO) => {
return await request.post({ url: `/pay/transfer/create`, data })
}
// 查询转账单列表
export const getTransferPage = async (params) => {
return await request.get({ url: `/pay/transfer/page`, params })
}
export const getTransfer = async (id: number) => {
return await request.get({ url: '/pay/transfer/get?id=' + id })
}
+5
View File
@@ -35,3 +35,8 @@ export const updateNotice = (data: NoticeVO) => {
export const deleteNotice = (id: number) => {
return request.delete({ url: '/system/notice/delete?id=' + id })
}
// 推送公告
export const pushNotice = (id: number) => {
return request.post({ url: '/system/notice/push?id=' + id })
}
-2
View File
@@ -15,8 +15,6 @@ export interface SmsLogVO {
userType: number | null
sendStatus: number | null
sendTime: Date | null
sendCode: number | null
sendMsg: string
apiSendCode: string
apiSendMsg: string
apiRequestId: string
+37
View File
@@ -0,0 +1,37 @@
import request from '@/config/axios'
export interface SocialClientVO {
id: number
name: string
socialType: number
userType: number
clientId: string
clientSecret: string
agentId: string
status: number
}
// 查询社交客户端列表
export const getSocialClientPage = async (params) => {
return await request.get({ url: `/system/social-client/page`, params })
}
// 查询社交客户端详情
export const getSocialClient = async (id: number) => {
return await request.get({ url: `/system/social-client/get?id=` + id })
}
// 新增社交客户端
export const createSocialClient = async (data: SocialClientVO) => {
return await request.post({ url: `/system/social-client/create`, data })
}
// 修改社交客户端
export const updateSocialClient = async (data: SocialClientVO) => {
return await request.put({ url: `/system/social-client/update`, data })
}
// 删除社交客户端
export const deleteSocialClient = async (id: number) => {
return await request.delete({ url: `/system/social-client/delete?id=` + id })
}
+24
View File
@@ -0,0 +1,24 @@
import request from '@/config/axios'
export interface SocialUserVO {
id: number
type: number
openid: string
token: string
rawTokenInfo: string
nickname: string
avatar: string
rawUserInfo: string
code: string
state: string
}
// 查询社交用户列表
export const getSocialUserPage = async (params) => {
return await request.get({ url: `/system/social-user/page`, params })
}
// 查询社交用户详情
export const getSocialUser = async (id: number) => {
return await request.get({ url: `/system/social-user/get?id=` + id })
}
+5
View File
@@ -22,6 +22,11 @@ export const getUserPage = (params: PageParam) => {
return request.get({ url: '/system/user/page', params })
}
// 查询所有用户列表
export const getAllUser = () => {
return request.get({ url: '/system/user/all' })
}
// 查询用户详情
export const getUser = (id: number) => {
return request.get({ url: '/system/user/get?id=' + id })
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

+856 -1
View File
@@ -1 +1,856 @@
{"type":"FeatureCollection","features":[{"type":"Feature","id":"710000","properties":{"id":"710000","cp":[121.509062,24.044332],"name":"台湾","childNum":6},"geometry":{"type":"MultiPolygon","coordinates":[["@@°Ü¯Û"],["@@ƛĴÕƊÉɼģºðʀ\\ƎsÆNŌÔĚäœnÜƤɊĂǀĆĴžĤNJŨxĚĮǂƺòƌ‚–âÔ®ĮXŦţƸZûЋƕƑGđ¨ĭMó·ęcëƝɉlÝƯֹÅŃ^Ó·śŃNjƏďíåɛGɉ™¿@ăƑŽ¥ĘWǬÏĶŁâ"],["@@\\p|WoYG¿¥I†j@¢"],["@@…¡‰@ˆV^RqˆBbAŒnTXeRz¤Lž«³I"],["@@ÆEE—„kWqë @œ"],["@@fced"],["@@„¯ɜÄèaì¯ØǓIġĽ"],["@@çûĖ롖hòř "]],"encodeOffsets":[[[122886,24033]],[[123335,22980]],[[122375,24193]],[[122518,24117]],[[124427,22618]],[[124862,26043]],[[126259,26318]],[[127671,26683]]]}},{"type":"Feature","id":"130000","properties":{"id":"130000","cp":[114.502461,38.045474],"name":"河北","childNum":3},"geometry":{"type":"MultiPolygon","coordinates":[["@@o~†Z]‚ªr‰ºc_ħ²G¼s`jΟnüsœłNX_“M`ǽÓnUK…Ĝēs¤­©yrý§uģŒc†JŠ›e"],["@@U`Ts¿m‚"],["@@oºƋÄd–eVŽDJj£€J|Ådz•Ft~žKŨ¸IÆv|”‡¢r}膎onb˜}`RÎÄn°ÒdÞ²„^®’lnÐèĄlðӜ×]ªÆ}LiĂ±Ö`^°Ç¶p®đDcœŋ`–ZÔ’¶êqvFƚ†N®ĆTH®¦O’¾ŠIbÐã´BĐɢŴÆíȦp–ĐÞXR€·nndOž¤’OÀĈƒ­Qg˜µFo|gȒęSWb©osx|hYh•gŃfmÖĩnº€T̒Sp›¢dYĤ¶UĈjl’ǐpäìë|³kÛfw²Xjz~ÂqbTŠÑ„ěŨ@|oM‡’zv¢ZrÃVw¬ŧˏfŒ°ÐT€ªqŽs{Sž¯r æÝlNd®²Ğ džiGʂJ™¼lr}~K¨ŸƐÌWö€™ÆŠzRš¤lêmĞL΄’@¡|q]SvK€ÑcwpÏρ†ĿćènĪWlĄkT}ˆJ”¤~ƒÈT„d„™pddʾĬŠ”ŽBVt„EÀ¢ôPĎƗè@~‚k–ü\\rÊĔÖæW_§¼F˜†´©òDòj’ˆYÈrbĞāøŀG{ƀ|¦ðrb|ÀH`pʞkv‚GpuARhÞÆǶgƊTǼƹS£¨¡ù³ŘÍ]¿Ây™ôEP xX¶¹܇O¡“gÚ¡IwÃ鑦ÅB‡Ï|ǰ…N«úmH¯‹âŸDùŽyŜžŲIÄuШDž•¸dɂ‡‚FŸƒ•›Oh‡đ©OŸ›iÃ`ww^ƒÌkŸ‘ÑH«ƇǤŗĺtFu…{Z}Ö@U‡´…ʚLg®¯Oı°ÃwŸ ^˜—€VbÉs‡ˆmA…ê]]w„§›RRl£‡ȭµu¯b{ÍDěïÿȧŽuT£ġƒěŗƃĝ“Q¨fV†Ƌ•ƅn­a@‘³@šď„yýIĹÊKšŭfċŰóŒxV@tˆƯŒJ”]eƒR¾fe|rHA˜|h~Ėƍl§ÏŠlTíb ØoˆÅbbx³^zÃ͚¶Sj®A”yÂhðk`š«P€”ˈµEF†Û¬Y¨Ļrõqi¼‰Wi°§’б´°^[ˆÀ|ĠO@ÆxO\\tŽa\\tĕtû{ġŒȧXýĪÓjùÎRb›š^ΛfK[ݏděYfíÙTyŽuUSyŌŏů@Oi½’éŅ­aVcř§ax¹XŻác‡žWU£ôãºQ¨÷Ñws¥qEH‰Ù|‰›šYQoŕÇyáĂ£MðoťÊ‰P¡mšWO¡€v†{ôvîēÜISpÌhp¨ ‘j†deŔQÖj˜X³à™Ĉ[n`Yp@Už–cM`’RKhŒEbœ”pŞlNut®Etq‚nsÁŠgA‹iú‹oH‡qCX‡”hfgu“~ϋWP½¢G^}¯ÅīGCŸÑ^ãziMáļMTÃƘrMc|O_ž¯Ŏ´|‡morDkO\\mĆJfl@c̬¢aĦtRıҙ¾ùƀ^juųœK­ƒUFy™—Ɲ…›īÛ÷ąV×qƥV¿aȉd³B›qPBm›aËđŻģm“Å®Vйd^K‡KoŸnYg“¯Xhqa”Ldu¥•ÍpDž¡KąÅƒkĝęěhq‡}HyÓ]¹ǧ£…Í÷¿qáµ§š™g‘¤o^á¾ZE‡¤i`ij{n•ƒOl»ŸWÝĔįhg›F[¿¡—ßkOüš_‰€ū‹i„DZàUtėGylƒ}ŒÓM}€jpEC~¡FtoQi‘šHkk{Ãmï‚"]],"encodeOffsets":[[[119712,40641]],[[121616,39981]],[[116462,37237]]]}},{"type":"Feature","id":"140000","properties":{"id":"140000","cp":[111.849248,36.857014],"name":"山西","childNum":1},"geometry":{"type":"Polygon","coordinates":["@@Þĩ҃S‰ra}Á€yWix±Üe´lè“ßÓǏok‘ćiµVZģ¡coœ‘TS˹ĪmnÕńe–hZg{gtwªpXaĚThȑp{¶Eh—®RćƑP¿£‘Pmc¸mQÝW•ďȥoÅîɡųAďä³aωJ‘½¥PG­ąSM­™…EÅruµé€‘Yӎ•Ō_d›ĒCo­Èµ]¯_²ÕjāŽK~©ÅØ^ԛkïçămϑk]­±ƒcݯÑÃmQÍ~_a—pm…~ç¡q“ˆu{JÅŧ·Ls}–EyÁÆcI{¤IiCfUc•ƌÃp§]웫vD@¡SÀ‘µM‚ÅwuŽYY‡¡DbÑc¡hƒ×]nkoQdaMç~eD•ÛtT‰©±@¥ù@É¡‰ZcW|WqOJmĩl«ħşvOÓ«IqăV—¥ŸD[mI~Ó¢cehiÍ]Ɠ~ĥqXŠ·eƷœn±“}v•[ěďŽŕ]_‘œ•`‰¹ƒ§ÕōI™o©b­s^}Ét±ū«³p£ÿ·Wµ|¡¥ăFÏs׌¥ŅxŸÊdÒ{ºvĴÎêÌɊ²¶€ü¨|ÞƸµȲ‘LLúÉƎ¤ϊęĔV`„_bª‹S^|ŸdŠzY|dz¥p†ZbÆ£¶ÒK}tĦÔņƠ‚PYzn€ÍvX¶Ěn ĠÔ„zý¦ª˜÷žÑĸَUȌ¸‚dòÜJð´’ìúNM¬ŒXZ´‘¤ŊǸ_tldIš{¦ƀðĠȤ¥NehXnYG‚‡R° ƬDj¬¸|CĞ„Kq‚ºfƐiĺ©ª~ĆOQª ¤@ìǦɌ²æBŒÊ”TœŸ˜ʂōĖ’šĴŞ–ȀœÆÿȄlŤĒö„t”νî¼ĨXhŒ‘˜|ªM¤Ðz"],"encodeOffsets":[[116874,41716]]}},{"type":"Feature","id":"150000","properties":{"id":"150000","cp":[111.670801,41.818311],"name":"内蒙古","childNum":2},"geometry":{"type":"MultiPolygon","coordinates":[["@@¯PqƒFB…‰|S•³C|kñ•H‹d‘iÄ¥sˆʼnő…PóÑÑE^‘ÅPpy_YtS™hQ·aHwsOnʼnÚs©iqj›‰€USiº]ïWš‰«gW¡A–Rë¥_ŽsgÁnUI«m‰…„‹]j‡vV¼euhwqA„aW˜ƒ_µj…»çjioQR¹ēÃßt@r³[ÛlćË^ÍÉáG“›OUۗOB±•XŸkŇ¹£k|e]ol™ŸkVͼÕqtaÏõjgÁ£§U^Œ”RLˆËnX°Ç’Bz†^~wfvˆypV ¯„ƫĉ˭ȫƗŷɿÿĿƑ˃ĝÿÃǃßËőó©ǐȍŒĖM×ÍEyx‹þp]Évïè‘vƀnÂĴÖ@‚‰†V~Ĉv¦wĖt—ējyÄDXÄxGQuv_›i¦aBçw‘˛wD™©{ŸtāmQ€{EJ§KPśƘƿ¥@‰sCT•É}ɃwˆƇy±ŸgÑ“}T[÷kÐ禫…SÒ¥¸ëBX½‰HáŵÀğtSÝÂa[ƣ°¯¦P]£ġ“–“Òk®G²„èQ°óMq}EŠLine truncated
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "710000",
"properties": {
"id": "710000",
"cp": [121.509062, 24.044332],
"name": "台湾",
"childNum": 6
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
["@@°Ü¯Û"],
[
"@@ƛĴÕƊÉɼģºðʀ\\ƎsÆNŌÔĚäœnÜƤɊĂǀĆĴžĤNJŨxĚĮǂƺòƌ‚–âÔ®ĮXŦţƸZûЋƕƑGđ¨ĭMó·ęcëƝɉlÝƯֹÅŃ^Ó·śŃNjƏďíåɛGɉ™¿@ăƑŽ¥ĘWǬÏĶŁâ"
],
["@@\\p|WoYG¿¥I†j@¢"],
["@@…¡‰@ˆV^RqˆBbAŒnTXeRz¤Lž«³I"],
["@@ÆEE—„kWqë @œ"],
["@@fced"],
["@@„¯ɜÄèaì¯ØǓIġĽ"],
["@@çûĖ롖hòř "]
],
"encodeOffsets": [
[[122886, 24033]],
[[123335, 22980]],
[[122375, 24193]],
[[122518, 24117]],
[[124427, 22618]],
[[124862, 26043]],
[[126259, 26318]],
[[127671, 26683]]
]
}
},
{
"type": "Feature",
"id": "130000",
"properties": {
"id": "130000",
"cp": [114.502461, 38.045474],
"name": "河北",
"childNum": 3
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
["@@o~†Z]‚ªr‰ºc_ħ²G¼s`jΟnüsœłNX_“M`ǽÓnUK…Ĝēs¤­©yrý§uģŒc†JŠ›e"],
["@@U`Ts¿m‚"],
[
"@@oºƋÄd–eVŽDJj£€J|Ådz•Ft~žKŨ¸IÆv|”‡¢r}膎onb˜}`RÎÄn°ÒdÞ²„^®’lnÐèĄlðӜ×]ªÆ}LiĂ±Ö`^°Ç¶p®đDcœŋ`–ZÔ’¶êqvFƚ†N®ĆTH®¦O’¾ŠIbÐã´BĐɢŴÆíȦp–ĐÞXR€·nndOž¤’OÀĈƒ­Qg˜µFo|gȒęSWb©osx|hYh•gŃfmÖĩnº€T̒Sp›¢dYĤ¶UĈjl’ǐpäìë|³kÛfw²Xjz~ÂqbTŠÑ„ěŨ@|oM‡’zv¢ZrÃVw¬ŧˏfŒ°ÐT€ªqŽs{Sž¯r æÝlNd®²Ğ džiGʂJ™¼lr}~K¨ŸƐÌWö€™ÆŠzRš¤lêmĞL΄’@¡|q]SvK€ÑcwpÏρ†ĿćènĪWlĄkT}ˆJ”¤~ƒÈT„d„™pddʾĬŠ”ŽBVt„EÀ¢ôPĎƗè@~‚k–ü\\rÊĔÖæW_§¼F˜†´©òDòj’ˆYÈrbĞāøŀG{ƀ|¦ðrb|ÀH`pʞkv‚GpuARhÞÆǶgƊTǼƹS£¨¡ù³ŘÍ]¿Ây™ôEP xX¶¹܇O¡“gÚ¡IwÃ鑦ÅB‡Ï|ǰ…N«úmH¯‹âŸDùŽyŜžŲIÄuШDž•¸dɂ‡‚FŸƒ•›Oh‡đ©OŸ›iÃ`ww^ƒÌkŸ‘ÑH«ƇǤŗĺtFu…{Z}Ö@U‡´…ʚLg®¯Oı°ÃwŸ ^˜—€VbÉs‡ˆmA…ê]]w„§›RRl£‡ȭµu¯b{ÍDěïÿȧŽuT£ġƒěŗƃĝ“Q¨fV†Ƌ•ƅn­a@‘³@šď„yýIĹÊKšŭfċŰóŒxV@tˆƯŒJ”]eƒR¾fe|rHA˜|h~Ėƍl§ÏŠlTíb ØoˆÅbbx³^zÃ͚¶Sj®A”yÂhðk`š«P€”ˈµEF†Û¬Y¨Ļrõqi¼‰Wi°§’б´°^[ˆÀ|ĠO@ÆxO\\tŽa\\tĕtû{ġŒȧXýĪÓjùÎRb›š^ΛfK[ݏděYfíÙTyŽuUSyŌŏů@Oi½’éŅ­aVcř§ax¹XŻác‡žWU£ôãºQ¨÷Ñws¥qEH‰Ù|‰›šYQoŕÇyáĂ£MðoťÊ‰P¡mšWO¡€v†{ôvîēÜISpÌhp¨ ‘j†deŔQÖj˜X³à™Ĉ[n`Yp@Už–cM`’RKhŒEbœ”pŞlNut®Etq‚nsÁŠgA‹iú‹oH‡qCX‡”hfgu“~ϋWP½¢G^}¯ÅīGCŸÑ^ãziMáļMTÃƘrMc|O_ž¯Ŏ´|‡morDkO\\mĆJfl@c̬¢aĦtRıҙ¾ùƀ^juųœK­ƒUFy™—Ɲ…›īÛ÷ąV×qƥV¿aȉd³B›qPBm›aËđŻģm“Å®Vйd^K‡KoŸnYg“¯Xhqa”Ldu¥•ÍpDž¡KąÅƒkĝęěhq‡}HyÓ]¹ǧ£…Í÷¿qáµ§š™g‘¤o^á¾ZE‡¤i`ij{n•ƒOl»ŸWÝĔįhg›F[¿¡—ßkOüš_‰€ū‹i„DZàUtėGylƒ}ŒÓM}€jpEC~¡FtoQi‘šHkk{Ãmï‚"
]
],
"encodeOffsets": [[[119712, 40641]], [[121616, 39981]], [[116462, 37237]]]
}
},
{
"type": "Feature",
"id": "140000",
"properties": {
"id": "140000",
"cp": [111.849248, 36.857014],
"name": "山西",
"childNum": 1
},
"geometry": {
"type": "Polygon",
"coordinates": [
"@@Þĩ҃S‰ra}Á€yWix±Üe´lè“ßÓǏok‘ćiµVZģ¡coœ‘TS˹ĪmnÕńe–hZg{gtwªpXaĚThȑp{¶Eh—®RćƑP¿£‘Pmc¸mQÝW•ďȥoÅîɡųAďä³aωJ‘½¥PG­ąSM­™…EÅruµé€‘Yӎ•Ō_d›ĒCo­Èµ]¯_²ÕjāŽK~©ÅØ^ԛkïçămϑk]­±ƒcݯÑÃmQÍ~_a—pm…~ç¡q“ˆu{JÅŧ·Ls}–EyÁÆcI{¤IiCfUc•ƌÃp§]웫vD@¡SÀ‘µM‚ÅwuŽYY‡¡DbÑc¡hƒ×]nkoQdaMç~eD•ÛtT‰©±@¥ù@É¡‰ZcW|WqOJmĩl«ħşvOÓ«IqăV—¥ŸD[mI~Ó¢cehiÍ]Ɠ~ĥqXŠ·eƷœn±“}v•[ěďŽŕ]_‘œ•`‰¹ƒ§ÕōI™o©b­s^}Ét±ū«³p£ÿ·Wµ|¡¥ăFÏs׌¥ŅxŸÊdÒ{ºvĴÎêÌɊ²¶€ü¨|ÞƸµȲ‘LLúÉƎ¤ϊęĔV`„_bª‹S^|ŸdŠzY|dz¥p†ZbÆ£¶ÒK}tĦÔņƠ‚PYzn€ÍvX¶Ěn ĠÔ„zý¦ª˜÷žÑĸَUȌ¸‚dòÜJð´’ìúNM¬ŒXZ´‘¤ŊǸ_tldIš{¦ƀðĠȤ¥NehXnYG‚‡R° ƬDj¬¸|CĞ„Kq‚ºfƐiĺ©ª~ĆOQª ¤@ìǦɌ²æBŒÊ”TœŸ˜ʂōĖ’šĴŞ–ȀœÆÿȄlŤĒö„t”νî¼ĨXhŒ‘˜|ªM¤Ðz"
],
"encodeOffsets": [[116874, 41716]]
}
},
{
"type": "Feature",
"id": "150000",
"properties": {
"id": "150000",
"cp": [111.670801, 41.818311],
"name": "内蒙古",
"childNum": 2
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
"@@¯PqƒFB…‰|S•³C|kñ•H‹d‘iÄ¥sˆʼnő…PóÑÑE^‘ÅPpy_YtS™hQ·aHwsOnʼnÚs©iqj›‰€USiº]ïWš‰«gW¡A–Rë¥_ŽsgÁnUI«m‰…„‹]j‡vV¼euhwqA„aW˜ƒ_µj…»çjioQR¹ēÃßt@r³[ÛlćË^ÍÉáG“›OUۗOB±•XŸkŇ¹£k|e]ol™ŸkVͼÕqtaÏõjgÁ£§U^Œ”RLˆËnX°Ç’Bz†^~wfvˆypV ¯„ƫĉ˭ȫƗŷɿÿĿƑ˃ĝÿÃǃßËőó©ǐȍŒĖM×ÍEyx‹þp]Évïè‘vƀnÂĴÖ@‚‰†V~Ĉv¦wĖt—ējyÄDXÄxGQuv_›i¦aBçw‘˛wD™©{ŸtāmQ€{EJ§KPśƘƿ¥@‰sCT•É}ɃwˆƇy±ŸgÑ“}T[÷kÐ禫…SÒ¥¸ëBX½‰HáŵÀğtSÝÂa[ƣ°¯¦P]£ġ“–“Òk®G²„èQ°óMq}EŠóƐÇ\\ƒ‡@áügQ͋u¥Fƒ“T՛¿Jû‡]|mvāÎYua^WoÀa·­ząÒot×¶CLƗi¯¤mƎHNJ¤îìɾŊìTdåwsRÖgĒųúÍġäÕ}Q¶—ˆ¿A•†‹[¡Œ{d×uQAƒ›M•xV‹vMOmăl«ct[wº_šÇʊŽŸjb£ĦS_é“QZ“_lwgOiýe`YYLq§IÁˆdz£ÙË[ÕªuƏ³ÍT—s·bÁĽäė[›b[ˆŗfãcn¥îC¿÷µ[ŏÀQ­ōšĉm¿Á^£mJVm‡—L[{Ï_£›F¥Ö{ŹA}…×Wu©ÅaųijƳhB{·TQqÙIķˑZđ©Yc|M¡…L•eVUóK_QWk’_ĥ‘¿ãZ•»X\\ĴuUƒè‡lG®ěłTĠğDєOrÍd‚ÆÍz]‹±…ŭ©ŸÅ’]ŒÅÐ}UË¥©Tċ™ïxgckfWgi\\ÏĒ¥HkµE˜ë{»ÏetcG±ahUiñiWsɁˆ·c–C‚Õk]wȑ|ća}w…VaĚ᠞ŒG°ùnM¬¯†{ÈˆÐÆA’¥ÄêJxÙ¢”hP¢Ûˆº€µwWOŸóFŽšÁz^ÀŗÎú´§¢T¤ǻƺSė‰ǵhÝÅQgvBHouʝl_o¿Ga{ïq{¥|ſĿHĂ÷aĝÇq‡Z‘ñiñC³ª—…»E`¨åXēÕqÉû[l•}ç@čƘóO¿¡ƒFUsA‰“ʽīccšocƒ‚ƒÇS}„“£‡IS~ălkĩXçmĈ…ŀЂoÐdxÒuL^T{r@¢‘žÍƒĝKén£kQ™‰yšÅõËXŷƏL§~}kqš»IHėDžjĝŸ»ÑÞoŸå°qTt|r©ÏS‹¯·eŨĕx«È[eMˆ¿yuˆ‘pN~¹ÏyN£{©’—g‹ħWí»Í¾s“əšDž_ÃĀɗ±ą™ijĉʍŌŷ—S›É“A‹±åǥɋ@럣R©ąP©}ĹªƏj¹erƒLDĝ·{i«ƫC£µsKCš…GS|úþX”gp›{ÁX¿Ÿć{ƱȏñZáĔyoÁhA™}ŅĆfdʼn„_¹„Y°ėǩÑ¡H¯¶oMQqð¡Ë™|‘Ñ`ƭŁX½·óۓxğįÅcQ‡ˆ“ƒs«tȋDžF“Ÿù^i‘t«Č¯[›hAi©á¥ÇĚ×l|¹y¯YȵƓ‹ñǙµï‚ċ™Ļ|Dœ™üȭ¶¡˜›oŽäÕG\\ďT¿Òõr¯œŸLguÏYęRƩšɷŌO\\İТæ^Ŋ IJȶȆbÜGŽĝ¬¿ĚVĎgª^íu½jÿĕęjık@Ľƒ]ėl¥Ë‡ĭûÁ„ƒėéV©±ćn©­ȇžÍq¯½•YÃÔʼn“ÉNѝÅÝy¹NqáʅDǡËñ­ƁYÅy̱os§ȋµʽǘǏƬɱà‘ưN¢ƔÊuľýľώȪƺɂļžxœZĈ}ÌʼnŪ˜ĺœŽĭFЛĽ̅ȣͽÒŵìƩÇϋÿȮǡŏçƑůĕ~Ǎ›¼ȳÐUf†dIxÿ\\G ˆzâɏÙOº·pqy£†@ŒŠqþ@Ǟ˽IBäƣzsÂZ†ÁàĻdñ°ŕzéØűzșCìDȐĴĺf®ŽÀľưø@ɜÖÞKĊŇƄ§‚͑těï͡VAġÑÑ»d³öǍÝXĉĕÖ{þĉu¸ËʅğU̎éhɹƆ̗̮ȘNJ֥ड़ࡰţાíϲäʮW¬®ҌeרūȠkɬɻ̼ãüfƠSצɩςåȈHϚÎKdzͲOðÏȆƘ¼CϚǚ࢚˼ФԂ¤ƌžĞ̪Qʤ´¼mȠJˀŸƲÀɠmǐnǔĎȆÞǠN~€ʢĜ‚¶ƌĆĘźʆȬ˪ĚǏĞGȖƴƀj`ĢçĶāàŃºē̃ĖćšYŒÀŎüôQÐÂŎŞdžŞêƖš˜oˆDĤÕºÑǘÛˤ³̀gńƘĔÀ^žªƂ`ªt¾äƚêĦĀ¼Ð€Ĕǎ¨Ȕ»͠^ˮÊȦƤøxRrŜH¤¸ÂxDĝŒ|ø˂˜ƮÐ¬ɚwɲFjĔ²Äw°dždÀɞ_ĸdîàŎjʜêTĞªŌ‡ŜWÈ|tqĢUB~´°ÎFC•ŽU¼pĀēƄN¦¾O¶ŠłKĊOj“Ě”j´ĜYp˜{¦„ˆSĚÍ\\Tš×ªV–÷Ší¨ÅDK°ßtŇĔKš¨ǵÂcḷ̌ĚǣȄĽF‡lġUĵœŇ‹ȣFʉɁƒMğįʏƶɷØŭOǽ«ƽū¹Ʊő̝Ȩ§ȞʘĖiɜɶʦ}¨֪ࠜ̀ƇǬ¹ǨE˦ĥªÔêFŽxúQ„Er´W„rh¤Ɛ \\talĈDJ˜Ü|[Pll̚¸ƎGú´Pž¬W¦†^¦–H]prR“n|or¾wLVnÇIujkmon£cX^Bh`¥V”„¦U¤¸}€xRj–[^xN[~ªŠxQ„‚[`ªHÆÂExx^wšN¶Ê˜|¨ì†˜€MrœdYp‚oRzNy˜ÀDs~€bcfÌ`L–¾n‹|¾T‚°c¨È¢a‚r¤–`[|òDŞĔöxElÖdH„ÀI`„Ď\\Àì~ƎR¼tf•¦^¢ķ¶e”ÐÚMŒptgj–„ɡČÅyġLû™ŇV®ŠÄÈƀ†Ď°P|ªVV†ªj–¬ĚÒêp¬–E|ŬÂc|ÀtƐK fˆ{ĘFǜƌXƲąo½Ę‘\\¥–o}›Ûu£ç­kX‘{uĩ«āíÓUŅßŢq€Ť¥lyň[€oi{¦‹L‡ń‡ðFȪȖ”ĒL„¿Ì‹ˆfŒ£K£ʺ™oqNŸƒwğc`ue—tOj×°KJ±qƒÆġm‰Ěŗos¬…qehqsuœƒH{¸kH¡Š…ÊRǪÇƌbȆ¢´ä܍¢NìÉʖ¦â©Ġu¦öČ^â£Ăh–šĖMÈÄw‚\\fŦ°W ¢¾luŸD„wŠ\\̀ʉÌÛM…Ā[bӞEn}¶Vc…ê“sƒ"
]
],
"encodeOffsets": [[[129102, 52189]]]
}
},
{
"type": "Feature",
"id": "210000",
"properties": {
"id": "210000",
"cp": [123.429096, 41.796767],
"name": "辽宁",
"childNum": 16
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
["@@L–Ž@@s™a"],
["@@MnNm"],
["@@d‚c"],
["@@eÀ‚C@b‚“‰"],
["@@f‡…Xwkbr–Ä`qg"],
["@@^jtW‘Q"],
["@@~ Y]c"],
["@@G`ĔN^_¿Z‚ÃM"],
["@@iX¶B‹Y"],
["@@„YƒZ"],
["@@L_{Epf"],
["@@^WqCT\\"],
["@@\\[“‹§t|”¤_"],
["@@m`n_"],
["@@Ïxnj{q_×^Giip"],
[
"@@@œé^B†‡ntˆaÊU—˜Ÿ]x ¯ÄPIJ­°h€ʙK³†VˆÕ@Y~†|EvĹsDŽ¦­L^p²ŸÒG ’Ël]„xxÄ_˜fT¤Ď¤cŽœP„–C¨¸TVjbgH²sdÎdHt`Bˆ—²¬GJję¶[ÐhjeXdlwhšðSȦªVÊπ‹Æ‘Z˜ÆŶ®²†^ŒÎyÅÎcPqń“ĚDMħĜŁH­ˆk„çvV[ij¼W–‚YÀäĦ’‘`XlžR`žôLUVžfK–¢†{NZdĒª’YĸÌÚJRr¸SA|ƴgŴĴÆbvªØX~†źBŽ|¦ÕœEž¤Ð`\\|Kˆ˜UnnI]¤ÀÂĊnŎ™R®Ő¿¶\\ÀøíDm¦ÎbŨab‰œaĘ\\ľã‚¸a˜tÎSƐ´©v\\ÖÚÌǴ¤Â‡¨JKr€Z_Z€fjþhPkx€`Y”’RIŒjJcVf~sCN¤ ˆE‚œhæm‰–sHy¨SðÑÌ\\\\ŸĐRZk°IS§fqŒßýáЍÙÉÖ[^¯ǤŲ„ê´\\¦¬ĆPM¯£Ÿˆ»uïpùzEx€žanµyoluqe¦W^£ÊL}ñrkqWňûP™‰UP¡ôJŠoo·ŒU}£Œ„[·¨@XŒĸŸ“‹‹DXm­Ûݏº‡›GU‹CÁª½{íĂ^cj‡k“¶Ã[q¤“LÉö³cux«zZfƒ²BWÇ®Yß½ve±ÃC•ý£W{Ú^’q^sÑ·¨‹ÍOt“¹·C¥‡GD›rí@wÕKţ݋˜Ÿ«V·i}xËÍ÷‘i©ĝ‡ɝǡ]ƒˆ{c™±OW‹³Ya±Ÿ‰_穂Hžĕoƫ€Ňqƒr³‰Lys[„ñ³¯OS–ďOMisZ†±ÅFC¥Pq{‚Ã[Pg}\\—¿ghćO…•k^ģÁFıĉĥM­oEqqZûěʼn³F‘¦oĵ—hŸÕP{¯~TÍlª‰N‰ßY“Ð{Ps{ÃVU™™eĎwk±ʼnVÓ½ŽJãÇÇ»Jm°dhcÀff‘dF~ˆ€ĀeĖ€d`sx² šƒ®EżĀdQ‹Âd^~ăÔHˆ¦\\›LKpĄVez¤NP ǹӗR™ÆąJSh­a[¦´Âghwm€BÐ¨źhI|žVVŽ—Ž|p] Â¼èNä¶ÜBÖ¼“L`‚¼bØæŒKV”ŸpoœúNZÞÒKxpw|ÊEMnzEQšŽIZ”ŽZ‡NBˆčÚFÜçmĩ‚WĪñt‘ÞĵÇñZ«uD‚±|Əlij¥ãn·±PmÍa‰–da‡ CL‡Ǒkùó¡³Ï«QaċϑOÃ¥ÕđQȥċƭy‹³ÃA"
]
],
"encodeOffsets": [
[[123686, 41445]],
[[126019, 40435]],
[[124393, 40128]],
[[126117, 39963]],
[[125322, 40140]],
[[126686, 40700]],
[[126041, 40374]],
[[125584, 40168]],
[[125453, 40165]],
[[125362, 40214]],
[[125280, 40291]],
[[125774, 39997]],
[[125976, 40496]],
[[125822, 39993]],
[[125509, 40217]],
[[122731, 40949]]
]
}
},
{
"type": "Feature",
"id": "220000",
"properties": { "id": "220000", "cp": [125.3245, 43.886841], "name": "吉林", "childNum": 1 },
"geometry": {
"type": "Polygon",
"coordinates": [
"@@‘p䔳PClƒFbbÍzš€wBG’ĭ€Z„Åi“»ƒlY­ċ²SgŽkÇ£—^S‰“qd¯•‹R…©éŽ£¯S†\\cZ¹iűƏCuƍÓX‡oR}“M^o•£…R}oªU­F…uuXHlEŕ‡€Ï©¤ÛmTŽþ¤D–²ÄufàÀ­XXȱAe„yYw¬dvõ´KÊ£”\\rµÄl”iˆdā]|DÂVŒœH¹ˆÞ®ÜWnŒC”Œķ W‹§@\\¸‹ƒ~¤‹Vp¸‰póIO¢ŠVOšŇürXql~òÉK]¤¥Xrfkvzpm¶bwyFoúvð‡¼¤ N°ąO¥«³[ƒéǡű_°Õ\\ÚÊĝŽþâőàerR¨­JYlďQ[ ÏYëЧTGz•tnŠß¡gFkMŸāGÁ¤ia É‰™È¹`\\xs€¬dĆkNnuNUŠ–užP@‚vRY¾•–\\¢…ŒGªóĄ~RãÖÎĢù‚đŴÕhQŽxtcæëSɽʼníëlj£ƍG£nj°KƘµDsØÑpyƸ®¿bXp‚]vbÍZuĂ{nˆ^IüœÀSք”¦EŒvRÎûh@℈[‚Əȉô~FNr¯ôçR±ƒ­HÑl•’Ģ–^¤¢‚OðŸŒævxsŒ]ÞÁTĠs¶¿âƊGW¾ìA¦·TѬ†è¥€ÏÐJ¨¼ÒÖ¼ƒƦɄxÊ~S–tD@ŠĂ¼Ŵ¡jlºWžvЉˆzƦZЎ²CH— „Axiukd‹ŒGgetqmcžÛ£Ozy¥cE}|…¾cZ…k‚‰¿uŐã[oxGikfeäT@…šSUwpiÚFM©’£è^ڟ‚`@v¶eň†f h˜eP¶žt“äOlÔUgƒÞzŸU`lœ}ÔÆUvØ_Ō¬Öi^ĉi§²ÃŠB~¡Ĉ™ÚEgc|DC_Ȧm²rBx¼MÔ¦ŮdĨÃâYx‘ƘDVÇĺĿg¿cwÅ\\¹˜¥Yĭlœ¤žOv†šLjM_a W`zļMž·\\swqÝSA‡š—q‰Śij¯Š‘°kŠRē°wx^Đkǂғ„œž“œŽ„‹\\]˜nrĂ}²ĊŲÒøãh·M{yMzysěnĒġV·°“G³¼XÀ““™¤¹i´o¤ŃšŸÈ`̃DzÄUĞd\\i֚ŒˆmÈBĤÜɲDEh LG¾ƀľ{WaŒYÍȏĢĘÔRîĐj‹}Ǟ“ccj‡oUb½š{“h§Ǿ{K‹ƖµÎ÷žGĀÖŠåưÎs­l›•yiē«‹`姝H¥Ae^§„GK}iã\\c]v©ģZ“mÃ|“[M}ģTɟĵ‘Â`À–çm‰‘FK¥ÚíÁbXš³ÌQґHof{‰]e€pt·GŋĜYünĎųVY^’˜ydõkÅZW„«WUa~U·Sb•wGçǑ‚“iW^q‹F‚“›uNĝ—·Ew„‹UtW·Ýďæ©PuqEzwAV•—XR‰ãQ`­©GŒM‡ehc›c”ďϝd‡©ÑW_ϗYƅŒ»…é\\ƒɹ~ǙG³mØ©BšuT§Ĥ½¢Ã_ý‘L¡‘ýŸqT^rme™\\Pp•ZZbƒyŸ’uybQ—efµ]UhĿDCmûvašÙNSkCwn‰cćfv~…Y‹„ÇG"
],
"encodeOffsets": [[130196, 42528]]
}
},
{
"type": "Feature",
"id": "230000",
"properties": {
"id": "230000",
"cp": [128.642464, 46.756967],
"name": "黑龙江",
"childNum": 2
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
"@@UƒµNÿ¥īè灋•HÍøƕ¶LŒǽ|g¨|”™Ža¾pViˆdd”~ÈiŒíďÓQġėǐZ΋ŽXb½|ſÃH½ŸKFgɱCģÛÇA‡n™‹jÕc[VĝDZÃ˄Ç_™ £ń³pŽj£º”š¿”»WH´¯”U¸đĢmžtĜyzzNN|g¸÷äűѱĉā~mq^—Œ[ƒ”››”ƒǁÑďlw]¯xQĔ‰¯l‰’€°řĴrŠ™˜BˆÞTxr[tޏĻN_yŸX`biN™Ku…P›£k‚ZĮ—¦[ºxÆÀdhŽĹŀUÈƗCw’áZħÄŭcÓ¥»NAw±qȥnD`{ChdÙFćš}¢‰A±Äj¨]ĊÕjŋ«×`VuÓś~_kŷVÝyh„“VkÄãPs”Oµ—fŸge‚Ň…µf@u_Ù ÙcŸªNªÙEojVx™T@†ãSefjlwH\\pŏäÀvŠŽlY†½d{†F~¦dyz¤PÜndsrhf‹HcŒvlwjFœ£G˜±DύƥY‡yϊu¹XikĿ¦ÏqƗǀOŜ¨LI|FRĂn sª|Cš˜zxAè¥bœfudTrFWÁ¹Am|˜ĔĕsķÆF‡´Nš‰}ć…UŠÕ@Áijſmužç’uð^ÊýowŒFzØÎĕNőžǏȎôªÌŒDŽàĀÄ˄ĞŀƒʀĀƘŸˮȬƬĊ°ƒUŸzou‡xe]}Ž…AyȑW¯ÌmK‡“Q]‹Īºif¸ÄX|sZt|½ÚUΠlkš^p{f¤lˆºlÆW –€A²˜PVܜPH”Êâ]ÎĈÌÜk´\\@qàsĔÄQºpRij¼èi†`¶—„bXƒrBgxfv»ŽuUiˆŒ^v~”J¬mVp´£Œ´VWrnP½ì¢BX‚¬h™ŠðX¹^TjVœŠriªj™tŊÄm€tPGx¸bgRšŽsT`ZozÆO]’ÒFô҆Oƒ‡ŊŒvŞ”p’cGŒêŠsx´DR–Œ{A†„EOr°Œ•žx|íœbˆ³Wm~DVjºéNN†Ëܲɶ­GƒxŷCStŸ}]ûō•SmtuÇÃĕN•™āg»šíT«u}ç½BĵÞʣ¥ëÊ¡Mێ³ãȅ¡ƋaǩÈÉQ‰†G¢·lG|›„tvgrrf«†ptęŘnŠÅĢr„I²¯LiØsPf˜_vĠd„xM prʹšL¤‹¤‡eˌƒÀđK“žïÙVY§]I‡óáĥ]ķ†Kˆ¥Œj|pŇ\\kzţ¦šnņäÔVĂîά|vW’®l¤èØr‚˜•xm¶ă~lÄƯĄ̈́öȄEÔ¤ØQĄ–Ą»ƢjȦOǺ¨ìSŖÆƬy”Qœv`–cwƒZSÌ®ü±DŽ]ŀç¬B¬©ńzƺŷɄeeOĨS’Œfm Ċ‚ƀP̎ēz©Ċ‚ÄÕÊmgŸÇsJ¥ƔˆŊśæ’΁Ñqv¿íUOµª‰ÂnĦÁ_½ä@ê텣P}Ġ[@gġ}g“ɊדûÏWXá¢užƻÌsNͽƎÁ§č՛AēeL³àydl›¦ĘVçŁpśdžĽĺſʃQíÜçÛġԏsĕ¬—Ǹ¯YßċġHµ ¡eå`ļƒrĉŘóƢFì“ĎWøxÊk†”ƈdƬv|–I|·©NqńRŀƒ¤é”eŊœŀ›ˆàŀU²ŕƀB‚Q£Ď}L¹Îk@©ĈuǰųǨ”Ú§ƈnTËÇéƟÊcfčŤ^Xm‡—HĊĕË«W·ċëx³ǔķÐċJā‚wİ_ĸ˜Ȁ^ôWr­°oú¬Ħ…ŨK~”ȰCĐ´Ƕ£’fNÎèâw¢XnŮeÂÆĶŽ¾¾xäLĴĘlļO¤ÒĨA¢Êɚ¨®‚ØCÔ ŬGƠ”ƦYĜ‡ĘÜƬDJ—g_ͥœ@čŅĻA“¶¯@wÎqC½Ĉ»NŸăëK™ďÍQ“Ùƫ[«Ãí•gßÔÇOÝáW‘ñuZ“¯ĥ€Ÿŕā¡ÑķJu¤E Ÿå¯°WKɱ_d_}}vyŸõu¬ï¹ÓU±½@gÏ¿rýD‰†g…Cd‰µ—°MFYxw¿CG£‹Rƛ½Õ{]L§{qqąš¿BÇƻğëšܭNJË|c²}Fµ}›ÙRsÓpg±ŠQNqǫŋRwŕnéÑÉKŸ†«SeYR…ŋ‹@{¤SJ}šD Ûǖ֍Ÿ]gr¡µŷjqWÛham³~S«“„›Þ]"
]
],
"encodeOffsets": [[[134456, 44547]]]
}
},
{
"type": "Feature",
"id": "320000",
"properties": {
"id": "320000",
"cp": [119.767413, 33.041544],
"name": "江苏",
"childNum": 1
},
"geometry": {
"type": "Polygon",
"coordinates": [
"@@cþÅPiŠ`ZŸRu¥É\\]~°ŽY`µ†Óƒ^phÁbnÀşúŽòa–ĬºTÖŒb‚˜e¦¦€{¸ZâćNpŒ©žHr|^ˆmjhŠSEb\\afv`sz^lkŽlj‹Ätg‹¤D˜­¾Xš¿À’|ДiZ„ȀåB·î}GL¢õcßjaŸyBFµÏC^ĭ•cÙt¿sğH]j{s©HM¢ƒQnDÀ©DaÜތ·jgàiDbPufjDk`dPOîƒhw¡ĥ‡¥šG˜ŸP²ĐobºrY†„î¶aHŢ´ ]´‚rılw³r_{£DB_Ûdåuk|ˆŨ¯F Cºyr{XFy™e³Þċ‡¿Â™kĭB¿„MvÛpm`rÚã”@ƹhågËÖƿxnlč¶Åì½Ot¾dJlŠVJʜǀœŞqvnOŠ^ŸJ”Z‘ż·Q}ê͎ÅmµÒ]Žƍ¦Dq}¬R^èĂ´ŀĻĊIԒtžIJyQŐĠMNtœR®òLh‰›Ěs©»œ}OӌGZz¶A\\jĨFˆäOĤ˜HYš†JvÞHNiÜaϚɖnFQlšNM¤ˆB´ĄNöɂtp–Ŭdf先‹qm¿QûŠùއÚb¤uŃJŴu»¹Ą•lȖħŴw̌ŵ²ǹǠ͛hĭłƕrçü±Y™xci‡tğ®jű¢KOķ•Coy`å®VTa­_Ā]ŐÝɞï²ʯÊ^]afYǸÃĆēĪȣJđ͍ôƋĝÄ͎ī‰çÛɈǥ£­ÛmY`ó£Z«§°Ó³QafusNıDž_k}¢m[ÝóDµ—¡RLčiXy‡ÅNïă¡¸iĔϑNÌŕoēdōîåŤûHcs}~Ûwbù¹£¦ÓCt‹OPrƒE^ÒoŠg™ĉIµžÛÅʹK…¤½phMŠü`o怆ŀ"
],
"encodeOffsets": [[121740, 32276]]
}
},
{
"type": "Feature",
"id": "330000",
"properties": {
"id": "330000",
"cp": [120.153576, 29.287459],
"name": "浙江",
"childNum": 45
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
["@@E^dQ]K"],
["@@jX^j‡"],
["@@sfŠbU‡"],
["@@qP\\xz[ck"],
["@@‘Rƒ¢‚FX}°[s_"],
["@@Cbœ\\—}"],
["@@e|v\\la{u"],
["@@v~u}"],
["@@QxÂF¯}"],
["@@¹nŒvÞs¯o"],
["@@rSkUEj"],
["@@bi­ZŒP"],
["@@p[}INf"],
["@@À¿€"],
["@@¹dnbŒ…"],
["@@rSŸBnR"],
["@@g~h}"],
["@@FlEk"],
["@@OdPc"],
["@@v[u\\"],
["@@FjâL~wyoo~›sµL–\\"],
["@@¬e¹aNˆ"],
["@@\\nÔ¡q]L³ë\\ÿ®ŒQ֎"],
["@@ÊA­©[¬"],
["@@KxŒv­"],
["@@@hlIk]"],
["@@pW{o||j"],
["@@Md|_mC"],
["@@¢…X£ÏylD¼XˆtH"],
["@@hlÜ[LykAvyfw^Ež›¤"],
["@@fp¤Mus“R"],
["@@®_ma~•LÁ¬šZ"],
["@@iM„xZ"],
["@@ZcYd"],
["@@Z~dOSo|A¿qZv"],
["@@@`”EN¡v"],
["@@|–TY{"],
["@@@n@m"],
["@@XWkCT\\"],
["@@ºwšZRkĕWO¢"],
["@@™X®±Grƪ\\ÔáXq{‹"],
["@@ůTG°ĄLHm°UC‹"],
[
"@@¤Ž€aÜx~}dtüGæţŎíĔcŖpMËВj碷ðĄÆMzˆjWKĎ¢Q¶˜À_꒔_Bı€i«pZ€gf€¤Nrq]§ĂN®«H±‡yƳí¾×ŸīàLłčŴǝĂíÀBŖÕªˆŠÁŖHŗʼnåqûõi¨hÜ·ƒñt»¹ýv_[«¸m‰YL¯‰Qª…mĉÅdMˆ•gÇjcº«•ęœ¬­K­´ƒB«Âącoċ\\xKd¡gěŧ«®á’[~ıxu·Å”KsËɏc¢Ù\\ĭƛëbf¹­ģSƒĜkáƉÔ­ĈZB{ŠaM‘µ‰fzʼnfåÂŧįƋǝÊĕġć£g³ne­ą»@­¦S®‚\\ßðCšh™iqªĭiAu‡A­µ”_W¥ƣO\\lċĢttC¨£t`ˆ™PZäuXßBs‡Ļyek€OđġĵHuXBšµ]׌‡­­\\›°®¬F¢¾pµ¼kŘó¬Wät’¸|@ž•L¨¸µr“ºù³Ù~§WI‹ŸZWŽ®’±Ð¨ÒÉx€`‰²pĜ•rOògtÁZ}þÙ]„’¡ŒŸFK‚wsPlU[}¦Rvn`hq¬\\”nQ´ĘRWb”‚_ rtČFI֊kŠŠĦPJ¶ÖÀÖJĈĄTĚòžC ²@Pú…Øzœ©PœCÈÚœĒ±„hŖ‡l¬â~nm¨f©–iļ«m‡nt–u†ÖZÜÄj“ŠLŽ®E̜Fª²iÊxبžIÈhhst"
],
["@@o\\V’zRZ}y"],
["@@†@°¡mۛGĕ¨§Ianá[ýƤjfæ‡ØL–•äGr™"]
],
"encodeOffsets": [
[[125592, 31553]],
[[125785, 31436]],
[[125729, 31431]],
[[125513, 31380]],
[[125223, 30438]],
[[125115, 30114]],
[[124815, 29155]],
[[124419, 28746]],
[[124095, 28635]],
[[124005, 28609]],
[[125000, 30713]],
[[125111, 30698]],
[[125078, 30682]],
[[125150, 30684]],
[[124014, 28103]],
[[125008, 31331]],
[[125411, 31468]],
[[125329, 31479]],
[[125626, 30916]],
[[125417, 30956]],
[[125254, 30976]],
[[125199, 30997]],
[[125095, 31058]],
[[125083, 30915]],
[[124885, 31015]],
[[125218, 30798]],
[[124867, 30838]],
[[124755, 30788]],
[[124802, 30809]],
[[125267, 30657]],
[[125218, 30578]],
[[125200, 30562]],
[[124968, 30474]],
[[125167, 30396]],
[[124955, 29879]],
[[124714, 29781]],
[[124762, 29462]],
[[124325, 28754]],
[[123990, 28459]],
[[125366, 31477]],
[[125115, 30363]],
[[125369, 31139]],
[[122495, 31878]],
[[125329, 30690]],
[[125192, 30787]]
]
}
},
{
"type": "Feature",
"id": "340000",
"properties": { "id": "340000", "cp": [117.283042, 31.26119], "name": "安徽", "childNum": 3 },
"geometry": {
"type": "MultiPolygon",
"coordinates": [
["@@^iuLX^"],
["@@‚e©Ehl"],
[
"@@°ZÆëϵmkǀwÌÕæhºgBĝâqÙĊz›ÖgņtÀÁÊÆá’hEz|WzqD¹€Ÿ°E‡ŧl{ævÜcA`¤C`|´qžxIJkq^³³ŸGšµbƒíZ…¹qpa±ď OH—¦™Ħˆx¢„gPícOl_iCveaOjCh߸i݋bÛªCC¿€m„RV§¢A|t^iĠGÀtÚs–d]ĮÐDE¶zAb àiödK¡~H¸íæAžǿYƒ“j{ď¿‘™À½W—®£ChŒÃsiŒkkly]_teu[bFa‰Tig‡n{]Gqªo‹ĈMYá|·¥f¥—őaSÕė™NµñĞ«ImŒ_m¿Âa]uĜp …Z_§{Cƒäg¤°r[_Yj‰ÆOdý“[ŽI[á·¥“Q_n‡ùgL¾mv™ˊBÜÆ¶ĊJhšp“c¹˜O]iŠ]œ¥ jtsggJǧw×jÉ©±›EFˍ­‰Ki”ÛÃÕYv…s•ˆm¬njĻª•§emná}k«ŕˆƒgđ²Ù›DǤ›í¡ªOy›†×Où±@DŸñSęćăÕIÕ¿IµĥO‰‰jNÕËT¡¿tNæŇàåyķrĕq§ÄĩsWÆßŽF¶žX®¿‰mŒ™w…RIޓfßoG‘³¾©uyH‘į{Ɓħ¯AFnuP…ÍÔzšŒV—dàôº^Ðæd´€‡oG¤{S‰¬ćxã}›ŧ×Kǥĩ«žÕOEзÖdÖsƘѨ[’Û^Xr¢¼˜§xvěƵ`K”§ tÒ´Cvlo¸fzŨð¾NY´ı~ÉĔē…ßúLÃϖ_ÈÏ|]ÂÏFl”g`bšežž€n¾¢pU‚h~ƴ˶_‚r sĄ~cž”ƈ]|r c~`¼{À{ȒiJjz`îÀT¥Û³…]’u}›f…ïQl{skl“oNdŸjŸäËzDvčoQŠďHI¦rb“tHĔ~BmlRš—V_„ħTLnñH±’DžœL‘¼L˜ªl§Ťa¸ŒĚlK²€\\RòvDcÎJbt[¤€D@®hh~kt°ǾzÖ@¾ªdb„YhüóZ ň¶vHrľ\\ʗJuxAT|dmÀO„‹[ÃԋG·ĚąĐlŪÚpSJ¨ĸˆLvÞcPæķŨŽ®mАˆálŸwKhïgA¢ųƩޖ¤OȜm’°ŒK´"
]
],
"encodeOffsets": [[[121722, 32278]], [[119475, 30423]], [[119168, 35472]]]
}
},
{
"type": "Feature",
"id": "350000",
"properties": {
"id": "350000",
"cp": [118.306239, 26.075302],
"name": "福建",
"childNum": 18
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
["@@“zht´‡]"],
["@@aj^~ĆG—©O"],
["@@ed¨„C}}i"],
["@@@vˆPGsQ"],
["@@‰sBz‚ddW]Q"],
["@@SލQ“{"],
["@@NŽVucW"],
["@@qptBAq"],
["@@‰’¸[mu"],
["@@Q\\pD]_"],
["@@jSwUadpF"],
["@@eXª~ƒ•"],
["@@AjvFso"],
["@@fT–›_Çí\\Ÿ™—v|ba¦jZÆy€°"],
["@@IjJi"],
["@@wJI€ˆxš«¼AoNe{M­"],
["@@K‰±¡Óˆ”ČäeZ"],
[
"@@k¡¹Eh~c®wBk‹UplÀ¡I•~Māe£bN¨gZý¡a±Öcp©PhžI”Ÿ¢Qq…ÇGj‹|¥U™ g[Ky¬ŏ–v@OpˆtÉEŸF„\\@ åA¬ˆV{Xģ‰ĐBy…cpě…¼³Ăp·¤ƒ¥o“hqqÚ¡ŅLsƒ^ᗞ§qlŸÀhH¨MCe»åÇGD¥zPO£čÙkJA¼ß–ėu›ĕeûҍiÁŧSW¥˜QŠûŗ½ùěcݧSùĩąSWó«íęACµ›eR—åǃRCÒÇZÍ¢‹ź±^dlsŒtjD¸•‚ZpužÔâÒH¾oLUêÃÔjjēò´ĄW‚ƛ…^Ñ¥‹ĦŸ@Çò–ŠmŒƒOw¡õyJ†yD}¢ďÑÈġfŠZd–a©º²z£šN–ƒjD°Ötj¶¬ZSÎ~¾c°¶Ðm˜x‚O¸¢Pl´žSL|¥žA†ȪĖM’ņIJg®áIJČĒü` ŽQF‡¬h|ÓJ@zµ |ê³È ¸UÖŬŬÀEttĸr‚]€˜ðŽM¤ĶIJHtÏ A’†žĬkvsq‡^aÎbvŒd–™fÊòSD€´Z^’xPsÞrv‹ƞŀ˜jJd×ŘÉ ®A–ΦĤd€xĆqAŒ†ZR”ÀMźŒnĊ»ŒİÐZ— YX–æJŠyĊ²ˆ·¶q§·–K@·{s‘Xãô«lŗ¶»o½E¡­«¢±¨Yˆ®Ø‹¶^A™vWĶGĒĢžPlzfˆļŽtàAvWYãšO_‡¤sD§ssČġ[kƤPX¦Ž`¶“ž®ˆBBvĪjv©šjx[L¥àï[F…¼ÍË»ğV`«•Ip™}ccÅĥZE‹ãoP…´B@ŠD—¸m±“z«Ƴ—¿å³BRضˆœWlâþäą`“]Z£Tc— ĹGµ¶H™m@_©—kŒ‰¾xĨ‡ôȉðX«½đCIbćqK³Á‹Äš¬OAwã»aLʼn‡ËĥW[“ÂGI—ÂNxij¤D¢ŽîĎÎB§°_JœGsƒ¥E@…¤uć…P‘å†cuMuw¢BI¿‡]zG¹guĮck\\_"
]
],
"encodeOffsets": [
[[123250, 27563]],
[[122541, 27268]],
[[123020, 27189]],
[[122916, 27125]],
[[122887, 26845]],
[[122808, 26762]],
[[122568, 25912]],
[[122778, 26197]],
[[122515, 26757]],
[[122816, 26587]],
[[123388, 27005]],
[[122450, 26243]],
[[122578, 25962]],
[[121255, 25103]],
[[120987, 24903]],
[[122339, 25802]],
[[121042, 25093]],
[[122439, 26024]]
]
}
},
{
"type": "Feature",
"id": "360000",
"properties": {
"id": "360000",
"cp": [115.592151, 27.676493],
"name": "江西",
"childNum": 1
},
"geometry": {
"type": "Polygon",
"coordinates": [
"@@ĢĨƐgÂMD~ņªe^\\^§„ý©j׍cZ†Ø¨zdÒa¶ˆlҍJŒìõ`oz÷@¤u޸´†ôęöY¼‰HČƶajlÞƩ¥éZ[”|h}^U Œ ¥p„ĄžƦO lt¸Æ €Q\\€ŠaÆ|CnÂOjt­ĚĤd’ÈŒF`’¶„@Ð딠¦ōҞ¨Sêv†HĢûXD®…QgėWiØPÞìºr¤dž€NĠ¢l–•ĄtZoœCƞÔºCxrpĠV®Ê{f_Y`_ƒeq’’®Aot`@o‚DXfkp¨|Šs¬\\D‘ÄSfè©Hn¬…^DhÆyøJh“ØxĢĀLʈ„ƠPżċĄwȠ̦G®ǒĤäTŠÆ~ĦwŠ«|TF¡Šn€c³Ïå¹]ĉđxe{ÎӐ†vOEm°BƂĨİ|G’vz½ª´€H’àp”eJ݆Qšxn‹ÀŠW­žEµàXÅĪt¨ÃĖrÄwÀFÎ|ňÓMå¼ibµ¯»åDT±m[“r«_gŽmQu~¥V\\OkxtL E¢‹ƒ‘Ú^~ýê‹Pó–qo슱_Êw§ÑªåƗ⼋mĉŹ‹¿NQ“…YB‹ąrwģcÍ¥B•Ÿ­ŗÊcØiI—žƝĿuŒqtāwO]‘³YCñTeɕš‹caub͈]trlu€ī…B‘ПGsĵıN£ï—^ķqss¿FūūV՟·´Ç{éĈý‰ÿ›OEˆR_ŸđûIċâJh­ŅıN‘ȩĕB…¦K{Tk³¡OP·wn—µÏd¯}½TÍ«YiµÕsC¯„iM•¤™­•¦¯P|ÿUHv“he¥oFTu‰õ\\ŽOSs‹MòđƇiaºćXŸĊĵà·çhƃ÷ǜ{‘ígu^›đg’m[×zkKN‘¶Õ»lčÓ{XSƉv©_ÈëJbVk„ĔVÀ¤P¾ºÈMÖxlò~ªÚàGĂ¢B„±’ÌŒK˜y’áV‡¼Ã~­…`g›ŸsÙfI›Ƌlę¹e|–~udjˆuTlXµf`¿JdŠ[\\˜„L‚‘²"
],
"encodeOffsets": [[116689, 26234]]
}
},
{
"type": "Feature",
"id": "370000",
"properties": {
"id": "370000",
"cp": [118.000923, 36.275807],
"name": "山东",
"childNum": 13
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
["@@Xjd]{K"],
["@@itbFHy"],
["@@HlGk"],
["@@T‚ŒGŸy"],
["@@K¬˜•‹U"],
["@@WdXc"],
["@@PtOs"],
["@@•LnXhc"],
["@@ppVƒu]Or"],
["@@cdzAUa"],
["@@udRhnCI‡"],
["@@ˆoIƒpR„"],
[
"@@Ľč{fzƤî’Kš–ÎMĮ]†—ZFˆ½Y]â£ph’™š¶¨râøÀ†ÎǨ¤^ºÄ”Gzˆ~grĚĜlĞÆ„LĆdž¢Îo¦–cv“Kb€gr°Wh”mZp ˆL]LºcU‰Æ­n”żĤÌǜbAnrOAœ´žȊcÀbƦUØrĆUÜøœĬƞ†š˜Ez„VL®öØBkŖÝĐ˹ŧ̄±ÀbÎɜnb²ĦhņBĖ›žįĦåXćì@L¯´ywƕCéõė ƿ¸‘lµ¾Z|†ZWyFYŸ¨Mf~C¿`€à_RÇzwƌfQnny´INoƬˆèôº|sT„JUš›‚L„îVj„ǎ¾Ē؍‚Dz²XPn±ŴPè¸ŔLƔÜƺ_T‘üÃĤBBċȉöA´fa„˜M¨{«M`‡¶d¡ô‰Ö°šmȰBÔjjŒ´PM|”c^d¤u•ƒ¤Û´Œä«ƢfPk¶Môlˆ]Lb„}su^ke{lC‘…M•rDŠÇ­]NÑFsmoõľH‰yGă{{çrnÓE‰‹ƕZGª¹Fj¢ïW…uøCǷ돡ąuhÛ¡^Kx•C`C\\bÅxì²ĝÝ¿_N‰īCȽĿåB¥¢·IŖÕy\\‡¹kx‡Ã£Č×GDyÕ¤ÁçFQ¡„KtŵƋ]CgÏAùSed‡cÚź—ŠuYfƒyMmhUWpSyGwMPqŀ—›Á¼zK›¶†G•­Y§Ëƒ@–´śÇµƕBmœ@Io‚g——Z¯u‹TMx}C‘‰VK‚ï{éƵP—™_K«™pÛÙqċtkkù]gŽ‹Tğwo•ɁsMõ³ă‡AN£™MRkmEʕč™ÛbMjÝGu…IZ™—GPģ‡ãħE[iµBEuŸDPԛ~ª¼ętŠœ]ŒûG§€¡QMsğNPŏįzs£Ug{đJĿļā³]ç«Qr~¥CƎÑ^n¶ÆéÎR~ݏY’I“] P‰umŝrƿ›‰›Iā‹[x‰edz‹L‘¯v¯s¬ÁY…~}…ťuٌg›ƋpÝĄ_ņī¶ÏSR´ÁP~ž¿Cyžċßdwk´Ss•X|t‰`Ä Èð€AªìÎT°¦Dd–€a^lĎDĶÚY°Ž`ĪŴǒˆ”àŠv\\ebŒZH„ŖR¬ŢƱùęO•ÑM­³FۃWp[ƒ"
]
],
"encodeOffsets": [
[[123806, 39303]],
[[123821, 39266]],
[[123742, 39256]],
[[123702, 39203]],
[[123649, 39066]],
[[123847, 38933]],
[[123580, 38839]],
[[123894, 37288]],
[[123043, 36624]],
[[123344, 38676]],
[[123522, 38857]],
[[123628, 38858]],
[[118260, 36742]]
]
}
},
{
"type": "Feature",
"id": "410000",
"properties": {
"id": "410000",
"cp": [113.665412, 33.757975],
"name": "河南",
"childNum": 1
},
"geometry": {
"type": "Polygon",
"coordinates": [
"@@•ýL™ùµP³swIÓxcŢĞð†´E®žÚPt†ĴXØx¶˜@«ŕŕQGƒ‹Yfa[şu“ßǩ™đš_X³ijÕčC]kbc•¥CS¯ëÍB©÷‹–³­Siˆ_}m˜YTtž³xlàcȂzÀD}ÂOQ³ÐTĨ¯†ƗòËŖ[hœł‹Ŧv~††}ÂZž«¤lPǕ£ªÝŴÅR§ØnhcŒtâk‡nύ­ľŹUÓÝdKuķ‡I§oTũÙďkęĆH¸ÓŒ\\ăŒ¿PcnS{wBIvɘĽ[GqµuŸŇôYgûƒZcaŽ©@½Õǽys¯}lgg@­C\\£as€IdÍuCQñ[L±ęk·‹ţb¨©kK—’»›KC²‘òGKmĨS`ƒ˜UQ™nk}AGē”sqaJ¥ĐGR‰ĎpCuÌy ã iMc”plk|tRk†ðœev~^‘´†¦ÜŽSí¿_iyjI|ȑ|¿_»d}qŸ^{“Ƈdă}Ÿtqµ`Ƴĕg}V¡om½fa™Ço³TTj¥„tĠ—Ry”K{ùÓjuµ{t}uËR‘iŸvGŠçJFjµŠÍyqΘàQÂFewixGw½Yŷpµú³XU›½ġy™łå‰kÚwZXˆ·l„¢Á¢K”zO„Λ΀jc¼htoDHr…|­J“½}JZ_¯iPq{tę½ĕ¦Zpĵø«kQ…Ťƒ]MÛfaQpě±ǽ¾]u­Fu‹÷nƒ™čįADp}AjmcEǒaª³o³ÆÍSƇĈÙDIzˑ赟^ˆKLœ—i—Þñ€[œƒaA²zz‰Ì÷Dœ|[šíijgf‚ÕÞd®|`ƒĆ~„oĠƑô³Ŋ‘D×°¯CsŠøÀ«ì‰UMhTº¨¸ǡîS–Ô„DruÂÇZ•ÖEŽ’vPZ„žW”~؋ÐtĄE¢¦Ðy¸bŠô´oŬ¬Ž²Ês~€€]®tªašpŎJ¨Öº„_ŠŔ–`’Ŗ^Ѝ\\Ĝu–”~m²Ƹ›¸fW‰ĦrƔ}Î^gjdfÔ¡J}\\n C˜¦þWxªJRÔŠu¬ĨĨmF†dM{\\d\\ŠYÊ¢ú@@¦ª²SŠÜsC–}fNècbpRmlØ^g„d¢aÒ¢CZˆZxvÆ¶N¿’¢T@€uCœ¬^ĊðÄn|žlGl’™Rjsp¢ED}€Fio~ÔNŽ‹„~zkĘHVsDzßjƒŬŒŠŢ`Pûàl¢˜\\ÀœEhŽİgÞē X¼Pk–„|m"
],
"encodeOffsets": [[118256, 37017]]
}
},
{
"type": "Feature",
"id": "420000",
"properties": {
"id": "420000",
"cp": [113.298572, 30.684355],
"name": "湖北",
"childNum": 3
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
["@@AB‚"],
["@@lskt"],
[
"@@¾«}{ra®pîÃ\\™›{øCŠËyyB±„b\\›ò˜Ý˜jK›‡L ]ĎĽÌ’JyÚCƈćÎT´Å´pb©È‘dFin~BCo°BĎĚømvŒ®E^vǾ½Ĝ²Ro‚bÜeNŽ„^ĺ£R†¬lĶ÷YoĖ¥Ě¾|sOr°jY`~I”¾®I†{GqpCgyl{‡£œÍƒÍyPL“¡ƒ¡¸kW‡xYlÙæŠšŁĢzœ¾žV´W¶ùŸo¾ZHxjwfx„GNÁ•³Xéæl¶‰EièIH‰ u’jÌQ~v|sv¶Ôi|ú¢Fh˜Qsğ¦ƒSiŠBg™ÐE^ÁÐ{–čnOÂȞUÎóĔ†ÊēIJ}Z³½Mŧïeyp·uk³DsѨŸL“¶_œÅuèw»—€¡WqÜ]\\‘Ò§tƗcÕ¸ÕFÏǝĉăxŻČƟO‡ƒKÉġÿ×wg”÷IÅzCg†]m«ªGeçÃTC’«[‰t§{loWeC@ps_Bp‘­r‘„f_``Z|ei¡—oċMqow€¹DƝӛDYpûs•–‹Ykıǃ}s¥ç³[§ŸcYЧHK„«Qy‰]¢“wwö€¸ïx¼ņ¾Xv®ÇÀµRĠЋžHMž±cÏd„ƒǍũȅȷ±DSyúĝ£ŤĀàtÖÿï[îb\\}pĭÉI±Ñy…¿³x¯N‰o‰|¹H™ÏÛm‹júË~Tš•u˜ęjCöAwě¬R’đl¯ Ñb­‰ŇT†Ŀ_[Œ‘IčĄʿnM¦ğ\\É[T·™k¹œ©oĕ@A¾w•ya¥Y\\¥Âaz¯ãÁ¡k¥ne£Ûw†E©Êō¶˓uoj_Uƒ¡cF¹­[Wv“P©w—huÕyBF“ƒ`R‹qJUw\\i¡{jŸŸEPïÿ½fć…QÑÀQ{ž‚°‡fLԁ~wXg—ītêݾ–ĺ‘Hdˆ³fJd]‹HJ²…E€ƒoU¥†HhwQsƐ»Xmg±çve›]Dm͂PˆoCc¾‹_h”–høYrŊU¶eD°Č_N~øĹĚ·`z’]Äþp¼…äÌQŒv\\rCŒé¾TnkžŐڀÜa‡“¼ÝƆ̶Ûo…d…ĔňТJq’Pb ¾|JŒ¾fXŠƐîĨ_Z¯À}úƲ‹N_ĒĊ^„‘ĈaŐyp»CÇĕKŠšñL³ŠġMŒ²wrIÒŭxjb[œžn«øœ˜—æˆàƒ ^²­h¯Ú€ŐªÞ¸€Y²ĒVø}Ā^İ™´‚LŠÚm„¥ÀJÞ{JVŒųÞŃx×sxxƈē ģMř–ÚðòIf–Ċ“Œ\\Ʈ±ŒdʧĘD†vČ_Àæ~DŒċ´A®µ†¨ØLV¦êHÒ¤"
]
],
"encodeOffsets": [[[113712, 34000]], [[115612, 30507]], [[113649, 34054]]]
}
},
{
"type": "Feature",
"id": "430000",
"properties": { "id": "430000", "cp": [111.782279, 28.09409], "name": "湖南", "childNum": 3 },
"geometry": {
"type": "MultiPolygon",
"coordinates": [
["@@—n„FTs"],
["@@ßÅÆá‰½ÔXr—†CO™“…ËR‘ïÿĩ­TooQyšÓ[‹ŅBE¬–ÎÓXa„į§Ã¸G °ITxp‰úxÚij¥Ïš–̾ŠedžÄ©ĸG…œàGh‚€M¤–Â_U}Ċ}¢pczfŠþg¤€”ÇòAV‘‹M"],
[
"@@©K—ƒA·³CQ±Á«³BUŠƑ¹AŠtćOw™D]ŒJiØSm¯b£‘ylƒ›X…HËѱH•«–‘C^õľA–Å§¤É¥„ïyuǙuA¢^{ÌC´­¦ŷJ£^[†“ª¿‡ĕ~•Ƈ…•N… skóā‡¹¿€ï]ă~÷O§­@—Vm¡‹Qđ¦¢Ĥ{ºjԏŽŒª¥nf´•~ÕoŸž×Ûą‹MąıuZœmZcÒ IJβSÊDŽŶ¨ƚƒ’CÖŎªQؼrŭŽ­«}NÏürʬŒmjr€@ĘrTW ­SsdHzƓ^ÇÂyUi¯DÅYlŹu{hTœ}mĉ–¹¥ě‰Dÿë©ıÓ[Oº£ž“¥ót€ł¹MՄžƪƒ`Pš…Di–ÛUоÅ‌ìˆU’ñB“È£ýhe‰dy¡oċ€`pfmjP~‚kZa…ZsÐd°wj§ƒ@€Ĵ®w~^‚kÀÅKvNmX\\¨a“”сqvíó¿F„¤¡@ũÑVw}S@j}¾«pĂr–ªg àÀ²NJ¶¶Dô…K‚|^ª†Ž°LX¾ŴäPᜣEXd›”^¶›IJÞܓ~‘u¸ǔ˜Ž›MRhsR…e†`ÄofIÔ\\Ø  i”ćymnú¨cj ¢»–GČìƊÿШXeĈ¾Oð Fi ¢|[jVxrIQŒ„_E”zAN¦zLU`œcªx”OTu RLÄ¢dV„i`p˔vŎµªÉžF~ƒØ€d¢ºgİàw¸Áb[¦Zb¦–z½xBĖ@ªpº›šlS¸Ö\\Ĕ[N¥ˀmĎă’J\\‹ŀ`€…ňSڊĖÁĐiO“Ĝ«BxDõĚiv—ž–S™Ì}iùŒžÜnšÐºGŠ{Šp°M´w†ÀÒzJ²ò¨ oTçüöoÛÿñŽőФ‚ùTz²CȆȸǎۃƑÐc°dPÎŸğ˶[Ƚu¯½WM¡­Éž“’B·rížnZŸÒ `‡¨GA¾\\pē˜XhÆRC­üWGġu…T靧Ŏѝ©ò³I±³}_‘‹EÃħg®ęisÁPDmÅ{‰b[Rşs·€kPŸŽƥƒóRo”O‹ŸVŸ~]{g\\“êYƪ¦kÝbiċƵŠGZ»Ěõ…ó·³vŝž£ø@pyö_‹ëŽIkѵ‡bcѧy…×dY؎ªiþž¨ƒ[]f]Ņ©C}ÁN‡»hĻħƏ’ĩ"
]
],
"encodeOffsets": [[[115640, 30489]], [[112543, 27312]], [[116690, 26230]]]
}
},
{
"type": "Feature",
"id": "440000",
"properties": {
"id": "440000",
"cp": [113.280637, 23.125178],
"name": "广东",
"childNum": 24
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
["@@QdˆAua"],
["@@ƒlxDLo"],
["@@sbhNLo"],
["@@Ă āŸ"],
["@@WltO[["],
["@@Krœ]S"],
["@@e„„I]y"],
["@@I|„Mym"],
["@@ƒÛ³LSŒž¼Y"],
["@@nvºB–ëui©`¾"],
["@@zdšÛ›Jw®"],
["@@†°…¯"],
["@@a yAª¸ËJIx،@€ĀHAmßV¡o•fu•o"],
["@@šs‰ŗÃÔėAƁ›ZšÄ ~°ČP‚‹äh"],
["@@‹¶Ý’Ì‚vmĞh­ı‡Q"],
["@@HœŠdSjĒ¢D}war…“u«ZqadYM"],
["@@elŒ\\LqqU"],
["@@~rMo\\"],
["@@f„^ƒC"],
["@@øPªoj÷ÍÝħXČx”°Q¨ıXNv"],
["@@gÇƳˆŽˆ”oˆŠˆ[~tly"],
["@@E–ÆC¿‘"],
["@@OŽP"],
[
"@@w‹†đóg‰™ĝ—[³‹¡VÙæÅöM̳¹pÁaËýý©D©Ü“JŹƕģGą¤{Ùū…ǘO²«BƱéA—Ò‰ĥ‡¡«BhlmtÃPµyU¯uc“d·w_bŝcīímGOŽ|KP’ȏ‡ŹãŝIŕŭŕ@Óoo¿ē‹±ß}Ž…ŭ‚ŸIJWÈCőâUâǙI›ğʼn©I›ijEׅÁ”³Aó›wXJþ±ÌŒÜӔĨ£L]ĈÙƺZǾĆĖMĸĤfŒÎĵl•ŨnȈ‘ĐtF”Š–FĤ–‚êk¶œ^k°f¶gŠŽœ}®Fa˜f`vXŲxl˜„¦–ÔÁ²¬ÐŸ¦pqÊ̲ˆi€XŸØRDÎ}†Ä@ZĠ’s„x®AR~®ETtĄZ†–ƈfŠŠHâÒÐA†µ\\S¸„^wĖkRzŠalŽŜ|E¨ÈNĀňZTŒ’pBh£\\ŒĎƀuXĖtKL–¶G|Ž»ĺEļĞ~ÜĢÛĊrˆO˜Ùîvd]nˆ¬VœÊĜ°R֟pM††–‚ƂªFbwžEÀˆ˜©Œž\\…¤]ŸI®¥D³|ˎ]CöAŤ¦…æ’´¥¸Lv¼€•¢ĽBaô–F~—š®²GÌҐEY„„œzk¤’°ahlV՞I^‹šCxĈPŽsB‰ƒºV‰¸@¾ªR²ĨN]´_eavSi‡vc•}p}Đ¼ƌkJœÚe thœ†_¸ ºx±ò_xN›Ë‹²‘@ƒă¡ßH©Ùñ}wkNÕ¹ÇO½¿£ĕ]ly_WìIžÇª`ŠuTÅxYĒÖ¼k֞’µ‚MžjJÚwn\\h‘œĒv]îh|’È›Ƅøègž¸Ķß ĉĈWb¹ƀdéƌNTtP[ŠöSvrCZžžaGuœbo´ŖÒÇА~¡zCI…özx¢„Pn‹•‰Èñ @ŒĥÒ¦†]ƞŠV}³ăĔñiiÄÓVépKG½Ä‘ÓávYo–C·sit‹iaÀy„ŧΡÈYDÑům}‰ý|m[węõĉZÅxUO}÷N¹³ĉo_qtă“qwµŁYلǝŕ¹tïÛUïmRCº…ˆĭ|µ›ÕÊK™½R‘ē ó]‘–GªęAx–»HO£|ām‡¡diď×YïYWªʼnOeÚtĐ«zđ¹T…ā‡úE™á²\\‹ķÍ}jYàÙÆſ¿Çdğ·ùTßÇţʄ¡XgWÀLJğ·¿ÃˆOj YÇ÷Qě‹i"
]
],
"encodeOffsets": [
[[117381, 22988]],
[[116552, 22934]],
[[116790, 22617]],
[[116973, 22545]],
[[116444, 22536]],
[[116931, 22515]],
[[116496, 22490]],
[[116453, 22449]],
[[113301, 21439]],
[[118726, 21604]],
[[118709, 21486]],
[[113210, 20816]],
[[115482, 22082]],
[[113171, 21585]],
[[113199, 21590]],
[[115232, 22102]],
[[115739, 22373]],
[[115134, 22184]],
[[113056, 21175]],
[[119573, 21271]],
[[119957, 24020]],
[[115859, 22356]],
[[116561, 22649]],
[[116285, 22746]]
]
}
},
{
"type": "Feature",
"id": "450000",
"properties": { "id": "450000", "cp": [108.320004, 22.82402], "name": "广西", "childNum": 2 },
"geometry": {
"type": "MultiPolygon",
"coordinates": [
["@@H– TQ§•A"],
[
"@@ĨʪƒLƒƊDÎĹĐCǦė¸zÚGn£¾›rªŀÜt¬@֛ڈSx~øOŒ˜ŶÐÂæȠ\\„ÈÜObĖw^oބLf¬°bI lTØB̈F£Ć¹gñĤaY“t¿¤VSñœK¸¤nM†¼‚JE±„½¸šŠño‹ÜCƆæĪ^ŠĚQÖ¦^‡ˆˆf´Q†üÜʝz¯šlzUĺš@쇀p¶n]sxtx¶@„~ÒĂJb©gk‚{°‚~c°`ԙ¬rV\\“la¼¤ôá`¯¹LC†ÆbŒxEræO‚v[H­˜„[~|aB£ÖsºdAĐzNÂðsŽÞƔ…Ĥªbƒ–ab`ho¡³F«èVloޤ™ÔRzpp®SŽĪº¨ÖƒºN…ij„d`’a”¦¤F³ºDÎńĀìŠCžĜº¦Ċ•~nS›|gźvZkCÆj°zVÈÁƔ]LÊFZg…čP­kini«‹qǀcz͔Y®¬Ů»qR×ō©DՄ‘§ƙǃŵTÉĩ±ŸıdÑnYY›IJvNĆÌØÜ Öp–}e³¦m‹©iÓ|¹Ÿħņ›|ª¦QF¢Â¬ʖovg¿em‡^ucà÷gՎuŒíÙćĝ}FϼĹ{µHK•sLSđƃr‹č¤[Ag‘oS‹ŇYMÿ§Ç{Fśbky‰lQxĕƒ]T·¶[B…ÑÏGáşşƇe€…•ăYSs­FQ}­Bƒw‘tYğÃ@~…C̀Q ×W‡j˱rÉ¥oÏ ±«ÓÂ¥•ƒ€k—ŽwWűŒmcih³K›~‰µh¯e]lµ›él•E쉕E“ďs‡’mǖŧē`ãògK_ÛsUʝ“ćğ¶hŒöŒO¤Ǜn³Žc‘`¡y‹¦C‘ez€YŠwa™–‘[ďĵűMę§]X˜Î_‚훘Û]é’ÛUćİÕBƣ±…dƒy¹T^džûÅÑŦ·‡PĻþÙ`K€¦˜…¢ÍeœĥR¿Œ³£[~Œäu¼dl‰t‚†W¸oRM¢ď\\zœ}Æzdvň–{ÎXF¶°Â_„ÒÂÏL©Ö•TmuŸ¼ãl‰›īkiqéfA„·Êµ\\őDc¥ÝF“y›Ôć˜c€űH_hL܋êĺШc}rn`½„Ì@¸¶ªVLŒŠhŒ‹\\•Ţĺk~ŽĠið°|gŒtTĭĸ^x‘vK˜VGréAé‘bUu›MJ‰VÃO¡…qĂXËS‰ģãlýàŸ_ju‡YÛÒB†œG^˜é֊¶§ŽƒEG”ÅzěƒƯ¤Ek‡N[kdåucé¬dnYpAyČ{`]þ¯T’bÜÈk‚¡Ġ•vŒàh„ÂƄ¢Jî¶²"
]
],
"encodeOffsets": [[[111707, 21520]], [[107619, 25527]]]
}
},
{
"type": "Feature",
"id": "460000",
"properties": { "id": "460000", "cp": [109.83119, 19.031971], "name": "海南", "childNum": 1 },
"geometry": {
"type": "Polygon",
"coordinates": [
"@@š¦Ŝil¢”XƦ‘ƞò–ïè§ŞCêɕrŧůÇąĻõ™·ĉ³œ̅kÇm@ċȧƒŧĥ‰Ľʉ­ƅſ“ȓÒ˦ŝE}ºƑ[ÍĜȋ gÎfǐÏĤ¨êƺ\\Ɔ¸ĠĎvʄȀœÐ¾jNðĀÒRŒšZdž™zÐŘΰH¨Ƣb²_Ġ "
],
"encodeOffsets": [[112750, 20508]]
}
},
{
"type": "Feature",
"id": "510000",
"properties": {
"id": "510000",
"cp": [104.065735, 30.659462],
"name": "四川",
"childNum": 2
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
["@@LqKr"],
[
"@@Š[ĻéV£ž_ţġñpG •réÏ·~ąSfy×͂·ºſƽiÍıƣıĻmHH}siaX@iǰÁÃ×t«ƒ­Tƒ¤J–JJŒyJ•ÈŠ`Ohߦ¡uËhIyCjmÿw…ZG……Ti‹SˆsO‰žB²ŸfNmsPaˆ{M{ŠõE‘^Hj}gYpaeuž¯‘oáwHjÁ½M¡pM“–uå‡mni{fk”\\oƒÎqCw†EZ¼K›ĝŠƒAy{m÷L‡wO×SimRI¯rK™õBS«sFe‡]fµ¢óY_ÆPRcue°Cbo׌bd£ŌIHgtrnyPt¦foaXďx›lBowz‹_{ÊéWiêE„GhܸºuFĈIxf®Ž•Y½ĀǙ]¤EyŸF²ċ’w¸¿@g¢§RGv»–áŸW`ÃĵJwi]t¥wO­½a[׈]`Ãi­üL€¦LabbTÀå’c}Íh™Æhˆ‹®BH€î|Ék­¤S†y£„ia©taį·Ɖ`ō¥Uh“O…ƒĝLk}©Fos‰´›Jm„µlŁu—…ø–nÑJWΪ–YÀïAetTžŅ‚ӍG™Ë«bo‰{ıwodƟ½ƒžOġܑµxàNÖ¾P²§HKv¾–]|•B‡ÆåoZ`¡Ø`ÀmºĠ~ÌЧnDž¿¤]wğ@sƒ‰rğu‰~‘Io”[é±¹ ¿žſđӉ@q‹gˆ¹zƱřaí°KtǤV»Ã[ĩǭƑ^ÇÓ@ỗs›Zϕ‹œÅĭ€Ƌ•ěpwDóÖሯneQˌq·•GCœýS]xŸ·ý‹q³•O՜Œ¶Qzßti{ř‰áÍÇWŝŭñzÇW‹pç¿JŒ™‚Xœĩè½cŒF–ÂLiVjx}\\N†ŇĖ¥Ge–“JA¼ÄHfÈu~¸Æ«dE³ÉMA|b˜Ò…˜ćhG¬CM‚õŠ„ƤąAvƒüV€éŀ‰_V̳ĐwQj´·ZeÈÁ¨X´Æ¡Qu·»Ÿ“˜ÕZ³ġqDo‰y`L¬gdp°şŠp¦ėìÅĮZްIä”h‚‘ˆzŠĵœf²å ›ĚрKp‹IN|‹„Ñz]ń……·FU×é»R³™MƒÉ»GM«€ki€™ér™}Ã`¹ăÞmȝnÁîRǀ³ĜoİzŔwǶVÚ£À]ɜ»ĆlƂ²Ġ…þTº·àUȞÏʦ¶†I’«dĽĢdĬ¿–»Ĕ׊h\\c¬†ä²GêëĤł¥ÀǿżÃÆMº}BÕĢyFVvw–ˆxBèĻĒ©Ĉ“tCĢɽŠȣ¦āæ·HĽî“ôNԓ~^¤Ɗœu„œ^s¼{TA¼ø°¢İªDè¾Ň¶ÝJ‘®Z´ğ~Sn|ªWÚ©òzPOȸ‚bð¢|‹øĞŠŒœŒQìÛÐ@Ğ™ǎRS¤Á§d…i“´ezÝúØã]Hq„kIŸþËQǦÃsǤ[E¬ÉŪÍxXƒ·ÖƁİlƞ¹ª¹|XÊwn‘ÆƄmÀêErĒtD®ċæcQƒ”E®³^ĭ¥©l}äQto˜ŖÜqƎkµ–„ªÔĻĴ¡@Ċ°B²Èw^^RsºT£ڿœQP‘JvÄz„^Đ¹Æ¯fLà´GC²‘dt˜­ĀRt¼¤ĦOðğfÔðDŨŁĞƘïžPȆ®âbMüÀXZ ¸£@Ś›»»QÉ­™]d“sÖ×_͖_ÌêŮPrĔĐÕGĂeZÜîĘqBhtO ¤tE[h|Y‹Ô‚ZśÎs´xº±UŒ’ñˆt|O’ĩĠºNbgþŠJy^dÂY Į„]Řz¦gC‚³€R`Šz’¢AjŒ¸CL„¤RÆ»@­Ŏk\\Ç´£YW}z@Z}‰Ã¶“oû¶]´^N‡Ò}èN‚ª–P˜Íy¹`S°´†ATe€VamdUĐwʄvĮÕ\\ƒu‹Æŗ¨Yp¹àZÂm™Wh{á„}WØǍ•Éüw™ga§áCNęÎ[ĀÕĪgÖɪX˜øx¬½Ů¦¦[€—„NΆL€ÜUÖ´òrÙŠxR^–†J˜k„ijnDX{Uƒ~ET{ļº¦PZc”jF²Ė@Žp˜g€ˆ¨“B{ƒu¨ŦyhoÚD®¯¢˜ WòàFΤ¨GDäz¦kŮPœġq˚¥À]€Ÿ˜eŽâÚ´ªKxī„Pˆ—Ö|æ[xäJÞĥ‚s’NÖ½ž€I†¬nĨY´®Ð—ƐŠ€mD™ŝuäđđEb…e’e_™v¡}ìęNJē}q”É埁T¯µRs¡M@}ůa†a­¯wvƉåZwž\\Z{åû^›"
]
],
"encodeOffsets": [[[108815, 30935]], [[110617, 31811]]]
}
},
{
"type": "Feature",
"id": "520000",
"properties": {
"id": "520000",
"cp": [106.713478, 26.578343],
"name": "贵州",
"childNum": 3
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
["@@†G\\†lY£‘in"],
["@@q‚|ˆ‚mc¯tχVSÎ"],
[
"@@hÑ£Is‡NgßH†›HªķÃh_¹ƒ¡ĝħń¦uيùŽgS¯JHŸ|sÝÅtÁïyMDč»eÕtA¤{b\\}—ƒG®u\\åPFq‹wÅaD…žK°ºâ_£ùbµ”mÁ‹ÛœĹM[q|hlaªāI}тƒµ@swtwm^oµˆD鼊yV™ky°ÉžûÛR…³‚‡eˆ‡¥]RՋěħ[ƅåÛDpŒ”J„iV™™‰ÂF²I…»mN·£›LbÒYb—WsÀbŽ™pki™TZĄă¶HŒq`……ĥ_JŸ¯ae«ƒKpÝx]aĕÛPƒÇȟ[ÁåŵÏő—÷Pw}‡TœÙ@Õs«ĿÛq©½œm¤ÙH·yǥĘĉBµĨÕnđ]K„©„œá‹ŸG纍§Õßg‡ǗĦTèƤƺ{¶ÉHÎd¾ŚÊ·OÐjXWrãLyzÉAL¾ę¢bĶėy_qMĔąro¼hĊžw¶øV¤w”²Ĉ]ʚKx|`ź¦ÂÈdr„cȁbe¸›`I¼čTF´¼Óýȃr¹ÍJ©k_șl³´_pН`oÒh޶pa‚^ÓĔ}D»^Xyœ`d˜[Kv…JPhèhCrĂĚÂ^Êƌ wˆZL­Ġ£šÁbrzOIl’MM”ĪŐžËr×ÎeŦŽtw|Œ¢mKjSǘňĂStÎŦEtqFT†¾†E쬬ôxÌO¢Ÿ KгŀºäY†„”PVgŎ¦Ŋm޼VZwVlŒ„z¤…ž£Tl®ctĽÚó{G­A‡ŒÇgeš~Αd¿æaSba¥KKûj®_ć^\\ؾbP®¦x^sxjĶI_Ä X‚⼕Hu¨Qh¡À@Ëô}ޱžGNìĎlT¸ˆ…`V~R°tbÕĊ`¸úÛtπFDu€[ƒMfqGH·¥yA‰ztMFe|R‚_Gk†ChZeÚ°to˜v`x‹b„ŒDnÐ{E}šZ˜è€x—†NEފREn˜[Pv@{~rĆAB§‚EO¿|UZ~ì„Uf¨J²ĂÝÆ€‚sª–B`„s¶œfvö¦ŠÕ~dÔq¨¸º»uù[[§´sb¤¢zþFœ¢Æ…Àhˆ™ÂˆW\\ıŽËI݊o±ĭŠ£þˆÊs}¡R]ŒěƒD‚g´VG¢‚j±®è†ºÃmpU[Á›‘Œëº°r›ÜbNu¸}Žº¼‡`ni”ºÔXĄ¤¼Ôdaµ€Á_À…†ftQQgœR—‘·Ǔ’v”}Ýלĵ]µœ“Wc¤F²›OĩųãW½¯K‚©…]€{†LóµCIµ±Mß¿hŸ•©āq¬o‚½ž~@i~TUxŪÒ¢@ƒ£ÀEîôruń‚”“‚b[§nWuMÆLl¿]x}ij­€½"
]
],
"encodeOffsets": [[[112158, 27383]], [[112105, 27474]], [[112095, 27476]]]
}
},
{
"type": "Feature",
"id": "530000",
"properties": {
"id": "530000",
"cp": [101.512251, 24.740609],
"name": "云南",
"childNum": 1
},
"geometry": {
"type": "Polygon",
"coordinates": [
"@@[„ùx½}ÑRH‘YīĺûsÍn‘iEoã½Ya²ė{c¬ĝg•ĂsA•ØÅwď‚õzFjw}—«Dx¿}UũlŸê™@•HÅ­F‰¨ÇoJ´Ónũuą¡Ã¢pÒŌ“Ø TF²‚xa²ËX€‚cʋlHîAßËŁkŻƑŷÉ©h™W­æßU‡“Ës¡¦}•teèÆ¶StǀÇ}Fd£j‹ĈZĆÆ‹¤T‚č\\Dƒ}O÷š£Uˆ§~ŃG™‚åŃDĝ¸œTsd¶¶Bªš¤u¢ŌĎo~t¾ÍŶÒtD¦Ú„iôö‰€z›ØX²ghįh½Û±¯€ÿm·zR¦Ɵ`ªŊÃh¢rOԍ´£Ym¼èêf¯ŪĽn„†cÚbŒw\\zlvWžªâˆ ¦g–mĿBş£¢ƹřbĥkǫßeeZkÙIKueT»sVesb‘aĕ  ¶®dNœĄÄpªyސ¼—„³BE˜®l‡ŽGœŭCœǶwêżĔÂe„pÍÀQƞpC„–¼ŲÈ­AÎô¶R„ä’Q^Øu¬°š_Èôc´¹ò¨P΢hlϦ´Ħ“Æ´sâDŽŲPnÊD^¯°’Upv†}®BP̪–jǬx–Söwlfòªv€qĸ|`H€­viļ€ndĜ­Ćhň•‚em·FyށqóžSᝑ³X_ĞçêtryvL¤§z„¦c¦¥jnŞk˜ˆlD¤øz½ĜàžĂŧMÅ|áƆàÊcðÂF܎‚áŢ¥\\\\º™İøÒÐJĴ‡„îD¦zK²ǏÎEh~’CD­hMn^ÌöÄ©ČZÀžaü„fɭyœpį´ěFűk]Ôě¢qlÅĆÙa¶~Äqššê€ljN¬¼H„ÊšNQ´ê¼VظE††^ŃÒyŒƒM{ŒJLoÒœęæŸe±Ķ›y‰’‡gã“¯JYÆĭĘëo¥Š‰o¯hcK«z_pŠrC´ĢÖY”—¼ v¸¢RŽÅW³Â§fǸYi³xR´ďUˊ`êĿU„û€uĆBƒƣö‰N€DH«Ĉg†——Ñ‚aB{ÊNF´¬c·Åv}eÇÃGB»”If•¦HňĕM…~[iwjUÁKE•Ž‹¾dĪçW›šI‹èÀŒoÈXòyŞŮÈXâÎŚŠj|àsRy‹µÖ›–Pr´þŒ ¸^wþTDŔ–Hr¸‹žRÌmf‡żÕâCôox–ĜƌÆĮŒ›Ð–œY˜tâŦÔ@]ÈǮƒ\\μģUsȯLbîƲŚºyh‡rŒŠ@ĒԝƀŸÀ²º\\êp“’JŠ}ĠvŠqt„Ġ@^xÀ£È†¨mËÏğ}n¹_¿¢×Y_æpˆÅ–A^{½•Lu¨GO±Õ½ßM¶w’ÁĢۂP‚›Ƣ¼pcIJxŠ|ap̬HšÐŒŊSfsðBZ¿©“XÏÒK•k†÷Eû¿‰S…rEFsÕūk”óVǥʼniTL‚¡n{‹uxţÏh™ôŝ¬ğōN“‘NJkyPaq™Âğ¤K®‡YŸxÉƋÁ]āęDqçgOg†ILu—\\_gz—]W¼ž~CÔē]bµogpў_oď`´³Țkl`IªºÎȄqÔþž»E³ĎSJ»œ_f·‚adÇqƒÇc¥Á_Źw{™L^ɱćx“U£µ÷xgĉp»ĆqNē`rĘzaĵĚ¡K½ÊBzyäKXqiWPÏɸ½řÍcÊG|µƕƣG˛÷Ÿk°_^ý|_zċBZocmø¯hhcæ\\lˆMFlư£Ĝ„ÆyH“„F¨‰µêÕ]—›HA…àӄ^it `þßäkŠĤÎT~Wlÿ¨„ÔPzUC–NVv [jâôDôď[}ž‰z¿–msSh‹¯{jïğl}šĹ[–őŒ‰gK‹©U·µË@¾ƒm_~q¡f¹…ÅË^»‘f³ø}Q•„¡Ö˳gͱ^ǁ…\\ëÃA_—¿bW›Ï[¶ƛ鏝£F{īZgm@|kHǭƁć¦UĔťƒ×ë}ǝƒeďºȡȘÏíBə£āĘPªij¶“ʼnÿ‡y©n‰ď£G¹¡I›Š±LÉĺÑdĉ܇W¥˜‰}g˜Á†{aqÃ¥aŠıęÏZ—ï`"
],
"encodeOffsets": [[104636, 22969]]
}
},
{
"type": "Feature",
"id": "540000",
"properties": { "id": "540000", "cp": [89.132212, 30.860361], "name": "西藏", "childNum": 1 },
"geometry": {
"type": "Polygon",
"coordinates": [
"@@hžľxŽŖ‰xƒÒVކºÅâAĪÝȆµę¯Ňa±r_w~uSÕň‘qOj]ɄQ…£Z……UDûoY’»©M[‹L¼qãË{V͕çWViŽ]ë©Ä÷àyƛh›ÚU°ŒŒa”d„cQƒ~Mx¥™cc¡ÙaSyF—ցk­ŒuRýq¿Ôµ•QĽ³aG{¿FµëªéĜÿª@¬·–K‰·àariĕĀ«V»Ŷ™Ĵū˜gèLǴŇƶaf‹tŒèBŚ£^Šâ†ǐÝ®–šM¦ÁǞÿ¬LhŸŽJ¾óƾƺcxw‹f]Y…´ƒ¦|œQLn°aœdĊ…œ\\¨o’œǀÍŎœ´ĩĀd`tÊQŞŕ|‚¨C^©œĈ¦„¦ÎJĊ{ŽëĎjª²rЉšl`¼Ą[t|¦St辉PŒÜK¸€d˜Ƅı]s¤—î_v¹ÎVòŦj˜£Əsc—¬_Ğ´|٘¦Avަw`ăaÝaa­¢e¤ı²©ªSªšÈMĄwžÉØŔì@T‘¤—Ę™\\õª@”þo´­xA s”ÂtŎKzó´ÇĊµ¢rž^nĊ­Æ¬×üGž¢‚³ {âĊ]š™G‚~bÀgVjzlhǶf€žOšfdЉªB]pj„•TO–tĊ‚n¤}®¦ƒČ¥d¢¼»ddš”Y¼Žt—¢eȤJ¤}Ǿ¡°§¤AГlc@ĝ”sªćļđAç‡wx•UuzEÖġ~AN¹ÄÅȀݦ¿ģŁéì±H…ãd«g[؉¼ēÀ•cīľġ¬cJ‘µ…ÐʥVȝ¸ßS¹†ý±ğkƁ¼ą^ɛ¤Ûÿ‰b[}¬ōõÃ]ËNm®g@•Bg}ÍF±ǐyL¥íCˆƒIij€Ï÷њį[¹¦[⚍EÛïÁÉdƅß{âNÆāŨߝ¾ě÷yC£‡k­´ÓH@¹†TZ¥¢įƒ·ÌAЧ®—Zc…v½ŸZ­¹|ŕWZqgW“|ieZÅYVӁqdq•bc²R@†c‡¥Rã»Ge†ŸeƃīQ•}J[ғK…¬Ə|o’ėjġĠÑN¡ð¯EBčnwôɍėªƒ²•CλŹġǝʅįĭạ̃ūȹ]ΓͧgšsgȽóϧµǛ†ęgſ¶ҍć`ĘąŌJޚä¤rÅň¥ÖÁUětęuůÞiĊÄÀ\\Æs¦ÓRb|Â^řÌkÄŷ¶½÷‡f±iMݑ›‰@ĥ°G¬ÃM¥n£Øą‚ğ¯ß”§aëbéüÑOčœk£{\\‘eµª×M‘šÉfm«Ƒ{Å׃Gŏǩãy³©WÑăû‚··‘Q—òı}¯ã‰I•éÕÂZ¨īès¶ZÈsŽæĔTŘvŽgÌsN@îá¾ó@‰˜ÙwU±ÉT廣TđŸWxq¹Zo‘b‹s[׌¯cĩv‡Œėŧ³BM|¹k‰ªħ—¥TzNYnݍßpęrñĠĉRS~½ŠěVVе‚õ‡«ŒM££µB•ĉ¥áºae~³AuĐh`Ü³ç@BۘïĿa©|z²Ý¼D”£à貋ŸƒIƒû›I ā€óK¥}rÝ_Á´éMaň¨€~ªSĈ½Ž½KÙóĿeƃÆBŽ·¬ën×W|Uº}LJrƳ˜lŒµ`bÔ`QˆˆÐÓ@s¬ñIŒÍ@ûws¡åQÑßÁ`ŋĴ{Ī“T•ÚÅTSij‚‹Yo|Ç[ǾµMW¢ĭiÕØ¿@˜šMh…pÕ]j†éò¿OƇĆƇp€êĉâlØw–ěsˆǩ‚ĵ¸c…bU¹ř¨WavquSMzeo_^gsÏ·¥Ó@~¯¿RiīB™Š\\”qTGªÇĜçPoŠÿfñòą¦óQīÈáP•œābß{ƒZŗĸIæÅ„hnszÁCËìñšÏ·ąĚÝUm®ó­L·ăU›Èíoù´Êj°ŁŤ_uµ^‘°Œìǖ@tĶĒ¡Æ‡M³Ģ«˜İĨÅ®ğ†RŽāð“ggheÆ¢z‚Ê©Ô\\°ÝĎz~ź¤Pn–MĪÖB£Ÿk™n鄧żćŠ˜ĆK„ǰ¼L¶è‰âz¨u¦¥LDĘz¬ýÎmĘd¾ß”Fz“hg²™Fy¦ĝ¤ċņbΛ@y‚Ąæm°NĮZRÖíŽJ²öLĸÒ¨Y®ƌÐV‰à˜tt_ڀÂyĠzž]Ţh€zĎ{†ĢX”ˆc|šÐqŽšfO¢¤ög‚ÌHNŽ„PKŖœŽ˜Uú´xx[xˆvĐCûŠìÖT¬¸^}Ìsòd´_އKgžLĴ…ÀBon|H@–Êx˜—¦BpŰˆŌ¿fµƌA¾zLjRxжF”œkĄźRzŀˆ~¶[”´Hnª–VƞuĒ­È¨ƎcƽÌm¸ÁÈM¦x͊ëÀxdžB’šú^´W†£–d„kɾĬpœw‚˂ØɦļĬIŚœÊ•n›Ŕa¸™~J°î”lɌxĤÊÈðhÌ®‚g˜T´øŽàCˆŽÀ^ªerrƘdž¢İP|Ė ŸWœªĦ^¶´ÂL„aT±üWƜ˜ǀRšŶUńšĖ[QhlLüA†‹Ü\\†qR›Ą©"
],
"encodeOffsets": [[90849, 37210]]
}
},
{
"type": "Feature",
"id": "610000",
"properties": {
"id": "610000",
"cp": [108.948024, 34.263161],
"name": "陕西",
"childNum": 1
},
"geometry": {
"type": "Polygon",
"coordinates": [
"@@˜p¢—ȮµšûG™Ħ}Ħšðǚ¶òƄ€jɂz°{ºØkÈęâ¦jª‚Bg‚\\œċ°s¬Ž’]jžú ‚E”Ȍdž¬s„t‡”RˆÆdĠݎwܔ¸ôW¾ƮłÒ_{’Ìšû¼„jº¹¢GǪÒ¯ĘƒZ`ºŊƒecņąš~BÂgzpâēòYǠȰÌTΨÂWœ|fcŸă§uF—Œ@NŸ¢XLƒŠRMº[ğȣſï|¥J™kc`sʼnǷ’Y¹‹W@µ÷K…ãï³ÛIcñ·VȋڍÒķø©—þ¥ƒy‚ÓŸğęmWµÎumZyOŅƟĥÓ~sÑL¤µaŅY¦ocyZ{‰y c]{ŒTa©ƒ`U_Ěē£ωÊƍKù’K¶ȱÝƷ§{û»ÅÁȹÍéuij|¹cÑd‘ŠìUYƒŽO‘uF–ÕÈYvÁCqӃT•Ǣí§·S¹NgŠV¬ë÷Át‡°Dد’C´ʼnƒópģ}„ċcE˅FŸŸéGU¥×K…§­¶³B‹Č}C¿åċ`wġB·¤őcƭ²ő[Å^axwQO…ÿEËߌ•ĤNĔŸwƇˆÄŠńwĪ­Šo[„_KÓª³“ÙnK‰Çƒěœÿ]ď€ă_d©·©Ýŏ°Ù®g]±„Ÿ‡ß˜å›—¬÷m\\›iaǑkěX{¢|ZKlçhLt€Ňîŵ€œè[€É@ƉĄEœ‡tƇÏ˜³­ħZ«mJ…›×¾‘MtÝĦ£IwÄå\\Õ{‡˜ƒOwĬ©LÙ³ÙgBƕŀr̛ĢŭO¥lãyC§HÍ£ßEñŸX¡—­°ÙCgpťz‘ˆb`wI„vA|§”‡—hoĕ@E±“iYd¥OϹS|}F@¾oAO²{tfžÜ—¢Fǂ҈W²°BĤh^Wx{@„¬‚­F¸¡„ķn£P|ŸªĴ@^ĠĈæb–Ôc¶l˜Yi…–^Mi˜cϰÂ[ä€vï¶gv@À“Ĭ·lJ¸sn|¼u~a]’ÆÈtŌºJp’ƒþ£KKf~ЦUbyäIšĺãn‡Ô¿^­žŵMT–hĠܤko¼Ŏìąǜh`[tŒRd²IJ_œXPrɲ‰l‘‚XžiL§àƒ–¹ŽH˜°Ȧqº®QC—bA†„ŌJ¸ĕÚ³ĺ§ `d¨YjžiZvRĺ±öVKkjGȊĐePОZmļKÀ€‚[ŠŽ`ösìh†ïÎoĬdtKÞ{¬èÒÒBŒÔpIJÇĬJŊ¦±J«ˆY§‹@·pH€µàåVKe›pW†ftsAÅqC·¬ko«pHÆuK@oŸHĆۄķhx“e‘n›S³àǍrqƶRbzy€¸ËАl›¼EºpĤ¼Œx¼½~Ğ’”à@†ÚüdK^ˆmÌSj"
],
"encodeOffsets": [[110234, 38774]]
}
},
{
"type": "Feature",
"id": "620000",
"properties": {
"id": "620000",
"cp": [103.823557, 36.058039],
"name": "甘肃",
"childNum": 2
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
["@@VuUv"],
[
"@@ũ‹EĠtt~nkh`Q‰¦ÅÄÜdw˜Ab×ĠąJˆ¤DüègĺqBqœj°lI¡ĨÒ¤úSHbš‡ŠjΑBаaZˆ¢KJŽ’O[|A£žDx}Nì•HUnrk„ kp€¼Y kMJn[aG‚áÚÏ[½rc†}aQxOgsPMnUs‡nc‹Z…ž–sKúvA›t„Þġ’£®ĀYKdnFwš¢JE°”Latf`¼h¬we|€Æ‡šbj}GA€·~WŽ”—`†¢MC¤tL©IJ°qdf”O‚“bÞĬ¹ttu`^ZúE`Œ[@„Æsîz®¡’C„ƳƜG²“R‘¢R’m”fŽwĸg܃‚ą G@pzJM½mŠhVy¸uÈÔO±¨{LfæU¶ßGĂq\\ª¬‡²I‚¥IʼnÈīoı‹ÓÑAçÑ|«LÝcspīðÍg…të_õ‰\\ĉñLYnĝg’ŸRǡÁiHLlõUĹ²uQjYi§Z_c¨Ÿ´ĹĖÙ·ŋI…ƒaBD˜­R¹ȥr—¯G•ºß„K¨jWk’ɱŠOq›Wij\\a­‹Q\\sg_ĆǛōëp»£lğۀgS•ŶN®À]ˆÓäm™ĹãJaz¥V}‰Le¤L„ýo‘¹IsŋÅÇ^‘Žbz…³tmEÁ´aйcčecÇN•ĊãÁ\\蝗dNj•]j†—ZµkÓda•ćå]ğij@ ©O{¤ĸm¢ƒE·®ƒ«|@Xwg]A챝‡XǁÑdzªc›wQÚŝñsÕ³ÛV_ýƒ˜¥\\ů¥©¾÷w—Ž©WÕÊĩhÿÖÁRo¸V¬âDb¨šhûx–Ê×nj~Zâƒg|šXÁnßYoº§ZÅŘvŒ[„ĭÖʃuďxcVbnUSf…B¯³_Tzº—ΕO©çMÑ~Mˆ³]µ^püµ”ŠÄY~y@X~¤Z³€[Èōl@®Å¼£QKƒ·Di‹¡By‘ÿ‰Q_´D¥hŗyƒ^ŸĭÁZ]cIzý‰ah¹MĪğP‘s{ò‡‹‘²Vw¹t³Ŝˁ[ŽÑ}X\\gsFŸ£sPAgěp×ëfYHāďÖqēŭOÏë“dLü•\\iŒ”t^c®šRʺ¶—¢H°mˆ‘rYŸ£BŸ¹čIoľu¶uI]vģSQ{ƒUŻ”Å}QÂ|̋°ƅ¤ĩŪU ęĄžÌZҞ\\v˜²PĔ»ƢNHƒĂyAmƂwVmž`”]ȏb•”H`‰Ì¢²ILvĜ—H®¤Dlt_„¢JJÄämèÔDëþgºƫ™”aʎÌrêYi~ ÎݤNpÀA¾Ĕ¼b…ð÷’Žˆ‡®‚”üs”zMzÖĖQdȨý†v§Tè|ªH’þa¸|šÐ ƒwKĢx¦ivr^ÿ ¸l öæfƟĴ·PJv}n\\h¹¶v†·À|\\ƁĚN´Ĝ€çèÁz]ġ¤²¨QÒŨTIl‡ªťØ}¼˗ƦvÄùØE‹’«Fï˛Iq”ōŒTvāÜŏ‚íÛߜÛV—j³âwGăÂíNOŠˆŠPìyV³ʼnĖýZso§HіiYw[߆\\X¦¥c]ÔƩÜ·«j‡ÐqvÁ¦m^ċ±R™¦΋ƈťĚgÀ»IïĨʗƮްƝ˜ĻþÍAƉſ±tÍEÕÞāNU͗¡\\ſčåÒʻĘm ƭÌŹöʥ’ëQ¤µ­ÇcƕªoIýˆ‰Iɐ_mkl³ă‰Ɠ¦j—¡Yz•Ňi–}Msßõ–īʋ —}ƒÁVmŸ_[n}eı­Uĥ¼‘ª•I{ΧDӜƻėoj‘qYhĹT©oūĶ£]ďxĩ‹ǑMĝ‰q`B´ƃ˺Ч—ç~™²ņj@”¥@đ´ί}ĥtPńǾV¬ufӃÉC‹tÓ̻‰…¹£G³€]ƖƾŎĪŪĘ̖¨ʈĢƂlɘ۪üºňUðǜȢƢż̌ȦǼ‚ĤŊɲĖ­Kq´ï¦—ºĒDzņɾªǀÞĈĂD†½ĄĎÌŗĞrôñnŽœN¼â¾ʄľԆ|DŽŽ֦ज़ȗlj̘̭ɺƅêgV̍ʆĠ·ÌĊv|ýĖÕWĊǎÞ´õ¼cÒÒBĢ͢UĜð͒s¨ňƃLĉÕÝ@ɛƯ÷¿Ľ­ĹeȏijëCȚDŲyê×Ŗyò¯ļcÂßY…tÁƤyAã˾J@ǝrý‹‰@¤…rz¸oP¹ɐÚyᐇHŸĀ[Jw…cVeȴϜ»ÈŽĖ}ƒŰŐèȭǢόĀƪÈŶë;Ñ̆ȤМľĮEŔ—ĹŊũ~ËUă{ŸĻƹɁύȩþĽvĽƓÉ@ē„ĽɲßǐƫʾǗĒpäWÐxnsÀ^ƆwW©¦cÅ¡Ji§vúF¶Ž¨c~c¼īŒeXǚ‹\\đ¾JŽwÀďksãA‹fÕ¦L}wa‚o”Z’‹D½†Ml«]eÒÅaɲáo½FõÛ]ĻÒ¡wYR£¢rvÓ®y®LF‹LzĈ„ôe]gx}•|KK}xklL]c¦£fRtív¦†PĤoH{tK"
]
],
"encodeOffsets": [[[108619, 36299]], [[108589, 36341]]]
}
},
{
"type": "Feature",
"id": "630000",
"properties": { "id": "630000", "cp": [96.778916, 35.623178], "name": "青海", "childNum": 2 },
"geometry": {
"type": "MultiPolygon",
"coordinates": [
["@@InJm"],
[
"@@CƒÆ½OŃĦsΰ~dz¦@@“Ņiš±è}ؘƄ˹A³r_ĞŠǒNΌĐw¤^ŬĵªpĺSZg’rpiƼĘԛ¨C|͖J’©Ħ»®VIJ~f\\m `Un„˜~ʌŸ•ĬàöNt•~ňjy–¢Zi˜Ɣ¥ĄŠk´nl`JʇŠJþ©pdƖ®È£¶ìRʦ‘źõƮËnŸʼėæÑƀĎ[‚˜¢VÎĂMÖÝÎF²sƊƀÎBļýƞ—¯ʘƭðħ¼Jh¿ŦęΌƇš¥²Q]Č¥nuÂÏriˆ¸¬ƪÛ^Ó¦d€¥[Wà…x\\ZŽjҕ¨GtpþYŊĕ´€zUO뇉P‰îMĄÁxH´á˜iÜUà›îÜՁĂÛSuŎ‹r“œJð̬EŒ‘FÁú×uÃÎkr“Ē{V}İ«O_ÌËĬ©ŽÓŧSRѱ§Ģ£^ÂyèçěM³Ƃę{[¸¿u…ºµ[gt£¸OƤĿéYŸõ·kŸq]juw¥Dĩƍ€õÇPéĽG‘ž©ã‡¤G…uȧþRcÕĕNy“yût“ˆ­‡ø‘†ï»a½ē¿BMoᣟÍj}éZËqbʍš“Ƭh¹ìÿÓAçãnIáI`ƒks£CG­ě˜Uy×Cy•…’Ÿ@¶ʡÊBnāzG„ơMē¼±O÷õJËĚăVŸĪũƆ£Œ¯{ËL½Ìzż“„VR|ĠTbuvJvµhĻĖH”Aëáa…­OÇðñęNw‡…œľ·L›mI±íĠĩPÉ×®ÿs—’cB³±JKßĊ«`…ađ»·QAmO’‘Vţéÿ¤¹SQt]]Çx€±¯A@ĉij¢Ó祖•ƒl¶ÅÛr—ŕspãRk~¦ª]Į­´“FR„åd­ČsCqđéFn¿Åƃm’Éx{W©ºƝºįkÕƂƑ¸wWūЩÈFž£\\tÈ¥ÄRÈýÌJ ƒlGr^×äùyÞ³fj”c†€¨£ÂZ|ǓMĝšÏ@ëÜőR‹›ĝ‰Œ÷¡{aïȷPu°ËXÙ{©TmĠ}Y³’­ÞIňµç½©C¡į÷¯B»|St»›]vƒųƒs»”}MÓ ÿʪƟǭA¡fs˜»PY¼c¡»¦c„ċ­¥£~msĉP•–Siƒ^o©A‰Šec‚™PeǵŽkg‚yUi¿h}aH™šĉ^|ᴟ¡HØûÅ«ĉ®]m€¡qĉ¶³ÈyôōLÁst“BŸ®wn±ă¥HSò뚣˜S’ë@לÊăxÇN©™©T±ª£IJ¡fb®ÞbŽb_Ą¥xu¥B—ž{łĝ³«`d˜Ɛt—¤ťiñžÍUuºí`£˜^tƃIJc—·ÛLO‹½Šsç¥Ts{ă\\_»™kϊ±q©čiìĉ|ÍIƒ¥ć¥›€]ª§D{ŝŖÉR_sÿc³Īō›ƿΑ›§p›[ĉ†›c¯bKm›R¥{³„Z†e^ŽŒwx¹dƽŽôIg §Mĕ ƹĴ¿—ǣÜ̓]‹Ý–]snåA{‹eŒƭ`ǻŊĿ\\ijŬű”YÂÿ¬jĖqŽßbЏ•L«¸©@ěĀ©ê¶ìÀEH|´bRľž–Ó¶rÀQþ‹vl®Õ‚E˜TzÜdb ˜hw¤{LR„ƒd“c‹b¯‹ÙVgœ‚ƜßzÃô쮍^jUèXΖ|UäÌ»rKŽ\\ŒªN‘¼pZCü†VY††¤ɃRi^rPҒTÖ}|br°qňb̰ªiƶGQ¾²„x¦PœmlŜ‘[Ĥ¡ΞsĦŸÔÏâ\\ªÚŒU\\f…¢N²§x|¤§„xĔsZPòʛ²SÐqF`ª„VƒÞŜĶƨVZŒÌL`ˆ¢dŐIqr\\oäõ–F礻Ŷ×h¹]Clـ\\¦ďÌį¬řtTӺƙgQÇÓHţĒ”´ÃbEÄlbʔC”|CˆŮˆk„Ʈ[ʼ¬ňœ´KŮÈΰÌζƶlð”ļA†TUvdTŠG†º̼ŠÔ€ŒsÊDԄveOg"
]
],
"encodeOffsets": [[[105308, 37219]], [[95370, 40081]]]
}
},
{
"type": "Feature",
"id": "640000",
"properties": { "id": "640000", "cp": [106.278179, 37.26637], "name": "宁夏", "childNum": 2 },
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
"@@KëÀęĞ«OęȿȕŸı]ʼn¡åįÕÔ«Ǵõƪ™ĚQÐZhv K°›öqÀѐS[ÃÖHƖčË‡nL]ûc…Ùß@‚“ĝ‘¾}w»»‹oģF¹œ»kÌÏ·{zPƒ§B­¢íyÅt@ƒ@áš]Yv_ssģ¼i߁”ĻL¾ġsKD£¡N_…“˜X¸}B~Haiˆ™Åf{«x»ge_bs“KF¯¡Ix™mELcÿZ¤­Ģ‘ƒÝœsuBLù•t†ŒYdˆmVtNmtOPhRw~bd…¾qÐ\\âÙH\\bImlNZŸ»loƒŸqlVm–Gā§~QCw¤™{A\\‘PKŸNY‡¯bF‡kC¥’sk‹Šs_Ã\\ă«¢ħkJi¯r›rAhĹûç£CU‡ĕĊ_ԗBixÅُĄnªÑaM~ħpOu¥sîeQ¥¤^dkKwlL~{L~–hw^‚ófćƒKyEŒ­K­zuÔ¡qQ¤xZÑ¢^ļöܾEpž±âbÊÑÆ^fk¬…NC¾‘Œ“YpxbK~¥Že֎ŒäBlt¿Đx½I[ĒǙŒWž‹f»Ĭ}d§dµùEuj¨‚IÆ¢¥dXªƅx¿]mtÏwßR͌X¢͎vÆzƂZò®ǢÌʆCrâºMÞzžÆMҔÊÓŊZľ–r°Î®Ȉmª²ĈUªĚøºˆĮ¦ÌĘk„^FłĬhĚiĀ˾iİbjÕ"
],
["@@mfwěwMrŢªv@G‰"]
],
"encodeOffsets": [[[109366, 40242]], [[108600, 36303]]]
}
},
{
"type": "Feature",
"id": "650000",
"properties": { "id": "650000", "cp": [85.617733, 40.792818], "name": "新疆", "childNum": 1 },
"geometry": {
"type": "Polygon",
"coordinates": [
"@@QØĔ²X¨”~ǘBºjʐߨvK”ƔX¨vĊOžÃƒ·¢i@~c—‡ĝe_«”Eš“}QxgɪëÏÃ@sÅyXoŖ{ô«ŸuX…ê•Îf`œC‚¹ÂÿÐGĮÕĞXŪōŸMźÈƺQèĽôe|¿ƸJR¤ĘEjcUóº¯Ĩ_ŘÁMª÷Ð¥Oéȇ¿ÖğǤǷÂF҇zÉx[]­Ĥĝ‰œ¦EP}ûƥé¿İƷTėƫœŕƅ™ƱB»Đ±’ēO…¦E–•}‘`cȺrĦáŖuҞª«IJ‡πdƺÏØZƴwʄ¤ĖGЙǂZ̓èH¶}ÚZצʥĪï|ÇĦMŔ»İĝLj‹ì¥Βœba­¯¥ǕǚkĆŵĦɑĺƯxūД̵nơʃĽá½M»›òmqóŘĝč˾ăC…ćāƿÝɽ©DZŅ¹đ¥˜³ðLrÁ®ɱĕģʼnǻ̋ȥơŻǛȡVï¹Ň۩ûkɗġƁ§ʇė̕ĩũƽō^ƕŠUv£ƁQï“Ƶkŏ½ΉÃŭdzLқʻ«ƭ\\lƒ‡ŭD‡“{ʓDkaFÃÄa“³ŤđÔGRÈƚhSӹŚsİ«ĐË[¥ÚDkº^Øg¼ŵ¸£EÍö•€ůʼnT¡c_‡ËKY‹ƧUśĵ„݃U_©rETÏʜ±OñtYw獃{£¨uM³x½şL©Ùá[ÓÐĥ Νtģ¢\\‚ś’nkO›w¥±ƒT»ƷFɯàĩÞáB¹Æ…ÑUw„੍žĽw[“mG½Èå~‡Æ÷QyŠěCFmĭZī—ŵVÁ™ƿQƛ—ûXS²‰b½KϽĉS›©ŷXĕŸ{ŽĕK·¥Ɨcqq©f¿]‡ßDõU³h—­gËÇïģÉɋw“k¯í}I·šœbmœÉ–ř›īJɥĻˁ×xo›ɹī‡l•c…¤³Xù]‘™DžA¿w͉ì¥wÇN·ÂËnƾƍdǧđ®Ɲv•Um©³G\\“}µĿ‡QyŹl㓛µEw‰LJQ½yƋBe¶ŋÀů‡ož¥A—˜Éw@•{Gpm¿Aij†ŽKLhˆ³`ñcËtW‚±»ÕS‰ëüÿďD‡u\\wwwù³—V›LŕƒOMËGh£õP¡™er™Ïd{“‡ġWÁ…č|yšg^ğyÁzÙs`—s|ÉåªÇ}m¢Ń¨`x¥’ù^•}ƒÌ¥H«‰Yªƅ”Aйn~Ꝛf¤áÀz„gŠÇDIԝ´AňĀ҄¶ûEYospõD[{ù°]u›Jq•U•|Soċxţ[õÔĥkŋÞŭZ˺óYËüċrw €ÞkrťË¿XGÉbřaDü·Ē÷Aê[Ää€I®BÕИÞ_¢āĠpŠÛÄȉĖġDKwbm‡ÄNô‡ŠfœƫVÉvi†dz—H‘‹QµâFšù­Âœ³¦{YGžƒd¢ĚÜO „€{Ö¦ÞÍÀPŒ^b–ƾŠlŽ[„vt×ĈÍE˨¡Đ~´î¸ùÎh€uè`¸ŸHÕŔVºwĠââWò‡@{œÙNÝ´ə²ȕn{¿¥{l—÷eé^e’ďˆXj©î\\ªÑò˜Üìc\\üqˆÕ[Č¡xoÂċªbØ­Œø|€¶ȴZdÆÂšońéŒGš\\”¼C°ÌƁn´nxšÊOĨ’ہƴĸ¢¸òTxÊǪMīИÖŲÃɎOvˆʦƢ~FއRěò—¿ġ~åŊœú‰Nšžš¸qŽ’Ę[Ĕ¶ÂćnÒPĒÜvúĀÊbÖ{Äî¸~Ŕünp¤ÂH¾œĄYÒ©ÊfºmԈĘcDoĬMŬ’˜S¤„s²‚”ʘچžȂVŦ –ŽèW°ªB|IJXŔþÈJĦÆæFĚêŠYĂªĂ]øªŖNÞüA€’fɨJ€˜¯ÎrDDšĤ€`€mz\\„§~D¬{vJÂ˜«lµĂb–¤p€ŌŰNĄ¨ĊXW|ų ¿¾ɄĦƐMT”‡òP˜÷fØĶK¢ȝ˔Sô¹òEð­”`Ɩ½ǒÂň×äı–§ĤƝ§C~¡‚hlå‚ǺŦŞkâ’~}ŽFøàIJaĞ‚fƠ¥Ž„Ŕdž˜®U¸ˆźXœv¢aƆúŪtŠųƠjd•ƺŠƺÅìnrh\\ĺ¯äɝĦ]èpĄ¦´LƞĬŠ´ƤǬ˼Ēɸ¤rºǼ²¨zÌPðŀbþ¹ļD¢¹œ\\ĜÑŚŸ¶ZƄ³àjĨoâŠȴLʉȮŒĐ­ĚăŽÀêZǚŐ¤qȂ\\L¢ŌİfÆs|zºeªÙæ§΢{Ā´ƐÚ¬¨Ĵà²łhʺKÞºÖTŠiƢ¾ªì°`öøu®Ê¾ãØ"
],
"encodeOffsets": [[88824, 50096]]
}
},
{
"type": "Feature",
"id": "110000",
"properties": {
"id": "110000",
"cp": [116.405285, 39.904989],
"name": "北京",
"childNum": 1
},
"geometry": {
"type": "Polygon",
"coordinates": [
"@@ĽOÁ›ûtŷmiÍt_H»Ĩ±d`й­{bw…Yr“³S]§§o¹€qGtm_Sŧ€“oa›‹FLg‘QN_•dV€@Zom_ć\\ߚc±x¯oœRcfe…£’o§ËgToÛJíĔóu…|wP¤™XnO¢ÉˆŦ¯rNÄā¤zâŖÈRpŢZŠœÚ{GŠrFt¦Òx§ø¹RóäV¤XdˆżâºWbwڍUd®bêņ¾‘jnŎGŃŶŠnzÚSeîĜZczî¾i]͜™QaúÍÔiþĩȨWĢ‹ü|Ėu[qb[swP@ÅğP¿{\\‡¥A¨Ï‘Ѩj¯ŠX\\¯œMK‘pA³[H…īu}}"
],
"encodeOffsets": [[120023, 41045]]
}
},
{
"type": "Feature",
"id": "120000",
"properties": {
"id": "120000",
"cp": [117.190182, 39.125596],
"name": "天津",
"childNum": 1
},
"geometry": {
"type": "Polygon",
"coordinates": [
"@@ŬgX§Ü«E…¶Ḟ“¬O_™ïlÁg“z±AXe™µÄĵ{¶]gitgšIj·›¥îakS€‰¨ÐƎk}ĕ{gB—qGf{¿a†U^fI“ư‹³õ{YƒıëNĿžk©ïËZŏ‘R§òoY×Ógc…ĥs¡bġ«@dekąI[nlPqCnp{ˆō³°`{PNdƗqSÄĻNNâyj]äžÒD ĬH°Æ]~¡HO¾ŒX}ÐxŒgp“gWˆrDGˆŒpù‚Š^L‚ˆrzWxˆZ^¨´T\\|~@I‰zƒ–bĤ‹œjeĊªz£®Ĕvě€L†mV¾Ô_ȔNW~zbĬvG†²ZmDM~”~"
],
"encodeOffsets": [[120237, 41215]]
}
},
{
"type": "Feature",
"id": "310000",
"properties": {
"id": "310000",
"cp": [121.472644, 31.231706],
"name": "上海",
"childNum": 6
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
["@@ɧư¬EpƸÁxc‡"],
["@@©„ªƒ"],
["@@”MA‹‘š"],
["@@Qp݁E§ÉC¾"],
["@@bŝՕÕEȣÚƥêImɇǦèÜĠŒÚžÃƌÃ͎ó"],
["@@ǜûȬɋŠŭ™×^‰sYŒɍDŋ‘ŽąñCG²«ªč@h–_p¯A{‡oloY€¬j@IJ`•gQڛhr|ǀ^MIJvtbe´R¯Ô¬¨YŽô¤r]ì†Ƭį"]
],
"encodeOffsets": [
[[124702, 32062]],
[[124547, 32200]],
[[124808, 31991]],
[[124726, 32110]],
[[124903, 32376]],
[[124438, 32149]]
]
}
},
{
"type": "Feature",
"id": "500000",
"properties": {
"id": "500000",
"cp": [107.304962, 29.533155],
"name": "重庆",
"childNum": 2
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
"@@vjG~nGŘŬĶȂƀƾ¹¸ØÎezĆT¸}êЖqHŸðqĖ䒊¥^CƒIj–²p…\\_ æüY|[YxƊæuž°xb®…Űb@~¢NQt°¶‚S栓Ê~rljĔëĚ¢~šuf`‘‚†fa‚ĔJåĊ„nÖ]„jƎćÊ@Š£¾a®£Ű{ŶĕF‹ègLk{Y|¡ĜWƔtƬJÑxq‹±ĢN´‰òK‰™–LÈüD|s`ŋ’ć]ƒÃ‰`đŒMûƱ½~Y°ħ`ƏíW‰½eI‹½{aŸ‘OIrÏ¡ĕŇa†p†µÜƅġ‘œ^ÖÛbÙŽŏml½S‹êqDu[R‹ãË»†ÿw`»y‘¸_ĺę}÷`M¯ċfCVµqʼn÷Z•gg“Œ`d½pDO‡ÎCnœ^uf²ènh¼WtƏxRGg¦…pV„†FI±ŽG^ŒIc´ec‡’G•ĹÞ½sëĬ„h˜xW‚}Kӈe­Xsbk”F¦›L‘ØgTkïƵNï¶}Gy“w\\oñ¡nmĈzjŸ•@™Óc£»Wă¹Ój“_m»ˆ¹·~MvÛaqœ»­‰êœ’\\ÂoVnŽÓØÍ™²«‹bq¿efE „€‹Ĝ^Qž~ Évý‡ş¤²Į‰pEİ}zcĺƒL‹½‡š¿gņ›¡ýE¡ya£³t\\¨\\vú»¼§·Ñr_oÒý¥u‚•_n»_ƒ•At©Þűā§IVeëƒY}{VPÀFA¨ąB}q@|Ou—\\Fm‰QF݅Mw˜å}]•€|FmϋCaƒwŒu_p—¯sfÙgY…DHl`{QEfNysBЦzG¸rHe‚„N\\CvEsÐùÜ_·ÖĉsaQ¯€}_U‡†xÃđŠq›NH¬•Äd^ÝŰR¬ã°wećJEž·vÝ·Hgƒ‚éFXjÉê`|yŒpxkAwœWĐpb¥eOsmzwqChóUQl¥F^laf‹anòsr›EvfQdÁUVf—ÎvÜ^efˆtET¬ôA\\œ¢sJŽnQTjP؈xøK|nBz‰„œĞ»LY‚…FDxӄvr“[ehľš•vN”¢o¾NiÂxGp⬐z›bfZo~hGi’]öF|‰|Nb‡tOMn eA±ŠtPT‡LjpYQ|†SH††YĀxinzDJ€Ìg¢và¥Pg‰_–ÇzII‹€II•„£®S¬„Øs쐣ŒN"
],
["@@ifjN@s"]
],
"encodeOffsets": [[[109628, 30765]], [[111725, 31320]]]
}
},
{
"type": "Feature",
"id": "810000",
"properties": {
"id": "810000",
"cp": [114.173355, 22.320048],
"name": "香港",
"childNum": 5
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
["@@AlBk"],
["@@mŽn"],
["@@EpFo"],
["@@ea¢pl¸Eõ¹‡hj[ƒ]ÔCΖ@lj˜¡uBXŸ…•´‹AI¹…[‹yDUˆ]W`çwZkmc–…M›žp€Åv›}I‹oJlcaƒfёKްä¬XJmРđhI®æÔtSHn€Eˆ„ÒrÈc"],
["@@rMUw‡AS®€e"]
],
"encodeOffsets": [
[[117111, 23002]],
[[117072, 22876]],
[[117045, 22887]],
[[116975, 23082]],
[[116882, 22747]]
]
}
},
{
"type": "Feature",
"id": "820000",
"properties": { "id": "820000", "cp": [113.54909, 22.198951], "name": "澳门", "childNum": 1 },
"geometry": {
"type": "Polygon",
"coordinates": ["@@kÊd°å§s"],
"encodeOffsets": [[116279, 22639]]
}
}
],
"UTF8Encoding": true
}
+1 -1
View File
@@ -3,7 +3,7 @@ defineComponent({
name: 'CardTitle'
})
const { title } = defineProps({
defineProps({
title: {
type: String,
required: true
+34
View File
@@ -0,0 +1,34 @@
<template>
<el-input v-model="color">
<template #prepend>
<el-color-picker v-model="color" :predefine="PREDEFINE_COLORS" />
</template>
</el-input>
</template>
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes'
import { PREDEFINE_COLORS } from '@/utils/color'
// 颜色输入框
defineOptions({ name: 'ColorInput' })
const props = defineProps({
modelValue: propTypes.string.def('')
})
const emit = defineEmits(['update:modelValue'])
const color = computed({
get: () => {
return props.modelValue
},
set: (val: string) => {
emit('update:modelValue', val)
}
})
</script>
<style scoped lang="scss">
:deep(.el-input-group__prepend) {
padding: 0;
}
</style>
@@ -1,20 +1,19 @@
<script lang="ts" setup>
<script setup lang="ts">
import { provide, computed, watch, onMounted } from 'vue'
import { propTypes } from '@/utils/propTypes'
import { ComponentSize, ElConfigProvider } from 'element-plus'
import { useLocaleStore } from '@/store/modules/locale'
import { useWindowSize } from '@vueuse/core'
import { useAppStore } from '@/store/modules/app'
import { setCssVar } from '@/utils'
import { useDesign } from '@/hooks/web/useDesign'
import { ElementPlusSize } from '@/types/elementPlus'
import { useWindowSize } from '@vueuse/core'
defineOptions({ name: 'ConfigGlobal' })
const { variables } = useDesign()
const appStore = useAppStore()
const props = defineProps({
size: propTypes.oneOf<ElementPlusSize>(['default', 'small', 'large']).def('default')
size: propTypes.oneOf<ComponentSize>(['default', 'small', 'large']).def('default')
})
provide('configGlobal', props)
@@ -53,9 +52,9 @@ const currentLocale = computed(() => localeStore.currentLocale)
<template>
<ElConfigProvider
:namespace="variables.elNamespace"
:locale="currentLocale.elLocale"
:message="{ max: 1 }"
:namespace="variables.elNamespace"
:size="size"
>
<slot></slot>
@@ -0,0 +1,235 @@
<template>
<div :class="['component', { active: active }]">
<div
:style="{
...style
}"
>
<component :is="component.id" :property="component.property" />
</div>
<div class="component-wrap">
<!-- 左侧组件名 -->
<div class="component-name" v-if="component.name">
{{ component.name }}
</div>
<!-- 左侧:组件操作工具栏 -->
<div class="component-toolbar" v-if="showToolbar && component.name && active">
<VerticalButtonGroup type="primary">
<el-tooltip content="上移" placement="right">
<el-button :disabled="!canMoveUp" @click.stop="handleMoveComponent(-1)">
<Icon icon="ep:arrow-up" />
</el-button>
</el-tooltip>
<el-tooltip content="下移" placement="right">
<el-button :disabled="!canMoveDown" @click.stop="handleMoveComponent(1)">
<Icon icon="ep:arrow-down" />
</el-button>
</el-tooltip>
<el-tooltip content="复制" placement="right">
<el-button @click.stop="handleCopyComponent()">
<Icon icon="ep:copy-document" />
</el-button>
</el-tooltip>
<el-tooltip content="删除" placement="right">
<el-button @click.stop="handleDeleteComponent()">
<Icon icon="ep:delete" />
</el-button>
</el-tooltip>
</VerticalButtonGroup>
</div>
</div>
</div>
</template>
<script lang="ts">
// 注册所有的组件
import { components } from '../components/mobile/index'
export default {
components: { ...components }
}
</script>
<script setup lang="ts">
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
import { propTypes } from '@/utils/propTypes'
import { object } from 'vue-types'
/**
* 组件容器
* 用于包裹组件,为组件提供 背景、外边距、内边距、边框等样式
*/
defineOptions({ name: 'ComponentContainer' })
type DiyComponentWithStyle = DiyComponent<any> & { property: { style?: ComponentStyle } }
const props = defineProps({
component: object<DiyComponentWithStyle>().isRequired,
active: propTypes.bool.def(false),
canMoveUp: propTypes.bool.def(false),
canMoveDown: propTypes.bool.def(false),
showToolbar: propTypes.bool.def(true)
})
/**
* 组件样式
*/
const style = computed(() => {
let componentStyle = props.component.property.style
if (!componentStyle) {
return {}
}
return {
marginTop: `${componentStyle.marginTop || 0}px`,
marginBottom: `${componentStyle.marginBottom || 0}px`,
marginLeft: `${componentStyle.marginLeft || 0}px`,
marginRight: `${componentStyle.marginRight || 0}px`,
paddingTop: `${componentStyle.paddingTop || 0}px`,
paddingRight: `${componentStyle.paddingRight || 0}px`,
paddingBottom: `${componentStyle.paddingBottom || 0}px`,
paddingLeft: `${componentStyle.paddingLeft || 0}px`,
borderTopLeftRadius: `${componentStyle.borderTopLeftRadius || 0}px`,
borderTopRightRadius: `${componentStyle.borderTopRightRadius || 0}px`,
borderBottomRightRadius: `${componentStyle.borderBottomRightRadius || 0}px`,
borderBottomLeftRadius: `${componentStyle.borderBottomLeftRadius || 0}px`,
overflow: 'hidden',
background:
componentStyle.bgType === 'color' ? componentStyle.bgColor : `url(${componentStyle.bgImg})`
}
})
const emits = defineEmits<{
(e: 'move', direction: number): void
(e: 'copy'): void
(e: 'delete'): void
}>()
/**
* 移动组件
* @param direction 移动方向
*/
const handleMoveComponent = (direction: number) => {
emits('move', direction)
}
/**
* 复制组件
*/
const handleCopyComponent = () => {
emits('copy')
}
/**
* 删除组件
*/
const handleDeleteComponent = () => {
emits('delete')
}
</script>
<style scoped lang="scss">
$active-border-width: 2px;
$hover-border-width: 1px;
$name-position: -85px;
$toolbar-position: -55px;
/* 组件 */
.component {
position: relative;
cursor: move;
.component-wrap {
position: absolute;
top: 0;
left: -$active-border-width;
display: block;
width: 100%;
height: 100%;
/* 鼠标放到组件上时 */
&:hover {
border: $hover-border-width dashed var(--el-color-primary);
box-shadow: 0 0 5px 0 rgb(24 144 255 / 30%);
.component-name {
top: $hover-border-width;
/* 防止加了边框之后,位置移动 */
left: $name-position - $hover-border-width;
}
}
/* 左侧:组件名称 */
.component-name {
position: absolute;
top: $active-border-width;
left: $name-position;
display: block;
width: 80px;
height: 25px;
font-size: 12px;
line-height: 25px;
text-align: center;
background: #fff;
box-shadow:
0 0 4px #00000014,
0 2px 6px #0000000f,
0 4px 8px 2px #0000000a;
/* 右侧小三角 */
&::after {
position: absolute;
top: 7.5px;
right: -10px;
width: 0;
height: 0;
border: 5px solid transparent;
border-left-color: #fff;
content: ' ';
}
}
/* 右侧:组件操作工具栏 */
.component-toolbar {
position: absolute;
top: 0;
right: $toolbar-position;
display: none;
/* 左侧小三角 */
&::before {
position: absolute;
top: 10px;
left: -10px;
width: 0;
height: 0;
border: 5px solid transparent;
border-right-color: #2d8cf0;
content: ' ';
}
}
}
/* 组件选中时 */
&.active {
margin-bottom: 4px;
.component-wrap {
margin-bottom: $active-border-width + $active-border-width;
border: $active-border-width solid var(--el-color-primary) !important;
box-shadow: 0 0 10px 0 rgb(24 144 255 / 30%);
.component-name {
top: 0 !important;
/* 防止加了边框之后,位置移动 */
left: $name-position - $active-border-width !important;
color: #fff;
background: var(--el-color-primary);
&::after {
border-left-color: var(--el-color-primary);
}
}
.component-toolbar {
display: block;
}
}
}
}
</style>
@@ -0,0 +1,164 @@
<template>
<el-tabs stretch>
<el-tab-pane label="内容">
<slot></slot>
</el-tab-pane>
<el-tab-pane label="样式" lazy>
<el-card header="组件样式" class="property-group">
<el-form :model="formData" label-width="80px">
<el-form-item label="组件背景" prop="bgType">
<el-radio-group v-model="formData.bgType">
<el-radio label="color">纯色</el-radio>
<el-radio label="img">图片</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="选择颜色" prop="bgColor" v-if="formData.bgType === 'color'">
<ColorInput v-model="formData.bgColor" />
</el-form-item>
<el-form-item label="上传图片" prop="bgImg" v-else>
<UploadImg v-model="formData.bgImg" :limit="1">
<template #tip>建议宽度 750px</template>
</UploadImg>
</el-form-item>
<el-tree :data="treeData" :expand-on-click-node="false">
<template #default="{ node, data }">
<el-form-item
:label="data.label"
:prop="data.prop"
:label-width="node.level === 1 ? '80px' : '62px'"
class="w-full m-b-0!"
>
<el-slider
v-model="formData[data.prop]"
:max="100"
:min="0"
show-input
input-size="small"
:show-input-controls="false"
@input="handleSliderChange(data.prop)"
/>
</el-form-item>
</template>
</el-tree>
<slot name="style" :formData="formData"></slot>
</el-form>
</el-card>
</el-tab-pane>
</el-tabs>
</template>
<script setup lang="ts">
import { ComponentStyle, usePropertyForm } from '@/components/DiyEditor/util'
/**
* 组件容器属性
* 用于包裹组件,为组件提供 背景、外边距、内边距、边框等样式
*/
defineOptions({ name: 'ComponentContainer' })
const props = defineProps<{ modelValue: ComponentStyle }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const treeData = [
{
label: '外部边距',
prop: 'margin',
children: [
{
label: '上',
prop: 'marginTop'
},
{
label: '右',
prop: 'marginRight'
},
{
label: '下',
prop: 'marginBottom'
},
{
label: '左',
prop: 'marginLeft'
}
]
},
{
label: '内部边距',
prop: 'padding',
children: [
{
label: '上',
prop: 'paddingTop'
},
{
label: '右',
prop: 'paddingRight'
},
{
label: '下',
prop: 'paddingBottom'
},
{
label: '左',
prop: 'paddingLeft'
}
]
},
{
label: '边框圆角',
prop: 'borderRadius',
children: [
{
label: '上左',
prop: 'borderTopLeftRadius'
},
{
label: '上右',
prop: 'borderTopRightRadius'
},
{
label: '下右',
prop: 'borderBottomRightRadius'
},
{
label: '下左',
prop: 'borderBottomLeftRadius'
}
]
}
]
const handleSliderChange = (prop: string) => {
switch (prop) {
case 'margin':
formData.value.marginTop = formData.value.margin
formData.value.marginRight = formData.value.margin
formData.value.marginBottom = formData.value.margin
formData.value.marginLeft = formData.value.margin
break
case 'padding':
formData.value.paddingTop = formData.value.padding
formData.value.paddingRight = formData.value.padding
formData.value.paddingBottom = formData.value.padding
formData.value.paddingLeft = formData.value.padding
break
case 'borderRadius':
formData.value.borderTopLeftRadius = formData.value.borderRadius
formData.value.borderTopRightRadius = formData.value.borderRadius
formData.value.borderBottomRightRadius = formData.value.borderRadius
formData.value.borderBottomLeftRadius = formData.value.borderRadius
break
}
}
</script>
<style scoped lang="scss">
:deep(.el-slider__runway) {
margin-right: 16px;
}
:deep(.el-input-number) {
width: 50px;
}
</style>
@@ -0,0 +1,205 @@
<template>
<el-aside class="editor-left" width="261px">
<el-scrollbar>
<el-collapse v-model="extendGroups">
<el-collapse-item
v-for="group in groups"
:key="group.name"
:name="group.name"
:title="group.name"
>
<draggable
class="component-container"
ghost-class="draggable-ghost"
item-key="index"
:list="group.components"
:sort="false"
:group="{ name: 'component', pull: 'clone', put: false }"
:clone="handleCloneComponent"
:animation="200"
:force-fallback="true"
>
<template #item="{ element }">
<div>
<div class="drag-placement">组件放置区域</div>
<div class="component">
<Icon :icon="element.icon" :size="32" />
<span class="mt-4px text-12px">{{ element.name }}</span>
</div>
</div>
</template>
</draggable>
</el-collapse-item>
</el-collapse>
</el-scrollbar>
</el-aside>
</template>
<script setup lang="ts">
import draggable from 'vuedraggable'
import { componentConfigs } from '../components/mobile/index'
import { cloneDeep } from 'lodash-es'
import { DiyComponent, DiyComponentLibrary } from '@/components/DiyEditor/util'
/** 组件库 */
defineOptions({ name: 'ComponentLibrary' })
// 组件列表
const props = defineProps<{
list: DiyComponentLibrary[]
}>()
const groups = reactive<any[]>([])
// 展开的折叠面板
const extendGroups = reactive<string[]>([])
watch(
() => props.list,
() => {
// 清除旧数据
extendGroups.length = 0
groups.length = 0
// 重新生成数据
props.list.forEach((group) => {
// 是否展开分组
if (group.extended) {
extendGroups.push(group.name)
}
// 查找组件
const components = group.components
.map((name) => componentConfigs[name] as DiyComponent<any>)
.filter((component) => component)
if (components.length > 0) {
groups.push({
name: group.name,
components
})
}
})
},
{
immediate: true
}
)
// 克隆组件
const handleCloneComponent = (component: DiyComponent<any>) => {
return cloneDeep(component)
}
</script>
<style scoped lang="scss">
.editor-left {
z-index: 1;
flex-shrink: 0;
box-shadow: 8px 0 8px -8px rgb(0 0 0 / 12%);
:deep(.el-collapse) {
border-top: none;
}
:deep(.el-collapse-item__wrap) {
border-bottom: none;
}
:deep(.el-collapse-item__content) {
padding-bottom: 0;
}
:deep(.el-collapse-item__header) {
height: 32px;
padding: 0 24px;
line-height: 32px;
background-color: var(--el-bg-color-page);
border-bottom: none;
}
.component-container {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.component {
display: flex;
width: 86px;
height: 86px;
cursor: move;
border-right: 1px solid var(--el-border-color-lighter);
border-bottom: 1px solid var(--el-border-color-lighter);
flex-direction: column;
align-items: center;
justify-content: center;
.el-icon {
margin-bottom: 4px;
color: gray;
}
}
.component.active,
.component:hover {
color: var(--el-color-white);
background: var(--el-color-primary);
.el-icon {
color: var(--el-color-white);
}
}
.component:nth-of-type(3n) {
border-right: none;
}
}
/* 拖拽占位提示,默认不显示 */
.drag-placement {
display: none;
color: #fff;
}
.drag-area {
/* 拖拽到手机区域时的样式 */
.draggable-ghost {
display: flex;
width: 100%;
height: 40px;
/* 条纹背景 */
background: linear-gradient(
45deg,
#91a8d5 0,
#91a8d5 10%,
#94b4eb 10%,
#94b4eb 50%,
#91a8d5 50%,
#91a8d5 60%,
#94b4eb 60%,
#94b4eb
);
background-size: 1rem 1rem;
transition: all 0.5s;
justify-content: center;
align-items: center;
span {
display: inline-block;
width: 140px;
height: 25px;
font-size: 12px;
line-height: 25px;
color: #fff;
text-align: center;
background: #5487df;
}
/* 拖拽时隐藏组件 */
.component {
display: none;
}
/* 拖拽时显示占位提示 */
.drag-placement {
display: block;
}
}
}
</style>
@@ -0,0 +1,50 @@
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 轮播图属性 */
export interface CarouselProperty {
// 类型:默认 | 卡片
type: 'default' | 'card'
// 指示器样式:点 | 数字
indicator: 'dot' | 'number'
// 是否自动播放
autoplay: boolean
// 播放间隔
interval: number
// 轮播内容
items: CarouselItemProperty[]
// 组件样式
style: ComponentStyle
}
// 轮播内容属性
export interface CarouselItemProperty {
// 类型:图片 | 视频
type: 'img' | 'video'
// 图片链接
imgUrl: string
// 视频链接
videoUrl: string
// 跳转链接
url: string
}
// 定义组件
export const component = {
id: 'Carousel',
name: '轮播图',
icon: 'system-uicons:carousel',
property: {
type: 'default',
indicator: 'dot',
autoplay: false,
interval: 3,
items: [
{ type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-01.jpg', videoUrl: '' },
{ type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-02.jpg', videoUrl: '' }
] as CarouselItemProperty[],
style: {
bgType: 'color',
bgColor: '#fff',
marginBottom: 8
} as ComponentStyle
}
} as DiyComponent<CarouselProperty>
@@ -0,0 +1,43 @@
<template>
<!-- 无图片 -->
<div
class="h-250px flex items-center justify-center bg-gray-3"
v-if="property.items.length === 0"
>
<Icon icon="tdesign:image" class="text-gray-8 text-120px!" />
</div>
<div v-else class="relative">
<el-carousel
height="174px"
:type="property.type === 'card' ? 'card' : ''"
:autoplay="property.autoplay"
:interval="property.interval * 1000"
:indicator-position="property.indicator === 'number' ? 'none' : undefined"
@change="handleIndexChange"
>
<el-carousel-item v-for="(item, index) in property.items" :key="index">
<el-image class="h-full w-full" :src="item.imgUrl" />
</el-carousel-item>
</el-carousel>
<div
v-if="property.indicator === 'number'"
class="absolute bottom-10px right-10px rounded-xl bg-black p-x-8px p-y-2px text-10px text-white opacity-40"
>{{ currentIndex }} / {{ property.items.length }}</div
>
</div>
</template>
<script setup lang="ts">
import { CarouselProperty } from './config'
/** 轮播图 */
defineOptions({ name: 'Carousel' })
defineProps<{ property: CarouselProperty }>()
const currentIndex = ref(0)
const handleIndexChange = (index: number) => {
currentIndex.value = index + 1
}
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,142 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<el-form label-width="80px" :model="formData">
<el-card header="样式设置" class="property-group" shadow="never">
<el-form-item label="样式" prop="type">
<el-radio-group v-model="formData.type">
<el-tooltip class="item" content="默认" placement="bottom">
<el-radio-button label="default">
<Icon icon="system-uicons:carousel" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="卡片" placement="bottom">
<el-radio-button label="card">
<Icon icon="ic:round-view-carousel" />
</el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="指示器" prop="indicator">
<el-radio-group v-model="formData.indicator">
<el-radio label="dot">小圆点</el-radio>
<el-radio label="number">数字</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="是否轮播" prop="autoplay">
<el-switch v-model="formData.autoplay" />
</el-form-item>
<el-form-item label="播放间隔" prop="interval" v-if="formData.autoplay">
<el-slider
v-model="formData.interval"
:max="10"
:min="0.5"
:step="0.5"
show-input
input-size="small"
:show-input-controls="false"
/>
<el-text type="info">单位:秒</el-text>
</el-form-item>
</el-card>
<el-card header="内容设置" class="property-group" shadow="never">
<el-text type="info" size="small"> 拖动左上角的小圆点可对其排序 </el-text>
<template v-if="formData.items[0]">
<draggable
:list="formData.items"
:force-fallback="true"
:animation="200"
handle=".drag-icon"
class="m-t-8px"
item-key="index"
>
<template #item="{ element, index }">
<div class="content mb-4px flex flex-col gap-4px rounded bg-gray-50 p-8px">
<div
class="m--8px m-b-8px flex flex-row items-center justify-between bg-gray-100 p-8px"
>
<Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
<Icon
icon="ep:delete"
class="cursor-pointer text-red-5"
@click="handleDeleteImage(index)"
v-if="formData.items.length > 1"
/>
</div>
<el-form-item label="类型" prop="type" class="m-b-8px!" label-width="50px">
<el-radio-group v-model="element.type">
<el-radio label="img">图片</el-radio>
<el-radio label="video">视频</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="图片"
class="m-b-8px!"
label-width="50px"
v-if="element.type === 'img'"
>
<UploadImg
v-model="element.imgUrl"
draggable="false"
height="80px"
width="100%"
class="min-w-80px"
/>
</el-form-item>
<template v-else>
<el-form-item label="封面" class="m-b-8px!" label-width="50px">
<UploadImg
v-model="element.imgUrl"
draggable="false"
height="80px"
width="100%"
class="min-w-80px"
/>
</el-form-item>
<el-form-item label="视频" class="m-b-8px!" label-width="50px">
<UploadFile
v-model="element.videoUrl"
:file-type="['mp4']"
:limit="1"
:file-size="100"
class="min-w-80px"
/>
</el-form-item>
</template>
<el-form-item label="链接" class="m-b-8px!" label-width="50px">
<el-input placeholder="链接" v-model="element.url" />
</el-form-item>
</div>
</template>
</draggable>
</template>
<el-button @click="handleAddImage" type="primary" plain class="w-full">
添加图片
</el-button>
</el-card>
</el-form>
</ComponentContainerProperty>
</template>
<script setup lang="ts">
import draggable from 'vuedraggable' //拖拽组件
import { CarouselItemProperty, CarouselProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
// 轮播图属性面板
defineOptions({ name: 'CarouselProperty' })
const props = defineProps<{ modelValue: CarouselProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
// 添加图片
const handleAddImage = () => {
formData.value.items.push({} as CarouselItemProperty)
}
// 删除图片
const handleDeleteImage = (index: number) => {
formData.value.items.splice(index, 1)
}
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,78 @@
import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
import { CouponTemplateValidityTypeEnum, PromotionDiscountTypeEnum } from '@/utils/constants'
import { floatToFixed2 } from '@/utils'
import { formatDate } from '@/utils/formatTime'
// 优惠值
export const CouponDiscount = defineComponent({
name: 'CouponDiscount',
props: {
coupon: {
type: CouponTemplateApi.CouponTemplateVO
}
},
setup(props) {
const coupon = props.coupon as CouponTemplateApi.CouponTemplateVO
// 折扣
let value = coupon.discountPercent + ''
let suffix = ' 折'
// 满减
if (coupon.discountType === PromotionDiscountTypeEnum.PRICE.type) {
value = floatToFixed2(coupon.discountPrice)
suffix = ' 元'
}
return () => (
<div>
<span class={'text-20px font-bold'}>{value}</span>
<span>{suffix}</span>
</div>
)
}
})
// 优惠描述
export const CouponDiscountDesc = defineComponent({
name: 'CouponDiscountDesc',
props: {
coupon: {
type: CouponTemplateApi.CouponTemplateVO
}
},
setup(props) {
const coupon = props.coupon as CouponTemplateApi.CouponTemplateVO
// 使用条件
const useCondition = coupon.usePrice > 0 ? `满${floatToFixed2(coupon.usePrice)}元,` : ''
// 优惠描述
const discountDesc =
coupon.discountType === PromotionDiscountTypeEnum.PRICE.type
? `减${floatToFixed2(coupon.discountPrice)}元`
: `打${coupon.discountPercent}折`
return () => (
<div>
<span>{useCondition}</span>
<span>{discountDesc}</span>
</div>
)
}
})
// 有效期
export const CouponValidTerm = defineComponent({
name: 'CouponValidTerm',
props: {
coupon: {
type: CouponTemplateApi.CouponTemplateVO
}
},
setup(props) {
const coupon = props.coupon as CouponTemplateApi.CouponTemplateVO
const text =
coupon.validityType === CouponTemplateValidityTypeEnum.DATE.type
? `有效期:${formatDate(coupon.validStartTime, 'YYYY-MM-DD')} 至 ${formatDate(
coupon.validEndTime,
'YYYY-MM-DD'
)}`
: `领取后第 ${coupon.fixedStartTerm} - ${coupon.fixedEndTerm} 天内可用`
return () => <div>{text}</div>
}
})
@@ -0,0 +1,47 @@
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 商品卡片属性 */
export interface CouponCardProperty {
// 列数
columns: number
// 背景图
bgImg: string
// 文字颜色
textColor: string
// 按钮样式
button: {
// 颜色
color: string
// 背景颜色
bgColor: string
}
// 间距
space: number
// 优惠券编号列表
couponIds: number[]
// 组件样式
style: ComponentStyle
}
// 定义组件
export const component = {
id: 'CouponCard',
name: '优惠券',
icon: 'ep:ticket',
property: {
columns: 1,
bgImg: '',
textColor: '#E9B461',
button: {
color: '#434343',
bgColor: ''
},
space: 0,
couponIds: [],
style: {
bgType: 'color',
bgColor: '',
marginBottom: 8
} as ComponentStyle
}
} as DiyComponent<CouponCardProperty>
@@ -0,0 +1,142 @@
<template>
<el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef">
<div
class="flex flex-row text-12px"
:style="{
gap: `${property.space}px`,
width: scrollbarWidth
}"
>
<div
class="box-content"
:style="{
background: property.bgImg
? `url(${property.bgImg}) 100% center / 100% 100% no-repeat`
: '#fff',
width: `${couponWidth}px`,
color: property.textColor
}"
v-for="(coupon, index) in couponList"
:key="index"
>
<!-- 布局1:1列-->
<div v-if="property.columns === 1" class="m-l-16px flex flex-row justify-between p-8px">
<div class="flex flex-col justify-evenly gap-4px">
<!-- 优惠值 -->
<CouponDiscount :coupon="coupon" />
<!-- 优惠描述 -->
<CouponDiscountDesc :coupon="coupon" />
<!-- 有效期 -->
<CouponValidTerm :coupon="coupon" />
</div>
<div class="flex flex-col justify-evenly">
<div
class="rounded-20px p-x-8px p-y-2px"
:style="{
color: property.button.color,
background: property.button.bgColor
}"
>
立即领取
</div>
</div>
</div>
<!-- 布局2:2列-->
<div
v-else-if="property.columns === 2"
class="m-l-16px flex flex-row justify-between p-8px"
>
<div class="flex flex-col justify-evenly gap-4px">
<!-- 优惠值 -->
<CouponDiscount :coupon="coupon" />
<div>{{ coupon.name }}</div>
</div>
<div class="flex flex-col">
<div
class="h-full w-20px rounded-20px p-x-2px p-y-8px text-center"
:style="{
color: property.button.color,
background: property.button.bgColor
}"
>
立即领取
</div>
</div>
</div>
<!-- 布局3:3列-->
<div v-else class="flex flex-col items-center justify-around gap-4px p-4px">
<!-- 优惠值 -->
<CouponDiscount :coupon="coupon" />
<div>{{ coupon.name }}</div>
<div
class="rounded-20px p-x-8px p-y-2px"
:style="{
color: property.button.color,
background: property.button.bgColor
}"
>
立即领取
</div>
</div>
</div>
</div>
</el-scrollbar>
</template>
<script setup lang="ts">
import { CouponCardProperty } from './config'
import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
import { CouponDiscount } from './component'
import {
CouponDiscountDesc,
CouponValidTerm
} from '@/components/DiyEditor/components/mobile/CouponCard/component'
/** 商品卡片 */
defineOptions({ name: 'CouponCard' })
// 定义属性
const props = defineProps<{ property: CouponCardProperty }>()
// 商品列表
const couponList = ref<CouponTemplateApi.CouponTemplateVO[]>([])
watch(
() => props.property.couponIds,
async () => {
if (props.property.couponIds?.length > 0) {
couponList.value = await CouponTemplateApi.getCouponTemplateList(props.property.couponIds)
}
},
{
immediate: true,
deep: true
}
)
// 手机宽度
const phoneWidth = ref(375)
// 容器
const containerRef = ref()
// 滚动条宽度
const scrollbarWidth = ref('100%')
// 优惠券的宽度
const couponWidth = ref(375)
// 计算布局参数
watch(
() => [props.property, phoneWidth, couponList.value.length],
() => {
// 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数
couponWidth.value =
(phoneWidth.value * 0.95 - props.property.space * (props.property.columns - 1)) /
props.property.columns
// 显示滚动条
scrollbarWidth.value = `${
couponWidth.value * couponList.value.length +
props.property.space * (couponList.value.length - 1)
}px`
},
{ immediate: true, deep: true }
)
onMounted(() => {
// 提取手机宽度
phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375
})
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,104 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<el-form label-width="80px" :model="formData">
<el-card header="优惠券列表" class="property-group" shadow="never">
<div
v-for="(coupon, index) in couponList"
:key="index"
class="flex items-center justify-between"
>
<el-text size="large" truncated>{{ coupon.name }}</el-text>
<el-text type="info" truncated>
<span v-if="coupon.usePrice > 0">满{{ floatToFixed2(coupon.usePrice) }}元,</span>
<span v-if="coupon.discountType === PromotionDiscountTypeEnum.PRICE.type">
减{{ floatToFixed2(coupon.discountPrice) }}元
</span>
<span v-else> 打{{ coupon.discountPercent }}折 </span>
</el-text>
</div>
<el-form-item label-width="0">
<el-button @click="handleAddCoupon" type="primary" plain class="m-t-8px w-full">
<Icon icon="ep:plus" class="mr-5px" /> 添加
</el-button>
</el-form-item>
</el-card>
<el-card header="优惠券样式" class="property-group" shadow="never">
<el-form-item label="列数" prop="type">
<el-radio-group v-model="formData.columns">
<el-tooltip class="item" content="一列" placement="bottom">
<el-radio-button :label="1">
<Icon icon="fluent:text-column-one-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="二列" placement="bottom">
<el-radio-button :label="2">
<Icon icon="fluent:text-column-two-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="三列" placement="bottom">
<el-radio-button :label="3">
<Icon icon="fluent:text-column-three-24-filled" />
</el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="背景图片" prop="bgImg">
<UploadImg v-model="formData.bgImg" height="80px" width="100%" class="min-w-160px" />
</el-form-item>
<el-form-item label="文字颜色" prop="textColor">
<ColorInput v-model="formData.textColor" />
</el-form-item>
<el-form-item label="按钮背景" prop="button.bgColor">
<ColorInput v-model="formData.button.bgColor" />
</el-form-item>
<el-form-item label="按钮文字" prop="button.color">
<ColorInput v-model="formData.button.color" />
</el-form-item>
<el-form-item label="间隔" prop="space">
<el-slider
v-model="formData.space"
:max="100"
:min="0"
show-input
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
</el-card>
</el-form>
</ComponentContainerProperty>
<!-- 优惠券选择 -->
<CouponSelect ref="couponSelectDialog" v-model:multiple-selection="couponList" />
</template>
<script setup lang="ts">
import { CouponCardProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
import { floatToFixed2 } from '@/utils'
import { PromotionDiscountTypeEnum } from '@/utils/constants'
import CouponSelect from '@/views/mall/promotion/coupon/components/CouponSelect.vue'
// 优惠券卡片属性面板
defineOptions({ name: 'CouponCardProperty' })
const props = defineProps<{ modelValue: CouponCardProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
// 优惠券列表
const couponList = ref<CouponTemplateApi.CouponTemplateVO[]>([])
const couponSelectDialog = ref()
// 添加优惠券
const handleAddCoupon = () => {
couponSelectDialog.value.open()
}
watch(
() => couponList.value,
() => {
formData.value.couponIds = couponList.value.map((coupon) => coupon.id)
}
)
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,29 @@
import { DiyComponent } from '@/components/DiyEditor/util'
/** 分割线属性 */
export interface DividerProperty {
// 高度
height: number
// 线宽
lineWidth: number
// 边距类型
paddingType: 'none' | 'horizontal'
// 颜色
lineColor: string
// 类型
borderType: 'solid' | 'dashed' | 'dotted' | 'none'
}
// 定义组件
export const component = {
id: 'Divider',
name: '分割线',
icon: 'tdesign:component-divider-vertical',
property: {
height: 30,
lineWidth: 1,
paddingType: 'none',
lineColor: '#dcdfe6',
borderType: 'solid'
}
} as DiyComponent<DividerProperty>
@@ -0,0 +1,29 @@
<template>
<div
class="flex items-center"
:style="{
height: property.height + 'px'
}"
>
<div
class="w-full"
:style="{
borderTopStyle: property.borderType,
borderTopColor: property.lineColor,
borderTopWidth: `${property.lineWidth}px`,
margin: property.paddingType === 'none' ? '0' : '0px 16px'
}"
></div>
</div>
</template>
<script setup lang="ts">
import { DividerProperty } from './config'
/** 页面顶部导航栏 */
defineOptions({ name: 'Divider' })
defineProps<{ property: DividerProperty }>()
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,80 @@
<template>
<el-form label-width="80px" :model="formData">
<el-form-item label="高度" prop="height">
<el-slider v-model="formData.height" :min="1" :max="100" show-input input-size="small" />
</el-form-item>
<el-form-item label="选择样式" prop="borderType">
<el-radio-group v-model="formData!.borderType">
<el-tooltip
placement="top"
v-for="(item, index) in BORDER_TYPES"
:key="index"
:content="item.text"
>
<el-radio-button :label="item.type">
<Icon :icon="item.icon" />
</el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<template v-if="formData.borderType !== 'none'">
<el-form-item label="线宽" prop="lineWidth">
<el-slider v-model="formData.lineWidth" :min="1" :max="30" show-input input-size="small" />
</el-form-item>
<el-form-item label="左右边距" prop="paddingType">
<el-radio-group v-model="formData!.paddingType">
<el-tooltip content="无边距" placement="top">
<el-radio-button label="none">
<Icon icon="tabler:box-padding" />
</el-radio-button>
</el-tooltip>
<el-tooltip content="左右留边" placement="top">
<el-radio-button label="horizontal">
<Icon icon="vaadin:padding" />
</el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="颜色">
<!-- 分割线颜色 -->
<ColorInput v-model="formData.lineColor" />
</el-form-item>
</template>
</el-form>
</template>
<script setup lang="ts">
import { DividerProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
// 导航栏属性面板
defineOptions({ name: 'DividerProperty' })
const props = defineProps<{ modelValue: DividerProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
//线类型
const BORDER_TYPES = [
{
icon: 'vaadin:line-h',
text: '实线',
type: 'solid'
},
{
icon: 'tabler:line-dashed',
text: '虚线',
type: 'dashed'
},
{
icon: 'tabler:line-dotted',
text: '点线',
type: 'dotted'
},
{
icon: 'entypo:progress-empty',
text: '无',
type: 'none'
}
]
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,27 @@
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 图片展示属性 */
export interface ImageBarProperty {
// 图片链接
imgUrl: string
// 跳转链接
url: string
// 组件样式
style: ComponentStyle
}
// 定义组件
export const component = {
id: 'ImageBar',
name: '图片展示',
icon: 'ep:picture',
property: {
imgUrl: '',
url: '',
style: {
bgType: 'color',
bgColor: '#fff',
marginBottom: 8
} as ComponentStyle
}
} as DiyComponent<ImageBarProperty>
@@ -0,0 +1,24 @@
<template>
<!-- 无图片 -->
<div class="h-50px flex items-center justify-center bg-gray-3" v-if="!property.imgUrl">
<Icon icon="ep:picture" class="text-gray-8 text-30px!" />
</div>
<el-image class="min-h-30px" v-else :src="property.imgUrl" />
</template>
<script setup lang="ts">
import { ImageBarProperty } from './config'
/** 图片展示 */
defineOptions({ name: 'ImageBar' })
defineProps<{ property: ImageBarProperty }>()
</script>
<style scoped lang="scss">
/* 图片 */
img {
display: block;
width: 100%;
height: 100%;
}
</style>
@@ -0,0 +1,34 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<el-form label-width="80px" :model="formData">
<el-form-item label="上传图片" prop="imgUrl">
<UploadImg
v-model="formData.imgUrl"
draggable="false"
height="80px"
width="100%"
class="min-w-80px"
>
<template #tip> 建议宽度750 </template>
</UploadImg>
</el-form-item>
<el-form-item label="链接" prop="url">
<el-input placeholder="链接" v-model="formData.url" />
</el-form-item>
</el-form>
</ComponentContainerProperty>
</template>
<script setup lang="ts">
import { ImageBarProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
// 图片展示属性面板
defineOptions({ name: 'ImageBarProperty' })
const props = defineProps<{ modelValue: ImageBarProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,48 @@
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 广告魔方属性 */
export interface MagicCubeProperty {
// 上圆角
borderRadiusTop: number
// 下圆角
borderRadiusBottom: number
// 间隔
space: number
// 导航菜单列表
list: MagicCubeItemProperty[]
// 组件样式
style: ComponentStyle
}
/** 广告魔方项目属性 */
export interface MagicCubeItemProperty {
// 图标链接
imgUrl: string
// 链接
url: string
// 宽
width: number
// 高
height: number
// 上
top: number
// 左
left: number
}
// 定义组件
export const component = {
id: 'MagicCube',
name: '广告魔方',
icon: 'bi:columns',
property: {
borderRadiusTop: 0,
borderRadiusBottom: 0,
space: 0,
list: [],
style: {
bgType: 'color',
bgColor: '#fff',
marginBottom: 8
} as ComponentStyle
}
} as DiyComponent<MagicCubeProperty>
@@ -0,0 +1,73 @@
<template>
<div
class="relative"
:style="{ height: `${rowCount * CUBE_SIZE}px`, width: `${4 * CUBE_SIZE}px` }"
>
<div
v-for="(item, index) in property.list"
:key="index"
class="absolute"
:style="{
width: `${item.width * CUBE_SIZE - property.space * 2}px`,
height: `${item.height * CUBE_SIZE - property.space * 2}px`,
margin: `${property.space}px`,
top: `${item.top * CUBE_SIZE}px`,
left: `${item.left * CUBE_SIZE}px`
}"
>
<el-image
class="h-full w-full"
fit="cover"
:src="item.imgUrl"
:style="{
borderTopLeftRadius: `${property.borderRadiusTop}px`,
borderTopRightRadius: `${property.borderRadiusTop}px`,
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
borderBottomRightRadius: `${property.borderRadiusBottom}px`
}"
>
<template #error>
<div class="image-slot">
<div
class="flex items-center justify-center"
:style="{
width: `${item.width * CUBE_SIZE}px`,
height: `${item.height * CUBE_SIZE}px`
}"
>
<Icon icon="ep-picture" color="gray" :size="CUBE_SIZE" />
</div>
</div>
</template>
</el-image>
</div>
</div>
</template>
<script setup lang="ts">
import { MagicCubeProperty } from './config'
/** 广告魔方 */
defineOptions({ name: 'MagicCube' })
const props = defineProps<{ property: MagicCubeProperty }>()
// 一个方块的大小
const CUBE_SIZE = 93.75
/**
* 计算方块的行数
* 行数用于计算魔方的总体高度,存在以下情况:
* 1. 没有数据时,默认就只显示一行的高度
* 2. 底部的空白不算高度,例如只有第一行有数据,那么就只显示一行的高度
* 3. 顶部及中间的空白算高度,例如一共有四行,只有最后一行有数据,那么也显示四行的高度
*/
const rowCount = computed(() => {
let count = 0
if (props.property.list.length > 0) {
// 最大行号
count = Math.max(...props.property.list.map((item) => item.bottom))
}
// 行号从 0 开始,所以加 1
return count + 1
})
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,76 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<!-- 表单 -->
<el-form label-width="80px" :model="formData" class="m-t-8px">
<el-text tag="p"> 魔方设置 </el-text>
<el-text type="info" size="small"> 每格尺寸187 * 187 </el-text>
<MagicCubeEditor
class="m-y-16px"
v-model="formData.list"
:rows="4"
:cols="4"
@hot-area-selected="handleHotAreaSelected"
/>
<template v-for="(hotArea, index) in formData.list" :key="index">
<template v-if="selectedHotAreaIndex === index">
<el-form-item label="上传图片" :prop="`list[${index}].imgUrl`">
<UploadImg v-model="hotArea.imgUrl" height="80px" width="80px" />
</el-form-item>
<el-form-item label="链接" :prop="`list[${index}].url`">
<el-input v-model="hotArea.url" placeholder="请输入链接" />
</el-form-item>
</template>
</template>
<el-form-item label="上圆角" prop="borderRadiusTop">
<el-slider
v-model="formData.borderRadiusTop"
:max="100"
:min="0"
show-input
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
<el-form-item label="下圆角" prop="borderRadiusBottom">
<el-slider
v-model="formData.borderRadiusBottom"
:max="100"
:min="0"
show-input
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
<el-form-item label="间隔" prop="space">
<el-slider
v-model="formData.space"
:max="100"
:min="0"
show-input
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
</el-form>
</ComponentContainerProperty>
</template>
<script setup lang="ts">
import { usePropertyForm } from '@/components/DiyEditor/util'
import { MagicCubeProperty } from '@/components/DiyEditor/components/mobile/MagicCube/config'
/** 广告魔方属性面板 */
defineOptions({ name: 'MagicCubeProperty' })
const props = defineProps<{ modelValue: MagicCubeProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
// 选中的热区
const selectedHotAreaIndex = ref(-1)
const handleHotAreaSelected = (_: any, index: number) => {
selectedHotAreaIndex.value = index
}
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,78 @@
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
import { cloneDeep } from 'lodash-es'
/** 宫格导航属性 */
export interface MenuGridProperty {
// 列数
column: number
// 导航菜单列表
list: MenuGridItemProperty[]
// 组件样式
style: ComponentStyle
}
/** 宫格导航项目属性 */
export interface MenuGridItemProperty {
// 图标链接
iconUrl: string
// 标题
title: string
// 标题颜色
titleColor: string
// 副标题
subtitle: string
// 副标题颜色
subtitleColor: string
// 链接
url: string
// 角标
badge: {
// 是否显示
show: boolean
// 角标文字
text: string
// 角标文字颜色
textColor: string
// 角标背景颜色
bgColor: string
}
}
export const EMPTY_MENU_GRID_ITEM_PROPERTY = {
title: '标题',
titleColor: '#333',
subtitle: '副标题',
subtitleColor: '#bbb',
badge: {
show: false,
textColor: '#fff',
bgColor: '#FF6000'
}
} as MenuGridItemProperty
// 定义组件
export const component = {
id: 'MenuGrid',
name: '宫格导航',
icon: 'bi:grid-3x3-gap',
property: {
column: 3,
list: [cloneDeep(EMPTY_MENU_GRID_ITEM_PROPERTY)],
style: {
bgType: 'color',
bgColor: '#fff',
marginBottom: 8,
marginLeft: 8,
marginRight: 8,
padding: 8,
paddingTop: 8,
paddingRight: 8,
paddingBottom: 8,
paddingLeft: 8,
borderRadius: 8,
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
borderBottomRightRadius: 8,
borderBottomLeftRadius: 8
} as ComponentStyle
}
} as DiyComponent<MenuGridProperty>
@@ -0,0 +1,35 @@
<template>
<div class="flex flex-row flex-wrap">
<div
v-for="(item, index) in property.list"
:key="index"
class="relative flex flex-col items-center p-b-14px p-t-20px"
:style="{ width: `${100 * (1 / property.column)}%` }"
>
<!-- 右上角角标 -->
<span
v-if="item.badge?.show"
class="absolute left-50% top-10px z-1 h-20px rounded-50% p-x-6px text-center text-12px leading-20px"
:style="{ color: item.badge.textColor, backgroundColor: item.badge.bgColor }"
>
{{ item.badge.text }}
</span>
<el-image v-if="item.iconUrl" class="h-28px w-28px" :src="item.iconUrl" />
<span class="m-t-8px h-16px text-12px leading-16px" :style="{ color: item.titleColor }">
{{ item.title }}
</span>
<span class="m-t-6px h-12px text-10px leading-12px" :style="{ color: item.subtitleColor }">
{{ item.subtitle }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
import { MenuGridProperty } from './config'
/** 宫格导航 */
defineOptions({ name: 'MenuGrid' })
defineProps<{ property: MenuGridProperty }>()
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,96 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<!-- 表单 -->
<el-form label-width="80px" :model="formData" class="m-t-8px">
<el-form-item label="每行数量" prop="column">
<el-radio-group v-model="formData.column">
<el-radio :label="3">3个</el-radio>
<el-radio :label="4">4个</el-radio>
</el-radio-group>
</el-form-item>
<el-text tag="p"> 菜单设置 </el-text>
<el-text type="info" size="small"> 拖动左侧的小圆点可以调整顺序 </el-text>
<template v-if="formData.list.length">
<VueDraggable
class="m-t-8px"
:list="formData.list"
item-key="index"
handle=".drag-icon"
:forceFallback="true"
:animation="200"
>
<template #item="{ element, index }">
<div class="mb-4px flex flex-col gap-4px rounded bg-gray-100 p-8px">
<div class="flex flex-row justify-between">
<Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
<Icon icon="ep:delete" class="text-red-500" @click="handleDeleteMenu(index)" />
</div>
<el-form-item label="图标" prop="iconUrl">
<UploadImg v-model="element.iconUrl" height="80px" width="80px">
<template #tip> 建议尺寸:44 * 44 </template>
</UploadImg>
</el-form-item>
<el-form-item label="标题" prop="title">
<InputWithColor v-model="element.title" v-model:color="element.titleColor" />
</el-form-item>
<el-form-item label="副标题" prop="subtitle">
<InputWithColor v-model="element.subtitle" v-model:color="element.subtitleColor" />
</el-form-item>
<el-form-item label="链接" prop="url">
<el-input v-model="element.url" />
</el-form-item>
<el-form-item label="显示角标" prop="badge.show">
<el-switch v-model="element.badge.show" />
</el-form-item>
<template v-if="element.badge.show">
<el-form-item label="角标内容" prop="badge.text">
<InputWithColor
v-model="element.badge.text"
v-model:color="element.badge.textColor"
/>
</el-form-item>
<el-form-item label="背景颜色" prop="badge.bgColor">
<ColorInput v-model="element.badge.bgColor" />
</el-form-item>
</template>
</div>
</template>
</VueDraggable>
</template>
<el-form-item label-width="0">
<el-button @click="handleAddMenu" type="primary" plain class="m-t-8px w-full">
<Icon icon="ep:plus" class="mr-5px" /> 添加菜单
</el-button>
</el-form-item>
</el-form>
</ComponentContainerProperty>
</template>
<script setup lang="ts">
import VueDraggable from 'vuedraggable'
import { usePropertyForm } from '@/components/DiyEditor/util'
import {
EMPTY_MENU_GRID_ITEM_PROPERTY,
MenuGridProperty
} from '@/components/DiyEditor/components/mobile/MenuGrid/config'
import { cloneDeep } from 'lodash-es'
/** 宫格导航属性面板 */
defineOptions({ name: 'MenuGridProperty' })
const props = defineProps<{ modelValue: MenuGridProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
/* 添加菜单 */
const handleAddMenu = () => {
formData.value.list.push(cloneDeep(EMPTY_MENU_GRID_ITEM_PROPERTY))
}
/* 删除菜单 */
const handleDeleteMenu = (index: number) => {
formData.value.list.splice(index, 1)
}
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,47 @@
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
import { cloneDeep } from 'lodash-es'
/** 列表导航属性 */
export interface MenuListProperty {
// 导航菜单列表
list: MenuListItemProperty[]
// 组件样式
style: ComponentStyle
}
/** 列表导航项目属性 */
export interface MenuListItemProperty {
// 图标链接
iconUrl: string
// 标题
title: string
// 标题颜色
titleColor: string
// 副标题
subtitle: string
// 副标题颜色
subtitleColor: string
// 链接
url: string
}
export const EMPTY_MENU_LIST_ITEM_PROPERTY = {
title: '标题',
titleColor: '#333',
subtitle: '副标题',
subtitleColor: '#bbb'
}
// 定义组件
export const component = {
id: 'MenuList',
name: '列表导航',
icon: 'fa-solid:list',
property: {
list: [cloneDeep(EMPTY_MENU_LIST_ITEM_PROPERTY)],
style: {
bgType: 'color',
bgColor: '#fff',
marginBottom: 8
} as ComponentStyle
}
} as DiyComponent<MenuListProperty>
@@ -0,0 +1,31 @@
<template>
<div class="min-h-42px flex flex-col">
<div
v-for="(item, index) in property.list"
:key="index"
class="item h-42px flex flex-row items-center justify-between gap-4px p-x-12px"
>
<div class="flex flex-1 flex-row items-center gap-8px">
<el-image v-if="item.iconUrl" class="h-16px w-16px" :src="item.iconUrl" />
<span class="text-16px" :style="{ color: item.titleColor }">{{ item.title }}</span>
</div>
<div class="item-center flex flex-row justify-center gap-4px">
<span class="text-12px" :style="{ color: item.subtitleColor }">{{ item.subtitle }}</span>
<Icon icon="ep-arrow-right" color="#000" :size="16" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { MenuListProperty } from './config'
/** 列表导航 */
defineOptions({ name: 'MenuList' })
defineProps<{ property: MenuListProperty }>()
</script>
<style scoped lang="scss">
.item + .item {
border-top: 1px solid #eee;
}
</style>
@@ -0,0 +1,75 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<el-text tag="p"> 菜单设置 </el-text>
<el-text type="info" size="small"> 拖动左侧的小圆点可以调整顺序 </el-text>
<!-- 表单 -->
<el-form label-width="60px" :model="formData" class="m-t-8px">
<div v-if="formData.list.length">
<VueDraggable
:list="formData.list"
item-key="index"
handle=".drag-icon"
:forceFallback="true"
:animation="200"
>
<template #item="{ element, index }">
<div class="mb-4px flex flex-col gap-4px rounded bg-gray-100 p-8px">
<div class="flex flex-row justify-between">
<Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
<Icon icon="ep:delete" class="text-red-500" @click="handleDeleteMenu(index)" />
</div>
<el-form-item label="图标" prop="iconUrl">
<UploadImg v-model="element.iconUrl" height="80px" width="80px">
<template #tip> 建议尺寸:44 * 44 </template>
</UploadImg>
</el-form-item>
<el-form-item label="标题" prop="title">
<InputWithColor v-model="element.title" v-model:color="element.titleColor" />
</el-form-item>
<el-form-item label="副标题" prop="subtitle">
<InputWithColor v-model="element.subtitle" v-model:color="element.subtitleColor" />
</el-form-item>
<el-form-item label="链接" prop="url">
<el-input v-model="element.url" />
</el-form-item>
</div>
</template>
</VueDraggable>
</div>
<el-form-item label-width="0">
<el-button @click="handleAddMenu" type="primary" plain class="m-t-8px w-full">
<Icon icon="ep:plus" class="mr-5px" /> 添加菜单
</el-button>
</el-form-item>
</el-form>
</ComponentContainerProperty>
</template>
<script setup lang="ts">
import VueDraggable from 'vuedraggable'
import { usePropertyForm } from '@/components/DiyEditor/util'
import {
EMPTY_MENU_LIST_ITEM_PROPERTY,
MenuListProperty
} from '@/components/DiyEditor/components/mobile/MenuList/config'
import { cloneDeep } from 'lodash-es'
/** 列表导航属性面板 */
defineOptions({ name: 'MenuListProperty' })
const props = defineProps<{ modelValue: MenuListProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
/* 添加菜单 */
const handleAddMenu = () => {
formData.value.list.push(cloneDeep(EMPTY_MENU_LIST_ITEM_PROPERTY))
}
/* 删除菜单 */
const handleDeleteMenu = (index: number) => {
formData.value.list.splice(index, 1)
}
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,66 @@
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
import { cloneDeep } from 'lodash-es'
/** 菜单导航属性 */
export interface MenuSwiperProperty {
// 布局: 图标+文字 | 图标
layout: 'iconText' | 'icon'
// 行数
row: number
// 列数
column: number
// 导航菜单列表
list: MenuSwiperItemProperty[]
// 组件样式
style: ComponentStyle
}
/** 菜单导航项目属性 */
export interface MenuSwiperItemProperty {
// 图标链接
iconUrl: string
// 标题
title: string
// 标题颜色
titleColor: string
// 链接
url: string
// 角标
badge: {
// 是否显示
show: boolean
// 角标文字
text: string
// 角标文字颜色
textColor: string
// 角标背景颜色
bgColor: string
}
}
export const EMPTY_MENU_SWIPER_ITEM_PROPERTY = {
title: '标题',
titleColor: '#333',
badge: {
show: false,
textColor: '#fff',
bgColor: '#FF6000'
}
} as MenuSwiperItemProperty
// 定义组件
export const component = {
id: 'MenuSwiper',
name: '菜单导航',
icon: 'bi:grid-3x2-gap',
property: {
layout: 'iconText',
row: 1,
column: 3,
list: [cloneDeep(EMPTY_MENU_SWIPER_ITEM_PROPERTY)],
style: {
bgType: 'color',
bgColor: '#fff',
marginBottom: 8
} as ComponentStyle
}
} as DiyComponent<MenuSwiperProperty>
@@ -0,0 +1,119 @@
<template>
<el-carousel
:height="`${carouselHeight}px`"
:autoplay="false"
arrow="hover"
indicator-position="outside"
>
<el-carousel-item v-for="(page, pageIndex) in pages" :key="pageIndex">
<div class="flex flex-row flex-wrap">
<div
v-for="(item, index) in page"
:key="index"
class="relative flex flex-col items-center justify-center"
:style="{ width: columnWidth, height: `${rowHeight}px` }"
>
<!-- 图标 + 角标 -->
<div class="relative" :class="`h-${ICON_SIZE}px w-${ICON_SIZE}px`">
<!-- 右上角角标 -->
<span
v-if="item.badge?.show"
class="absolute right--10px top--10px z-1 h-20px rounded-10px p-x-6px text-center text-12px leading-20px"
:style="{ color: item.badge.textColor, backgroundColor: item.badge.bgColor }"
>
{{ item.badge.text }}
</span>
<el-image v-if="item.iconUrl" :src="item.iconUrl" class="h-full w-full" />
</div>
<!-- 标题 -->
<span
v-if="property.layout === 'iconText'"
class="text-14px"
:style="{
color: item.titleColor,
height: `${TITLE_HEIGHT}px`,
lineHeight: `${TITLE_HEIGHT}px`
}"
>
{{ item.title }}
</span>
</div>
</div>
</el-carousel-item>
</el-carousel>
</template>
<script setup lang="ts">
import { MenuSwiperProperty, MenuSwiperItemProperty } from './config'
/** 菜单导航 */
defineOptions({ name: 'MenuSwiper' })
const props = defineProps<{ property: MenuSwiperProperty }>()
// 标题的高度
const TITLE_HEIGHT = 20
// 图标的高度
const ICON_SIZE = 50
// 垂直间距:一行上下的间距
const SPACE_Y = 16
// 分页
const pages = ref<MenuSwiperItemProperty[][]>([])
// 轮播图高度
const carouselHeight = ref(0)
// 行高
const rowHeight = ref(0)
// 列宽
const columnWidth = ref('')
watch(
() => props.property,
() => {
// 计算列宽:每一列的百分比
columnWidth.value = `${100 * (1 / props.property.column)}%`
// 计算行高:图标 + 文字(仅显示图片时为0) + 垂直间距 * 2
rowHeight.value =
(props.property.layout === 'iconText' ? ICON_SIZE + TITLE_HEIGHT : ICON_SIZE) + SPACE_Y * 2
// 计算轮播的高度:行数 * 行高
carouselHeight.value = props.property.row * rowHeight.value
// 每页数量:行数 * 列数
const pageSize = props.property.row * props.property.column
// 清空分页
pages.value = []
// 每一页的菜单
let pageItems: MenuSwiperItemProperty[] = []
for (const item of props.property.list) {
// 本页满员,新建下一页
if (pageItems.length === pageSize) {
pageItems = []
}
// 增加一页
if (pageItems.length === 0) {
pages.value.push(pageItems)
}
// 本页增加一个
pageItems.push(item)
}
},
{ immediate: true, deep: true }
)
</script>
<style lang="scss">
// 重写指示器样式,与 APP 保持一致
:root {
.el-carousel__indicator {
padding-top: 0;
padding-bottom: 0;
.el-carousel__button {
--el-carousel-indicator-height: 6px;
--el-carousel-indicator-width: 6px;
--el-carousel-indicator-out-color: #ff6000;
border-radius: 6px;
}
}
.el-carousel__indicator.is-active {
.el-carousel__button {
--el-carousel-indicator-width: 12px;
}
}
}
</style>
@@ -0,0 +1,106 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<!-- 表单 -->
<el-form label-width="80px" :model="formData" class="m-t-8px">
<el-form-item label="布局" prop="layout">
<el-radio-group v-model="formData.layout">
<el-radio label="iconText">图标+文字</el-radio>
<el-radio label="icon">仅图标</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="行数" prop="row">
<el-radio-group v-model="formData.row">
<el-radio :label="1">1行</el-radio>
<el-radio :label="2">2行</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="列数" prop="column">
<el-radio-group v-model="formData.column">
<el-radio :label="3">3列</el-radio>
<el-radio :label="4">4列</el-radio>
<el-radio :label="5">5列</el-radio>
</el-radio-group>
</el-form-item>
<el-text tag="p"> 菜单设置 </el-text>
<el-text type="info" size="small"> 拖动左侧的小圆点可以调整顺序 </el-text>
<template v-if="formData.list.length">
<VueDraggable
class="m-t-8px"
:list="formData.list"
item-key="index"
handle=".drag-icon"
:forceFallback="true"
:animation="200"
>
<template #item="{ element, index }">
<div class="mb-4px flex flex-col gap-4px rounded bg-gray-100 p-8px">
<div class="flex flex-row justify-between">
<Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
<Icon icon="ep:delete" class="text-red-500" @click="handleDeleteMenu(index)" />
</div>
<el-form-item label="图标" prop="iconUrl">
<UploadImg v-model="element.iconUrl" height="80px" width="80px">
<template #tip> 建议尺寸:98 * 98 </template>
</UploadImg>
</el-form-item>
<el-form-item label="标题" prop="title">
<InputWithColor v-model="element.title" v-model:color="element.titleColor" />
</el-form-item>
<el-form-item label="链接" prop="url">
<el-input v-model="element.url" />
</el-form-item>
<el-form-item label="显示角标" prop="badge.show">
<el-switch v-model="element.badge.show" />
</el-form-item>
<template v-if="element.badge.show">
<el-form-item label="角标内容" prop="badge.text">
<InputWithColor
v-model="element.badge.text"
v-model:color="element.badge.textColor"
/>
</el-form-item>
<el-form-item label="背景颜色" prop="badge.bgColor">
<ColorInput v-model="element.badge.bgColor" />
</el-form-item>
</template>
</div>
</template>
</VueDraggable>
</template>
<el-form-item label-width="0">
<el-button @click="handleAddMenu" type="primary" plain class="m-t-8px w-full">
<Icon icon="ep:plus" class="mr-5px" /> 添加菜单
</el-button>
</el-form-item>
</el-form>
</ComponentContainerProperty>
</template>
<script setup lang="ts">
import VueDraggable from 'vuedraggable'
import { usePropertyForm } from '@/components/DiyEditor/util'
import {
EMPTY_MENU_SWIPER_ITEM_PROPERTY,
MenuSwiperProperty
} from '@/components/DiyEditor/components/mobile/MenuSwiper/config'
import { cloneDeep } from 'lodash-es'
/** 菜单导航属性面板 */
defineOptions({ name: 'MenuSwiperProperty' })
const props = defineProps<{ modelValue: MenuSwiperProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
/* 添加菜单 */
const handleAddMenu = () => {
formData.value.list.push(cloneDeep(EMPTY_MENU_SWIPER_ITEM_PROPERTY))
}
/* 删除菜单 */
const handleDeleteMenu = (index: number) => {
formData.value.list.splice(index, 1)
}
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,38 @@
import { DiyComponent } from '@/components/DiyEditor/util'
/** 顶部导航栏属性 */
export interface NavigationBarProperty {
// 页面标题
title: string
// 页面描述
description: string
// 顶部导航高度
navBarHeight: number
// 页面背景颜色
backgroundColor: string
// 页面背景图片
backgroundImage: string
// 样式类型:默认 | 沉浸式
styleType: 'default' | 'immersion'
// 常驻显示
alwaysShow: boolean
// 是否显示返回按钮
showGoBack: boolean
}
// 定义组件
export const component = {
id: 'NavigationBar',
name: '顶部导航栏',
icon: 'tabler:layout-navbar',
property: {
title: '页面标题',
description: '',
navBarHeight: 35,
backgroundColor: '#fff',
backgroundImage: '',
styleType: 'default',
alwaysShow: true,
showGoBack: true
}
} as DiyComponent<NavigationBarProperty>
@@ -0,0 +1,62 @@
<template>
<div
class="navigation-bar"
:style="{
height: `${property.navBarHeight}px`,
backgroundColor: property.backgroundColor,
backgroundImage: `url(${property.backgroundImage})`
}"
>
<!-- 左侧 -->
<div class="left">
<Icon icon="ep:arrow-left" v-show="property.showGoBack" />
</div>
<!-- 中间 -->
<div
class="center"
:style="{
height: `${property.navBarHeight}px`,
lineHeight: `${property.navBarHeight}px`
}"
>
{{ property.title }}
</div>
<!-- 右侧 -->
<div class="right"></div>
</div>
</template>
<script setup lang="ts">
import { NavigationBarProperty } from './config'
/** 页面顶部导航栏 */
defineOptions({ name: 'NavigationBar' })
defineProps<{ property: NavigationBarProperty }>()
</script>
<style lang="scss" scoped>
.navigation-bar {
display: flex;
height: 35px;
background: #fff;
justify-content: space-between;
align-items: center;
/* 左边 */
.left {
margin-left: 8px;
}
.center {
font-size: 14px;
line-height: 35px;
color: #333;
text-align: center;
flex: 1;
}
/* 右边 */
.right {
margin-right: 8px;
}
}
</style>
@@ -0,0 +1,63 @@
<template>
<el-form label-width="80px" :model="formData" :rules="rules">
<el-form-item label="页面标题" prop="title">
<el-input v-model="formData!.title" placeholder="页面标题" maxlength="25" show-word-limit />
</el-form-item>
<el-form-item label="页面描述" prop="description">
<el-input
type="textarea"
v-model="formData!.description"
placeholder="用户通过微信分享给朋友时,会自动显示页面描述"
/>
</el-form-item>
<el-form-item label="样式" prop="styleType">
<el-radio-group v-model="formData!.styleType">
<el-radio label="default">默认</el-radio>
<el-radio label="immersion">沉浸式</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="常驻显示" prop="alwaysShow" v-if="formData.styleType === 'immersion'">
<el-radio-group v-model="formData!.alwaysShow">
<el-radio :label="false">关闭</el-radio>
<el-radio :label="true">开启</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="高度" prop="navBarHeight">
<el-slider
v-model="formData!.navBarHeight"
:max="100"
:min="35"
show-input
input-size="small"
/>
</el-form-item>
<el-form-item label="返回按钮" prop="showGoBack">
<el-switch v-model="formData!.showGoBack" />
</el-form-item>
<el-form-item label="背景颜色" prop="backgroundColor">
<ColorInput v-model="formData!.backgroundColor" />
</el-form-item>
<el-form-item label="背景图片" prop="backgroundImage">
<UploadImg v-model="formData!.backgroundImage" :limit="1">
<template #tip>建议宽度 750px</template>
</UploadImg>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { NavigationBarProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
// 导航栏属性面板
defineOptions({ name: 'NavigationBarProperty' })
// 表单校验
const rules = {
name: [{ required: true, message: '请输入页面名称', trigger: 'blur' }]
}
const props = defineProps<{ modelValue: NavigationBarProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,39 @@
import { DiyComponent } from '@/components/DiyEditor/util'
/** 公告栏属性 */
export interface NoticeBarProperty {
// 图标地址
iconUrl: string
// 公告内容列表
contents: NoticeContentProperty[]
// 背景颜色
backgroundColor: string
// 文字颜色
textColor: string
}
/** 内容属性 */
export interface NoticeContentProperty {
// 内容文字
text: string
// 链接地址
url: string
}
// 定义组件
export const component = {
id: 'NoticeBar',
name: '公告栏',
icon: 'ep:bell',
property: {
iconUrl: 'http://mall.yudao.iocoder.cn/static/images/xinjian.png',
contents: [
{
text: '',
url: ''
}
],
backgroundColor: '#fff',
textColor: '#333'
}
} as DiyComponent<NoticeBarProperty>
@@ -0,0 +1,26 @@
<template>
<div
class="flex items-center p-y-4px text-12px"
:style="{ backgroundColor: property.backgroundColor, color: property.textColor }"
>
<el-image :src="property.iconUrl" class="h-18px" />
<el-divider direction="vertical" />
<el-carousel height="24px" direction="vertical" :autoplay="true" class="flex-1 p-r-8px">
<el-carousel-item v-for="(item, index) in property.contents" :key="index">
<div class="h-24px truncate leading-24px">{{ item.text }}</div>
</el-carousel-item>
</el-carousel>
<Icon icon="ep:arrow-right" />
</div>
</template>
<script setup lang="ts">
import { NoticeBarProperty } from './config'
/** 公告栏 */
defineOptions({ name: 'NoticeBar' })
defineProps<{ property: NoticeBarProperty }>()
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,77 @@
<template>
<el-form label-width="80px" :model="formData" :rules="rules">
<el-form-item label="公告图标" prop="iconUrl">
<UploadImg v-model="formData.iconUrl" height="48px">
<template #tip>建议尺寸:24 * 24</template>
</UploadImg>
</el-form-item>
<el-form-item label="背景颜色" prop="backgroundColor">
<ColorInput v-model="formData.backgroundColor" />
</el-form-item>
<el-form-item label="文字颜色" prop="文字颜色">
<ColorInput v-model="formData.textColor" />
</el-form-item>
<el-text tag="p"> 公告内容 </el-text>
<el-text type="info" size="small"> 拖动左上角的小圆点可以调整热词顺序 </el-text>
<template v-if="formData.contents.length">
<VueDraggable
:list="formData.contents"
item-key="index"
handle=".drag-icon"
:forceFallback="true"
:animation="200"
class="m-t-8px"
>
<template #item="{ element, index }">
<div class="mb-4px flex flex-row gap-4px rounded bg-gray-100 p-8px">
<div class="flex flex-col items-start justify-between">
<Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
<Icon
icon="ep:delete"
class="cursor-pointer text-red-5"
@click="handleDeleteContent(index)"
v-if="formData.contents.length > 1"
/>
</div>
<div class="w-full flex flex-col gap-8px">
<el-input v-model="element.text" placeholder="请输入公告" />
<el-input v-model="element.url" placeholder="请输入链接" />
</div>
</div>
</template>
</VueDraggable>
</template>
<el-form-item label-width="0">
<el-button @click="handleAddContent" type="primary" plain class="m-t-8px w-full">
添加内容
</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { NoticeBarProperty, NoticeContentProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import VueDraggable from 'vuedraggable'
// 通知栏属性面板
defineOptions({ name: 'NoticeBarProperty' })
// 表单校验
const rules = {
content: [{ required: true, message: '请输入公告', trigger: 'blur' }]
}
const props = defineProps<{ modelValue: NoticeBarProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
/* 添加公告 */
const handleAddContent = () => {
formData.value.contents.push({} as NoticeContentProperty)
}
/* 删除公告 */
const handleDeleteContent = (index: number) => {
formData.value.contents.splice(index, 1)
}
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,23 @@
import { DiyComponent } from '@/components/DiyEditor/util'
/** 页面设置属性 */
export interface PageConfigProperty {
// 页面描述
description: string
// 页面背景颜色
backgroundColor: string
// 页面背景图片
backgroundImage: string
}
// 定义页面组件
export const component = {
id: 'PageConfig',
name: '页面设置',
icon: 'ep:document',
property: {
description: '',
backgroundColor: '#f5f5f5',
backgroundImage: ''
}
} as DiyComponent<PageConfigProperty>
@@ -0,0 +1,34 @@
<template>
<el-form label-width="80px" :model="formData" :rules="rules">
<el-form-item label="页面描述" prop="description">
<el-input
type="textarea"
v-model="formData!.description"
placeholder="用户通过微信分享给朋友时,会自动显示页面描述"
/>
</el-form-item>
<el-form-item label="背景颜色" prop="backgroundColor">
<ColorInput v-model="formData!.backgroundColor" />
</el-form-item>
<el-form-item label="背景图片" prop="backgroundImage">
<UploadImg v-model="formData!.backgroundImage" :limit="1">
<template #tip>建议宽度 750px</template>
</UploadImg>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { PageConfigProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
// 导航栏属性面板
defineOptions({ name: 'PageConfigProperty' })
// 表单校验
const rules = {}
const props = defineProps<{ modelValue: PageConfigProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,97 @@
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 商品卡片属性 */
export interface ProductCardProperty {
// 布局类型:单列大图 | 单列小图 | 双列
layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'
// 商品字段
fields: {
// 商品名称
name: ProductCardFieldProperty
// 商品简介
introduction: ProductCardFieldProperty
// 商品价格
price: ProductCardFieldProperty
// 商品市场价
marketPrice: ProductCardFieldProperty
// 商品销量
salesCount: ProductCardFieldProperty
// 商品库存
stock: ProductCardFieldProperty
}
// 角标
badge: {
// 是否显示
show: boolean
// 角标图片
imgUrl: string
}
// 按钮
btnBuy: {
// 类型:文字 | 图片
type: 'text' | 'img'
// 文字
text: string
// 文字按钮:背景渐变起始颜色
bgBeginColor: string
// 文字按钮:背景渐变结束颜色
bgEndColor: string
// 图片按钮:图片地址
imgUrl: string
}
// 上圆角
borderRadiusTop: number
// 下圆角
borderRadiusBottom: number
// 间距
space: number
// 商品编号列表
spuIds: number[]
// 组件样式
style: ComponentStyle
}
// 商品字段
export interface ProductCardFieldProperty {
// 是否显示
show: boolean
// 颜色
color: string
}
// 定义组件
export const component = {
id: 'ProductCard',
name: '商品卡片',
icon: 'fluent:text-column-two-left-24-filled',
property: {
layoutType: 'oneColBigImg',
fields: {
name: { show: true, color: '#000' },
introduction: { show: true, color: '#999' },
price: { show: true, color: '#ff3000' },
marketPrice: { show: true, color: '#c4c4c4' },
salesCount: { show: true, color: '#c4c4c4' },
stock: { show: false, color: '#c4c4c4' }
},
badge: { show: false, imgUrl: '' },
btnBuy: {
type: 'text',
text: '立即购买',
// todo: @owen 根据主题色配置
bgBeginColor: '#FF6000',
bgEndColor: '#FE832A',
imgUrl: ''
},
borderRadiusTop: 8,
borderRadiusBottom: 8,
space: 8,
spuIds: [],
style: {
bgType: 'color',
bgColor: '',
marginLeft: 8,
marginRight: 8,
marginBottom: 8
} as ComponentStyle
}
} as DiyComponent<ProductCardProperty>
@@ -0,0 +1,165 @@
<template>
<div :class="`box-content min-h-30px w-full flex flex-row flex-wrap`" ref="containerRef">
<div
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
:style="{
...calculateSpace(index),
...calculateWidth(),
borderTopLeftRadius: `${property.borderRadiusTop}px`,
borderTopRightRadius: `${property.borderRadiusTop}px`,
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
borderBottomRightRadius: `${property.borderRadiusBottom}px`
}"
v-for="(spu, index) in spuList"
:key="index"
>
<!-- 角标 -->
<div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center">
<el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
</div>
<!-- 商品封面图 -->
<div
:class="[
'h-140px',
{
'w-full': property.layoutType !== 'oneColSmallImg',
'w-140px': property.layoutType === 'oneColSmallImg'
}
]"
>
<el-image fit="cover" class="h-full w-full" :src="spu.picUrl" />
</div>
<div
:class="[
' flex flex-col gap-8px p-8px box-border',
{
'w-full': property.layoutType !== 'oneColSmallImg',
'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg'
}
]"
>
<!-- 商品名称 -->
<div
v-if="property.fields.name.show"
:class="[
'text-14px ',
{
truncate: property.layoutType !== 'oneColSmallImg',
'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg'
}
]"
:style="{ color: property.fields.name.color }"
>
{{ spu.name }}
</div>
<!-- 商品简介 -->
<div
v-if="property.fields.introduction.show"
class="truncate text-12px"
:style="{ color: property.fields.introduction.color }"
>
{{ spu.introduction }}
</div>
<div>
<!-- 价格 -->
<span
v-if="property.fields.price.show"
class="text-16px"
:style="{ color: property.fields.price.color }"
>
¥{{ spu.price }}
</span>
<!-- 市场价 -->
<span
v-if="property.fields.marketPrice.show && spu.marketPrice"
class="ml-4px text-10px line-through"
:style="{ color: property.fields.marketPrice.color }"
>¥{{ spu.marketPrice }}</span
>
</div>
<div class="text-12px">
<!-- 销量 -->
<span
v-if="property.fields.salesCount.show"
:style="{ color: property.fields.salesCount.color }"
>
已售{{ (spu.salesCount || 0) + (spu.virtualSalesCount || 0) }}件
</span>
<!-- 库存 -->
<span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }">
库存{{ spu.stock || 0 }}
</span>
</div>
</div>
<!-- 购买按钮 -->
<div class="absolute bottom-8px right-8px">
<!-- 文字按钮 -->
<span
v-if="property.btnBuy.type === 'text'"
class="rounded-full p-x-12px p-y-4px text-12px text-white"
:style="{
background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`
}"
>
{{ property.btnBuy.text }}
</span>
<!-- 图片按钮 -->
<el-image
v-else
class="h-28px w-28px rounded-full"
fit="cover"
:src="property.btnBuy.imgUrl"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ProductCardProperty } from './config'
import * as ProductSpuApi from '@/api/mall/product/spu'
/** 商品卡片 */
defineOptions({ name: 'ProductCard' })
// 定义属性
const props = defineProps<{ property: ProductCardProperty }>()
// 商品列表
const spuList = ref<ProductSpuApi.Spu[]>([])
watch(
() => props.property.spuIds,
async () => {
spuList.value = await ProductSpuApi.getSpuDetailList(props.property.spuIds)
},
{
immediate: true,
deep: true
}
)
/**
* 计算商品的间距
* @param index 商品索引
*/
const calculateSpace = (index: number) => {
// 商品的列数
const columns = props.property.layoutType === 'twoCol' ? 2 : 1
// 第一列没有左边距
const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px'
// 第一行没有上边距
const marginTop = index < columns ? '0' : props.property.space + 'px'
return { marginLeft, marginTop }
}
// 容器
const containerRef = ref()
// 计算商品的宽度
const calculateWidth = () => {
let width = '100%'
// 双列时每列的宽度为:(总宽度 - 间距)/ 2
if (props.property.layoutType === 'twoCol') {
width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px`
}
return { width }
}
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,149 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<el-form label-width="80px" :model="formData">
<el-card header="商品列表" class="property-group" shadow="never">
<SpuShowcase v-model="formData.spuIds" />
</el-card>
<el-card header="商品样式" class="property-group" shadow="never">
<el-form-item label="布局" prop="type">
<el-radio-group v-model="formData.layoutType">
<el-tooltip class="item" content="单列大图" placement="bottom">
<el-radio-button label="oneColBigImg">
<Icon icon="fluent:text-column-one-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="单列小图" placement="bottom">
<el-radio-button label="oneColSmallImg">
<Icon icon="fluent:text-column-two-left-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="双列" placement="bottom">
<el-radio-button label="twoCol">
<Icon icon="fluent:text-column-two-24-filled" />
</el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="商品名称" prop="fields.name.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.name.color" />
<el-checkbox v-model="formData.fields.name.show" />
</div>
</el-form-item>
<el-form-item label="商品简介" prop="fields.introduction.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.introduction.color" />
<el-checkbox v-model="formData.fields.introduction.show" />
</div>
</el-form-item>
<el-form-item label="商品价格" prop="fields.price.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.price.color" />
<el-checkbox v-model="formData.fields.price.show" />
</div>
</el-form-item>
<el-form-item label="市场价" prop="fields.marketPrice.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.marketPrice.color" />
<el-checkbox v-model="formData.fields.marketPrice.show" />
</div>
</el-form-item>
<el-form-item label="商品销量" prop="fields.salesCount.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.salesCount.color" />
<el-checkbox v-model="formData.fields.salesCount.show" />
</div>
</el-form-item>
<el-form-item label="商品库存" prop="fields.stock.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.stock.color" />
<el-checkbox v-model="formData.fields.stock.show" />
</div>
</el-form-item>
</el-card>
<el-card header="角标" class="property-group" shadow="never">
<el-form-item label="角标" prop="badge.show">
<el-switch v-model="formData.badge.show" />
</el-form-item>
<el-form-item label="角标" prop="badge.imgUrl" v-if="formData.badge.show">
<UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
<template #tip> 建议尺寸:36 * 22 </template>
</UploadImg>
</el-form-item>
</el-card>
<el-card header="按钮" class="property-group" shadow="never">
<el-form-item label="按钮类型" prop="btnBuy.type">
<el-radio-group v-model="formData.btnBuy.type">
<el-radio-button label="text">文字</el-radio-button>
<el-radio-button label="img">图片</el-radio-button>
</el-radio-group>
</el-form-item>
<template v-if="formData.btnBuy.type === 'text'">
<el-form-item label="按钮文字" prop="btnBuy.text">
<el-input v-model="formData.btnBuy.text" />
</el-form-item>
<el-form-item label="左侧背景" prop="btnBuy.bgBeginColor">
<ColorInput v-model="formData.btnBuy.bgBeginColor" />
</el-form-item>
<el-form-item label="右侧背景" prop="btnBuy.bgEndColor">
<ColorInput v-model="formData.btnBuy.bgEndColor" />
</el-form-item>
</template>
<template v-else>
<el-form-item label="图片" prop="btnBuy.imgUrl">
<UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px">
<template #tip> 建议尺寸:56 * 56 </template>
</UploadImg>
</el-form-item>
</template>
</el-card>
<el-card header="商品样式" class="property-group" shadow="never">
<el-form-item label="上圆角" prop="borderRadiusTop">
<el-slider
v-model="formData.borderRadiusTop"
:max="100"
:min="0"
show-input
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
<el-form-item label="下圆角" prop="borderRadiusBottom">
<el-slider
v-model="formData.borderRadiusBottom"
:max="100"
:min="0"
show-input
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
<el-form-item label="间隔" prop="space">
<el-slider
v-model="formData.space"
:max="100"
:min="0"
show-input
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
</el-card>
</el-form>
</ComponentContainerProperty>
</template>
<script setup lang="ts">
import { ProductCardProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue'
// 商品卡片属性面板
defineOptions({ name: 'ProductCardProperty' })
const props = defineProps<{ modelValue: ProductCardProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,64 @@
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 商品卡片属性 */
export interface ProductListProperty {
// 布局类型:双列 | 三列 | 水平滑动
layoutType: 'twoCol' | 'threeCol' | 'horizSwiper'
// 商品字段
fields: {
// 商品名称
name: ProductListFieldProperty
// 商品价格
price: ProductListFieldProperty
}
// 角标
badge: {
// 是否显示
show: boolean
// 角标图片
imgUrl: string
}
// 上圆角
borderRadiusTop: number
// 下圆角
borderRadiusBottom: number
// 间距
space: number
// 商品编号列表
spuIds: number[]
// 组件样式
style: ComponentStyle
}
// 商品字段
export interface ProductListFieldProperty {
// 是否显示
show: boolean
// 颜色
color: string
}
// 定义组件
export const component = {
id: 'ProductList',
name: '商品栏',
icon: 'fluent:text-column-two-24-filled',
property: {
layoutType: 'twoCol',
fields: {
name: { show: true, color: '#000' },
price: { show: true, color: '#ff3000' }
},
badge: { show: false, imgUrl: '' },
borderRadiusTop: 8,
borderRadiusBottom: 8,
space: 8,
spuIds: [],
style: {
bgType: 'color',
bgColor: '',
marginLeft: 8,
marginRight: 8,
marginBottom: 8
} as ComponentStyle
}
} as DiyComponent<ProductListProperty>
@@ -0,0 +1,131 @@
<template>
<el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef">
<!-- 商品网格 -->
<div
class="grid overflow-x-auto"
:style="{
gridGap: `${property.space}px`,
gridTemplateColumns,
width: scrollbarWidth
}"
>
<!-- 商品 -->
<div
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
:style="{
borderTopLeftRadius: `${property.borderRadiusTop}px`,
borderTopRightRadius: `${property.borderRadiusTop}px`,
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
borderBottomRightRadius: `${property.borderRadiusBottom}px`
}"
v-for="(spu, index) in spuList"
:key="index"
>
<!-- 角标 -->
<div
v-if="property.badge.show"
class="absolute left-0 top-0 z-1 items-center justify-center"
>
<el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
</div>
<!-- 商品封面图 -->
<el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" />
<div
:class="[
'flex flex-col gap-8px p-8px box-border',
{
'w-[calc(100%-64px)]': columns === 2,
'w-full': columns === 3
}
]"
>
<!-- 商品名称 -->
<div
v-if="property.fields.name.show"
class="truncate text-12px"
:style="{ color: property.fields.name.color }"
>
{{ spu.name }}
</div>
<div>
<!-- 商品价格 -->
<span
v-if="property.fields.price.show"
class="text-12px"
:style="{ color: property.fields.price.color }"
>
¥{{ spu.price }}
</span>
</div>
</div>
</div>
</div>
</el-scrollbar>
</template>
<script setup lang="ts">
import { ProductListProperty } from './config'
import * as ProductSpuApi from '@/api/mall/product/spu'
/** 商品卡片 */
defineOptions({ name: 'ProductList' })
// 定义属性
const props = defineProps<{ property: ProductListProperty }>()
// 商品列表
const spuList = ref<ProductSpuApi.Spu[]>([])
watch(
() => props.property.spuIds,
async () => {
spuList.value = await ProductSpuApi.getSpuDetailList(props.property.spuIds)
},
{
immediate: true,
deep: true
}
)
// 手机宽度
const phoneWidth = ref(375)
// 容器
const containerRef = ref()
// 商品的列数
const columns = ref(2)
// 滚动条宽度
const scrollbarWidth = ref('100%')
// 商品图大小
const imageSize = ref('0')
// 商品网络列数
const gridTemplateColumns = ref('')
// 计算布局参数
watch(
() => [props.property, phoneWidth, spuList.value.length],
() => {
// 计算列数
columns.value = props.property.layoutType === 'twoCol' ? 2 : 3
// 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数
const productWidth =
(phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value
// 商品图布局:2列时,左右布局 3列时,上下布局
imageSize.value = columns.value === 2 ? '64px' : `${productWidth}px`
// 根据布局类型,计算行数、列数
if (props.property.layoutType === 'horizSwiper') {
// 单行显示
gridTemplateColumns.value = `repeat(auto-fill, ${productWidth}px)`
// 显示滚动条
scrollbarWidth.value = `${
productWidth * spuList.value.length + props.property.space * (spuList.value.length - 1)
}px`
} else {
// 指定列数
gridTemplateColumns.value = `repeat(${columns.value}, auto)`
// 不滚动
scrollbarWidth.value = '100%'
}
},
{ immediate: true, deep: true }
)
onMounted(() => {
// 提取手机宽度
phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375
})
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,99 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<el-form label-width="80px" :model="formData">
<el-card header="商品列表" class="property-group" shadow="never">
<SpuShowcase v-model="formData.spuIds" />
</el-card>
<el-card header="商品样式" class="property-group" shadow="never">
<el-form-item label="布局" prop="type">
<el-radio-group v-model="formData.layoutType">
<el-tooltip class="item" content="双列" placement="bottom">
<el-radio-button label="twoCol">
<Icon icon="fluent:text-column-two-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="三列" placement="bottom">
<el-radio-button label="threeCol">
<Icon icon="fluent:text-column-three-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="水平滑动" placement="bottom">
<el-radio-button label="horizSwiper">
<Icon icon="system-uicons:carousel" />
</el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="商品名称" prop="fields.name.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.name.color" />
<el-checkbox v-model="formData.fields.name.show" />
</div>
</el-form-item>
<el-form-item label="商品价格" prop="fields.price.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.price.color" />
<el-checkbox v-model="formData.fields.price.show" />
</div>
</el-form-item>
</el-card>
<el-card header="角标" class="property-group" shadow="never">
<el-form-item label="角标" prop="badge.show">
<el-switch v-model="formData.badge.show" />
</el-form-item>
<el-form-item label="角标" prop="badge.imgUrl" v-if="formData.badge.show">
<UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
<template #tip> 建议尺寸:36 * 22 </template>
</UploadImg>
</el-form-item>
</el-card>
<el-card header="商品样式" class="property-group" shadow="never">
<el-form-item label="上圆角" prop="borderRadiusTop">
<el-slider
v-model="formData.borderRadiusTop"
:max="100"
:min="0"
show-input
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
<el-form-item label="下圆角" prop="borderRadiusBottom">
<el-slider
v-model="formData.borderRadiusBottom"
:max="100"
:min="0"
show-input
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
<el-form-item label="间隔" prop="space">
<el-slider
v-model="formData.space"
:max="100"
:min="0"
show-input
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
</el-card>
</el-form>
</ComponentContainerProperty>
</template>
<script setup lang="ts">
import { ProductListProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue'
// 商品卡片属性面板
defineOptions({ name: 'ProductListProperty' })
const props = defineProps<{ modelValue: ProductListProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,43 @@
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 搜索框属性 */
export interface SearchProperty {
height: number // 搜索栏高度
showScan: boolean // 显示扫一扫
borderRadius: number // 框体样式
placeholder: string // 占位文字
placeholderPosition: PlaceholderPosition // 占位文字位置
backgroundColor: string // 框体颜色
textColor: string // 字体颜色
hotKeywords: string[] // 热词
style: ComponentStyle
}
// 文字位置
export type PlaceholderPosition = 'left' | 'center'
// 定义组件
export const component = {
id: 'SearchBar',
name: '搜索框',
icon: 'ep:search',
property: {
height: 28,
showScan: false,
borderRadius: 0,
placeholder: '搜索商品',
placeholderPosition: 'left',
backgroundColor: 'rgb(238, 238, 238)',
textColor: 'rgb(150, 151, 153)',
hotKeywords: [],
style: {
bgType: 'color',
bgColor: '#fff',
marginBottom: 8,
paddingTop: 8,
paddingRight: 8,
paddingBottom: 8,
paddingLeft: 8
} as ComponentStyle
}
} as DiyComponent<SearchProperty>
@@ -0,0 +1,75 @@
<template>
<div
class="search-bar"
:style="{
color: property.textColor
}"
>
<!-- 搜索框 -->
<div
class="inner"
:style="{
height: `${property.height}px`,
background: property.backgroundColor,
borderRadius: `${property.borderRadius}px`
}"
>
<div
class="placeholder"
:style="{
justifyContent: property.placeholderPosition
}"
>
<Icon icon="ep:search" />
<span>{{ property.placeholder || '搜索商品' }}</span>
</div>
<div class="right">
<!-- 搜索热词 -->
<span v-for="(keyword, index) in property.hotKeywords" :key="index">{{ keyword }}</span>
<!-- 扫一扫 -->
<Icon icon="ant-design:scan-outlined" v-show="property.showScan" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { SearchProperty } from './config'
/** 搜索框 */
defineOptions({ name: 'SearchBar' })
defineProps<{ property: SearchProperty }>()
</script>
<style scoped lang="scss">
.search-bar {
/* 搜索框 */
.inner {
position: relative;
display: flex;
min-height: 28px;
font-size: 14px;
align-items: center;
.placeholder {
display: flex;
width: 100%;
padding: 0 8px;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
align-items: center;
gap: 2px;
}
.right {
position: absolute;
right: 8px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
}
}
</style>
@@ -0,0 +1,99 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<el-text tag="p"> 搜索热词 </el-text>
<el-text type="info" size="small"> 拖动左侧的小圆点可以调整热词顺序 </el-text>
<!-- 表单 -->
<el-form label-width="80px" :model="formData" class="m-t-8px">
<div v-if="formData.hotKeywords.length">
<VueDraggable
:list="formData.hotKeywords"
item-key="index"
handle=".drag-icon"
:forceFallback="true"
:animation="200"
>
<template #item="{ index }">
<div class="mb-4px flex flex-row items-center gap-4px rounded bg-gray-100 p-8px">
<Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
<el-input v-model="formData.hotKeywords[index]" placeholder="请输入热词" />
<Icon icon="ep:delete" class="text-red-500" @click="deleteHotWord(index)" />
</div>
</template>
</VueDraggable>
</div>
<el-form-item label-width="0">
<el-button @click="handleAddHotWord" type="primary" plain class="m-t-8px w-full">
添加热词
</el-button>
</el-form-item>
<el-form-item label="框体样式">
<el-radio-group v-model="formData!.borderRadius">
<el-tooltip content="方形" placement="top">
<el-radio-button :label="0">
<Icon icon="tabler:input-search" />
</el-radio-button>
</el-tooltip>
<el-tooltip content="圆形" placement="top">
<el-radio-button :label="10">
<Icon icon="iconoir:input-search" />
</el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="提示文字" prop="placeholder">
<el-input v-model="formData.placeholder" />
</el-form-item>
<el-form-item label="文本位置" prop="placeholderPosition">
<el-radio-group v-model="formData!.placeholderPosition">
<el-tooltip content="居左" placement="top">
<el-radio-button label="left">
<Icon icon="ant-design:align-left-outlined" />
</el-radio-button>
</el-tooltip>
<el-tooltip content="居中" placement="top">
<el-radio-button label="center">
<Icon icon="ant-design:align-center-outlined" />
</el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="扫一扫" prop="showScan">
<el-switch v-model="formData!.showScan" />
</el-form-item>
<el-form-item label="框体高度" prop="height">
<el-slider v-model="formData!.height" :max="50" :min="28" show-input input-size="small" />
</el-form-item>
<el-form-item label="框体颜色" prop="backgroundColor">
<ColorInput v-model="formData.backgroundColor" />
</el-form-item>
<el-form-item class="lef" label="文本颜色" prop="textColor">
<ColorInput v-model="formData.textColor" />
</el-form-item>
</el-form>
</ComponentContainerProperty>
</template>
<script setup lang="ts">
import VueDraggable from 'vuedraggable'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { SearchProperty } from '@/components/DiyEditor/components/mobile/SearchBar/config'
/** 搜索框属性面板 */
defineOptions({ name: 'SearchProperty' })
const props = defineProps<{ modelValue: SearchProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
/* 添加热词 */
const handleAddHotWord = () => {
formData.value.hotKeywords.push('')
}
/* 删除热词 */
const deleteHotWord = (index: number) => {
formData.value.hotKeywords.splice(index, 1)
}
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,97 @@
import { DiyComponent } from '@/components/DiyEditor/util'
/** 底部导航菜单属性 */
export interface TabBarProperty {
// 选项列表
items: TabBarItemProperty[]
// 主题
theme: string
// 样式
style: TabBarStyle
}
// 选项属性
export interface TabBarItemProperty {
// 标签文字
text: string
// 链接
url: string
// 默认图标链接
iconUrl: string
// 选中的图标链接
activeIconUrl: string
}
// 样式
export interface TabBarStyle {
// 背景类型
bgType: 'color' | 'img'
// 背景颜色
bgColor: string
// 图片链接
bgImg: string
// 默认颜色
color: string
// 选中的颜色
activeColor: string
}
// 定义组件
export const component = {
id: 'TabBar',
name: '底部导航',
icon: 'fluent:table-bottom-row-16-filled',
property: {
theme: 'red',
style: {
bgType: 'color',
bgColor: '#fff',
color: '#282828',
activeColor: '#fc4141'
},
items: [
{
text: '首页',
url: '/pages/index/index',
iconUrl: 'http://mall.yudao.iocoder.cn/static/images/1-001.png',
activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/1-002.png'
},
{
text: '分类',
url: '/pages/index/category?id=3',
iconUrl: 'http://mall.yudao.iocoder.cn/static/images/2-001.png',
activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/2-002.png'
},
{
text: '购物车',
url: '/pages/index/cart',
iconUrl: 'http://mall.yudao.iocoder.cn/static/images/3-001.png',
activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/3-002.png'
},
{
text: '我的',
url: '/pages/index/user',
iconUrl: 'http://mall.yudao.iocoder.cn/static/images/4-001.png',
activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/4-002.png'
}
]
}
} as DiyComponent<TabBarProperty>
export const THEME_LIST = [
{ id: 'red', name: '中国红', icon: 'icon-park-twotone:theme', color: '#d10019' },
{ id: 'orange', name: '桔橙', icon: 'icon-park-twotone:theme', color: '#f37b1d' },
{ id: 'gold', name: '明黄', icon: 'icon-park-twotone:theme', color: '#fbbd08' },
{ id: 'green', name: '橄榄绿', icon: 'icon-park-twotone:theme', color: '#8dc63f' },
{ id: 'cyan', name: '天青', icon: 'icon-park-twotone:theme', color: '#1cbbb4' },
{ id: 'blue', name: '海蓝', icon: 'icon-park-twotone:theme', color: '#0081ff' },
{ id: 'purple', name: '姹紫', icon: 'icon-park-twotone:theme', color: '#6739b6' },
{ id: 'brightRed', name: '嫣红', icon: 'icon-park-twotone:theme', color: '#e54d42' },
{ id: 'forestGreen', name: '森绿', icon: 'icon-park-twotone:theme', color: '#39b54a' },
{ id: 'mauve', name: '木槿', icon: 'icon-park-twotone:theme', color: '#9c26b0' },
{ id: 'pink', name: '桃粉', icon: 'icon-park-twotone:theme', color: '#e03997' },
{ id: 'brown', name: '棕褐', icon: 'icon-park-twotone:theme', color: '#a5673f' },
{ id: 'grey', name: '玄灰', icon: 'icon-park-twotone:theme', color: '#8799a3' },
{ id: 'gray', name: '草灰', icon: 'icon-park-twotone:theme', color: '#aaaaaa' },
{ id: 'black', name: '墨黑', icon: 'icon-park-twotone:theme', color: '#333333' }
]
@@ -0,0 +1,59 @@
<template>
<div class="tab-bar">
<div
class="tab-bar-bg"
:style="{
background:
property.style.bgType === 'color'
? property.style.bgColor
: `url(${property.style.bgImg})`,
backgroundSize: '100% 100%',
backgroundRepeat: 'no-repeat'
}"
>
<div v-for="(item, index) in property.items" :key="index" class="tab-bar-item">
<img :src="index === 0 ? item.activeIconUrl : item.iconUrl" alt="" />
<span :style="{ color: index === 0 ? property.style.activeColor : property.style.color }">
{{ item.text }}
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { TabBarProperty } from './config'
/** 页面底部导航栏 */
defineOptions({ name: 'TabBar' })
defineProps<{ property: TabBarProperty }>()
</script>
<style lang="scss" scoped>
.tab-bar {
z-index: 2;
width: 100%;
.tab-bar-bg {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
padding: 8px 0;
.tab-bar-item {
display: flex;
width: 100%;
font-size: 12px;
flex-direction: column;
align-items: center;
justify-content: center;
img {
width: 26px;
height: 26px;
border-radius: 4px;
}
}
}
}
</style>
@@ -0,0 +1,145 @@
<template>
<div class="tab-bar">
<!-- 表单 -->
<el-form :model="formData" label-width="80px">
<el-form-item label="主题" prop="theme">
<el-select v-model="formData!.theme" @change="handleThemeChange">
<el-option
v-for="(theme, index) in THEME_LIST"
:key="index"
:label="theme.name"
:value="theme.id"
>
<template #default>
<div class="flex items-center justify-between">
<Icon :icon="theme.icon" :color="theme.color" />
<span>{{ theme.name }}</span>
</div>
</template>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="默认颜色">
<ColorInput v-model="formData!.style.color" />
</el-form-item>
<el-form-item label="选中颜色">
<ColorInput v-model="formData!.style.activeColor" />
</el-form-item>
<el-form-item label="导航背景">
<el-radio-group v-model="formData!.style.bgType">
<el-radio-button label="color">纯色</el-radio-button>
<el-radio-button label="img">图片</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="选择颜色" v-if="formData!.style.bgType === 'color'">
<ColorInput v-model="formData!.style.bgColor" />
</el-form-item>
<el-form-item label="选择图片" v-if="formData!.style.bgType === 'img'">
<UploadImg v-model="formData!.style.bgImg" width="100%" height="50px" class="min-w-200px">
<template #tip> 建议尺寸 375 * 50 </template>
</UploadImg>
</el-form-item>
<el-text tag="p">图标设置</el-text>
<el-text type="info" size="small"> 拖动左上角的小圆点可对其排序, 图标建议尺寸 44*44 </el-text>
<draggable
:list="formData!.items"
item-key="index"
:forceFallback="true"
:animation="200"
handle=".drag-icon"
class="m-t-8px"
>
<template #item="{ element, index }">
<div class="mb-4px flex flex-row gap-4px rounded bg-gray-100 p-8px">
<div class="flex flex-col items-start justify-between">
<Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
<Icon
icon="ep:delete"
class="cursor-pointer text-red-5"
@click="handleDeleteItem(index)"
v-if="formData.items.length > 1"
/>
</div>
<div class="w-full flex flex-col">
<div class="m-b-8px flex items-center justify-around">
<div class="flex flex-col items-center justify-between">
<UploadImg
v-model="element.iconUrl"
width="40px"
height="40px"
:show-delete="false"
:show-btn-text="false"
/>
<el-text size="small">默认图片</el-text>
</div>
<div>
<UploadImg
v-model="element.activeIconUrl"
width="40px"
height="40px"
:show-delete="false"
:show-btn-text="false"
/>
<el-text>选中图片</el-text>
</div>
</div>
<el-form-item prop="text" label-width="0" class="m-b-8px!">
<el-input v-model="element.text" placeholder="请输入文字" />
</el-form-item>
<el-form-item prop="url" label-width="0" class="m-b-0!">
<el-input v-model="element.url" placeholder="请选择链接" />
</el-form-item>
</div>
</div>
</template>
</draggable>
<el-form-item label-width="0">
<!-- 添加导航按钮 -->
<el-tooltip content="最多添加5个">
<el-button
@click="handleAddItem"
class="m-b-16px w-full"
type="primary"
plain
:disabled="formData!.items.length >= 5"
>
添加导航
</el-button>
</el-tooltip>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import draggable from 'vuedraggable' //拖拽组件
import { TabBarItemProperty, TabBarProperty, THEME_LIST } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
// 底部导航栏
defineOptions({ name: 'TabBarProperty' })
const props = defineProps<{ modelValue: TabBarProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
/** 添加导航项 */
const handleAddItem = () => {
formData?.value?.items?.push({} as TabBarItemProperty)
}
/** 删除导航项 */
const handleDeleteItem = (index: number) => {
formData?.value?.items?.splice(index, 1)
}
// 要的主题
const handleThemeChange = () => {
const theme = THEME_LIST.find((theme) => theme.id === formData.value.theme)
if (theme?.color) {
formData.value.style.activeColor = theme.color
}
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,65 @@
import { DiyComponent } from '@/components/DiyEditor/util'
/** 标题栏属性 */
export interface TitleBarProperty {
// 主标题
title: string
// 副标题
description: string
// 标题大小
titleSize: number
// 描述大小
descriptionSize: number
// 标题粗细
titleWeight: number
// 显示位置
position: 'left' | 'center'
// 描述粗细
descriptionWeight: number
// 标题颜色
titleColor: string
// 描述颜色
descriptionColor: string
// 背景颜色
backgroundColor: string
// 底部分割线
showBottomBorder: false
// 查看更多
more: {
// 是否显示查看更多
show: false
// 样式选择
type: 'text' | 'icon' | 'all'
// 自定义文字
text: string
// 链接
url: string
}
}
// 定义组件
export const component = {
id: 'TitleBar',
name: '标题栏',
icon: 'material-symbols:line-start',
property: {
title: '主标题',
description: '副标题',
titleSize: 16,
descriptionSize: 12,
titleWeight: 400,
position: 'left',
descriptionWeight: 200,
titleColor: 'rgba(50, 50, 51, 10)',
descriptionColor: 'rgba(150, 151, 153, 10)',
backgroundColor: 'rgba(255, 255, 255, 10)',
showBottomBorder: false,
more: {
//查看更多
show: false,
type: 'icon',
text: '查看更多',
url: ''
}
}
} as DiyComponent<TitleBarProperty>
@@ -0,0 +1,80 @@
<template>
<div
class="title-bar"
:style="{
background: property.backgroundColor,
borderBottom: property.showBottomBorder ? '1px solid #F9F9F9' : '1px solid #fff'
}"
>
<div>
<!-- 标题 -->
<div
:style="{
fontSize: `${property.titleSize}px`,
fontWeight: property.titleWeight,
color: property.titleColor,
textAlign: property.position
}"
v-if="property.title"
>
{{ property.title }}
</div>
<!-- 副标题 -->
<div
:style="{
fontSize: `${property.descriptionSize}px`,
fontWeight: property.descriptionWeight,
color: property.descriptionColor,
textAlign: property.position
}"
class="m-t-8px"
v-if="property.description"
>
{{ property.description }}
</div>
</div>
<!-- 更多 -->
<div
class="more"
v-show="property.more.show"
:style="{
color: property.more.type === 'text' ? '#38f' : ''
}"
>
{{ property.more.type === 'icon' ? '' : property.more.text }}
<Icon icon="ep:arrow-right" v-if="property.more.type !== 'text'" />
</div>
</div>
</template>
<script setup lang="ts">
import { TitleBarProperty } from './config'
/** 标题栏 */
defineOptions({ name: 'TitleBar' })
defineProps<{ property: TitleBarProperty }>()
</script>
<style scoped lang="scss">
.title-bar {
position: relative;
width: 100%;
min-height: 20px;
padding: 8px 16px;
border: 2px solid #fff;
box-sizing: border-box;
/* 更多 */
.more {
position: absolute;
top: 0;
right: 8px;
bottom: 0;
display: flex;
margin: auto;
font-size: 10px;
color: #969799;
align-items: center;
justify-content: center;
}
}
</style>
@@ -0,0 +1,115 @@
<template>
<section class="title-bar">
<el-form label-width="85px" :model="formData" :rules="rules">
<el-form-item label="主标题" prop="title">
<el-input
v-model="formData.title"
placeholder="请输入主标题"
show-word-limit
maxlength="20"
/>
</el-form-item>
<el-form-item label="副标题" prop="description">
<el-input
type="textarea"
v-model="formData.description"
placeholder="请输入副标题"
maxlength="50"
show-word-limit
/>
</el-form-item>
<el-form-item label="显示位置" prop="position">
<el-radio-group v-model="formData!.position">
<el-tooltip content="居左" placement="top">
<el-radio-button label="left">
<Icon icon="ant-design:align-left-outlined" />
</el-radio-button>
</el-tooltip>
<el-tooltip content="居中" placement="top">
<el-radio-button label="center">
<Icon icon="ant-design:align-center-outlined" />
</el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="标题大小" prop="titleSize">
<el-slider v-model="formData.titleSize" :max="60" :min="10" show-input input-size="small" />
</el-form-item>
<el-form-item label="副标题大小" prop="descriptionSize">
<el-slider
v-model="formData.descriptionSize"
:max="60"
:min="10"
show-input
input-size="small"
/>
</el-form-item>
<el-form-item label="标题粗细" prop="titleWeight">
<el-slider
v-model="formData.titleWeight"
:min="100"
:max="900"
:step="100"
show-input
input-size="small"
/>
</el-form-item>
<el-form-item label="副标题粗细" prop="descriptionWeight">
<el-slider
v-model="formData.descriptionWeight"
:min="100"
:max="900"
:step="100"
show-input
input-size="small"
/>
</el-form-item>
<el-form-item label="标题颜色" prop="titleColor">
<ColorInput v-model="formData.titleColor" />
</el-form-item>
<el-form-item label="副标题颜色" prop="descriptionColor">
<ColorInput v-model="formData.descriptionColor" />
</el-form-item>
<el-form-item label="背景颜色" prop="backgroundColor">
<ColorInput v-model="formData.backgroundColor" />
</el-form-item>
<el-form-item label="底部分割线" prop="showBottomBorder">
<el-switch v-model="formData!.showBottomBorder" />
</el-form-item>
<el-form-item label="查看更多" prop="more.show">
<el-checkbox v-model="formData.more.show" />
</el-form-item>
<!-- 更多样式选择 -->
<template v-if="formData.more.show">
<el-form-item label="样式" prop="more.type">
<el-radio-group v-model="formData.more.type">
<el-radio label="text">文字</el-radio>
<el-radio label="icon">图标</el-radio>
<el-radio label="all">文字+图标</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="更多文字" prop="more.text" v-show="formData.more.type !== 'icon'">
<el-input v-model="formData.more.text" />
</el-form-item>
<el-form-item label="跳转链接" prop="more.url">
<el-input v-model="formData.more.url" placeholder="请输入跳转链接" />
</el-form-item>
</template>
</el-form>
</section>
</template>
<script setup lang="ts">
import { TitleBarProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
// 导航栏属性面板
defineOptions({ name: 'TitleBarProperty' })
const props = defineProps<{ modelValue: TitleBarProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
// 表单校验
const rules = {}
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,37 @@
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 视频播放属性 */
export interface VideoPlayerProperty {
// 视频链接
videoUrl: string
// 封面链接
posterUrl: string
// 是否自动播放
autoplay: boolean
// 组件样式
style: VideoPlayerStyle
}
// 视频播放样式
export interface VideoPlayerStyle extends ComponentStyle {
// 视频高度
height: number
}
// 定义组件
export const component = {
id: 'VideoPlayer',
name: '视频播放',
icon: 'ep:video-play',
property: {
videoUrl: '',
posterUrl: '',
autoplay: false,
style: {
bgType: 'color',
bgColor: '#fff',
marginBottom: 8,
height: 300
} as ComponentStyle
}
} as DiyComponent<VideoPlayerProperty>
@@ -0,0 +1,30 @@
<template>
<div class="w-full" :style="{ height: `${property.style.height}px` }">
<el-image class="w-full w-full" :src="property.posterUrl" v-if="property.posterUrl" />
<video
v-else
class="w-full w-full"
:src="property.videoUrl"
:poster="property.posterUrl"
:autoplay="property.autoplay"
controls
></video>
</div>
</template>
<script setup lang="ts">
import { VideoPlayerProperty } from './config'
/** 视频播放 */
defineOptions({ name: 'VideoPlayer' })
defineProps<{ property: VideoPlayerProperty }>()
</script>
<style scoped lang="scss">
/* 图片 */
img {
display: block;
width: 100%;
height: 100%;
}
</style>
@@ -0,0 +1,55 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<template #style="{ formData }">
<el-form-item label="高度" prop="height">
<el-slider
v-model="formData.height"
:max="500"
:min="100"
show-input
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
</template>
<el-form label-width="80px" :model="formData">
<el-form-item label="上传视频" prop="videoUrl">
<UploadFile
v-model="formData.videoUrl"
:file-type="['mp4']"
:limit="1"
:file-size="100"
class="min-w-80px"
/>
</el-form-item>
<el-form-item label="上传封面" prop="posterUrl">
<UploadImg
v-model="formData.posterUrl"
draggable="false"
height="80px"
width="100%"
class="min-w-80px"
>
<template #tip> 建议宽度750 </template>
</UploadImg>
</el-form-item>
<el-form-item label="自动播放" prop="autoplay">
<el-switch v-model="formData.autoplay" />
</el-form-item>
</el-form>
</ComponentContainerProperty>
</template>
<script setup lang="ts">
import { VideoPlayerProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
// 视频播放属性面板
defineOptions({ name: 'VideoPlayerProperty' })
const props = defineProps<{ modelValue: VideoPlayerProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,61 @@
/*
* 组件注册
*
* 组件规范:
* 1. 每个子目录就是一个独立的组件,每个目录包括以下三个文件:
* 2. config.ts:组件配置,必选,用于定义组件、组件默认的属性、定义属性的类型
* 3. index.vue:组件展示,用于展示组件的渲染效果。可以不提供,如 Page(页面设置),只需要属性配置表单即可
* 4. property.vue:组件属性表单,用于配置组件,必选,
*
* 注:
* 组件ID以config.ts中配置的id为准,与组件目录的名称无关,但还是建议组件目录的名称与组件ID保持一致
*/
// 导入组件界面模块
const viewModules: Record<string, any> = import.meta.glob('./*/*.vue')
// 导入配置模块
const configModules: Record<string, any> = import.meta.glob('./*/config.ts', { eager: true })
// 界面模块
const components = {}
// 组件配置模块
const componentConfigs = {}
// 组件界面的类型
type ViewType = 'index' | 'property'
/**
* 注册组件的界面模块
*
* @param componentId 组件ID
* @param configPath 配置模块的文件路径
* @param viewType 组件界面的类型
*/
const registerComponentViewModule = (
componentId: string,
configPath: string,
viewType: ViewType
) => {
const viewPath = configPath.replace('config.ts', `${viewType}.vue`)
const viewModule = viewModules[viewPath]
if (viewModule) {
// 定义异步组件
components[componentId] = defineAsyncComponent(viewModule)
}
}
// 注册
Object.keys(configModules).forEach((modulePath: string) => {
const component = configModules[modulePath].component
const componentId = component?.id
if (componentId) {
// 注册组件
componentConfigs[componentId] = component
// 注册预览界面
registerComponentViewModule(componentId, modulePath, 'index')
// 注册属性配置表单
registerComponentViewModule(`${componentId}Property`, modulePath, 'property')
}
})
export { components, componentConfigs }
+470
View File
@@ -0,0 +1,470 @@
<template>
<el-container class="editor">
<!-- 顶部:工具栏 -->
<el-header class="editor-header">
<!-- 左侧操作区 -->
<slot name="toolBarLeft"></slot>
<!-- 中心操作区 -->
<div class="header-center flex flex-1 items-center justify-center">
<span>{{ title }}</span>
</div>
<!-- 右侧操作区 -->
<el-button-group class="header-right">
<el-tooltip content="重置">
<el-button @click="handleReset">
<Icon icon="system-uicons:reset-alt" :size="24" />
</el-button>
</el-tooltip>
<el-tooltip content="预览">
<el-button @click="handlePreview">
<Icon icon="ep:view" :size="24" />
</el-button>
</el-tooltip>
<el-tooltip content="保存">
<el-button @click="handleSave">
<Icon icon="ep:check" :size="24" />
</el-button>
</el-tooltip>
</el-button-group>
</el-header>
<!-- 中心区域 -->
<el-container class="editor-container">
<!-- 左侧:组件库 -->
<ComponentLibrary ref="componentLibrary" :list="libs" v-if="libs && libs.length > 0" />
<!-- 中心设计区域 -->
<div class="editor-center page-prop-area" @click="handlePageSelected">
<!-- 手机顶部 -->
<div class="editor-design-top">
<!-- 手机顶部状态栏 -->
<img src="@/assets/imgs/diy/statusBar.png" alt="" class="status-bar" />
<!-- 手机顶部导航栏 -->
<ComponentContainer
v-if="showNavigationBar"
:component="navigationBarComponent"
:show-toolbar="false"
:active="selectedComponent?.id === navigationBarComponent.id"
@click="handleNavigationBarSelected"
class="cursor-pointer!"
/>
</div>
<!-- 手机页面编辑区域 -->
<el-scrollbar
height="100%"
wrap-class="editor-design-center page-prop-area"
view-class="phone-container"
:view-style="{
backgroundColor: pageConfigComponent.property.backgroundColor,
backgroundImage: `url(${pageConfigComponent.property.backgroundImage})`
}"
>
<draggable
class="page-prop-area drag-area"
v-model="pageComponents"
item-key="index"
:animation="200"
filter=".component-toolbar"
ghost-class="draggable-ghost"
:force-fallback="true"
group="component"
@change="handleComponentChange"
>
<template #item="{ element, index }">
<ComponentContainer
:component="element"
:active="selectedComponentIndex === index"
:can-move-up="index > 0"
:can-move-down="index < pageComponents.length - 1"
@move="(direction) => handleMoveComponent(index, direction)"
@copy="handleCopyComponent(index)"
@delete="handleDeleteComponent(index)"
@click="handleComponentSelected(element, index)"
/>
</template>
</draggable>
</el-scrollbar>
<!-- 手机底部导航 -->
<div v-if="showTabBar" :class="['editor-design-bottom', 'component', 'cursor-pointer!']">
<ComponentContainer
:component="tabBarComponent"
:show-toolbar="false"
:active="selectedComponent?.id === tabBarComponent.id"
@click="handleTabBarSelected"
/>
</div>
</div>
<!-- 右侧属性面板 -->
<el-aside class="editor-right" width="350px" v-if="selectedComponent?.property">
<el-card
shadow="never"
body-class="h-[calc(100%-var(--el-card-padding)-var(--el-card-padding))]"
class="h-full"
>
<!-- 组件名称 -->
<template #header>
<div class="flex items-center gap-8px">
<Icon :icon="selectedComponent.icon" color="gray" />
<span>{{ selectedComponent.name }}</span>
</div>
</template>
<el-scrollbar
class="m-[calc(0px-var(--el-card-padding))]"
view-class="p-[var(--el-card-padding)] p-b-[calc(var(--el-card-padding)+var(--el-card-padding))] property"
>
<component
:is="selectedComponent.id + 'Property'"
v-model="selectedComponent.property"
/>
</el-scrollbar>
</el-card>
</el-aside>
</el-container>
</el-container>
</template>
<script lang="ts">
// 注册所有的组件
import { components } from './components/mobile/index'
export default {
components: { ...components }
}
</script>
<script lang="ts" setup>
import draggable from 'vuedraggable'
import ComponentLibrary from './components/ComponentLibrary.vue'
import { cloneDeep, includes } from 'lodash-es'
import { component as PAGE_CONFIG_COMPONENT } from '@/components/DiyEditor/components/mobile/PageConfig/config'
import { component as NAVIGATION_BAR_COMPONENT } from './components/mobile/NavigationBar/config'
import { component as TAB_BAR_COMPONENT } from './components/mobile/TabBar/config'
import { isString } from '@/utils/is'
import { DiyComponent, DiyComponentLibrary, PageConfig } from '@/components/DiyEditor/util'
import { componentConfigs } from '@/components/DiyEditor/components/mobile'
/** 页面装修详情页 */
defineOptions({ name: 'DiyPageDetail' })
// 消息弹窗
const message = useMessage()
// 左侧组件库
const componentLibrary = ref()
// 页面设置组件
const pageConfigComponent = ref<DiyComponent<any>>(cloneDeep(PAGE_CONFIG_COMPONENT))
// 顶部导航栏
const navigationBarComponent = ref<DiyComponent<any>>(cloneDeep(NAVIGATION_BAR_COMPONENT))
// 底部导航菜单
const tabBarComponent = ref<DiyComponent<any>>(cloneDeep(TAB_BAR_COMPONENT))
// 选中的组件,默认选中顶部导航栏
const selectedComponent = ref<DiyComponent<any>>()
// 选中的组件索引
const selectedComponentIndex = ref<number>(-1)
// 组件列表
const pageComponents = ref<DiyComponent<any>[]>([])
// 定义属性
const props = defineProps<{
// 页面配置,支持Json字符串
modelValue: string | PageConfig
// 标题
title: string
// 组件库
libs: DiyComponentLibrary[]
// 是否显示顶部导航栏
showNavigationBar: boolean
// 是否显示底部导航菜单
showTabBar: boolean
// 是否显示页面配置
showPageConfig: boolean
}>()
// 监听传入的页面配置
watch(
() => props.modelValue,
() => {
const modelValue = isString(props.modelValue)
? (JSON.parse(props.modelValue) as PageConfig)
: props.modelValue
pageConfigComponent.value.property = modelValue?.page || PAGE_CONFIG_COMPONENT.property
navigationBarComponent.value.property =
modelValue?.navigationBar || NAVIGATION_BAR_COMPONENT.property
tabBarComponent.value.property = modelValue?.tabBar || TAB_BAR_COMPONENT.property
// 查找对应的页面组件
pageComponents.value = (modelValue?.components || []).map((item) => {
const component = componentConfigs[item.id]
return { ...component, property: item.property }
})
},
{
immediate: true
}
)
// 保存
const handleSave = () => {
const pageConfig = {
page: pageConfigComponent.value.property,
navigationBar: navigationBarComponent.value.property,
tabBar: tabBarComponent.value.property,
components: pageComponents.value.map((component) => {
// 只保留APP有用的字段
return { id: component.id, property: component.property }
})
} as PageConfig
if (!props.showTabBar) {
delete pageConfig.tabBar
}
// 发送数据更新通知
const modelValue = isString(props.modelValue) ? JSON.stringify(pageConfig) : pageConfig
emits('update:modelValue', modelValue)
// 发送保存通知
emits('save', pageConfig)
}
// 处理页面选中:显示属性表单
const handlePageSelected = (event: any) => {
if (!props.showPageConfig) return
// 配置了样式 page-prop-area 的元素,才显示页面设置
if (includes(event?.target?.classList, 'page-prop-area')) {
handleComponentSelected(unref(pageConfigComponent))
}
}
/**
* 选中组件
*
* @param component 组件
* @param index 组件的索引
*/
const handleComponentSelected = (component: DiyComponent<any>, index: number = -1) => {
selectedComponent.value = component
selectedComponentIndex.value = index
}
// 选中顶部导航栏
const handleNavigationBarSelected = () => {
handleComponentSelected(unref(navigationBarComponent))
}
// 选中底部导航菜单
const handleTabBarSelected = () => {
handleComponentSelected(unref(tabBarComponent))
}
// 组件变动
const handleComponentChange = (dragEvent: any) => {
// 新增,即从组件库拖拽添加组件
if (dragEvent.added) {
const { element, newIndex } = dragEvent.added
handleComponentSelected(element, newIndex)
} else if (dragEvent.moved) {
// 拖拽排序
const { newIndex } = dragEvent.moved
// 保持选中
selectedComponentIndex.value = newIndex
}
}
// 交换组件
const swapComponent = (oldIndex: number, newIndex: number) => {
;[pageComponents.value[oldIndex], pageComponents.value[newIndex]] = [
pageComponents.value[newIndex],
pageComponents.value[oldIndex]
]
// 保持选中
selectedComponentIndex.value = newIndex
}
/** 移动组件 */
const handleMoveComponent = (index: number, direction: number) => {
const newIndex = index + direction
if (newIndex < 0 || newIndex >= pageComponents.value.length) return
swapComponent(index, newIndex)
}
/** 复制组件 */
const handleCopyComponent = (index: number) => {
const component = cloneDeep(pageComponents.value[index])
pageComponents.value.splice(index + 1, 0, component)
}
/**
* 删除组件
* @param index 当前组件index
*/
const handleDeleteComponent = (index: number) => {
// 删除组件
pageComponents.value.splice(index, 1)
if (index < pageComponents.value.length) {
// 1. 不是最后一个组件时,删除后选中下面的组件
let bottomIndex = index
handleComponentSelected(pageComponents.value[bottomIndex], bottomIndex)
} else if (pageComponents.value.length > 0) {
// 2. 不是第一个组件时,删除后选中上面的组件
let topIndex = index - 1
handleComponentSelected(pageComponents.value[topIndex], topIndex)
} else {
// 3. 组件全部删除之后,显示页面设置
handleComponentSelected(unref(pageConfigComponent))
}
}
// 工具栏操作
const emits = defineEmits(['reset', 'preview', 'save', 'update:modelValue'])
// 重置
const handleReset = () => {
message.warning('开发中~')
emits('reset')
}
// 预览
const handlePreview = () => {
message.warning('开发中~')
emits('preview')
}
// 设置默认选中的组件
const setDefaultSelectedComponent = () => {
if (props.showPageConfig) {
selectedComponent.value = unref(pageConfigComponent)
} else if (props.showNavigationBar) {
selectedComponent.value = unref(navigationBarComponent)
} else if (props.showTabBar) {
selectedComponent.value = unref(tabBarComponent)
}
}
watch(
() => [props.showPageConfig, props.showNavigationBar, props.showTabBar],
() => setDefaultSelectedComponent()
)
onMounted(() => setDefaultSelectedComponent())
</script>
<style lang="scss" scoped>
/* 手机宽度 */
$phone-width: 375px;
$toolbar-height: 42px;
/* 根节点样式 */
.editor {
display: flex;
height: 100%;
margin: calc(0px - var(--app-content-padding));
flex-direction: column;
/* 顶部:工具栏 */
.editor-header {
display: flex;
height: $toolbar-height;
padding: 0;
background-color: var(--el-bg-color);
border-bottom: solid 1px var(--el-border-color);
align-items: center;
justify-content: space-between;
/* 工具栏:右侧按钮 */
.header-right {
height: 100%;
.el-button {
height: 100%;
}
}
/* 隐藏工具栏按钮的边框 */
:deep(.el-radio-button__inner),
:deep(.el-button) {
border-top: none !important;
border-bottom: none !important;
border-radius: 0 !important;
}
}
/* 中心操作区 */
.editor-container {
height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) -
$toolbar-height
);
/* 右侧属性面板 */
.editor-right {
overflow: hidden;
box-shadow: -8px 0 8px -8px rgb(0 0 0 / 12%);
flex-shrink: 0;
/* 属性面板顶部:减少内边距 */
:deep(.el-card__header) {
padding: 8px 16px;
}
/* 属性面板分组 */
:deep(.property-group) {
margin: 0 -20px;
&.el-card {
border: none;
}
/* 属性分组名称 */
.el-card__header {
padding: 8px 32px;
background: var(--el-bg-color-page);
border: none;
}
.el-card__body {
border: none;
}
}
}
/* 中心区域 */
.editor-center {
position: relative;
display: flex;
width: 100%;
margin: 16px 0 0;
overflow: hidden;
background-color: var(--app-content-bg-color);
flex: 1 1 0;
flex-direction: column;
justify-content: center;
/* 手机顶部 */
.editor-design-top {
display: flex;
width: $phone-width;
margin: 0 auto;
flex-direction: column;
/* 手机顶部状态栏 */
.status-bar {
width: $phone-width;
height: 20px;
background-color: #fff;
}
}
/* 手机底部导航 */
.editor-design-bottom {
width: $phone-width;
margin: 0 auto;
}
/* 手机页面编辑区域 */
:deep(.editor-design-center) {
width: 100%;
/* 主体内容 */
.phone-container {
position: relative;
width: $phone-width;
height: 100%;
margin: 0 auto;
background-repeat: no-repeat;
background-size: 100% 100%;
.drag-area {
width: 100%;
height: 100%;
}
}
}
}
}
}
</style>
+121
View File
@@ -0,0 +1,121 @@
import { ref, Ref } from 'vue'
import { PageConfigProperty } from '@/components/DiyEditor/components/mobile/PageConfig/config'
import { NavigationBarProperty } from '@/components/DiyEditor/components/mobile/NavigationBar/config'
import { TabBarProperty } from '@/components/DiyEditor/components/mobile/TabBar/config'
// 页面装修组件
export interface DiyComponent<T> {
// 组件唯一标识
id: string
// 组件名称
name: string
// 组件图标
icon: string
// 组件属性
property: T
}
// 页面装修组件库
export interface DiyComponentLibrary {
// 组件库名称
name: string
// 是否展开
extended: boolean
// 组件列表
components: string[]
}
// 组件样式
export interface ComponentStyle {
// 背景类型
bgType: 'color' | 'img'
// 背景颜色
bgColor: string
// 背景图片
bgImg: string
// 外边距
margin: number
marginTop: number
marginRight: number
marginBottom: number
marginLeft: number
// 内边距
padding: number
paddingTop: number
paddingRight: number
paddingBottom: number
paddingLeft: number
// 边框圆角
borderRadius: number
borderTopLeftRadius: number
borderTopRightRadius: number
borderBottomRightRadius: number
borderBottomLeftRadius: number
}
// 页面配置
export interface PageConfig {
// 页面属性
page: PageConfigProperty
// 顶部导航栏属性
navigationBar: NavigationBarProperty
// 底部导航菜单属性
tabBar?: TabBarProperty
// 页面组件列表
components: PageComponent[]
}
// 页面组件,只保留组件ID,组件属性
export interface PageComponent extends Pick<DiyComponent<any>, 'id' | 'property'> {}
// 属性表单监听
export function usePropertyForm<T>(modelValue: T, emit: Function): { formData: Ref<T> } {
const formData = ref<T>()
// 监听属性数据变动
watch(
() => modelValue,
() => {
formData.value = modelValue
},
{
deep: true,
immediate: true
}
)
// 监听表单数据变动
watch(
() => formData.value,
() => {
emit('update:modelValue', formData.value)
},
{
deep: true
}
)
return { formData }
}
// 页面组件库
export const PAGE_LIBS = [
{
name: '基础组件',
extended: true,
components: ['SearchBar', 'NoticeBar', 'MenuSwiper', 'MenuGrid', 'MenuList']
},
{
name: '图文组件',
extended: true,
components: ['ImageBar', 'Carousel', 'TitleBar', 'VideoPlayer', 'Divider', 'MagicCube']
},
{ name: '商品组件', extended: true, components: ['ProductCard', 'ProductList'] },
{
name: '会员组件',
extended: true,
components: ['UserCard', 'UserOrder', 'UserWallet', 'UserCoupon']
},
{
name: '营销组件',
extended: true,
components: ['CombinationCard', 'SeckillCard', 'PointCard', 'CouponCard']
}
] as DiyComponentLibrary[]
+59
View File
@@ -0,0 +1,59 @@
<template>
<el-input v-model="valueRef" v-bind="$attrs">
<template #append>
<el-color-picker v-model="colorRef" :predefine="PREDEFINE_COLORS" />
</template>
</el-input>
</template>
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { PREDEFINE_COLORS } from '@/utils/color'
/**
* 带颜色选择器输入框
*/
defineOptions({ name: 'InputWithColor' })
const props = defineProps({
modelValue: propTypes.string.def('').isRequired,
color: propTypes.string.def('').isRequired
})
watch(
() => props.modelValue,
(val: string) => {
if (val === unref(valueRef)) return
valueRef.value = val
}
)
const emit = defineEmits(['update:modelValue', 'update:color'])
// 输入框的值
const valueRef = ref(props.modelValue)
watch(
() => valueRef.value,
(val: string) => {
emit('update:modelValue', val)
}
)
// 颜色
const colorRef = ref(props.color)
watch(
() => colorRef.value,
(val: string) => {
emit('update:color', val)
}
)
</script>
<style scoped lang="scss">
:deep(.el-input-group__append) {
padding: 0;
.el-color-picker__trigger {
padding: 0;
border-left: none;
border-radius: 0 var(--el-input-border-radius) var(--el-input-border-radius) 0;
}
}
</style>
+270
View File
@@ -0,0 +1,270 @@
<template>
<div class="relative">
<table class="cube-table">
<!-- 底层:魔方矩阵 -->
<tbody>
<tr v-for="(rowCubes, row) in cubes" :key="row">
<td
v-for="(cube, col) in rowCubes"
:key="col"
:class="['cube', { active: cube.active }]"
:style="{
width: `${cubeSize}px`,
height: `${cubeSize}px`
}"
@click="handleCubeClick(row, col)"
@mouseenter="handleCellHover(row, col)"
>
<Icon icon="ep-plus" />
</td>
</tr>
</tbody>
<!-- 顶层:热区 -->
<div
v-for="(hotArea, index) in hotAreas"
:key="index"
class="hot-area"
:style="{
top: `${cubeSize * hotArea.top}px`,
left: `${cubeSize * hotArea.left}px`,
height: `${cubeSize * hotArea.height}px`,
width: `${cubeSize * hotArea.width}px`
}"
@click="handleHotAreaSelected(hotArea, index)"
@mouseover="exitHotAreaSelectMode"
>
<!-- 右上角热区删除按钮 -->
<div
v-if="selectedHotAreaIndex === index"
class="btn-delete"
@click="handleDeleteHotArea(index)"
>
<Icon icon="ep:circle-close-filled" />
</div>
{{ `${hotArea.width}×${hotArea.height}` }}
</div>
</table>
</div>
</template>
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import * as vueTypes from 'vue-types'
import { Point, Rect, isContains, isOverlap, createRect } from './util'
// 魔方编辑器
// 有两部分组成:
// 1. 魔方矩阵:位于底层,由方块组件的二维表格,用于创建热区
// 操作方法:
// 1.1 点击其中一个方块就会进入热区选择模式
// 1.2 再次点击另外一个方块时,结束热区选择模式
// 1.3 在两个方块中间的区域创建热区
// 如果两次点击的都是同一方块,就只创建一个格子的热区
// 2. 热区:位于顶层,采用绝对定位,覆盖在魔方矩阵上面。
defineOptions({ name: 'MagicCubeEditor' })
/**
* 方块
* @property active 是否激活
*/
type Cube = Point & { active: boolean }
// 定义属性
const props = defineProps({
// 热区列表
modelValue: vueTypes.array<any>().isRequired,
// 行数,默认 4 行
rows: propTypes.number.def(4),
// 列数,默认 4 列
cols: propTypes.number.def(4),
// 方块大小,单位px,默认75px
cubeSize: propTypes.number.def(75)
})
// 魔方矩阵:所有的方块
const cubes = ref<Cube[][]>([])
// 监听行数、列数变化
watch(
() => [props.rows, props.cols],
() => {
// 清空魔方
cubes.value = []
if (!props.rows || !props.cols) return
// 初始化魔方
for (let row = 0; row < props.rows; row++) {
cubes.value[row] = []
for (let col = 0; col < props.cols; col++) {
cubes.value[row].push({ x: col, y: row, active: false })
}
}
},
{ immediate: true }
)
// 热区列表
const hotAreas = ref<Rect[]>([])
// 初始化热区
watch(
() => props.modelValue,
() => (hotAreas.value = props.modelValue || []),
{ immediate: true }
)
// 热区起始方块
const hotAreaBeginCube = ref<Cube>()
// 是否开启了热区选择模式
const isHotAreaSelectMode = () => !!hotAreaBeginCube.value
/**
* 处理鼠标点击方块
*
* @param currentRow 当前行号
* @param currentCol 当前列号
*/
const handleCubeClick = (currentRow: number, currentCol: number) => {
const currentCube = cubes.value[currentRow][currentCol]
// 情况1:进入热区选择模式
if (!isHotAreaSelectMode()) {
hotAreaBeginCube.value = currentCube
hotAreaBeginCube.value.active = true
return
}
// 情况2:结束热区选择模式
hotAreas.value.push(createRect(hotAreaBeginCube.value!, currentCube))
// 结束热区选择模式
exitHotAreaSelectMode()
// 创建后就选中热区
let hotAreaIndex = hotAreas.value.length - 1
handleHotAreaSelected(hotAreas.value[hotAreaIndex], hotAreaIndex)
// 发送热区变动通知
emitUpdateModelValue()
}
/**
* 处理鼠标经过方块
*
* @param currentRow 当前行号
* @param currentCol 当前列号
*/
const handleCellHover = (currentRow: number, currentCol: number) => {
// 当前没有进入热区选择模式
if (!isHotAreaSelectMode()) return
// 当前已选的区域
const currentSelectedArea = createRect(
hotAreaBeginCube.value!,
cubes.value[currentRow][currentCol]
)
// 热区不允许重叠
for (const hotArea of hotAreas.value) {
// 检查是否重叠
if (isOverlap(hotArea, currentSelectedArea)) {
// 结束热区选择模式
exitHotAreaSelectMode()
return
}
}
// 激活选中区域内部的方块
eachCube((_, __, cube) => {
cube.active = isContains(currentSelectedArea, cube)
})
}
/**
* 处理热区删除
*
* @param index 热区索引
*/
const handleDeleteHotArea = (index: number) => {
hotAreas.value.splice(index, 1)
// 结束热区选择模式
exitHotAreaSelectMode()
// 发送热区变动通知
emitUpdateModelValue()
}
// 发送模型更新
const emit = defineEmits(['update:modelValue', 'hotAreaSelected'])
// 发送热区变动通知
const emitUpdateModelValue = () => emit('update:modelValue', hotAreas)
// 热区选中
const selectedHotAreaIndex = ref(-1)
const handleHotAreaSelected = (hotArea: Rect, index: number) => {
selectedHotAreaIndex.value = index
emit('hotAreaSelected', hotArea, index)
}
/**
* 结束热区选择模式
*/
function exitHotAreaSelectMode() {
// 移除方块激活标记
eachCube((_, __, cube) => {
if (cube.active) {
cube.active = false
}
})
// 清除起点
hotAreaBeginCube.value = undefined
}
/**
* 迭代魔方矩阵
* @param callback 回调
*/
const eachCube = (callback: (x: number, y: number, cube: Cube) => void) => {
for (let x = 0; x < cubes.value.length; x++) {
for (let y = 0; y < cubes.value[x].length; y++) {
callback(x, y, cubes.value[x][y])
}
}
}
</script>
<style lang="scss" scoped>
.cube-table {
position: relative;
border-spacing: 0;
border-collapse: collapse;
.cube {
border: 1px solid var(--el-border-color);
text-align: center;
color: var(--el-text-color-secondary);
cursor: pointer;
box-sizing: border-box;
&.active {
background: var(--el-color-primary-light-9);
}
}
.hot-area {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--el-color-primary);
background: var(--el-color-primary-light-8);
color: var(--el-color-primary);
box-sizing: border-box;
border-spacing: 0;
border-collapse: collapse;
cursor: pointer;
.btn-delete {
z-index: 1;
position: absolute;
top: -8px;
right: -8px;
height: 16px;
width: 16px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: #fff;
}
}
}
</style>
+72
View File
@@ -0,0 +1,72 @@
// 坐标点
export interface Point {
x: number
y: number
}
// 矩形
export interface Rect {
// 左上角 X 轴坐标
left: number
// 左上角 Y 轴坐标
top: number
// 右下角 X 轴坐标
right: number
// 右下角 Y 轴坐标
bottom: number
// 矩形宽度
width: number
// 矩形高度
height: number
}
/**
* 判断两个矩形是否重叠
* @param a 矩形 A
* @param b 矩形 B
*/
export const isOverlap = (a: Rect, b: Rect): boolean => {
return (
a.left < b.left + b.width &&
a.left + a.width > b.left &&
a.top < b.top + b.height &&
a.height + a.top > b.top
)
}
/**
* 检查坐标点是否在矩形内
* @param hotArea 矩形
* @param point 坐标
*/
export const isContains = (hotArea: Rect, point: Point): boolean => {
return (
point.x >= hotArea.left &&
point.x < hotArea.right &&
point.y >= hotArea.top &&
point.y < hotArea.bottom
)
}
/**
* 在两个坐标点中间,创建一个矩形
*
* 存在以下情况:
* 1. 两个坐标点是同一个位置,只占一个位置的正方形,宽高都为 1
* 2. X 轴坐标相同,只占一行的矩形,高度为 1
* 3. Y 轴坐标相同,只占一列的矩形,宽度为 1
* 4. 多行多列的矩形
*
* @param a 坐标点一
* @param b 坐标点二
*/
export const createRect = (a: Point, b: Point): Rect => {
// 计算矩形的范围
const [left, left2] = [a.x, b.x].sort()
const [top, top2] = [a.y, b.y].sort()
const right = left2 + 1
const bottom = top2 + 1
const height = bottom - top
const width = right - left
return { left, right, top, bottom, height, width }
}
+31 -1
View File
@@ -1,5 +1,5 @@
<template>
<ElDialog v-model="showSearch" :show-close="false" title="菜单搜索">
<ElDialog v-if="isModal" v-model="showSearch" :show-close="false" title="菜单搜索">
<el-select
filterable
:reserve-keyword="false"
@@ -17,11 +17,34 @@
/>
</el-select>
</ElDialog>
<div v-else class="custom-hover" @click.stop="showTopSearch = !showTopSearch">
<Icon icon="ep:search" />
<el-select
filterable
:reserve-keyword="false"
remote
placeholder="请输入菜单内容"
:remote-method="remoteMethod"
class="overflow-hidden transition-all-600"
:class="showTopSearch ? 'w-220px ml2' : 'w-0'"
@change="handleChange"
>
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
</template>
<script lang="ts" setup>
defineProps({
isModal: {
type: Boolean,
default: true
}
})
const router = useRouter() // 路由对象
const showSearch = ref(false) // 是否显示弹框
const showTopSearch = ref(false) // 是否显示顶部搜索框
const value: Ref = ref('') // 用户输入的值
const routers = router.getRoutes() // 路由对象
@@ -50,14 +73,21 @@ function remoteMethod(data) {
function handleChange(path) {
router.push({ path })
hiddenTopSearch();
}
function hiddenTopSearch() {
showTopSearch.value = false
}
onMounted(() => {
window.addEventListener('keydown', listenKey)
window.addEventListener('click', hiddenTopSearch)
})
onUnmounted(() => {
window.removeEventListener('keydown', listenKey)
window.removeEventListener('click', hiddenTopSearch)
})
// 监听 ctrl + k
Loaded 100 of 234 files, more files were not shown because too many files have changed in this diff. Show more