wangzhibo
4 天以前 10595d8632f959d0954d1939243196f4eed02372
first commit
532个文件已添加
1个文件已修改
49938 ■■■■■ 已修改文件
.gitignore 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.hbuilderx/launch.json 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.hintrc 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.prettierrc 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.vscode/config.code-snippets 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.vscode/settings.json 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.vscode/template.code-snippets 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
App.vue 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
LICENSE 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
README.md 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
androidPrivacy.json 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/address/item.vue 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/address/select.vue 150 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/agree-btn.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/coupon/get.vue 166 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/coupon/item/bg.png 补丁 | 查看 | 原始文档 | blame | 历史
components/coupon/item/index.vue 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/coupon/select.vue 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/goods/cover.vue 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/goods/group.vue 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/goods/item.vue 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/goods/spec.vue 393 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/sms-btn.vue 231 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/dev.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/index.ts 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/prod.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/proxy.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/bootstrap/eps.ts 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/bootstrap/index.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/bootstrap/modules.ts 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/hooks/app.ts 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/hooks/comm.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/hooks/hmr.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/hooks/index.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/hooks/pager.ts 180 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/hooks/wx.ts 289 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/index.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/module/index.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/router/index.ts 325 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/service/base.ts 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/service/index.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/service/request.ts 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/service/sign.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/store/dict.ts 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/store/index.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/store/user.ts 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/types/index.d.ts 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/upload/comm.ts 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/upload/index.ts 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/utils/canvas.ts 603 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/utils/comm.ts 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/utils/index.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/utils/qrcode.ts 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/utils/storage.ts 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cool/utils/ui.ts 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/logo.png 补丁 | 查看 | 原始文档 | blame | 历史
hooks/index.ts 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hooks/order.ts 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hooks/shopping-cart.ts 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hooks/spec.ts 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
index.html 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
locale/en.json 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
locale/es.json 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
locale/fr.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
locale/index.ts 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
locale/zh-Hans.json 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
locale/zh-Hant.json 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
main.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
manifest.json 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package-lock.json 5427 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages.json 306 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/basic/button.vue 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/basic/icon.vue 210 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/basic/image.vue 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/basic/loading.vue 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/basic/tag.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/basic/text.vue 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/basic/toast.vue 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/extend/action-sheet.vue 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/extend/captcha.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/extend/confirm.vue 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/extend/dialog.vue 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/extend/filter-bar.vue 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/extend/page.vue 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/extend/service.vue 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/extend/slider-verify.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/extend/tree.vue 150 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/form/checkbox.vue 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/form/form.vue 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/form/input-number.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/form/input.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/form/radio.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/form/rate.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/form/select-city.vue 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/form/select-date.vue 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/form/select-popup.vue 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/form/select.vue 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/form/switch.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/form/textarea.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/form/upload.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/static/avatar1.png 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/static/avatar2.png 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/static/avatar3.png 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/static/avatar4.png 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/static/bg1.png 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/static/bg2.png 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/static/bg3.png 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/static/bg4.png 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/avatar.vue 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/badge.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/banner.vue 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/card.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/countdown.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/divider.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/flex.vue 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/grid.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/list-index.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/list.vue 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/loadmore.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/noticebar.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/popup.vue 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/progress.vue 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/scroller.vue 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/search.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/skeleton.vue 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/slider.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/tabs.vue 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/timeline.vue 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/topbar.vue 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/demo/view/waterfall.vue 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/goods/comment.vue 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/goods/components/comment-item.vue 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/goods/components/filter-bar.vue 350 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/goods/components/goods-comment.vue 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/goods/components/goods-detail.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/goods/components/goods-info.vue 158 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/goods/detail.vue 250 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/goods/list.vue 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/goods/search.vue 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/index/admin.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/index/category.vue 243 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/index/components/banner.vue 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/index/components/coupon-activity.vue 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/index/components/hot-category.vue 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/index/components/tabbar.vue 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/index/home.vue 174 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/index/my.vue 298 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/index/shopping-cart.vue 239 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/index/website.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/market/coupon.vue 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/order/comment.vue 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/order/components/cancel.vue 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/order/components/comment.vue 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/order/components/op-btns.vue 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/order/components/op.vue 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/order/components/pay.vue 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/order/components/status-tag.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/order/detail.vue 231 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/order/dict/index.ts 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/order/list.vue 240 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/order/logistics.vue 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/order/refund.vue 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/order/submit.vue 210 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/recycle/appointment.vue 918 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/recycle/banner.vue 368 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/recycle/home.vue 373 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/recycle/record.vue 467 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/recycle/transfer.vue 505 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/user/about.vue 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/user/address-edit.vue 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/user/address-list.vue 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/user/captcha.vue 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/user/doc.vue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/user/edit.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/user/login.vue 473 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/user/set.vue 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/user/static/icon/apple.png 补丁 | 查看 | 原始文档 | blame | 历史
pages/user/static/icon/phone.png 补丁 | 查看 | 原始文档 | blame | 历史
pages/user/static/icon/wx.png 补丁 | 查看 | 原始文档 | blame | 历史
pnpm-lock.yaml 2829 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
router/index.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
static/css/common.scss 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
static/css/iconfont/font.ttf 补丁 | 查看 | 原始文档 | blame | 历史
static/css/iconfont/font.woff 补丁 | 查看 | 原始文档 | blame | 历史
static/css/iconfont/font.woff2 补丁 | 查看 | 原始文档 | blame | 历史
static/css/iconfont/index.scss 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
static/css/index.scss 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
static/empty/address.png 补丁 | 查看 | 原始文档 | blame | 历史
static/empty/comm.png 补丁 | 查看 | 原始文档 | blame | 历史
static/empty/coupon.png 补丁 | 查看 | 原始文档 | blame | 历史
static/empty/message.png 补丁 | 查看 | 原始文档 | blame | 历史
static/empty/network.png 补丁 | 查看 | 原始文档 | blame | 历史
static/empty/order.png 补丁 | 查看 | 原始文档 | blame | 历史
static/empty/spopping-cart.png 补丁 | 查看 | 原始文档 | blame | 历史
static/icon/alipay.png 补丁 | 查看 | 原始文档 | blame | 历史
static/icon/carbonpay.png 补丁 | 查看 | 原始文档 | blame | 历史
static/icon/tabbar/admin.png 补丁 | 查看 | 原始文档 | blame | 历史
static/icon/tabbar/admin2.png 补丁 | 查看 | 原始文档 | blame | 历史
static/icon/tabbar/category.png 补丁 | 查看 | 原始文档 | blame | 历史
static/icon/tabbar/category2.png 补丁 | 查看 | 原始文档 | blame | 历史
static/icon/tabbar/cool.png 补丁 | 查看 | 原始文档 | blame | 历史
static/icon/tabbar/cool2.png 补丁 | 查看 | 原始文档 | blame | 历史
static/icon/tabbar/home.png 补丁 | 查看 | 原始文档 | blame | 历史
static/icon/tabbar/home2.png 补丁 | 查看 | 原始文档 | blame | 历史
static/icon/tabbar/my.png 补丁 | 查看 | 原始文档 | blame | 历史
static/icon/tabbar/my2.png 补丁 | 查看 | 原始文档 | blame | 历史
static/icon/tabbar/recycle.png 补丁 | 查看 | 原始文档 | blame | 历史
static/icon/tabbar/recycle2.png 补丁 | 查看 | 原始文档 | blame | 历史
static/icon/tabbar/shopping-cart.png 补丁 | 查看 | 原始文档 | blame | 历史
static/icon/tabbar/shopping-cart2.png 补丁 | 查看 | 原始文档 | blame | 历史
static/icon/wxpay.png 补丁 | 查看 | 原始文档 | blame | 历史
static/logo.png 补丁 | 查看 | 原始文档 | blame | 历史
static/recycle/appointment.png 补丁 | 查看 | 原始文档 | blame | 历史
static/recycle/qrcode.png 补丁 | 查看 | 原始文档 | blame | 历史
static/recycle/record.png 补丁 | 查看 | 原始文档 | blame | 历史
static/recycle/transfer.png 补丁 | 查看 | 原始文档 | blame | 历史
tsconfig.json 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
types/env.d.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
types/shop.d.ts 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni.scss 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-app/components/cl-version-about/cl-version-about.vue 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-app/components/cl-version-upgrade/cl-version-upgrade.vue 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-app/config.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-app/hooks/cache.ts 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-app/hooks/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-app/hooks/version.ts 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-app/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-app/pages/complain/detail.vue 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-app/pages/complain/list.vue 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-app/pages/complain/submit.vue 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-app/pages/feedback/detail.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-app/pages/feedback/list.vue 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-app/pages/feedback/submit.vue 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-app/pages/version/demo.vue 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-app/pages_init.json 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-app/static/bg.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-app/utils/index.ts 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/components/msg-item.vue 187 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/components/msg-list.vue 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/components/tools/emoji.vue 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/components/tools/fn.vue 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/components/tools/index.vue 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/config.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/hooks/index.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/hooks/message.ts 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/hooks/session.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/hooks/socket.ts 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/hooks/tools.ts 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/pages/chat.vue 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/pages_init.json 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/add.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/angry-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/anguished-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/astonished-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/confounded-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/confused-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/crying-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/disappointed-but-relieved-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/disappointed-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/dizzy-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/drooling-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/expressionless-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-savouring-delicious-food.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-screaming-in-fear.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-throwing-a-kiss.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-with-cold-sweat.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-with-cowboy-hat.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-with-finger-covering-closed-lips.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-with-head-bandage.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-with-look-of-triumph.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-with-medical-mask.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-with-monocle.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-with-one-eyebrow-raised.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-with-open-mouth-and-cold-sweat.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-with-open-mouth-vomiting.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-with-open-mouth.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-with-party-horn-and-party-hat.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-with-pleading-eyes.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-with-rolling-eyes.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-with-stuck-out-tongue-and-tightly-closed-eyes.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-with-stuck-out-tongue-and-winking-eye.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-with-stuck-out-tongue.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-with-thermometer.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-with-uneven-eyes-and-wavy-mouth.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/face-without-mouth.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/fearful-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/flushed-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/freezing-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/frowning-face-with-open-mouth.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/grimacing-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/grinning-face-with-one-large-and-one-small-eye.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/grinning-face-with-smiling-eyes.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/grinning-face-with-star-eyes.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/grinning-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/hugging-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/hushed-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/imp.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/kissing-face-with-closed-eyes.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/kissing-face-with-smiling-eyes.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/kissing-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/loudly-crying-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/lying-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/money-mouth-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/nauseated-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/nerd-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/neutral-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/overheated-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/pensive-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/persevering-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/pouting-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/relieved-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/rolling-on-the-floor-laughing.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/serious-face-with-symbols-covering-mouth.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/shocked-face-with-exploding-head.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/sleeping-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/sleepy-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/slightly-frowning-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/slightly-smiling-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/smiling-face-with-halo.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/smiling-face-with-heart-shaped-eyes.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/smiling-face-with-horns.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/smiling-face-with-open-mouth-and-smiling-eyes.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/smiling-face-with-open-mouth-and-tightly-closed-eyes.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/smiling-face-with-open-mouth.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/smiling-face-with-smiling-eyes-and-hand-covering-mouth.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/smiling-face-with-smiling-eyes-and-three-hearts.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/smiling-face-with-smiling-eyes.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/smiling-face-with-sunglasses.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/smirking-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/sneezing-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/thinking-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/tired-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/upside-down-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/weary-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/white-frowning-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/white-smiling-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/winking-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/worried-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/emoji/zipper-mouth-face.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/fn-camera.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/fn-pic.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/minus.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/static/plus.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/types/index.d.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-cs/utils/index.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/changelog.md 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-action-sheet/cl-action-sheet.vue 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-avatar-group/cl-avatar-group.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-avatar/cl-avatar.vue 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-badge/cl-badge.vue 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-banner/cl-banner.vue 293 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-button/cl-button.vue 179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-captcha/cl-captcha.vue 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-card/cl-card.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-checkbox-group/cl-checkbox-group.vue 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-checkbox/cl-checkbox.vue 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-col/cl-col.vue 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-confirm/cl-confirm.vue 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-countdown/cl-countdown.vue 273 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-dialog/cl-dialog.vue 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-divider/cl-divider.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-empty/cl-empty.vue 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-filter-bar/cl-filter-bar.vue 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-filter-item/cl-filter-item.vue 348 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-footer/cl-footer.vue 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-form-item/cl-form-item.vue 286 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-form/cl-form.vue 244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-grid-item/cl-grid-item.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-grid/cl-grid.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-guide/cl-guide.vue 321 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-icon/cl-icon.vue 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-image/cl-image.vue 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-input-number/cl-input-number.vue 206 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-input/cl-input.vue 240 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-list-index/cl-list-index.vue 456 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-list-item/cl-list-item.vue 258 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-list/cl-list.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-loading-mask/cl-loading-mask.vue 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-loading/cl-loading.vue 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-loadmore/cl-loadmore.vue 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-noticebar/cl-noticebar.vue 220 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-page/cl-page.vue 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-pay/ali.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-pay/cl-pay.vue 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-pay/wx.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-popup/cl-popup.vue 295 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-progress/cl-progress.vue 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-radio-group/cl-radio-group.vue 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-radio/cl-radio.vue 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-rate/cl-rate.vue 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-row/cl-row.vue 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-scroller/cl-scroller.vue 265 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-search/cl-search.vue 208 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-select-city/cl-select-city.vue 307 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-select-date/cl-select-date.vue 294 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-select-inner/cl-select-inner.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-select-inner/config.ts 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-select-popup/cl-select-popup.vue 302 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-select-region/cl-select-region.vue 145 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-select/cl-select.vue 236 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-service/cl-service.vue 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-share-popup/cl-share-popup.vue 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-share/alipay.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-share/cl-share.vue 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-share/qq.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-share/wx.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-share/wx2.png 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-skeleton/cl-skeleton.vue 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-slider-verify/cl-slider-verify.vue 261 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-slider/cl-slider.vue 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-status-bar/cl-status-bar.vue 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-sticky/cl-sticky.vue 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-switch/cl-switch.vue 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-tabs/cl-tabs.vue 394 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-tag/cl-tag.vue 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-text/cl-text.vue 179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-textarea/cl-textarea.vue 201 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-timeline-item/cl-timeline-item.vue 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-timeline/cl-timeline.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-toast/cl-toast.vue 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-topbar/cl-topbar.vue 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-tree-item/cl-tree-item.vue 238 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-tree-select/cl-tree-select.vue 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-tree/cl-tree.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-tree/helper.ts 158 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-upload/cl-upload.vue 350 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-video/cl-video.vue 208 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-waterfall-column/cl-waterfall-column.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/components/cl-waterfall/cl-waterfall.vue 225 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/config.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/data/city-pca.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/hooks/index.ts 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/hooks/style.ts 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/index.scss 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/index.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/readme.md 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/action-sheet.scss 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/avatar.scss 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/badge.scss 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/banner.scss 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/button.scss 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/captcha.scss 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/card.scss 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/checkbox.scss 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/col.scss 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/column.scss 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/common.scss 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/confirm.scss 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/countdown.scss 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/dialog.scss 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/divider.scss 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/empty.scss 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/filter-bar.scss 236 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/footer.scss 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/form.scss 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/grid.scss 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/guide.scss 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/icon.scss 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/image.scss 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/index.scss 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/input-number.scss 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/input.scss 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/list-index.scss 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/list.scss 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/loading-mask.scss 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/loading.scss 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/loadmore.scss 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/noticebar.scss 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/page.scss 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/pay.scss 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/popup.scss 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/progress.scss 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/radio.scss 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/rate.scss 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/row.scss 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/scroller.scss 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/search.scss 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/select-city.scss 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/select-date.scss 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/select-inner.scss 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/select-popup.scss 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/select.scss 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/share.scss 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/skeleton.scss 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/slider-verify.scss 167 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/status-bar.scss 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/steps.scss 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/sticky.scss 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/switch.scss 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/tabs.scss 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/tag.scss 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/text.scss 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/textarea.scss 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/timeline.scss 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/toast.scss 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/topbar.scss 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/tree.scss 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/upload.scss 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/video.scss 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/css/waterfall.scss 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/iconfont/iconfont.scss 439 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/iconfont/iconfont.ttf 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/iconfont/iconfont.woff 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/iconfont/iconfont.woff2 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/static/iconfont/index.scss 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/theme.scss 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/types/action-sheet.d.ts 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/types/confirm.d.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/types/form.d.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/types/index.d.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/types/list-index.d.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/types/loading.d.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/types/page.d.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/types/pay.d.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/types/share.d.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/types/timeline.d.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/types/toast.d.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/types/tree.d.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/types/waterfall.d.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/utils/async-validator.ts 1409 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/cool-ui/utils/index.ts 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/mp-html/README.md 194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/mp-html/changelog.md 150 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/mp-html/components/mp-html/mp-html.vue 498 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/mp-html/components/mp-html/node/node.vue 587 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/mp-html/components/mp-html/parser.js 1393 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/mp-html/package.json 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/mp-html/static/app-plus/mp-html/js/handler.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/mp-html/static/app-plus/mp-html/js/uni.webview.min.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uni_modules/mp-html/static/app-plus/mp-html/local.html 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
New file
@@ -0,0 +1,17 @@
.DS_Store
node_modules/
unpackage/
/dist/
/build/
# Log files
npm-debug.log*
# Editor directories and files
.project
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*
.hbuilderx/launch.json
New file
@@ -0,0 +1,36 @@
{
    // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
    // launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
    "version" : "0.0",
    "configurations" : [
        {
            "app-plus" : {
                "launchtype" : "local"
            },
            "default" : {
                "launchtype" : "local"
            },
            "h5" : {
                "launchtype" : "local"
            },
            "mp-alipay" : {
                "launchtype" : "local"
            },
            "mp-toutiao" : {
                "launchtype" : "local"
            },
            "mp-weixin" : {
                "launchtype" : "local"
            },
            "type" : "uniCloud"
        },
        {
            "playground" : "standard",
            "type" : "uni-app:app-ios"
        },
        {
            "playground" : "standard",
            "type" : "uni-app:app-android"
        }
    ]
}
.hintrc
New file
@@ -0,0 +1,8 @@
{
  "extends": [
    "development"
  ],
  "hints": {
    "typescript-config/is-valid": "off"
  }
}
.prettierrc
New file
@@ -0,0 +1,8 @@
{
    "tabWidth": 4,
    "useTabs": true,
    "semi": true,
    "jsxBracketSameLine": true,
    "singleQuote": false,
    "printWidth": 100
}
.vscode/config.code-snippets
New file
@@ -0,0 +1,15 @@
{
    "module-config": {
        "prefix": "module-config",
        "scope": "typescript",
        "body": [
            "import type { ModuleConfig } from \"/@/cool\";",
            "",
            "export default (): ModuleConfig => {",
            "    return {};",
            "};",
            ""
        ],
        "description": "module config snippets"
    }
}
.vscode/settings.json
New file
@@ -0,0 +1,4 @@
{
    "editor.cursorSmoothCaretAnimation": "on",
    "editor.formatOnSave": true,
}
.vscode/template.code-snippets
New file
@@ -0,0 +1,119 @@
{
    "page": {
        "prefix": "page",
        "scope": "vue",
        "body": [
            "<template>",
            "    <cl-page>",
            "        <view class=\"page\">",
            "            $1",
            "        </view>",
            "    </cl-page>",
            "</template>",
            "",
            "<script lang=\"ts\" setup>",
            "import { useCool } from \"/@/cool\";",
            "",
            "const { service, router } = useCool();",
            "</script>",
            "",
            "<style lang=\"scss\" scoped>",
            ".page {",
            "    ",
            "}",
            "</style>",
            "",
        ],
        "description": "page snippets",
    },
    "component": {
        "prefix": "component",
        "scope": "vue",
        "body": [
            "<template>",
            "    <view class=\"$1\">",
            "        ",
            "    </view>",
            "</template>",
            "",
            "<script lang=\"ts\" setup>",
            "import { useCool } from \"/@/cool\";",
            "",
            "const { service, router } = useCool();",
            "</script>",
            "",
            "<style lang=\"scss\" scoped>",
            ".component$2 {",
            "    ",
            "}",
            "</style>",
            "",
        ],
        "description": "component snippets",
    },
    "popup": {
        "prefix": "popup",
        "scope": "vue",
        "body": [
            "<template>",
            "    <cl-popup v-model=\"visible\" direction=\"bottom\">",
            "        <view class=\"component$1\"> </view>",
            "    </cl-popup>",
            "</template>",
            "",
            "<script lang=\"ts\" setup>",
            "import { ref } from \"vue\";",
            "import { useCool } from \"/@/cool\";",
            "",
            "const { service, router } = useCool();",
            "",
            "// 是否可见",
            "const visible = ref(false);",
            "",
            "// 打开",
            "function open() {",
            "    visible.value = true;",
            "}",
            "",
            "// 关闭",
            "function close() {",
            "    visible.value = false;",
            "}",
            "",
            "defineExpose({",
            "    open,",
            "    close,",
            "});",
            "</script>",
            "",
            "<style lang=\"scss\" scoped>",
            ".component {",
            "}",
            "</style>",
            "",
        ],
        "description": "popup snippets",
    },
    "pager": {
        "prefix": "pager",
        "scope": "typescript",
        "body": [
            "const { service } = useCool();",
            "const { onRefresh, list } = usePager();",
            "",
            "function refresh(params?: any) {",
            "    const { data, next } = onRefresh(params);",
            "    next(service.$1.page(data));",
            "}",
            "",
            "onReady(() => {",
            "    refresh();",
            "});",
            "",
            "defineExpose({",
            "    refresh,",
            "});",
        ],
        "description": "pager snippets",
    },
}
App.vue
New file
@@ -0,0 +1,32 @@
<script setup lang="ts">
import { useStore } from "/@/cool";
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
onLaunch(() => {
    console.log("App Launch");
    const { dict, user } = useStore();
    // 获取字典
    dict.refresh();
    if (user.token) {
        // 获取登录用户信息
        user.get();
    }
});
onShow(() => {
    console.log("App Show");
});
onHide(() => {
    console.log("App Hide");
});
</script>
<style lang="scss">
@import "/static/css/iconfont/index.scss";
@import "/$/cool-ui/index.scss";
@import "/@/static/css/index.scss";
</style>
LICENSE
New file
@@ -0,0 +1,33 @@
MIT License
Copyright (c) [2025] [厦门闪酷科技开发有限公司]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
MIT 许可证
版权所有 (c) [2025] [厦门闪酷科技开发有限公司]
特此免费授予获得本软件及相关文档文件(“软件”)副本的任何人无限制地处理本软件的权限,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件的副本,并允许软件提供给其的人员这样做,但须符合以下条件:
上述版权声明和本许可声明应包含在软件的所有副本或主要部分中。
本软件按“原样”提供,不提供任何明示或暗示的担保,包括但不限于对适销性、特定用途适用性和非侵权的担保。在任何情况下,作者或版权持有人均不对因软件或软件使用或其他交易而产生的任何索赔、损害或其他责任负责,无论是在合同诉讼、侵权诉讼或其他诉讼中。
README.md
@@ -1,4 +1,112 @@
## cool-admin-uniapp
# COOL-UNI 项目脚手架
京申-小程序
## 简介
[演示、文档地址](https://uni-docs.cool-js.com/)
## 更快
#### 启动快
基于 `vite`,快速的冷启动,不需要等待打包,即时的热模块更新,真正的按需编译。
#### 开发快
新增 `eps` 模式,自动扫描接口,代码智能提示。
<img src="https://uni-docs.cool-js.com/images/service-tip.gif" height="300px" />
#### 对接快
有什么功能是前端一个人做不了??大不了全干了
👉👉 [管理前端(vue3)开发文档、强大的 CRUD 组件](https://cool-js.com/admin/vue/introduce.html#%E4%BB%A3%E7%A0%81%E4%BB%93%E5%BA%93)
👉👉 [服务端(node、midway)开发文档、一键生成代码](https://cool-js.com/admin/node/introduce.html#%E4%BB%A3%E7%A0%81%E4%BB%93%E5%BA%93)
👉👉 [演示地址](https://show.cool-admin.com) 😁
    <img src="https://vue.cool-admin.com/show/admin.jpg" width="500px" />
## 更强
内置请求、路由、文件上传、组件通信、缓存等方法及 ui 库和 hooks
```html
<script lang="ts" setup>
    import { useCool } from "/@/cool";
    import { useUi } from "/$/cool-ui";
    const { service, router, storage, upload } = useCool();
    const ui = useUi();
    // 请求
    service.test.page().then((res) => {
        consoe.log(res);
    });
    // 跳转
    router.push({
        path: "/pages/goods/info",
        query: {
            id: 1,
        },
    });
    // 全局事件
    ui.showLoading();
    ui.showToast();
    // 储存
    storage.set("token", "a123huis");
    // 文件上传
    uni.chooseImage({
        count: 1,
        sourceType: ["album", "camera"],
        success(res) {
            upload(res.tempFiles[0]).then((url) => {
                console.log(url);
            });
        },
    });
</script>
```
## 更全
#### 细腻的代码
-   `service` 无感刷新,直接调用后端接口
    ```ts
    const { service } = useCool();
    ```
-   提供 `entity` 描述,写 `any` 和不写的都哭了
    ```ts
    const list = ref<Eps.UserInfoEntity[]>([]);
    ```
#### 活跃的社区
-   拥有自己的知识库系统
-   [官方有问必答](https://cool-js.com/help/list.html)
#### 丰富的插件
-   [Ai 智能模块](https://cool-js.com/plugin/detail.html?id=58)
-   [客服聊天模块](https://cool-js.com/plugin/detail.html?id=56)
-   [企业机器人](https://cool-js.com/plugin/detail.html?id=41)、[飞书推送](https://cool-js.com/plugin/detail.html?id=30)
-   [各厂商的支付模块](https://cool-js.com/plugin/detail.html?id=33)
-   [云存储](https://cool-js.com/plugin/detail.html?id=36)
-   [PDF 打印](https://cool-js.com/plugin/detail.html?id=44)
-   [更多](https://cool-js.com/plugin/list.html)
androidPrivacy.json
New file
@@ -0,0 +1,3 @@
{
    "prompt" : "template"
}
components/address/item.vue
New file
@@ -0,0 +1,71 @@
<template>
    <view
        class="address-item"
        :style="{
            backgroundColor,
        }"
    >
        <view class="address-item__inner">
            <template v-if="item">
                <cl-text block color="info" :size="24"
                    >{{ item?.province }}{{ item?.city }}{{ item?.district }}</cl-text
                >
                <cl-text block bold :size="30" :line-height="1.2" :margin="[14, 0, 14, 0]">{{
                    item?.address
                }}</cl-text>
                <cl-row type="flex">
                    <cl-text>{{ item?.contact }}</cl-text>
                    <cl-text type="phone" :margin="[0, 20, 0, 100]" :value="item?.phone" />
                    <cl-tag size="small" type="primary" round v-if="item?.isDefault"
                        >默认地址</cl-tag
                    >
                </cl-row>
            </template>
            <template v-else>
                <cl-text block bold :size="30" :margin="[6, 0, 14, 0]">选择地址</cl-text>
                <cl-text color="info" :size="24">设置默认地址后优先使用</cl-text>
            </template>
        </view>
        <view class="address-item__icon">
            <slot name="icon"> </slot>
        </view>
    </view>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
const props = defineProps({
    item: Object as PropType<Eps.UserAddressEntity>,
    backgroundColor: {
        type: String,
        default: "#ffffff",
    },
});
</script>
<style lang="scss" scoped>
.address-item {
    display: flex;
    box-sizing: border-box;
    width: 100%;
    border-radius: 24rpx;
    padding: 24rpx;
    &__inner {
        flex: 1;
    }
    &__icon {
        display: flex;
        align-items: center;
        justify-content: center;
        height: initial;
        font-size: 32rpx;
        margin-left: 24rpx;
    }
}
</style>
components/address/select.vue
New file
@@ -0,0 +1,150 @@
<template>
    <view class="select-address" @tap="open()">
        <address-item :item="data || address.info">
            <template #icon>
                <text class="cl-icon-arrow-right" v-if="!disabled"></text>
            </template>
        </address-item>
    </view>
    <cl-popup
        v-model="visible"
        direction="bottom"
        :border-radius="[32, 32, 0, 0]"
        :padding="0"
        show-close-btn
    >
        <view class="select-address__popup">
            <cl-text :size="30" block bold :margin="[30, 0, 30, 30]">选择地址</cl-text>
            <cl-loading-mask :loading="loading">
                <scroll-view scroll-y class="scroller">
                    <view class="list">
                        <view
                            class="item"
                            v-for="item in list"
                            :key="item.id"
                            :class="{
                                'is-active': item.id == address.info?.id,
                            }"
                            @tap="select(item)"
                        >
                            <address-item :item="item" background-color="#f7f7f7">
                                <template #icon>
                                    <text class="cl-icon-check-border"></text>
                                </template>
                            </address-item>
                        </view>
                    </view>
                    <cl-empty icon="address" :fixed="false" text="暂无地址" v-if="isEmpty(list)" />
                </scroll-view>
            </cl-loading-mask>
            <cl-footer :fixed="false" :vt="[visible]">
                <cl-button custom type="primary" @tap="router.push('/pages/user/address-edit')"
                    >添加收货地址</cl-button
                >
            </cl-footer>
        </view>
    </cl-popup>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useAddress } from "/@/hooks";
import { useCool } from "/@/cool";
import { onShow } from "@dcloudio/uni-app";
import AddressItem from "./item.vue";
import { isEmpty } from "lodash-es";
const props = defineProps({
    disabled: Boolean,
    data: Object,
});
const { service, router } = useCool();
const address = useAddress();
const visible = ref(false);
const loading = ref(false);
// 地址列表
const list = ref<Eps.UserAddressEntity[]>([]);
// 获取地址
async function refresh() {
    loading.value = true;
    await service.user.address.list().then((res) => {
        list.value = res;
    });
    loading.value = false;
}
function select(item: Eps.UserAddressEntity) {
    address.set(item);
    close();
}
function open() {
    if (!props.disabled) {
        visible.value = true;
    }
}
function close() {
    visible.value = false;
}
onShow(() => {
    refresh();
    address.getDefault();
});
defineExpose({
    open,
    close,
});
</script>
<style lang="scss" scoped>
.select-address {
    margin-bottom: 24rpx;
    &__popup {
        .scroller {
            height: 50vh;
            .list {
                padding: 0 32rpx;
                .item {
                    border: 2rpx solid #f7f7f7;
                    border-radius: 24rpx;
                    margin-bottom: 24rpx;
                    .cl-icon-check-border {
                        display: none;
                        color: $cl-color-primary;
                        font-size: 46rpx;
                    }
                    &.is-active {
                        border-color: $cl-color-primary;
                        .cl-icon-check-border {
                            display: block;
                        }
                    }
                    &:last-child {
                        margin-bottom: 0;
                    }
                }
            }
        }
    }
}
</style>
components/agree-btn.vue
New file
@@ -0,0 +1,61 @@
<template>
    <cl-checkbox :size="34" v-model="agree" round>
        <view class="agree-btn">
            {{ $t("已阅读并同意") }}
            <text @tap.stop="toDoc('用户协议', 'userAgreement')">{{ $t("用户协议") }}</text>
            {{ $t("和") }}
            <text @tap.stop="toDoc('隐私政策', 'privacyPolicy')">{{ $t("隐私政策") }}</text>
        </view>
    </cl-checkbox>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useCool } from "/@/cool";
import { useUi } from "/$/cool-ui";
import { useI18n } from "vue-i18n";
const { router } = useCool();
const ui = useUi();
const { t } = useI18n();
const agree = ref(false);
function toDoc(title: string, key: string) {
    router.push({
        path: "/pages/user/doc",
        query: {
            title,
            key,
        },
    });
}
function check() {
    if (!agree.value) {
        ui.showToast(t("请先勾选同意后再进行登录"));
    }
    return agree.value;
}
defineExpose({
    check,
});
</script>
<style lang="scss" scoped>
.agree-btn {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    color: #999999;
    letter-spacing: 1rpx;
    text {
        color: $cl-color-primary;
        padding: 0 10rpx;
    }
}
</style>
components/coupon/get.vue
New file
@@ -0,0 +1,166 @@
<template>
    <view class="coupon-get__btns">
        <cl-skeleton
            v-for="item in list"
            :key="item.id"
            :margin="[0, 10, 0, 0]"
            :height="46"
            :loading-style="{
                width: '150rpx',
            }"
            :loading="loading"
        >
            <view class="item" @tap="open">
                <text class="shop-icon-coupon"></text>
                <text>{{ item.text }}</text>
            </view>
        </cl-skeleton>
    </view>
    <cl-popup
        v-model="visible"
        direction="bottom"
        :border-radius="[32, 32, 0, 0]"
        :padding="0"
        title="领取优惠券"
        show-close-btn
    >
        <view class="coupon-get">
            <scroll-view class="scroller" scroll-y>
                <view class="list">
                    <coupon-item :item="item" v-for="item in list" :key="item.id">
                        <template #btn>
                            <cl-button
                                type="primary"
                                :height="46"
                                :width="116"
                                :font-size="22"
                                :disabled="!!item.userId"
                                size="small"
                                :margin="[0, 0, 0, 20]"
                                @tap="toGet(item)"
                            >
                                {{ item.userId ? "已领取" : "立即领取" }}
                            </cl-button>
                        </template>
                    </coupon-item>
                </view>
                <cl-empty :fixed="false" :margin="[-40, 0, 0, 0]" v-if="isEmpty(list)" />
            </scroll-view>
        </view>
    </cl-popup>
</template>
<script lang="ts" setup>
import { onMounted, ref } from "vue";
import { useCool } from "/@/cool";
import { useUi } from "/$/cool-ui";
import CouponItem from "./item/index.vue";
import { isEmpty } from "lodash-es";
const emit = defineEmits(["success"]);
const { service } = useCool();
const ui = useUi();
// 是否可见
const visible = ref(false);
// 获取中
const loading = ref(false);
// 优惠券列表
const list = ref<CouponInfo[]>([{}, {}]);
// 获取列表
async function refresh() {
    await service.market.coupon.info.page({ page: 1, size: 100 }).then((res) => {
        list.value = res.list.map((e) => {
            switch (e.type) {
                case 0:
                    e.text = `满${e.condition?.fullAmount}减${Math.floor(e.amount!)}元`;
                    break;
                default:
                    e.text = "";
                    break;
            }
            return e;
        });
    });
}
// 打开弹窗
function open() {
    visible.value = true;
    refresh();
}
// 关闭弹窗
function close() {
    visible.value = false;
}
// 领券
async function toGet(item: CouponInfo) {
    item.userId = true;
    service.market.coupon.user
        .receive({
            couponId: item.id,
        })
        .then(() => {
            ui.showToast("领取成功");
        })
        .catch((err) => {
            ui.showToast(err.message);
            item.userId = null;
        });
}
onMounted(async () => {
    loading.value = true;
    await refresh();
    loading.value = false;
});
defineExpose({
    open,
    close,
});
</script>
<style lang="scss" scoped>
.coupon-get {
    &__btns {
        display: flex;
        flex-wrap: wrap;
        .item {
            display: inline-flex;
            align-items: center;
            padding: 0 6rpx;
            height: 100%;
            border: 2rpx solid $cl-color-primary;
            box-sizing: border-box;
            border-radius: 8rpx;
            font-size: 22rpx;
            line-height: 1;
            .shop-icon-coupon {
                font-size: 36rpx;
                margin-right: 2rpx;
            }
        }
    }
    .scroller {
        height: 60vh;
        .list {
            padding: 0 32rpx;
        }
    }
}
</style>
components/coupon/item/bg.png
components/coupon/item/index.vue
New file
@@ -0,0 +1,168 @@
<template>
    <view
        class="coupon-item"
        :class="{
            'is-disabled': info.useStatus == 1 || disabled,
        }"
    >
        <image :src="Bg" class="bg" />
        <view class="a">
            <text class="amount">{{ info.amount }}</text>
            <text class="doc">{{ doc }}</text>
        </view>
        <view class="b">
            <view class="top">
                <view class="text">
                    <cl-text :ellipsis="1" bold :size="30" :margin="[0, 0, 10, 0]">{{
                        info.title
                    }}</cl-text>
                    <cl-text :ellipsis="1" color="info" :size="24">{{ info.description }}</cl-text>
                </view>
                <slot name="btn"></slot>
            </view>
            <view class="bottom">
                <text class="time">有效期:{{ info.time }}</text>
            </view>
        </view>
        <view class="status" v-if="info.useStatus == 1">已使用</view>
        <view class="status" v-if="checked">使用中</view>
    </view>
</template>
<script lang="ts" setup>
import { computed, type PropType } from "vue";
import Bg from "./bg.png";
import dayjs from "dayjs";
const props = defineProps({
    item: {
        type: Object as PropType<CouponInfo>,
        default: () => ({}),
    },
    checked: Boolean,
    disabled: Boolean,
});
// 详情
const info = computed(() => {
    return {
        ...props.item,
        amount: Math.floor(props.item.amount!),
        useStatus: props.item.useStatus,
        time:
            dayjs(props.item.startTime).format("YYYY-MM-DD") +
            " ~ " +
            dayjs(props.item.endTime).format("YYYY-MM-DD"),
    };
});
// 使用说明
const doc = computed(() => {
    const { type, condition } = props.item || {};
    switch (type) {
        case 0:
            return `满${condition.fullAmount}可用`;
    }
});
</script>
<style lang="scss" scoped>
.coupon-item {
    display: flex;
    height: 200rpx;
    position: relative;
    margin-bottom: 24rpx;
    overflow: hidden;
    .bg {
        height: 100%;
        width: 100%;
        position: absolute;
    }
    .status {
        transform: rotate(45deg);
        height: 46rpx;
        width: 200rpx;
        background-color: #2b2e3d;
        position: absolute;
        right: -60rpx;
        top: 20rpx;
        display: flex;
        align-items: center;
        justify-content: center;
        color: #fff;
        font-size: 22rpx;
        line-height: 46rpx;
    }
    .a {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        position: relative;
        color: #fff;
        height: 100%;
        width: 172rpx;
        margin-left: 10rpx;
        padding: 0 20rpx;
        box-sizing: border-box;
        .amount {
            font-size: 50rpx;
            font-weight: 500;
            margin-bottom: 10rpx;
            line-height: 1;
            &::before {
                content: "¥";
                font-size: 38rpx;
                position: relative;
                top: -1rpx;
            }
        }
        .doc {
            font-size: 22rpx;
            color: #ddd;
        }
    }
    .b {
        display: flex;
        flex-direction: column;
        flex: 1;
        position: relative;
        padding: 0 20rpx;
        .top {
            display: flex;
            align-items: center;
            flex: 1;
        }
        .bottom {
            display: flex;
            align-items: center;
            border-top: 1rpx solid #ddd;
            height: 54rpx;
            .time {
                font-size: 22rpx;
                color: #999;
            }
        }
    }
    &.is-disabled {
        opacity: 0.4;
    }
}
</style>
components/coupon/select.vue
New file
@@ -0,0 +1,134 @@
<template>
    <view class="coupon-select__inner" @tap="open">
        <template v-if="checked">
            <cl-text :size="28" color="error">-</cl-text>
            <cl-text :size="28" type="price" color="error" :value="checked.amount" />
        </template>
        <cl-text color="info" v-else>选择优惠券</cl-text>
    </view>
    <cl-popup
        v-model="visible"
        direction="bottom"
        :border-radius="[32, 32, 0, 0]"
        :padding="0"
        title="选择优惠券"
        show-close-btn
    >
        <view class="coupon-select">
            <scroll-view class="scroller" scroll-y>
                <view class="list">
                    <view class="item" v-for="item in checkList" :key="item.id" @tap="select(item)">
                        <coupon-item
                            :item="item"
                            :checked="checked?.id == item.id"
                            :disabled="item.disabled"
                        />
                    </view>
                </view>
                <cl-empty :fixed="false" :margin="[-40, 0, 0, 0]" v-if="isEmpty(list)" />
            </scroll-view>
        </view>
    </cl-popup>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue";
import { useCool } from "/@/cool";
import { isEmpty } from "lodash-es";
import CouponItem from "./item/index.vue";
import { useUi } from "/$/cool-ui";
const props = defineProps({
    totalAmount: {
        type: Number,
        default: 0,
    },
});
const emit = defineEmits(["success"]);
const { service } = useCool();
const ui = useUi();
// 是否可见
const visible = ref(false);
// 选中值
const checked = ref<CouponInfo>();
// 优惠券列表
const list = ref<CouponInfo[]>();
// 待选列表
const checkList = computed(() => {
    return list.value?.map((e) => {
        let disabled = true;
        switch (e.type) {
            case 0:
                if (props.totalAmount >= e.condition?.fullAmount!) {
                    disabled = false;
                }
                break;
        }
        return {
            ...e,
            disabled,
        };
    });
});
// 打开弹窗
function open() {
    visible.value = true;
    // 获取未使用的优惠券
    service.market.coupon.user.page({ page: 1, size: 100, status: 0 }).then((res) => {
        list.value = res.list;
    });
}
// 关闭弹窗
function close() {
    visible.value = false;
}
// 选择
function select(item: CouponInfo) {
    if (item.disabled) {
        ui.showToast("优惠券不可用");
    } else {
        if (checked.value?.id == item.id) {
            checked.value = undefined;
        } else {
            checked.value = item;
        }
        close();
    }
}
defineExpose({
    open,
    close,
    checked,
});
</script>
<style lang="scss" scoped>
.coupon-select {
    height: 60vh;
    .scroller {
        height: 100%;
        .list {
            padding: 0 32rpx;
        }
    }
}
</style>
components/goods/cover.vue
New file
@@ -0,0 +1,96 @@
<template>
    <cl-image
        :src="resizeImage(current, 300)"
        :radius="radius"
        :size="size"
        :background-color="backgroundColor"
        @tap="toGoods"
    >
        <view class="zoom" @tap.stop="preview()" v-if="zoom">
            <text class="shop-icon-zoom"></text>
        </view>
    </cl-image>
</template>
<script lang="ts" setup>
import { isEmpty } from "lodash-es";
import { computed, type PropType } from "vue";
import { useCool, useUpload } from "/@/cool";
const props = defineProps({
    item: {
        type: Object as PropType<Eps.GoodsInfoEntity>,
        default: () => ({}),
    },
    spec: {
        type: Object as PropType<Eps.GoodsSpecEntity>,
        default: () => ({}),
    },
    size: [Number, String, Array],
    radius: {
        type: [Number, String],
        default: 24,
    },
    backgroundColor: String,
    link: {
        type: Boolean,
        default: true,
    },
    zoom: Boolean,
});
const { router } = useCool();
const { resizeImage } = useUpload();
const urls = computed(() => {
    if (props.spec) {
        const arr = props.spec.images || [];
        if (!isEmpty(arr)) {
            return arr;
        }
    }
    return [props.item.mainPic];
});
const current = computed(() => urls.value[0]);
// 商品详情
function toGoods() {
    if (props.link) {
        router.push({
            path: "/pages/goods/detail",
            query: {
                id: props.item.id,
                specId: props.spec?.id,
            },
        });
    }
}
// 大图预览
function preview() {
    uni.previewImage({
        urls: urls.value,
        current: current.value,
    });
}
</script>
<style lang="scss" scoped>
.zoom {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    right: 8rpx;
    top: 8rpx;
    height: 30rpx;
    width: 30rpx;
    background-color: rgba(60, 60, 60, 0.7);
    color: #fff;
    font-size: 18rpx;
    border-radius: 100%;
}
</style>
components/goods/group.vue
New file
@@ -0,0 +1,202 @@
<template>
    <view
        class="goods-group"
        :class="{
            'is-border': border,
        }"
    >
        <view class="goods" v-for="goods in list" :key="goods.id">
            <!-- 名称 -->
            <view
                class="name"
                @tap="
                    router.push({
                        path: '/pages/goods/detail',
                        query: {
                            id: goods.id,
                        },
                    })
                "
            >
                <cl-text :ellipsis="1" :value="goods.name" bold />
                <cl-icon name="arrow-right" :size="20" :margin="[0, 0, 0, 6]" />
            </view>
            <!-- 规格 -->
            <view class="specs">
                <view class="item" v-for="item in goods.children" :key="item.id">
                    <view class="checkbox" v-if="showCheckbox">
                        <cl-checkbox round v-model="item.checked" />
                    </view>
                    <goods-cover :size="140" :item="item.goodsInfo" :spec="item.spec" />
                    <view class="det">
                        <cl-row>
                            <view class="spec" @tap="onSpec(item)">
                                <cl-text :size="24" :ellipsis="1">{{ item.spec?.name }}</cl-text>
                                <cl-icon
                                    name="arrow-bottom"
                                    :size="20"
                                    :margin="[0, 0, 0, 10]"
                                    v-if="specEdit"
                                />
                            </view>
                        </cl-row>
                        <view class="flex1" />
                        <cl-row type="flex" justify="space-between">
                            <cl-text
                                type="price"
                                color="error"
                                :size="36"
                                :value="item.spec?.price"
                            />
                            <slot name="count" :item="item">
                                <cl-text color="info" v-if="readonly">x{{ item.count }}</cl-text>
                                <cl-input-number
                                    v-model="item.count"
                                    :min="1"
                                    :max="item.spec?.stock"
                                    v-else
                                />
                            </slot>
                        </cl-row>
                    </view>
                </view>
            </view>
            <slot name="goods" :goods="goods"></slot>
        </view>
    </view>
</template>
<script lang="ts" setup>
import { computed, type PropType } from "vue";
import { useCool } from "/@/cool";
import GoodsCover from "/@/components/goods/cover.vue";
interface Item {
    id: number;
    name: string;
    children: OrderGoods[];
    [key: string]: any;
}
const props = defineProps({
    // 商品数据
    data: {
        type: Array as PropType<OrderGoods[]>,
        default: () => [],
    },
    // 能否编辑规格
    specEdit: Boolean,
    // 显示多选框
    showCheckbox: Boolean,
    // 带边框
    border: Boolean,
    // 是否只读
    readonly: Boolean,
});
const emit = defineEmits(["spec"]);
const { router } = useCool();
const list = computed(() => {
    const arr: Item[] = [];
    props.data
        .filter((e) => !!e)
        .forEach((e) => {
            const d = arr.find((a) => a.id == e.goodsInfo.id);
            if (d) {
                d.children?.push(e);
            } else {
                arr.push({
                    id: e.goodsInfo.id!,
                    name: e.goodsInfo.title!,
                    children: [e],
                });
            }
        });
    return arr;
});
function onSpec(item: OrderGoods) {
    if (props.specEdit) {
        emit("spec", item);
    }
}
defineExpose({
    list,
});
</script>
<style lang="scss" scoped>
.goods-group {
    .goods {
        background-color: #ffffff;
        border-radius: 24rpx;
        padding: 24rpx;
        margin-bottom: 24rpx;
        .name {
            display: flex;
            align-items: center;
            margin-bottom: 24rpx;
        }
        .specs {
            .item {
                display: flex;
                margin-bottom: 24rpx;
                .checkbox {
                    display: flex;
                    align-items: center;
                    width: 34rpx;
                    margin-right: 24rpx;
                    .cl-checkbox {
                        position: absolute;
                    }
                }
                .det {
                    display: flex;
                    flex-direction: column;
                    margin-left: 24rpx;
                    padding: 10rpx 0;
                    flex: 1;
                    .spec {
                        display: inline-flex;
                        align-items: center;
                        height: 46rpx;
                        padding: 0 16rpx;
                        border-radius: 10rpx;
                        background-color: $cl-color-bg;
                    }
                }
                &:last-child {
                    margin-bottom: 0;
                }
            }
        }
    }
    &.is-border {
        .goods {
            border: $cl-border-width solid $cl-border-color;
        }
    }
}
</style>
components/goods/item.vue
New file
@@ -0,0 +1,68 @@
<template>
    <view class="goods-item" @tap="toDetail">
        <cl-image
            mode="aspectFill"
            background-color="#fff"
            :size="[350, '100%']"
            :radius="[16, 16, 0, 0]"
            :src="resizeImage(item.mainPic!, 360)"
        />
        <view class="goods-item__det">
            <cl-text
                :value="item.title"
                block
                :margin="[0, 0, 16, 0]"
                :ellipsis="2"
                :line-height="1.4"
            />
            <cl-row>
                <cl-text type="price" color="#333" :size="34" :value="item.price" bold />
                <cl-text
                    color="info"
                    :size="22"
                    :margin="[0, 0, 0, 10]"
                    :value="`${item.sold || 0}件已售`"
                />
            </cl-row>
        </view>
    </view>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import { useCool, useUpload } from "/@/cool";
const props = defineProps({
    item: {
        type: Object as PropType<Eps.GoodsInfoEntity>,
        default: () => ({}),
    },
});
const { router } = useCool();
const { resizeImage } = useUpload();
function toDetail() {
    router.push({
        path: "/pages/goods/detail",
        query: {
            id: props.item.id,
        },
    });
}
</script>
<style lang="scss" scoped>
.goods-item {
    position: relative;
    background-color: #ffffff;
    border-radius: 16rpx;
    margin-bottom: 20rpx;
    .goods-item__det {
        padding: 20rpx;
    }
}
</style>
components/goods/spec.vue
New file
@@ -0,0 +1,393 @@
<template>
    <cl-popup
        v-model="spec.visible"
        direction="bottom"
        :border-radius="[32, 32, 0, 0]"
        :padding="0"
        show-close-btn
        @open="onOpen"
    >
        <view class="goods-spec">
            <view class="goods">
                <goods-cover :item="spec.goods" :spec="spec.info" :link="false" :size="160" />
                <view class="det">
                    <cl-row :margin="[0, 0, 16, 0]">
                        <cl-text
                            :value="spec.info?.price || spec.goods?.price"
                            type="price"
                            color="error"
                            :size="40"
                            bold
                        />
                        <cl-text
                            color="info"
                            :size="24"
                            :margin="[0, 0, 0, 30]"
                            :value="`库存:${stock}`"
                            v-if="spec.info"
                        />
                    </cl-row>
                    <cl-text
                        :ellipsis="2"
                        :line-height="1.2"
                        :color="spec.info ? 'primary' : 'info'"
                        :value="spec.text"
                    />
                </view>
            </view>
            <cl-row type="flex" justify="space-between" :margin="[0, 24, 16, 24]">
                <cl-text bold>规格分类({{ list.length + 1 }})</cl-text>
                <cl-icon
                    :size="40"
                    :class-name="`${isGrid ? 'shop-icon-list' : 'shop-icon-list2'}`"
                    @tap="isGrid = !isGrid"
                />
            </cl-row>
            <!-- 规格列表 -->
            <cl-loading-mask :loading="loading">
                <scroll-view scroll-y class="scroller">
                    <view
                        class="list"
                        :class="{
                            'is-grid': isGrid,
                        }"
                    >
                        <cl-row :gutter="20">
                            <cl-col
                                v-for="(item, label) in list"
                                :key="label"
                                :span="isGrid ? 8 : 24"
                            >
                                <view
                                    class="item"
                                    :class="{
                                        'is-active': spec.info?.id == item.id,
                                        'is-disabled': !item.stock,
                                    }"
                                    @tap="select(item)"
                                >
                                    <view class="cover">
                                        <goods-cover
                                            :item="spec.goods"
                                            :spec="item"
                                            :link="false"
                                            :radius="isGrid ? 24 : 12"
                                            :zoom="isGrid"
                                        />
                                    </view>
                                    <view class="name">
                                        <text>{{ item.name }}</text>
                                    </view>
                                </view>
                            </cl-col>
                        </cl-row>
                        <cl-list-item
                            label="数量"
                            :arrow-icon="false"
                            :margin="[0, -10, 0, -10]"
                            v-if="spec.info"
                        >
                            <cl-input-number v-model="spec.num" :min="1" :max="stock" />
                        </cl-list-item>
                    </view>
                </scroll-view>
            </cl-loading-mask>
            <!-- 底部按钮 -->
            <cl-footer :fixed="false" :vt="[spec.action]">
                <!-- 加入购物车 -->
                <template v-if="spec.action == 'spCart'">
                    <cl-button custom type="primary" :disabled="disabled" @tap="toAdd"
                        >确定</cl-button
                    >
                </template>
                <!-- 选规格的时候 -->
                <template v-else-if="spec.action == 'select'">
                    <cl-button custom :disabled="disabled" @tap="toAdd">加入购物车</cl-button>
                    <cl-button custom type="primary" :disabled="disabled" @tap="toBuy"
                        >立即购买</cl-button
                    >
                </template>
                <!-- 立即购买 -->
                <template v-else-if="spec.action == 'buy'">
                    <cl-button custom type="primary" :disabled="disabled" @tap="toBuy"
                        >立即购买</cl-button
                    >
                </template>
                <!-- 编辑 -->
                <template v-else-if="spec.action == 'edit'">
                    <cl-button custom type="primary" :disabled="disabled" @tap="toEdit"
                        >确定</cl-button
                    >
                </template>
            </cl-footer>
        </view>
    </cl-popup>
</template>
<script lang="ts" setup>
import { isEmpty } from "lodash-es";
import { computed, nextTick, onUnmounted, ref } from "vue";
import { useCool } from "/@/cool";
import { useSpec } from "/@/hooks";
import { useShoppingCart } from "/@/hooks";
import { useUi } from "/$/cool-ui";
import GoodsCover from "/@/components/goods/cover.vue";
const { service, router } = useCool();
const spec = useSpec();
const spCart = useShoppingCart();
const ui = useUi();
// 列表是否格子布局
const isGrid = ref(true);
// 加载规格中
const loading = ref(false);
// 规格列表
const list = ref<Eps.GoodsSpecEntity[]>([]);
// 库存数,减去购物车数量
const stock = computed(() => {
    const a = spec.info?.stock || 0;
    const b = spCart.list.find((e) => e.spec?.id == spec.info?.id)?.count || 0;
    if (spec.action == "edit") {
        return a;
    }
    let d = a - b;
    if (d < 0) {
        d = 0;
    }
    return d;
});
// 是否能下单
const disabled = computed(() => {
    return !stock.value;
});
// 获取规格列表
async function refresh(params?: any) {
    loading.value = true;
    await service.goods.spec
        .list({
            goodsId: spec.goods?.id,
            order: "sortNum",
            sort: "desc",
            ...params,
        })
        .then((res) => {
            list.value = res.map((e) => {
                let arr = e.images || [];
                if (isEmpty(arr)) {
                    arr = [spec.goods?.mainPic];
                }
                return {
                    ...e,
                    cover: arr[0],
                };
            });
            // 设置规格
            spec.set(list.value.find((e) => e.id == spec.specId));
        });
    loading.value = false;
}
// 监听打开
function onOpen() {
    refresh();
}
// 选择规格
function select(item: Eps.GoodsSpecEntity) {
    if (spec.info?.id == item.id) {
        spec.set();
    } else {
        spec.set(item);
    }
    nextTick(() => {
        spec.setNum(1);
    });
}
// 购买
function toBuy() {
    spec.close();
    router.push({
        path: "/pages/order/submit",
        params: {
            buyList: [
                {
                    spec: spec.info!,
                    goodsInfo: spec.goods!,
                    count: spec.num,
                },
            ],
        },
    });
}
// 编辑
function toEdit() {
    spec.emit("edit");
    spec.close();
}
// 加入购物车
function toAdd() {
    spCart.add({
        count: spec.num,
        spec: spec.info!,
        goodsInfo: spec.goods!,
    });
    ui.showToast("添加购物车成功");
    spec.close();
}
onUnmounted(() => {
    spec.clear();
    spec.close();
});
defineExpose({
    refresh,
});
</script>
<style lang="scss" scoped>
.goods-spec {
    .goods {
        display: flex;
        padding: 24rpx;
        .det {
            flex: 1;
            padding: 24rpx 24rpx 0 24rpx;
        }
    }
    .scroller {
        height: 50vh;
        .list {
            padding: 0 24rpx;
            .item {
                display: flex;
                align-items: center;
                margin-bottom: 16rpx;
                border-radius: 12rpx;
                border: 2rpx solid #eeeeee;
                padding: 12rpx;
                .cover {
                    height: 80rpx;
                    width: 80rpx;
                    flex-shrink: 0;
                    position: relative;
                }
                .name {
                    display: flex;
                    align-items: center;
                    padding: 0 16rpx;
                    font-size: 24rpx;
                    line-height: 1.4;
                    flex: 1;
                    text {
                        word-break: break-all;
                        overflow: hidden;
                        text-overflow: ellipsis;
                        display: -webkit-box;
                        -webkit-box-orient: vertical;
                        -webkit-line-clamp: 2;
                    }
                }
                &.is-active {
                    border-color: $cl-color-primary;
                }
                &.is-disabled {
                    .cover {
                        &::after {
                            display: flex;
                            align-items: center;
                            justify-content: center;
                            height: 100rpx;
                            width: 100rpx;
                            content: "无货";
                            color: #fff;
                            background-color: rgba(50, 50, 50, 0.6);
                            border-radius: 100%;
                            position: absolute;
                            left: calc(50% - 50rpx);
                            top: calc(50% - 50rpx);
                            font-size: 26rpx;
                            border: 1rpx solid currentColor;
                            box-sizing: border-box;
                            transform: scale(0.6);
                        }
                    }
                    .name {
                        color: $cl-color-disabled;
                    }
                }
            }
            &.is-grid {
                .item {
                    display: flex;
                    flex-direction: column;
                    align-items: center;
                    justify-content: center;
                    width: 100%;
                    border-radius: 28rpx;
                    padding: 0;
                    margin-bottom: 24rpx;
                    .cover {
                        height: 216rpx;
                        width: 100%;
                        &::after {
                            transform: scale(1);
                        }
                    }
                    .name {
                        flex: none;
                        height: 70rpx;
                        line-height: 1.2;
                    }
                }
            }
        }
    }
}
</style>
components/sms-btn.vue
New file
@@ -0,0 +1,231 @@
<template>
    <view class="sms-btn">
        <slot :disabled="isDisabled" :countdown="countdown" :btnText="btnText">
            <cl-button
                :border="false"
                background-color="transparent"
                color="#FE6B03"
                :height="height"
                :font-size="fontSize"
                fill
                :size="size"
                :disabled="isDisabled"
                @tap="open"
            >
                {{ btnText }}
            </cl-button>
        </slot>
        <cl-popup
            v-model="captcha.visible"
            :ref="setRefs('popup')"
            :padding="40"
            border-radius="24rpx"
        >
            <cl-loading-mask :loading="captcha.loading">
                <view class="sms-popup">
                    <view class="head">
                        <cl-text bold :size="28" :value="$t('获取短信验证码')"></cl-text>
                        <cl-icon :size="32" name="close" @tap="close"></cl-icon>
                    </view>
                    <view class="row">
                        <cl-input
                            v-model="form.code"
                            :placeholder="$t('验证码')"
                            :maxlength="4"
                            :height="70"
                            :clearable="false"
                            :focus="refs.popup?.isFocus"
                            :border="false"
                            background-color="#f7f7f7"
                            @confirm="send"
                        />
                        <image :src="captcha.img" mode="aspectFit" @tap="getCaptcha" />
                    </view>
                    <cl-button
                        type="primary"
                        fill
                        :disabled="!form.code"
                        :loading="captcha.sending"
                        :height="70"
                        @tap="send"
                    >
                        {{ $t("发送短信") }}
                    </cl-button>
                </view>
            </cl-loading-mask>
        </cl-popup>
    </view>
</template>
<script lang="ts" setup>
import { computed, type PropType, reactive, ref } from "vue";
import { useCool } from "../cool";
import { useUi } from "/$/cool-ui";
import { useI18n } from "vue-i18n";
const props = defineProps({
    phone: String,
    type: String,
    height: Number,
    fontSize: Number,
    size: String as PropType<"large" | "default" | "small">,
    border: {
        type: Boolean,
        default: true,
    },
    plain: Boolean,
});
const emit = defineEmits(["success"]);
const { service, refs, setRefs } = useCool();
const ui = useUi();
const { t } = useI18n();
// 验证码
const captcha = reactive({
    visible: false,
    loading: false,
    sending: false,
    img: "",
});
// 倒计时
const countdown = ref(0);
// 是否禁用
const isDisabled = computed(() => countdown.value > 0 || !props.phone);
// 按钮文案
const btnText = computed(() =>
    countdown.value > 0 ? t("{n}s后重新获取", { n: countdown.value }) : t("获取验证码")
);
// 表单
const form = reactive({
    code: "",
    captchaId: "",
});
// 开始倒计时
function startCountdown() {
    countdown.value = 60;
    function fn() {
        countdown.value--;
        if (countdown.value < 1) {
            clearInterval(timer);
        }
    }
    const timer = setInterval(fn, 1000);
    fn();
}
// 发送短信
async function send() {
    if (form.code) {
        captcha.sending = true;
        await service.user.login
            .smsCode({
                phone: props.phone,
                ...form,
            })
            .then(() => {
                ui.showToast(t("短信已发送,请查收"));
                startCountdown();
                close();
                emit("success");
            })
            .catch((err) => {
                ui.showToast(err.message);
                getCaptcha();
            });
        captcha.sending = false;
    } else {
        ui.showToast(t("请填写验证码"));
    }
}
// 获取图片验证码
async function getCaptcha() {
    clear();
    captcha.loading = true;
    await service.user.login
        .captcha({ color: "#2c3142", phone: props.phone })
        .then((res) => {
            form.captchaId = res.captchaId;
            captcha.img = res.data;
        })
        .catch((err) => {
            ui.showToast(err.message);
        });
    captcha.loading = false;
}
// 打开
function open() {
    if (props.phone) {
        if (/^(?:(?:\+|00)86)?1[3-9]\d{9}$/.test(props.phone)) {
            captcha.visible = true;
            getCaptcha();
        } else {
            ui.showToast(t("请填写正确的手机号格式"));
        }
    }
}
// 关闭
function close() {
    captcha.visible = false;
    clear();
}
// 清空
function clear() {
    form.code = "";
    form.captchaId = "";
}
defineExpose({
    open,
    send,
    getCaptcha,
    startCountdown,
});
</script>
<style lang="scss" scoped>
.sms-popup {
    width: 400rpx;
    .head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        margin-bottom: 30rpx;
    }
    .row {
        display: flex;
        align-items: center;
        margin-bottom: 30rpx;
        image {
            height: 70rpx;
            width: 200rpx;
            flex-shrink: 0;
        }
    }
}
</style>
config/dev.ts
New file
@@ -0,0 +1,17 @@
import { host, value } from "./proxy";
export default {
    // 根地址
    host,
    // 请求地址
    get baseUrl() {
        // #ifdef H5
        return `/${value}`;
        // #endif
        // #ifndef H5
        return this.host + "";
        // #endif
    },
};
config/index.ts
New file
@@ -0,0 +1,36 @@
import dev from "./dev";
import prod from "./prod";
// 是否开发模式
export const isDev = import.meta.env.MODE === "development";
// 代理环境
const proxy = isDev ? dev : prod;
// 配置
export const config = {
    // 应用信息
    app: {
        // 应用名称
        name: "再生资源",
        // 应用描述
        desc: "再生资源回收利用",
        // 页面配置
        pages: {
            login: "/pages/user/login",
        },
        // 微信配置
        wx: {
            debug: false,
        },
    },
    // 忽略
    ignore: {
        token: [],
    },
    ...proxy,
};
export * from "./proxy";
config/prod.ts
New file
@@ -0,0 +1,17 @@
import { host } from "./proxy";
export default {
    // 根地址
    host,
    // 请求地址
    get baseUrl() {
        // #ifdef H5
        return "/api";
        // #endif
        // #ifndef H5
        return this.host + "/api";
        // #endif
    },
};
config/proxy.ts
New file
@@ -0,0 +1,18 @@
const proxy = {
    "/dev/": {
        target: "http://127.0.0.1:8001",
        changeOrigin: true,
        rewrite: (path: string) => path.replace(/^\/dev/, ""),
    },
    "/prod/": {
        target: "https://show.cool-admin.com",
        changeOrigin: true,
        rewrite: (path: string) => path.replace(/^\/prod/, "/api"),
    },
};
const value = "dev";
const host = proxy[`/${value}/`]?.target;
export { proxy, host, value };
cool/bootstrap/eps.ts
New file
@@ -0,0 +1,91 @@
import { merge } from "lodash-es";
import { BaseService, service } from "../service";
import { path2Obj } from "../utils";
import { isDev } from "/@/config";
import { eps } from "virtual:eps";
// 读取本地所有 service
const files = import.meta.glob("/service/**/*", {
    eager: true,
});
// 数据集合
const services: any[] = [];
// 取值
for (const i in files) {
    try {
        // @ts-ignore
        services.push(new files[i].default());
    } catch (e) {
        console.error(`[service] ${i} error: `, e);
    }
}
export function createEps() {
    // 设置 request 方法
    function set(d: any) {
        if (d.namespace) {
            const a: any = new BaseService(d.namespace);
            for (const i in d) {
                const { path, method = "get" } = d[i];
                if (path) {
                    a.request = a.request;
                    a[i] = function (data?: any) {
                        return this.request({
                            url: path,
                            method,
                            [method.toLocaleLowerCase() == "post" ? "data" : "params"]: data,
                        });
                    };
                }
            }
            for (const i in a) {
                d[i] = a[i];
            }
        } else {
            for (const i in d) {
                set(d[i]);
            }
        }
    }
    // 遍历每一个方法
    set(eps.service);
    // 合并 eps
    merge(service, eps.service);
    // 合并[local]
    merge(
        service,
        path2Obj(
            services.map((e) => {
                return {
                    path: (e.namespace || "").replace("app/", ""),
                    value: e,
                };
            })
        )
    );
    // 提示
    if (isDev) {
        console.log("[cool-eps] updated");
    }
}
// 监听 vite 触发事件
if (import.meta.hot) {
    import.meta.hot.on("eps-update", ({ service }) => {
        if (service) {
            eps.service = service;
        }
        createEps();
    });
}
cool/bootstrap/index.ts
New file
@@ -0,0 +1,15 @@
import { createPinia } from "pinia";
import { createEps } from "./eps";
import { createModules } from "./modules";
import { type App } from "vue";
export async function bootstrap(app: App) {
    // 状态共享存储
    app.use(createPinia());
    // 创建 EPS
    createEps();
    // 创建 uni_modules
    createModules();
}
cool/bootstrap/modules.ts
New file
@@ -0,0 +1,38 @@
import { keys, orderBy } from "lodash-es";
import { module } from "../module";
export async function createModules() {
    // 加载 uni_modules 插件
    const files: any = import.meta.glob("/uni_modules/cool-*/config.ts", {
        eager: true,
    });
    const modules = orderBy(
        keys(files).map((k) => {
            const [, , name] = k.split("/");
            return {
                name,
                value: files[k]?.default,
            };
        }),
        "order",
        "desc",
    );
    for (let i in modules) {
        const { name, value } = modules[i];
        const data = value ? value() : undefined;
        // 添加模块
        module.add({
            name,
            ...data,
        });
        // 触发加载事件
        if (data) {
            await data.onLoad?.(data.options);
        }
    }
}
cool/hooks/app.ts
New file
@@ -0,0 +1,28 @@
import { reactive, ref } from "vue";
import { storage } from "../utils";
import { config } from "/@/config";
import { defineStore } from "pinia";
// 主题
export const useTheme = defineStore("theme", () => {
    const name = ref(storage.get("theme") || "default");
    function set(value: string) {
        name.value = value;
        storage.set("theme", value);
    }
    return {
        name,
        set,
    };
});
export function useApp() {
    const info = reactive(config.app);
    return {
        info,
        theme: useTheme(),
    };
}
cool/hooks/comm.ts
New file
@@ -0,0 +1,11 @@
import { reactive } from "vue";
export function useRefs() {
    const refs = reactive<{ [key: string]: any }>({});
    function setRefs(name: string) {
        return (el: any) => {
            refs[name] = el;
        };
    }
    return { refs, setRefs };
}
cool/hooks/hmr.ts
New file
@@ -0,0 +1,23 @@
// 解决热更新后失效问题;
const data = import.meta.hot?.data.getData?.() || {};
if (import.meta.hot) {
    import.meta.hot.data.getData = () => {
        return data;
    };
}
export const hmr = {
    data,
    setData(key: string, value: any) {
        data[key] = value;
    },
    getData(key: string, defaultValue?: any) {
        if (defaultValue !== undefined && !data[key]) {
            this.setData(key, defaultValue);
        }
        return data[key];
    }
};
cool/hooks/index.ts
New file
@@ -0,0 +1,21 @@
import { router } from "../router";
import { service } from "../service";
import { upload } from "../upload";
import { storage } from "../utils";
import { useRefs } from "./comm";
export function useCool() {
    return {
        router,
        service,
        upload,
        storage,
        ...useRefs(),
    };
}
export * from "./app";
export * from "./comm";
export * from "./hmr";
export * from "./pager";
export * from "./wx";
cool/hooks/pager.ts
New file
@@ -0,0 +1,180 @@
import { computed, getCurrentInstance, onUnmounted, reactive } from "vue";
import { onPullDownRefresh, onReachBottom, onUnload } from "@dcloudio/uni-app";
import { useUi } from "/$/cool-ui";
interface Res {
    list: any[];
    pagination: {
        total: number;
        page: number;
        size: number;
        [key: string]: any;
    };
    [key: string]: any;
}
export function usePager<T = any>(data: T[] = []) {
    const { proxy }: any = getCurrentInstance();
    const ui = useUi();
    // 分页信息
    const pager = reactive({
        params: {},
        pagination: {
            page: 1,
            size: 20,
            total: 0,
        },
        list: data,
        loading: false,
        finished: false,
    });
    // 事件
    const events: any = {};
    // 列表
    const list = computed(() => pager.list);
    // 刷新
    async function refresh(params?: any) {
        if (pager.loading) {
            return false;
        }
        if (proxy.refresh) {
            await proxy.refresh(params);
        } else if (proxy.$.exposed.refresh) {
            await proxy.$.exposed.refresh(params);
        } else {
            console.log("use defineExpose({ refresh })");
        }
    }
    // 数据
    function onData(cb: (list: T[]) => void) {
        events.onData = cb;
    }
    // 刷新
    function onRefresh(params: any = {}, options?: { clear?: boolean; loading?: boolean }) {
        const { clear, loading = true } = options || {};
        // 是否清空
        if (clear) {
            if (params.page == 1) {
                pager.list = [];
                pager.finished = false;
            }
        }
        // 合并请求参数
        Object.assign(pager.params, params);
        const data = {
            ...pager.pagination,
            ...pager.params,
            total: undefined,
        };
        // 是否显示加载动画
        if (data.page == 1 && loading) {
            ui.showLoading();
        }
        pager.loading = true;
        // 完成
        function done() {
            ui.hideLoading();
            pager.loading = false;
        }
        return {
            data,
            done,
            next: (req: Promise<Res>) => {
                return new Promise((resolve, reject) => {
                    req.then((res: Res) => {
                        // 设置列表数据
                        if (data.page == 1) {
                            pager.list = res.list;
                        } else {
                            pager.list.push(...res.list);
                        }
                        // 追加事件
                        if (events.onData) {
                            events.onData(res.list);
                        }
                        // 是否加载完成
                        pager.finished = pager.list.length === res.pagination.total;
                        // 分页信息
                        pager.pagination = res.pagination;
                        done();
                        resolve(res);
                    }).catch((err) => {
                        done();
                        ui.showToast(err.message);
                        reject(err);
                    });
                });
            },
        };
    }
    // 关闭
    function close() {
        isReg = false;
        ui.hideLoading();
    }
    // 下拉刷新
    async function onDown(end?: () => void) {
        await refresh({ page: 1 });
        end?.();
    }
    // 上拉加载
    function onUp() {
        if (!pager.finished) {
            refresh({ page: pager.pagination.page + 1 });
        }
    }
    // 是否注册,避免在组件中重复注入事件问题
    let isReg = true;
    // 上拉加载
    onReachBottom(() => {
        if (isReg) {
            onUp();
        }
    });
    // 下拉刷新
    onPullDownRefresh(() => {
        if (isReg) {
            onDown(uni.stopPullDownRefresh);
        }
    });
    // 组件销毁
    onUnmounted(close);
    // 离开页面
    onUnload(close);
    return {
        pager,
        list,
        onData,
        onRefresh,
        onPullDownRefresh,
        onReachBottom,
        onDown,
        onUp,
    };
}
cool/hooks/wx.ts
New file
@@ -0,0 +1,289 @@
import { ref } from "vue";
import { onReady, onShow } from "@dcloudio/uni-app";
import { config } from "/@/config";
import { getUrlParam, storage } from "../utils";
import { service } from "../service";
import { useI18n } from "vue-i18n";
// #ifdef H5
import wx from "weixin-js-sdk";
// #endif
export function useWx() {
    const { platform } = uni.getSystemInfoSync();
    const { t } = useI18n();
    // 授权码
    const code = ref("");
    // 获取授权码
    async function getCode() {
        return new Promise((resolve) => {
            // #ifdef MP-WEIXIN
            uni.login({
                provider: "weixin",
                success: (res) => {
                    code.value = res.code;
                    resolve(res.code);
                },
            });
            // #endif
        });
    }
    // 是否微信浏览器
    function isWxBrowser() {
        // #ifdef H5
        const ua: any = window.navigator.userAgent.toLowerCase();
        if (ua.match(/MicroMessenger/i) == "micromessenger") {
            return true;
        } else {
            return false;
        }
        // #endif
        // #ifndef H5
        return false;
        // #endif
    }
    // 是否安装了微信
    function hasApp() {
        // #ifdef APP
        return plus.runtime.isApplicationExist({ pname: "com.tencent.mm", action: "weixin://" });
        // #endif
        // #ifndef APP
        return true;
        // #endif
    }
    // 下载微信
    function downloadApp() {
        // #ifdef APP
        if (platform == "android") {
            const Uri: any = plus.android.importClass("android.net.Uri");
            const uri: any = Uri.parse("market://details?id=" + "com.tencent.mm");
            const Intent: any = plus.android.importClass("android.content.Intent");
            const intent: any = new Intent(Intent.ACTION_VIEW, uri);
            const main: any = plus.android.runtimeMainActivity();
            main.startActivity(intent);
        } else {
            plus.runtime.openURL(
                "itms-apps://" + "itunes.apple.com/cn/app/wechat/id414478124?mt=8"
            );
        }
        // #endif
    }
    // 微信公众号配置
    const mpConfig = {
        appId: "",
    };
    // 获取微信公众号配置
    function getMpConfig() {
        // #ifdef H5
        if (isWxBrowser()) {
            service.user.comm
                .wxMpConfig({
                    url: `${location.origin}${location.pathname}`,
                })
                .then((res) => {
                    wx.config({
                        debug: config.app.wx.debug,
                        jsApiList: ["chooseWXPay"],
                        ...res,
                    });
                    Object.assign(mpConfig, res);
                });
        }
        // #endif
    }
    // 微信公众号授权
    function mpAuth() {
        const redirect_uri = encodeURIComponent(
            `${location.origin}${location.pathname}#/pages/user/login`
        );
        const response_type = "code";
        const scope = "snsapi_userinfo";
        const state = "STATE";
        const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${mpConfig.appId}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}#wechat_redirect`;
        location.href = url;
    }
    // 微信公众号登录
    async function mpLogin() {
        return new Promise((resolve) => {
            const code = getUrlParam("code");
            const mpCode = storage.get("mpCode");
            let url = window.location.href;
            url = url.replace(/(\?[^#]*)#/, "#");
            window.history.replaceState({}, "", url);
            if (code != mpCode) {
                storage.set("mpCode", code);
                resolve(code);
            } else {
                resolve(null);
            }
        });
    }
    // 微信公众号支付
    async function mpPay(params: wx.IchooseWXPay & { timeStamp: number }): Promise<void> {
        return new Promise((resolve, reject) => {
            if (!isWxBrowser()) {
                return reject({
                    message: t("请在微信浏览器中打开"),
                });
            }
            wx.chooseWXPay({
                ...params,
                timestamp: params.timeStamp,
                success() {
                    resolve();
                },
                complete(e: { errMsg: string }) {
                    switch (e.errMsg) {
                        case "chooseWXPay:cancel":
                            reject({ message: t("已取消支付") });
                            break;
                        default:
                            reject({ message: t("支付失败") });
                    }
                },
            });
        });
    }
    // 微信app登录
    function appLogin(): Promise<string> {
        let all: any;
        let Service: any;
        return new Promise((resolve, reject) => {
            plus.oauth.getServices((Services: any) => {
                all = Services;
                Object.keys(all).some((key) => {
                    if (all[key].id == "weixin") {
                        Service = all[key];
                    }
                });
                Service.authorize(resolve, reject);
            }, reject);
        });
    }
    // 微信app支付
    function appPay(orderInfo: {
        appid: string;
        noncestr: string;
        package: string;
        partnerid: string;
        prepayid: string;
        timestamp: string;
        sign: string;
        [key: string]: any;
    }): Promise<void> {
        return new Promise((resolve, reject) => {
            uni.requestPayment({
                provider: "wxpay",
                orderInfo,
                success() {
                    resolve();
                },
                fail() {
                    reject({ message: t("已取消支付") });
                },
            });
        });
    }
    // 微信小程序登录
    async function miniLogin(): Promise<{ code: string; [key: string]: any }> {
        return new Promise((resolve, reject) => {
            // 兼容 Mac
            const k = platform === "mac" ? "getUserInfo" : "getUserProfile";
            uni[k]({
                lang: "zh_CN",
                desc: t("授权信息仅用于用户登录"),
                success({ iv, encryptedData, signature, rawData }) {
                    function next() {
                        resolve({
                            iv,
                            encryptedData,
                            signature,
                            rawData,
                            code: code.value,
                        });
                    }
                    // 检查登录状态是否过期
                    uni.checkSession({
                        success() {
                            next();
                        },
                        fail() {
                            getCode().then(next);
                        },
                    });
                },
                fail(err) {
                    console.error(err);
                    getCode();
                    reject({
                        message: t("登录授权失败"),
                    });
                },
            });
        });
    }
    // 微信小程序支付
    function miniPay(params: any): Promise<void> {
        return new Promise((resolve, reject) => {
            uni.requestPayment({
                provider: "wxpay",
                ...params,
                success() {
                    resolve();
                },
                fail() {
                    reject({ message: t("已取消支付") });
                },
            });
        });
    }
    onShow(() => {
        getCode();
    });
    onReady(() => {
        getMpConfig();
    });
    return {
        code,
        getCode,
        isWxBrowser,
        hasApp,
        downloadApp,
        mpConfig,
        mpAuth,
        mpLogin,
        mpPay,
        miniLogin,
        miniPay,
        appLogin,
        appPay,
    };
}
cool/index.ts
New file
@@ -0,0 +1,9 @@
export * from "./hooks";
export * from "./router";
export * from "./store";
export * from "./upload";
export * from "./service";
export * from "./module";
export * from "../config";
export type * from "./types";
export { storage } from "./utils";
cool/module/index.ts
New file
@@ -0,0 +1,21 @@
import type { Module } from "../types";
import { hmr } from "../hooks";
// 模块列表
const list: Module[] = hmr.getData("modules", []);
// 模块对象
const module = {
    list,
    get(name: string): Module {
        return this.list.find((e) => e.name == name)!;
    },
    config(name: string) {
        return this.get(name).options || {};
    },
    add(data: Module) {
        this.list.push(data);
    },
};
export { module };
cool/router/index.ts
New file
@@ -0,0 +1,325 @@
import { last } from "lodash-es";
import { ctx } from "virtual:ctx";
import { storage } from "../utils";
import { config } from "../../config";
type PushOptions =
    | string
    | {
            path: string;
            mode?: "navigateTo" | "redirectTo" | "reLaunch" | "switchTab" | "preloadPage";
            events?: {
                [key: string]: (data: any) => void;
            };
            query?: {
                [key: string]: any;
            };
            params?: {
                [key: string]: any;
            };
            isGuard?: boolean;
            [key: string]: any;
      };
type Tabs = {
    text?: string;
    pagePath: string;
    iconPath?: string;
    selectedIconPath?: string;
    [key: string]: any;
}[];
// 路由列表
const routes = [...ctx.pages];
// 子包
if (ctx.subPackages) {
    ctx.subPackages.forEach((a: { pages: any[]; root: string }) => {
        a.pages.forEach((b) => {
            routes.push({
                ...b,
                path: a.root + "/" + b.path,
            });
        });
    });
}
// 钩子函数
const fn: { [key: string]: (...args: any[]) => any } = {};
// 路由
const router = {
    // 底部导航
    get tabs(): Tabs {
        if (ctx.tabBar) {
            return ctx.tabBar.list || [];
        } else {
            return [];
        }
    },
    // 全局样式配置
    globalStyle: ctx.globalStyle,
    // 路由列表
    routes,
    // 跳转参数(地址栏)
    get query() {
        const info = this.info();
        return {
            ...info?.query,
        };
    },
    // 跳转参数(缓存)
    get params() {
        return storage.get("router-params") || {};
    },
    // 页面路径
    get pages() {
        return {
            home: "/" + (ctx.tabBar ? this.tabs[0].pagePath : ctx.pages[0].path),
            ...config.app.pages,
        };
    },
    // 当前页面信息
    currentPage(): { [key: string]: any } {
        return last(getCurrentPages())!;
    },
    // 当前路由路径
    get path() {
        return router.info()?.path;
    },
    // 当前路由信息
    info() {
        const page = last(getCurrentPages());
        if (page) {
            const { route, $page, $vm, $getAppWebview }: any = page;
            const q: any = {};
            try {
                $page?.fullPath
                    .split("?")[1]
                    .split("&")
                    .forEach((e: string) => {
                        const [k, v] = e.split("=");
                        q[k] = decodeURIComponent(v);
                    });
            } catch (e) {}
            // 页面配置
            const style = this.routes.find((e) => e.path == route)?.style;
            let d = {
                $vm,
                $getAppWebview,
                path: `/${route}`,
                fullPath: $page?.fullPath,
                query: q || {},
                isTab: this.isTab(route),
                style,
                isCustomNavbar: style?.navigationStyle == "custom",
            };
            return d;
        } else {
            return null;
        }
    },
    // 路由跳转
    push(options: PushOptions) {
        if (typeof options == "string") {
            options = {
                path: options,
                mode: "navigateTo",
            };
        }
        let {
            path,
            mode = "navigateTo",
            animationType,
            animationDuration,
            events,
            success,
            fail,
            complete,
            query,
            params,
            isGuard = true,
        } = options || {};
        if (query) {
            let arr = [];
            for (let i in query) {
                if (query[i] !== undefined) {
                    arr.push(`${i}=${query[i]}`);
                }
            }
            path += "?" + arr.join("&");
        }
        if (params) {
            storage.set("router-params", params);
        }
        let data = {
            url: path,
            animationType,
            animationDuration,
            events,
            success,
            fail,
            complete,
        };
        if (this.isTab(path)) {
            mode = "switchTab";
        }
        const next = () => {
            switch (mode) {
                case "navigateTo":
                    uni.navigateTo(data);
                    break;
                case "redirectTo":
                    uni.redirectTo(data);
                    break;
                case "reLaunch":
                    uni.reLaunch(data);
                    break;
                case "switchTab":
                    uni.switchTab(data);
                    break;
                case "preloadPage":
                    uni.preloadPage(data);
                    break;
            }
        };
        if (fn.beforeEach && isGuard) {
            fn.beforeEach({ path: options.path, query }, next, (options: PushOptions) => {
                this.push(options);
            });
        } else {
            next();
        }
    },
    // 后退
    back(options?: UniApp.NavigateBackOptions) {
        if (this.isFirstPage()) {
            this.home();
        } else {
            uni.navigateBack(options || {});
        }
    },
    // 执行当前页面的某个方法
    callMethod(name: string, data?: any) {
        const { $vm } = this.info()!;
        if ($vm) {
            if ($vm.$.exposed?.[name]) {
                return $vm.$.exposed[name](data);
            }
        }
    },
    // 页面栈长度是否只有1
    isFirstPage() {
        return getCurrentPages().length == 1;
    },
    // 是否当前页
    isCurrentPage(path: string) {
        return this.info()?.path == path;
    },
    // 回到首页
    home() {
        this.push(this.pages.home);
    },
    // 跳转 Tab 页
    switchTab(name: string) {
        let item = this.tabs.find((e) => e.pagePath.includes(name));
        if (item) {
            this.push({
                path: `/${item.pagePath}`,
                mode: "switchTab",
            });
        } else {
            console.error("Not found tab", name);
        }
    },
    // 是否是 Tab 页
    isTab(path: string) {
        return !!this.tabs.find((e) => path == `/${e.pagePath}`);
    },
    // 去登陆
    login(options?: { reLaunch: boolean }) {
        const { reLaunch = false } = options || {};
        this.push({
            path: this.pages.login,
            mode: reLaunch ? "reLaunch" : "navigateTo",
            isGuard: false,
        });
    },
    // 登录成功后操作
    nextLogin(type?: string) {
        const pages = getCurrentPages();
        const index = pages.findIndex((e) => this.pages.login.includes(e.route!));
        if (index <= 0) {
            this.home();
        } else {
            router.back({
                delta: pages.length - index,
            });
        }
        // 登录方式
        storage.set("loginType", type);
        // 登录回调
        if (fn.afterLogin) {
            fn.afterLogin();
        }
        // 事件
        uni.$emit("afterLogin", { type });
    },
    // 跳转前钩子
    beforeEach(callback: (to: any, next: () => void) => void) {
        fn.beforeEach = callback;
    },
    // 登录后回调
    afterLogin(callback: () => void) {
        fn.afterLogin = callback;
    },
};
export { router };
cool/service/base.ts
New file
@@ -0,0 +1,89 @@
import { config } from "../../config";
import request from "./request";
export class BaseService {
    namespace?: string;
    constructor(namespace?: string) {
        if (namespace) {
            this.namespace = namespace;
        }
    }
    // 发送请求
    async request(options: any = {}) {
        let url = options.url;
        if (url && url.indexOf("http") < 0) {
            if (this.namespace) {
                url = this.namespace + url;
            }
            if (options.proxy !== false) {
                url = config.baseUrl + "/" + url;
            }
        }
        // 处理参数
        options.data =
            options.method?.toLocaleUpperCase() == "POST" ? options.data : options.params;
        return request({
            ...options,
            url,
        });
    }
    // 获取列表
    async list(data: any) {
        return this.request({
            url: "/list",
            method: "POST",
            data,
        });
    }
    // 分页查询
    async page(data: any) {
        return this.request({
            url: "/page",
            method: "POST",
            data,
        });
    }
    // 获取信息
    async info(params: any) {
        return this.request({
            url: "/info",
            params,
        });
    }
    // 更新数据
    async update(data: any) {
        return this.request({
            url: "/update",
            method: "POST",
            data,
        });
    }
    // 删除数据
    async delete(data: any) {
        return this.request({
            url: "/delete",
            method: "POST",
            data,
        });
    }
    // 添加数据
    async add(data: any) {
        return this.request({
            url: "/add",
            method: "POST",
            data,
        });
    }
}
cool/service/index.ts
New file
@@ -0,0 +1,9 @@
import { BaseService } from "./base";
// service 数据集合
// @ts-ignore
export const service: Eps.Service = {
    request: new BaseService().request,
};
export * from "./base";
cool/service/request.ts
New file
@@ -0,0 +1,133 @@
import { useStore } from "../store";
import { router } from "../router";
import { isDev, config } from "../../config";
import { storage } from "../utils";
import { getLocale, t } from "/@/locale";
// 请求队列
let requests: any[] = [];
// Token 是否刷新中
let isRefreshing = false;
export default function request(options: any) {
    const { user } = useStore();
    // 标识
    let Authorization = user.token || "";
    // 忽略标识
    config.ignore.token.forEach((e) => {
        if (options.url.includes(e)) {
            Authorization = "";
        }
    });
    if (isDev) {
        console.log(`[${options.method || "GET"}] ${options.url}`);
    }
    return new Promise(async (resolve, reject) => {
        // 继续请求
        function next() {
            uni.request({
                ...options,
                header: {
                    Authorization,
                    language: getLocale(),
                    ...options.header,
                },
                success(res: any) {
                    const { code, data, message } = res.data as {
                        code: number;
                        message: string;
                        data: any;
                    };
                    // 无权限
                    if (res.statusCode === 401) {
                        if (router.info()?.path == router.pages.login) {
                            return reject({ message });
                        } else {
                            user.logout();
                        }
                    }
                    // 服务异常
                    if (res.statusCode === 502) {
                        return reject({
                            message: t("服务异常"),
                        });
                    }
                    // 未找到
                    if (res.statusCode === 404) {
                        return reject({
                            message: `[404] ${options.url}`,
                        });
                    }
                    // 成功
                    if (res.statusCode === 200) {
                        switch (code) {
                            case 1000:
                                resolve(data);
                                break;
                            default:
                                reject({ message, code });
                        }
                    } else {
                        reject({ message: t("服务异常") });
                    }
                },
                fail(err: any) {
                    reject({ message: err.errMsg });
                },
            });
        }
        // 刷新token处理
        if (!options.url.includes("refreshToken")) {
            if (Authorization) {
                // 判断 token 是否过期
                if (storage.isExpired("token")) {
                    // 判断 refreshToken 是否过期
                    if (storage.isExpired("refreshToken")) {
                        // 退出登录
                        return user.logout();
                    }
                    // 是否在刷新中
                    if (!isRefreshing) {
                        isRefreshing = true;
                        user.refreshToken()
                            .then((token) => {
                                requests.forEach((cb) => cb(token));
                                requests = [];
                                isRefreshing = false;
                            })
                            .catch((err) => {
                                user.logout();
                                reject(err);
                            });
                    }
                    return new Promise((resolve) => {
                        // 继续请求
                        requests.push((token: string) => {
                            // 重新设置 token
                            Authorization = token;
                            next();
                            resolve();
                        });
                    });
                }
            }
        }
        next();
    });
}
cool/service/sign.ts
New file
@@ -0,0 +1,22 @@
import md5 from "md5";
function useSign(params: any) {
    const timestamp = new Date().getTime();
    let arr = [`timestamp=${timestamp}`];
    for (const i in params) {
        arr.push(`${i}=${decodeURIComponent(params[i])}`);
    }
    arr.sort();
    const sign = md5(arr.join("&"));
    return {
        timestamp,
        sign,
    };
}
export { useSign };
cool/store/dict.ts
New file
@@ -0,0 +1,67 @@
import { defineStore } from "pinia";
import { computed, reactive, toRaw } from "vue";
import { deepTree, isEmpty } from "../utils";
import { service } from "../service";
import { isDev } from "/@/config";
import { isString } from "lodash-es";
import type { Dict } from "../types";
const useDictStore = defineStore("dict", () => {
    // 对象数据
    const data = reactive<Dict.Data>({});
    // 获取数据列表
    function get(name: Dict.Key) {
        return computed(() => data[name]).value || [];
    }
    // 获取名称
    function getLabel(name: Dict.Key | any[], value: any): string {
        const arr: any[] = String(value)?.split(",") || [];
        return arr
            .map((e) => {
                return (isString(name) ? get(name) : name).find((a) => a.value == e)?.label;
            })
            .filter(Boolean)
            .join(",");
    }
    // 刷新
    async function refresh(types?: Dict.Key[]) {
        return service.dict.info
            .data({
                types,
            })
            .then((res: Dict.Data) => {
                const d: any = {};
                for (const [i, arr] of Object.entries(res)) {
                    arr.forEach((e) => {
                        e.label = e.name;
                        e.value = isEmpty(e.value) ? e.id : e.value;
                    });
                    d[i] = deepTree(arr, "desc");
                }
                Object.assign(data, d);
                if (isDev) {
                    console.log("字典数据:");
                    console.log(toRaw(data));
                }
                return data;
            });
    }
    return {
        data,
        get,
        getLabel,
        refresh,
    };
});
export { useDictStore };
cool/store/index.ts
New file
@@ -0,0 +1,9 @@
import { useUserStore } from "./user";
import { useDictStore } from "./dict";
export function useStore() {
    return {
        user: useUserStore(),
        dict: useDictStore(),
    };
}
cool/store/user.ts
New file
@@ -0,0 +1,94 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { deepMerge, storage } from "../utils";
import { router } from "../router";
import { service } from "../service";
import type { User } from "../types";
// 本地缓存
const data = storage.info();
const useUserStore = defineStore("user", function () {
    // 标识
    const token = ref(data.token || "");
    // 设置标识
    function setToken(data: User.Token) {
        token.value = data.token;
        // 访问
        storage.set("token", data.token, data.expire - 5);
        // 刷新
        storage.set("refreshToken", data.refreshToken, data.refreshExpire - 5);
    }
    // 刷新标识
    async function refreshToken() {
        return service.user.login
            .refreshToken({
                refreshToken: storage.get("refreshToken"),
            })
            .then((res) => {
                setToken(res);
                return res.token;
            });
    }
    // 用户信息
    const info = ref<User.Info | undefined>(data.userInfo);
    // 设置用户信息
    function set(value: User.Info) {
        info.value = value;
        storage.set("userInfo", value);
    }
    // 更新用户信息
    async function update(data: User.Info & { [key: string]: any }) {
        set(deepMerge(info.value, data));
        return service.user.info.updatePerson(data);
    }
    // 清除用户
    function clear() {
        storage.remove("userInfo");
        storage.remove("token");
        storage.remove("refreshToken");
        token.value = "";
        info.value = undefined;
    }
    // 退出
    function logout() {
        clear();
        router.login({ reLaunch: true });
    }
    // 获取用户信息
    async function get() {
        return service.user.info
            .person()
            .then((res) => {
                if (res) {
                    set(res);
                }
                return res;
            })
            .catch(() => {
                logout();
            });
    }
    return {
        token,
        setToken,
        refreshToken,
        info,
        get,
        set,
        update,
        logout,
    };
});
export { useUserStore };
cool/types/index.d.ts
New file
@@ -0,0 +1,45 @@
export declare interface ModuleConfig {
    name?: string;
    description?: string;
    order?: number;
    demo?: { label: string; path: string };
    options?: {
        [key: string]: any;
    };
    onLoad?(options?: any): any;
}
export declare interface Module extends ModuleConfig {
    name: string;
    options: {
        [key: string]: any;
    };
    [key: string]: any;
}
export namespace User {
    interface Token {
        token: string;
        expire: number;
        refreshToken: string;
        refreshExpire: number;
    }
    interface Info extends Eps.UserInfoEntity {}
}
export namespace Dict {
    type Key = Eps.DictKey | (string & {});
    interface Item {
        id: string;
        label: string;
        value: any;
        children?: Item[];
        [key: string]: any;
    }
    interface Data {
        [key: string]: Item[];
    }
}
cool/upload/comm.ts
New file
@@ -0,0 +1,52 @@
import { isArray, has, isObject } from "lodash-es";
type Size = number | number[] | { h?: number; w?: number; m?: string };
function parse(rules: string[], { url, size }: { url: string; size: Size }) {
    if (!url) {
        return "";
    }
    if (url.startsWith("blob:") || url.includes("file://")) {
        return url;
    }
    let h = 0;
    let w = 0;
    if (isArray(size)) {
        h = size[0];
        w = size[1];
    } else if (isObject(size) && has(size, "h")) {
        h = size.h!;
        w = size.w!;
        if (size.m) {
            rules.push(`m_${size.m}`);
        }
    } else {
        h = w = Number(size);
    }
    url += url.includes("?") ? "&" : "?";
    if (h) {
        rules.push(`h_${h}`);
    }
    if (w) {
        rules.push(`w_${w}`);
    }
    return `${url}${rules.join(",")}`;
}
function videoPoster(url: string, size: Size) {
    return parse(["x-oss-process=video/snapshot,t_1000,f_jpg,m_fast"], { url, size });
}
function resizeImage(url: string, size: Size) {
    return parse(["x-oss-process=image/resize"], { url, size });
}
export { videoPoster, resizeImage };
cool/upload/index.ts
New file
@@ -0,0 +1,141 @@
import dayjs from "dayjs";
import { config } from "../../config";
import { service } from "../service";
import { basename, pathJoin, uuid } from "../utils";
import { useStore } from "../store";
import { videoPoster, resizeImage } from "./comm";
declare interface UploadCallback {
    onProgressUpdate?(options: UniApp.OnProgressUpdateResult): void;
    onTask?(task: UniApp.UploadTask): void;
}
export async function upload(file: any, cb?: UploadCallback): Promise<string> {
    const { onProgressUpdate, onTask } = cb || {};
    // 获取上传模式
    const { mode, type } = await service.base.comm.uploadMode();
    // 用户缓存
    const { user } = useStore();
    // 本地上传
    const isLocal = mode == "local";
    // 文件名
    const fileName = uuid() + "_" + (file.name || basename(file.path));
    // Key
    const key = isLocal ? fileName : pathJoin("app", dayjs().format("YYYY-MM-DD"), fileName);
    // 多种上传请求
    return new Promise((resolve, reject) => {
        // 上传文件
        function next({ host, preview, data }: { host: string; preview?: string; data?: any }) {
            // 签名数据
            const fd = {
                ...data,
                key,
            };
            // 上传
            const task = uni.uploadFile({
                url: host,
                filePath: file.path,
                name: "file",
                header: isLocal
                    ? {
                            Authorization: user.token,
                      }
                    : {},
                formData: fd,
                success(res) {
                    if (isLocal) {
                        const { code, data, message } = JSON.parse(res.data);
                        if (code == 1000) {
                            resolve(data);
                        } else {
                            reject(message);
                        }
                    } else {
                        resolve(pathJoin(preview || host, fd.key));
                    }
                },
                fail(err) {
                    reject(err);
                },
            });
            if (onTask) {
                onTask(task);
            }
            if (onProgressUpdate) {
                task.onProgressUpdate(onProgressUpdate);
            }
        }
        if (isLocal) {
            next({
                host: config.baseUrl + "/app/base/comm/upload",
            });
        } else {
            service.base.comm
                .upload(
                    type == "aws"
                        ? {
                                key,
                          }
                        : {}
                )
                .then((res) => {
                    switch (type) {
                        // 腾讯
                        case "cos":
                            next({
                                host: res.url,
                                data: res.credentials,
                            });
                            break;
                        // 阿里
                        case "oss":
                            next({
                                host: res.host,
                                data: {
                                    OSSAccessKeyId: res.OSSAccessKeyId,
                                    policy: res.policy,
                                    signature: res.signature,
                                },
                            });
                            break;
                        // 七牛
                        case "qiniu":
                            next({
                                host: res.uploadUrl,
                                preview: res.publicDomain,
                                data: {
                                    token: res.token,
                                },
                            });
                            break;
                        // aws
                        case "aws":
                            next({
                                host: res.url,
                                data: res.fields,
                            });
                            break;
                    }
                })
                .catch(reject);
        }
    });
}
export function useUpload() {
    return {
        upload,
        videoPoster,
        resizeImage,
    };
}
cool/utils/canvas.ts
New file
@@ -0,0 +1,603 @@
import { getCurrentInstance } from "vue";
import { isEmpty, isString, cloneDeep, isObject } from "lodash-es";
// 渲染参数
declare interface RenderOptions {
    x: number;
    y: number;
    height?: number;
    width?: number;
    [key: string]: any;
}
// 文本渲染参数
declare interface TextRenderOptions extends RenderOptions {
    text: string;
    color?: string;
    fontSize?: number;
    textAlign?: "left" | "right" | "center";
    overflow?: "ellipsis";
    lineClamp?: number;
    letterSpace?: number;
    lineHeight?: number;
}
// 图片渲染参数
declare interface ImageRenderOptions extends RenderOptions {
    url: string;
    mode?: "aspectFill" | "aspectFit";
    radius?: number;
}
// 块渲染参数
declare interface DivRenderOptions extends RenderOptions {
    radius?: number;
    backgroundColor?: string;
    border?: {
        width: number;
        color: string;
    };
}
// 导出图片参数
declare interface CreateImageOptins {
    x?: number;
    y?: number;
    width?: number;
    height?: number;
    destWidth?: number;
    destHeight?: number;
    fileType?: "jpg" | "png";
    quality?: number;
}
class Canvas {
    ctx: any;
    canvasId: any;
    scope: any;
    renderQuene: any;
    imageQueue: any;
    constructor(canvasId: string) {
        // 绘图上下文
        this.ctx = null;
        // canvas id
        this.canvasId = canvasId;
        // 当前页面作用域
        const { proxy }: any = getCurrentInstance();
        this.scope = proxy;
        // 渲染队列
        this.renderQuene = [];
        // 图片队列
        this.imageQueue = [];
        // 创建画布
        this.create();
    }
    // 创建画布
    create() {
        this.ctx = uni.createCanvasContext(this.canvasId, this.scope);
        return this;
    }
    // 块
    div(options: DivRenderOptions) {
        let render = () => {
            this.divRender(options);
        };
        this.renderQuene.push(render);
        return this;
    }
    // 文本
    text(options: TextRenderOptions) {
        let render = () => {
            this.textRender(options);
        };
        this.renderQuene.push(render);
        return this;
    }
    // 图片
    image(options: ImageRenderOptions) {
        let render = () => {
            this.imageRender(options);
        };
        this.imageQueue.push(options);
        this.renderQuene.push(render);
        return this;
    }
    // 绘画
    draw(save = false) {
        return new Promise((resolve) => {
            let next = () => {
                this.render();
                this.ctx.draw(save, () => {
                    resolve(true);
                });
            };
            if (!isEmpty(this.imageQueue)) {
                this.preLoadImage().then(next);
            } else {
                next();
            }
        });
    }
    // 生成图片
    createImage(options?: CreateImageOptins): Promise<string> {
        return new Promise((resolve, reject) => {
            let data = {
                canvasId: this.canvasId,
                ...options,
                success: (res: any) => {
                    // #ifdef MP-ALIPAY
                    resolve(res.apFilePath);
                    // #endif
                    // #ifndef MP-ALIPAY
                    resolve(res.tempFilePath);
                    // #endif
                },
                fail: reject,
            };
            // #ifdef MP-ALIPAY
            this.ctx.toTempFilePath(data);
            // #endif
            // #ifndef MP-ALIPAY
            uni.canvasToTempFilePath(data, this.scope);
            // #endif
        });
    }
    // 保存图片
    saveImage(options?: CreateImageOptins) {
        uni.showLoading({
            title: "图片下载中...",
        });
        this.createImage(options).then((path: any) => {
            return new Promise((resolve) => {
                uni.hideLoading();
                uni.saveImageToPhotosAlbum({
                    filePath: path,
                    success: () => {
                        uni.showToast({
                            title: "保存图片成功",
                        });
                        resolve(path);
                    },
                    fail: (err) => {
                        // #ifdef MP-ALIPAY
                        uni.showToast({
                            title: "保存图片成功",
                        });
                        // #endif
                        // #ifndef MP-ALIPAY
                        uni.showToast({
                            title: "保存图片失败",
                            icon: "none",
                        });
                        // #endif
                    },
                });
            });
        });
    }
    // 预览图片
    previewImage(options?: CreateImageOptins) {
        this.createImage(options).then((url: string | any) => {
            uni.previewImage({
                urls: [url],
            });
        });
    }
    // 下载图片
    downLoadImage(item: any) {
        return new Promise((resolve, reject) => {
            if (!item.url) {
                return reject("url 不能为空");
            }
            // 处理base64
            // #ifdef MP
            if (item.url.indexOf("data:image") >= 0) {
                let extName = item.url.match(/data\:\S+\/(\S+);/);
                if (extName) {
                    extName = extName[1];
                }
                const fs = uni.getFileSystemManager();
                const fileName = Date.now() + "." + extName;
                // @ts-ignore
                const filePath = wx.env.USER_DATA_PATH + "/" + fileName;
                return fs.writeFile({
                    filePath,
                    data: item.url.replace(/^data:\S+\/\S+;base64,/, ""),
                    encoding: "base64",
                    success: () => {
                        item.url = filePath;
                        resolve(filePath);
                    },
                });
            }
            // #endif
            // 是否网络图片
            const isHttp = item.url.includes("http");
            uni.getImageInfo({
                src: item.url,
                success: (result) => {
                    item.sheight = result.height;
                    item.swidth = result.width;
                    if (isHttp) {
                        item.url = result.path;
                    }
                    resolve(item.url);
                },
                fail: (err) => {
                    console.log(err, item.url);
                    reject(err);
                },
            });
            return 1;
        });
    }
    // 预加载图片
    async preLoadImage() {
        await Promise.all(this.imageQueue.map(this.downLoadImage));
    }
    // 设置背景颜色
    setBackground(options: any) {
        if (!options) return null;
        let backgroundColor;
        if (!isString(options)) {
            backgroundColor = options;
        }
        if (isString(options.backgroundColor)) {
            backgroundColor = options.backgroundColor;
        }
        if (isObject(options.backgroundColor)) {
            let { startX, startY, endX, endY, gradient } = options.backgroundColor;
            const rgb = this.ctx.createLinearGradient(startX, startY, endX, endY);
            for (let i = 0, l = gradient.length; i < l; i++) {
                rgb.addColorStop(gradient[i].step, gradient[i].color);
            }
            backgroundColor = rgb;
        }
        this.ctx.setFillStyle(backgroundColor);
        return this;
    }
    // 设置边框
    setBorder(options: any) {
        if (!options.border) return this;
        let { x, y, width: w, height: h, border, radius: r } = options;
        if (border.width) {
            this.ctx.setLineWidth(border.width);
        }
        if (border.color) {
            this.ctx.setStrokeStyle(border.color);
        }
        // 偏移距离
        let p = border.width / 2;
        // 是否有圆角
        if (r) {
            this.drawRadiusRoute(x - p, y - p, w + 2 * p, h + 2 * p, r + p);
            this.ctx.stroke();
        } else {
            this.ctx.strokeRect(x - p, y - p, w + 2 * p, h + 2 * p);
        }
        return this;
    }
    // 设置缩放,旋转
    setTransform(options: any) {
        if (options.scale) {
        }
        if (options.rotate) {
        }
    }
    // 带有圆角的路径绘制
    drawRadiusRoute(x: number, y: number, w: number, h: number, r: number) {
        this.ctx.beginPath();
        this.ctx.moveTo(x + r, y, y);
        this.ctx.lineTo(x + w - r, y);
        this.ctx.arc(x + w - r, y + r, r, 1.5 * Math.PI, 0);
        this.ctx.lineTo(x + w, y + h - r);
        this.ctx.arc(x + w - r, y + h - r, r, 0, 0.5 * Math.PI);
        this.ctx.lineTo(x + r, y + h);
        this.ctx.arc(x + r, y + h - r, r, 0.5 * Math.PI, Math.PI);
        this.ctx.lineTo(x, y + r);
        this.ctx.arc(x + r, y + r, r, Math.PI, 1.5 * Math.PI);
        this.ctx.closePath();
    }
    // 裁剪图片
    cropImage(
        mode: "aspectFill" | "aspectFit",
        width: number,
        height: number,
        sWidth: number,
        sHeight: number,
        x: number,
        y: number
    ) {
        let cx, cy, cw, ch, sx, sy, sw, sh;
        switch (mode) {
            case "aspectFill":
                if (width <= height) {
                    let p = width / sWidth;
                    cw = width;
                    ch = sHeight * p;
                    cx = 0;
                    cy = (height - ch) / 2;
                } else {
                    let p = height / sHeight;
                    cw = sWidth * p;
                    ch = height;
                    cx = (width - cw) / 2;
                    cy = 0;
                }
                break;
            case "aspectFit":
                if (width <= height) {
                    let p = height / sHeight;
                    sw = width / p;
                    sh = sHeight;
                    sx = x + (sWidth - sw) / 2;
                    sy = y;
                } else {
                    let p = width / sWidth;
                    sw = sWidth;
                    sh = height / p;
                    sx = x;
                    sy = y + (sHeight - sh) / 2;
                }
                break;
        }
        return { cx, cy, cw, ch, sx, sy, sw, sh };
    }
    // 获取文本内容
    getTextRows({
        text,
        fontSize = 14,
        width = 100,
        lineClamp = 1,
        overflow,
        letterSpace = 0,
    }: any) {
        let arr: any[] = [[]];
        let a = 0;
        for (let i = 0; i < text.length; i++) {
            let b = this.getFontPx(text[i], { fontSize, letterSpace });
            if (a + b > width) {
                a = b;
                arr.push(text[i]);
            } else {
                // 最后一行且设置超出省略号
                if (
                    overflow == "ellipsis" &&
                    arr.length == lineClamp &&
                    a + 3 * this.getFontPx(".", { fontSize, letterSpace }) > width - 5
                ) {
                    arr[arr.length - 1] += "...";
                    break;
                } else {
                    a += b;
                    arr[arr.length - 1] += text[i];
                }
            }
        }
        return arr;
    }
    // 获取单个字体像素大小
    getFontPx(text: string, { fontSize = 14, letterSpace }: any) {
        if (!text) {
            return fontSize / 2 + fontSize / 14 + letterSpace;
        }
        let ch = text.charCodeAt(0);
        if ((ch >= 0x0001 && ch <= 0x007e) || (0xff60 <= ch && ch <= 0xff9f)) {
            return fontSize / 2 + fontSize / 14 + letterSpace;
        } else {
            return fontSize + letterSpace;
        }
    }
    // 渲染块
    divRender(options: DivRenderOptions) {
        this.ctx.save();
        this.setBackground(options);
        this.setBorder(options);
        this.setTransform(options);
        // 区分是否有圆角采用不同模式渲染
        if (options.radius) {
            let { x, y } = options;
            let w = options.width || 0;
            let h = options.height || 0;
            let r = options.radius || 0;
            // 画路径
            this.drawRadiusRoute(x, y, w, h, r);
            // 填充
            this.ctx.fill();
        } else {
            this.ctx.fillRect(options.x, options.y, options.width, options.height);
        }
        this.ctx.restore();
    }
    // 渲染文本
    textRender(options: TextRenderOptions) {
        let {
            fontSize = 14,
            textAlign,
            width,
            color = "#000000",
            x,
            y,
            letterSpace,
            lineHeight = 14,
        } = options || {};
        this.ctx.save();
        // 设置字体大小
        this.ctx.setFontSize(fontSize);
        // 设置字体颜色
        this.ctx.setFillStyle(color);
        // 获取文本内容
        let rows = this.getTextRows(options);
        // 获取文本行高
        let lh = lineHeight - fontSize;
        // 左偏移
        let offsetLeft = 0;
        // 字体对齐
        if (textAlign && width) {
            this.ctx.textAlign = textAlign;
            switch (textAlign) {
                case "left":
                    break;
                case "center":
                    offsetLeft = width / 2;
                    break;
                case "right":
                    offsetLeft = width;
                    break;
            }
        }
        // 逐行写入
        for (let i = 0; i < rows.length; i++) {
            let d = offsetLeft;
            if (letterSpace) {
                for (let j = 0; j < rows[i].length; j++) {
                    // 写入文字
                    this.ctx.fillText(rows[i][j], x + d, (i + 1) * fontSize + y + lh * i);
                    // 设置偏移
                    d += this.getFontPx(rows[i][j], options);
                }
            } else {
                // 写入文字
                this.ctx.fillText(rows[i], x + offsetLeft, (i + 1) * fontSize + y + lh * i);
            }
        }
        this.ctx.restore();
    }
    // 渲染图片
    imageRender(options: ImageRenderOptions) {
        this.ctx.save();
        if (options.radius) {
            // 画路径
            this.drawRadiusRoute(
                options.x,
                options.y,
                options.width || options.swidth,
                options.height || options.sHeight,
                options.radius
            );
            // 填充
            this.ctx.fill();
            // 裁剪
            this.ctx.clip();
        }
        let temp = cloneDeep(this.imageQueue[0]);
        if (options.mode) {
            let { cx, cy, cw, ch, sx, sy, sw, sh } = this.cropImage(
                options.mode,
                temp.swidth,
                temp.sheight,
                temp.width,
                temp.height,
                temp.x,
                temp.y
            );
            switch (options.mode) {
                case "aspectFit":
                    this.ctx.drawImage(temp.url, sx, sy, sw, sh);
                    break;
                case "aspectFill":
                    this.ctx.drawImage(
                        temp.url,
                        cx,
                        cy,
                        cw,
                        ch,
                        temp.x,
                        temp.y,
                        temp.width,
                        temp.height
                    );
                    break;
            }
        } else {
            this.ctx.drawImage(
                temp.url,
                temp.x,
                temp.y,
                temp.width || temp.swidth,
                temp.height || temp.sheight
            );
        }
        this.imageQueue.shift();
        this.ctx.restore();
    }
    // 渲染全部
    render() {
        this.renderQuene.forEach((ele: any) => {
            ele();
        });
    }
}
export { Canvas };
cool/utils/comm.ts
New file
@@ -0,0 +1,168 @@
import { orderBy } from "lodash-es";
export const { platform } = uni.getSystemInfoSync();
// 是否安卓
export const isAndroid = platform == "android";
// 是否苹果
export const isIos = platform == "ios";
// 是否小数
export function isDecimal(value: any): boolean {
    return String(value).length - String(value).indexOf(".") + 1 > 0;
}
// 首字母大写
export function firstUpperCase(value: string): string {
    return value.replace(/\b(\w)(\w*)/g, function ($0, $1, $2) {
        return $1.toUpperCase() + $2;
    });
}
// 深度合并
export function deepMerge(a: any, b: any) {
    let k;
    for (k in b) {
        a[k] =
            a[k] && a[k].toString() === "[object Object]" ? deepMerge(a[k], b[k]) : (a[k] = b[k]);
    }
    return a;
}
// 获取地址栏参数
export function getUrlParam(name: string): string | null {
    const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
    const r = window.location.search.substr(1).match(reg);
    if (r != null) return decodeURIComponent(r[2]);
    return null;
}
// 列表转树形
export function deepTree(list: any[], sort?: "desc" | "asc"): any[] {
    const newList: any[] = [];
    const map: any = {};
    orderBy(list, "orderNum", sort)
        .map((e) => {
            map[e.id] = e;
            return e;
        })
        .forEach((e) => {
            const parent = map[e.parentId];
            if (parent) {
                (parent.children || (parent.children = [])).push(e);
            } else {
                newList.push(e);
            }
        });
    return newList;
}
// 路径转对象
export function path2Obj(list: any[]) {
    const data: any = {};
    list.forEach(({ path, value }) => {
        const arr: string[] = path.split("/");
        const parents = arr.slice(0, arr.length - 1);
        const name = basename(path).replace(".ts", "");
        let curr = data;
        parents.forEach((k) => {
            if (!curr[k]) {
                curr[k] = {};
            }
            curr = curr[k];
        });
        curr[name] = value;
    });
    return data;
}
// 路径拼接
export function pathJoin(...parts: string[]): string {
    if (parts.length === 0) {
        return "";
    }
    const firstPart = parts[0];
    let isAbsolute = false;
    // 检查第一个部分是否以 "http" 开头,以确定路径类型(绝对还是相对)
    if (firstPart.startsWith("http")) {
        isAbsolute = true;
    }
    // 标准化路径,去除任何开头或结尾的斜杠
    const normalizedParts = parts.map((part) => part.replace(/(^\/+|\/+$)/g, ""));
    if (isAbsolute) {
        // 如果是绝对路径,使用斜杠连接部分
        return normalizedParts.join("/");
    } else {
        // 如果是相对路径,使用平台特定的分隔符连接部分
        return normalizedParts.join("/");
    }
}
// 文件名
export function filename(path: string): string {
    return basename(path.substring(0, path.lastIndexOf(".")));
}
// 路径名称
export function basename(path: string): string {
    let index = path.lastIndexOf("/");
    index = index > -1 ? index : path.lastIndexOf("\\");
    if (index < 0) {
        return path;
    }
    return path.substring(index + 1);
}
// 文件扩展名
export function extname(path: string): string {
    return path.substring((path || "").lastIndexOf(".") + 1);
}
// 横杠转驼峰
export function toCamel(str: string): string {
    return str.replace(/([^-])(?:-+([^-]))/g, function ($0, $1, $2) {
        return $1 + $2.toUpperCase();
    });
}
// uuid
export function uuid(): string {
    const s: any[] = [];
    const hexDigits = "0123456789abcdef";
    for (let i = 0; i < 36; i++) {
        s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
    }
    s[14] = "4";
    s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
    s[8] = s[13] = s[18] = s[23] = "-";
    return s.join("");
}
// 延迟
export function sleep(duration: number) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(true);
        }, duration);
    });
}
// 是否为空
export function isEmpty(val: any) {
    return val === "" || val === null || val === undefined;
}
cool/utils/index.ts
New file
@@ -0,0 +1,4 @@
export * from "./comm";
export * from "./ui";
export * from "./canvas";
export * from "./storage";
cool/utils/qrcode.ts
New file
@@ -0,0 +1,52 @@
import QRCode from "qrcode";
const QRCODE_KEY = "recycleQrcodeBase64";
export function generateQrcode(text: string): Promise<string> {
    return new Promise((resolve) => {
        QRCode.toDataURL(
            text,
            {
                width: 300,
                margin: 2,
            },
            (err, url) => {
                if (err) {
                    console.error("❌ 二维码生成失败", err);
                    resolve("");
                    return;
                }
                resolve(url);
            },
        );
    });
}
export function generateRecycleQrcode(text: string): Promise<string> {
    return new Promise((resolve) => {
        const cached = uni.getStorageSync(QRCODE_KEY);
        if (cached) return resolve(cached);
        generateQrcode(text).then((url) => {
            if (url) {
                uni.setStorageSync(QRCODE_KEY, url);
            }
            resolve(url);
        });
    });
}
/**
 * 清除指定二维码缓存
 * @param key
 */
export function clearQrcodeCache(key: string) {
    uni.removeStorageSync(key);
}
/**
 * 清除所有二维码缓存
 */
export function clearAllQrcodeCache() {
    uni.clearStorageSync();
}
cool/utils/storage.ts
New file
@@ -0,0 +1,75 @@
export const storage = {
    // 后缀标识
    suffix: "_deadtime",
    /**
     * 获取
     * @param {*} key 关键字
     */
    get(key: string): any {
        return uni.getStorageSync(key);
    },
    /**
     * 获取全部
     */
    info() {
        const { keys } = uni.getStorageInfoSync();
        const d: any = {};
        keys.forEach((e: string) => {
            d[e] = uni.getStorageSync(e);
        });
        return d;
    },
    /**
     * 设置
     * @param {*} key 关键字
     * @param {*} value 值
     * @param {*} expires 过期时间
     */
    set(key: string, value: any, expires?: number): void {
        uni.setStorageSync(key, value);
        if (expires) {
            uni.setStorageSync(
                `${key}${this.suffix}`,
                Date.parse(String(new Date())) + expires * 1000
            );
        }
    },
    /**
     * 是否过期
     * @param {*} key 关键字
     */
    isExpired(key: string): boolean {
        return uni.getStorageSync(`${key}${this.suffix}`) - Date.parse(String(new Date())) <= 0;
    },
    /**
     * 删除
     * @param {*} key 关键字
     */
    remove(key: string) {
        return uni.removeStorageSync(key);
    },
    /**
     * 清理
     */
    clear() {
        uni.clearStorageSync();
    },
    /**
     * 获取一次后删除
     */
    once(key: string) {
        const value = this.get(key);
        this.remove(key);
        return value;
    },
};
cool/utils/ui.ts
New file
@@ -0,0 +1,72 @@
import { isArray, isEmpty, isNumber } from "lodash-es";
import { computed, getCurrentInstance, nextTick, ref } from "vue";
// 获取父组件
export function getParent(name: string, k1: string[], k2?: string[]) {
    const { proxy }: any = getCurrentInstance();
    const d = ref();
    let n = 10;
    const next = () => {
        let parent = proxy.$parent;
        while (parent) {
            if (parent.$options.name !== name) {
                parent = parent.$parent;
            } else {
                if (isArray(k2)) {
                    nextTick(() => {
                        const child: any = {};
                        (k2 || []).map((key: string) => {
                            if (proxy[key]) {
                                child[key] = proxy[key];
                            }
                        });
                        if (!parent.__children) {
                            parent.__children = [];
                        }
                        if (!isEmpty(child)) {
                            parent.__children.push(child);
                        }
                    });
                }
                return (k1 || []).reduce((res: any, key: string) => {
                    res[key] = parent[key];
                    return res;
                }, {});
            }
        }
        return parent || d.value;
    };
    return computed(() => next());
}
// 获取元素位置信息
export async function getRect(selector: string): Promise<any> {
    return new Promise((resolve) => {
        uni.createSelectorQuery()
            .select(selector)
            .boundingClientRect((res) => {
                resolve(res);
            })
            .exec();
    });
}
// 解析rpx
export function parseRpx(val: any): string {
    return isArray(val) ? val.map(parseRpx).join(" ") : isNumber(val) ? `${val}rpx` : val;
}
// px 转 rpx
export function px2Rpx(px: number) {
    return px / (uni.upx2px(100) / 100);
}
doc/logo.png
hooks/index.ts
New file
@@ -0,0 +1,29 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { service } from "/@/cool";
export const useAddress = defineStore("address", () => {
    const info = ref<Eps.UserAddressEntity>();
    function set(data: Eps.UserAddressEntity) {
        info.value = data;
    }
    function getDefault() {
        if (!info.value || info.value?.isDefault) {
            service.user.address.default().then((res) => {
                info.value = res;
            });
        }
    }
    return {
        info,
        set,
        getDefault,
    };
});
export * from "./shopping-cart";
export * from "./spec";
export * from "./order";
hooks/order.ts
New file
@@ -0,0 +1,65 @@
import { service, useWx } from "/@/cool";
export function useOrder() {
    const wx = useWx();
    const payTypes = [
        {
            label: "微信支付",
            value: 1,
            key: "wxpay",
            icon: "/static/icon/wxpay.png",
        },
        // {
        //     label: "支付宝支付",
        //     value: 2,
        //     key: "alipay",
        //     icon: "/static/icon/alipay.png",
        // },
        {
            label: "碳积分支付",
            value: 3,
            key: "carbonpay",
            icon: "/static/icon/carbonpay.png",
        },
    ];
    function resolvePayType(type?: string | number) {
        return (
            payTypes.find((e) => e.key === type || e.value == type) || payTypes[0]
        );
    }
    async function toPay(orderId: number, type: string | number = "wxpay") {
        const pay = resolvePayType(type);
        if (pay?.key === "carbonpay") {
            return service.order.pay.carbonPay({ orderId });
        }
        // #ifdef MP-WEIXIN
        return service.order.pay.wxMiniPay({ orderId }).then((res) => {
            return wx.miniPay(res.data);
        });
        // #endif
        // #ifdef H5
        if (wx.isWxBrowser()) {
            return service.order.pay.wxMpPay({ orderId }).then((res) => {
                return wx.mpPay(res.data);
            });
        }
        // #endif
        // #ifdef APP
        return service.order.pay.wxAppPay({ orderId }).then((res) => {
            return wx.appPay(res.data);
        });
        // #endif
    }
    return {
        toPay,
        payTypes,
    };
}
hooks/shopping-cart.ts
New file
@@ -0,0 +1,70 @@
import { defineStore } from "pinia";
import { computed, ref, watch } from "vue";
import { storage } from "/@/cool";
import { uuid } from "/@/cool/utils";
// 购物车
export const useShoppingCart = defineStore("shopping-cart", () => {
    const list = ref<OrderGoods[]>(storage.get("shopping-cart.list") || []);
    // 购物车数量
    const num = computed(() => {
        return list.value.length;
    });
    // 数量+1
    function add(data: OrderGoods) {
        const d = list.value.find((e) => e.spec?.id == data.spec?.id);
        if (d) {
            // 判定库存
            d.count += data.count || 1;
            if (d.count > data.spec.stock!) {
                d.count = data.spec.stock || 1;
            }
        } else {
            list.value.push({
                ...data,
                id: uuid(),
            });
        }
    }
    // 删除规格
    function del(id: string) {
        const i = list.value.findIndex((e) => e.id == id);
        if (i >= 0) {
            list.value.splice(i, 1);
        }
    }
    // 删除规格根据 specId
    function delBySpecId(id: number) {
        const i = list.value.findIndex((e) => e.spec?.id == id);
        if (i >= 0) {
            list.value.splice(i, 1);
        }
    }
    // 监听更新
    watch(
        list,
        (val) => {
            storage.set("shopping-cart.list", val);
        },
        {
            deep: true,
        },
    );
    return {
        list,
        num,
        add,
        del,
        delBySpecId,
    };
});
hooks/spec.ts
New file
@@ -0,0 +1,115 @@
import { defineStore } from "pinia";
import { computed, ref } from "vue";
type Action = "select" | "spCart" | "buy" | "edit";
export const useSpec = defineStore("goods.spec", () => {
    const visible = ref(false);
    // 打开类型
    const action = ref<Action>("select");
    // 商品信息
    const goods = ref<Eps.GoodsInfoEntity>();
    // 已选规格信息
    const info = ref<Eps.GoodsSpecEntity>();
    // 已选数量
    const num = ref(1);
    // 指定规格id
    const specId = ref();
    // 回调函数
    let cb: ((action: Action) => void) | undefined;
    // 选中文案
    const text = computed(() => {
        return info.value ? `已选:${info.value.name}` : "选择规格";
    });
    // 打开弹窗
    function open(options: {
        action: Action;
        goods: Eps.GoodsInfoEntity;
        spec?: Eps.GoodsSpecEntity;
        specId?: number;
        count?: number;
        item?: OrderGoods;
        callback?: typeof cb;
    }) {
        if (!options.goods) {
            return false;
        }
        visible.value = true;
        action.value = options.action || "select";
        goods.value = options.goods;
        specId.value = options.specId || options.spec?.id || specId.value;
        if (options.spec) {
            info.value = options.spec;
        }
        cb = options.callback;
        if (options.action == "edit") {
            num.value = options.count || 0;
        }
    }
    // 关闭弹窗
    function close() {
        visible.value = false;
    }
    // 设置选中规格
    function set(data?: Eps.GoodsSpecEntity) {
        info.value = data;
        setId(data?.id!);
    }
    // 设置规格id
    function setId(id: number) {
        specId.value = id;
    }
    // 设置数量
    function setNum(val?: number) {
        num.value = val || 0;
    }
    // 回调
    function emit(action: Action) {
        if (cb) {
            cb(action);
        }
    }
    // 清空选项
    function clear() {
        num.value = 1;
        info.value = undefined;
        goods.value = undefined;
        specId.value = undefined;
        cb = undefined;
    }
    return {
        visible,
        action,
        goods,
        info,
        emit,
        num,
        setNum,
        specId,
        text,
        open,
        close,
        set,
        setId,
        clear,
    };
});
index.html
New file
@@ -0,0 +1,24 @@
<!doctype html>
<html>
    <head>
        <meta charset="UTF-8" />
        <script>
            var coverSupport =
                "CSS" in window &&
                typeof CSS.supports === "function" &&
                (CSS.supports("top: env(a)") || CSS.supports("top: constant(a)"));
            document.write(
                '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
                    (coverSupport ? ", viewport-fit=cover" : "") +
                    '" />',
            );
        </script>
        <title></title>
        <!--preload-links-->
        <!--app-context-->
    </head>
    <body>
        <div id="app"><!--app-html--></div>
        <script type="module" src="/main.ts"></script>
    </body>
</html>
locale/en.json
New file
@@ -0,0 +1,132 @@
{
  "已阅读并同意": "Read and Agree",
  "和": "and",
  "请先勾选同意后再进行登录": "Please check the agreement before logging in",
  "page.设置": "Settings",
  "page.编辑": "Edit",
  "page.关于我们": "About Us",
  "演示": "Demo",
  "官网": "Official Website",
  "系统": "System",
  "我的": "Mine",
  "清空": "Clear",
  "确认": "Confirm",
  "暂无数据": "No data",
  "取消": "Cancel",
  "账号": "Account",
  "头像": "Avatar",
  "昵称": "Nickname",
  "手机号": "Mobile number",
  "关于": "About",
  "用户协议": "User agreement",
  "隐私政策": "Privacy policy",
  "切换账号": "Switch account",
  "退出登录": "Log out",
  "头像更新成功": "Avatar updated successfully",
  "手机号登录": "Log in with mobile number",
  "请填写手机号码": "Please fill in the mobile number",
  "微信一键登录": "One-click login with WeChat",
  "其他登录方式": "Other login methods",
  "获取你的头像、昵称": "Get your avatar and nickname",
  "用于向用户提供有辨识度的界面": "Used to provide a recognizable interface for users",
  "请选择所在地区": "Please select your region",
  "搜索": "Search",
  "顶部": "Top",
  "正在刷新": "Refreshing",
  "下拉刷新": "Pull down to refresh",
  "释放刷新": "Release to refresh",
  "选择支付方式": "Select payment method",
  "取消支付": "Cancel payment",
  "微信支付": "WeChat Pay",
  "支付宝支付": "Alipay",
  "加载中": "Loading",
  "提示": "Tip",
  "上拉加载更多": "Pull up to load more",
  "没有更多了": "No more",
  "搜索关键字": "Search keyword",
  "请填写": "Please fill in",
  "上一步": "Previous step",
  "下一步": "Next step",
  "跳过": "Skip",
  "完成": "Finish",
  "上传/拍摄": "Upload/Take Photo",
  "图片地址错误": "Image Address Error",
  "拖动验证": "Drag to Verify",
  "请向右拖动滑块完成拼图": "Drag the slider to complete the puzzle",
  "请拖动滑块旋转至正确位置": "Drag the slider to rotate to the correct position",
  "分享至": "Share to",
  "QQ好友": "QQ Friends",
  "朋友圈": "Moments",
  "微信好友": "WeChat Friends",
  "确定": "OK",
  "请选择": "Please Select",
  "选择": "Select",
  "选择日期": "Select Date",
  "请选择时间": "Please Select Time",
  "年": "Year",
  "月": "Month",
  "日": "Day",
  "时": "Hour",
  "分": "Minute",
  "秒": "Second",
  "未登录": "Not logged in",
  "写签名会更容易获得别人的关注哦~": "Writing a signature makes it easier to gain others' attention~",
  "总点击": "Total clicks",
  "赞": "Like",
  "关注": "Follow",
  "粉丝": "Fans",
  "接单模式": "Order receiving mode",
  "已关闭": "Closed",
  "消息通知": "Message notification",
  "已开启": "Enabled",
  "我的订单": "My orders",
  "我的账单": "My bills",
  "观看历史": "Viewing history",
  "数据看板": "Data dashboard",
  "邀请好友": "Invite friends",
  "设置": "Settings",
  "待支付": "Pending payment",
  "未发货": "Not shipped",
  "已发货": "Shipped",
  "售后 / 退款": "After-sales / Refund",
  "订单模块不存在,请在插件市场中下载": "Order module does not exist. Please download it in the plugin market.",
  "财务模块不存在,请在插件市场中下载": "Finance module does not exist. Please download it in the plugin market.",
  "消息模块不存在,请在插件市场中下载": "Message module does not exist. Please download it in the plugin market.",
  "uniapp快速开发脚手架": "Uniapp rapid development scaffolding",
  "选择语言": "Select Language",
  "插件 / 模块": "Plugin/Module",
  "服务异常": "Service exception",
  "请在微信浏览器中打开": "Please open in WeChat browser",
  "已取消支付": "Payment has been cancelled",
  "支付失败": "Payment failed",
  "授权信息仅用于用户登录": "Authorization information is only used for user login",
  "登录授权失败": "Login authorization failed",
  "获取短信验证码": "Get SMS verification code",
  "验证码": "Verification code",
  "发送短信": "Send SMS",
  "{n}s后重新获取": "Retry in {n}s",
  "获取验证码": "Get verification code",
  "短信已发送,请查收": "SMS has been sent. Please check.",
  "请填写验证码": "Please fill in the verification code",
  "请填写正确的手机号格式": "Please fill in the correct mobile phone number format",
  "请填写昵称、限16个字符或汉字": "Please fill in your nickname, limited to 16 characters or Chinese characters",
  "保存": "Save",
  "通过手机登录": "Log in via mobile phone",
  "通过微信登录": "Log in via WeChat",
  "温馨提示": "Warm reminder",
  "您还未安装微信~": "You haven't installed WeChat yet~",
  "去下载": "Go to download",
  "手机号一键登录": "One-key login with mobile number",
  "当前环境不支持一键登录,请切换至验证码登录": "One-key login is not supported in the current environment. Please switch to verification code login",
  "一键登录": "One-key login",
  "我已阅读并同意": "I have read and agreed",
  "并使用本机号码登录": "And log in with this mobile number",
  "请上传头像": "Please upload an avatar",
  "请输入昵称": "Please enter your nickname",
  "请填写昵称": "Please fill in your nickname",
  "用户信息保存成功": "User information saved successfully",
  "输入验证码": "Enter verification code",
  "已发送至": "Has been sent to",
  "登录失效,请重试~": "Login expired, please try again~",
  "联系我们": "Contact us"
}
locale/es.json
New file
@@ -0,0 +1,132 @@
{
  "已阅读并同意": "Leído y aceptado",
  "和": "y",
  "请先勾选同意后再进行登录": "Por favor, marque la casilla de verificación de aceptación antes de iniciar sesión",
  "page.设置": "Configuración",
  "page.编辑": "Edición",
  "page.关于我们": "Sobre nosotros",
  "演示": "Demostración",
  "官网": "Sitio web oficial",
  "系统": "Sistema",
  "我的": "Mío",
  "请选择所在地区": "Por favor, selecciona la región",
  "搜索": "Buscar",
  "顶部": "Superior",
  "正在刷新": "Refrescando",
  "下拉刷新": "Deslizar hacia abajo para actualizar",
  "释放刷新": "Soltar para actualizar",
  "选择支付方式": "Seleccionar método de pago",
  "取消支付": "Cancelar pago",
  "微信支付": "Pago con WeChat",
  "支付宝支付": "Pago con Alipay",
  "加载中": "Cargando",
  "提示": "Advertencia",
  "上拉加载更多": "Deslizar hacia arriba para cargar más",
  "没有更多了": "No hay más",
  "搜索关键字": "Palabra clave de búsqueda",
  "请填写": "Por favor, complete",
  "上一步": "Paso anterior",
  "下一步": "Paso siguiente",
  "跳过": "Saltar",
  "完成": "Completar",
  "未登录": "No registrado",
  "写签名会更容易获得别人的关注哦~": "Escribir una firma te ayudará a llamar más la atención de los demás.",
  "总点击": "Total de clics",
  "赞": "Me gusta",
  "关注": "Seguir",
  "粉丝": "Seguidores",
  "接单模式": "Modo de recibir pedidos",
  "已关闭": "Cerrado",
  "消息通知": "Notificaciones de mensajes",
  "已开启": "Encendido",
  "我的订单": "Mis pedidos",
  "我的账单": "Mi factura",
  "观看历史": "Historial de reproducción",
  "数据看板": "Tablero de datos",
  "邀请好友": "Invitar amigos",
  "设置": "Configuración",
  "待支付": "Por pagar",
  "未发货": "No enviado",
  "已发货": "Enviado",
  "售后 / 退款": "Servicio después de la venta / Reembolso",
  "清空": "Limpiar",
  "确认": "Confirmar",
  "暂无数据": "Sin datos",
  "取消": "Cancelar",
  "账号": "Cuenta",
  "头像": "Avatar",
  "昵称": "Nombre de usuario",
  "手机号": "Número de teléfono",
  "关于": "Acerca de",
  "用户协议": "Términos de usuario",
  "隐私政策": "Política de privacidad",
  "切换账号": "Cambiar de cuenta",
  "退出登录": "Cerrar sesión",
  "头像更新成功": "Actualización de avatar exitosa",
  "手机号登录": "Inicio de sesión con número de teléfono",
  "请填写手机号码": "Por favor ingrese su número de teléfono",
  "微信一键登录": "Inicio de sesión con WeChat de un solo clic",
  "其他登录方式": "Otros métodos de inicio de sesión",
  "获取你的头像、昵称": "Obtener su avatar y nombre de usuario",
  "用于向用户提供有辨识度的界面": "Para proporcionar una interfaz identificable al usuario",
  "上传/拍摄": "Subir/Tomar",
  "图片地址错误": "Dirección de imagen incorrecta",
  "拖动验证": "Arrastrar para validar",
  "请向右拖动滑块完成拼图": "Arrastre el deslizador hacia la derecha para completar el rompecabezas",
  "请拖动滑块旋转至正确位置": "Arrastre el deslizador y girelo hasta la posición correcta",
  "分享至": "Compartir a",
  "QQ好友": "Amigos de QQ",
  "朋友圈": "Momento",
  "微信好友": "Amigos de WeChat",
  "确定": "Aceptar",
  "请选择": "Seleccionar",
  "选择": "Selección",
  "选择日期": "Seleccionar fecha",
  "请选择时间": "Seleccionar hora",
  "年": "Año",
  "月": "Mes",
  "日": "Día",
  "时": "Hora",
  "分": "Minuto",
  "秒": "Segundo",
  "请填写昵称、限16个字符或汉字": "Por favor complete el nombre de usuario, máximo 16 caracteres o chinos",
  "保存": "Guardar",
  "通过手机登录": "Iniciar sesión con el teléfono móvil",
  "通过微信登录": "Iniciar sesión con WeChat",
  "温馨提示": "Advertencia",
  "您还未安装微信~": "Aún no has instalado WeChat~",
  "去下载": "Ir a descargar",
  "手机号一键登录": "Iniciar sesión con un solo toque del número de teléfono",
  "当前环境不支持一键登录,请切换至验证码登录": "La autenticación unificada no está disponible en el entorno actual. Cambie a la autenticación con código de verificación",
  "一键登录": "Autenticación unificada",
  "我已阅读并同意": "He leído y aceptado",
  "并使用本机号码登录": "Y usar el número de este teléfono para iniciar sesión",
  "请上传头像": "Por favor cargue una foto de perfil",
  "请输入昵称": "Por favor ingrese el nombre de usuario",
  "请填写昵称": "Por favor complete el nombre de usuario",
  "用户信息保存成功": "La información del usuario se ha guardado correctamente",
  "输入验证码": "Ingrese el código de verificación",
  "已发送至": "Se ha enviado a",
  "登录失效,请重试~": "La sesión ha caducado. Inténtelo de nuevo~",
  "联系我们": "Contáctenos",
  "订单模块不存在,请在插件市场中下载": "El módulo de pedidos no existe. Descárguelo en el mercado de complementos.",
  "财务模块不存在,请在插件市场中下载": "El módulo financiero no existe. Descárguelo en el mercado de complementos.",
  "消息模块不存在,请在插件市场中下载": "El módulo de mensajes no existe. Descárguelo en el mercado de complementos.",
  "uniapp快速开发脚手架": "Entorno de desarrollo rápido de uniapp",
  "选择语言": "Seleccionar idioma",
  "插件 / 模块": "Complementos / Módulos",
  "服务异常": "Error en el servicio",
  "请在微信浏览器中打开": "Abra en el navegador de WeChat",
  "已取消支付": "Pago cancelado",
  "支付失败": "Fallo en el pago",
  "授权信息仅用于用户登录": "La información de autorización solo se utiliza para el inicio de sesión de usuario",
  "登录授权失败": "Fallo en la autorización de inicio de sesión",
  "获取短信验证码": "Obtener código de verificación por SMS",
  "验证码": "Código de verificación",
  "发送短信": "Enviar SMS",
  "{n}s后重新获取": "Obtener de nuevo en {n} s",
  "获取验证码": "Obtener código de verificación",
  "短信已发送,请查收": "El SMS se ha enviado. Compruebe su bandeja de entrada.",
  "请填写验证码": "Por favor, rellene el código de verificación",
  "请填写正确的手机号格式": "Por favor, introduzca un número de teléfono válido"
}
locale/fr.json
New file
@@ -0,0 +1 @@
{}
locale/index.ts
New file
@@ -0,0 +1,53 @@
import { createI18n } from "vue-i18n";
import zhHans from "./zh-Hans.json";
import zhHant from "./zh-Hant.json";
import en from "./en.json";
import es from "./es.json";
import fr from "./fr.json";
const i18n = createI18n({
    locale: uni.getLocale(),
    // 配置后,使用命令 cool-i18n create 翻译,会自动更新 locale 目录
    messages: {
        "zh-Hans": zhHans,
        "zh-Hant": zhHant,
        en,
        es,
    },
});
const localeMap: { [key: string]: string } = {
    "zh-Hans": "zh-cn",
    "zh-Hant": "zh-tw",
};
function t(name: string, data?: any) {
    let d = i18n.global.t(name, data);
    if (data) {
        for (const i in data) {
            d = d.replace(`{${i}}`, data[i]);
        }
    }
    return d;
}
function setLocale(locale: string) {
    uni.setLocale(locale);
    i18n.global.locale = locale;
}
function getLocale(): string {
    const locale = uni.getLocale();
    for (const i in localeMap) {
        if (i == locale) {
            return localeMap[i];
        }
    }
    return locale;
}
export { i18n, t, setLocale, getLocale };
locale/zh-Hans.json
New file
@@ -0,0 +1,132 @@
{
  "上传/拍摄": "上传/拍摄",
  "图片地址错误": "图片地址错误",
  "拖动验证": "拖动验证",
  "请向右拖动滑块完成拼图": "请向右拖动滑块完成拼图",
  "请拖动滑块旋转至正确位置": "请拖动滑块旋转至正确位置",
  "分享至": "分享至",
  "QQ好友": "QQ好友",
  "朋友圈": "朋友圈",
  "微信好友": "微信好友",
  "确定": "确定",
  "请选择": "请选择",
  "选择": "选择",
  "选择日期": "选择日期",
  "请选择时间": "请选择时间",
  "年": "年",
  "月": "月",
  "日": "日",
  "时": "时",
  "分": "分",
  "秒": "秒",
  "请选择所在地区": "请选择所在地区",
  "搜索": "搜索",
  "顶部": "顶部",
  "正在刷新": "正在刷新",
  "下拉刷新": "下拉刷新",
  "释放刷新": "释放刷新",
  "选择支付方式": "选择支付方式",
  "取消支付": "取消支付",
  "微信支付": "微信支付",
  "支付宝支付": "支付宝支付",
  "加载中": "加载中",
  "提示": "提示",
  "上拉加载更多": "上拉加载更多",
  "没有更多了": "没有更多了",
  "搜索关键字": "搜索关键字",
  "请填写": "请填写",
  "上一步": "上一步",
  "下一步": "下一步",
  "跳过": "跳过",
  "完成": "完成",
  "清空": "清空",
  "确认": "确认",
  "暂无数据": "暂无数据",
  "取消": "取消",
  "账号": "账号",
  "头像": "头像",
  "昵称": "昵称",
  "手机号": "手机号",
  "关于": "关于",
  "用户协议": "用户协议",
  "隐私政策": "隐私政策",
  "切换账号": "切换账号",
  "退出登录": "退出登录",
  "头像更新成功": "头像更新成功",
  "手机号登录": "手机号登录",
  "请填写手机号码": "请填写手机号码",
  "微信一键登录": "微信一键登录",
  "其他登录方式": "其他登录方式",
  "获取你的头像、昵称": "获取你的头像、昵称",
  "用于向用户提供有辨识度的界面": "用于向用户提供有辨识度的界面",
  "请填写昵称、限16个字符或汉字": "请填写昵称、限16个字符或汉字",
  "保存": "保存",
  "通过手机登录": "通过手机登录",
  "通过微信登录": "通过微信登录",
  "温馨提示": "温馨提示",
  "您还未安装微信~": "您还未安装微信~",
  "去下载": "去下载",
  "手机号一键登录": "手机号一键登录",
  "当前环境不支持一键登录,请切换至验证码登录": "当前环境不支持一键登录,请切换至验证码登录",
  "一键登录": "一键登录",
  "我已阅读并同意": "我已阅读并同意",
  "并使用本机号码登录": "并使用本机号码登录",
  "请上传头像": "请上传头像",
  "请输入昵称": "请输入昵称",
  "请填写昵称": "请填写昵称",
  "用户信息保存成功": "用户信息保存成功",
  "输入验证码": "输入验证码",
  "已发送至": "已发送至",
  "登录失效,请重试~": "登录失效,请重试~",
  "联系我们": "联系我们",
  "未登录": "未登录",
  "写签名会更容易获得别人的关注哦~": "写签名会更容易获得别人的关注哦~",
  "总点击": "总点击",
  "赞": "赞",
  "关注": "关注",
  "粉丝": "粉丝",
  "接单模式": "接单模式",
  "已关闭": "已关闭",
  "消息通知": "消息通知",
  "已开启": "已开启",
  "我的订单": "我的订单",
  "我的账单": "我的账单",
  "观看历史": "观看历史",
  "数据看板": "数据看板",
  "邀请好友": "邀请好友",
  "设置": "设置",
  "待支付": "待支付",
  "未发货": "未发货",
  "已发货": "已发货",
  "售后 / 退款": "售后 / 退款",
  "订单模块不存在,请在插件市场中下载": "订单模块不存在,请在插件市场中下载",
  "财务模块不存在,请在插件市场中下载": "财务模块不存在,请在插件市场中下载",
  "消息模块不存在,请在插件市场中下载": "消息模块不存在,请在插件市场中下载",
  "uniapp快速开发脚手架": "uniapp快速开发脚手架",
  "选择语言": "选择语言",
  "插件 / 模块": "插件 / 模块",
  "服务异常": "服务异常",
  "请在微信浏览器中打开": "请在微信浏览器中打开",
  "已取消支付": "已取消支付",
  "支付失败": "支付失败",
  "授权信息仅用于用户登录": "授权信息仅用于用户登录",
  "登录授权失败": "登录授权失败",
  "获取短信验证码": "获取短信验证码",
  "验证码": "验证码",
  "发送短信": "发送短信",
  "{n}s后重新获取": "{n}s后重新获取",
  "获取验证码": "获取验证码",
  "短信已发送,请查收": "短信已发送,请查收",
  "请填写验证码": "请填写验证码",
  "请填写正确的手机号格式": "请填写正确的手机号格式",
  "已阅读并同意": "已阅读并同意",
  "和": "和",
  "请先勾选同意后再进行登录": "请先勾选同意后再进行登录",
  "page.设置": "设置",
  "page.编辑": "编辑",
  "page.关于我们": "关于我们",
  "演示": "演示",
  "官网": "官网",
  "系统": "系统",
  "我的": "我的"
}
locale/zh-Hant.json
New file
@@ -0,0 +1,132 @@
{
  "已阅读并同意": "已閱讀並同意",
  "和": "和",
  "请先勾选同意后再进行登录": "請先勾選同意後再進行登錄",
  "page.设置": "設置",
  "page.编辑": "編輯",
  "page.关于我们": "關於我們",
  "演示": "演示",
  "官网": "官網",
  "系统": "系統",
  "我的": "我的",
  "请选择所在地区": "請選擇所在地區",
  "搜索": "搜尋",
  "顶部": "頂部",
  "正在刷新": "正在刷新",
  "下拉刷新": "下拉刷新",
  "释放刷新": "釋放刷新",
  "选择支付方式": "選擇支付方式",
  "取消支付": "取消支付",
  "微信支付": "微信支付",
  "支付宝支付": "支付寶支付",
  "加载中": "加載中",
  "提示": "提示",
  "上拉加载更多": "上拉加載更多",
  "没有更多了": "沒有更多了",
  "搜索关键字": "搜索關鍵字",
  "请填写": "請填寫",
  "上一步": "上一步",
  "下一步": "下一步",
  "跳过": "跳過",
  "完成": "完成",
  "上传/拍摄": "上傳/拍攝",
  "图片地址错误": "圖片地址錯誤",
  "拖动验证": "拖動驗證",
  "请向右拖动滑块完成拼图": "請向右拖動滑塊完成拼圖",
  "请拖动滑块旋转至正确位置": "請拖動滑塊旋轉至正確位置",
  "分享至": "分享至",
  "QQ好友": "QQ好友",
  "朋友圈": "朋友圈",
  "微信好友": "微信好友",
  "确定": "確定",
  "请选择": "請選擇",
  "选择": "選擇",
  "选择日期": "選擇日期",
  "请选择时间": "請選擇時間",
  "年": "年",
  "月": "月",
  "日": "日",
  "时": "時",
  "分": "分",
  "秒": "秒",
  "未登录": "未登錄",
  "写签名会更容易获得别人的关注哦~": "寫簽名會更容易獲得別人的關注哦~",
  "总点击": "總點擊",
  "赞": "讚",
  "关注": "關注",
  "粉丝": "粉絲",
  "接单模式": "接單模式",
  "已关闭": "已關閉",
  "消息通知": "消息通知",
  "已开启": "已開啟",
  "我的订单": "我的訂單",
  "我的账单": "我的賬單",
  "观看历史": "觀看歷史",
  "数据看板": "數據看板",
  "邀请好友": "邀請好友",
  "设置": "設置",
  "待支付": "待支付",
  "未发货": "未發貨",
  "已发货": "已發貨",
  "售后 / 退款": "售後 / 退款",
  "清空": "清空",
  "确认": "確認",
  "暂无数据": "暂无數據",
  "取消": "取消",
  "账号": "帳號",
  "头像": "頭像",
  "昵称": "暱稱",
  "手机号": "手機號",
  "关于": "關於",
  "用户协议": "用戶協議",
  "隐私政策": "隱私政策",
  "切换账号": "切換帳號",
  "退出登录": "退出登錄",
  "头像更新成功": "頭像更新成功",
  "手机号登录": "手機號登錄",
  "请填写手机号码": "請填寫手機號碼",
  "微信一键登录": "微信一鍵登錄",
  "其他登录方式": "其他登錄方式",
  "获取你的头像、昵称": "獲取你的頭像、暱稱",
  "用于向用户提供有辨识度的界面": "用於向用戶提供有辨识度的界面",
  "请填写昵称、限16个字符或汉字": "請填寫暱稱、限16個字元或漢字",
  "保存": "儲存",
  "通过手机登录": "透過手機登入",
  "通过微信登录": "透過微信登入",
  "温馨提示": "溫馨提示",
  "您还未安装微信~": "您還未安裝微信~",
  "去下载": "去下載",
  "手机号一键登录": "手機號一鍵登入",
  "当前环境不支持一键登录,请切换至验证码登录": "當前環境不支援一鍵登入,請切換至驗證碼登入",
  "一键登录": "一鍵登入",
  "我已阅读并同意": "我已閱讀並同意",
  "并使用本机号码登录": "並使用本機號碼登入",
  "请上传头像": "請上傳頭像",
  "请输入昵称": "請輸入暱稱",
  "请填写昵称": "請填寫暱稱",
  "用户信息保存成功": "用戶資訊儲存成功",
  "输入验证码": "輸入驗證碼",
  "已发送至": "已發送至",
  "登录失效,请重试~": "登入失效,請重試~",
  "联系我们": "聯繫我們",
  "订单模块不存在,请在插件市场中下载": "訂單模組不存在,請在插件市場中下載",
  "财务模块不存在,请在插件市场中下载": "財務模組不存在,請在插件市場中下載",
  "消息模块不存在,请在插件市场中下载": "消息模組不存在,請在插件市場中下載",
  "uniapp快速开发脚手架": "uniapp快速開發腳手架",
  "选择语言": "選擇語言",
  "插件 / 模块": "插件 / 模組",
  "服务异常": "服務異常",
  "请在微信浏览器中打开": "請在微信瀏覽器中打開",
  "已取消支付": "已取消支付",
  "支付失败": "支付失敗",
  "授权信息仅用于用户登录": "授權信息僅用於用戶登錄",
  "登录授权失败": "登錄授權失敗",
  "获取短信验证码": "獲取短信驗證碼",
  "验证码": "驗證碼",
  "发送短信": "發送短信",
  "{n}s后重新获取": "{n}s後重新獲取",
  "获取验证码": "獲取驗證碼",
  "短信已发送,请查收": "短信已發送,請查收",
  "请填写验证码": "請填寫驗證碼",
  "请填写正确的手机号格式": "請填寫正確的手機號格式"
}
main.ts
New file
@@ -0,0 +1,16 @@
import { createSSRApp } from "vue";
import { bootstrap } from "/@/cool/bootstrap";
import App from "./App.vue";
import { i18n } from "./locale";
import "./router";
export function createApp() {
    const app = createSSRApp(App);
    app.use(i18n);
    bootstrap(app);
    return {
        app,
    };
}
manifest.json
New file
@@ -0,0 +1,197 @@
{
    "name": "再生资源",
    "appid": "__UNI__75A96B5",
    "description": "",
    "versionName": "1.0.0",
    "versionCode": 100,
    "transformPx": false,
    "app-plus": {
        "usingComponents": true,
        "splashscreen": {
            "alwaysShowBeforeRender": true,
            "waiting": true,
            "autoclose": true,
            "delay": 0
        },
        "modules": {
            "VideoPlayer": {},
            "Share": {},
            "Payment": {},
            "OAuth": {},
            "Geolocation": {},
            "Camera": {},
            "Push": {}
        },
        "distribute": {
            "android": {
                "permissions": [
                    "<uses-feature android:name=\"android.hardware.camera\"/>",
                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
                    "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
                    "<uses-permission android:name=\"android.permission.CAPTURE_VIDEO_OUTPUT\"/>",
                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
                    "<uses-permission android:name=\"android.permission.INTERNET\"/>",
                    "<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
                    "<uses-permission android:name=\"android.permission.STATUS_BAR\"/>",
                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
                    "<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
                ],
                "abiFilters": [
                    "armeabi-v7a",
                    "arm64-v8a",
                    "x86"
                ]
            },
            "ios": {
                "capabilities": {
                    "entitlements": {
                        "com.apple.developer.associated-domains": []
                    }
                },
                "idfa": true,
                "privacyDescription": {
                    "NSUserTrackingUsageDescription": "请放心,开启权限不会获取您在其他站点的隐私信息,该权限仅用于标识设备并保障服务安全与提示浏览体验"
                },
                "dSYMs": false
            },
            "sdkConfigs": {
                "payment": {
                    "alipay": {
                        "__platform__": [
                            "ios",
                            "android"
                        ]
                    },
                    "weixin": {
                        "__platform__": [
                            "ios",
                            "android"
                        ],
                        "appid": "wx348f72db1512fa2e",
                        "UniversalLinks": ""
                    }
                },
                "ad": {},
                "share": {
                    "weixin": {
                        "appid": "wx348f72db1512fa2e",
                        "UniversalLinks": ""
                    }
                },
                "oauth": {
                    "weixin": {
                        "appid": "wx348f72db1512fa2e",
                        "appsecret": "test",
                        "UniversalLinks": ""
                    },
                    "apple": {}
                },
                "geolocation": {
                    "system": {
                        "__platform__": [
                            "ios",
                            "android"
                        ]
                    }
                },
                "push": {
                    "unipush": {
                        "offline": false,
                        "icons": {
                            "small": {
                                "ldpi": ""
                            }
                        }
                    }
                },
                "maps": {}
            },
            "icons": {
                "android": {
                    "hdpi": "unpackage/res/icons/72x72.png",
                    "xhdpi": "unpackage/res/icons/96x96.png",
                    "xxhdpi": "unpackage/res/icons/144x144.png",
                    "xxxhdpi": "unpackage/res/icons/192x192.png"
                },
                "ios": {
                    "appstore": "unpackage/res/icons/1024x1024.png",
                    "ipad": {
                        "app": "unpackage/res/icons/76x76.png",
                        "app@2x": "unpackage/res/icons/152x152.png",
                        "notification": "unpackage/res/icons/20x20.png",
                        "notification@2x": "unpackage/res/icons/40x40.png",
                        "proapp@2x": "unpackage/res/icons/167x167.png",
                        "settings": "unpackage/res/icons/29x29.png",
                        "settings@2x": "unpackage/res/icons/58x58.png",
                        "spotlight": "unpackage/res/icons/40x40.png",
                        "spotlight@2x": "unpackage/res/icons/80x80.png"
                    },
                    "iphone": {
                        "app@2x": "unpackage/res/icons/120x120.png",
                        "app@3x": "unpackage/res/icons/180x180.png",
                        "notification@2x": "unpackage/res/icons/40x40.png",
                        "notification@3x": "unpackage/res/icons/60x60.png",
                        "settings@2x": "unpackage/res/icons/58x58.png",
                        "settings@3x": "unpackage/res/icons/87x87.png",
                        "spotlight@2x": "unpackage/res/icons/80x80.png",
                        "spotlight@3x": "unpackage/res/icons/120x120.png"
                    }
                }
            },
            "splashscreen": {
                "useOriginalMsgbox": true
            }
        },
        "safearea": {
            "bottom": {
                "offset": "none"
            }
        },
        "uniStatistics": {
            "enable": true
        }
    },
    "quickapp": {},
    "mp-weixin": {
        "appid": "wxdebc4de0b5584ca4",
        "setting": {
            "urlCheck": true,
            "es6": false
        },
        "usingComponents": true
    },
    "mp-alipay": {
        "usingComponents": true
    },
    "mp-baidu": {
        "usingComponents": true
    },
    "mp-toutiao": {
        "usingComponents": true
    },
    "uniStatistics": {
        "enable": false
    },
    "vueVersion": "3",
    "h5": {
        "router": {
            "base": "./",
            "mode": "hash"
        },
        "devServer": {
            "https": false
        }
    },
    "locale": "zh-Hans"
}
package-lock.json
New file
Diff too large
package.json
New file
@@ -0,0 +1,31 @@
{
    "name": "cool-uni",
    "version": "8.0.0",
    "license": "MIT",
    "dependencies": {
        "@dcloudio/uni-app": "3.0.0-3081220230817001",
        "@hyoga/uni-socket.io": "^3.0.4",
        "dayjs": "^1.11.13",
        "js-pinyin": "^0.2.5",
        "lodash-es": "^4.17.21",
        "md5": "^2.3.0",
        "pinia": "^2.1.7",
        "qrcode": "^1.5.4",
        "vue": "^3.5.13",
        "vue-i18n": "9.1.9",
        "weixin-js-sdk": "^1.6.5"
    },
    "engines": {
        "node": ">= 16"
    },
    "devDependencies": {
        "@cool-vue/vite-plugin": "^8.0.3",
        "@dcloudio/types": "^3.4.14",
        "@types/lodash-es": "^4.17.12",
        "@types/md5": "^2.3.2",
        "@types/node": "^20.11.26",
        "@vue/tsconfig": "^0.5.1",
        "typescript": "~5.5.4",
        "vite": "^5.4.14"
    }
}
pages.json
New file
@@ -0,0 +1,306 @@
{
    "pages": [
        {
            "path": "pages/index/home",
            "style": {
                "navigationStyle": "custom"
            }
        },
        {
            "path": "pages/recycle/home",
            "style": {
                "disableScroll": true,
                "navigationBarTitleText": "资源回收"
            }
        },
        {
            "path": "pages/index/category",
            "style": {
                "disableScroll": true,
                "navigationBarTitleText": "分类"
            }
        },
        {
            "path": "pages/index/shopping-cart",
            "style": {
                "navigationStyle": "custom",
                "navigationBarTitleText": "购物车"
            }
        },
        {
            "path": "pages/index/my",
            "style": {
                "navigationStyle": "custom",
                "navigationBarTitleText": "我的",
                "navigationBarTextStyle": "white"
            }
        }
    ],
    "subPackages": [
        {
            "root": "uni_modules/cool-app/pages",
            "pages": [
                {
                    "path": "version/demo",
                    "style": {
                        "navigationBarTitleText": "版本升级"
                    }
                },
                {
                    "path": "complain/detail",
                    "style": {
                        "navigationBarTitleText": "投诉详情"
                    }
                },
                {
                    "path": "complain/list",
                    "style": {
                        "navigationBarTitleText": "投诉列表"
                    }
                },
                {
                    "path": "complain/submit",
                    "style": {
                        "navigationStyle": "custom"
                    }
                },
                {
                    "path": "feedback/detail",
                    "style": {
                        "navigationBarTitleText": "反馈详情"
                    }
                },
                {
                    "path": "feedback/list",
                    "style": {
                        "navigationBarTitleText": "反馈列表"
                    }
                },
                {
                    "path": "feedback/submit",
                    "style": {
                        "navigationStyle": "custom"
                    }
                }
            ],
            "isTemp": true
        },
        {
            "root": "uni_modules/cool-cs/pages",
            "pages": [
                {
                    "path": "chat",
                    "style": {
                        "navigationBarTitleText": "客服聊天"
                    }
                }
            ],
            "isTemp": true
        },
        {
            "root": "pages/market",
            "pages": [
                {
                    "path": "coupon",
                    "style": {
                        "navigationBarTitleText": "我的优惠券",
                        "enablePullDownRefresh": true
                    }
                }
            ]
        },
        {
            "root": "pages/recycle",
            "pages": [
                {
                    "path": "record",
                    "style": {
                        "navigationBarTitleText": "回收记录",
                        "enablePullDownRefresh": true
                    }
                },
                {
                    "path": "appointment",
                    "style": {
                        "navigationBarTitleText": "预约回收",
                        "enablePullDownRefresh": true
                    }
                },
                {
                    "path": "transfer",
                    "style": {
                        "navigationBarTitleText": "转赠记录",
                        "enablePullDownRefresh": true
                    }
                }
            ]
        },
        {
            "root": "pages/order",
            "pages": [
                {
                    "path": "refund",
                    "style": {
                        "navigationBarTitleText": "退款申请"
                    }
                },
                {
                    "path": "comment",
                    "style": {
                        "navigationBarTitleText": "商品评价"
                    }
                },
                {
                    "path": "logistics",
                    "style": {
                        "navigationBarTitleText": "物流详情",
                        "enablePullDownRefresh": true
                    }
                },
                {
                    "path": "list",
                    "style": {
                        "navigationBarTitleText": "订单列表",
                        "enablePullDownRefresh": true
                    }
                },
                {
                    "path": "detail",
                    "style": {
                        "navigationBarTitleText": "订单详情",
                        "enablePullDownRefresh": true,
                        "navigationStyle": "custom"
                    }
                },
                {
                    "path": "submit",
                    "style": {
                        "navigationBarTitleText": "订单提交"
                    }
                }
            ]
        },
        {
            "root": "pages/goods",
            "pages": [
                {
                    "path": "search",
                    "style": {
                        "navigationStyle": "custom"
                    }
                },
                {
                    "path": "list",
                    "style": {
                        "navigationStyle": "custom",
                        "enablePullDownRefresh": true
                    }
                },
                {
                    "path": "detail",
                    "style": {
                        "navigationStyle": "custom",
                        "enablePullDownRefresh": true
                    }
                },
                {
                    "path": "comment",
                    "style": {
                        "navigationBarTitleText": "商品评价"
                    }
                }
            ]
        },
        {
            "root": "pages/user",
            "pages": [
                {
                    "path": "address-list",
                    "style": {
                        "navigationBarTitleText": "收货地址",
                        "enablePullDownRefresh": true
                    }
                },
                {
                    "path": "address-edit",
                    "style": {
                        "navigationBarTitleText": ""
                    }
                },
                {
                    "path": "doc",
                    "style": {
                        "navigationBarTitleText": ""
                    }
                },
                {
                    "path": "set",
                    "style": {
                        "navigationBarTitleText": "设置"
                    }
                },
                {
                    "path": "edit",
                    "style": {
                        "navigationBarTitleText": "编辑"
                    }
                },
                {
                    "path": "login",
                    "style": {
                        "navigationStyle": "custom"
                    }
                },
                {
                    "path": "captcha",
                    "style": {
                        "navigationStyle": "custom"
                    }
                },
                {
                    "path": "about",
                    "style": {
                        "navigationBarTitleText": ""
                    }
                }
            ]
        }
    ],
    "globalStyle": {
        "navigationBarTextStyle": "black",
        "navigationBarTitleText": "再生资源",
        "navigationBarBackgroundColor": "#ffffff",
        "backgroundColor": "#f6f7fa"
    },
    "tabBar": {
        "backgroundColor": "#ffffff",
        "borderStyle": "white",
        "list": [
            {
                "pagePath": "pages/index/home",
                "iconPath": "static/icon/tabbar/home.png",
                "selectedIconPath": "static/icon/tabbar/home2.png"
            },
            {
                "pagePath": "pages/recycle/home",
                "iconPath": "static/icon/tabbar/recycle.png",
                "selectedIconPath": "static/icon/tabbar/recycle2.png"
            },
            {
                "pagePath": "pages/index/category",
                "iconPath": "static/icon/tabbar/category.png",
                "selectedIconPath": "static/icon/tabbar/category2.png"
            },
            {
                "pagePath": "pages/index/shopping-cart",
                "iconPath": "static/icon/tabbar/shopping-cart.png",
                "selectedIconPath": "static/icon/tabbar/shopping-cart2.png"
            },
            {
                "pagePath": "pages/index/my",
                "iconPath": "static/icon/tabbar/my.png",
                "selectedIconPath": "static/icon/tabbar/my2.png"
            }
        ]
    }
}
pages/demo/basic/button.vue
New file
@@ -0,0 +1,140 @@
<template>
    <cl-page :padding="20">
        <view class="page-demo-button">
            <cl-card label="基础用法">
                <cl-row :gutter="20">
                    <cl-col :span="8">
                        <cl-button fill @tap="onTap">默认</cl-button>
                    </cl-col>
                    <cl-col :span="8">
                        <cl-button type="primary" fill>主要</cl-button>
                    </cl-col>
                    <cl-col :span="8">
                        <cl-button type="success" fill>成功</cl-button>
                    </cl-col>
                    <cl-col :span="8">
                        <cl-button type="error" fill>失败</cl-button>
                    </cl-col>
                    <cl-col :span="8">
                        <cl-button type="warning" fill>警告</cl-button>
                    </cl-col>
                    <cl-col :span="8">
                        <cl-button type="info" fill>信息</cl-button>
                    </cl-col>
                </cl-row>
            </cl-card>
            <cl-card label="朴素">
                <cl-row :gutter="20">
                    <cl-col :span="8">
                        <cl-button type="primary" plain fill>主要</cl-button>
                    </cl-col>
                    <cl-col :span="8">
                        <cl-button type="success" plain fill>成功</cl-button>
                    </cl-col>
                    <cl-col :span="8">
                        <cl-button type="error" plain fill>失败</cl-button>
                    </cl-col>
                    <cl-col :span="8">
                        <cl-button type="warning" plain fill>警告</cl-button>
                    </cl-col>
                    <cl-col :span="8">
                        <cl-button type="info" plain fill>信息</cl-button>
                    </cl-col>
                </cl-row>
            </cl-card>
            <cl-card label="不同尺寸">
                <cl-button size="small">小</cl-button>
                <cl-button size="default">默认</cl-button>
                <cl-button size="large">大</cl-button>
                <cl-button :height="90" :width="200" :font-size="34">90*200</cl-button>
            </cl-card>
            <cl-card label="图标">
                <cl-button icon="cl-icon-search">搜索</cl-button>
                <cl-button icon="cl-icon-chart-bar">统计</cl-button>
            </cl-card>
            <cl-card label="圆角">
                <cl-button type="primary" round>默认</cl-button>
                <cl-button round>默认</cl-button>
            </cl-card>
            <cl-card label="加载中">
                <cl-row :gutter="20">
                    <cl-col :span="8">
                        <cl-button loading fill @tap="onTap">默认</cl-button>
                    </cl-col>
                    <cl-col :span="8">
                        <cl-button type="primary" fill loading>主要</cl-button>
                    </cl-col>
                    <cl-col :span="8">
                        <cl-button type="success" fill loading>成功</cl-button>
                    </cl-col>
                    <cl-col :span="8">
                        <cl-button type="error" fill loading>失败</cl-button>
                    </cl-col>
                    <cl-col :span="8">
                        <cl-button type="warning" fill loading>警告</cl-button>
                    </cl-col>
                    <cl-col :span="8">
                        <cl-button type="info" fill loading>信息</cl-button>
                    </cl-col>
                </cl-row>
            </cl-card>
            <cl-card label="禁用">
                <cl-row :gutter="20">
                    <cl-col :span="8">
                        <cl-button disabled fill @tap="onTap">默认</cl-button>
                    </cl-col>
                    <cl-col :span="8">
                        <cl-button type="primary" fill disabled>主要</cl-button>
                    </cl-col>
                    <cl-col :span="8">
                        <cl-button type="success" fill disabled>成功</cl-button>
                    </cl-col>
                    <cl-col :span="8">
                        <cl-button type="error" fill disabled>失败</cl-button>
                    </cl-col>
                    <cl-col :span="8">
                        <cl-button type="warning" fill disabled>警告</cl-button>
                    </cl-col>
                    <cl-col :span="8">
                        <cl-button type="info" fill disabled>信息</cl-button>
                    </cl-col>
                </cl-row>
            </cl-card>
            <cl-card label="没有边框">
                <cl-button :border="false">默认</cl-button>
            </cl-card>
            <cl-card label="自定义颜色">
                <cl-button :border="false" color="#ffffff" background-color="#008000"
                    >默认</cl-button
                >
                <cl-button color="#008000" plain>朴素</cl-button>
            </cl-card>
        </view>
    </cl-page>
</template>
<script lang="ts" setup>
import { useUi } from "/$/cool-ui";
const ui = useUi();
function onTap() {
    ui.showToast("点我干嘛");
}
</script>
<style lang="scss">
.page-demo-button {
    .cl-col {
        padding-bottom: 20rpx;
    }
}
</style>
pages/demo/basic/icon.vue
New file
@@ -0,0 +1,210 @@
<template>
    <cl-page :padding="20">
        <cl-card label="尺寸">
            <cl-icon name="like"></cl-icon>
            <cl-icon name="like" :size="40"></cl-icon>
            <cl-icon name="like" :size="50"></cl-icon>
        </cl-card>
        <cl-card label="颜色">
            <cl-icon name="like" color="primary"></cl-icon>
            <cl-icon name="like" color="error"></cl-icon>
            <cl-icon name="like" color="success"></cl-icon>
        </cl-card>
        <cl-card label="方向">
            <cl-grid :column="4">
                <cl-grid-item v-for="item in arrow" :key="item">
                    <view class="item" @tap="copy(item)">
                        <cl-icon :name="item" :size="44"></cl-icon>
                        <cl-text :value="item" align="center" :margin="[15, 10, 0, 10]" />
                    </view>
                </cl-grid-item>
            </cl-grid>
        </cl-card>
        <cl-card label="操作">
            <cl-grid :column="4">
                <cl-grid-item v-for="item in op" :key="item">
                    <view class="item" @tap="copy(item)">
                        <cl-icon :name="item" :size="44"></cl-icon>
                        <cl-text :value="item" align="center" :margin="[15, 10, 0, 10]" />
                    </view>
                </cl-grid-item>
            </cl-grid>
        </cl-card>
        <cl-card label="展示">
            <cl-grid :column="4">
                <cl-grid-item v-for="item in dis" :key="item">
                    <view class="item" @tap="copy(item)">
                        <cl-icon :name="item" :size="44"></cl-icon>
                        <cl-text :value="item" align="center" :margin="[15, 10, 0, 10]" />
                    </view>
                </cl-grid-item>
            </cl-grid>
        </cl-card>
        <cl-card label="圆形">
            <cl-grid :column="4">
                <cl-grid-item v-for="item in circular" :key="item">
                    <view class="item" @tap="copy(item)">
                        <cl-icon :name="item" :size="44"></cl-icon>
                        <cl-text :value="item" align="center" :margin="[15, 10, 0, 10]" />
                    </view>
                </cl-grid-item>
            </cl-grid>
        </cl-card>
        <cl-card label="其他">
            <cl-grid :column="4">
                <cl-grid-item v-for="item in list" :key="item">
                    <view class="item" @tap="copy(item)">
                        <cl-icon :name="item" :size="44"></cl-icon>
                        <cl-text :value="item" align="center" :margin="[15, 10, 0, 10]" />
                    </view>
                </cl-grid-item>
            </cl-grid>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const arrow = ref([
    "top",
    "back-top",
    "upgrade",
    "",
    "back",
    "enter",
    "",
    "",
    "arrow-double-left",
    "arrow-double-right",
    "",
    "",
    "arrow-bottom",
    "arrow-top",
    "arrow-left",
    "arrow-right",
    "caret-bottom",
    "caret-top",
]);
const op = ref([
    "plus",
    "minus",
    "plus-border",
    "minus-border",
    "enlarge",
    "shrink",
    "",
    "",
    "check",
    "close",
    "success-fill",
    "delete-fill",
    "toast-success",
    "toast-error",
    "",
    "",
    "cloud-download",
    "cloud-upload",
    "delete",
    "stop",
]);
const dis = ref([
    "meh",
    "meh-fill",
    "cry",
    "cry-fill",
    "smile",
    "smile-fill",
    "like",
    "like-fill",
    "location",
    "location-fill",
    "notification",
    "notification-fill",
    "eye-open",
    "eye-close",
    "favor-fill",
    "good-fill",
]);
const circular = ref([
    "chart-pie",
    "play",
    "more",
    "keyboard-9",
    "keyboard-26",
    "time",
    "refresh",
    "check-border",
    "close-border",
    "help-border",
    "warning-border",
    "toast-waiting",
    "clock-fill",
    "help-fill",
    "prompt-fill",
]);
const list = ref([
    "exit",
    "qrcode",
    "msg",
    "app",
    "payment",
    "face-auth",
    "folder",
    "bill",
    "phone",
    "share",
    "link",
    "rise",
    "decline",
    "active",
    "cropper",
    "fullscreen",
    "chart-bar",
    "set",
    "map",
    "calendar",
    "customer-service",
    "edit",
    "scan",
    "pay",
    "image",
    "search",
    "toast-warning",
    "history-fill",
    "amount-fill",
    "wallet-fill",
    "doc-fill",
    "camera-fill",
    "mail-fill",
    "bank-card-fill",
    "relay-fill",
    "comment-fill",
]);
function copy(name: string) {
    uni.setClipboardData({
        data: name,
    });
}
</script>
<style lang="scss" scoped>
.item {
    display: flex;
    flex-direction: column;
    align-items: center;
    height: 150rpx;
    padding-top: 20rpx;
}
</style>
pages/demo/basic/image.vue
New file
@@ -0,0 +1,83 @@
<template>
    <cl-page :padding="20">
        <cl-card label="裁剪模式">
            <cl-row :gutter="20">
                <cl-col :span="6">
                    <cl-image src="/pages/demo/static/bg1.png" :size="140" mode="scaleToFill" />
                    <cl-text value="scaleToFill"></cl-text>
                </cl-col>
                <cl-col :span="6">
                    <cl-image src="/pages/demo/static/bg1.png" :size="140" mode="aspectFit" />
                    <cl-text value="aspectFit"></cl-text>
                </cl-col>
                <cl-col :span="6">
                    <cl-image src="/pages/demo/static/bg1.png" :size="140" mode="aspectFill" />
                    <cl-text value="aspectFill"></cl-text>
                </cl-col>
                <cl-col :span="6">
                    <cl-image src="/pages/demo/static/bg1.png" :size="140" mode="widthFix" />
                    <cl-text value="widthFix"></cl-text>
                </cl-col>
            </cl-row>
        </cl-card>
        <cl-card label="圆角">
            <cl-row type="flex">
                <cl-col :span="6">
                    <cl-image src="/pages/demo/static/avatar1.png" :size="140" round />
                </cl-col>
                <cl-col :span="6">
                    <cl-image src="/pages/demo/static/avatar1.png" :size="140" :radius="16" />
                </cl-col>
            </cl-row>
        </cl-card>
        <cl-card label="自定义大小">
            <cl-row type="flex">
                <cl-image
                    src="/pages/demo/static/avatar1.png"
                    :size="[150, 280]"
                    mode="aspectFill"
                />
            </cl-row>
        </cl-card>
        <cl-card label="点击预览">
            <cl-row type="flex">
                <cl-image
                    src="/pages/demo/static/bg1.png"
                    :size="140"
                    :preview-list="previewList"
                />
            </cl-row>
        </cl-card>
        <cl-card label="插槽:地址为空">
            <cl-row type="flex">
                <cl-image src="" :size="150" />
                <cl-image src="" :size="150" :margin="[0, 0, 0, 20]">
                    <template #placeholder>
                        <cl-text color="red" value="自定义"></cl-text>
                    </template>
                </cl-image>
            </cl-row>
        </cl-card>
        <cl-card label="插槽:加载错误">
            <cl-image src="https://xxxx.png" :size="150" />
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import Bg from "/pages/demo/static/bg1.png";
import Avatar from "/pages/demo/static/avatar1.png";
const previewList = ref([Bg, Avatar]);
</script>
pages/demo/basic/loading.vue
New file
@@ -0,0 +1,52 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-row>
                <cl-col :span="4">
                    <cl-loading></cl-loading>
                </cl-col>
            </cl-row>
        </cl-card>
        <cl-card label="不同大小">
            <cl-row>
                <cl-col :span="4">
                    <cl-loading></cl-loading>
                </cl-col>
                <cl-col :span="4">
                    <cl-loading :size="60"></cl-loading>
                </cl-col>
            </cl-row>
        </cl-card>
        <cl-card label="加载区域">
            <cl-loading-mask loading>
                <view class="space">
                    <cl-form>
                        <cl-form-item label="昵称">
                            <cl-input />
                        </cl-form-item>
                        <cl-form-item label="性别">
                            <cl-select />
                        </cl-form-item>
                    </cl-form>
                </view>
            </cl-loading-mask>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.space {
    font-size: 28rpx;
    background-color: #f6f7fa;
    position: relative;
    padding: 20rpx;
    box-sizing: border-box;
    border-radius: 16rpx;
}
</style>
pages/demo/basic/tag.vue
New file
@@ -0,0 +1,36 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-tag>默认</cl-tag>
        </cl-card>
        <cl-card label="圆角">
            <cl-tag round>默认</cl-tag>
        </cl-card>
        <cl-card label="尺寸">
            <cl-tag size="large" :margin="[0, 20, 0, 0]">大尺寸</cl-tag>
            <cl-tag size="default" :margin="[0, 20, 0, 0]">默认尺寸</cl-tag>
            <cl-tag size="small">小尺寸</cl-tag>
        </cl-card>
        <cl-card label="标签颜色">
            <cl-tag type="primary" :margin="[0, 20, 0, 0]">主要</cl-tag>
            <cl-tag type="success" :margin="[0, 20, 0, 0]">成功</cl-tag>
            <cl-tag type="error" :margin="[0, 20, 0, 0]">失败</cl-tag>
            <cl-tag type="warning" :margin="[0, 20, 0, 0]">警告</cl-tag>
            <cl-tag type="info" :margin="[0, 20, 0, 0]">信息</cl-tag>
        </cl-card>
        <cl-card label="自定义颜色">
            <cl-tag color="#626aef">标签</cl-tag>
        </cl-card>
        <cl-card label="其他">
            <cl-tag plain :margin="[0, 20, 0, 0]">镂空</cl-tag>
            <cl-tag closable>可移除标签</cl-tag>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup></script>
pages/demo/basic/text.vue
New file
@@ -0,0 +1,76 @@
<template>
    <cl-page :padding="20">
        <view class="page-demo-text">
            <cl-card label="基础用法">
                <cl-text value="云想衣裳花想容,春风拂槛露华浓。" />
            </cl-card>
            <cl-card label="不同颜色">
                <cl-row>
                    <cl-col :span="8">
                        <cl-text value="主色" color="primary" />
                    </cl-col>
                    <cl-col :span="8">
                        <cl-text value="成功" color="success" />
                    </cl-col>
                    <cl-col :span="8">
                        <cl-text value="错误" color="error" />
                    </cl-col>
                    <cl-col :span="8">
                        <cl-text value="警告" color="warning" />
                    </cl-col>
                    <cl-col :span="8">
                        <cl-text value="信息" color="info" />
                    </cl-col>
                    <cl-col :span="8">
                        <cl-text value="自定义颜色" color="#628bca" />
                    </cl-col>
                </cl-row>
            </cl-card>
            <cl-card label="金额">
                <cl-text type="price" :size="40" value="19450" />
            </cl-card>
            <cl-card label="手机号">
                <cl-text type="phone" value="17605043035" />
            </cl-card>
            <cl-card label="超出省略">
                <cl-text
                    :ellipsis="2"
                    :line-height="1.4"
                    value="锦瑟无端五十弦,一弦一柱思华年。庄生晓梦迷蝴蝶,望帝春心托杜鹃。沧海月明珠有泪,蓝田日暖玉生烟。此情可待成追忆,只是当时已惘然。"
                />
            </cl-card>
            <cl-card label="图标">
                <cl-row>
                    <cl-col :span="12">
                        <cl-text prefix-icon="cl-icon-search" value="请填写名称" />
                    </cl-col>
                    <cl-col :span="12">
                        <cl-text suffix-icon="cl-icon-time" value="2022-02-06" />
                    </cl-col>
                </cl-row>
            </cl-card>
            <cl-card label="自定义">
                <cl-text
                    color="green"
                    :size="30"
                    :margin="[20, 0, 20, 0]"
                    value="颜色(green)、大小(30)、间距(20)"
                />
            </cl-card>
        </view>
    </cl-page>
</template>
<script lang="ts" setup></script>
pages/demo/basic/toast.vue
New file
@@ -0,0 +1,62 @@
<template>
    <cl-page :padding="20">
        <cl-toast ref="Toast"></cl-toast>
        <cl-card label="基础用法">
            <cl-button @tap="open()">默认</cl-button>
        </cl-card>
        <cl-card label="不同位置">
            <cl-button @tap="open()">默认</cl-button>
            <cl-button @tap="open({ position: 'top' })">顶部</cl-button>
            <cl-button @tap="open({ position: 'center' })">中间</cl-button>
            <cl-button @tap="open({ position: 'bottom' })">底部</cl-button>
        </cl-card>
        <cl-card label="不同类型">
            <cl-button @tap="open({ type: 'success' })">成功</cl-button>
            <cl-button @tap="open({ type: 'error' })">失败</cl-button>
            <cl-button @tap="open({ type: 'warning' })">警告</cl-button>
            <cl-button @tap="open({ type: 'info' })">信息</cl-button>
        </cl-card>
        <cl-card label="其他">
            <cl-button @tap="open({ icon: 'cl-icon-good-fill', message: '带图标' })"
                >带图标</cl-button
            >
            <cl-button
                @tap="
                    open({
                        message: '带图片',
                        image: {
                            url: '/static/logo.png',
                            style: {
                                height: '200rpx',
                                width: '200rpx',
                                borderRadius: '20rpx',
                            },
                        },
                    })
                "
                >带图片</cl-button
            >
        </cl-card>
        <cl-card label="不同类型">
            <cl-button @tap="open({ clear: true })">只显示一个</cl-button>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const Toast = ref<ClToast.Ref>();
function open(data?: any) {
    Toast.value?.open({
        message: "请输入收货人姓名",
        ...data,
    });
}
</script>
pages/demo/extend/action-sheet.vue
New file
@@ -0,0 +1,92 @@
<template>
    <cl-page :padding="20">
        <cl-action-sheet ref="ActionSheet" />
        <cl-card label="基础用法">
            <cl-button @tap="open">打开</cl-button>
        </cl-card>
        <cl-card label="添加图标">
            <cl-button @tap="open2">打开</cl-button>
        </cl-card>
        <cl-card label="禁用">
            <cl-button @tap="open3">打开</cl-button>
        </cl-card>
        <cl-card label="关闭回调">
            <cl-button @tap="open4">打开</cl-button>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useUi } from "/$/cool-ui";
const ui = useUi();
const ActionSheet = ref<ClActionSheet.Ref>();
function open() {
    ActionSheet.value?.open({
        list: [
            {
                label: "删除好友",
            },
        ],
    });
}
function open2() {
    ActionSheet.value?.open({
        list: [
            {
                label: "微信支付",
                icon: "cl-icon-payment",
            },
        ],
    });
}
function open3() {
    ActionSheet.value?.open({
        title: "删除好友会同时删除所有聊天记录",
        list: [
            {
                label: "删除好友",
                color: "red",
            },
        ],
    });
}
function open4() {
    ActionSheet.value?.open({
        closeOnClickModal: false,
        list: [
            {
                label: "删除好友",
                color: "red",
            },
        ],
        beforeClose(index, done) {
            if (index == 0) {
                ui.showConfirm({
                    title: "提示",
                    message: "是否删除该联系人",
                    callback(action) {
                        if (action == "confirm") {
                            ui.showToast("删除成功");
                        }
                        done();
                    },
                });
            } else {
                done();
            }
        },
    });
}
</script>
pages/demo/extend/captcha.vue
New file
@@ -0,0 +1,28 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-captcha focus></cl-captcha>
        </cl-card>
        <cl-card label="其他数量">
            <cl-captcha :len="6" :gutter="10" :height="100"></cl-captcha>
        </cl-card>
        <cl-card label="自动完成">
            <cl-captcha v-model="val" @done="onDone"></cl-captcha>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useUi } from "/$/cool-ui";
const ui = useUi();
const val = ref("");
function onDone() {
    ui.showToast(val.value);
}
</script>
pages/demo/extend/confirm.vue
New file
@@ -0,0 +1,103 @@
<template>
    <cl-page :padding="20">
        <cl-confirm ref="Confirm"> </cl-confirm>
        <cl-confirm ref="Confirm2">
            <cl-input />
        </cl-confirm>
        <cl-confirm ref="Confirm3"> </cl-confirm>
        <cl-confirm ref="Confirm4"> </cl-confirm>
        <cl-card label="基础用法">
            <cl-button @tap="open">打开</cl-button>
        </cl-card>
        <cl-card label="自定义内容">
            <cl-button @tap="open2">打开</cl-button>
        </cl-card>
        <cl-card label="关闭回掉">
            <cl-button @tap="open3">打开</cl-button>
        </cl-card>
        <cl-card label="不同类型">
            <cl-button @tap="open4('success')">成功</cl-button>
            <cl-button @tap="open4('warning')">警告</cl-button>
            <cl-button @tap="open4('error')">错误</cl-button>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useUi } from "/$/cool-ui";
const ui = useUi();
const Confirm = ref<ClConfirm.Ref>();
const Confirm2 = ref<ClConfirm.Ref>();
const Confirm3 = ref<ClConfirm.Ref>();
const Confirm4 = ref<ClConfirm.Ref>();
function open() {
    Confirm.value?.open({
        title: "提示",
        message: "你有一个待取信件",
        callback(action) {
            console.log(action);
            switch (action) {
                case "confirm":
                    ui.showToast("领取成功");
                    break;
                case "cancel":
                    ui.showToast("已取消");
                    break;
                case "close":
                    ui.showToast("已关闭");
                    break;
            }
        },
    });
}
function open2() {
    Confirm2.value?.open({
        title: "提示",
    });
}
function open3() {
    Confirm3.value?.open({
        title: "提示",
        message: "你有一个待取信件",
        beforeClose(action, { done, showLoading, hideLoading }) {
            console.log(action);
            if (action == "confirm") {
                showLoading();
                setTimeout(() => {
                    done();
                    ui.showToast("领取成功");
                }, 1500);
            } else {
                done();
            }
        },
    });
}
function open4(type: "success" | "warning" | "error") {
    Confirm4.value?.open({
        title: "提示",
        message: "这是一条消息",
        type,
        showCancelButton: false,
    });
}
</script>
pages/demo/extend/dialog.vue
New file
@@ -0,0 +1,44 @@
<template>
    <cl-page :padding="20">
        <cl-dialog title="标题" v-model="visible">
            <text>云想衣裳花想容,春风拂槛露华浓。若非群玉山头见,会向瑶台月下逢。</text>
        </cl-dialog>
        <cl-dialog :ref="setRefs('d1')">
            <text>云想衣裳花想容,春风拂槛露华浓。若非群玉山头见,会向瑶台月下逢。</text>
        </cl-dialog>
        <cl-dialog :ref="setRefs('d2')" title="标题">
            <text>云想衣裳花想容,春风拂槛露华浓。若非群玉山头见,会向瑶台月下逢。</text>
            <template #footer>
                <cl-button @tap="refs.d2?.close()">好诗!</cl-button>
            </template>
        </cl-dialog>
        <cl-card label="基础用法">
            <cl-button @tap="open">打开</cl-button>
        </cl-card>
        <cl-card label="无标题">
            <cl-button @tap="refs.d1?.open()">打开</cl-button>
        </cl-card>
        <cl-card label="带按钮">
            <cl-button @tap="refs.d2?.open()">打开</cl-button>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useCool } from "/@/cool";
const { refs, setRefs } = useCool();
const visible = ref(false);
function open() {
    visible.value = true;
}
</script>
pages/demo/extend/filter-bar.vue
New file
@@ -0,0 +1,112 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-filter-bar v-model="filter" :num="4">
                <cl-filter-item label="排序" prop="order" type="order"></cl-filter-item>
                <cl-filter-item label="开关" prop="switch" type="switch"></cl-filter-item>
            </cl-filter-bar>
        </cl-card>
        <cl-card label="展开列表">
            <cl-filter-bar v-model="filter">
                <cl-filter-item
                    label="多选"
                    prop="d1"
                    type="dropdown"
                    multiple
                    :options="[
                        {
                            label: 'A',
                            value: 1,
                        },
                        {
                            label: 'B',
                            value: 2,
                        },
                    ]"
                ></cl-filter-item>
                <cl-filter-item
                    label="单选"
                    prop="d2"
                    type="dropdown"
                    :options="[
                        {
                            label: 'A',
                            value: 1,
                        },
                        {
                            label: 'B',
                            value: 2,
                        },
                    ]"
                ></cl-filter-item>
            </cl-filter-bar>
        </cl-card>
        <cl-card label="展开宫格">
            <cl-filter-bar v-model="filter">
                <cl-filter-item
                    label="多选"
                    prop="d1"
                    type="dropdown"
                    multiple
                    :options="[
                        {
                            label: 'A',
                            value: 1,
                        },
                        {
                            label: 'B',
                            value: 2,
                        },
                    ]"
                    theme="grid"
                ></cl-filter-item>
                <cl-filter-item
                    label="单选"
                    prop="d2"
                    type="dropdown"
                    :options="[
                        {
                            label: 'A',
                            value: 1,
                        },
                        {
                            label: 'B',
                            value: 2,
                        },
                    ]"
                    theme="grid"
                ></cl-filter-item>
            </cl-filter-bar>
        </cl-card>
        <cl-card label="展开自定义">
            <cl-filter-bar v-model="filter">
                <cl-filter-item label="输入框" prop="input" type="dropdown">
                    <template #dropdown>
                        <view class="cs" @tap.stop>
                            <cl-input />
                            <cl-button :margin="[0, 0, 0, 20]">搜索</cl-button>
                        </view>
                    </template>
                </cl-filter-item>
            </cl-filter-bar>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const filter = ref({
    d1: [],
});
</script>
<style lang="scss" scoped>
.cs {
    display: flex;
    padding: 20rpx;
}
</style>
pages/demo/extend/page.vue
New file
@@ -0,0 +1,69 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-text
                value="由于 uniapp 限制,cl-page 组件可作为全局组件使用"
                block
                color="warning"
                :margin="[0, 0, 30, 0]"
            />
            <cl-row>
                <cl-button @tap="openToast">提示框</cl-button>
                <cl-button @tap="openConfirm">确认框</cl-button>
                <cl-button @tap="openLoading">加载框</cl-button>
            </cl-row>
        </cl-card>
        <cl-card label="主题示例">
            <cl-text
                value="开发时,请根据设计图自行调整"
                block
                color="warning"
                :margin="[0, 0, 30, 0]"
            />
            <cl-row>
                <cl-button @tap="setTheme('default')">默认</cl-button>
                <cl-button @tap="setTheme('grey')">灰色</cl-button>
            </cl-row>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { useUi } from "/$/cool-ui";
import { useApp, useCool } from "/@/cool";
const { router } = useCool();
const ui = useUi();
const app = useApp();
function setTheme(name: string) {
    app.theme.set(name);
    router.push("/pages/demo/form/form");
}
function openToast() {
    ui.showToast("cool-cli");
}
function openConfirm() {
    ui.showConfirm({
        title: "提示",
        type: "warning",
        message: "是否要删除该联系人?",
    });
}
function openLoading() {
    ui.showLoading({
        text: "2秒后关闭",
        border: false,
    });
    setTimeout(() => {
        ui.hideLoading();
    }, 2000);
}
</script>
pages/demo/extend/service.vue
New file
@@ -0,0 +1,74 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-service :service="get()" :mask="{ text: '获取数据中' }">
                <template #default="{ data }">
                    <view class="count">
                        <view class="item">
                            <cl-icon name="favor-fill" :size="40" :margin="[0, 0, 20, 0]"></cl-icon>
                            <cl-text>{{ data?.star || 0 }}</cl-text>
                        </view>
                        <view class="item">
                            <cl-icon name="chart-pie" :size="40" :margin="[0, 0, 20, 0]"></cl-icon>
                            <cl-text>{{ data?.count || 0 }}</cl-text>
                        </view>
                        <view class="item">
                            <cl-icon
                                name="wallet-fill"
                                :size="40"
                                :margin="[0, 0, 20, 0]"
                            ></cl-icon>
                            <cl-text>{{ data?.wallet || 0 }}元</cl-text>
                        </view>
                    </view>
                </template>
            </cl-service>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
function get() {
    // 实际使用示例
    // return service.xxx.count()
    // 模拟 service 请求
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve({
                star: Math.ceil(Math.random() * 1000),
                count: Math.ceil(Math.random() * 1000),
                wallet: Math.ceil(Math.random() * 10000),
            });
        }, 1500);
    });
}
</script>
<style lang="scss" scoped>
.count {
    display: flex;
    padding: 20rpx 0;
    .item {
        flex: 1;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        text {
            &:first-child {
                font-size: 42rpx;
            }
            &:last-child {
                font-size: 30rpx;
                margin-top: 20rpx;
            }
        }
    }
}
</style>
pages/demo/extend/slider-verify.vue
New file
@@ -0,0 +1,28 @@
<template>
    <cl-page :padding="20">
        <view class="page">
            <cl-card label="拖动验证">
                <cl-button @tap="refs.sliderVerify?.open()">打开</cl-button>
                <cl-slider-verify title="拖动验证" :image="Bg1" :ref="setRefs('sliderVerify')" />
            </cl-card>
            <cl-card label="旋转验证">
                <cl-button @tap="refs.sliderVerify2?.open()">打开</cl-button>
                <cl-slider-verify
                    title="旋转验证"
                    type="rotate"
                    :image="Avatar4"
                    :ref="setRefs('sliderVerify2')"
                />
            </cl-card>
        </view>
    </cl-page>
</template>
<script setup lang="ts">
import { useCool } from "/@/cool";
import Bg1 from "../static/bg1.png";
import Avatar4 from "../static/avatar4.png";
const { refs, setRefs } = useCool();
</script>
pages/demo/extend/tree.vue
New file
@@ -0,0 +1,150 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-row :margin="[0, 0, 10, 0]">
                <cl-tag>{{ refs.tree?.label }}</cl-tag>
            </cl-row>
            <cl-tag type="error">{{ value }}</cl-tag>
            <view class="bor">
                <cl-tree :ref="setRefs('tree')" v-model="value" :data="data"> </cl-tree>
            </view>
        </cl-card>
        <cl-card label="多选">
            <cl-row :margin="[0, 0, 10, 0]">
                <cl-tag>{{ refs.treeMultiple?.label }}</cl-tag>
            </cl-row>
            <cl-tag type="error">{{ value2 }}</cl-tag>
            <view class="bor">
                <cl-tree multiple :ref="setRefs('treeMultiple')" v-model="value2" :data="data" />
            </view>
        </cl-card>
        <cl-card label="弹出框选择">
            <cl-list-item :arrow-icon="false" label="节点选择" @tap="refs.treeSelect?.open">
                <cl-text :value="refs.treeSelect?.label || '请选择'" :ellipsis="1" />
            </cl-list-item>
            <cl-tree-select
                :ref="setRefs('treeSelect')"
                v-model="value3"
                :data="data"
                :show-picker="false"
            />
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useCool } from "/@/cool";
const { refs, setRefs } = useCool();
const value = ref(3);
const value2 = ref([12, 3]);
const value3 = ref();
const data = ref([
    {
        label: "Level one 1",
        value: 1,
        children: [
            {
                label: "Level two 1-1",
                value: 2,
                children: [
                    {
                        label: "Level three 1-1-1",
                        value: 3,
                    },
                ],
            },
        ],
    },
    {
        label: "Level one 2",
        value: 4,
        children: [
            {
                label: "Level two 2-1",
                value: 5,
                children: [
                    {
                        label: "Level three 2-1-1",
                        value: 6,
                        children: [
                            {
                                label: "Level four 2-1-1-1",
                                value: 14,
                                children: [
                                    {
                                        label: "Level five 2-1-1-1-1",
                                        value: 16,
                                    },
                                ],
                            },
                            {
                                label: "Level four 2-1-1-2",
                                value: 15,
                                children: [
                                    {
                                        label: "Level five 2-1-1-2-1",
                                        value: 17,
                                    },
                                ],
                            },
                        ],
                    },
                ],
            },
            {
                label: "Level two 2-2",
                value: 7,
            },
        ],
    },
    {
        label: "Level one 3",
        value: 8,
        children: [
            {
                label: "Level two 3-1",
                value: 9,
                children: [
                    {
                        label: "Level three 3-1-1",
                        value: 10,
                    },
                ],
            },
            {
                label: "Level two 3-2",
                value: 11,
                children: [
                    {
                        label: "Level three 3-2-1",
                        value: 12,
                    },
                ],
            },
        ],
    },
    {
        label: "Level one 4",
        value: 13,
    },
]);
</script>
<style lang="scss" scoped>
.bor {
    border: $cl-border-width solid #eee;
    border-radius: 16rpx;
    margin-top: 20rpx;
    padding: 10rpx;
}
</style>
pages/demo/form/checkbox.vue
New file
@@ -0,0 +1,111 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-checkbox-group v-model="v0">
                <cl-checkbox label="1">炸鸡</cl-checkbox>
                <cl-checkbox label="2">汉堡</cl-checkbox>
                <cl-checkbox label="3">薯条</cl-checkbox>
            </cl-checkbox-group>
        </cl-card>
        <cl-card label="圆角">
            <cl-checkbox-group v-model="v1" round>
                <cl-checkbox label="1">炸鸡</cl-checkbox>
                <cl-checkbox label="2">汉堡</cl-checkbox>
            </cl-checkbox-group>
        </cl-card>
        <cl-card label="禁用">
            <cl-checkbox-group v-model="v2">
                <cl-checkbox label="1">炸鸡</cl-checkbox>
                <cl-checkbox disabled label="2">汉堡</cl-checkbox>
            </cl-checkbox-group>
        </cl-card>
        <cl-card label="开关">
            <cl-checkbox v-model="v3">已阅读并同意《用户协议》及《隐私政策》</cl-checkbox>
        </cl-card>
        <cl-card label="边框">
            <cl-checkbox-group v-model="v4">
                <cl-checkbox border label="1">炸鸡</cl-checkbox>
                <cl-checkbox border label="2">汉堡</cl-checkbox>
            </cl-checkbox-group>
        </cl-card>
        <cl-card label="边框填充">
            <cl-checkbox-group v-model="v5" fill border>
                <cl-checkbox label="1">
                    <cl-text align="right" block>鸡米花(靠右)</cl-text>
                </cl-checkbox>
                <cl-checkbox label="2">可乐</cl-checkbox>
                <cl-checkbox label="3">蛋挞</cl-checkbox>
            </cl-checkbox-group>
        </cl-card>
        <cl-card label="只显示文字">
            <cl-checkbox-group v-model="v6" text border>
                <cl-checkbox label="1">炸鸡</cl-checkbox>
                <cl-checkbox label="2">汉堡</cl-checkbox>
            </cl-checkbox-group>
        </cl-card>
        <cl-card label="带背景">
            <cl-checkbox-group v-model="v6" text border bg>
                <cl-checkbox label="1">炸鸡</cl-checkbox>
                <cl-checkbox label="2">汉堡</cl-checkbox>
            </cl-checkbox-group>
        </cl-card>
        <cl-card label="边框圆角">
            <cl-checkbox-group v-model="v7" round border>
                <cl-checkbox label="1">炸鸡</cl-checkbox>
                <cl-checkbox label="2">汉堡</cl-checkbox>
            </cl-checkbox-group>
        </cl-card>
        <cl-card label="文字贼多">
            <cl-checkbox-group v-model="v8">
                <cl-checkbox label="1">
                    汉堡、可乐、鸡肉卷、鸡排、鸡腿、蛋挞、薯条、鸡翅、原味鸡
                </cl-checkbox>
                <cl-checkbox label="2"> 鸡块、鸡米发、鸡腿堡 </cl-checkbox>
            </cl-checkbox-group>
        </cl-card>
        <cl-card label="自定义图标">
            <cl-checkbox-group v-model="v9">
                <cl-checkbox
                    v-for="(item, index) in ['汉堡', '可乐', '薯条']"
                    :key="index"
                    :label="index"
                >
                    <template #icon="{ checked }">
                        <cl-icon
                            name="like-fill"
                            :size="36"
                            :color="checked ? 'primary' : 'info'"
                        />
                    </template>
                    {{ item }}
                </cl-checkbox>
            </cl-checkbox-group>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const v0 = ref(["1"]);
const v1 = ref(["2"]);
const v2 = ref([]);
const v3 = ref(true);
const v4 = ref(["1"]);
const v5 = ref(["1", "2"]);
const v6 = ref(["1", "2"]);
const v7 = ref(["1", "2"]);
const v8 = ref(["1", "2"]);
const v9 = ref([]);
</script>
pages/demo/form/form.vue
New file
@@ -0,0 +1,186 @@
<template>
    <cl-page :padding="20">
        <view class="page">
            <cl-card label="基础用法">
                <cl-form
                    ref="Form"
                    v-model="form"
                    :rules="rules"
                    :disabled="loading"
                    label-position="left"
                >
                    <cl-form-item label="活动名称" prop="name">
                        <cl-input v-model="form.name" placeholder="请填写活动名称"></cl-input>
                    </cl-form-item>
                    <cl-form-item label="活动时间" prop="date">
                        <cl-select-date v-model="form.date"></cl-select-date>
                    </cl-form-item>
                    <cl-form-item label="活动类型" prop="type">
                        <cl-select v-model="form.type" :options="options.type"></cl-select>
                    </cl-form-item>
                    <cl-form-item label="活动人数" prop="num" justify="end">
                        <cl-input-number v-model="form.num" :min="1" :max="100"></cl-input-number>
                    </cl-form-item>
                    <cl-form-item label="活动区域" prop="area" label-position="top">
                        <cl-checkbox-group v-model="form.area">
                            <cl-checkbox
                                v-for="(item, index) in options.area"
                                :key="index"
                                :label="item.value"
                            >
                                {{ item.label }}
                            </cl-checkbox>
                        </cl-checkbox-group>
                    </cl-form-item>
                    <cl-form-item label="资源" prop="source" label-position="top">
                        <cl-radio-group v-model="form.source">
                            <cl-radio
                                v-for="(item, index) in options.source"
                                :key="index"
                                :label="item.value"
                            >
                                {{ item.label }}
                            </cl-radio>
                        </cl-radio-group>
                    </cl-form-item>
                    <cl-form-item label="活动封面" prop="cover" label-position="top">
                        <cl-upload v-model="form.cover" />
                    </cl-form-item>
                    <cl-form-item label="活动海报(最多上传6张)" prop="pics" label-position="top">
                        <cl-upload v-model="form.pics" multiple :limit="6" />
                    </cl-form-item>
                    <cl-form-item label="活动描述" prop="remark" label-position="top">
                        <cl-textarea v-model="form.remark" count placeholder="请填写活动描述" />
                    </cl-form-item>
                    <cl-form-item label="活动赞助商" prop="company" label-position="top">
                        <cl-input v-model="form.company" placeholder="请填写活动赞助商" />
                        <template #append>
                            <cl-icon name="help-border" :size="36"></cl-icon>
                        </template>
                    </cl-form-item>
                </cl-form>
            </cl-card>
            <cl-footer border>
                <cl-button size="large" round fill @tap="clear">清空</cl-button>
                <cl-button size="large" round fill @tap="submit" type="success" :loading="loading"
                    >提交</cl-button
                >
            </cl-footer>
        </view>
    </cl-page>
</template>
<script lang="ts" setup>
import { reactive, ref } from "vue";
import { useUi } from "/$/cool-ui";
import { onPageScroll } from "@dcloudio/uni-app";
onPageScroll((e) => {});
const ui = useUi();
const form = ref({
    name: "",
    type: undefined,
    date: undefined,
    area: [0, 1],
    source: 0,
    num: 5,
    remark: "",
    cover: "",
    pics: [],
    company: "",
});
const rules = reactive({
    name: {
        required: true,
        message: "活动名称不能为空",
    },
    type: {
        required: true,
        message: "活动类型不能为空",
    },
    date: {
        required: true,
        message: "活动时间不能为空",
    },
    cover: {
        required: true,
        message: "活动封面不能为空",
    },
});
const options = reactive({
    area: [
        {
            label: "一区",
            value: 0,
        },
        {
            label: "二区",
            value: 1,
        },
    ],
    type: [
        {
            label: "线上活动",
            value: 0,
        },
        {
            label: "推广活动",
            value: 1,
        },
        {
            label: "线下活动",
            value: 2,
        },
    ],
    source: [
        {
            label: "赞助",
            value: 0,
        },
        {
            label: "场地",
            value: 1,
        },
    ],
});
const Form = ref<ClForm.Ref>();
const loading = ref(false);
function submit() {
    Form.value?.validate((valid, errors) => {
        if (valid) {
            loading.value = true;
            setTimeout(() => {
                ui.showToast("提交成功");
                loading.value = false;
            }, 1500);
        }
    });
}
function reset() {
    Form.value?.reset();
}
function clear() {
    Form.value?.clear();
}
</script>
pages/demo/form/input-number.vue
New file
@@ -0,0 +1,25 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-input-number />
        </cl-card>
        <cl-card label="步数">
            <cl-input-number :step="500" :max="10000" />
        </cl-card>
        <cl-card label="禁用">
            <cl-input-number disabled />
        </cl-card>
        <cl-card label="范围(4-10)">
            <cl-input-number :max="10" :min="4" />
        </cl-card>
        <cl-card label="单位">
            <cl-input-number unit="个" />
        </cl-card>
    </cl-page>
</template>
<script lang="ts"></script>
pages/demo/form/input.vue
New file
@@ -0,0 +1,61 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-input />
        </cl-card>
        <cl-card label="数字键盘">
            <cl-input type="number" />
        </cl-card>
        <cl-card label="禁用">
            <view style="margin-bottom: 20rpx">
                <cl-button
                    size="small"
                    :type="disabled ? 'success' : 'error'"
                    @tap="disabled = !disabled"
                >
                    {{ disabled ? "启用" : "禁用" }}
                </cl-button>
            </view>
            <cl-input :disabled="disabled" />
        </cl-card>
        <cl-card label="无边框">
            <cl-input :border="false" />
        </cl-card>
        <cl-card label="前置元素">
            <cl-input>
                <template #prepend>
                    <text>https://</text>
                </template>
            </cl-input>
        </cl-card>
        <cl-card label="后置元素">
            <cl-input>
                <template #append>
                    <text>元</text>
                </template>
            </cl-input>
        </cl-card>
        <cl-card label="自定义">
            <cl-input
                :height="80"
                :padding="[0, 32, 0, 32]"
                :radius="16"
                background-color="#eee"
                :border="false"
            />
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const disabled = ref(true);
</script>
pages/demo/form/radio.vue
New file
@@ -0,0 +1,95 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-radio v-model="v1" label="1">炸鸡</cl-radio>
            <cl-radio v-model="v1" label="2">汉堡</cl-radio>
            <cl-radio v-model="v1" label="3">薯条</cl-radio>
            <cl-radio v-model="v1" label="4">可乐</cl-radio>
            <cl-radio v-model="v1" label="5">冰淇淋</cl-radio>
        </cl-card>
        <cl-card label="禁用">
            <cl-radio v-model="v3" disabled label="1">炸鸡</cl-radio>
            <cl-radio v-model="v3" disabled label="2">汉堡</cl-radio>
        </cl-card>
        <cl-card label="边框">
            <cl-radio v-model="v4" border label="1">炸鸡</cl-radio>
            <cl-radio v-model="v4" border label="2">汉堡</cl-radio>
        </cl-card>
        <cl-card label="边框填充">
            <cl-radio-group v-model="v5" fill border>
                <cl-radio label="1">
                    <cl-text align="right" block>鸡米花(靠右)</cl-text>
                </cl-radio>
                <cl-radio label="2">可乐</cl-radio>
                <cl-radio label="3">蛋挞</cl-radio>
            </cl-radio-group>
        </cl-card>
        <cl-card label="只显示文字">
            <cl-radio-group v-model="v6" text border>
                <cl-radio label="1">炸鸡</cl-radio>
                <cl-radio label="2">汉堡</cl-radio>
            </cl-radio-group>
        </cl-card>
        <cl-card label="带背景">
            <cl-radio-group v-model="v6" text border bg>
                <cl-radio label="1">炸鸡</cl-radio>
                <cl-radio label="2">汉堡</cl-radio>
            </cl-radio-group>
        </cl-card>
        <cl-card label="圆角">
            <cl-radio-group v-model="v7" round border>
                <cl-radio label="1">炸鸡</cl-radio>
                <cl-radio label="2">汉堡</cl-radio>
            </cl-radio-group>
        </cl-card>
        <cl-card label="文字贼多">
            <cl-radio-group v-model="v8">
                <cl-radio label="1">
                    汉堡、可乐、鸡肉卷、鸡排、鸡腿、蛋挞、薯条、鸡翅、原味鸡
                </cl-radio>
                <cl-radio label="2"> 鸡块、鸡米发、鸡腿堡 </cl-radio>
            </cl-radio-group>
        </cl-card>
        <cl-card label="自定义图标">
            <cl-radio-group v-model="v9">
                <cl-radio
                    v-for="(item, index) in ['汉堡', '可乐', '薯条']"
                    :key="index"
                    :label="index"
                >
                    <template #icon="{ checked }">
                        <cl-icon
                            name="like-fill"
                            :size="36"
                            :color="checked ? 'primary' : 'info'"
                        />
                    </template>
                    {{ item }}
                </cl-radio>
            </cl-radio-group>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const v1 = ref("1");
const v2 = ref("2");
const v3 = ref("2");
const v4 = ref("2");
const v5 = ref("2");
const v6 = ref("2");
const v7 = ref("1");
const v8 = ref("1");
const v9 = ref("1");
</script>
pages/demo/form/rate.vue
New file
@@ -0,0 +1,19 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-rate :model-value="2.7"></cl-rate>
        </cl-card>
        <cl-card label="禁用">
            <cl-rate :model-value="2.7" disabled></cl-rate>
        </cl-card>
        <cl-card label="多色">
            <cl-rate :model-value="2" :color="['#F56C6C', '#E6A23C', '#67C23A']"></cl-rate>
        </cl-card>
        <cl-card label="其他图标">
            <cl-rate :model-value="1" icon="cl-icon-like"></cl-rate>
        </cl-card>
    </cl-page>
</template>
pages/demo/form/select-city.vue
New file
@@ -0,0 +1,50 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-select-city v-model="v1" />
        </cl-card>
        <cl-card label="省市区乡镇4级">
            <cl-select-city v-model="v2" :data="pcas" placeholder="省市区县、乡镇" />
        </cl-card>
        <cl-card label="自定义">
            <cl-select-city v-model="v3">
                <template #default="{ selection }">
                    <cl-row>
                        <cl-tag
                            :margin="[0, 20, 20, 0]"
                            v-for="item in selection"
                            plain
                            round
                            :key="item.code"
                            >{{ item.name }}</cl-tag
                        >
                    </cl-row>
                    <cl-button round>选择所在地区</cl-button>
                </template>
            </cl-select-city>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { onReady } from "@dcloudio/uni-app";
const v1 = ref([]);
const v2 = ref([]);
const v3 = ref([]);
const pcas = ref();
onReady(() => {
    uni.request({
        url: "https://cool-service.oss-cn-shanghai.aliyuncs.com/app%2Fbase%2F8fe445f06db5463386ae940d45b4bd6c_city-pcas.json",
        success(res) {
            pcas.value = res.data;
        },
    });
});
</script>
pages/demo/form/select-date.vue
New file
@@ -0,0 +1,65 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-select-date v-model="v1" />
        </cl-card>
        <cl-card label="时-分">
            <cl-select-date v-model="v2" :mode="['hour', 'minute']" format="HH:mm" />
        </cl-card>
        <cl-card label="年-月">
            <cl-select-date v-model="v3" :mode="['year', 'month']" format="YYYY-MM" />
        </cl-card>
        <cl-card label="起始时间">
            <cl-select-date v-model="v4" start="2023-01-01 08:00:00" end="2025-12-12 12:00:00" />
            <cl-row :margin="[20, 0, 0, 0]">
                <cl-text value="2023-01-01 08:00:00 < " />
                <cl-text value="时间" color="red" :margin="[0, 10, 0, 10]" />
                <cl-text value=" < 2025-12-12 12:00:00" />
            </cl-row>
        </cl-card>
        <cl-card label="展示格式">
            <cl-select-date
                v-model="v5"
                :display-format="
                    (date: string) => {
                        return date ? dayjs(date).format('YYYY年MM月DD日 HH时mm分ss秒') : '';
                    }
                "
            />
        </cl-card>
        <cl-card label="自定义">
            <cl-select-date v-model="v6" title="请选择预约时间">
                <template #default="{ value }">
                    <cl-select-inner round background-color="#f7f7f7">
                        <cl-text
                            prefix-icon="cl-icon-time"
                            :value="
                                value
                                    ? dayjs(value).format('YYYY年MM月DD日 HH时mm分ss秒')
                                    : '请选择预约时间'
                            "
                        />
                    </cl-select-inner>
                </template>
            </cl-select-date>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import dayjs from "dayjs";
import { ref } from "vue";
const v1 = ref();
const v2 = ref();
const v3 = ref();
const v4 = ref();
const v5 = ref();
const v6 = ref();
</script>
pages/demo/form/select-popup.vue
New file
@@ -0,0 +1,86 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-select-popup v-model="v1" title="选择歌曲" :options="list" />
        </cl-card>
        <cl-card label="多选">
            <cl-select-popup v-model="v2" multiple title="选择歌曲" :options="list" />
        </cl-card>
        <cl-card label="必填">
            <cl-select-popup v-model="v3" required title="选择歌曲" :options="list" />
        </cl-card>
        <cl-card label="自定义">
            <cl-select-popup v-model="v4" required title="选择歌曲" :options="list">
                <template #item="{ item }">
                    {{ item }}
                </template>
                <template #default="{ label, value }">
                    <cl-select-inner round background-color="#f7f7f7">
                        <cl-text value="你选择了" :size="24" color="info" />
                        <cl-tag round size="small" :margin="[0, 0, 0, 20]">{{ label }}</cl-tag>
                    </cl-select-inner>
                </template>
            </cl-select-popup>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const v1 = ref();
const v2 = ref([]);
const v3 = ref();
const v4 = ref();
const list = ref([
    {
        label: "冠军相",
        value: 1,
    },
    {
        label: "鬼脸",
        value: 2,
    },
    {
        label: "说唱歌手",
        value: 3,
    },
    {
        label: "一般的一天",
        value: 4,
    },
    {
        label: "会魔法的老人",
        value: 5,
    },
    {
        label: "C级浪漫",
        value: 6,
    },
    {
        label: "我想",
        value: 7,
    },
    {
        label: "小河淌水",
        value: 8,
    },
    {
        label: "午夜派对",
        value: 9,
    },
    {
        label: "POW!!!!!!!!!!!!!引爆",
        value: 10,
    },
    {
        label: "流浪`地球",
        value: 11,
    },
]);
</script>
pages/demo/form/select.vue
New file
@@ -0,0 +1,92 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-select v-model="v1" :options="list"></cl-select>
        </cl-card>
        <cl-card label="日期">
            <cl-select v-model="v2" mode="date" fields="month"></cl-select>
        </cl-card>
        <cl-card label="时间">
            <cl-select v-model="v3" mode="time"></cl-select>
        </cl-card>
        <cl-card label="禁用">
            <cl-select v-model="v4" :options="list" disabled></cl-select>
        </cl-card>
        <cl-card label="省市区">
            <cl-select-region v-model="v5"></cl-select-region>
        </cl-card>
        <cl-card label="自定义">
            <cl-select v-model="v6" :options="list">
                <template #default="{ label, value }">
                    <cl-select-inner round background-color="#f7f7f7">
                        <cl-text value="你选择了" :size="24" color="info" />
                        <cl-tag round size="small" :margin="[0, 0, 0, 20]">{{ label }}</cl-tag>
                    </cl-select-inner>
                </template>
            </cl-select>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const v1 = ref();
const v2 = ref();
const v3 = ref();
const v4 = ref(2);
const v5 = ref([]);
const v6 = ref(2);
const list = ref([
    {
        label: "冠军相",
        value: 1,
    },
    {
        label: "鬼脸",
        value: 2,
    },
    {
        label: "说唱歌手",
        value: 3,
    },
    {
        label: "一般的一天",
        value: 4,
    },
    {
        label: "会魔法的老人",
        value: 5,
    },
    {
        label: "C级浪漫",
        value: 6,
    },
    {
        label: "我想",
        value: 7,
    },
    {
        label: "小河淌水",
        value: 8,
    },
    {
        label: "午夜派对",
        value: 9,
    },
    {
        label: "POW!!!!!!!!!!!!!引爆",
        value: 10,
    },
    {
        label: "流浪`地球",
        value: 11,
    },
]);
</script>
pages/demo/form/switch.vue
New file
@@ -0,0 +1,21 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-switch></cl-switch>
        </cl-card>
        <cl-card label="其他颜色">
            <cl-switch color="red"></cl-switch>
        </cl-card>
        <cl-card label="禁用">
            <cl-switch v-model="v3" disabled></cl-switch>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const v3 = ref(true);
</script>
pages/demo/form/textarea.vue
New file
@@ -0,0 +1,11 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-textarea />
        </cl-card>
        <cl-card label="统计字数">
            <cl-textarea count />
        </cl-card>
    </cl-page>
</template>
pages/demo/form/upload.vue
New file
@@ -0,0 +1,23 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-upload v-model="v1" test />
        </cl-card>
        <cl-card label="自定义大小">
            <cl-upload :size="[200, 300]" v-model="v2" test />
        </cl-card>
        <cl-card label="多图">
            <cl-upload multiple v-model="v3" test />
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const v1 = ref();
const v2 = ref();
const v3 = ref([]);
</script>
pages/demo/static/avatar1.png
pages/demo/static/avatar2.png
pages/demo/static/avatar3.png
pages/demo/static/avatar4.png
pages/demo/static/bg1.png
pages/demo/static/bg2.png
pages/demo/static/bg3.png
pages/demo/static/bg4.png
pages/demo/view/avatar.vue
New file
@@ -0,0 +1,58 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-row>
                <cl-col :span="4">
                    <cl-avatar src="/pages/demo/static/avatar1.png"></cl-avatar>
                </cl-col>
                <cl-col :span="4">
                    <cl-avatar src="/pages/demo/static/avatar1.png" shape="square"></cl-avatar>
                </cl-col>
            </cl-row>
        </cl-card>
        <cl-card label="占位">
            <cl-row>
                <cl-col :span="4">
                    <cl-avatar></cl-avatar>
                </cl-col>
                <cl-col :span="4">
                    <cl-avatar name="神仙" background-color="red"></cl-avatar>
                </cl-col>
            </cl-row>
        </cl-card>
        <cl-card label="不同大小">
            <cl-row>
                <cl-col :span="6">
                    <cl-avatar :size="100" src="/pages/demo/static/avatar1.png"></cl-avatar>
                </cl-col>
                <cl-col :span="6">
                    <cl-avatar :size="120" src="/pages/demo/static/avatar2.png"></cl-avatar>
                </cl-col>
                <cl-col :span="6">
                    <cl-avatar :size="140" src="/pages/demo/static/avatar3.png"></cl-avatar>
                </cl-col>
            </cl-row>
        </cl-card>
        <cl-card label="头像组">
            <cl-avatar-group :urls="urls"></cl-avatar-group>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const urls = ref([
    "/pages/demo/static/avatar1.png",
    "/pages/demo/static/avatar2.png",
    "/pages/demo/static/avatar3.png",
    "/pages/demo/static/avatar4.png",
]);
</script>
pages/demo/view/badge.vue
New file
@@ -0,0 +1,61 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-badge :value="16">
                <cl-button>消息</cl-button>
            </cl-badge>
        </cl-card>
        <cl-card label="自定义内容">
            <cl-badge value="NEW">
                <cl-button>自定义内容</cl-button>
            </cl-badge>
        </cl-card>
        <cl-card label="自定义内容">
            <cl-badge :value="132" :max="99">
                <cl-button>最大</cl-button>
            </cl-badge>
        </cl-card>
        <cl-card label="点形状">
            <cl-badge :value="132" is-dot>
                <cl-button>点形状</cl-button>
            </cl-badge>
        </cl-card>
        <cl-card label="不同样式">
            <cl-badge :value="132" plain>
                <cl-button>不同样式</cl-button>
            </cl-badge>
        </cl-card>
        <cl-card label="不同状态">
            <cl-row>
                <cl-col :span="6">
                    <cl-badge type="primary" is-dot>
                        <cl-button>主要</cl-button>
                    </cl-badge>
                </cl-col>
                <cl-col :span="6">
                    <cl-badge type="success" value="Hot">
                        <cl-button>成功</cl-button>
                    </cl-badge>
                </cl-col>
                <cl-col :span="6">
                    <cl-badge type="error" is-dot>
                        <cl-button>失败</cl-button>
                    </cl-badge>
                </cl-col>
                <cl-col :span="6">
                    <cl-badge type="warning" :value="20">
                        <cl-button>警告</cl-button>
                    </cl-badge>
                </cl-col>
            </cl-row>
        </cl-card>
    </cl-page>
</template>
pages/demo/view/banner.vue
New file
@@ -0,0 +1,38 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-banner :list="urls"></cl-banner>
        </cl-card>
        <cl-card label="卡片">
            <cl-banner :list="urls" type="card"></cl-banner>
        </cl-card>
        <cl-card label="衔接">
            <cl-banner :list="urls" type="chain" @select="onSelect"></cl-banner>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const urls = ref([
    {
        url: "/pages/demo/static/bg1.png",
    },
    {
        url: "/pages/demo/static/bg2.png",
    },
    {
        url: "/pages/demo/static/bg3.png",
    },
    {
        url: "/pages/demo/static/bg4.png",
    },
]);
function onSelect(i: number) {
    console.log(i);
}
</script>
pages/demo/view/card.vue
New file
@@ -0,0 +1,15 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-text value="目光所至则心向往之" />
        </cl-card>
        <cl-card label="显示更多" more more-text="查看更多">
            <cl-text value="被爱好似有依靠" />
        </cl-card>
        <cl-card label="记载中" loading>
            <cl-text value="他也成为了这平淡生活里最明媚的一道光" />
        </cl-card>
    </cl-page>
</template>
pages/demo/view/countdown.vue
New file
@@ -0,0 +1,29 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-countdown :hour="5"></cl-countdown>
        </cl-card>
        <cl-card label="精简模式">
            <cl-countdown :hour="1" simple></cl-countdown>
        </cl-card>
        <cl-card label="自定义格式">
            <cl-countdown :hour="5" format="{d}天{h}时{m}分{s}秒后结束"></cl-countdown>
        </cl-card>
        <cl-card label="自定义样式">
            <cl-countdown
                :hour="5"
                :font-size="40"
                :number-style="{
                    backgroundColor: '#f56c6c',
                    color: '#fff',
                    borderRadius: '8rpx',
                    padding: '6rpx',
                    margin: '0 6rpx',
                }"
            ></cl-countdown>
        </cl-card>
    </cl-page>
</template>
pages/demo/view/divider.vue
New file
@@ -0,0 +1,25 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-divider background-color="#ffffff">默认</cl-divider>
            <cl-divider color="#409EFF" background-color="#ffffff">不同颜色</cl-divider>
        </cl-card>
        <cl-card label="渐变颜色">
            <cl-divider :color="['#fff', '#F56C6C']" background-color="#ffffff"
                >渐变颜色</cl-divider
            >
        </cl-card>
        <cl-card label="线条宽度">
            <cl-divider width="50%" background-color="#ffffff">线条宽度</cl-divider>
        </cl-card>
        <cl-card label="自定义">
            <cl-divider background-color="#ffffff">
                <text class="cl-icon-favor"></text>
                <text>猜你喜欢</text>
            </cl-divider>
        </cl-card>
    </cl-page>
</template>
pages/demo/view/flex.vue
New file
@@ -0,0 +1,74 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-row :gutter="20">
                <cl-col :span="4" @tap="onTap">
                    <view class="item">4</view>
                </cl-col>
                <cl-col :span="12">
                    <view class="item">12</view>
                </cl-col>
                <cl-col :span="8">
                    <view class="item">8</view>
                </cl-col>
            </cl-row>
            <cl-row :gutter="20">
                <cl-col :span="8">
                    <view class="item">8</view>
                </cl-col>
                <cl-col :span="8">
                    <view class="item">8</view>
                </cl-col>
                <cl-col :span="8">
                    <view class="item">8</view>
                </cl-col>
            </cl-row>
            <cl-row :gutter="20">
                <cl-col :span="14">
                    <view class="item">14</view>
                </cl-col>
                <cl-col :span="4">
                    <view class="item">4</view>
                </cl-col>
                <cl-col :span="6">
                    <view class="item">6</view>
                </cl-col>
            </cl-row>
            <cl-row :gutter="20" type="flex" justify="space-between">
                <cl-col :span="6">
                    <view class="item">6</view>
                </cl-col>
                <cl-col :span="6">
                    <view class="item">6</view>
                </cl-col>
            </cl-row>
        </cl-card>
    </cl-page>
</template>
<script setup lang="ts">
function onTap() {
    console.log("tap");
}
</script>
<style lang="scss" scoped>
.item {
    background-color: #f6f7fa;
    text-align: center;
    font-size: 28rpx;
    height: 60rpx;
    line-height: 60rpx;
    margin: 10rpx 0;
}
</style>
pages/demo/view/grid.vue
New file
@@ -0,0 +1,30 @@
<template>
    <cl-page :padding="20">
        <cl-card label="3列">
            <cl-grid :column="3">
                <cl-grid-item v-for="item in 9" :key="item">
                    <view class="item">{{ item }}</view>
                </cl-grid-item>
            </cl-grid>
        </cl-card>
        <cl-card label="6列">
            <cl-grid :column="6">
                <cl-grid-item v-for="item in 24" :key="item">
                    <view class="item">{{ item }}</view>
                </cl-grid-item>
            </cl-grid>
        </cl-card>
    </cl-page>
</template>
<style lang="scss" scoped>
.item {
    background-color: #f6f7fa;
    text-align: center;
    font-size: 28rpx;
    height: 60rpx;
    line-height: 60rpx;
    margin: 10rpx;
}
</style>
pages/demo/view/list-index.vue
New file
@@ -0,0 +1,21 @@
<template>
    <cl-page fullscreen>
        <cl-list-index :data="pcas"></cl-list-index>
    </cl-page>
</template>
<script lang="ts" setup>
import { onReady } from "@dcloudio/uni-app";
import { ref } from "vue";
const pcas = ref();
onReady(() => {
    uni.request({
        url: "https://cool-service.oss-cn-shanghai.aliyuncs.com/app%2Fbase%2F8fe445f06db5463386ae940d45b4bd6c_city-pcas.json",
        success(res) {
            pcas.value = res.data;
        },
    });
});
</script>
pages/demo/view/list.vue
New file
@@ -0,0 +1,64 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-list-item label="账单"></cl-list-item>
        </cl-card>
        <cl-card label="带图标">
            <cl-list-item label="神仙">
                <template #icon>
                    <cl-image :size="60" src="/pages/demo/static/avatar1.png" />
                </template>
            </cl-list-item>
        </cl-card>
        <cl-card label="带描述">
            <cl-list-item label="余额">
                <cl-text type="price" :size="30" :value="159.2" />
            </cl-list-item>
        </cl-card>
        <cl-card label="禁用">
            <cl-list-item label="银行卡" disabled>
                <cl-text color="disabled">兴业银行(***1113)</cl-text>
            </cl-list-item>
        </cl-card>
        <cl-card label="滑动">
            <cl-list-item label="向右滑动" swipe="left">
                <template #menu>
                    <cl-button type="primary">置顶</cl-button>
                    <cl-button type="error">删除</cl-button>
                </template>
            </cl-list-item>
            <cl-list-item label="向左滑动" swipe="right">
                <template #menu>
                    <cl-button type="primary">置顶</cl-button>
                    <cl-button type="error">删除</cl-button>
                </template>
            </cl-list-item>
        </cl-card>
        <cl-card label="自定义">
            <cl-list-item justify="start">
                <cl-row type="flex" align="center">
                    <cl-avatar></cl-avatar>
                    <cl-text value="神仙都没用" :margin="[0, 0, 0, 20]" />
                </cl-row>
                <template #append>
                    <cl-button>修改头像</cl-button>
                </template>
            </cl-list-item>
        </cl-card>
        <cl-card label="按钮组">
            <cl-list :radius="20">
                <cl-list-item label="A"></cl-list-item>
                <cl-list-item label="B"></cl-list-item>
                <cl-list-item label="C"></cl-list-item>
            </cl-list>
        </cl-card>
    </cl-page>
</template>
pages/demo/view/loadmore.vue
New file
@@ -0,0 +1,11 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-loadmore loading background-color="#ffffff"></cl-loadmore>
        </cl-card>
        <cl-card label="不显示分隔符">
            <cl-loadmore loading :divider="false" background-color="#ffffff"></cl-loadmore>
        </cl-card>
    </cl-page>
</template>
pages/demo/view/noticebar.vue
New file
@@ -0,0 +1,35 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-noticebar text="云想衣裳花想容,春风拂槛露华浓。"></cl-noticebar>
        </cl-card>
        <cl-card label="带图标">
            <cl-noticebar
                icon="cl-icon-notification"
                text="云想衣裳花想容,春风拂槛露华浓。"
            ></cl-noticebar>
        </cl-card>
        <cl-card label="可关闭">
            <cl-noticebar text="云想衣裳花想容,春风拂槛露华浓。" closeable></cl-noticebar>
        </cl-card>
        <cl-card label="可滑动">
            <cl-noticebar text="云想衣裳花想容,春风拂槛露华浓。" scrollable></cl-noticebar>
        </cl-card>
        <cl-card label="不同方向">
            <cl-noticebar
                :text="[
                    '云想衣裳花想容,春风拂槛露华浓云想衣裳花想容,春风拂槛露华浓云想衣裳花想容,春风拂槛露华浓。',
                    '这世界那么多人',
                    '目光所至则心向往之',
                ]"
                direction="vertical"
                scrollable
                :duration="3"
            ></cl-noticebar>
        </cl-card>
    </cl-page>
</template>
pages/demo/view/popup.vue
New file
@@ -0,0 +1,63 @@
<template>
    <cl-page :padding="20">
        <cl-popup v-model="visible">
            <cl-text value="云想衣裳花想容,春风拂槛露华浓。" />
        </cl-popup>
        <cl-popup v-model="visible_modal" :modal="false" :padding="0">
            <cl-button type="primary" @tap="visible_modal = false">点我关闭</cl-button>
        </cl-popup>
        <cl-popup v-model="visible_custom" background-color="#333" border-radius="20rpx">
            <cl-text color="#fff" value="云想衣裳花想容,春风拂槛露华浓。" />
        </cl-popup>
        <cl-popup v-model="visible_left" direction="left">
            <cl-text value="云想衣裳花想容,春风拂槛露华浓。" />
        </cl-popup>
        <cl-popup v-model="visible_top" direction="top">
            <cl-text value="云想衣裳花想容,春风拂槛露华浓。" />
        </cl-popup>
        <cl-popup v-model="visible_right" direction="right">
            <cl-text value="云想衣裳花想容,春风拂槛露华浓。" />
        </cl-popup>
        <cl-popup v-model="visible_bottom" direction="bottom">
            <cl-text value="云想衣裳花想容,春风拂槛露华浓。" />
        </cl-popup>
        <cl-card label="基础用法">
            <cl-button @tap="visible = true">打开</cl-button>
        </cl-card>
        <cl-card label="不显示遮罩层">
            <cl-button @tap="visible_modal = true">打开</cl-button>
        </cl-card>
        <cl-card label="添加样式">
            <cl-button @tap="visible_custom = true">打开</cl-button>
        </cl-card>
        <cl-card label="方向">
            <cl-button @tap="visible_left = true">左边</cl-button>
            <cl-button @tap="visible_top = true">上边</cl-button>
            <cl-button @tap="visible_right = true">右边</cl-button>
            <cl-button @tap="visible_bottom = true">下边</cl-button>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const visible = ref(false);
const visible_modal = ref(false);
const visible_custom = ref(false);
const visible_left = ref(false);
const visible_top = ref(false);
const visible_right = ref(false);
const visible_bottom = ref(false);
</script>
pages/demo/view/progress.vue
New file
@@ -0,0 +1,34 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-progress :value="40"></cl-progress>
        </cl-card>
        <cl-card label="不同颜色">
            <cl-progress color="#67C23A" :value="40"></cl-progress>
        </cl-card>
        <cl-card label="可调节颜色">
            <cl-progress :value="60" :color="['#F56C6C', '#E6A23C', '#67C23A']"></cl-progress>
        </cl-card>
        <cl-card label="自定义">
            <cl-progress :value="val">
                <template #text>
                    <cl-row :padding="[0, 0, 0, 15]">
                        <cl-text v-if="val >= 90">优</cl-text>
                        <cl-text v-else-if="val >= 80">良</cl-text>
                        <cl-text v-else-if="val >= 60">及格</cl-text>
                        <cl-text v-else>{{ val }}</cl-text>
                    </cl-row>
                </template>
            </cl-progress>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const val = ref(80);
</script>
pages/demo/view/scroller.vue
New file
@@ -0,0 +1,43 @@
<template>
    <cl-page fullscreen>
        <cl-scroller :ref="setRefs('scroller')" @up="loadmore" @down="refresh">
            <cl-list-item
                v-for="(item, index) in list"
                :key="index"
                :label="`${item + 1}`"
                :padding="[0, 20, 0, 20]"
                border
            />
        </cl-scroller>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useCool } from "/@/cool";
import { onReady } from "@dcloudio/uni-app";
import { range } from "lodash-es";
const { refs, setRefs } = useCool();
const list = ref<number[]>([]);
function loadmore() {
    setTimeout(() => {
        const data = range(list.value.length, list.value.length + 20);
        list.value.push(...data);
    }, 300);
}
function refresh() {
    list.value = range(20);
    setTimeout(() => {
        refs.scroller.end();
    }, 50000);
}
onReady(() => {
    refresh();
});
</script>
pages/demo/view/search.vue
New file
@@ -0,0 +1,19 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-search placeholder="搜索图片、文章、链接"></cl-search>
        </cl-card>
        <cl-card label="不显示按钮">
            <cl-search placeholder="搜索图片、文章、链接" :show-search-button="false"></cl-search>
        </cl-card>
        <cl-card label="追加内容">
            <cl-search placeholder="搜索图片、文章、链接">
                <template #append>
                    <cl-avatar :size="60" src=""></cl-avatar>
                </template>
            </cl-search>
        </cl-card>
    </cl-page>
</template>
pages/demo/view/skeleton.vue
New file
@@ -0,0 +1,86 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <view class="count">
                <view class="item" v-for="(item, index) in list" :key="index">
                    <cl-skeleton
                        :height="120"
                        :width="120"
                        :radius="120"
                        :margin="[0, 0, 20, 0]"
                        :loading="!item.avatar"
                    >
                        <cl-image
                            :radius="120"
                            :src="item.avatar"
                            background-color="#fff"
                            v-if="item.avatar"
                        />
                    </cl-skeleton>
                    <cl-skeleton
                        :custom-style="{
                            height: '28rpx',
                            width: '90rpx',
                            borderRadius: '6rpx',
                        }"
                        :loading="!item.name"
                    >
                        <cl-text :size="24" align="center" block>{{ item.name }}</cl-text>
                    </cl-skeleton>
                </view>
            </view>
        </cl-card>
        <cl-card label="基础用法2">
            <cl-banner :list="list" type="card" :height="300">
                <template #item="{ item }">
                    <cl-skeleton :radius="20" height="100%" :loading="!item.bg">
                        <image class="cl-banner-item__image" :src="item.bg" mode="aspectFill" />
                    </cl-skeleton>
                </template>
            </cl-banner>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { onReady } from "@dcloudio/uni-app";
import { uniqueId } from "lodash-es";
import { ref } from "vue";
const list = ref<any[]>([{}, {}, {}, {}]);
function get() {
    setTimeout(() => {
        list.value = list.value.map((_, i) => {
            return {
                id: uniqueId(),
                avatar: `/pages/demo/static/avatar${i + 1}.png`,
                bg: `/pages/demo/static/bg${i + 1}.png`,
                name: ["小青", "小白", "小黑", "笑死"][i],
            };
        });
    }, 1500);
}
onReady(() => {
    get();
});
</script>
<style lang="scss" scoped>
.count {
    display: flex;
    padding: 20rpx 0;
    .item {
        flex: 1;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        height: 200rpx;
    }
}
</style>
pages/demo/view/slider.vue
New file
@@ -0,0 +1,7 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-slider show-value></cl-slider>
        </cl-card>
    </cl-page>
</template>
pages/demo/view/tabs.vue
New file
@@ -0,0 +1,133 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-tabs v-model="active" :list="list" />
        </cl-card>
        <cl-card label="下拉框">
            <cl-tabs
                v-model="active"
                :border="false"
                :list="list"
                show-dropdown
                :ref="setRefs('tabs')"
            >
                <template #dropdown>
                    <view class="dropdown">
                        <cl-tag
                            v-for="item in list"
                            :key="item.value"
                            :margin="10"
                            :type="active === item.value ? 'primary' : 'info'"
                            @tap="check(item.value)"
                        >
                            {{ item.label }}
                        </cl-tag>
                    </view>
                </template>
            </cl-tabs>
        </cl-card>
        <cl-card label="居中">
            <cl-tabs v-model="active2" :list="list2" justify="center" />
        </cl-card>
        <cl-card label="填充">
            <cl-tabs v-model="active2" :list="list2" fill />
        </cl-card>
        <cl-card label="块状">
            <cl-tabs v-model="active2" :list="list2" :show-line="false" :show-slider="true" />
        </cl-card>
        <cl-card label="自定义">
            <cl-tabs
                v-model="active"
                :list="list"
                :border="false"
                color="red"
                background-color="f6f7fa"
                :show-line="false"
            />
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useCool } from "/@/cool";
const { refs, setRefs } = useCool();
const active = ref(1);
const list = ref([
    {
        label: "百亿补贴",
        value: 1,
    },
    {
        label: "包邮",
        value: 2,
    },
    {
        label: "限时达",
        value: 3,
    },
    {
        label: "同城速配",
        value: 4,
    },
    {
        label: "货到付款",
        value: 5,
    },
    {
        label: "分期免息",
        value: 6,
    },
    {
        label: "配送全球",
        value: 7,
    },
    {
        label: "促销",
        value: 8,
    },
]);
const active2 = ref(2);
const list2 = ref([
    {
        label: "盒装",
        value: 1,
    },
    {
        label: "纸包装",
        value: 2,
    },
    {
        label: "袋装",
        value: 3,
    },
    {
        label: "罐装",
        value: 4,
    },
]);
function check(value: any) {
    active.value = value;
    refs.tabs.closeDropdown();
}
</script>
<style lang="scss" scoped>
.dropdown {
    background-color: #fff;
    padding: 10rpx;
    border: $cl-border-width solid $cl-border-color;
    border-radius: 12rpx;
}
</style>
pages/demo/view/timeline.vue
New file
@@ -0,0 +1,51 @@
<template>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-timeline>
                <cl-timeline-item v-for="(item, index) in list" :key="index" :timestamp="item.timestamp"
                    :show-line="item.showLine" :content="item.content" :icon="item.icon" :color="item.color">
                </cl-timeline-item>
            </cl-timeline>
        </cl-card>
        <cl-card label="自定义内容">
            <cl-timeline>
                <cl-timeline-item v-for="(item, index) in list" :key="index" :timestamp="item.timestamp"
                    :show-line="item.showLine" :content="item.content" :icon="item.icon" :color="item.color">
                    <template #content>
                        <cl-tag>{{ item.content }}</cl-tag>
                    </template>
                </cl-timeline-item>
            </cl-timeline>
        </cl-card>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
const list = ref<ClTimeline.Item[]>([
    {
        timestamp: "昨天",
        content: "开通账号,赠送500"
    },
    {
        timestamp: "今天",
        content: "成功转入9000",
        color: "#409EFF"
    },
    {
        timestamp: "11-06",
        content: "开始计算收益",
        icon: "cl-icon-toast-waiting",
        color: "#E6A23C"
    },
    {
        timestamp: "11-09",
        content: "收益到账",
        showLine: true,
        icon: "cl-icon-check-border",
        color: "#67C23A"
    }
])
</script>
pages/demo/view/topbar.vue
New file
@@ -0,0 +1,41 @@
<template>
    <cl-topbar title="Topbar 顶部导航栏" description="小标题"></cl-topbar>
    <cl-page :padding="20">
        <cl-card label="基础用法">
            <cl-topbar title="Topbar 顶部导航栏"></cl-topbar>
        </cl-card>
        <cl-card label="带描述">
            <cl-topbar title="Topbar 顶部导航栏" description="兼容多端"></cl-topbar>
        </cl-card>
        <cl-card label="自定义">
            <cl-topbar title="标题">
                <template #append>
                    <view class="cl-topbar__icon" @tap="toToast">
                        <text class="cl-icon-warning-border"></text>
                    </view>
                </template>
            </cl-topbar>
        </cl-card>
        <cl-card label="不需要返回键">
            <cl-topbar title="标题" :show-back="false"></cl-topbar>
        </cl-card>
        <cl-card label="其他颜色">
            <cl-topbar title="标题" color="#fff" background-color="#000"></cl-topbar>
        </cl-card>
    </cl-page>
</template>
<script setup lang="ts">
import { useUi } from "/$/cool-ui";
const ui = useUi();
function toToast() {
    ui.showTips("Cool-uni");
}
</script>
pages/demo/view/waterfall.vue
New file
@@ -0,0 +1,107 @@
<template>
    <cl-page :padding="20">
        <cl-waterfall ref="Waterfall" :column="2">
            <template #default="{ list }">
                <cl-waterfall-column v-for="(item, index) in list" :key="index">
                    <view class="goods-item" v-for="(item2, index2) in item" :key="index2">
                        <image :src="item2.url" mode="widthFix"></image>
                        <cl-text :value="item2.title" :margin="20" :line-height="1.5" />
                    </view>
                </cl-waterfall-column>
            </template>
        </cl-waterfall>
        <cl-loadmore :loading="loading" :divider="false"></cl-loadmore>
    </cl-page>
</template>
<script lang="ts" setup>
import { onReady } from "@dcloudio/uni-app";
import { ref } from "vue";
import { usePager } from "/@/cool";
const { onRefresh, onData } = usePager();
const Waterfall = ref<ClWaterfall.Ref>();
const loading = ref(false);
function refresh(params?: any) {
    const { data, next } = onRefresh(params);
    onData((list) => {
        if (data.page == 1) {
            Waterfall.value?.refresh(list);
        } else {
            Waterfall.value?.append(list);
        }
    });
    loading.value = true;
    return next(
        new Promise((resolve) => {
            setTimeout(() => {
                loading.value = false;
                resolve({
                    list: [
                        {
                            title: "Watslim 擦窗机器人全自动智能遥控电动擦窗机神器 智能款",
                            url: "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fhbimg.b0.upaiyun.com%2F47d3d4dab03cd71a8800c389d4dd8d5928ab826c37ac3-Vck8iS_fw658&refer=http%3A%2F%2Fhbimg.b0.upaiyun.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1654339724&t=ec32099a3891504569cb6ec390154c54",
                        },
                        {
                            title: "富士instax立拍立得 一次成像相机 mini11 冰晶白",
                            url: "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fhbimg.b0.upaiyun.com%2Feed5cf6958521b0b9e36bb9f28b90d59caffa5e1a661-MdQUZ1_fw658&refer=http%3A%2F%2Fhbimg.b0.upaiyun.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1654339724&t=16e5d80bab1e1fb385c3e8e14d11e0dd",
                        },
                        {
                            title: "syitren Syitren PARON赛塔林黑胶唱片机HIFi复古",
                            url: "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fhbimg.b0.upaiyun.com%2F438d277c48a7364d7f5c9947e5cab9b8928de5131b023-llMbUl_fw658&refer=http%3A%2F%2Fhbimg.b0.upaiyun.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1654339724&t=c648048f6b243018dc3d98c4c2191d15",
                        },
                        {
                            title: "华为路由AX3 凌霄双核/wifi6/智能分频3000M/高速路由器",
                            url: "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fhbimg.b0.upaiyun.com%2Fed5b7cc0611afb58821f6efa2ccbfc9f9c3f14c11c3a1-2dUtHQ_fw658&refer=http%3A%2F%2Fhbimg.b0.upaiyun.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1654339724&t=2d4e188339ad12fdceac4217ed6e0e5e",
                        },
                        {
                            title: "千幻魔镜 十代 vr眼镜手机VR 智能3D眼镜VR游戏头盔",
                            url: "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fhbimg.b0.upaiyun.com%2F111d7605120959a2ca836310277f8daf42855ec639e87-kzE5gK_fw658&refer=http%3A%2F%2Fhbimg.b0.upaiyun.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1654339724&t=5e37c84f9f506f05e812031c660fe56b",
                        },
                        {
                            title: "神火(supfire) X60-T强光手电筒变焦远射USB充电式",
                            url: "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fhbimg.b0.upaiyun.com%2F8813230e61b64518e74f2b1b088e96036d62d79231874-bkqhNM_fw658&refer=http%3A%2F%2Fhbimg.b0.upaiyun.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1654339724&t=0ac1b94c1d8e7478c122dadcd6b3915b",
                        },
                    ],
                    pagination: {
                        total: 100,
                        page: data.page,
                        size: 20,
                    },
                });
            }, 1000);
        }),
    );
}
onReady(() => {
    refresh();
});
defineExpose({
    refresh,
});
</script>
<style lang="scss" scoped>
.goods-item {
    margin-bottom: 20rpx;
    border: $cl-border-width solid #eee;
    border-radius: 16rpx;
    overflow: hidden;
    background-color: #fff;
    image {
        display: block;
        width: 100%;
    }
}
</style>
pages/goods/comment.vue
New file
@@ -0,0 +1,57 @@
<template>
    <cl-page>
        <view class="page">
            <view class="list">
                <view class="item" v-for="item in list">
                    <comment-item :item="item" />
                </view>
                <cl-empty text="暂无评论" icon="message" v-if="isEmpty(list)" />
            </view>
        </view>
    </cl-page>
</template>
<script lang="ts" setup>
import { onMounted } from "vue";
import { useCool, usePager } from "/@/cool";
import { isEmpty } from "lodash-es";
import CommentItem from "./components/comment-item.vue";
const { service, router } = useCool();
const { list, onRefresh } = usePager<Eps.GoodsCommentEntity>();
function refresh(params?: any) {
    const { next, data } = onRefresh(params);
    next(service.goods.comment.page(data));
}
onMounted(() => {
    refresh({
        goodsId: router.query.goodsId,
    });
});
defineExpose({
    refresh,
});
</script>
<style lang="scss" scoped>
.page {
    padding: 24rpx;
    .list {
        .item {
            background-color: #fff;
            padding: 24rpx;
            border-radius: 24rpx;
            margin-bottom: 24rpx;
            &:last-child {
                margin-bottom: 0;
            }
        }
    }
}
</style>
pages/goods/components/comment-item.vue
New file
@@ -0,0 +1,81 @@
<template>
    <view class="comment-item">
        <view class="inner">
            <view class="user">
                <cl-avatar :size="70" :src="item.avatarUrl" />
                <cl-row :margin="[4, 0, 0, 20]">
                    <cl-text :size="24" block :margin="[0, 0, 4, 0]">{{ item.nickName }}</cl-text>
                    <cl-rate disabled :size="24" v-model="item.starCount" />
                </cl-row>
            </view>
            <cl-text
                :ellipsis="demo ? 2 : undefined"
                block
                :line-height="1.5"
                :margin="[20, 0, 0, 0]"
                >{{ item.content }}</cl-text
            >
        </view>
        <view class="pics" v-if="!isEmpty(item.pics)">
            <cl-image
                :size="120"
                :src="resizeImage(item.pics[0], 120)"
                :preview-list="item.pics"
                :radius="12"
            />
            <text class="num" v-if="1 || item.pics.length > 1">+{{ item.pics.length }}</text>
        </view>
    </view>
</template>
<script lang="ts" setup>
import { type PropType } from "vue";
import { isEmpty } from "lodash-es";
import { useUpload } from "/@/cool";
defineProps({
    item: {
        type: Object as PropType<Eps.GoodsCommentEntity>,
        default: () => ({}),
    },
    demo: Boolean,
});
const { resizeImage } = useUpload();
</script>
<style lang="scss" scoped>
.comment-item {
    display: flex;
    .inner {
        flex: 1;
        .user {
            display: flex;
            align-items: center;
        }
    }
    .pics {
        position: relative;
        margin-left: 24rpx;
        .num {
            position: absolute;
            top: 84rpx;
            right: 10rpx;
            font-size: 20rpx;
            background-color: rgba(0, 0, 0, 0.6);
            color: #fff;
            padding: 4rpx;
            border-radius: 6rpx;
            letter-spacing: 1rpx;
            line-height: 1;
        }
    }
}
</style>
pages/goods/components/filter-bar.vue
New file
@@ -0,0 +1,350 @@
<template>
    <view class="filter-bar">
        <cl-filter-bar v-model="flt.form" @change="flt.onChange">
            <cl-filter-item label="综合" prop="score" type="switch" />
            <cl-filter-item label="销量" prop="sold" type="order" />
            <cl-filter-item label="价格" prop="price" type="order" />
            <view class="cl-filter-item" @tap="adv.open">
                <text>筛选</text>
                <cl-icon class-name="shop-icon-filter" :size="36" />
            </view>
        </cl-filter-bar>
    </view>
    <!-- 侧边筛选 -->
    <cl-popup v-model="adv.visible" direction="right" :size="600" :padding="0">
        <view class="filter-adv">
            <!-- 筛选内容 -->
            <scroll-view scroll-y class="filter-adv__container">
                <view class="filter-adv__list">
                    <!-- 价格 -->
                    <view class="filter-adv__item">
                        <text class="label">价格区间</text>
                        <cl-row type="flex" justify="space-between">
                            <cl-input
                                v-model="adv.form.minPrice"
                                placeholder="最低价"
                                type="number"
                                round
                                align="center"
                                @confirm="toSearch"
                            />
                            <cl-icon name="minus" :margin="[0, 20, 0, 20]" />
                            <cl-input
                                v-model="adv.form.maxPrice"
                                placeholder="最高价"
                                type="number"
                                round
                                align="center"
                                @confirm="toSearch"
                            />
                        </cl-row>
                    </view>
                    <!-- 分类 -->
                    <view class="filter-adv__item">
                        <cl-loading-mask :loading="type.loading">
                            <view class="category">
                                <!-- 一级 -->
                                <template v-for="m1 in type.list">
                                    <view class="m1" :key="m1.id" v-if="m1.children">
                                        <text class="label">
                                            {{ m1.name }}
                                        </text>
                                        <view class="m1-list">
                                            <!-- 二级 -->
                                            <view
                                                class="m2"
                                                v-for="m2 in m1.children"
                                                :key="m2.id"
                                                :class="{
                                                    'is-active': adv.form.typeId.includes(m2.id!),
                                                }"
                                                @tap="type.select(m2)"
                                            >
                                                {{ m2.name }}
                                            </view>
                                        </view>
                                    </view>
                                </template>
                            </view>
                        </cl-loading-mask>
                    </view>
                </view>
            </scroll-view>
            <!-- 操作按钮 -->
            <view class="filter-adv__footer">
                <view class="inner">
                    <button class="reset" @tap="adv.reset">重置</button>
                    <button class="confirm" @tap="adv.confirm">确定</button>
                </view>
            </view>
        </view>
    </cl-popup>
</template>
<script lang="ts" setup>
import { cloneDeep, isEmpty } from "lodash-es";
import { useCool } from "/@/cool";
import { deepTree } from "/@/cool/utils";
import { onMounted, reactive } from "vue";
interface Type extends Eps.GoodsTypeEntity {
    children?: Eps.GoodsTypeEntity[];
}
const emit = defineEmits(["search"]);
const { service, router } = useCool();
// 高级搜索
const adv = reactive({
    visible: false,
    form: {
        minPrice: "",
        maxPrice: "",
        typeId: [] as number[],
    },
    _form: null as any,
    open() {
        adv.visible = true;
        // 默认表单
        if (!adv._form) {
            adv._form = cloneDeep(adv.form);
        }
        // 获取分类
        type.refresh();
    },
    close() {
        adv.visible = false;
    },
    reset() {
        adv.form = cloneDeep(adv._form);
        adv.confirm();
    },
    confirm() {
        adv.close();
        toSearch();
    },
});
// 过滤栏
const flt = reactive({
    form: {
        sold: "",
        price: "desc",
        score: false,
    },
    onChange({ prop, value }: { prop: "sold" | "price" | "score"; value: never }) {
        flt.form.sold = "";
        flt.form.price = "";
        flt.form.score = false;
        flt.form[prop] = value;
        switch (prop) {
            case "score":
                toSearch({
                    order: undefined,
                    sort: undefined,
                });
                break;
            case "price":
            case "sold":
                toSearch({
                    order: value ? prop : undefined,
                    sort: value || undefined,
                });
                break;
        }
    },
});
// 商品分类
const type = reactive({
    list: [] as Type[],
    loading: false,
    async refresh() {
        type.loading = true;
        await service.goods.type.list().then((res) => {
            type.list = deepTree(res);
        });
        type.loading = false;
    },
    select(item: Type) {
        const i = adv.form.typeId.indexOf(item.id!);
        if (i >= 0) {
            adv.form.typeId.splice(i, 1);
        } else {
            adv.form.typeId.push(item.id!);
        }
    },
});
// 搜索
function toSearch(params?: any) {
    const { minPrice, maxPrice, typeId } = adv.form;
    const data = {
        page: 1,
        minPrice: minPrice === "" ? undefined : minPrice,
        maxPrice: maxPrice === "" ? undefined : maxPrice,
        typeId: isEmpty(typeId) ? undefined : typeId,
        ...params,
    };
    emit("search", data);
}
onMounted(() => {
    const { typeId } = router.query;
    if (typeId) {
        adv.form.typeId = typeId.split(",").map(Number);
    }
});
</script>
<style lang="scss" scoped>
.filter-adv {
    background-color: $cl-color-bg;
    height: 100%;
    &__container {
        height: calc(100% - 100rpx);
    }
    &__list {
        padding: 24rpx;
    }
    &__item {
        background-color: #fff;
        margin-bottom: 24rpx;
        padding: 24rpx;
        border-radius: 24rpx;
        .label {
            display: block;
            font-size: 26rpx;
            margin-bottom: 24rpx;
            line-height: 1;
        }
        .category {
            min-height: 200rpx;
            .m1 {
                margin-bottom: 30rpx;
                &-name {
                    display: flex;
                    align-items: center;
                    height: 80rpx;
                    font-size: 28rpx;
                }
                &-list {
                    width: 100%;
                    line-height: normal;
                    border: 2rpx solid #f7f7f7;
                    padding: 5rpx 20rpx;
                    box-sizing: border-box;
                    border-radius: 24rpx;
                }
                &:last-child {
                    margin-bottom: 0;
                }
                .m2 {
                    display: inline-flex;
                    justify-content: center;
                    align-items: center;
                    border-radius: 10rpx;
                    margin: 10rpx 20rpx 10rpx 0;
                    padding: 0 25rpx;
                    font-size: 22rpx;
                    height: 50rpx;
                    text-align: center;
                    border-radius: 50rpx;
                    color: #444;
                    overflow: hidden;
                    text-overflow: ellipsis;
                    white-space: nowrap;
                    border: 2rpx solid #f7f7f7;
                    background-color: #f7f7f7;
                    &.is-active {
                        color: $cl-color-primary;
                        border-color: $cl-color-primary;
                        background-color: rgba($cl-color-primary, 0.1);
                    }
                }
            }
        }
        &:last-child {
            margin-bottom: 0;
        }
    }
    &__footer {
        display: flex;
        align-items: center;
        justify-content: center;
        height: 100rpx;
        padding: 0 30rpx;
        background-color: #fff;
        .inner {
            display: inline-flex;
            border: 2rpx solid $cl-color-primary;
            background-color: $cl-color-primary;
            border-radius: 80rpx;
            overflow: hidden;
            flex: 1;
            button {
                height: 64rpx;
                line-height: 64rpx;
                padding: 0;
                margin: 0;
                font-size: 28rpx;
                flex: 1;
                background-color: transparent;
                color: #fff;
                &::after {
                    border: 0;
                }
                &.reset {
                    color: $cl-color-primary;
                    background-color: #fff;
                    border-radius: 80rpx 0 80rpx 80rpx;
                }
            }
        }
    }
}
</style>
pages/goods/components/goods-comment.vue
New file
@@ -0,0 +1,90 @@
<template>
    <view class="goods-comment">
        <cl-row type="flex" justify="space-between" :height="90">
            <cl-text :size="28" bold :value="`商品评论(${total})`"></cl-text>
            <cl-text :size="24" color="info" @tap="toAll">
                全部
                <text class="cl-icon-arrow-right"></text>
            </cl-text>
        </cl-row>
        <view class="list">
            <view class="item" v-for="item in list" :key="item.id" @tap="toAll">
                <comment-item :item="item" demo />
            </view>
            <cl-empty
                text="暂无评论"
                icon="message"
                :icon-size="200"
                :fixed="false"
                :padding="[30, 0, 130, 0]"
                v-if="isEmpty(list)"
            />
        </view>
    </view>
</template>
<script lang="ts" setup>
import { onMounted, ref, type PropType } from "vue";
import { useCool } from "/@/cool";
import { isEmpty } from "lodash-es";
import CommentItem from "./comment-item.vue";
const props = defineProps({
    info: Object as PropType<Eps.GoodsInfoEntity>,
});
const { service, router } = useCool();
const list = ref<Eps.GoodsCommentEntity[]>([]);
const total = ref(0);
function refresh() {
    service.goods.comment
        .page({
            goodsId: router.query.id,
            page: 1,
            size: 2,
            order: "createTime",
            sort: "desc",
        })
        .then((res) => {
            list.value = res.list;
            total.value = res.pagination.total || 0;
        });
}
function toAll() {
    router.push({
        path: "/pages/goods/comment",
        query: {
            goodsId: router.query.id,
        },
    });
}
onMounted(() => {
    refresh();
});
</script>
<style lang="scss" scoped>
.goods-comment {
    position: relative;
    background-color: #fff;
    padding: 0 30rpx 24rpx 30rpx;
    margin-bottom: 24rpx;
    .list {
        .item {
            margin-bottom: 24rpx;
            &:last-child {
                margin-bottom: 0;
            }
        }
    }
}
</style>
pages/goods/components/goods-detail.vue
New file
@@ -0,0 +1,36 @@
<template>
    <view class="goods-detail">
        <cl-row type="flex" :height="90" :padding="[0, 30, 0, 30]">
            <cl-text :size="28" bold value="商品详情"></cl-text>
        </cl-row>
        <view class="mp-html" v-if="info?.content">
            <mp-html :content="info?.content"></mp-html>
        </view>
        <cl-empty
            icon="comm"
            text="暂无描述"
            :icon-size="200"
            :fixed="false"
            :padding="[30, 0, 130, 0]"
            v-else
        />
    </view>
</template>
<script lang="ts" setup>
import { type PropType } from "vue";
defineProps({
    info: Object as PropType<Eps.GoodsInfoEntity>,
});
</script>
<style lang="scss" scoped>
.goods-detail {
    position: relative;
    background-color: #fff;
    margin-bottom: 24rpx;
}
</style>
pages/goods/components/goods-info.vue
New file
@@ -0,0 +1,158 @@
<template>
    <view class="goods-info">
        <view class="banner">
            <swiper class="cl-banner" indicator-dots>
                <swiper-item v-for="(item, index) in banner" :key="index">
                    <image class="cl-banner-item__image" mode="aspectFill" :src="item" />
                </swiper-item>
            </swiper>
        </view>
        <view class="content">
            <!-- 售价 -->
            <cl-row type="flex" justify="space-between" :padding="[20, 0, 20, 0]">
                <cl-skeleton
                    :height="54"
                    :loading="!info"
                    :loading-style="{
                        width: '200rpx',
                    }"
                >
                    <cl-text
                        type="price"
                        bold
                        color="error"
                        :size="50"
                        :value="spec.info?.price || info?.price"
                    />
                </cl-skeleton>
                <cl-skeleton
                    :loading-style="{
                        height: '38rpx',
                        width: '90rpx',
                        borderRadius: '38rpx',
                    }"
                    :loading="!info"
                >
                    <cl-tag round size="small">已售 {{ info?.sold || 0 }}</cl-tag>
                </cl-skeleton>
            </cl-row>
            <!-- 优惠券 -->
            <cl-row :margin="[0, 0, 20, 0]">
                <coupon-get />
            </cl-row>
            <!-- 标题 -->
            <cl-skeleton
                :margin="[0, 0, 20, 0]"
                :loading-style="{
                    height: '48rpx',
                }"
                :loading="!info"
            >
                <cl-text bold block :line-height="1.5" :size="32">
                    {{ info?.title }} {{ spec.info?.name }}
                </cl-text>
            </cl-skeleton>
            <!-- 其他 -->
            <view class="menu">
                <!-- 规格 -->
                <view
                    class="item"
                    @tap="
                        spec.open({
                            action: 'select',
                            goods: info!,
                        })
                    "
                >
                    <cl-icon class-name="shop-icon-sku" :size="52" :margin="[0, 10, 0, 0]" />
                    <cl-text :ellipsis="1">
                        {{ spec.text }}
                    </cl-text>
                    <text
                        class="cl-icon-close-border"
                        @tap.stop="spec.clear"
                        v-if="!!spec.info"
                    ></text>
                </view>
            </view>
        </view>
    </view>
</template>
<script lang="ts" setup>
import { computed, type PropType } from "vue";
import { useUpload } from "/@/cool";
import { useSpec } from "/@/hooks";
import { isEmpty } from "lodash-es";
import CouponGet from "/@/components/coupon/get.vue";
const props = defineProps({
    info: Object as PropType<Eps.GoodsInfoEntity>,
});
const { resizeImage } = useUpload();
const spec = useSpec();
// 轮播图列表
const banner = computed(() => {
    // 规格图
    if (!isEmpty(spec.info?.images)) {
        return spec.info?.images;
    }
    // 商品示例图
    if (!isEmpty(props.info?.pics)) {
        return props.info?.pics;
    }
    // 主图
    return [props.info?.mainPic];
});
</script>
<style lang="scss" scoped>
.goods-info {
    margin-bottom: 24rpx;
    .banner {
        .cl-banner {
            height: 760rpx;
            .cl-banner-item__image {
                border-radius: 0;
            }
        }
    }
    .content {
        padding: 10rpx 30rpx;
        background-color: #fff;
        .menu {
            .item {
                display: flex;
                align-items: center;
                height: 80rpx;
                background-color: $cl-color-bg;
                padding: 0 16rpx;
                border-radius: 12rpx;
                margin-bottom: 20rpx;
                padding-right: 60rpx;
                position: relative;
                .cl-icon-close-border {
                    position: absolute;
                    right: 20rpx;
                    color: $cl-color-info;
                }
            }
        }
    }
}
</style>
pages/goods/detail.vue
New file
@@ -0,0 +1,250 @@
<template>
    <cl-page>
        <view class="page">
            <cl-sticky>
                <cl-topbar :border="false">
                    <cl-tabs
                        v-model="tabs.active"
                        :list="tabs.list"
                        justify="center"
                        un-color="#777"
                        @change="tabs.onChange"
                        v-if="tabs.scrollTop >= 200"
                    />
                    <view class="cl-topbar__text" v-else>
                        <text class="cl-topbar__title">商品详情</text>
                    </view>
                </cl-topbar>
            </cl-sticky>
            <!-- 商品信息 -->
            <goods-info :info="info" />
            <!-- 商品评论 -->
            <goods-comment :info="info" />
            <!-- 商品详情 -->
            <goods-detail :info="info" />
            <!-- 商品规格 -->
            <goods-spec :ref="setRefs('goodsSpec')" />
            <cl-footer border :flex="false" :padding="0">
                <view class="footer">
                    <view class="icons">
                        <view class="item" @tap="toCs">
                            <cl-icon
                                class-name="shop-icon-message"
                                :size="56"
                                :margin="[0, 0, 8, 0]"
                            />
                            <cl-text :size="22" value="客服" />
                        </view>
                        <view class="item" @tap="router.push('/pages/index/shopping-cart')">
                            <cl-badge :value="spCart.num" :margin="[10, 6, 0, 0]">
                                <cl-icon
                                    class-name="shop-icon-q-order"
                                    :size="56"
                                    :margin="[0, 0, 8, 0]"
                                />
                            </cl-badge>
                            <cl-text :size="22" value="购物车" />
                        </view>
                    </view>
                    <cl-button :disabled="!info" @tap="openSpec('spCart')">加入购物车</cl-button>
                    <cl-button :disabled="!info" type="primary" @tap="openSpec('buy')"
                        >立即购买</cl-button
                    >
                </view>
            </cl-footer>
        </view>
    </cl-page>
</template>
<script lang="ts" setup>
import { reactive, ref } from "vue";
import { useCool } from "/@/cool";
import { onPageScroll, onReady } from "@dcloudio/uni-app";
import { last } from "lodash-es";
import { useShoppingCart, useSpec } from "/@/hooks";
import GoodsInfo from "./components/goods-info.vue";
import GoodsComment from "./components/goods-comment.vue";
import GoodsDetail from "./components/goods-detail.vue";
import GoodsSpec from "/@/components/goods/spec.vue";
const { service, router, refs, setRefs } = useCool();
const spec = useSpec();
const spCart = useShoppingCart();
const { statusBarHeight = 0 } = uni.getSystemInfoSync();
// 顶部选项栏
const tabs = reactive({
    scrollTop: 0,
    active: "info",
    list: [
        {
            label: "信息",
            value: "info",
            top: 0,
        },
        {
            label: "评论",
            value: "comment",
            top: 0,
        },
        {
            label: "详情",
            value: "detail",
            top: 0,
        },
    ],
    getSelector(name: string) {
        let selector = `.goods-${name}`;
        // #ifdef MP
        selector = ".page >>> " + selector;
        // #endif
        return selector;
    },
    async getTop() {
        return Promise.all(
            tabs.list.map((e) => {
                return new Promise((resolve) => {
                    uni.createSelectorQuery()
                        .select(tabs.getSelector(e.value))
                        .boundingClientRect((res) => {
                            e.top = res.top || 0 - 44;
                            resolve(true);
                        })
                        .exec();
                });
            }),
        );
    },
    lock: null as any,
    async onScroll(top: number) {
        tabs.scrollTop = top;
        if (!tabs.lock) {
            if (!last(tabs.list)?.top) {
                await tabs.getTop();
            }
            const d = last(tabs.list.filter((e) => top + 44 >= e.top));
            if (d) {
                tabs.active = d.value;
            }
        }
    },
    onChange(name: string) {
        if (tabs.lock) {
            clearTimeout(tabs.lock);
        }
        uni.createSelectorQuery()
            .select(tabs.getSelector(name))
            .boundingClientRect((res) => {
                uni.pageScrollTo({
                    scrollTop: (res.top || 0) - statusBarHeight - 44 + tabs.scrollTop,
                    duration: 200,
                });
                tabs.lock = setTimeout(() => {
                    tabs.lock = null;
                }, 500);
            })
            .exec();
    },
});
// 商品详情
const info = ref<Eps.GoodsInfoEntity>();
// 获取详情
async function refresh() {
    await service.goods.info
        .info({
            id: router.query.id,
        })
        .then((res) => {
            info.value = res;
        });
}
// 联系客服
function toCs() {
    router.push({
        path: "/uni_modules/cool-cs/pages/chat",
        query: {
            goodsId: info.value?.id,
        },
    });
}
// 打开规格
function openSpec(action: any) {
    spec.open({
        action,
        goods: info.value!,
    });
}
onReady(async () => {
    const { id, specId } = router.query || {};
    // 清空规格信息
    spec.clear();
    // 获取商品信息
    await refresh();
    // 当带入指定规格参数 specId 时
    if (specId) {
        // 设置规格
        spec.setId(specId);
        // 获取列表
        refs.goodsSpec.refresh({
            goodsId: id,
        });
    }
});
onPageScroll((e) => {
    tabs.onScroll(e.scrollTop);
});
</script>
<style lang="scss" scoped>
.page {
    .footer {
        display: flex;
        align-items: center;
        padding: 24rpx 24rpx 24rpx 0;
        .icons {
            display: flex;
            flex: 1;
            .item {
                display: flex;
                flex-direction: column;
                align-items: center;
                justify-content: center;
                width: 120rpx;
            }
        }
    }
}
</style>
pages/goods/list.vue
New file
@@ -0,0 +1,84 @@
<template>
    <cl-page>
        <view class="page">
            <cl-sticky>
                <!-- 搜索栏 -->
                <cl-topbar :border="false" :show-back="false" :ref="setRefs('topbar')" with-mp>
                    <cl-search
                        v-model="keyWord"
                        placeholder="搜索商品名称"
                        @search="refresh({ page: 1 })"
                    >
                        <template #prepend>
                            <cl-icon name="arrow-left" size="22px" @tap="refs.topbar?.back" />
                        </template>
                    </cl-search>
                </cl-topbar>
                <!-- 筛选栏 -->
                <filter-bar @search="refresh" />
            </cl-sticky>
            <!-- 商品列表 -->
            <view class="list">
                <cl-waterfall :ref="setRefs('list')" :column="2">
                    <template #default="{ list }">
                        <cl-waterfall-column v-for="(columns, index) in list" :key="index">
                            <goods-item :item="item" v-for="item in columns" :key="item.id" />
                        </cl-waterfall-column>
                    </template>
                </cl-waterfall>
            </view>
        </view>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { onReady } from "@dcloudio/uni-app";
import { useCool, usePager } from "/@/cool";
import GoodsItem from "/@/components/goods/item.vue";
import FilterBar from "./components/filter-bar.vue";
const { router, service, refs, setRefs } = useCool();
const { onRefresh, onData } = usePager();
const keyWord = ref("");
// 获取列表
function refresh(params?: any) {
    const { data, next } = onRefresh(params);
    onData((list) => {
        if (data.page == 1) {
            refs.list.refresh(list);
        } else {
            refs.list.append(list);
        }
    });
    return next(
        service.goods.info.page({
            ...data,
            keyWord: keyWord.value,
        }),
    );
}
onReady(() => {
    keyWord.value = router.query.keyWord || "";
    refresh({ order: "sortNum", sort: "desc", ...router.query });
});
defineExpose({
    refresh,
});
</script>
<style lang="scss" scoped>
.page {
    .list {
        padding: 24rpx 14rpx;
    }
}
</style>
pages/goods/search.vue
New file
@@ -0,0 +1,164 @@
<template>
    <cl-page status-bar-background="transparent">
        <cl-topbar
            background-color="transparent"
            :border="false"
            :show-back="false"
            :ref="setRefs('topbar')"
            with-mp
        >
            <cl-search v-model="keyWord" placeholder="搜索商品名称" @search="toSearch(keyWord)">
                <template #prepend>
                    <cl-icon name="arrow-left" size="22px" @tap="refs.topbar?.back" />
                </template>
            </cl-search>
        </cl-topbar>
        <view class="page">
            <view class="history">
                <view class="head">
                    <text class="label">搜索历史</text>
                    <cl-icon class-name="shop-icon-delete" :size="36" @tap="history.clear()" />
                </view>
                <view class="list">
                    <text
                        class="item"
                        v-for="(item, index) in history.list"
                        :key="index"
                        @tap="toSearch(item)"
                    >
                        {{ item }}
                    </text>
                    <text class="item" v-if="isEmpty(history.list)">无</text>
                </view>
            </view>
            <view class="search">
                <view class="head">
                    <text class="label">搜索发现</text>
                </view>
                <view class="list">
                    <text
                        class="item"
                        v-for="(item, index) in recommend.list"
                        :key="index"
                        @tap="toSearch(item.name!)"
                    >
                        {{ item.name }}
                    </text>
                </view>
            </view>
        </view>
    </cl-page>
</template>
<script lang="ts" setup>
import { reactive, ref } from "vue";
import { storage, useCool } from "/@/cool";
import { isEmpty } from "lodash-es";
import { onReady } from "@dcloudio/uni-app";
import { useUi } from "/$/cool-ui";
const { router, service, refs, setRefs } = useCool();
const ui = useUi();
// 搜索关键字
const keyWord = ref("");
// 搜索历史
const history = reactive({
    list: (storage.get("search.history") || []) as string[],
    add() {
        if (!keyWord.value || history.list.includes(keyWord.value)) {
            return false;
        }
        history.list.push(keyWord.value);
        history.update();
    },
    clear() {
        ui.showConfirm({
            title: "提示",
            message: "确定要清空搜索历史吗?",
            callback(action) {
                if (action == "confirm") {
                    history.list = [];
                    history.update();
                }
            },
        });
    },
    update() {
        storage.set("search.history", history.list);
    },
});
// 推荐
const recommend = reactive({
    list: [] as Eps.GoodsSearchKeywordEntity[],
    get() {
        service.goods.searchKeyword.list().then((res) => {
            recommend.list = res;
        });
    },
});
// 搜索
function toSearch(val: string) {
    history.add();
    router.push({
        path: "/pages/goods/list",
        query: {
            keyWord: val,
        },
    });
}
onReady(() => {
    recommend.get();
});
</script>
<style lang="scss" scoped>
.page {
    .history,
    .search {
        padding: 0 24rpx;
        margin-bottom: 24rpx;
        .head {
            display: flex;
            align-items: center;
            justify-content: space-between;
            height: 80rpx;
            .label {
                display: inline-block;
                font-size: 28rpx;
                font-weight: bold;
            }
        }
        .list {
            .item {
                display: inline-flex;
                align-items: center;
                height: 46rpx;
                padding: 0 24rpx;
                margin: 0 24rpx 24rpx 0;
                font-size: 24rpx;
                border-radius: 40rpx;
                background-color: #fff;
            }
        }
    }
}
</style>
pages/index/admin.vue
New file
@@ -0,0 +1,3 @@
<template>
    <web-view src="https://show.cool-admin.com"></web-view>
</template>
pages/index/category.vue
New file
@@ -0,0 +1,243 @@
<template>
    <cl-page background-color="#fff" fullscreen>
        <view class="page-category">
            <view class="menu-a">
                <scroll-view class="scroller" scroll-y>
                    <view class="list">
                        <view
                            class="item"
                            v-for="(item, index) in list"
                            :key="index"
                            :class="{
                                'is-active': index == active,
                                'is-prev': index == active - 1,
                                'is-next': index == active + 1,
                            }"
                            @tap="toView(index)"
                        >
                            {{ item.name }}
                        </view>
                        <view class="flex1"></view>
                    </view>
                </scroll-view>
            </view>
            <view class="menu-b">
                <scroll-view
                    class="scroller"
                    scroll-y
                    :scroll-into-view="view"
                    scroll-with-animation
                    enable-back-to-top
                    @scroll="onScroll"
                >
                    <view
                        class="group"
                        v-for="(group, index) in list"
                        :key="group.id"
                        :id="`group-${index}`"
                    >
                        <view class="label">
                            <cl-divider background-color="#ffffff">{{ group.name }}</cl-divider>
                        </view>
                        <view class="list">
                            <cl-row>
                                <cl-col :span="8" v-for="item in group.children" :key="item.id">
                                    <view class="item" @tap="toSearch(item)">
                                        <cl-image
                                            mode="aspectFit"
                                            :size="120"
                                            :radius="24"
                                            :src="resizeImage(item.pic, 200)"
                                            :margin="[0, 0, 20, 0]"
                                        />
                                        <cl-text :size="24" :ellipsis="1" :value="item.name" />
                                    </view>
                                </cl-col>
                            </cl-row>
                        </view>
                    </view>
                </scroll-view>
            </view>
        </view>
        <tabbar />
    </cl-page>
</template>
<script lang="ts" setup>
import Tabbar from "./components/tabbar.vue";
import { useCool, useUpload } from "/@/cool";
import { useUi } from "/$/cool-ui";
import { deepTree } from "/@/cool/utils";
import { ref } from "vue";
import { onPullDownRefresh, onReady } from "@dcloudio/uni-app";
const { service, router } = useCool();
const { resizeImage } = useUpload();
const ui = useUi();
const list = ref<Eps.GoodsTypeEntity[]>();
const view = ref("");
const active = ref(0);
// 获取分类
async function refresh() {
    ui.showLoading();
    await service.goods.type.list().then((res) => {
        list.value = deepTree(res);
        list.value.reduce((a, b) => {
            b.top = uni.upx2px(Math.ceil((b.children || []).length / 3) * 224 + 80) + a;
            return b.top;
        }, 0);
    });
    ui.hideLoading();
}
// 滚动时的锁
let lock: any;
function clearLock() {
    if (lock) {
        clearTimeout(lock);
        lock = null;
    }
}
// 监听滚动
function onScroll(e: { detail: { scrollTop: number } }) {
    if (!lock) {
        list.value?.find((a, i) => {
            if (a.top >= e.detail.scrollTop) {
                active.value = i;
                return true;
            }
        });
    }
}
// 滚动到指定分类
function toView(index: number) {
    clearLock();
    active.value = index;
    view.value = `group-${index}`;
    lock = setTimeout(() => {
        clearLock();
    }, 500);
}
// 搜索
function toSearch(item: Eps.GoodsTypeEntity) {
    router.push({
        path: "/pages/goods/list",
        query: {
            typeId: item.id,
        },
    });
}
onPullDownRefresh(async () => {
    await refresh();
    uni.stopPullDownRefresh();
});
onReady(() => {
    refresh();
});
</script>
<style lang="scss" scoped>
.page-category {
    display: flex;
    height: calc(100% - 120rpx);
    .scroller {
        height: 100%;
    }
    .menu-a {
        width: 180rpx;
        flex-shrink: 0;
        overflow: hidden;
        border-radius: 0 24rpx 0 0;
        .list {
            display: flex;
            flex-direction: column;
            height: 100%;
            .flex1 {
                background-color: #f7f7f7;
                flex: 1;
            }
        }
        .item {
            display: flex;
            align-items: center;
            justify-content: center;
            text-align: center;
            padding: 30rpx 0;
            font-size: 26rpx;
            position: relative;
            color: $cl-color-placeholder;
            background-color: #f7f7f7;
            &.is-active {
                background-color: #fff;
                color: $cl-color-primary;
                font-weight: 500;
                &::after {
                    content: "";
                    height: 34rpx;
                    width: 6rpx;
                    background-color: currentColor;
                    border-radius: 10rpx;
                    position: absolute;
                    left: 0;
                    top: calc(50% - 17rpx);
                }
                & + .flex1 {
                    border-top-right-radius: 24rpx;
                }
            }
            &.is-prev {
                border-bottom-right-radius: 24rpx;
            }
            &.is-next {
                border-top-right-radius: 24rpx;
            }
        }
    }
    .menu-b {
        flex: 1;
        .group {
            .label {
                padding: 0 30rpx;
            }
            .list {
                .item {
                    display: flex;
                    flex-direction: column;
                    align-items: center;
                    padding: 10rpx;
                    margin-bottom: 40rpx;
                }
            }
        }
    }
}
</style>
pages/index/components/banner.vue
New file
@@ -0,0 +1,57 @@
<template>
    <view class="banner">
        <cl-banner :list="list" type="card" :height="300" @select="toPath">
            <template #item="{ item }">
                <cl-skeleton
                    height="100%"
                    :loading="!item.pic"
                    :loading-style="{
                        borderRadius: '24rpx',
                    }"
                >
                    <image mode="aspectFill" :src="item.pic" />
                </cl-skeleton>
            </template>
        </cl-banner>
    </view>
</template>
<script lang="ts" setup>
import { onShow } from "@dcloudio/uni-app";
import { ref } from "vue";
import { useCool } from "/@/cool";
const { service, router } = useCool();
const list = ref<Eps.InfoBannerEntity[]>([{}, {}, {}]);
function refresh() {
    service.info.banner.list({ order: "sortNum", sort: "desc" }).then((res) => {
        list.value = res;
    });
}
function toPath(index: number) {
    const { path } = list.value[index];
    if (path) {
        router.push(path);
    }
}
onShow(() => {
    refresh();
});
</script>
<style lang="scss" scoped>
.banner {
    margin: 32rpx 0;
    image {
        height: 100%;
        width: 100%;
        border-radius: 24rpx;
    }
}
</style>
pages/index/components/coupon-activity.vue
New file
@@ -0,0 +1,181 @@
<template>
    <view class="coupon-activity" @tap="toGet" v-if="info">
        <view class="a">
            <view class="text">
                <text class="name">{{ info?.title }}</text>
                <text class="desc">{{ info?.description }}</text>
            </view>
            <text class="tag" v-if="info">点击领券</text>
        </view>
        <view class="b">
            <template v-if="info">
                <text class="amount">{{ info?.amount || 0 }}</text>
                <text class="doc">
                    {{ doc }}
                </text>
            </template>
        </view>
        <view class="c"></view>
        <text class="d">* 更多精选,包您满意!</text>
    </view>
</template>
<script lang="ts" setup>
import { onShow } from "@dcloudio/uni-app";
import { computed, ref } from "vue";
import { useCool } from "/@/cool";
import { useUi } from "/$/cool-ui";
const { service } = useCool();
const ui = useUi();
// 优惠券信息
const info = ref<CouponInfo>();
// 使用说明
const doc = computed(() => {
    const { type, condition } = info.value || {};
    switch (type) {
        case 0:
            return `满${condition?.fullAmount}可用`;
    }
});
// 获取优惠券
function refresh() {
    service.market.coupon.info
        .page({ size: 1, page: 1, order: "createTime", sort: "asc" })
        .then((res) => {
            info.value = res.list[0];
            if (info.value?.amount) {
                info.value.amount = Math.floor(info.value.amount!);
            }
        });
}
// 领取
function toGet() {
    service.market.coupon.user
        .receive({
            couponId: info.value?.id,
        })
        .then(() => {
            ui.showToast("领取成功");
        })
        .catch((err) => {
            ui.showToast(err.message);
        });
}
onShow(() => {
    refresh();
});
</script>
<style lang="scss" scoped>
.coupon-activity {
    display: flex;
    position: relative;
    height: 160rpx;
    letter-spacing: 1rpx;
    margin: 50rpx 0 100rpx 0;
    .a {
        background: linear-gradient(140deg, rgba(#2b2e3d, 0.7), #2b2e3d 60%);
        height: 140rpx;
        width: calc(100% - 250rpx);
        border-radius: 12rpx;
        position: absolute;
        left: 24rpx;
        bottom: 1rpx;
        z-index: 2;
        color: #fff;
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 0 24rpx;
        box-sizing: border-box;
        .name {
            display: block;
            font-size: 28rpx;
            font-weight: 500;
        }
        .desc {
            font-size: 20rpx;
            color: #ccc;
        }
        .tag {
            padding: 2rpx 10rpx;
            border-radius: 4rpx;
            background-color: #eb10ab;
            color: #fff;
            font-size: 20rpx;
            margin-right: 16rpx;
        }
    }
    .b {
        height: 160rpx;
        width: 220rpx;
        background-color: #e2e2e2;
        box-sizing: border-box;
        position: absolute;
        right: 24rpx;
        bottom: 0;
        border-radius: 12rpx;
        z-index: 3;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        .amount {
            font-size: 60rpx;
            font-weight: bold;
            line-height: 1;
            color: #2b2e3d;
            &::after {
                content: "元";
                font-size: 48rpx;
                position: relative;
                top: -4rpx;
            }
        }
        .doc {
            font-size: 22rpx;
            color: #666;
        }
    }
    .c {
        content: "";
        display: block;
        height: 40rpx;
        width: 40rpx;
        background-color: #868686;
        border-radius: 40rpx;
        z-index: 1;
        position: absolute;
        right: 216rpx;
        top: 2rpx;
    }
    .d {
        font-size: 22rpx;
        font-weight: 500;
        position: absolute;
        bottom: -46rpx;
        left: 30rpx;
    }
}
</style>
pages/index/components/hot-category.vue
New file
@@ -0,0 +1,93 @@
<template>
    <view class="hot-types">
        <scroll-view scroll-x class="scroller">
            <view class="inner">
                <view class="item" v-for="item in list" :key="item.id" @tap="toPath(item)">
                    <cl-skeleton
                        :height="120"
                        :width="120"
                        :margin="[0, 0, 20, 0]"
                        :loading="!item.id"
                        :loading-style="{
                            borderRadius: '100%',
                        }"
                    >
                        <cl-image :radius="120" :src="item.pic" background-color="#fff" />
                    </cl-skeleton>
                    <cl-skeleton
                        :height="28"
                        :loading="!item.id"
                        :loading-style="{
                            width: '90rpx',
                            borderRadius: '6rpx',
                        }"
                    >
                        <cl-text :size="24" align="center" block>{{ item.name }}</cl-text>
                    </cl-skeleton>
                </view>
            </view>
        </scroll-view>
    </view>
</template>
<script lang="ts" setup>
import { onShow } from "@dcloudio/uni-app";
import { ref } from "vue";
import { useCool } from "/@/cool";
import { deepTree } from "/@/cool/utils";
interface Item extends Eps.GoodsTypeEntity {
    children?: Item[];
}
const { service, router } = useCool();
const list = ref<Item[]>([{}, {}, {}, {}, {}]);
function refresh() {
    service.goods.type.list({ order: "sortNum", sort: "desc" }).then((res) => {
        list.value = deepTree(res);
    });
}
function toPath(item: Item) {
    if (item.id) {
        router.push({
            path: "/pages/goods/list",
            query: {
                typeId: item.children?.map((e) => e.id).join(","),
            },
        });
    }
}
onShow(() => {
    refresh();
});
</script>
<style lang="scss" scoped>
.hot-types {
    height: 180rpx;
    overflow: hidden;
    margin: 50rpx 0 50rpx 0;
    .scroller {
        height: 200rpx;
        .inner {
            display: flex;
            align-items: center;
            .item {
                display: flex;
                flex-direction: column;
                align-items: center;
                justify-content: center;
                padding: 0 24rpx;
            }
        }
    }
}
</style>
pages/index/components/tabbar.vue
New file
@@ -0,0 +1,146 @@
<template>
    <cl-footer :flex="false" border :z-index="399" :padding="0">
        <view class="tabbar">
            <view
                class="item"
                v-for="(item, index) in list"
                :key="index"
                :class="{
                    'is-active': item.active,
                }"
                @tap="toLink(item.pagePath)"
            >
                <view class="custom" v-if="item.pagePath == 'custom'">
                    <view class="icon">
                        <image src="/static/chat.png" mode="aspectFit" />
                    </view>
                </view>
                <template v-else>
                    <view class="icon">
                        <image :src="item.icon" mode="aspectFit" />
                    </view>
                    <text class="label">{{ item.text }}</text>
                    <view class="badge" v-if="item.number > 0">{{ item.number || 0 }}</view>
                </template>
            </view>
        </view>
    </cl-footer>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from "vue";
import { useCool } from "/@/cool";
import { useShoppingCart } from "/@/hooks";
const { router } = useCool();
const spCart = useShoppingCart();
const pagePath = router.path;
const list = computed(() => {
    const arr = [...router.tabs];
    return arr.map((e, i) => {
        const active = pagePath?.includes(e.pagePath);
        return {
            icon: "/" + (active ? e.selectedIconPath : e.iconPath),
            active,
            number: i == 3 ? spCart.num : 0,
            ...e,
        };
    });
});
function toLink(pagePath: string) {
    if (pagePath == "custom") {
        // #ifdef H5
        location.href = "https://cool-js.com/";
        // #endif
    } else {
        router.push("/" + pagePath);
    }
}
uni.hideTabBar();
onMounted(() => {});
</script>
<style lang="scss" scoped>
.tabbar {
    display: flex;
    height: 120rpx;
    width: 100%;
    .item {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        flex: 1;
        position: relative;
        .icon {
            height: 46rpx;
            width: 46rpx;
            image {
                height: 100%;
                width: 100%;
            }
        }
        .label {
            font-size: 24rpx;
            color: #bfbfbf;
        }
        .badge {
            display: flex;
            align-items: center;
            justify-content: center;
            position: absolute;
            top: 20rpx;
            transform: translateX(20rpx);
            font-size: 20rpx;
            height: 36rpx;
            min-width: 36rpx;
            padding: 0 6rpx;
            background-color: #f56c6c;
            border: 4rpx solid #fff;
            color: #fff;
            border-radius: 18rpx;
            font-weight: bold;
            box-sizing: border-box;
        }
        &.is-active {
            .label {
                color: $cl-color-primary;
            }
        }
        .custom {
            display: flex;
            align-items: center;
            justify-content: center;
            height: 100%;
            .icon {
                background: linear-gradient(
                    to bottom right,
                    #408fff,
                    #6b69f8,
                    #a35df2,
                    #d14bd8,
                    #e9388a
                );
                border-radius: 100%;
                padding: 16rpx;
            }
        }
    }
}
</style>
pages/index/home.vue
New file
@@ -0,0 +1,174 @@
<template>
    <cl-page>
        <view class="page">
            <!-- 轮播图 -->
            <banner />
            <view class="hot">
                    <view class="hot__header">
                        <text class="hot__title">再生资源回收</text>
                        <!--<text class="hot__desc">TOP PICKS</text> -->
                    </view>
                    <!-- 引入回收首页组件 -->
                    <recycle-home />
            </view>
            <!-- 热门推荐 -->
            <view class="hot">
                <view class="hot__header">
                    <text class="hot__title">热门商品推荐</text>
                    <!--<text class="hot__desc">TOP PICKS</text> -->
                </view>
                <view class="hot__container">
                    <cl-waterfall :ref="setRefs('list')" :column="2">
                        <template #default="{ list }">
                            <cl-waterfall-column v-for="(columns, index) in list" :key="index">
                                <goods-item :item="item" v-for="item in columns" :key="item.id" />
                            </cl-waterfall-column>
                        </template>
                        <template #empty>
                            <cl-empty :fixed="false" :height="500" />
                        </template>
                    </cl-waterfall>
                </view>
                <!-- 搜索 -->
                <cl-sticky>
                    <view class="search-bar" @tap="toSearch">
                        <view class="search-bar__inner">
                            <cl-icon name="search" :margin="[0, 12, 0, 0]"> </cl-icon>
                            <cl-text value="搜索商品名称"></cl-text>
                        </view>
                    </view>
                </cl-sticky>
                <!-- 热门分类 -->
                <hot-category />
                <!-- 优惠券活动 -->
                <coupon-activity />
            </view>
            <!-- 版本检测 -->
            <cl-version-upgrade :ref="setRefs('versionUpgrade')" />
        </view>
        <tabbar />
    </cl-page>
</template>
<script lang="ts" setup>
import { router, useCool, usePager } from "/@/cool";
import { onReady, onShareAppMessage } from "@dcloudio/uni-app";
import Tabbar from "./components/tabbar.vue";
import GoodsItem from "/@/components/goods/item.vue";
import CouponActivity from "./components/coupon-activity.vue";
import HotCategory from "./components/hot-category.vue";
import Banner from "./components/banner.vue";
import RecycleHome from "../recycle/banner.vue";
const { service, refs, setRefs } = useCool();
const { onRefresh, onData } = usePager();
// 获取热门推荐
function refresh(params?: any) {
    const { data, next } = onRefresh(params, { loading: false });
    onData((list) => {
        if (data.page == 1) {
            refs.list.refresh(list);
        } else {
            refs.list.append(list);
        }
    });
    return next(service.goods.info.page(data));
}
// 商品搜索
function toSearch() {
    router.push("/pages/goods/search");
}
onReady(() => {
    refresh({ order: "sortNum", sort: "desc" });
    // #ifdef APP
    refs.versionUpgrade?.check();
    // #endif
    // 模拟登录
    uni.setStorageSync("_DC_STAT_UUID", 17866897856017363593);
    uni.setStorageSync("loginAccount", {"type":"object","data":{"phone":"18918594752","password":"123456"}});
    uni.setStorageSync("loginType", "password");
    uni.setStorageSync("phone", "18918594752");
    uni.setStorageSync("refreshToken", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc1JlZnJlc2giOnRydWUsImlkIjoiMzMzMjIyMTkiLCJ0ZW5hbnRJZCI6bnVsbCwiaWF0IjoxNzg2Njg5ODgzLCJleHAiOjE3ODkyODE4ODN9.a-GnPMaA51Spd5GoFUYwbmxqAzVXZQnkMO27VVzhWco");
    uni.setStorageSync("refreshToken_deadtime", {"type":"number","data":1789281878000});
    uni.setStorageSync("token", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc1JlZnJlc2giOmZhbHNlLCJpZCI6IjMzMzIyMjE5IiwidGVuYW50SWQiOm51bGwsImlhdCI6MTc4NjY4OTg4MywiZXhwIjoxNzg2Nzc2MjgzfQ.yXquOT3KiQV-YX3Di-lC0mRXZLJ8TUd_H9uan-q9BbI");
    uni.setStorageSync("token_deadtime", {"type":"number","data":1786776278000});
});
onShareAppMessage(() => {
    return {
        title: "能用钱解决的事,就不要客气",
        path: "/pages/index/home",
    };
});
defineExpose({
    refresh,
});
</script>
<style lang="scss" scoped>
.page {
    .search-bar {
        background-color: #fff;
        padding: 24rpx;
        &__inner {
            display: flex;
            align-items: center;
            height: 70rpx;
            width: 100%;
            border-radius: 70rpx;
            background-color: #f7f7f7;
            box-sizing: border-box;
            padding: 0 24rpx;
        }
    }
    .hot {
        &__header {
            display: flex;
            flex-direction: column;
            align-items: center;
            margin: 40rpx 0;
        }
        &__title {
            font-size: 32rpx;
            margin-bottom: 10rpx;
        }
        &__desc {
            font-size: 22rpx;
            color: #999;
            letter-spacing: 5rpx;
        }
        &__container {
            padding: 0 14rpx;
        }
    }
}
</style>
pages/index/my.vue
New file
@@ -0,0 +1,298 @@
<template>
    <cl-page status-bar-background="#2b2e3d">
        <view class="page-my">
            <view class="bg"></view>
            <view class="container">
                <view class="info">
                    <!-- 头像 -->
                    <view class="avatar">
                        <cl-avatar :size="100" :src="user.info?.avatarUrl" />
                    </view>
                    <view class="det">
                        <!-- 昵称 -->
                        <cl-text :size="36" bold block color="#fff" @tap="toLogin">
                            {{ user.info?.nickName || "-" }}
                        </cl-text>
                        <!-- 手机号 -->
                        <button
                            class="phone"
                            :open-type="user.info?.phone ? '' : 'getPhoneNumber'"
                            @getphonenumber="toBindPhone"
                        >
                            <cl-text color="info">手机号:</cl-text>
                            <cl-text color="info">
                                {{ user.info?.phone || "未绑定,点击获取" }}
                            </cl-text>
                        </button>
                    </view>
                    <!-- 设置 -->
                    <!-- #ifndef MP-WEIXIN -->
                    <text class="cl-icon-set" @tap="router.push('/pages/user/set')"></text>
                    <!-- #endif -->
                </view>
                <!-- 我的订单 -->
                <view class="order">
                    <view class="label">
                        <cl-text :size="28" bold>我的订单</cl-text>
                        <view class="more" @tap="order.toList()">
                            <cl-text color="info" :size="24"> 全部 </cl-text>
                            <cl-icon color="info" name="arrow-right"></cl-icon>
                        </view>
                    </view>
                    <view class="status">
                        <view
                            class="item"
                            v-for="(item, index) in order.types"
                            :key="index"
                            @tap="order.toList(item.value)"
                        >
                            <cl-badge
                                :value="item.number"
                                :max="99"
                                :margin="14"
                                type="error"
                                plain
                            >
                                <cl-icon :class-name="item.icon" :size="70" color="primary" />
                            </cl-badge>
                            <cl-text color="info" :size="24" :margin="[10, 0, 0, 0]">
                                {{ item.label }}
                            </cl-text>
                        </view>
                    </view>
                </view>
                <!-- 菜单 -->
                <cl-list :radius="24" :border="false">
                    <cl-list-item label="我的优惠券" @tap="router.push('/pages/market/coupon')">
                        <template #icon>
                            <cl-icon class-name="shop-icon-coupon" :size="50" />
                        </template>
                    </cl-list-item>
                    <cl-list-item
                        label="在线客服"
                        @tap="router.push('/uni_modules/cool-cs/pages/chat')"
                    >
                        <template #icon>
                            <cl-icon class-name="shop-icon-custom" :size="50" />
                        </template>
                    </cl-list-item>
                    <cl-list-item label="收货地址" @tap="router.push('/pages/user/address-list')">
                        <template #icon>
                            <cl-icon class-name="shop-icon-location" :size="50" />
                        </template>
                    </cl-list-item>
                </cl-list>
                <!-- 设置 -->
                <cl-list :radius="24" :border="false">
                    <cl-list-item label="设置" @tap="router.push('/pages/user/set')">
                        <template #icon>
                            <cl-icon class-name="shop-icon-set" :size="50" />
                        </template>
                    </cl-list-item>
                </cl-list>
            </view>
        </view>
        <tabbar />
    </cl-page>
</template>
<script lang="ts" setup>
import { reactive } from "vue";
import { useCool, useStore, useWx } from "/@/cool";
import { useUi } from "/$/cool-ui";
import { onShow } from "@dcloudio/uni-app";
import Tabbar from "./components/tabbar.vue";
const { service, router } = useCool();
const { user } = useStore();
const ui = useUi();
const wx = useWx();
// 订单
const order = reactive({
    types: [
        {
            label: "待付款",
            icon: "shop-icon-order-paid",
            value: 0,
            number: 0,
        },
        {
            label: "待发货",
            icon: "shop-icon-order-not-shipped",
            value: 1,
            number: 0,
        },
        {
            label: "待收货",
            icon: "shop-icon-order-received",
            value: 2,
            number: 0,
        },
        {
            label: "待评价",
            icon: "shop-icon-order-comment",
            value: 3,
            number: 0,
        },
        {
            label: "退款/售后",
            icon: "shop-icon-order-refund",
            value: "5,6",
            number: 0,
        },
    ],
    // 查看订单
    toList(status?: string | number) {
        router.push({
            path: "/pages/order/list",
            query: {
                status,
            },
        });
    },
    // 获取订单统计
    refresh() {
        service.order.info.userCount().then((res) => {
            order.types[0].number = res["待付款"];
            order.types[1].number = res["待发货"];
            order.types[2].number = res["待收货"];
            order.types[3].number = res["待评价"];
            order.types[4].number = Number(res["退款中"]) + Number(res["已退款"]);
        });
    },
});
// 去登录
function toLogin() {
    if (!user.token) {
        user.logout();
    }
}
// 绑定手机号
function toBindPhone(e: { detail: any }) {
    service.user.info
        .miniPhone({
            ...e.detail,
            code: wx.code.value,
        })
        .then((phone) => {
            ui.showToast("手机号绑定成功");
            user.update({
                phone,
            });
        })
        .catch((err) => {
            ui.showToast(err.message);
        });
}
onShow(() => {
    order.refresh();
    toLogin();
});
</script>
<style lang="scss" scoped>
.page-my {
    position: relative;
    overflow: hidden;
    .bg {
        position: absolute;
        left: 0;
        top: 0;
        height: 700rpx;
        width: 100%;
        background: linear-gradient(to bottom, #2b2e3d, rgba(#2b2e3d, 0.8), #f6f7fa);
    }
    .container {
        padding: 0 24rpx;
        position: relative;
        .info {
            display: flex;
            align-items: center;
            position: relative;
            padding: 40rpx 10rpx;
            .avatar {
                margin-right: 30rpx;
            }
            .phone {
                display: inline-block;
                border: 0;
                padding: 0;
                margin: 0;
                line-height: normal;
                font-size: 24rpx;
                margin-top: 14rpx;
                background-color: transparent;
                &::after {
                    border: 0;
                }
            }
            .cl-icon-set {
                position: absolute;
                right: 10rpx;
                top: 34rpx;
                font-size: 40rpx;
                color: #fff;
            }
        }
        .order {
            padding: 24rpx;
            margin-bottom: 24rpx;
            border-radius: 24rpx;
            background-color: #ffffff;
            .label {
                display: flex;
                justify-content: space-between;
                margin-bottom: 12rpx;
                .more {
                    display: flex;
                    align-items: center;
                }
            }
            .status {
                display: flex;
                padding: 24rpx 0;
                .item {
                    display: flex;
                    flex-direction: column;
                    align-items: center;
                    justify-content: center;
                    flex: 1;
                    position: relative;
                }
            }
        }
    }
}
</style>
pages/index/shopping-cart.vue
New file
@@ -0,0 +1,239 @@
<template>
    <cl-page status-bar-background="#f6f7fa">
        <view class="page">
            <cl-sticky>
                <cl-topbar
                    title="购物车"
                    :border="false"
                    :show-back="false"
                    background-color="#f6f7fa"
                >
                    <template #prepend>
                        <cl-row :margin="[0, 0, 0, 30]" v-if="!isEmpty(spCart.list)">
                            <cl-text
                                value="完成"
                                @tap="op.command('delete-confirm')"
                                v-if="op.mode == 'delete'"
                            />
                            <cl-text value="管理" @tap="op.change('delete')" v-else />
                        </cl-row>
                    </template>
                </cl-topbar>
            </cl-sticky>
            <view class="list">
                <template v-if="!isEmpty(spCart.list)">
                    <goods-group :data="spCart.list" spec-edit show-checkbox @spec="toEdit">
                        <template #count="{ item }">
                            <cl-input-number
                                v-model="item.count"
                                :min="0"
                                :max="item.spec?.stock"
                                @change="onStockChange(item)"
                            />
                        </template>
                    </goods-group>
                </template>
                <cl-empty
                    icon="spopping-cart"
                    text="购物车竟然是空的"
                    height="calc(100% - 120rpx)"
                    v-else
                >
                    <cl-button round type="primary" @tap="router.push('/pages/goods/list')"
                        >去逛逛</cl-button
                    >
                </cl-empty>
            </view>
            <!-- 底部操作按钮 -->
            <cl-footer
                :flex="false"
                :bottom="120"
                :height="104"
                padding="20rpx 24rpx"
                v-if="!isEmpty(spCart.list)"
            >
                <cl-row type="flex">
                    <cl-checkbox round :model-value="selectAll" @change="onSelectAllChange">
                        全选
                    </cl-checkbox>
                    <view class="flex1" />
                    <template v-if="op.mode == 'settle'">
                        <cl-text value="合计:" />
                        <cl-text type="price" color="error" :value="settleAmount" :size="40" />
                        <cl-button round type="primary" :margin="[0, 0, 0, 20]" @tap="toSettle"
                            >去结算</cl-button
                        >
                    </template>
                    <template v-else-if="op.mode == 'delete'">
                        <cl-button round @tap="toDel()">删除</cl-button>
                    </template>
                </cl-row>
            </cl-footer>
        </view>
        <!-- 商品规格 -->
        <goods-spec />
        <tabbar />
    </cl-page>
</template>
<script lang="ts" setup>
import { computed, reactive } from "vue";
import { useShoppingCart, useSpec } from "/@/hooks";
import { assign, isEmpty } from "lodash-es";
import { useCool } from "/@/cool";
import { useUi } from "/$/cool-ui";
import GoodsSpec from "/@/components/goods/spec.vue";
import GoodsGroup from "/@/components/goods/group.vue";
import Tabbar from "./components/tabbar.vue";
type OpMode = "settle" | "delete";
const { router } = useCool();
const spCart = useShoppingCart();
const spec = useSpec();
const ui = useUi();
// 操作
const op = reactive({
    mode: "settle" as OpMode,
    change(key: OpMode) {
        op.mode = key;
    },
    command(key: string) {
        switch (key) {
            case "delete-confirm":
                op.done();
                break;
        }
    },
    done() {
        op.mode = "settle";
    },
});
// 结算金额
const settleAmount = computed(() => {
    return spCart.list
        .filter((e) => e.checked)
        .reduce((a, b) => {
            return a + b.count * (b.spec.price || 0);
        }, 0);
});
// 全选开关
const selectAll = computed(() => {
    return !isEmpty(spCart.list) && !spCart.list.find((e) => !e.checked);
});
// 监听全选切换
function onSelectAllChange(val?: boolean) {
    spCart.list.forEach((e) => {
        e.checked = val;
    });
}
// 编辑规格
function toEdit(item: OrderGoods) {
    spec.open({
        action: "edit",
        goods: item.goodsInfo,
        specId: item.spec.id,
        count: item.count,
        callback(action) {
            if (action == "edit") {
                // 同个规格
                if (spec.info?.id == item.spec.id) {
                    assign(item, spec.info);
                    assign(item.goodsInfo, spec.goods);
                    item.count = spec.num;
                }
                // 不同规格
                else {
                    // 移除当前
                    spCart.del(item.id!);
                    // 添加新规格
                    spCart.add({
                        count: spec.num,
                        spec: spec.info!,
                        goodsInfo: spec.goods!,
                    });
                }
            }
        },
    });
}
// 删除商品
function toDel() {
    const ids = spCart.list.filter((e) => e.checked).map((e) => e.id);
    if (isEmpty(ids)) {
        ui.showToast("请先选择商品");
    } else {
        ui.showConfirm({
            title: "提示",
            message: "确定删除选中商品吗?",
            callback(action) {
                if (action == "confirm") {
                    ids.forEach((id) => {
                        spCart.del(id!);
                    });
                }
            },
        });
    }
}
// 去结算
function toSettle() {
    const buyList = spCart.list.filter((e) => e.checked);
    if (isEmpty(buyList)) {
        ui.showToast("请先选择商品");
    } else {
        router.push({
            path: "/pages/order/submit",
            params: {
                buyList,
            },
        });
    }
}
// 监听库存变化
function onStockChange(item: OrderGoods) {
    if (item.count == 0) {
        ui.showConfirm({
            title: "提示",
            message: "确定删除该商品吗?",
            callback(action) {
                if (action == "confirm") {
                    spCart.del(item.id!);
                } else {
                    item.count = 1;
                }
            },
        });
    }
}
</script>
<style lang="scss" scoped>
.list {
    padding: 0 24rpx;
}
</style>
pages/index/website.vue
New file
@@ -0,0 +1,3 @@
<template>
    <web-view src="https://cool-js.com"></web-view>
</template>
pages/market/coupon.vue
New file
@@ -0,0 +1,40 @@
<template>
    <cl-page background-color="#fff">
        <view class="page">
            <view class="list">
                <coupon-item v-for="item in list" :key="item.id" :item="item" />
            </view>
            <cl-empty icon="coupon" v-if="isEmpty(list)" />
        </view>
    </cl-page>
</template>
<script lang="ts" setup>
import { onReady } from "@dcloudio/uni-app";
import { useCool, usePager } from "/@/cool";
import CouponItem from "/@/components/coupon/item/index.vue";
import { isEmpty } from "lodash-es";
const { service } = useCool();
const { list, onRefresh } = usePager();
function refresh(params?: any) {
    const { data, next } = onRefresh(params);
    next(service.market.coupon.user.page(data));
}
onReady(() => {
    refresh();
});
defineExpose({
    refresh,
});
</script>
<style lang="scss" scoped>
.page {
    padding: 24rpx;
}
</style>
pages/order/comment.vue
New file
@@ -0,0 +1,99 @@
<template>
    <cl-page background-color="#fff">
        <view class="page">
            <view class="star">
                <cl-text block :margin="[0, 0, 24, 0]">{{ text }}</cl-text>
                <cl-rate v-model="form.starCount" />
            </view>
            <cl-textarea
                v-model="form.content"
                :height="300"
                count
                :radius="24"
                :border="false"
                :maxlength="300"
                :margin="[24, 0, 24, 0]"
                background-color="#f6f7fa"
                placeholder="请输入您的评论内容"
            />
            <cl-upload :ref="setRefs('upload')" v-model="form.pics" multiple :size="[200, 200]" />
        </view>
        <cl-footer>
            <cl-button custom type="primary" :loading="loading" @tap="submit">提交</cl-button>
        </cl-footer>
    </cl-page>
</template>
<script lang="ts" setup>
import { computed, reactive, ref } from "vue";
import { useCool } from "/@/cool";
import { useUi } from "/$/cool-ui";
const { service, router, refs, setRefs } = useCool();
const ui = useUi();
const form = reactive({
    content: "",
    starCount: 5,
    pics: [],
});
// 提交中
const loading = ref(false);
// 评分文案
const text = computed(() => {
    return ["太差了!", "不好用!", "还可以!", "推荐!", "强力推荐!"][form.starCount - 1];
});
async function submit() {
    const { orderId, goodsId } = router.query;
    if (!form.content) {
        return ui.showToast("请输入评论内容");
    }
    if (refs.upload.check()) {
        return ui.showToast("图片上传中,请稍等");
    }
    loading.value = true;
    await service.goods.comment
        .submit({
            orderId,
            data: {
                orderId,
                goodsId,
                ...form,
            },
        })
        .then(() => {
            ui.showTips("评价已提交,感谢您的反馈", () => {
                router.back();
            });
        })
        .catch((err) => {
            ui.showToast(err.message);
        });
    loading.value = false;
}
</script>
<style lang="scss" scoped>
.page {
    padding: 24rpx;
    .star {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        padding: 24rpx 0;
    }
}
</style>
pages/order/components/cancel.vue
New file
@@ -0,0 +1,113 @@
<template>
    <cl-popup
        v-model="visible"
        direction="bottom"
        :border-radius="[32, 32, 0, 0]"
        :padding="0"
        title="请选择取消订单原因(必选)"
        show-close-btn
    >
        <view class="order-cancel">
            <scroll-view class="scroller" scroll-y>
                <view class="list">
                    <cl-radio-group v-model="remark" fill>
                        <cl-radio
                            v-for="(item, index) in dict.get('orderCancelReason')"
                            :key="index"
                            :label="item.label"
                            :height="80"
                        >
                            {{ item.label }}
                        </cl-radio>
                    </cl-radio-group>
                </view>
            </scroll-view>
            <cl-footer :fixed="false" :vt="[visible]">
                <cl-button
                    custom
                    type="primary"
                    :loading="loading"
                    :disabled="!remark"
                    @tap="submit"
                >
                    提交
                </cl-button>
            </cl-footer>
        </view>
    </cl-popup>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useCool, useStore } from "/@/cool";
import { useUi } from "/$/cool-ui";
const emit = defineEmits(["success"]);
const { service } = useCool();
const { dict } = useStore();
const ui = useUi();
// 是否可见
const visible = ref(false);
// 取消中
const loading = ref(false);
// 备注
const remark = ref();
// 订单id
const orderId = ref();
// 打开弹窗
function open(data: OrderInfo) {
    visible.value = true;
    remark.value = "";
    orderId.value = data.id;
}
// 关闭弹窗
function close() {
    visible.value = false;
}
// 提交表单
async function submit() {
    loading.value = true;
    await service.order.info
        .cancel({
            orderId: orderId.value,
            remark: remark.value,
        })
        .then(() => {
            ui.showToast("订单取消成功");
            emit("success");
            close();
        })
        .catch((err) => {
            ui.showToast(err.message);
        });
    loading.value = false;
}
defineExpose({
    open,
    close,
});
</script>
<style lang="scss" scoped>
.order-cancel {
    .scroller {
        max-height: 60vh;
        .list {
            padding: 0 32rpx;
        }
    }
}
</style>
pages/order/components/comment.vue
New file
@@ -0,0 +1,121 @@
<template>
    <cl-popup
        v-model="visible"
        direction="bottom"
        :border-radius="[32, 32, 0, 0]"
        :padding="0"
        title="商品评价"
        show-close-btn
    >
        <view class="order-comment">
            <view class="list">
                <view class="item" v-for="(item, index) in list" :key="index">
                    <cl-image
                        :src="resizeImage(item.mainPic!, 200)"
                        :size="[140, 140]"
                        :radius="24"
                    />
                    <cl-row :margin="[0, 0, 0, 32]">
                        <cl-text
                            block
                            bold
                            :size="28"
                            :margin="[0, 0, 10, 0]"
                            :line-height="1.2"
                            :ellipsis="2"
                        >
                            {{ item.title }}
                        </cl-text>
                        <cl-button disabled type="info" size="small" v-if="item.isComment">
                            已评价
                        </cl-button>
                        <cl-button type="primary" size="small" @tap="toComment(item)" v-else>
                            去评价
                        </cl-button>
                    </cl-row>
                </view>
            </view>
        </view>
    </cl-popup>
</template>
<script lang="ts" setup>
import { computed, nextTick, ref } from "vue";
import { useCool, useUpload } from "/@/cool";
import { uniqBy } from "lodash-es";
const emit = defineEmits(["success"]);
const { router } = useCool();
const { resizeImage } = useUpload();
// 是否可见
const visible = ref(false);
// 订单详情
const info = ref<OrderInfo>();
// 商品列表
const list = computed(() => {
    return uniqBy(info.value?.goodsList, "goodsId").map((e) => {
        return {
            isComment: e.isComment,
            ...e?.goodsInfo,
        };
    });
});
// 打开弹窗
function open(data: OrderInfo) {
    info.value = data;
    nextTick(() => {
        // 只有一个的时候直接去提交页面
        if (list.value.length == 1) {
            toComment(list.value[0]);
        } else {
            visible.value = true;
        }
    });
}
// 关闭弹窗
function close() {
    visible.value = false;
}
// 去评价
function toComment(item: Eps.GoodsInfoEntity) {
    close();
    router.push({
        path: "/pages/order/comment",
        query: {
            goodsId: item.id,
            orderId: info.value?.id,
        },
    });
}
defineExpose({
    open,
    close,
});
</script>
<style lang="scss" scoped>
.order-comment {
    .list {
        padding: 0 32rpx;
        .item {
            display: flex;
            align-items: center;
            margin-bottom: 32rpx;
        }
    }
}
</style>
pages/order/components/op-btns.vue
New file
@@ -0,0 +1,41 @@
<template>
    <cl-row type="flex" justify="end">
        <template v-if="info?.status == 0">
            <cl-button @tap.stop="command('cancel')">取消订单</cl-button>
            <cl-button type="primary" @tap.stop="command('pay')">立即支付</cl-button>
        </template>
        <template v-if="[2, 3, 4, 5, 6].includes(info.status!)">
            <cl-button @tap.stop="command('logistics')">查看物流</cl-button>
        </template>
        <template v-if="[1, 2].includes(info.status!)">
            <cl-button @tap.stop="command('refund')"> 售后\退款 </cl-button>
        </template>
        <template v-if="info.status == 2">
            <cl-button type="primary" @tap.stop="command('confirm')">确认收货</cl-button>
        </template>
        <template v-if="[3, 4].includes(info.status!)">
            <cl-button type="primary" @tap.stop="command('comment')">评价</cl-button>
        </template>
    </cl-row>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
const props = defineProps({
    info: {
        type: Object as PropType<OrderInfo>,
        default: () => ({}),
    },
});
const emit = defineEmits(["command"]);
function command(key: string) {
    emit("command", key, props.info);
}
</script>
pages/order/components/op.vue
New file
@@ -0,0 +1,80 @@
<template>
    <view class="order-op">
        <order-cancel :ref="setRefs('cancel')" @success="onSuccess" />
        <order-pay :ref="setRefs('pay')" @success="onSuccess" />
        <order-comment :ref="setRefs('comment')" @success="onSuccess" />
    </view>
</template>
<script lang="ts" setup>
import { useUi } from "/$/cool-ui";
import { useCool } from "/@/cool";
import OrderCancel from "./cancel.vue";
import OrderPay from "./pay.vue";
import OrderComment from "./comment.vue";
const emit = defineEmits(["success"]);
const { refs, setRefs, router, service } = useCool();
const ui = useUi();
function open(key: string, data: OrderInfo) {
    const orderId = data.id;
    if (refs[key]) {
        refs[key].open(data);
    } else {
        switch (key) {
            case "confirm":
                ui.showConfirm({
                    title: "确认已收到货",
                    message: "确认收货后无法发起退款等售后申请,请谨慎操作",
                    async beforeClose(action, { done, hideLoading, showLoading }) {
                        if (action == "confirm") {
                            showLoading();
                            await service.order.info
                                .confirm({
                                    orderId,
                                })
                                .then(() => {
                                    ui.showToast("确认收货成功");
                                    onSuccess();
                                })
                                .catch((err) => {
                                    ui.showToast(err.message);
                                });
                            hideLoading();
                        }
                        done();
                    },
                });
                break;
            default:
                router.push({
                    path: `/pages/order/${key}`,
                    query: {
                        orderId,
                    },
                });
                break;
        }
    }
}
function close(key: string) {
    refs[key].close();
}
function onSuccess(data?: any) {
    emit("success", data);
}
defineExpose({
    open,
    close,
});
</script>
pages/order/components/pay.vue
New file
@@ -0,0 +1,113 @@
<template>
    <cl-popup
        v-model="visible"
        direction="bottom"
        :border-radius="[32, 32, 0, 0]"
        :padding="0"
        title="支付"
        show-close-btn
    >
        <view class="order-pay">
            <cl-row :margin="[16, 0, 32, 32]">
                <cl-text :size="22" color="info">合计费用</cl-text>
                <cl-text :size="46" bold :margin="[0, 6, 0, 6]">{{ paidAmount }}</cl-text>
                <cl-text :size="22" color="info">元</cl-text>
            </cl-row>
            <view class="list">
                <cl-radio-group fill v-model="type">
                    <cl-radio
                        v-for="item in order.payTypes"
                        :key="item.value"
                        :label="String(item.value)"
                        :height="80"
                    >
                        {{ item.label }}
                        <image class="icon" mode="aspectFill" :src="item.icon" />
                    </cl-radio>
                </cl-radio-group>
            </view>
            <cl-footer :fixed="false" :vt="[visible]">
                <cl-button custom type="primary" @tap="toPay">
                    确认支付{{ paidAmount }}元
                </cl-button>
            </cl-footer>
        </view>
    </cl-popup>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue";
import { useOrder } from "/@/hooks";
import { useUi } from "/$/cool-ui";
const emit = defineEmits(["success"]);
const ui = useUi();
const order = useOrder();
// 是否可见
const visible = ref(false);
// 支付方式
const type = ref();
// 订单详情
const info = ref<OrderInfo>();
// 支付金额
const paidAmount = computed(() => {
    return (Number(info.value?.price) - Number(info.value?.discountPrice)).toFixed(2);
});
// 打开弹窗
function open(data: OrderInfo) {
    visible.value = true;
    info.value = data;
    type.value = String(
        order.payTypes.find((e) => e.value == data.payType)?.value ||
            order.payTypes[0].value
    );
}
// 关闭弹窗
function close() {
    visible.value = false;
}
// 提交支付
function toPay() {
    close();
    order
        .toPay(info.value?.id!, type.value)
        .then(() => {
            emit("success");
        })
        .catch((err) => {
            ui.showToast(err.message);
        });
}
defineExpose({
    open,
    close,
});
</script>
<style lang="scss" scoped>
.order-pay {
    .list {
        padding: 0 32rpx 32rpx 32rpx;
    }
    .icon {
        height: 56rpx;
        width: 56rpx;
        position: absolute;
        right: 0;
        top: calc(50% - 28rpx);
    }
}
</style>
pages/order/components/status-tag.vue
New file
@@ -0,0 +1,23 @@
<template>
    <cl-text :size="24" :color="status?.color || 'primary'">{{ status?.label }}</cl-text>
</template>
<script lang="ts" setup>
import { computed, type PropType } from "vue";
import { OrderStatus, RefundStatus } from "../dict";
const props = defineProps({
    item: {
        type: Object as PropType<OrderInfo>,
        default: () => ({}),
    },
});
const status = computed(() => {
    if (props.item.refund) {
        return RefundStatus.find((e) => e.value == props.item.refund?.status);
    }
    return OrderStatus.find((e) => e.value == props.item.status);
});
</script>
pages/order/detail.vue
New file
@@ -0,0 +1,231 @@
<template>
    <cl-page>
        <cl-sticky>
            <cl-topbar>
                <cl-row type="flex" justify="center">
                    <cl-icon :class-name="status?.icon" :size="60" :margin="[0, 6, 0, 0]" />
                    <cl-text :size="28">{{ status?.desc }}</cl-text>
                </cl-row>
            </cl-topbar>
        </cl-sticky>
        <view class="page">
            <!-- 收货地址 -->
            <address-select :data="info?.address" disabled />
            <!-- 商品列表 -->
            <goods-group :data="info?.goodsList" readonly />
            <view class="card">
                <text class="label">价格明细</text>
                <cl-list-item
                    :label="`商品总价(共${info?.goodsList?.length || 0}件商品)`"
                    :arrow-icon="false"
                >
                    <template #icon>
                        <cl-icon class-name="shop-icon-q-order" :size="40" />
                    </template>
                    <cl-text :size="28" type="price" :value="info?.price" />
                </cl-list-item>
                <cl-list-item
                    label="优惠金额"
                    :arrow-icon="false"
                    @tap="showDiscount = !showDiscount"
                >
                    <template #icon>
                        <cl-icon class-name="shop-icon-coupon" :size="40" />
                    </template>
                    <cl-text :size="28" color="error">-</cl-text>
                    <cl-text :size="28" type="price" color="error" :value="info?.discountPrice" />
                    <cl-icon
                        name="arrow-bottom"
                        :margin="[0, 0, 0, 10]"
                        :size="24"
                        v-if="info?.discountSource"
                    />
                </cl-list-item>
                <!-- 优惠信息 -->
                <view class="discount-info" v-if="showDiscount && info?.discountSource">
                    {{ info?.discountSource?.info.title }}:满
                    {{ info?.discountSource?.info.condition?.fullAmount }} 元减
                    {{ info?.discountSource?.info.amount }} 元
                </view>
                <cl-list-item label="实付金额" :arrow-icon="false">
                    <template #icon>
                        <cl-icon class-name="shop-icon-q-pay" :size="40" />
                    </template>
                    <cl-text :size="32" type="price" :value="paidAmount" />
                </cl-list-item>
            </view>
            <view class="card" v-if="info?.refund">
                <text class="label">售后/退款</text>
                <cl-list-item label="退款金额" :arrow-icon="false">
                    <cl-text
                        :size="32"
                        type="price"
                        color="error"
                        :value="info?.refund?.amount || 0"
                    />
                </cl-list-item>
                <cl-list-item label="退款状态" :arrow-icon="false">
                    {{ dict.getLabel(RefundStatus, info.refund.status) }}
                </cl-list-item>
                <cl-list-item label="申请原因" :arrow-icon="false">
                    {{ info.refund.reason }}
                </cl-list-item>
                <template v-if="info.refund.status == 2">
                    <cl-list-item label="拒绝原因" :arrow-icon="false" />
                    <cl-text block color="error" :margin="[0, 0, 20, 0]" :line-height="1.4">{{
                        info.refund.refuseReason
                    }}</cl-text>
                </template>
            </view>
            <view class="card">
                <text class="label">订单信息</text>
                <cl-list-item label="订单编号" :arrow-icon="false">
                    <cl-text copy>{{ info?.orderNum }} </cl-text>
                    <cl-button
                        size="small"
                        type="primary"
                        :height="40"
                        :width="70"
                        :font-size="22"
                        :margin="[0, 0, 0, 10]"
                        @tap="toCopy"
                        >复制</cl-button
                    >
                </cl-list-item>
                <cl-list-item label="支付方式" :arrow-icon="false">
                    {{ dict.getLabel(PayType, info?.payType) }}
                </cl-list-item>
                <cl-list-item label="支付时间" :arrow-icon="false" v-if="info?.payType">
                    {{ info?.payTime }}
                </cl-list-item>
                <cl-list-item label="下单时间" :arrow-icon="false">
                    {{ info?.createTime }}
                </cl-list-item>
                <cl-list-item label="订单备注" :arrow-icon="false">
                    {{ info?.remark || "暂无备注" }}
                </cl-list-item>
            </view>
        </view>
        <!-- 底部操作按钮 -->
        <cl-footer :flex="false" border :vt="[info?.status]" v-if="info">
            <order-op-btns :info="info" @command="refs.orderOp?.open" />
        </cl-footer>
        <!-- 订单操作 -->
        <order-op :ref="setRefs('orderOp')" @success="refresh()" />
    </cl-page>
</template>
<script lang="ts" setup>
import { onPullDownRefresh, onReady, onShow } from "@dcloudio/uni-app";
import { useCool, useStore } from "/@/cool";
import { computed, ref } from "vue";
import { useUi } from "/$/cool-ui";
import { OrderStatus, PayType, RefundStatus } from "./dict";
import AddressSelect from "/@/components/address/select.vue";
import GoodsGroup from "/@/components/goods/group.vue";
import OrderOp from "./components/op.vue";
import OrderOpBtns from "./components/op-btns.vue";
const { router, service, refs, setRefs } = useCool();
const { dict } = useStore();
const ui = useUi();
// 显示优惠信息
const showDiscount = ref(false);
// 订单详情
const info = ref<OrderInfo>();
// 订单状态
const status = computed(() => {
    return OrderStatus.find((e) => e.value == info.value?.status);
});
// 实付金额
const paidAmount = computed(() => {
    return Number(info.value?.price || 0) - Number(info.value?.discountPrice || 0);
});
// 获取订单详情
async function refresh() {
    ui.showLoading();
    await service.order.info
        .info({
            id: router.query.id,
        })
        .then((res) => {
            info.value = res;
        })
        .catch((err) => {
            ui.showTips(err.message, () => {
                router.back();
            });
        });
    ui.hideLoading();
}
// 复制订单号
function toCopy() {
    uni.setClipboardData({
        data: info.value?.orderNum!,
    });
}
onPullDownRefresh(async () => {
    await refresh();
    uni.stopPullDownRefresh();
});
onReady(() => {
    refresh();
});
onShow(() => {
    if (ui.loaded) {
        refresh();
    }
});
</script>
<style lang="scss" scoped>
.page {
    padding: 24rpx;
    .discount-info {
        background-color: $cl-color-bg;
        color: #444;
        padding: 12rpx;
        border-radius: 12rpx;
        font-size: 24rpx;
        text-align: right;
    }
}
</style>
pages/order/dict/index.ts
New file
@@ -0,0 +1,98 @@
export const OrderStatus = [
    {
        label: "待付款",
        desc: "等待用户付款",
        value: 0,
        color: "info",
        icon: "shop-icon-order-paid",
    },
    {
        label: "待发货",
        desc: "等待商家发货",
        value: 1,
        icon: "shop-icon-order-not-shipped",
    },
    {
        label: "待收货",
        desc: "等待用户收货",
        value: 2,
        icon: "shop-icon-order-received",
    },
    {
        label: "待评价",
        desc: "等待用户评价",
        value: 3,
        icon: "shop-icon-order-comment",
    },
    {
        label: "已完成",
        desc: "完成",
        value: 4,
        color: "success",
        icon: "shop-icon-order-success",
    },
    {
        label: "退款中",
        desc: "后台退款中",
        value: 5,
        color: "error",
        icon: "shop-icon-order-refund",
    },
    {
        label: "已退款",
        desc: "订单已退款",
        value: 6,
        color: "warning",
        icon: "shop-icon-order-refund",
    },
    {
        label: "已关闭",
        desc: "订单已关闭",
        value: 7,
        color: "info",
        icon: "shop-icon-order-close",
    },
];
export const PayType = [
    {
        label: "未支付",
        value: 0,
    },
    {
        label: "微信",
        value: 1,
        key: "wxpay",
        icon: "/static/icon/wxpay.png",
    },
    {
        label: "支付宝",
        value: 2,
        key: "alipay",
        icon: "/static/icon/alipay.png",
    },
    {
        label: "碳积分",
        value: 3,
        key: "carbonpay",
        icon: "/static/icon/carbonpay.png",
    },
];
export const RefundStatus = [
    {
        label: "申请中",
        value: 0,
        color: "warning",
    },
    {
        label: "已退款",
        value: 1,
        color: "success",
    },
    {
        label: "拒绝退款",
        value: 2,
        color: "error",
    },
];
pages/order/list.vue
New file
@@ -0,0 +1,240 @@
<template>
    <cl-page>
        <view class="page">
            <cl-sticky>
                <cl-tabs
                    v-model="tabs.active"
                    un-color="#999"
                    :list="tabs.list"
                    fill
                    @change="tabs.onChange"
                />
            </cl-sticky>
            <view class="list">
                <view
                    class="item"
                    v-for="item in list"
                    :key="item.id"
                    @tap="
                        router.push({
                            path: '/pages/order/detail',
                            query: {
                                id: item.id,
                            },
                        })
                    "
                >
                    <!-- 订单号、状态 -->
                    <cl-row type="flex" justify="space-between" :margin="[0, 0, 32, 0]">
                        <cl-text :size="28" bold>{{ item.orderNum }}</cl-text>
                        <order-status-tag :item="item" />
                    </cl-row>
                    <!-- 退款、关闭原因 -->
                    <view class="reason" v-if="item.closeRemark || item.refund?.refuseReason">
                        <cl-text prefix-icon="cl-icon-warning-border" :size="24" :ellipsis="1">
                            {{ item.closeRemark || item.refund?.refuseReason }}
                        </cl-text>
                    </view>
                    <!-- 商品图片 -->
                    <view class="pics">
                        <scroll-view scroll-x class="scroller">
                            <view class="inner">
                                <cl-image
                                    v-for="url in item.pics"
                                    :key="url"
                                    :src="resizeImage(url, 200)"
                                    :size="[140, 140]"
                                    :radius="24"
                                    :margin="[0, 16, 0, 0]"
                                />
                            </view>
                        </scroll-view>
                        <view class="cover">
                            <cl-text
                                type="price"
                                :size="30"
                                :value="item.price! - (item.discountPrice || 0)"
                                block
                                bold
                                :margin="[0, 0, 10, 0]"
                            />
                            <cl-text :size="22" color="info">共 {{ item.pics.length }} 件</cl-text>
                        </view>
                    </view>
                    <!-- 操作按钮 -->
                    <order-op-btns :info="item" @command="refs.orderOp?.open" />
                </view>
                <cl-empty icon="order" text="暂无订单" v-if="isEmpty(list)" />
            </view>
        </view>
        <!-- 订单操作 -->
        <order-op :ref="setRefs('orderOp')" @success="refresh()" />
    </cl-page>
</template>
<script lang="ts" setup>
import { onReady, onShow } from "@dcloudio/uni-app";
import { useCool, usePager, useUpload } from "/@/cool";
import { reactive } from "vue";
import { isEmpty } from "lodash-es";
import { useUi } from "/$/cool-ui";
import OrderOp from "./components/op.vue";
import OrderOpBtns from "./components/op-btns.vue";
import OrderStatusTag from "./components/status-tag.vue";
const { service, router, refs, setRefs } = useCool();
const { resizeImage } = useUpload();
const ui = useUi();
const { list, onRefresh, onData } = usePager<OrderInfo>();
// 状态选项卡
const tabs = reactive({
    active: "0",
    list: [
        {
            label: "全部",
        },
        {
            label: "待付款",
            value: "0",
        },
        {
            label: "待发货",
            value: "1",
        },
        {
            label: "待收货",
            value: "2",
        },
        {
            label: "售后/退款",
            value: "5,6",
        },
        {
            label: "待评价",
            value: "3",
        },
        {
            label: "已完成",
            value: "4",
        },
    ],
    onChange() {
        refresh({
            page: 1,
        });
    },
});
// 获取订单列表
function refresh(params?: any, loading?: boolean) {
    const { data, next } = onRefresh(params, { loading });
    next(
        service.order.info.page({
            status: tabs.active?.split(","),
            ...data,
        }),
    );
}
onData((list) => {
    list.forEach((e) => {
        e.pics = (e.goodsList || []).map((a) => {
            let arr = a.spec.images || [];
            if (isEmpty(arr)) {
                arr = [a.goodsInfo?.mainPic];
            }
            return arr[0];
        });
    });
});
onReady(() => {
    tabs.active = router.query.status || undefined;
    refresh();
});
onShow(() => {
    if (ui.loaded) {
        refresh({}, false);
    }
});
defineExpose({
    refresh,
});
</script>
<style lang="scss" scoped>
.page {
    .list {
        padding: 24rpx;
        .item {
            background-color: #fff;
            padding: 32rpx;
            border-radius: 24rpx;
            margin-bottom: 24rpx;
            .reason {
                display: flex;
                align-items: center;
                border-radius: 16rpx;
                background-color: $cl-color-bg;
                padding: 16rpx;
                margin-bottom: 32rpx;
            }
            .pics {
                height: 140rpx;
                margin-bottom: 32rpx;
                overflow: hidden;
                position: relative;
                .scroller {
                    height: 160rpx;
                    .inner {
                        display: flex;
                        &::after {
                            content: "";
                            display: block;
                            width: 144rpx;
                            flex-shrink: 0;
                        }
                    }
                }
                .cover {
                    display: flex;
                    flex-direction: column;
                    justify-content: center;
                    align-items: flex-end;
                    position: absolute;
                    right: 0;
                    top: 0;
                    height: 100%;
                    width: 160rpx;
                    background-color: rgba(255, 255, 255, 0.95);
                }
            }
            &:last-child {
                margin-bottom: 0;
            }
        }
    }
}
</style>
pages/order/logistics.vue
New file
@@ -0,0 +1,108 @@
<template>
    <cl-page>
        <view class="page">
            <!-- 收货地址 -->
            <address-select disabled />
            <!-- 物流列表 -->
            <view class="card">
                <template v-if="logistics">
                    <cl-row type="flex" justify="space-between" :margin="[10, 0, 20, 0]">
                        <cl-text :size="24" color="info">
                            {{ logistics.expName }} {{ logistics.number }}
                        </cl-text>
                        <cl-text :size="24" color="info" @tap="toCopy">复制</cl-text>
                    </cl-row>
                    <!-- 运送记录 -->
                    <view class="record">
                        <cl-timeline>
                            <cl-timeline-item
                                v-for="(item, index) in logistics.list"
                                :key="index"
                                :timestamp="item.time"
                                :content="item.status"
                            />
                        </cl-timeline>
                    </view>
                </template>
                <cl-empty :fixed="false" :height="600" text="暂无物流信息" v-else />
            </view>
        </view>
    </cl-page>
</template>
<script lang="ts" setup>
import { onPullDownRefresh, onReady } from "@dcloudio/uni-app";
import { useCool } from "/@/cool";
import { ref } from "vue";
import { useUi } from "/$/cool-ui";
import AddressSelect from "/@/components/address/select.vue";
const { router, service } = useCool();
const ui = useUi();
// 订单详情
const info = ref<OrderInfo>();
// 物流信息
const logistics = ref();
// 获取订单详情
async function refresh() {
    const { orderId } = router.query;
    ui.showLoading();
    try {
        service.order.info
            .info({
                id: orderId,
            })
            .then((res) => {
                info.value = res;
            });
        await service.order.info
            .logistics({
                orderId,
            })
            .then((res) => {
                logistics.value = res;
            });
    } catch (err: any) {
        ui.showTips(err.message, () => {
            router.back();
        });
    }
    ui.hideLoading();
}
// 复制物流单号
function toCopy() {
    uni.setClipboardData({
        data: info.value?.logistics.num!,
    });
}
onPullDownRefresh(async () => {
    await refresh();
    uni.stopPullDownRefresh();
});
onReady(() => {
    refresh();
});
</script>
<style lang="scss" scoped>
.page {
    padding: 24rpx;
    .record {
        margin-top: 6rpx;
    }
}
</style>
pages/order/refund.vue
New file
@@ -0,0 +1,126 @@
<template>
    <cl-page>
        <view class="page">
            <view class="card">
                <cl-list-item label="申请原因" :arrow-icon="false" :margin="[0, -20, 0, 0]">
                    <cl-select-popup
                        v-model="form.reason"
                        :border="false"
                        :options="list"
                        title="选择申请原因"
                    />
                </cl-list-item>
                <cl-list-item label="退款金额" :arrow-icon="false">
                    <cl-text type="price" :size="40" color="error" :value="refundAmount || 0" />
                </cl-list-item>
            </view>
            <goods-group :data="info?.goodsList" readonly />
        </view>
        <cl-footer>
            <cl-button custom type="primary" :loading="loading" @tap="submit">提交</cl-button>
        </cl-footer>
    </cl-page>
</template>
<script lang="ts" setup>
import { computed, reactive, ref } from "vue";
import { useCool, useStore } from "/@/cool";
import { onReady } from "@dcloudio/uni-app";
import { useUi } from "/$/cool-ui";
import GoodsGroup from "/@/components/goods/group.vue";
const { service, router } = useCool();
const ui = useUi();
const { dict } = useStore();
// 表单
const form = reactive({
    reason: "",
});
// 提交中
const loading = ref(false);
// 订单信息
const info = ref<OrderInfo>();
// 退款金额
const refundAmount = computed(() => {
    return info.value?.goodsList?.reduce((a, b) => {
        return a + b.price * b.count;
    }, 0);
});
// 原因列表
const list = computed(() => {
    return dict.get("orderRefundReason").map((e) => {
        return {
            label: e.label,
            value: e.label,
        };
    });
});
// 获取订单信息
async function refresh() {
    ui.showLoading();
    await service.order.info
        .info({
            id: router.query.orderId,
        })
        .then((res) => {
            info.value = res;
        })
        .catch((err) => {
            ui.showTips(err.message, () => {
                router.back();
            });
        });
    ui.hideLoading();
}
// 提交
async function submit() {
    if (!form.reason) {
        return ui.showToast("请选择申请原因");
    }
    loading.value = true;
    await service.order.info
        .refund({
            orderId: router.query.orderId,
            reason: form.reason,
        })
        .then(() => {
            ui.showTips("退款申请提交成功,等待商家处理", () => {
                router.back();
            });
        })
        .catch((err) => {
            ui.showToast(err.message);
        });
    loading.value = false;
}
onReady(() => {
    refresh();
});
</script>
<style lang="scss" scoped>
.page {
    padding: 24rpx;
    .goods {
        display: flex;
        padding: 24rpx;
    }
}
</style>
pages/order/submit.vue
New file
@@ -0,0 +1,210 @@
<template>
    <cl-page>
        <view class="page">
            <!-- 收货地址 -->
            <address-select />
            <!-- 商品列表 -->
            <goods-group :data="list" />
            <view class="card">
                <text class="label">订单备注</text>
                <cl-textarea
                    v-model="form.remark"
                    placeholder="选填,付款后商家可见"
                    background-color="#f7f7f7"
                    :border="false"
                    :margin="[8, 0, 10, 0]"
                    count
                />
            </view>
            <view class="card">
                <text class="label">价格明细</text>
                <cl-list-item :label="`商品总价(共${total}件商品)`" :arrow-icon="false">
                    <template #icon>
                        <cl-icon class-name="shop-icon-q-order" :size="40"></cl-icon>
                    </template>
                    <cl-text :size="28" type="price" :value="totalAmount"></cl-text>
                </cl-list-item>
                <cl-list-item label="优惠券">
                    <template #icon>
                        <cl-icon class-name="shop-icon-coupon" :size="40"></cl-icon>
                    </template>
                    <coupon-select :total-amount="totalAmount" :ref="setRefs('couponSelect')" />
                </cl-list-item>
                <cl-list-item label="合计" :arrow-icon="false">
                    <template #icon>
                        <cl-icon class-name="shop-icon-q-pay" :size="40"></cl-icon>
                    </template>
                    <cl-text :size="32" type="price" :value="paidAmount"></cl-text>
                </cl-list-item>
            </view>
            <view class="card">
                <text class="label">支付方式</text>
                <cl-radio-group fill v-model="form.payType">
                    <cl-radio
                        v-for="item in order.payTypes"
                        :key="item.value"
                        :label="String(item.value)"
                        :height="80"
                    >
                        {{ item.label }}
                        <image class="pay-icon" mode="aspectFill" :src="item.icon" />
                    </cl-radio>
                </cl-radio-group>
            </view>
        </view>
        <cl-footer :flex="false" border :padding="24">
            <cl-row type="flex" justify="space-between">
                <cl-text type="price" color="error" :size="50" :value="paidAmount" />
                <cl-button type="primary" :width="200" :loading="loading" @tap="submit"
                    >提交订单</cl-button
                >
            </cl-row>
        </cl-footer>
    </cl-page>
</template>
<script lang="ts" setup>
import { onReady } from "@dcloudio/uni-app";
import { useCool } from "/@/cool";
import { computed, reactive, ref } from "vue";
import { useAddress, useShoppingCart } from "/@/hooks";
import { useUi } from "/$/cool-ui";
import AddressSelect from "/@/components/address/select.vue";
import CouponSelect from "/@/components/coupon/select.vue";
import GoodsGroup from "/@/components/goods/group.vue";
import { useOrder } from "/@/hooks";
const { router, service, refs, setRefs } = useCool();
const address = useAddress();
const ui = useUi();
const spCart = useShoppingCart();
const order = useOrder();
// 表单:payType 用字符串绑定 radio(小程序 label 都是字符串),提交时再转数字
const form = reactive({
    payType: String(order.payTypes[0]?.value ?? 1),
    remark: "",
});
// 提交中
const loading = ref(false);
// 购买列表
const list = ref<OrderGoods[]>([]);
// 商品总数
const total = computed(() => {
    return list.value.reduce((a, b) => {
        return a + b.count;
    }, 0);
});
// 总金额
const totalAmount = computed(() => {
    return list.value.reduce((a, b) => {
        return a + (b.spec.price || 0) * b.count;
    }, 0);
});
// 实付金额
const paidAmount = computed(() => {
    return totalAmount.value - (refs.couponSelect?.checked?.amount || 0);
});
// 提交
async function submit() {
    if (!address.info?.id) {
        return ui.showToast("请选择收货地址");
    }
    loading.value = true;
    await service.order.info
        .create({
            data: {
                remark: form.remark,
                goodsList: list.value.map((e) => {
                    return {
                        goodsInfo: e.goodsInfo,
                        spec: e.spec,
                        count: e.count,
                        goodsId: e.goodsInfo.id,
                    };
                }),
                couponId: refs.couponSelect?.checked?.id,
                addressId: address.info?.id,
                title: "购买商品",
                payType: Number(form.payType) || 0,
            },
        })
        .then(async (res) => {
            // 删除购物车数据
            list.value.forEach((e) => {
                spCart.delBySpecId(e.spec.id!);
            });
            // 跳转订单详情
            function next() {
                router.push({
                    path: "/pages/order/detail",
                    query: {
                        id: res.id,
                    },
                    mode: "redirectTo",
                });
            }
            await order
                .toPay(res.id, form.payType)
                .then(() => {
                    next();
                })
                .catch((err) => {
                    ui.showTips(err.message, () => {
                        next();
                    });
                });
        })
        .catch((err) => {
            ui.showTips(err.message, () => {
                router.push({
                    path: "/pages/order/list",
                    mode: "redirectTo",
                });
            });
        });
    loading.value = false;
}
onReady(() => {
    list.value = router.params.buyList;
});
</script>
<style lang="scss" scoped>
.page {
    padding: 24rpx;
    .pay-icon {
        height: 56rpx;
        width: 56rpx;
        position: absolute;
        right: 0;
        top: calc(50% - 28rpx);
    }
}
</style>
pages/recycle/appointment.vue
New file
@@ -0,0 +1,918 @@
<template>
    <cl-page background-color="#f7f7f7">
        <view class="appointment-page">
            <cl-sticky>
                <view class="tab-bar">
                    <view class="tab-left">
                        <view
                            v-for="tab in tabs"
                            :key="tab.value"
                            class="tab-item"
                            :class="{ 'is-active': activeTab === tab.value }"
                            @tap="switchTab(tab.value)"
                        >
                            <text>{{ tab.label }}</text>
                            <view v-if="activeTab === tab.value" class="tab-line"></view>
                        </view>
                    </view>
                    <view class="add-btn" @tap="openAdd">新增预约</view>
                </view>
            </cl-sticky>
            <scroll-view
                class="record-scroll"
                scroll-y
                :lower-threshold="80"
                @scrolltolower="loadMore"
                :refresher-enabled="true"
                :refresher-triggered="refreshing"
                @refresherrefresh="onRefresh"
            >
                <view class="record-list">
                    <view
                        v-for="item in list"
                        :key="item.id"
                        class="record-card"
                        :class="{ 'is-clickable': activeTab === 'booked' }"
                        @tap="openAppointmentQrcode(item)"
                    >
                        <view class="card-header">
                            <text class="order-no">单号:{{ item.appointmentNo }}</text>
                            <text class="status" :class="`status-${item.status}`">
                                {{ item.status }}
                            </text>
                        </view>
                        <view class="card-body">
                            <view class="info-row">
                                <text class="info-label">预约时间</text>
                                <text class="info-value">
                                    {{ item.appointmentDate || "-" }}
                                    {{ dict.getLabel("appointmentslot", item.appointmentSlot) || item.appointmentSlot || "" }}
                                </text>
                            </view>
                            <view class="info-row">
                                <text class="info-label">预计重量</text>
                                <text class="info-value">{{ item.expectedWeight ?? 0 }} kg</text>
                            </view>
                            <view class="info-row">
                                <text class="info-label">联系人</text>
                                <text class="info-value">
                                    {{ item.contactName || "-" }}
                                    {{ item.contactPhone || "" }}
                                </text>
                            </view>
                            <view class="info-row">
                                <text class="info-label">上门地址</text>
                                <text class="info-value addr">{{ item.address || "-" }}</text>
                            </view>
                        </view>
                        <view v-if="activeTab === 'booked'" class="card-toggle">
                            <text>点击出示预约码</text>
                        </view>
                    </view>
                </view>
                <cl-loadmore
                    v-if="list.length > 0"
                    :loading="loading"
                    :finish="finished"
                    :divider="false"
                />
                <cl-empty
                    v-if="!loading && list.length === 0"
                    :fixed="false"
                    text="暂无预约记录"
                />
            </scroll-view>
        </view>
        <!-- 新增预约 -->
        <cl-popup
            v-model="showAdd"
            direction="bottom"
            title="新增预约"
            :padding="0"
            :border-radius="[32, 32, 0, 0]"
            show-close-btn
        >
            <scroll-view scroll-y class="add-scroll">
                <view class="form-card">
                    <view class="form-item" @tap="openAddressPicker">
                        <text class="form-label">上门地址</text>
                        <view class="form-picker">
                            <view v-if="form.address" class="addr-preview">
                                <text class="addr-line">
                                    {{ form.contactName }} {{ form.contactPhone }}
                                </text>
                                <text class="addr-line muted">{{ form.address }}</text>
                            </view>
                            <text v-else class="placeholder">从地址库选择,也可下方自行填写</text>
                            <cl-icon name="arrow-right" color="#ccc" />
                        </view>
                    </view>
                    <view class="form-item">
                        <text class="form-label">联系人</text>
                        <input
                            class="form-input"
                            v-model="form.contactName"
                            placeholder="请输入联系人"
                            placeholder-class="placeholder"
                        />
                    </view>
                    <view class="form-item">
                        <text class="form-label">联系电话</text>
                        <input
                            class="form-input"
                            type="number"
                            maxlength="11"
                            v-model="form.contactPhone"
                            placeholder="请输入手机号"
                            placeholder-class="placeholder"
                        />
                    </view>
                    <view class="form-item">
                        <text class="form-label">详细地址</text>
                        <input
                            class="form-input"
                            v-model="form.address"
                            placeholder="请输入上门地址"
                            placeholder-class="placeholder"
                        />
                    </view>
                    <view class="form-item">
                        <text class="form-label">预约日期</text>
                        <picker mode="date" :start="minDate" :value="form.appointmentDate" @change="onDateChange">
                            <view class="form-picker">
                                <text v-if="form.appointmentDate">{{ form.appointmentDate }}</text>
                                <text v-else class="placeholder">请选择预约日期</text>
                                <cl-icon name="arrow-right" color="#ccc" />
                            </view>
                        </picker>
                    </view>
                    <view class="form-item">
                        <text class="form-label">时间段</text>
                        <picker
                            mode="selector"
                            :range="slotOptions"
                            :value="slotIndex"
                            @change="onSlotChange"
                        >
                            <view class="form-picker">
                                <text v-if="slotLabel">{{ slotLabel }}</text>
                                <text v-else class="placeholder">请选择时间段</text>
                                <cl-icon name="arrow-right" color="#ccc" />
                            </view>
                        </picker>
                    </view>
                    <view class="form-item">
                        <text class="form-label">预计重量</text>
                        <input
                            class="form-input"
                            type="digit"
                            v-model="form.expectedWeight"
                            placeholder="请输入预计重量(kg)"
                            placeholder-class="placeholder"
                        />
                    </view>
                </view>
            </scroll-view>
            <view class="add-footer">
                <button class="submit-btn" :disabled="submitting" @tap="submitAppointment">
                    {{ submitting ? "提交中..." : "确认预约" }}
                </button>
            </view>
        </cl-popup>
        <!-- 选择收件地址 -->
        <cl-popup
            v-model="showAddress"
            direction="bottom"
            title="选择地址"
            :padding="0"
            :border-radius="[32, 32, 0, 0]"
            :z-index="700"
            show-close-btn
        >
            <scroll-view scroll-y class="addr-scroll">
                <view class="addr-list">
                    <view
                        v-for="item in addressList"
                        :key="item.id"
                        class="addr-item"
                        :class="{ 'is-active': selectedAddressId === item.id }"
                        @tap="selectAddress(item)"
                    >
                        <address-item :item="item" background-color="#f7f7f7">
                            <template #icon>
                                <text class="cl-icon-check-border"></text>
                            </template>
                        </address-item>
                    </view>
                </view>
                <cl-empty
                    v-if="!addressLoading && addressList.length === 0"
                    icon="address"
                    :fixed="false"
                    text="暂无地址"
                />
            </scroll-view>
            <view class="add-footer">
                <cl-button custom type="primary" @tap="goAddAddress">添加收货地址</cl-button>
            </view>
        </cl-popup>
        <!-- 预约码 -->
        <cl-popup
            v-model="showQrcode"
            direction="center"
            :close-on-click-overlay="true"
            background-color="transparent"
            @close="showQrcode = false"
        >
            <view class="qrcode-popup" @tap.stop>
                <view class="qrcode-card">
                    <text class="qrcode-title">预约码</text>
                    <text class="qrcode-desc">向分拣员出示此码,关联本次预约</text>
                    <view class="qrcode-image-wrap">
                        <image
                            v-if="qrcodeUrl"
                            class="qrcode-image"
                            :src="qrcodeUrl"
                            mode="aspectFit"
                        />
                        <text v-else class="placeholder">生成中...</text>
                    </view>
                    <text class="qrcode-code">预约单号:{{ currentAppointmentNo }}</text>
                    <view class="qrcode-close" @tap="showQrcode = false">
                        <cl-icon name="close" :size="36" color="#999" />
                    </view>
                </view>
            </view>
        </cl-popup>
    </cl-page>
</template>
<script lang="ts" setup>
import { computed, reactive, ref, watch } from "vue";
import { onReady, onShow, onPullDownRefresh } from "@dcloudio/uni-app";
import { useCool, useStore } from "/@/cool";
import { useAddress } from "/@/hooks";
import AddressItem from "/@/components/address/item.vue";
import { generateQrcode } from "/@/cool/utils/qrcode";
const { service, router } = useCool();
const { user, dict } = useStore();
const address = useAddress();
const tabs = [
    { label: "已预约", value: "booked" },
    { label: "已完成", value: "done" },
] as const;
type TabValue = (typeof tabs)[number]["value"];
const activeTab = ref<TabValue>("booked");
const list = ref<Eps.RecycleAppointmentEntity[]>([]);
const loading = ref(false);
const refreshing = ref(false);
const finished = ref(false);
const page = ref(1);
const size = 10;
const showAdd = ref(false);
const submitting = ref(false);
const showAddress = ref(false);
const addressLoading = ref(false);
const addressList = ref<Eps.UserAddressEntity[]>([]);
const selectedAddressId = ref<number>();
const showQrcode = ref(false);
const qrcodeUrl = ref("");
const currentAppointmentNo = ref("");
const form = reactive({
    contactName: "",
    contactPhone: "",
    address: "",
    appointmentDate: "",
    appointmentSlot: "",
    expectedWeight: "",
});
const slotList = computed(() => dict.get("appointmentslot"));
const slotOptions = computed(() => slotList.value.map((e) => e.name || e.label));
const slotLabel = computed(() => {
    return dict.getLabel("appointmentslot", form.appointmentSlot) || form.appointmentSlot;
});
const minDate = computed(() => {
    const d = new Date();
    const pad = (n: number) => String(n).padStart(2, "0");
    return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
});
const slotIndex = computed(() => {
    const idx = slotList.value.findIndex(
        (e) => e.value == form.appointmentSlot || e.name == form.appointmentSlot,
    );
    return idx >= 0 ? idx : 0;
});
function defaultSlot() {
    const first = slotList.value[0];
    return first?.value ?? first?.name ?? "";
}
let reqSeq = 0;
function getUnionid() {
    return user.info?.phone || "18918594752";
}
function formatAddress(item: Eps.UserAddressEntity) {
    return `${item.province || ""}${item.city || ""}${item.district || ""}${item.address || ""}`;
}
function fillFormByAddress(item?: Eps.UserAddressEntity) {
    if (!item) return;
    form.contactName = item.contact || "";
    form.contactPhone = item.phone || "";
    form.address = formatAddress(item);
    selectedAddressId.value = item.id;
}
function buildQuery() {
    return {
        page: page.value,
        size,
        campusUserId: getUnionid(),
        unionid: getUnionid(),
        tab: activeTab.value,
        status: activeTab.value === "done" ? "已完成" : undefined,
    };
}
async function loadList(reset = false) {
    if (loading.value && !reset) return;
    if (!reset && finished.value) return;
    if (reset) {
        page.value = 1;
        finished.value = false;
        list.value = [];
    }
    const current = ++reqSeq;
    loading.value = true;
    try {
        const res = await service.shop.recycleappointment.page(buildQuery());
        if (current !== reqSeq) return;
        console.log("【预约记录返回】", res);
        const newList = res?.list || [];
        list.value = page.value === 1 ? newList : [...list.value, ...newList];
        const total = res?.pagination?.total;
        finished.value =
            newList.length < size || (typeof total === "number" && list.value.length >= total);
        page.value++;
    } catch (e) {
        if (current !== reqSeq) return;
        console.error("【预约记录加载失败】", e);
    } finally {
        if (current === reqSeq) {
            loading.value = false;
            refreshing.value = false;
        }
    }
}
function switchTab(val: TabValue) {
    if (activeTab.value === val) return;
    activeTab.value = val;
    loadList(true);
}
function loadMore() {
    if (!loading.value && !finished.value) {
        loadList(false);
    }
}
function onRefresh() {
    refreshing.value = true;
    loadList(true);
}
async function openAppointmentQrcode(item: Eps.RecycleAppointmentEntity) {
    if (activeTab.value !== "booked" || !item.appointmentNo) return;
    currentAppointmentNo.value = item.appointmentNo;
    qrcodeUrl.value = "";
    showQrcode.value = true;
    try {
        qrcodeUrl.value = await generateQrcode(item.appointmentNo);
    } catch (e) {
        console.error("【预约码生成失败】", e);
        uni.showToast({ title: "预约码生成失败", icon: "none" });
    }
}
function resetForm() {
    form.contactName = "";
    form.contactPhone = "";
    form.address = "";
    form.appointmentDate = minDate.value;
    form.appointmentSlot = defaultSlot();
    form.expectedWeight = "";
    selectedAddressId.value = undefined;
}
function openAdd() {
    resetForm();
    address.getDefault();
    fillFormByAddress(address.info);
    showAdd.value = true;
}
function onDateChange(e: any) {
    form.appointmentDate = e.detail.value;
}
function onSlotChange(e: any) {
    const item = slotList.value[Number(e.detail.value)];
    form.appointmentSlot = item?.value ?? item?.name ?? "";
}
async function refreshAddressList() {
    addressLoading.value = true;
    try {
        addressList.value = (await service.user.address.list()) || [];
    } catch (e) {
        console.error("【地址列表加载失败】", e);
        addressList.value = [];
    } finally {
        addressLoading.value = false;
    }
}
async function openAddressPicker() {
    await refreshAddressList();
    showAddress.value = true;
}
function selectAddress(item: Eps.UserAddressEntity) {
    address.set(item);
    fillFormByAddress(item);
    showAddress.value = false;
}
function goAddAddress() {
    showAddress.value = false;
    router.push("/pages/user/address-edit");
}
function genAppointmentNo() {
    const d = new Date();
    const pad = (n: number) => String(n).padStart(2, "0");
    const rand = String(Math.floor(Math.random() * 1000)).padStart(3, "0");
    return `AP${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}${rand}`;
}
async function submitAppointment() {
    if (!form.contactName) {
        uni.showToast({ title: "请填写联系人", icon: "none" });
        return;
    }
    if (!form.contactPhone || form.contactPhone.length !== 11) {
        uni.showToast({ title: "请填写正确手机号", icon: "none" });
        return;
    }
    if (!form.address) {
        uni.showToast({ title: "请填写上门地址", icon: "none" });
        return;
    }
    if (!form.appointmentDate) {
        uni.showToast({ title: "请选择预约日期", icon: "none" });
        return;
    }
    if (!form.appointmentSlot) {
        uni.showToast({ title: "请选择时间段", icon: "none" });
        return;
    }
    submitting.value = true;
    try {
        await service.shop.recycleappointment.add({
            appointmentNo: genAppointmentNo(),
            campusUserId: getUnionid(),
            campusUserName: user.info?.nickName || "",
            contactName: form.contactName,
            contactPhone: form.contactPhone,
            address: form.address,
            appointmentDate: form.appointmentDate,
            appointmentSlot: form.appointmentSlot,
            expectedWeight: Number(form.expectedWeight) || 0,
            status: "已预约",
        });
        uni.showToast({ title: "预约成功", icon: "success" });
        showAdd.value = false;
        activeTab.value = "booked";
        loadList(true);
    } catch (e) {
        console.error("【预约提交失败】", e);
        uni.showToast({ title: "预约失败,请重试", icon: "none" });
    } finally {
        submitting.value = false;
    }
}
watch(
    () => address.info,
    (val) => {
        if (showAdd.value && val && !form.contactName && !form.address) {
            fillFormByAddress(val);
        }
    },
);
onReady(() => {
    dict.refresh(["appointmentslot"]);
    loadList(true);
    address.getDefault();
});
onShow(() => {
    refreshAddressList();
    if (showAdd.value && address.info) {
        fillFormByAddress(address.info);
    }
});
onPullDownRefresh(() => {
    loadList(true).finally(() => {
        uni.stopPullDownRefresh();
    });
});
</script>
<style lang="scss" scoped>
.appointment-page {
    height: 100vh;
    display: flex;
    flex-direction: column;
    .tab-bar {
        display: flex;
        align-items: center;
        justify-content: space-between;
        background: #fff;
        padding: 0 24rpx 0 40rpx;
        border-bottom: 1rpx solid #f0f0f0;
    }
    .tab-left {
        display: flex;
    }
    .tab-item {
        position: relative;
        padding: 28rpx 30rpx;
        font-size: 28rpx;
        color: #666;
        transition: color 0.2s;
        &.is-active {
            color: #2ecc71;
            font-weight: 600;
        }
        .tab-line {
            position: absolute;
            bottom: 0;
            left: 50%;
            transform: translateX(-50%);
            width: 40rpx;
            height: 4rpx;
            background: #2ecc71;
            border-radius: 2rpx;
        }
    }
    .add-btn {
        padding: 10rpx 22rpx;
        border-radius: 28rpx;
        background: #2ecc71;
        color: #fff;
        font-size: 24rpx;
        font-weight: 500;
    }
    .record-scroll {
        flex: 1;
        height: 0;
        padding: 24rpx;
        box-sizing: border-box;
    }
    .record-list {
        display: flex;
        flex-direction: column;
        gap: 20rpx;
    }
    .record-card {
        background: #fff;
        border-radius: 20rpx;
        padding: 28rpx;
        box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
        &.is-clickable:active {
            opacity: 0.92;
        }
    }
    .card-toggle {
        margin-top: 16rpx;
        padding-top: 16rpx;
        border-top: 1rpx dashed #f0f0f0;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 22rpx;
        color: #2ecc71;
    }
    .card-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding-bottom: 20rpx;
        border-bottom: 1rpx solid #f5f5f5;
    }
    .order-no {
        font-size: 26rpx;
        color: #333;
        font-weight: 500;
    }
    .status {
        font-size: 22rpx;
        padding: 6rpx 16rpx;
        border-radius: 20rpx;
        background: #f5f5f5;
        color: #999;
        &.status-待审核,
        &.status-待接单 {
            background: #fef5e7;
            color: #e67e22;
        }
        &.status-已接单,
        &.status-上门中 {
            background: #eaf4fc;
            color: #2980b9;
        }
        &.status-已完成 {
            background: #e8f8f0;
            color: #27ae60;
        }
        &.status-已取消 {
            background: #fef0f0;
            color: #e74c3c;
        }
    }
    .card-body {
        padding-top: 20rpx;
        display: flex;
        flex-direction: column;
        gap: 14rpx;
    }
    .info-row {
        display: flex;
        justify-content: space-between;
        align-items: flex-start;
        gap: 24rpx;
    }
    .info-label {
        font-size: 24rpx;
        color: #999;
        flex-shrink: 0;
    }
    .info-value {
        font-size: 26rpx;
        color: #333;
        font-weight: 500;
        text-align: right;
        &.addr {
            max-width: 420rpx;
            line-height: 1.4;
        }
    }
}
.add-scroll {
    height: 62vh;
    padding: 0 32rpx 20rpx;
    box-sizing: border-box;
}
.form-card {
    background: #fff;
}
.form-item {
    display: flex;
    align-items: flex-start;
    padding: 24rpx 0;
    border-bottom: 1rpx solid #f5f5f5;
}
.form-label {
    width: 160rpx;
    font-size: 26rpx;
    color: #666;
    flex-shrink: 0;
    line-height: 60rpx;
}
.form-input {
    flex: 1;
    height: 60rpx;
    font-size: 26rpx;
    color: #333;
}
.form-picker {
    flex: 1;
    min-height: 60rpx;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12rpx;
    font-size: 26rpx;
    color: #333;
}
.addr-preview {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 6rpx;
}
.addr-line {
    font-size: 26rpx;
    color: #333;
    line-height: 1.4;
    &.muted {
        font-size: 22rpx;
        color: #999;
    }
}
.placeholder {
    color: #ccc;
    font-size: 26rpx;
}
.add-footer {
    padding: 16rpx 32rpx calc(16rpx + env(safe-area-inset-bottom));
}
.submit-btn {
    width: 100%;
    height: 80rpx;
    line-height: 80rpx;
    background: linear-gradient(135deg, #2ecc71, #27ae60);
    color: #fff;
    font-size: 30rpx;
    font-weight: 600;
    border-radius: 40rpx;
    border: none;
    letter-spacing: 4rpx;
    &[disabled] {
        opacity: 0.6;
    }
}
.addr-scroll {
    height: 50vh;
}
.addr-list {
    padding: 0 32rpx 24rpx;
}
.addr-item {
    border: 2rpx solid #f7f7f7;
    border-radius: 24rpx;
    margin-bottom: 24rpx;
    .cl-icon-check-border {
        display: none;
        color: #2ecc71;
        font-size: 46rpx;
    }
    &.is-active {
        border-color: #2ecc71;
        .cl-icon-check-border {
            display: block;
        }
    }
    &:last-child {
        margin-bottom: 0;
    }
}
.qrcode-popup {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
}
.qrcode-card {
    position: relative;
    width: 600rpx;
    background: #fff;
    border-radius: 28rpx;
    padding: 50rpx 40rpx 40rpx;
    display: flex;
    flex-direction: column;
    align-items: center;
}
.qrcode-title {
    font-size: 34rpx;
    font-weight: 700;
    color: #333;
    margin-bottom: 10rpx;
}
.qrcode-desc {
    font-size: 24rpx;
    color: #999;
    margin-bottom: 36rpx;
}
.qrcode-image-wrap {
    width: 480rpx;
    height: 480rpx;
    border-radius: 16rpx;
    overflow: hidden;
    background: #f7f7f7;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 28rpx;
}
.qrcode-image {
    width: 100%;
    height: 100%;
}
.qrcode-code {
    font-size: 26rpx;
    color: #666;
    letter-spacing: 2rpx;
    word-break: break-all;
    text-align: center;
}
.qrcode-close {
    position: absolute;
    top: 20rpx;
    right: 20rpx;
    width: 60rpx;
    height: 60rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: #f5f5f5;
}
</style>
pages/recycle/banner.vue
New file
@@ -0,0 +1,368 @@
<!--
    再生资源回收 - 首页功能模块组件
    路径建议:/components/recycle-home.vue
    使用方式:在首页 <view class="hot"> 再生资源回收 </view> 区域直接引入
-->
<template>
    <view class="recycle-home">
        <!-- ===== 第一行:数据看板 ===== -->
        <view class="recycle-dashboard">
            <view class="dashboard-bg"></view>
            <view class="dashboard-inner">
                <view class="dash-item">
                    <text class="dash-num">{{ dashboard.carbonPoints ?? 0 }}</text>
                    <text class="dash-label">碳积分</text>
                </view>
                <view class="dash-divider"></view>
                <view class="dash-item">
                    <text class="dash-num">{{ dashboard.recycleCount ?? 0 }}</text>
                    <text class="dash-label">回收次数</text>
                </view>
                <view class="dash-divider"></view>
                <view class="dash-item">
                    <text class="dash-num dash-highlight">
                        {{ dashboard.overPercent ?? 0 }}%
                    </text>
                    <text class="dash-label">积分排名</text>
                </view>
            </view>
        </view>
        <!-- ===== 第二行:4 个功能按钮 ===== -->
        <view class="recycle-actions">
            <!-- ① 我的回收码 -->
            <view class="action-item" @tap="openQrcode">
                <view class="action-icon icon-green">
                    <image class="icon-img" :src="icons.qrcode" mode="aspectFit" />
                </view>
                <text class="action-text">我的回收码</text>
            </view>
            <!-- ② 回收记录 -->
            <view class="action-item" @tap="goRecord">
                <view class="action-icon icon-blue">
                    <image class="icon-img" :src="icons.record" mode="aspectFit" />
                </view>
                <text class="action-text">回收记录</text>
            </view>
            <!-- ③ 我要预约 -->
            <view class="action-item" @tap="goAppointment">
                <view class="action-icon icon-orange">
                    <image class="icon-img" :src="icons.appointment" mode="aspectFit" />
                </view>
                <text class="action-text">预约回收</text>
            </view>
            <!-- ④ 积分转赠 -->
            <view class="action-item" @tap="goTransfer">
                <view class="action-icon icon-purple">
                    <image class="icon-img" :src="icons.transfer" mode="aspectFit" />
                </view>
                <text class="action-text">积分转赠</text>
            </view>
        </view>
        <!-- ===== 二维码弹层 ===== -->
        <cl-popup v-model="showQrcode" direction="center" :close-on-click-overlay="true" background-color="transparent"
            @close="showQrcode = false">
            <view class="qrcode-popup" @tap.stop>
                <view class="qrcode-card">
                    <text class="qrcode-title">我的回收码</text>
                    <text class="qrcode-desc">向回收员出示此码完成回收</text>
                    <view class="qrcode-image-wrap">
                        <image class="qrcode-image" :src="qrcodeUrl" mode="aspectFit" />
                    </view>
                    <text class="qrcode-code">回收码:{{ userCode }}</text>
                    <view class="qrcode-close" @tap="showQrcode = false">
                        <cl-icon name="close" :size="36" color="#999" />
                    </view>
                </view>
            </view>
        </cl-popup>
    </view>
</template>
<script lang="ts" setup>
    import { ref, onMounted } from "vue";
    import { router, useCool, useStore, usePager } from "/@/cool";
    import { onReady, onShareAppMessage } from "@dcloudio/uni-app";
    import { generateRecycleQrcode } from "/@/cool/utils/qrcode";
    const { user } = useStore();
    const { service, refs, setRefs } = useCool();
    const { onRefresh, onData } = usePager();
    // ========== 静态图标路径(4 个按钮图标) ==========
    // 实际项目中放到 /static/recycle/ 目录下
    const icons = {
        qrcode: "/static/recycle/qrcode.png",
        record: "/static/recycle/record.png",
        appointment: "/static/recycle/appointment.png",
        transfer: "/static/recycle/transfer.png",
    };
    // ========== 数据 ==========
    const dashboard = ref({
        carbonPoints: 0,
        recycleCount: 0,
        overPercent: 0,
    });
    const showQrcode = ref(false);
    const userCode = ref("");
    const qrcodeUrl = ref("");
    // ========== 初始化加载 ==========
    onMounted(() => {
        loadDashboard();
    });
    /** 加载首页数据看板 */
    async function loadDashboard() {
        try {
            const unionid = user.info?.phone||"18918594752";
            service.user.info.dashboard({ unionid:unionid  }).then((res) => {
                console.log("【再生资源看板数据】", res);
                dashboard.value = {
                    carbonPoints: res.carbonPoints ?? 0,
                    recycleCount: res.recycleCount ?? 0,
                    overPercent: res.overPercent ?? 0,
                };
            });
        } catch (e) {
            console.error("【再生资源看板加载失败】", e);
        }
    }
    /** 加载用户回收码 */
    async function loadMyCode() {
        try {
            const phone = user.info?.phone ?? "18918594752";
            const res = await generateRecycleQrcode(phone);
            userCode.value = phone;
            qrcodeUrl.value = res;
        } catch (e) {
            console.error("【回收码加载失败】", e);
            // 兜底:没有接口时给个默认值
            userCode.value = "RC-LOADING";
        }
    }
    // ========== 按钮事件 ==========
    /** ① 打开二维码 */
    function openQrcode() {
        loadMyCode();
        showQrcode.value = true;
    }
    /** ② 跳转回收记录 */
    function goRecord() {
        router.push("/pages/recycle/record");
    }
    /** ③ 跳转我要预约 */
    function goAppointment() {
        router.push("/pages/recycle/appointment");
    }
    /** ④ 跳转积分转赠 */
    function goTransfer() {
        router.push("/pages/recycle/transfer");
    }
</script>
<style lang="scss" scoped>
    .recycle-home {
        padding: 0 24rpx;
        /* ========== 数据看板 ========== */
        .recycle-dashboard {
            position: relative;
            border-radius: 24rpx;
            overflow: hidden;
            margin-bottom: 30rpx;
            box-shadow: 0 8rpx 24rpx rgba(46, 204, 113, 0.2);
            .dashboard-bg {
                position: absolute;
                inset: 0;
                background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
                z-index: 0;
            }
            .dashboard-inner {
                position: relative;
                z-index: 1;
                display: flex;
                align-items: center;
                justify-content: space-around;
                padding: 36rpx 20rpx;
            }
            .dash-item {
                display: flex;
                flex-direction: column;
                align-items: center;
                flex: 1;
            }
            .dash-num {
                font-size: 44rpx;
                font-weight: 700;
                color: #ffffff;
                line-height: 1.2;
            }
            .dash-highlight {
                color: #ffd700;
                text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
            }
            .dash-label {
                font-size: 22rpx;
                color: rgba(255, 255, 255, 0.85);
                margin-top: 8rpx;
            }
            .dash-divider {
                width: 2rpx;
                height: 50rpx;
                background-color: rgba(255, 255, 255, 0.3);
            }
        }
        /* ========== 4 个功能按钮 ========== */
        .recycle-actions {
            display: flex;
            justify-content: space-around;
            align-items: center;
            background-color: #fff;
            border-radius: 20rpx;
            padding: 28rpx 10rpx;
            margin-bottom: 20rpx;
            box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
        }
        .action-item {
            display: flex;
            flex-direction: column;
            align-items: center;
            transition: transform 0.15s ease;
            &:active {
                transform: scale(0.92);
            }
        }
        .action-icon {
            width: 96rpx;
            height: 96rpx;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            margin-bottom: 14rpx;
            .icon-img {
                width: 52rpx;
                height: 52rpx;
            }
            &.icon-green {
                background: linear-gradient(135deg, #e8f8f0, #d5f5e3);
            }
            &.icon-blue {
                background: linear-gradient(135deg, #eaf4fc, #d6eaf8);
            }
            &.icon-orange {
                background: linear-gradient(135deg, #fef5e7, #fdebd0);
            }
            &.icon-purple {
                background: linear-gradient(135deg, #f5eef8, #ebdef0);
            }
        }
        .action-text {
            font-size: 24rpx;
            color: #333;
            font-weight: 500;
        }
        /* ========== 二维码弹层 ========== */
        .qrcode-popup {
            display: flex;
            align-items: center;
            justify-content: center;
            width: 100%;
        }
        .qrcode-card {
            position: relative;
            width: 600rpx;
            background: #fff;
            border-radius: 28rpx;
            padding: 50rpx 40rpx 40rpx;
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        .qrcode-title {
            font-size: 34rpx;
            font-weight: 700;
            color: #333;
            margin-bottom: 10rpx;
        }
        .qrcode-desc {
            font-size: 24rpx;
            color: #999;
            margin-bottom: 36rpx;
        }
        .qrcode-image-wrap {
            width: 480rpx;
            height: 480rpx;
            border-radius: 16rpx;
            overflow: hidden;
            background: #f7f7f7;
            display: flex;
            align-items: center;
            justify-content: center;
            margin-bottom: 28rpx;
        }
        .qrcode-image {
            width: 100%;
            height: 100%;
        }
        .qrcode-code {
            font-size: 26rpx;
            color: #666;
            letter-spacing: 2rpx;
        }
        .qrcode-close {
            position: absolute;
            top: 20rpx;
            right: 20rpx;
            width: 60rpx;
            height: 60rpx;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 50%;
            background: #f5f5f5;
        }
    }
</style>
pages/recycle/home.vue
New file
@@ -0,0 +1,373 @@
<!--
    再生资源回收 - 首页功能模块组件
    路径建议:/components/recycle-home.vue
    使用方式:在首页 <view class="hot"> 再生资源回收 </view> 区域直接引入
-->
<template>
    <view class="recycle-home">
        <!-- ===== 第一行:数据看板 ===== -->
        <view class="recycle-dashboard">
            <view class="dashboard-bg"></view>
            <view class="dashboard-inner">
                <view class="dash-item">
                    <text class="dash-num">{{ dashboard.carbonPoints ?? 0 }}</text>
                    <text class="dash-label">碳积分</text>
                </view>
                <view class="dash-divider"></view>
                <view class="dash-item">
                    <text class="dash-num">{{ dashboard.recycleCount ?? 0 }}</text>
                    <text class="dash-label">回收次数</text>
                </view>
                <view class="dash-divider"></view>
                <view class="dash-item">
                    <text class="dash-num dash-highlight">
                        {{ dashboard.overPercent ?? 0 }}%
                    </text>
                    <text class="dash-label">积分排名</text>
                </view>
            </view>
        </view>
        <!-- ===== 第二行:4 个功能按钮 ===== -->
        <view class="recycle-actions">
            <!-- ① 我的回收码 -->
            <view class="action-item" @tap="openQrcode">
                <view class="action-icon icon-green">
                    <image class="icon-img" :src="icons.qrcode" mode="aspectFit" />
                </view>
                <text class="action-text">我的回收码</text>
            </view>
            <!-- ② 回收记录 -->
            <view class="action-item" @tap="goRecord">
                <view class="action-icon icon-blue">
                    <image class="icon-img" :src="icons.record" mode="aspectFit" />
                </view>
                <text class="action-text">回收记录</text>
            </view>
            <!-- ③ 我要预约 -->
            <view class="action-item" @tap="goAppointment">
                <view class="action-icon icon-orange">
                    <image class="icon-img" :src="icons.appointment" mode="aspectFit" />
                </view>
                <text class="action-text">我要预约</text>
            </view>
            <!-- ④ 积分转赠 -->
            <view class="action-item" @tap="goTransfer">
                <view class="action-icon icon-purple">
                    <image class="icon-img" :src="icons.transfer" mode="aspectFit" />
                </view>
                <text class="action-text">积分转赠</text>
            </view>
        </view>
        <!-- ===== 二维码弹层 ===== -->
        <cl-popup v-model="showQrcode" direction="center" :close-on-click-overlay="true" background-color="transparent"
            @close="showQrcode = false">
            <view class="qrcode-popup" @tap.stop>
                <view class="qrcode-card">
                    <text class="qrcode-title">我的回收码</text>
                    <text class="qrcode-desc">向回收员出示此码完成回收</text>
                    <view class="qrcode-image-wrap">
                        <!--<image class="qrcode-image" :src="qrcodeUrl" mode="aspectFit" /> -->
                        <canvas
                            canvas-id="recycleQrcode"
                            id="recycleQrcode"
                            style="width: 300px; height: 300px;"
                        />
                    </view>
                    <text class="qrcode-code">回收码:{{ userCode }}</text>
                    <view class="qrcode-close" @tap="showQrcode = false">
                        <cl-icon name="close" :size="36" color="#999" />
                    </view>
                </view>
            </view>
        </cl-popup>
    </view>
</template>
<script lang="ts" setup>
    import { ref, onMounted } from "vue";
    import { router, useCool, useStore, usePager } from "/@/cool";
    import { onReady, onShareAppMessage } from "@dcloudio/uni-app";
    import { generateRecycleQrcode } from "/@/cool/utils/qrcode";
    const { user } = useStore();
    const { service, refs, setRefs } = useCool();
    const { onRefresh, onData } = usePager();
    // ========== 静态图标路径(4 个按钮图标) ==========
    // 实际项目中放到 /static/recycle/ 目录下
    const icons = {
        qrcode: "/static/recycle/qrcode.png",
        record: "/static/recycle/record.png",
        appointment: "/static/recycle/appointment.png",
        transfer: "/static/recycle/transfer.png",
    };
    // ========== 数据 ==========
    const dashboard = ref({
        carbonPoints: 0,
        recycleCount: 0,
        overPercent: 0,
    });
    const showQrcode = ref(false);
    const userCode = ref("");
    const qrcodeUrl = ref("");
    // ========== 初始化加载 ==========
    onMounted(() => {
        loadDashboard();
    });
    /** 加载首页数据看板 */
    async function loadDashboard() {
        try {
            const unionid = user.info?.phone||"18918594752";
            service.user.info.dashboard({ unionid:unionid  }).then((res) => {
                console.log("【再生资源看板数据】", res);
                dashboard.value = {
                    carbonPoints: res.carbonPoints ?? 0,
                    recycleCount: res.recycleCount ?? 0,
                    overPercent: res.overPercent ?? 0,
                };
            });
        } catch (e) {
            console.error("【再生资源看板加载失败】", e);
        }
    }
    /** 加载用户回收码 */
    async function loadMyCode() {
        try {
            const phone = user.info?.phone ?? "18918594752";
            const res = await generateRecycleQrcode(phone);
            console.log("【我的回收码】", res);
            userCode.value = phone;
            qrcodeUrl.value = res;
        } catch (e) {
            console.error("【回收码加载失败】", e);
            // 兜底:没有接口时给个默认值
            userCode.value = "RC-LOADING";
        }
    }
    // ========== 按钮事件 ==========
    /** ① 打开二维码 */
    function openQrcode() {
        loadMyCode();
        showQrcode.value = true;
    }
    /** ② 跳转回收记录 */
    function goRecord() {
        router.push("/pages/recycle/record");
    }
    /** ③ 跳转我要预约 */
    function goAppointment() {
        router.push("/pages/recycle/appointment");
    }
    /** ④ 跳转积分转赠 */
    function goTransfer() {
        router.push("/pages/recycle/transfer");
    }
</script>
<style lang="scss" scoped>
    .recycle-home {
        padding: 0 24rpx;
        /* ========== 数据看板 ========== */
        .recycle-dashboard {
            position: relative;
            border-radius: 24rpx;
            overflow: hidden;
            margin-bottom: 30rpx;
            box-shadow: 0 8rpx 24rpx rgba(46, 204, 113, 0.2);
            .dashboard-bg {
                position: absolute;
                inset: 0;
                background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
                z-index: 0;
            }
            .dashboard-inner {
                position: relative;
                z-index: 1;
                display: flex;
                align-items: center;
                justify-content: space-around;
                padding: 36rpx 20rpx;
            }
            .dash-item {
                display: flex;
                flex-direction: column;
                align-items: center;
                flex: 1;
            }
            .dash-num {
                font-size: 44rpx;
                font-weight: 700;
                color: #ffffff;
                line-height: 1.2;
            }
            .dash-highlight {
                color: #ffd700;
                text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
            }
            .dash-label {
                font-size: 22rpx;
                color: rgba(255, 255, 255, 0.85);
                margin-top: 8rpx;
            }
            .dash-divider {
                width: 2rpx;
                height: 50rpx;
                background-color: rgba(255, 255, 255, 0.3);
            }
        }
        /* ========== 4 个功能按钮 ========== */
        .recycle-actions {
            display: flex;
            justify-content: space-around;
            align-items: center;
            background-color: #fff;
            border-radius: 20rpx;
            padding: 28rpx 10rpx;
            margin-bottom: 20rpx;
            box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
        }
        .action-item {
            display: flex;
            flex-direction: column;
            align-items: center;
            transition: transform 0.15s ease;
            &:active {
                transform: scale(0.92);
            }
        }
        .action-icon {
            width: 96rpx;
            height: 96rpx;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            margin-bottom: 14rpx;
            .icon-img {
                width: 52rpx;
                height: 52rpx;
            }
            &.icon-green {
                background: linear-gradient(135deg, #e8f8f0, #d5f5e3);
            }
            &.icon-blue {
                background: linear-gradient(135deg, #eaf4fc, #d6eaf8);
            }
            &.icon-orange {
                background: linear-gradient(135deg, #fef5e7, #fdebd0);
            }
            &.icon-purple {
                background: linear-gradient(135deg, #f5eef8, #ebdef0);
            }
        }
        .action-text {
            font-size: 24rpx;
            color: #333;
            font-weight: 500;
        }
        /* ========== 二维码弹层 ========== */
        .qrcode-popup {
            display: flex;
            align-items: center;
            justify-content: center;
            width: 100%;
        }
        .qrcode-card {
            position: relative;
            width: 600rpx;
            background: #fff;
            border-radius: 28rpx;
            padding: 50rpx 40rpx 40rpx;
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        .qrcode-title {
            font-size: 34rpx;
            font-weight: 700;
            color: #333;
            margin-bottom: 10rpx;
        }
        .qrcode-desc {
            font-size: 24rpx;
            color: #999;
            margin-bottom: 36rpx;
        }
        .qrcode-image-wrap {
            width: 480rpx;
            height: 480rpx;
            border-radius: 16rpx;
            overflow: hidden;
            background: #f7f7f7;
            display: flex;
            align-items: center;
            justify-content: center;
            margin-bottom: 28rpx;
        }
        .qrcode-image {
            width: 100%;
            height: 100%;
        }
        .qrcode-code {
            font-size: 26rpx;
            color: #666;
            letter-spacing: 2rpx;
        }
        .qrcode-close {
            position: absolute;
            top: 20rpx;
            right: 20rpx;
            width: 60rpx;
            height: 60rpx;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 50%;
            background: #f5f5f5;
        }
    }
</style>
pages/recycle/record.vue
New file
@@ -0,0 +1,467 @@
<template>
    <cl-page background-color="#f7f7f7">
        <view class="record-page">
            <!-- 顶部 Tabs -->
            <cl-sticky>
                <view class="tab-bar">
                    <view
                        v-for="tab in tabs"
                        :key="tab.value"
                        class="tab-item"
                        :class="{ 'is-active': activeTab === tab.value }"
                        @tap="switchTab(tab.value)"
                    >
                        <text>{{ tab.label }}</text>
                        <view v-if="activeTab === tab.value" class="tab-line"></view>
                    </view>
                </view>
            </cl-sticky>
            <!-- 列表 -->
            <scroll-view
                class="record-scroll"
                scroll-y
                :lower-threshold="80"
                @scrolltolower="loadMore"
                :refresher-enabled="true"
                :refresher-triggered="refreshing"
                @refresherrefresh="onRefresh"
            >
                <view class="record-list">
                    <view
                        v-for="item in list"
                        :key="item.id"
                        class="record-card"
                        @tap="toggleDetail(item)"
                    >
                        <view class="card-header">
                            <text class="order-no">单号:{{ item.orderNo }}</text>
                            <text class="status" :class="`status-${item.status}`">
                                {{ item.status }}
                            </text>
                        </view>
                        <view class="card-body">
                            <view class="info-row">
                                <text class="info-label">回收类型</text>
                                <text class="info-value">{{ item.orderType || "-" }}</text>
                            </view>
                            <view class="info-row">
                                <text class="info-label">总重量</text>
                                <text class="info-value">{{ item.totalWeight ?? 0 }} kg</text>
                            </view>
                            <view class="info-row">
                                <text class="info-label">获得积分</text>
                                <text class="info-value text-green">+{{ item.totalCarbonPoint ?? 0 }}</text>
                            </view>
                            <view class="info-row">
                                <text class="info-label">回收时间</text>
                                <text class="info-value">{{ item.finishTime || item.createTime || "-" }}</text>
                            </view>
                        </view>
                        <view class="card-toggle">
                            <text>{{ isExpanded(item) ? "收起明细" : "查看明细" }}</text>
                            <cl-icon
                                :name="isExpanded(item) ? 'arrow-top' : 'arrow-bottom'"
                                :size="24"
                                color="#999"
                            />
                        </view>
                        <view v-if="isExpanded(item)" class="item-detail" @tap.stop>
                            <view v-if="detailLoading[item.orderNo]" class="detail-tip">
                                加载中...
                            </view>
                            <view
                                v-else-if="!(detailMap[item.orderNo] || []).length"
                                class="detail-tip"
                            >
                                暂无明细
                            </view>
                            <view v-else class="detail-list">
                                <view
                                    v-for="row in detailMap[item.orderNo]"
                                    :key="row.id"
                                    class="detail-row"
                                >
                                    <view class="detail-main">
                                        <text class="detail-name">{{ row.itemName || "-" }}</text>
                                        <text class="detail-category">{{ row.category || "" }}</text>
                                    </view>
                                    <view class="detail-nums">
                                        <text>{{ row.weight ?? 0 }} kg</text>
                                        <text class="text-green">+{{ row.carbonPoint ?? 0 }}</text>
                                    </view>
                                </view>
                            </view>
                        </view>
                    </view>
                </view>
                <cl-loadmore
                    v-if="list.length > 0"
                    :loading="loading"
                    :finish="finished"
                    :divider="false"
                />
                <cl-empty
                    v-if="!loading && list.length === 0"
                    :fixed="false"
                    text="暂无回收记录"
                />
            </scroll-view>
        </view>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { onReady, onPullDownRefresh } from "@dcloudio/uni-app";
import { useCool, useStore } from "/@/cool";
const { service } = useCool();
const { user } = useStore();
const tabs = [
    { label: "现场回收", value: "onsite" },
    { label: "预约回收", value: "appointment" },
] as const;
type TabValue = (typeof tabs)[number]["value"];
const activeTab = ref<TabValue>("onsite");
const list = ref<Eps.RecycleOrderEntity[]>([]);
const loading = ref(false);
const refreshing = ref(false);
const finished = ref(false);
const page = ref(1);
const size = 10;
const expandedOrderNo = ref("");
const detailMap = ref<Record<string, Eps.RecycleOrderItemEntity[]>>({});
const detailLoading = ref<Record<string, boolean>>({});
let reqSeq = 0;
function getUnionid() {
    return user.info?.phone || "18918594752";
}
/** 现场:appointmentId 为空;预约:appointmentId 不为空 */
function buildQuery() {
    const unionid = getUnionid();
    const isOnsite = activeTab.value === "onsite";
    return {
        page: page.value,
        size,
        campusUserId: unionid,
        unionid,
        orderType: isOnsite ? "现场" : "预约",
        // 供后端 pageQueryOp.where 判断 appointmentId 是否为空(标准 fieldEq 不会处理 IS NULL)
        appointmentIdIsNull: isOnsite,
    };
}
async function loadList(reset = false) {
    if (loading.value && !reset) return;
    if (!reset && finished.value) return;
    if (reset) {
        page.value = 1;
        finished.value = false;
        list.value = [];
        expandedOrderNo.value = "";
        detailMap.value = {};
        detailLoading.value = {};
    }
    const current = ++reqSeq;
    loading.value = true;
    try {
        const res = await service.shop.recycleorder.page(buildQuery());
        if (current !== reqSeq) return;
        console.log("【回收记录返回】", res);
        const newList = res?.list || [];
        list.value = page.value === 1 ? newList : [...list.value, ...newList];
        const total = res?.pagination?.total;
        finished.value =
            newList.length < size || (typeof total === "number" && list.value.length >= total);
        page.value++;
    } catch (e) {
        if (current !== reqSeq) return;
        console.error("【回收记录加载失败】", e);
    } finally {
        if (current === reqSeq) {
            loading.value = false;
            refreshing.value = false;
        }
    }
}
function isExpanded(item: Eps.RecycleOrderEntity) {
    return !!item.orderNo && expandedOrderNo.value === item.orderNo;
}
async function loadDetail(orderNo: string) {
    if (detailMap.value[orderNo] || detailLoading.value[orderNo]) return;
    detailLoading.value = { ...detailLoading.value, [orderNo]: true };
    try {
        const res = await service.shop.recycleorderitem.page({
            page: 1,
            size: 50,
            orderNo,
        });
        console.log("【回收明细返回】", orderNo, res);
        detailMap.value = {
            ...detailMap.value,
            [orderNo]: res?.list || [],
        };
    } catch (e) {
        console.error("【回收明细加载失败】", e);
        detailMap.value = { ...detailMap.value, [orderNo]: [] };
    } finally {
        detailLoading.value = { ...detailLoading.value, [orderNo]: false };
    }
}
function toggleDetail(item: Eps.RecycleOrderEntity) {
    const orderNo = item.orderNo;
    if (!orderNo) return;
    if (expandedOrderNo.value === orderNo) {
        expandedOrderNo.value = "";
        return;
    }
    expandedOrderNo.value = orderNo;
    loadDetail(orderNo);
}
function switchTab(val: TabValue) {
    if (activeTab.value === val) return;
    activeTab.value = val;
    loadList(true);
}
function loadMore() {
    if (!loading.value && !finished.value) {
        loadList(false);
    }
}
function onRefresh() {
    refreshing.value = true;
    loadList(true);
}
onReady(() => {
    loadList(true);
});
onPullDownRefresh(() => {
    loadList(true).finally(() => {
        uni.stopPullDownRefresh();
    });
});
</script>
<style lang="scss" scoped>
.record-page {
    height: 100vh;
    display: flex;
    flex-direction: column;
    .tab-bar {
        display: flex;
        background: #fff;
        padding: 0 40rpx;
        border-bottom: 1rpx solid #f0f0f0;
        .tab-item {
            position: relative;
            padding: 28rpx 30rpx;
            font-size: 28rpx;
            color: #666;
            transition: color 0.2s;
            &.is-active {
                color: #2ecc71;
                font-weight: 600;
            }
            .tab-line {
                position: absolute;
                bottom: 0;
                left: 50%;
                transform: translateX(-50%);
                width: 40rpx;
                height: 4rpx;
                background: #2ecc71;
                border-radius: 2rpx;
            }
        }
    }
    .record-scroll {
        flex: 1;
        height: 0;
        padding: 24rpx;
        box-sizing: border-box;
    }
    .record-list {
        display: flex;
        flex-direction: column;
        gap: 20rpx;
    }
    .record-card {
        background: #fff;
        border-radius: 20rpx;
        padding: 28rpx;
        box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
        .card-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding-bottom: 20rpx;
            border-bottom: 1rpx solid #f5f5f5;
        }
        .order-no {
            font-size: 26rpx;
            color: #333;
            font-weight: 500;
        }
        .status {
            font-size: 22rpx;
            padding: 6rpx 16rpx;
            border-radius: 20rpx;
            background: #f5f5f5;
            color: #999;
            &.status-待处理 {
                background: #fef5e7;
                color: #e67e22;
            }
            &.status-进行中 {
                background: #eaf4fc;
                color: #2980b9;
            }
            &.status-已完成 {
                background: #e8f8f0;
                color: #27ae60;
            }
            &.status-已取消 {
                background: #fef0f0;
                color: #e74c3c;
            }
        }
        .card-body {
            padding-top: 20rpx;
            display: flex;
            flex-direction: column;
            gap: 14rpx;
        }
        .info-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .info-label {
            font-size: 24rpx;
            color: #999;
        }
        .info-value {
            font-size: 26rpx;
            color: #333;
            font-weight: 500;
        }
        .text-green {
            color: #27ae60;
            font-weight: 600;
        }
        .card-toggle {
            margin-top: 16rpx;
            padding-top: 16rpx;
            border-top: 1rpx dashed #f0f0f0;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8rpx;
            font-size: 22rpx;
            color: #999;
        }
        .item-detail {
            margin-top: 16rpx;
            padding: 16rpx;
            background: #f8faf8;
            border-radius: 12rpx;
        }
        .detail-tip {
            text-align: center;
            font-size: 22rpx;
            color: #999;
            padding: 12rpx 0;
        }
        .detail-list {
            display: flex;
            flex-direction: column;
            gap: 12rpx;
        }
        .detail-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
            gap: 16rpx;
        }
        .detail-main {
            flex: 1;
            min-width: 0;
            display: flex;
            flex-direction: column;
            gap: 4rpx;
        }
        .detail-name {
            font-size: 24rpx;
            color: #333;
            font-weight: 500;
        }
        .detail-category {
            font-size: 20rpx;
            color: #999;
        }
        .detail-nums {
            display: flex;
            align-items: center;
            gap: 16rpx;
            font-size: 22rpx;
            color: #666;
            flex-shrink: 0;
        }
    }
}
</style>
pages/recycle/transfer.vue
New file
@@ -0,0 +1,505 @@
<template>
    <cl-page background-color="#f7f7f7">
        <view class="transfer-page">
            <cl-sticky>
                <view class="tab-bar">
                    <view class="tab-left">
                        <view
                            v-for="tab in tabs"
                            :key="tab.value"
                            class="tab-item"
                            :class="{ 'is-active': activeTab === tab.value }"
                            @tap="switchTab(tab.value)"
                        >
                            <text>{{ tab.label }}</text>
                            <view v-if="activeTab === tab.value" class="tab-line"></view>
                        </view>
                    </view>
                    <view class="tab-right">
                        <text class="balance-chip">余额 {{ carbonPoints }}</text>
                        <view class="add-btn" @tap="startTransfer">转赠他人</view>
                    </view>
                </view>
            </cl-sticky>
            <scroll-view
                class="record-scroll"
                scroll-y
                :lower-threshold="80"
                @scrolltolower="loadMore"
                :refresher-enabled="true"
                :refresher-triggered="refreshing"
                @refresherrefresh="onRefresh"
            >
                <view class="record-list">
                    <view v-for="item in list" :key="item.id" class="record-card">
                        <view class="card-header">
                            <text class="source">{{ item.sourceType || item.type }}</text>
                            <text class="amount" :class="item.type === '收入' ? 'is-in' : 'is-out'">
                                {{ item.type === '收入' ? '+' : '-' }}{{ item.amount ?? 0 }}
                            </text>
                        </view>
                        <view class="card-body">
                            <text class="remark">{{ item.remark || "-" }}</text>
                            <text class="time">{{ item.createTime || "-" }}</text>
                        </view>
                    </view>
                </view>
                <cl-loadmore
                    v-if="list.length > 0"
                    :loading="loading"
                    :finish="finished"
                    :divider="false"
                />
                <cl-empty
                    v-if="!loading && list.length === 0"
                    :fixed="false"
                    text="暂无记录"
                />
            </scroll-view>
        </view>
        <cl-popup
            v-model="showAmount"
            direction="center"
            :close-on-click-overlay="false"
            background-color="transparent"
        >
            <view class="amount-popup" @tap.stop>
                <text class="amount-title">确认转赠</text>
                <text class="amount-desc">转给:{{ targetName }}</text>
                <text class="amount-desc">当前积分:{{ carbonPoints }}</text>
                <input
                    class="amount-input"
                    type="digit"
                    v-model="transferAmount"
                    placeholder="请输入转赠积分"
                    placeholder-class="placeholder"
                />
                <view class="amount-btns">
                    <button class="btn-cancel" @tap="showAmount = false">取消</button>
                    <button class="btn-ok" :disabled="submitting" @tap="submitTransfer">
                        {{ submitting ? "提交中..." : "确认" }}
                    </button>
                </view>
            </view>
        </cl-popup>
    </cl-page>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue";
import { onReady, onPullDownRefresh } from "@dcloudio/uni-app";
import { useCool, useStore } from "/@/cool";
const { service } = useCool();
const { user } = useStore();
const tabs = [
    { label: "转赠", value: "gift" },
    { label: "收支", value: "io" },
] as const;
type TabValue = (typeof tabs)[number]["value"];
const activeTab = ref<TabValue>("gift");
const list = ref<Eps.RecycleTransactionEntity[]>([]);
const loading = ref(false);
const refreshing = ref(false);
const finished = ref(false);
const page = ref(1);
const size = 10;
const carbonPoints = ref(0);
const showAmount = ref(false);
const submitting = ref(false);
const transferAmount = ref("");
const targetUnionid = ref("");
const targetUser = ref<{ unionid?: string; nickName?: string; phone?: string }>({});
const targetName = computed(() => {
    return targetUser.value.nickName || targetUser.value.phone || targetUnionid.value || "-";
});
let reqSeq = 0;
function getUnionid() {
    return user.info?.phone || "18918594752";
}
async function loadBalance() {
    try {
        const unionid = getUnionid();
        const res = await service.user.info.dashboard({ unionid });
        carbonPoints.value = res.carbonPoints ?? 0;
    } catch (e) {
        console.error("【碳积分加载失败】", e);
    }
}
function buildQuery() {
    return {
        page: page.value,
        size,
        userId: getUnionid(),
        tab: activeTab.value,
    };
}
async function loadList(reset = false) {
    if (loading.value && !reset) return;
    if (!reset && finished.value) return;
    if (reset) {
        page.value = 1;
        finished.value = false;
        list.value = [];
    }
    const current = ++reqSeq;
    loading.value = true;
    try {
        const res = await service.shop.transaction.page(buildQuery());
        if (current !== reqSeq) return;
        console.log("【赠送记录返回】", res);
        const newList = res?.list || [];
        list.value = page.value === 1 ? newList : [...list.value, ...newList];
        const total = res?.pagination?.total;
        finished.value =
            newList.length < size || (typeof total === "number" && list.value.length >= total);
        page.value++;
    } catch (e) {
        if (current !== reqSeq) return;
        console.error("【赠送记录加载失败】", e);
    } finally {
        if (current === reqSeq) {
            loading.value = false;
            refreshing.value = false;
        }
    }
}
function switchTab(val: TabValue) {
    if (activeTab.value === val) return;
    activeTab.value = val;
    loadList(true);
}
function loadMore() {
    if (!loading.value && !finished.value) {
        loadList(false);
    }
}
function onRefresh() {
    refreshing.value = true;
    loadBalance();
    loadList(true);
}
function handleScanResult(code: string) {
    const toUnionid = String(code || "").trim();
    if (!toUnionid) {
        uni.showToast({ title: "未识别到回收码", icon: "none" });
        return;
    }
    if (toUnionid === getUnionid()) {
        uni.showToast({ title: "不能转赠给自己", icon: "none" });
        return;
    }
    service.shop.transaction
        .checkUser({ unionid: toUnionid })
        .then((res) => {
            targetUnionid.value = toUnionid;
            targetUser.value = res || { unionid: toUnionid };
            transferAmount.value = "";
            showAmount.value = true;
        })
        .catch((e) => {
            uni.showToast({
                title: e?.message || "请选择系统用户作为接受转赠对象",
                icon: "none",
            });
        });
}
function startTransfer() {
    loadBalance();
    // #ifdef APP-PLUS || MP-WEIXIN
    uni.scanCode({
        scanType: ["qrCode"],
        success(res) {
            console.log("【扫码结果】", res.result);
            handleScanResult(res.result);
        },
        fail() {
            uni.showToast({ title: "扫码取消", icon: "none" });
        },
    });
    // #endif
    // #ifdef H5
    uni.showModal({
        title: "模拟扫码",
        editable: true,
        placeholderText: "请输入对方回收码",
        success(res) {
            if (res.confirm && res.content) {
                handleScanResult(res.content);
            }
        },
    });
    // #endif
}
async function submitTransfer() {
    const amount = Number(transferAmount.value);
    if (!amount || amount <= 0) {
        uni.showToast({ title: "请输入转赠积分", icon: "none" });
        return;
    }
    if (amount > carbonPoints.value) {
        uni.showToast({ title: "碳积分不足", icon: "none" });
        return;
    }
    submitting.value = true;
    try {
        await service.shop.transaction.transfer({
            fromUnionid: getUnionid(),
            toUnionid: targetUnionid.value,
            amount,
        });
        uni.showToast({ title: "转赠成功", icon: "success" });
        showAmount.value = false;
        transferAmount.value = "";
        activeTab.value = "gift";
        await loadBalance();
        loadList(true);
    } catch (e: any) {
        console.error("【转赠失败】", e);
        uni.showToast({ title: e?.message || "转赠失败,请重试", icon: "none" });
    } finally {
        submitting.value = false;
    }
}
onReady(() => {
    loadBalance();
    loadList(true);
});
onPullDownRefresh(() => {
    loadBalance();
    loadList(true).finally(() => {
        uni.stopPullDownRefresh();
    });
});
</script>
<style lang="scss" scoped>
.transfer-page {
    height: 100vh;
    display: flex;
    flex-direction: column;
    .tab-bar {
        display: flex;
        align-items: center;
        justify-content: space-between;
        background: #fff;
        padding: 0 24rpx 0 16rpx;
        border-bottom: 1rpx solid #f0f0f0;
    }
    .tab-left {
        display: flex;
        align-items: center;
    }
    .tab-item {
        position: relative;
        padding: 28rpx 24rpx;
        font-size: 28rpx;
        color: #666;
        &.is-active {
            color: #2ecc71;
            font-weight: 600;
        }
        .tab-line {
            position: absolute;
            bottom: 0;
            left: 50%;
            transform: translateX(-50%);
            width: 40rpx;
            height: 4rpx;
            background: #2ecc71;
            border-radius: 2rpx;
        }
    }
    .tab-right {
        display: flex;
        align-items: center;
        gap: 12rpx;
    }
    .balance-chip {
        font-size: 22rpx;
        color: #27ae60;
        background: #e8f8f0;
        padding: 6rpx 14rpx;
        border-radius: 20rpx;
    }
    .add-btn {
        padding: 10rpx 22rpx;
        border-radius: 28rpx;
        background: #2ecc71;
        color: #fff;
        font-size: 24rpx;
        font-weight: 500;
    }
    .record-scroll {
        flex: 1;
        height: 0;
        padding: 24rpx;
        box-sizing: border-box;
    }
    .record-list {
        display: flex;
        flex-direction: column;
        gap: 20rpx;
    }
    .record-card {
        background: #fff;
        border-radius: 20rpx;
        padding: 28rpx;
        box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
    }
    .card-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding-bottom: 16rpx;
        border-bottom: 1rpx solid #f5f5f5;
    }
    .source {
        font-size: 26rpx;
        color: #333;
        font-weight: 500;
    }
    .amount {
        font-size: 32rpx;
        font-weight: 700;
        &.is-in {
            color: #27ae60;
        }
        &.is-out {
            color: #e74c3c;
        }
    }
    .card-body {
        padding-top: 16rpx;
        display: flex;
        flex-direction: column;
        gap: 8rpx;
    }
    .remark {
        font-size: 26rpx;
        color: #555;
        line-height: 1.4;
    }
    .time {
        font-size: 22rpx;
        color: #999;
    }
}
.amount-popup {
    width: 560rpx;
    background: #fff;
    border-radius: 24rpx;
    padding: 40rpx 36rpx;
    display: flex;
    flex-direction: column;
}
.amount-title {
    font-size: 32rpx;
    font-weight: 700;
    color: #333;
    text-align: center;
    margin-bottom: 24rpx;
}
.amount-desc {
    font-size: 26rpx;
    color: #666;
    margin-bottom: 12rpx;
}
.amount-input {
    height: 80rpx;
    margin: 16rpx 0 32rpx;
    padding: 0 24rpx;
    background: #f7f7f7;
    border-radius: 12rpx;
    font-size: 30rpx;
    color: #333;
}
.placeholder {
    color: #ccc;
}
.amount-btns {
    display: flex;
    gap: 20rpx;
    button {
        flex: 1;
        height: 70rpx;
        line-height: 70rpx;
        border-radius: 35rpx;
        font-size: 28rpx;
        border: none;
    }
    .btn-cancel {
        background: #f5f5f5;
        color: #666;
    }
    .btn-ok {
        background: linear-gradient(135deg, #2ecc71, #27ae60);
        color: #fff;
        font-weight: 600;
        &[disabled] {
            opacity: 0.6;
        }
    }
}
</style>
pages/user/about.vue
New file
@@ -0,0 +1,57 @@
<template>
    <cl-page>
        <view class="page-about">
            <view class="logo">
                <image src="/static/logo.png" />
            </view>
            <!-- 版本 -->
            <cl-version-about />
            <cl-list>
                <cl-list-item label="联系我们" />
            </cl-list>
            <cl-text
                block
                align="center"
                :size="24"
                color="info"
                :line-height="1.2"
                :margin="[80, 0, 0, 0]"
            >
                {{ app.info.desc }}
            </cl-text>
        </view>
    </cl-page>
</template>
<script lang="ts" setup>
import { onReady } from "@dcloudio/uni-app";
import { useApp } from "/@/cool";
const app = useApp();
onReady(() => {
    uni.setNavigationBarTitle({
        title: `关于${app.info.name}`,
    });
});
</script>
<style lang="scss" scoped>
.page-about {
    .logo {
        display: flex;
        justify-content: center;
        padding: 100rpx 0;
        image {
            height: 160rpx;
            width: 160rpx;
            border-radius: 24rpx;
            box-shadow: 0 25rpx 30rpx -25rpx #666666;
        }
    }
}
</style>
pages/user/address-edit.vue
New file
@@ -0,0 +1,151 @@
<template>
    <cl-page>
        <view class="page">
            <cl-form :ref="setRefs('form')" v-model="form" label-position="top">
                <view class="card">
                    <cl-form-item
                        label="联系人"
                        prop="contact"
                        :rules="{
                            required: true,
                            message: '联系人不能为空',
                        }"
                    >
                        <cl-input v-model="form.contact" placeholder="请填写联系人" />
                    </cl-form-item>
                    <cl-form-item
                        label="手机号码"
                        prop="phone"
                        :rules="{
                            required: true,
                            message: '手机号码不能为空',
                        }"
                    >
                        <cl-input
                            v-model="form.phone"
                            :maxlength="11"
                            placeholder="请填写手机号码"
                        />
                    </cl-form-item>
                    <cl-form-item
                        label="省市区"
                        prop="province"
                        required
                        :rules="{
                            required: true,
                            message: '省市区不能为空',
                        }"
                    >
                        <cl-select-city v-model="pca" @change="onPcaChange" />
                    </cl-form-item>
                    <cl-form-item
                        label="详细地址"
                        prop="address"
                        :rules="{
                            required: true,
                            message: '详细地址不能为空',
                        }"
                        :margin="[0, 0, 10, 0]"
                    >
                        <cl-textarea v-model="form.address" count placeholder="街道、楼牌号等" />
                    </cl-form-item>
                </view>
                <view class="card">
                    <cl-row type="flex" justify="space-between" :padding="[10, 0, 10, 0]">
                        <cl-row>
                            <cl-text :size="28" block :margin="[0, 0, 6, 0]">设置默认地址</cl-text>
                            <cl-text :size="22" color="info">提醒:下单会优先使用该地址</cl-text>
                        </cl-row>
                        <cl-switch v-model="form.isDefault" />
                    </cl-row>
                </view>
            </cl-form>
        </view>
        <cl-footer>
            <cl-button custom type="primary" @tap="save">保存</cl-button>
        </cl-footer>
    </cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useCool } from "/@/cool";
import { onReady } from "@dcloudio/uni-app";
import { useUi } from "/$/cool-ui";
const { service, router, refs, setRefs } = useCool();
const ui = useUi();
// 省市区
const pca = ref<string[]>([]);
// 表单
const form = ref<Eps.UserAddressEntity>({});
// 监听省市区改变
function onPcaChange([province, city, district]: string[]) {
    form.value.province = province;
    form.value.city = city;
    form.value.district = district;
}
// 获取地址详情
async function refresh() {
    ui.showLoading();
    await service.user.address
        .info({
            id: router.query.id,
        })
        .then((res) => {
            form.value = res;
            pca.value = [res.province!, res.city!, res.district!];
        })
        .catch((err) => {
            ui.showToast(err.message);
        });
    ui.hideLoading();
}
// 保存
function save() {
    refs.form.validate((valid: boolean) => {
        if (valid) {
            service.user.address[form.value.id ? "update" : "add"](form.value)
                .then(() => {
                    ui.showTips("地址保存成功", () => {
                        router.back();
                    });
                })
                .catch((err) => {
                    ui.showToast(err.message);
                });
        }
    });
}
onReady(() => {
    const { id } = router.query;
    if (id) {
        refresh();
    }
    uni.setNavigationBarTitle({
        title: id ? "编辑地址" : "添加地址",
    });
});
</script>
<style lang="scss" scoped>
.page {
    padding: 24rpx;
}
</style>
pages/user/address-list.vue
New file
@@ -0,0 +1,116 @@
<template>
    <cl-page>
        <view class="page">
            <view class="list">
                <view class="item" v-for="(item, index) in list" :key="item.id">
                    <cl-list-item swipe="right" :arrow-icon="false" :radius="16">
                        <template #menu>
                            <cl-button type="error" @tap="del(item.id!, index)">删除</cl-button>
                        </template>
                        <address-item :item="item">
                            <template #icon>
                                <cl-icon name="edit" :size="34" @tap="edit(item.id)" />
                            </template>
                        </address-item>
                    </cl-list-item>
                </view>
            </view>
            <cl-empty icon="address" v-if="isEmpty(list)" />
        </view>
        <cl-footer>
            <cl-button custom type="primary" @tap="edit()">添加收货地址</cl-button>
        </cl-footer>
    </cl-page>
</template>
<script lang="ts" setup>
import { onReady, onShow } from "@dcloudio/uni-app";
import { useCool, usePager } from "/@/cool";
import { isEmpty } from "lodash-es";
import { useUi } from "/$/cool-ui";
import AddressItem from "/@/components/address/item.vue";
const { service, router } = useCool();
const { list, onRefresh } = usePager<Eps.UserAddressEntity>();
const ui = useUi();
// 刷新
function refresh(params?: any, loading?: boolean) {
    const { data, next } = onRefresh(params, { loading });
    next(service.user.address.page(data));
}
// 添加、编辑
function edit(id?: number) {
    router.push({
        path: "/pages/user/address-edit",
        query: {
            id,
        },
    });
}
// 删除
function del(id: number, index: number) {
    ui.showConfirm({
        title: "提示",
        message: "确定删除该地址吗?",
        callback(action) {
            if (action == "confirm") {
                service.user.address
                    .delete({
                        ids: [id],
                    })
                    .then(() => {
                        list.value.splice(index, 1);
                        ui.showToast("删除成功");
                    })
                    .catch((err) => {
                        ui.showToast(err.message);
                    });
            }
        },
    });
}
onShow(() => {
    if (ui.loaded) {
        refresh({}, false);
    }
});
onReady(() => {
    refresh();
});
defineExpose({
    refresh,
});
</script>
<style lang="scss" scoped>
.page {
    padding: 24rpx;
    .list {
        .item {
            margin-bottom: 24rpx;
            address-item {
                flex: 1;
            }
            :deep(.address-item) {
                padding: 14rpx 4rpx;
            }
            &:last-child {
                margin-bottom: 0;
            }
        }
    }
}
</style>
pages/user/captcha.vue
New file
@@ -0,0 +1,132 @@
<template>
    <cl-page background-color="#fff">
        <view class="page-captcha">
            <cl-topbar :border="false"> </cl-topbar>
            <view class="container">
                <text class="label">输入验证码</text>
                <text class="tips">已发送至 +86 {{ form.phone }}</text>
                <view class="code">
                    <cl-captcha
                        focus
                        v-model="form.smsCode"
                        :length="len"
                        :gutter="26"
                        @done="next"
                    />
                </view>
                <cl-button
                    type="primary"
                    :disabled="form.smsCode.length !== len"
                    :loading="saving"
                    fill
                    :height="90"
                    :font-size="30"
                    @tap="next"
                >
                    确定
                </cl-button>
                <view class="send">
                    <sms-btn
                        size="small"
                        :border="false"
                        :phone="form.phone"
                        :ref="setRefs('smsBtn')"
                    />
                </view>
            </view>
        </view>
    </cl-page>
</template>
<script lang="ts" setup>
import { onReady } from "@dcloudio/uni-app";
import { reactive, ref } from "vue";
import { useCool, useStore } from "/@/cool";
import { useUi } from "/$/cool-ui";
import SmsBtn from "/@/components/sms-btn.vue";
const { service, router, refs, setRefs } = useCool();
const { user } = useStore();
const ui = useUi();
// 验证码长度
const len = 4;
// 保存状态
const saving = ref(false);
// 表单
const form = reactive({
    smsCode: "",
    phone: "",
});
// 下一步
function next() {
    saving.value = true;
    service.user.login
        .phone(form)
        .then(async (res) => {
            // 设置token
            user.setToken(res);
            // 设置用户信息
            await user.get();
            // 登录跳转
            router.nextLogin();
        })
        .catch((err) => {
            ui.showTips(err.message || "登录失效,请重试~");
            saving.value = false;
            form.smsCode = "";
        });
}
onReady(() => {
    form.phone = router.query.phone || "";
    refs.smsBtn.startCountdown();
});
</script>
<style lang="scss" scoped>
.page-captcha {
    .container {
        display: flex;
        flex-direction: column;
        width: 80%;
        margin: 0 auto;
        padding-top: 140rpx;
    }
    .label {
        font-size: 52rpx;
        font-weight: 500;
        margin-bottom: 44rpx;
        font-weight: bold;
        color: #151515;
    }
    .tips {
        font-size: 28rpx;
        color: #151515;
        font-weight: 500;
    }
    .code {
        margin: 34rpx -26rpx 62rpx -26rpx;
    }
    .send {
        display: flex;
        justify-content: center;
        font-size: 24rpx;
        margin-top: 30rpx;
    }
}
</style>
pages/user/doc.vue
New file
@@ -0,0 +1,37 @@
<template>
    <view class="doc mp-html">
        <mp-html :content="content"></mp-html>
    </view>
</template>
<script setup lang="ts">
import { onReady } from "@dcloudio/uni-app";
import { ref } from "vue";
import { useCool } from "/@/cool";
const { router, service } = useCool();
const content = ref("");
onReady(() => {
    const { title, key } = router.query;
    uni.setNavigationBarTitle({
        title,
    });
    service.base.comm
        .param({
            key,
        })
        .then((res) => {
            content.value = res;
        });
});
</script>
<style lang="scss" scoped>
.doc {
    padding: 20rpx;
}
</style>
pages/user/edit.vue
New file
@@ -0,0 +1,67 @@
<template>
    <cl-page>
        <view class="page">
            <view class="form">
                <cl-form label-position="top">
                    <cl-form-item label="昵称">
                        <cl-input
                            v-model="form.nickName"
                            type="nickname"
                            :border="false"
                            :height="80"
                            :border-radius="12"
                            placeholder="请填写昵称"
                        />
                    </cl-form-item>
                </cl-form>
            </view>
            <cl-footer>
                <cl-button custom type="primary" :loading="loading" @tap="save"> 保存 </cl-button>
            </cl-footer>
        </view>
    </cl-page>
</template>
<script lang="ts" setup>
import { reactive, ref } from "vue";
import { useCool, useStore } from "/@/cool";
import { useUi } from "/$/cool-ui";
import { onReady } from "@dcloudio/uni-app";
const { router } = useCool();
const { user } = useStore();
const ui = useUi();
const loading = ref(false);
const form = reactive({
    nickName: "",
});
async function save() {
    loading.value = true;
    await user.update(form).catch((err) => {
        ui.showToast(err.message);
    });
    loading.value = false;
    ui.showTips("用户信息保存成功", () => {
        router.back();
    });
}
onReady(() => {
    form.nickName = user.info?.nickName || "";
});
</script>
<style lang="scss" scoped>
.page {
    .form {
        padding: 20rpx 24rpx;
    }
}
</style>
pages/user/login.vue
New file
@@ -0,0 +1,473 @@
<template>
    <cl-page background-color="#fff">
        <cl-topbar :border="false" background-color="transparent" />
        <view class="page-login">
            <!-- Logo -->
            <view class="logo">
                <image src="/static/logo.png" mode="aspectFill" />
                <text>{{ app.info.name }}</text>
            </view>
            <div class="container">
                <!-- 登录方式 -->
                <view class="mode" :class="[`is-${mode}`]">
                    <!-- 手机号 -->
                    <template v-if="mode == 'phone'">
                        <text class="label">手机号登录</text>
                        <view class="phone">
                            <text>+86</text>
                            <cl-input
                                v-model="phone"
                                type="number"
                                placeholder="请填写手机号码"
                                :border="false"
                                :maxlength="11"
                                :font-size="30"
                                background-color="transparent"
                            />
                        </view>
                        <sms-btn
                            :ref="setRefs('smsBtn')"
                            :phone="phone"
                            @success="phoneLogin(false)"
                        >
                            <template #default="{ disabled, btnText }">
                                <cl-button
                                    fill
                                    type="primary"
                                    :height="90"
                                    :font-size="30"
                                    :disabled="disabled"
                                    @tap="phoneLogin"
                                >
                                    {{ btnText }}
                                </cl-button>
                            </template>
                        </sms-btn>
                    </template>
                    <!-- 微信登录 -->
                    <template v-else-if="mode == 'wx'">
                        <cl-button
                            type="primary"
                            fill
                            :height="90"
                            :font-size="30"
                            :loading="loading"
                            @tap="wxLogin"
                        >
                            微信一键登录
                        </cl-button>
                    </template>
                    <!-- 协议 -->
                    <view class="agree">
                        <agree-btn :ref="setRefs('agreeBtn')" />
                    </view>
                </view>
            </div>
            <!-- 其他登录方式 -->
            <view class="other" v-if="!isEmpty(platformsEnv)">
                <cl-divider width="400rpx" background-color="#ffffff">
                    <cl-text color="#ccc" value="其他登录方式" />
                </cl-divider>
                <view class="platform">
                    <view
                        class="platform__item"
                        v-for="(item, index) in platformsEnv"
                        :key="index"
                        @tap="changeMode(item)"
                    >
                        <image :src="item.icon" mode="aspectFit" v-if="item.icon" />
                        <text>{{ item.label }}</text>
                    </view>
                </view>
            </view>
        </view>
    </cl-page>
</template>
<script lang="ts" setup>
import { computed, reactive, ref } from "vue";
import { onReady } from "@dcloudio/uni-app";
import { useApp, useCool, useStore, useWx } from "/@/cool";
import { useUi } from "/$/cool-ui";
import SmsBtn from "/@/components/sms-btn.vue";
import AgreeBtn from "/@/components/agree-btn.vue";
import { config } from "/@/config";
import { ctx } from "virtual:ctx";
import { cloneDeep, isEmpty } from "lodash-es";
interface Platform {
    label: string;
    value: any;
    icon?: string;
    hidden?: boolean;
    onClick?: () => void;
}
const { service, router, refs, setRefs, storage } = useCool();
const { user } = useStore();
const app = useApp();
const ui = useUi();
const wx = useWx();
// 登录中
const loading = ref(false);
// 手机号
const phone = ref(storage.get("phone") || "");
// 登录方式
const mode = ref();
// 登录平台
const platforms = ref<Platform[]>([
    {
        label: "通过手机登录",
        value: "phone",
        icon: "/pages/user/static/icon/phone.png",
        hidden: false,
    },
    {
        label: "通过微信登录",
        value: "wx",
        icon: "/pages/user/static/icon/wx.png",
        hidden: true,
    },
]);
// 环境校验
const platformsEnv = computed(() => {
    let arr = cloneDeep(platforms.value);
    // #ifdef H5
    if (wx.isWxBrowser()) {
        arr[1].hidden = false;
    }
    // #endif
    // #ifdef MP-WEIXIN
    arr[1].hidden = false;
    // #endif
    // 过滤隐藏的
    arr = arr.filter((e) => !e.hidden);
    // 默认第一个登录方式
    if (!mode.value) {
        mode.value = arr[0]?.value;
    }
    // 过滤不是当前登录方式
    return arr.filter((e) => e.value != mode.value);
});
// 切换模式
function changeMode(item: Platform) {
    if (item.onClick) {
        item.onClick();
    } else {
        mode.value = item.value;
    }
}
// 登录请求
async function nextLogin(key: "mini" | "mp" | "uniPhone", data: any) {
    return service.user.login[key](data)
        .then(async (res) => {
            // 设置token
            user.setToken(res);
            // 获取用户信息
            await user.get();
            // 登录跳转
            router.nextLogin(key);
        })
        .catch((err) => {
            ui.showTips(err.message);
        });
}
// 短信登录
function phoneLogin(sms?: boolean) {
    if (sms) {
        check(() => {
            refs.smsBtn.open();
        });
    } else {
        storage.set("phone", phone.value);
        router.push({
            path: "/pages/user/captcha",
            query: {
                phone: phone.value,
            },
        });
    }
}
// 微信登录
function wxLogin() {
    check(async () => {
        // #ifdef APP
        if (wx.hasApp()) {
            wx.appLogin().then((code) => {
                //
            });
        } else {
            ui.showConfirm({
                title: "温馨提示",
                message: "您还未安装微信~",
                showCancelButton: false,
                confirmButtonText: "去下载",
                callback(action) {
                    if (action == "confirm") {
                        wx.downloadApp();
                    }
                },
            });
        }
        // #endif
        // #ifdef MP-WEIXIN
        loading.value = true;
        await wx
            .miniLogin()
            .then(async (res) => {
                await nextLogin("mini", res);
            })
            .catch((err) => {
                ui.showToast(err.message);
            });
        loading.value = false;
        // #endif
        // #ifdef H5
        wx.mpAuth();
        // #endif
    });
}
// 公众号登录
function mpLogin() {
    // #ifdef H5
    wx.mpLogin().then(async (code) => {
        if (code) {
            ui.showLoading();
            await nextLogin("mp", { code });
            ui.hideLoading();
        }
    });
    // #endif
}
// 协议检测
function check(cb: () => void) {
    if (refs.agreeBtn.check()) {
        cb();
    }
}
// 手机号一键登录
const univerify = reactive({
    error: "",
    check() {
        // #ifdef APP
        uni.preLogin({
            provider: "univerify",
            success() {
                platforms.value.push({
                    label: "手机号一键登录",
                    value: "univerify",
                    onClick() {
                        univerify.login();
                    },
                });
            },
            fail(err) {
                univerify.error = err.metadata?.msg || "当前环境不支持一键登录,请切换至验证码登录";
            },
        });
        // #endif
    },
    login() {
        uni.login({
            provider: "univerify",
            univerifyStyle: {
                authButton: {
                    normalColor: "#6b69f8",
                    highlightColor: "#6b69f8",
                    disabledColor: "#73aaf5",
                    textColor: "#ffffff",
                    title: "一键登录",
                    borderRadius: "12px",
                },
                privacyTerms: {
                    defaultCheckBoxState: true, // 条款勾选框初始状态
                    textColor: "#BBBBBB",
                    termsColor: "#5496E3",
                    prefix: "我已阅读并同意",
                    suffix: "并使用本机号码登录",
                    privacyItems: [
                        // 自定义协议条款,最大支持2个,需要同时设置url和title. 否则不生效
                        {
                            url: `${config.baseUrl}/app/base/comm/html?key=userAgreement`,
                            title: "用户协议",
                        },
                        {
                            url: `${config.baseUrl}/app/base/comm/html?key=privacyPolicy`,
                            title: "隐私政策",
                        },
                    ],
                },
            },
            async success(res: { authResult: any }) {
                await nextLogin("uniPhone", {
                    appId: ctx.appid,
                    ...res.authResult,
                });
                uni.closeAuthView();
            },
            fail() {
                if (univerify.error) {
                    ui.showToast(univerify.error);
                }
            },
        });
    },
});
// 手机号一键登录环境检测
univerify.check();
onReady(() => {
    // 公众号登录授权回调
    mpLogin();
});
</script>
<style lang="scss" scoped>
.page-login {
    .logo {
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        padding-top: 20%;
        image {
            display: block;
            height: 150rpx;
            width: 150rpx;
            border-radius: 24rpx;
            margin-bottom: 30rpx;
            box-shadow: 0 25rpx 30rpx -25rpx #666666;
        }
        text {
            font-size: 36rpx;
            font-weight: bold;
            letter-spacing: 1rpx;
            color: #333;
        }
    }
    .container {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        margin-top: 80rpx;
        .mode {
            width: 100%;
            padding: 0 80rpx;
            box-sizing: border-box;
            .label {
                display: block;
                font-size: 36rpx;
                font-weight: 500;
                margin-bottom: 30rpx;
            }
            &.is-phone {
                .phone {
                    display: flex;
                    align-items: center;
                    background-color: #eeeeee;
                    border-radius: 10rpx;
                    height: 90rpx;
                    margin-bottom: 30rpx;
                    font-size: 30rpx;
                    text {
                        display: inline-block;
                        padding: 0 40rpx;
                        border-right: $cl-border-width solid $cl-border-color;
                        font-weight: bold;
                        color: #404040;
                    }
                    input {
                        height: 100%;
                        flex: 1;
                        padding: 0 30rpx;
                    }
                }
            }
        }
        .agree {
            text-align: center;
            margin: 50rpx -60rpx 0 0;
        }
    }
    .other {
        margin-top: 100rpx;
        .platform {
            display: flex;
            flex-direction: column;
            align-items: center;
            margin: 20rpx 0 60rpx 0;
            &__item {
                display: flex;
                align-items: center;
                justify-content: center;
                border: 1px solid #000000;
                height: 30px;
                width: 130px;
                margin-bottom: 28rpx;
                border-radius: 6px;
                background-color: #ffffff;
                image {
                    height: 32rpx;
                    width: 32rpx;
                    margin-right: 10rpx;
                }
                text {
                    font-size: 24rpx;
                    color: #000000;
                }
            }
        }
    }
}
</style>
pages/user/set.vue
New file
@@ -0,0 +1,154 @@
<template>
    <cl-page>
        <view class="page-set">
            <template v-if="user.info">
                <cl-text value="账号" :margin="[0, 0, 20, 20]" block />
                <cl-list :radius="16">
                    <cl-list-item label="头像" :arrow-icon="false">
                        <view class="avatar">
                            <!-- #ifdef MP-WEIXIN -->
                            <button open-type="chooseAvatar" @chooseavatar="uploadAvatar">
                                <cl-avatar round :size="88" :src="user.info.avatarUrl" />
                            </button>
                            <!-- #endif -->
                            <!-- #ifndef MP-WEIXIN -->
                            <cl-avatar
                                round
                                :size="88"
                                :src="user.info.avatarUrl"
                                @tap="uploadAvatar()"
                            />
                            <!-- #endif -->
                        </view>
                    </cl-list-item>
                    <cl-list-item label="昵称" @tap="router.push('/pages/user/edit')">
                        <cl-text :value="user.info.nickName" />
                    </cl-list-item>
                    <cl-list-item label="手机号" :arrow-icon="false">
                        <cl-text :value="user.info.phone" />
                    </cl-list-item>
                    <cl-list-item label="ID" :arrow-icon="false" :border="false">
                        <cl-text :value="user.info.id" />
                    </cl-list-item>
                </cl-list>
            </template>
            <cl-text value="关于" :margin="[30, 0, 20, 20]" block />
            <cl-list :radius="16">
                <cl-list-item
                    :label="`关于${app.info.name}`"
                    @tap="router.push('/pages/user/about')"
                />
                <cl-list-item
                    label="用户协议"
                    @tap="
                        router.push({
                            path: '/pages/user/doc',
                            query: {
                                key: 'userAgreement',
                                title: '用户协议',
                            },
                        })
                    "
                />
                <cl-list-item
                    label="隐私政策"
                    @tap="
                        router.push({
                            path: '/pages/user/doc',
                            query: {
                                key: 'privacyPolicy',
                                title: '隐私政策',
                            },
                        })
                    "
                />
            </cl-list>
            <cl-text value="反馈" :margin="[30, 0, 20, 20]" block />
            <cl-list :radius="16">
                <cl-list-item
                    label="意见反馈"
                    @tap="router.push('/uni_modules/cool-app/pages/feedback/list')"
                />
                <cl-list-item
                    label="投诉举报"
                    @tap="router.push('/uni_modules/cool-app/pages/complain/list')"
                />
            </cl-list>
            <cl-list :radius="16">
                <cl-list-item label="切换账号" @tap="router.push('/pages/user/login')" />
                <cl-list-item label="退出登录" :arrow-icon="false" @tap="user.logout()">
                    <cl-icon :size="36" name="exit" />
                </cl-list-item>
            </cl-list>
        </view>
    </cl-page>
</template>
<script lang="ts" setup>
import { onReady } from "@dcloudio/uni-app";
import { useApp, useCool, useStore } from "/@/cool";
import { useUi } from "/$/cool-ui";
const { router, upload } = useCool();
const { user } = useStore();
const ui = useUi();
const app = useApp();
// 上传头像
function uploadAvatar(e?: { detail: { avatarUrl: string } }) {
    function next(path: string) {
        upload({ path }).then((url) => {
            ui.showToast("头像更新成功");
            user.update({
                avatarUrl: url,
            });
        });
    }
    if (e) {
        next(e.detail.avatarUrl);
    } else {
        uni.chooseImage({
            count: 1,
            success(res) {
                next(res.tempFiles[0].path);
            },
        });
    }
}
onReady(() => {
    user.get();
});
</script>
<style lang="scss" scoped>
.page-set {
    padding: 20rpx 24rpx;
    .avatar {
        padding: 10rpx 0;
        height: 88rpx;
        button {
            padding: 0;
            margin: 0;
            line-height: normal;
            background-color: transparent;
            &::after {
                border: 0;
            }
        }
    }
}
</style>
pages/user/static/icon/apple.png
pages/user/static/icon/phone.png
pages/user/static/icon/wx.png
pnpm-lock.yaml
New file
@@ -0,0 +1,2829 @@
lockfileVersion: '9.0'
settings:
  autoInstallPeers: true
  excludeLinksFromLockfile: false
importers:
  .:
    dependencies:
      '@dcloudio/uni-app':
        specifier: 3.0.0-3081220230817001
        version: 3.0.0-3081220230817001(@dcloudio/types@3.4.14)(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))
      '@hyoga/uni-socket.io':
        specifier: ^3.0.4
        version: 3.0.4
      dayjs:
        specifier: ^1.11.13
        version: 1.11.13
      js-pinyin:
        specifier: ^0.2.5
        version: 0.2.7
      lodash-es:
        specifier: ^4.17.21
        version: 4.17.21
      md5:
        specifier: ^2.3.0
        version: 2.3.0
      pinia:
        specifier: ^2.1.7
        version: 2.3.1(typescript@5.5.4)(vue@3.5.13(typescript@5.5.4))
      vue:
        specifier: ^3.5.13
        version: 3.5.13(typescript@5.5.4)
      vue-i18n:
        specifier: 9.1.9
        version: 9.1.9(vue@3.5.13(typescript@5.5.4))
      weixin-js-sdk:
        specifier: ^1.6.5
        version: 1.6.5
    devDependencies:
      '@cool-vue/vite-plugin':
        specifier: ^8.0.3
        version: 8.0.3
      '@dcloudio/types':
        specifier: ^3.4.14
        version: 3.4.14
      '@types/lodash-es':
        specifier: ^4.17.12
        version: 4.17.12
      '@types/md5':
        specifier: ^2.3.2
        version: 2.3.5
      '@types/node':
        specifier: ^20.11.26
        version: 20.17.17
      '@vue/tsconfig':
        specifier: ^0.5.1
        version: 0.5.1
      typescript:
        specifier: ~5.5.4
        version: 5.5.4
      vite:
        specifier: ^5.4.14
        version: 5.4.14(@types/node@20.17.17)
packages:
  '@ampproject/remapping@2.3.0':
    resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
    engines: {node: '>=6.0.0'}
  '@babel/code-frame@7.26.2':
    resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==}
    engines: {node: '>=6.9.0'}
  '@babel/compat-data@7.26.8':
    resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==}
    engines: {node: '>=6.9.0'}
  '@babel/core@7.26.8':
    resolution: {integrity: sha512-l+lkXCHS6tQEc5oUpK28xBOZ6+HwaH7YwoYQbLFiYb4nS2/l1tKnZEtEWkD0GuiYdvArf9qBS0XlQGXzPMsNqQ==}
    engines: {node: '>=6.9.0'}
  '@babel/generator@7.26.8':
    resolution: {integrity: sha512-ef383X5++iZHWAXX0SXQR6ZyQhw/0KtTkrTz61WXRhFM6dhpHulO/RJz79L8S6ugZHJkOOkUrUdxgdF2YiPFnA==}
    engines: {node: '>=6.9.0'}
  '@babel/helper-compilation-targets@7.26.5':
    resolution: {integrity: sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==}
    engines: {node: '>=6.9.0'}
  '@babel/helper-module-imports@7.25.9':
    resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==}
    engines: {node: '>=6.9.0'}
  '@babel/helper-module-transforms@7.26.0':
    resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==}
    engines: {node: '>=6.9.0'}
    peerDependencies:
      '@babel/core': ^7.0.0
  '@babel/helper-string-parser@7.25.9':
    resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
    engines: {node: '>=6.9.0'}
  '@babel/helper-validator-identifier@7.25.9':
    resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
    engines: {node: '>=6.9.0'}
  '@babel/helper-validator-option@7.25.9':
    resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==}
    engines: {node: '>=6.9.0'}
  '@babel/helpers@7.26.7':
    resolution: {integrity: sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==}
    engines: {node: '>=6.9.0'}
  '@babel/parser@7.26.8':
    resolution: {integrity: sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==}
    engines: {node: '>=6.0.0'}
    hasBin: true
  '@babel/template@7.26.8':
    resolution: {integrity: sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==}
    engines: {node: '>=6.9.0'}
  '@babel/traverse@7.26.8':
    resolution: {integrity: sha512-nic9tRkjYH0oB2dzr/JoGIm+4Q6SuYeLEiIiZDwBscRMYFJ+tMAz98fuel9ZnbXViA2I0HVSSRRK8DW5fjXStA==}
    engines: {node: '>=6.9.0'}
  '@babel/types@7.26.8':
    resolution: {integrity: sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==}
    engines: {node: '>=6.9.0'}
  '@cool-vue/vite-plugin@8.0.3':
    resolution: {integrity: sha512-5vxH/EyPXOJvKClllhaTZUeWh9XRJZwZJxyRlHuK2nAuD+pQxTXZ85WB4QPociMrJcOC8uZ/97EdrS4yG2Jv3Q==}
  '@dcloudio/types@3.4.14':
    resolution: {integrity: sha512-VolQeZfTh8pQFsr2IlfIVX93blfvGTuBoJuZUc7iWOqtHV8gDrq6fXLhzsVlgZyuhhRZLOxlo33rkEqnY+ucAw==}
  '@dcloudio/uni-app@3.0.0-3081220230817001':
    resolution: {integrity: sha512-sf8PzXHSd/VFCn7X9+QbD4nrY21FnIwjrhyX2hFit7PZK9kpyQluqCmG7YuotuXJzsNML8c8u2O9q5FaeOyTHg==}
    peerDependencies:
      '@dcloudio/types': ^3.3.2
  '@dcloudio/uni-cli-shared@3.0.0-3081220230817001':
    resolution: {integrity: sha512-FOeydfIdKZg+MnQsHSFLXBzzmXtgCWnmEyjw3MHNN5mGwQB6+f7vK8sLw+yjizD/j71eMnh8XXCk+dV3nD/Vzg==}
    engines: {node: ^14.18.0 || >=16.0.0}
  '@dcloudio/uni-cloud@3.0.0-3081220230817001':
    resolution: {integrity: sha512-rrHN4PvvylrhmivAoSv4h8ZcQ3ZWHnMRXhQWPC9FBR1TInrrxrrAHuMqSZn6pTIXhCiRA3Kuy6aDNVCbt6VA+g==}
  '@dcloudio/uni-components@3.0.0-3081220230817001':
    resolution: {integrity: sha512-FlIrTgR/9Yp8FWYfhXfLPd0jJtBwJwRBAWWmLNaufkTojm6X7rX8wQOd13n+7gJGKzfXcEV8npOYpVou4P46Pw==}
  '@dcloudio/uni-h5-vite@3.0.0-3081220230817001':
    resolution: {integrity: sha512-x7F6mUTjHiOx7+lj5yIrzhD7gr0HTNJaku5gV3O8bFjxKLcIkFkAyliKPbqJ4QZCmCCmEfJpOGKrDhk35tlibg==}
  '@dcloudio/uni-h5-vue@3.0.0-3081220230817001':
    resolution: {integrity: sha512-q1eFoXb5/4whNIJ1CumKmeypPjZofw8xDQRHZeGZ7NVRQ0/MYnEm6A0n/vkmEo1IgTZIY83m/jr6RNg7a9Ko/A==}
  '@dcloudio/uni-h5@3.0.0-3081220230817001':
    resolution: {integrity: sha512-m2RCZpYxuAGnXlQKSQ8FGy4OlnUJJ8XMx2HNQWriDwtjDpAGDU8I2LZQZ7MHCNPIUd503y+mgU2NPlSQHnrlkQ==}
  '@dcloudio/uni-i18n@3.0.0-3081220230817001':
    resolution: {integrity: sha512-ooAIoNCy+DPlMtA4k9eoaJJsYQtici85le+ietIIzLdJoa2YUnudYv9CvG9Mw/RsKQI+MLl96ADAPgUpGzounw==}
  '@dcloudio/uni-push@3.0.0-3081220230817001':
    resolution: {integrity: sha512-1IyVJlBnZVVvY49SQKpZG5Oxak5Wl51NvYEozuYoeBaaTeZObm7B6eyd722sXi0Dd6N1sE3kIdm/XReprK53eg==}
  '@dcloudio/uni-shared@3.0.0-3081220230817001':
    resolution: {integrity: sha512-NI1pBO40VqvnWjwNXad3CqrUYvr4ffGjiDMgJGMP13rgOEAqamU7ozBimoASDVPKyyfSHTeuYuh0gtaaLu4CsQ==}
  '@dcloudio/uni-stat@3.0.0-3081220230817001':
    resolution: {integrity: sha512-xpbsor5WKU1eCbo0dmQ0hBUX6RrdG7DNDzCSD6oimndIXVGLkFhWrIC3brQ7S1G2GQvXdXnXJpUVskI3qNe/uw==}
  '@esbuild/aix-ppc64@0.21.5':
    resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
    engines: {node: '>=12'}
    cpu: [ppc64]
    os: [aix]
  '@esbuild/android-arm64@0.17.19':
    resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==}
    engines: {node: '>=12'}
    cpu: [arm64]
    os: [android]
  '@esbuild/android-arm64@0.21.5':
    resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
    engines: {node: '>=12'}
    cpu: [arm64]
    os: [android]
  '@esbuild/android-arm@0.17.19':
    resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==}
    engines: {node: '>=12'}
    cpu: [arm]
    os: [android]
  '@esbuild/android-arm@0.21.5':
    resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
    engines: {node: '>=12'}
    cpu: [arm]
    os: [android]
  '@esbuild/android-x64@0.17.19':
    resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [android]
  '@esbuild/android-x64@0.21.5':
    resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [android]
  '@esbuild/darwin-arm64@0.17.19':
    resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==}
    engines: {node: '>=12'}
    cpu: [arm64]
    os: [darwin]
  '@esbuild/darwin-arm64@0.21.5':
    resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
    engines: {node: '>=12'}
    cpu: [arm64]
    os: [darwin]
  '@esbuild/darwin-x64@0.17.19':
    resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [darwin]
  '@esbuild/darwin-x64@0.21.5':
    resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [darwin]
  '@esbuild/freebsd-arm64@0.17.19':
    resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==}
    engines: {node: '>=12'}
    cpu: [arm64]
    os: [freebsd]
  '@esbuild/freebsd-arm64@0.21.5':
    resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
    engines: {node: '>=12'}
    cpu: [arm64]
    os: [freebsd]
  '@esbuild/freebsd-x64@0.17.19':
    resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [freebsd]
  '@esbuild/freebsd-x64@0.21.5':
    resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [freebsd]
  '@esbuild/linux-arm64@0.17.19':
    resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==}
    engines: {node: '>=12'}
    cpu: [arm64]
    os: [linux]
  '@esbuild/linux-arm64@0.21.5':
    resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
    engines: {node: '>=12'}
    cpu: [arm64]
    os: [linux]
  '@esbuild/linux-arm@0.17.19':
    resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==}
    engines: {node: '>=12'}
    cpu: [arm]
    os: [linux]
  '@esbuild/linux-arm@0.21.5':
    resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
    engines: {node: '>=12'}
    cpu: [arm]
    os: [linux]
  '@esbuild/linux-ia32@0.17.19':
    resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==}
    engines: {node: '>=12'}
    cpu: [ia32]
    os: [linux]
  '@esbuild/linux-ia32@0.21.5':
    resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
    engines: {node: '>=12'}
    cpu: [ia32]
    os: [linux]
  '@esbuild/linux-loong64@0.17.19':
    resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==}
    engines: {node: '>=12'}
    cpu: [loong64]
    os: [linux]
  '@esbuild/linux-loong64@0.21.5':
    resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
    engines: {node: '>=12'}
    cpu: [loong64]
    os: [linux]
  '@esbuild/linux-mips64el@0.17.19':
    resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==}
    engines: {node: '>=12'}
    cpu: [mips64el]
    os: [linux]
  '@esbuild/linux-mips64el@0.21.5':
    resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
    engines: {node: '>=12'}
    cpu: [mips64el]
    os: [linux]
  '@esbuild/linux-ppc64@0.17.19':
    resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==}
    engines: {node: '>=12'}
    cpu: [ppc64]
    os: [linux]
  '@esbuild/linux-ppc64@0.21.5':
    resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
    engines: {node: '>=12'}
    cpu: [ppc64]
    os: [linux]
  '@esbuild/linux-riscv64@0.17.19':
    resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==}
    engines: {node: '>=12'}
    cpu: [riscv64]
    os: [linux]
  '@esbuild/linux-riscv64@0.21.5':
    resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
    engines: {node: '>=12'}
    cpu: [riscv64]
    os: [linux]
  '@esbuild/linux-s390x@0.17.19':
    resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==}
    engines: {node: '>=12'}
    cpu: [s390x]
    os: [linux]
  '@esbuild/linux-s390x@0.21.5':
    resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
    engines: {node: '>=12'}
    cpu: [s390x]
    os: [linux]
  '@esbuild/linux-x64@0.17.19':
    resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [linux]
  '@esbuild/linux-x64@0.21.5':
    resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [linux]
  '@esbuild/netbsd-x64@0.17.19':
    resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [netbsd]
  '@esbuild/netbsd-x64@0.21.5':
    resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [netbsd]
  '@esbuild/openbsd-x64@0.17.19':
    resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [openbsd]
  '@esbuild/openbsd-x64@0.21.5':
    resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [openbsd]
  '@esbuild/sunos-x64@0.17.19':
    resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [sunos]
  '@esbuild/sunos-x64@0.21.5':
    resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [sunos]
  '@esbuild/win32-arm64@0.17.19':
    resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==}
    engines: {node: '>=12'}
    cpu: [arm64]
    os: [win32]
  '@esbuild/win32-arm64@0.21.5':
    resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
    engines: {node: '>=12'}
    cpu: [arm64]
    os: [win32]
  '@esbuild/win32-ia32@0.17.19':
    resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==}
    engines: {node: '>=12'}
    cpu: [ia32]
    os: [win32]
  '@esbuild/win32-ia32@0.21.5':
    resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
    engines: {node: '>=12'}
    cpu: [ia32]
    os: [win32]
  '@esbuild/win32-x64@0.17.19':
    resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [win32]
  '@esbuild/win32-x64@0.21.5':
    resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [win32]
  '@hyoga/uni-socket.io@3.0.4':
    resolution: {integrity: sha512-m04M/ALXpTWcKoJGPDMzJosmzuopyAXF51DFN/qxLyXnbCFeQwsWOXT/XNLl4aWoBVGwQ7IptECFFRCA6z5C8A==}
  '@intlify/core-base@9.1.9':
    resolution: {integrity: sha512-x5T0p/Ja0S8hs5xs+ImKyYckVkL4CzcEXykVYYV6rcbXxJTe2o58IquSqX9bdncVKbRZP7GlBU1EcRaQEEJ+vw==}
    engines: {node: '>= 10'}
  '@intlify/devtools-if@9.1.9':
    resolution: {integrity: sha512-oKSMKjttG3Ut/1UGEZjSdghuP3fwA15zpDPcjkf/1FjlOIm6uIBGMNS5jXzsZy593u+P/YcnrZD6cD3IVFz9vQ==}
    engines: {node: '>= 10'}
  '@intlify/message-compiler@9.1.9':
    resolution: {integrity: sha512-6YgCMF46Xd0IH2hMRLCssZI3gFG4aywidoWQ3QP4RGYQXQYYfFC54DxhSgfIPpVoPLQ+4AD29eoYmhiHZ+qLFQ==}
    engines: {node: '>= 10'}
  '@intlify/message-resolver@9.1.9':
    resolution: {integrity: sha512-Lx/DBpigeK0sz2BBbzv5mu9/dAlt98HxwbG7xLawC3O2xMF9MNWU5FtOziwYG6TDIjNq0O/3ZbOJAxwITIWXEA==}
    engines: {node: '>= 10'}
  '@intlify/runtime@9.1.9':
    resolution: {integrity: sha512-XgPw8+UlHCiie3fI41HPVa/VDJb3/aSH7bLhY1hJvlvNV713PFtb4p4Jo+rlE0gAoMsMCGcsiT982fImolSltg==}
    engines: {node: '>= 10'}
  '@intlify/shared@9.1.9':
    resolution: {integrity: sha512-xKGM1d0EAxdDFCWedcYXOm6V5Pfw/TMudd6/qCdEb4tv0hk9EKeg7lwQF1azE0dP2phvx0yXxrt7UQK+IZjNdw==}
    engines: {node: '>= 10'}
  '@intlify/vue-devtools@9.1.9':
    resolution: {integrity: sha512-YPehH9uL4vZcGXky4Ev5qQIITnHKIvsD2GKGXgqf+05osMUI6WSEQHaN9USRa318Rs8RyyPCiDfmA0hRu3k7og==}
    engines: {node: '>= 10'}
  '@isaacs/cliui@8.0.2':
    resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
    engines: {node: '>=12'}
  '@jridgewell/gen-mapping@0.3.8':
    resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
    engines: {node: '>=6.0.0'}
  '@jridgewell/resolve-uri@3.1.2':
    resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
    engines: {node: '>=6.0.0'}
  '@jridgewell/set-array@1.2.1':
    resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
    engines: {node: '>=6.0.0'}
  '@jridgewell/sourcemap-codec@1.5.0':
    resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
  '@jridgewell/trace-mapping@0.3.25':
    resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
  '@nodelib/fs.scandir@2.1.5':
    resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
    engines: {node: '>= 8'}
  '@nodelib/fs.stat@2.0.5':
    resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
    engines: {node: '>= 8'}
  '@nodelib/fs.walk@1.2.8':
    resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
    engines: {node: '>= 8'}
  '@pkgjs/parseargs@0.11.0':
    resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
    engines: {node: '>=14'}
  '@rollup/pluginutils@4.2.1':
    resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
    engines: {node: '>= 8.0.0'}
  '@rollup/rollup-android-arm-eabi@4.34.6':
    resolution: {integrity: sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg==}
    cpu: [arm]
    os: [android]
  '@rollup/rollup-android-arm64@4.34.6':
    resolution: {integrity: sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA==}
    cpu: [arm64]
    os: [android]
  '@rollup/rollup-darwin-arm64@4.34.6':
    resolution: {integrity: sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg==}
    cpu: [arm64]
    os: [darwin]
  '@rollup/rollup-darwin-x64@4.34.6':
    resolution: {integrity: sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg==}
    cpu: [x64]
    os: [darwin]
  '@rollup/rollup-freebsd-arm64@4.34.6':
    resolution: {integrity: sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ==}
    cpu: [arm64]
    os: [freebsd]
  '@rollup/rollup-freebsd-x64@4.34.6':
    resolution: {integrity: sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ==}
    cpu: [x64]
    os: [freebsd]
  '@rollup/rollup-linux-arm-gnueabihf@4.34.6':
    resolution: {integrity: sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==}
    cpu: [arm]
    os: [linux]
    libc: [glibc]
  '@rollup/rollup-linux-arm-musleabihf@4.34.6':
    resolution: {integrity: sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==}
    cpu: [arm]
    os: [linux]
    libc: [musl]
  '@rollup/rollup-linux-arm64-gnu@4.34.6':
    resolution: {integrity: sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==}
    cpu: [arm64]
    os: [linux]
    libc: [glibc]
  '@rollup/rollup-linux-arm64-musl@4.34.6':
    resolution: {integrity: sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==}
    cpu: [arm64]
    os: [linux]
    libc: [musl]
  '@rollup/rollup-linux-loongarch64-gnu@4.34.6':
    resolution: {integrity: sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==}
    cpu: [loong64]
    os: [linux]
    libc: [glibc]
  '@rollup/rollup-linux-powerpc64le-gnu@4.34.6':
    resolution: {integrity: sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==}
    cpu: [ppc64]
    os: [linux]
    libc: [glibc]
  '@rollup/rollup-linux-riscv64-gnu@4.34.6':
    resolution: {integrity: sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==}
    cpu: [riscv64]
    os: [linux]
    libc: [glibc]
  '@rollup/rollup-linux-s390x-gnu@4.34.6':
    resolution: {integrity: sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==}
    cpu: [s390x]
    os: [linux]
    libc: [glibc]
  '@rollup/rollup-linux-x64-gnu@4.34.6':
    resolution: {integrity: sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==}
    cpu: [x64]
    os: [linux]
    libc: [glibc]
  '@rollup/rollup-linux-x64-musl@4.34.6':
    resolution: {integrity: sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==}
    cpu: [x64]
    os: [linux]
    libc: [musl]
  '@rollup/rollup-win32-arm64-msvc@4.34.6':
    resolution: {integrity: sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==}
    cpu: [arm64]
    os: [win32]
  '@rollup/rollup-win32-ia32-msvc@4.34.6':
    resolution: {integrity: sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA==}
    cpu: [ia32]
    os: [win32]
  '@rollup/rollup-win32-x64-msvc@4.34.6':
    resolution: {integrity: sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w==}
    cpu: [x64]
    os: [win32]
  '@trysound/sax@0.2.0':
    resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
    engines: {node: '>=10.13.0'}
  '@types/estree@1.0.6':
    resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
  '@types/gensync@1.0.4':
    resolution: {integrity: sha512-C3YYeRQWp2fmq9OryX+FoDy8nXS6scQ7dPptD8LnFDAUNcKWJjXQKDNJD3HVm+kOUsXhTOkpi69vI4EuAr95bA==}
  '@types/lodash-es@4.17.12':
    resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
  '@types/lodash@4.17.15':
    resolution: {integrity: sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==}
  '@types/md5@2.3.5':
    resolution: {integrity: sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw==}
  '@types/node@20.17.17':
    resolution: {integrity: sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg==}
  '@vue/compiler-core@3.2.47':
    resolution: {integrity: sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==}
  '@vue/compiler-core@3.5.13':
    resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==}
  '@vue/compiler-dom@3.2.47':
    resolution: {integrity: sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==}
  '@vue/compiler-dom@3.5.13':
    resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==}
  '@vue/compiler-sfc@3.2.47':
    resolution: {integrity: sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==}
  '@vue/compiler-sfc@3.5.13':
    resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==}
  '@vue/compiler-ssr@3.2.47':
    resolution: {integrity: sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==}
  '@vue/compiler-ssr@3.5.13':
    resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==}
  '@vue/devtools-api@6.6.4':
    resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
  '@vue/reactivity-transform@3.2.47':
    resolution: {integrity: sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==}
  '@vue/reactivity@3.5.13':
    resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==}
  '@vue/runtime-core@3.5.13':
    resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==}
  '@vue/runtime-dom@3.5.13':
    resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==}
  '@vue/server-renderer@3.2.47':
    resolution: {integrity: sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==}
    peerDependencies:
      vue: 3.2.47
  '@vue/server-renderer@3.5.13':
    resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==}
    peerDependencies:
      vue: 3.5.13
  '@vue/shared@3.2.47':
    resolution: {integrity: sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==}
  '@vue/shared@3.5.13':
    resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
  '@vue/tsconfig@0.5.1':
    resolution: {integrity: sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==}
  ansi-regex@5.0.1:
    resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
    engines: {node: '>=8'}
  ansi-regex@6.1.0:
    resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
    engines: {node: '>=12'}
  ansi-styles@4.3.0:
    resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
    engines: {node: '>=8'}
  ansi-styles@6.2.1:
    resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
    engines: {node: '>=12'}
  anymatch@3.1.3:
    resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
    engines: {node: '>= 8'}
  asynckit@0.4.0:
    resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
  autoprefixer@10.4.20:
    resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==}
    engines: {node: ^10 || ^12 || >=14}
    hasBin: true
    peerDependencies:
      postcss: ^8.1.0
  axios@1.7.9:
    resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==}
  balanced-match@1.0.2:
    resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
  base64url@3.0.1:
    resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==}
    engines: {node: '>=6.0.0'}
  binary-extensions@2.3.0:
    resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
    engines: {node: '>=8'}
  boolbase@1.0.0:
    resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
  brace-expansion@2.0.1:
    resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
  braces@3.0.3:
    resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
    engines: {node: '>=8'}
  browserslist@4.24.4:
    resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==}
    engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
    hasBin: true
  caniuse-lite@1.0.30001698:
    resolution: {integrity: sha512-xJ3km2oiG/MbNU8G6zIq6XRZ6HtAOVXsbOrP/blGazi52kc5Yy7b6sDA5O+FbROzRrV7BSTllLHuNvmawYUJjw==}
  charenc@0.0.2:
    resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==}
  chokidar@3.6.0:
    resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
    engines: {node: '>= 8.10.0'}
  color-convert@2.0.1:
    resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
    engines: {node: '>=7.0.0'}
  color-name@1.1.4:
    resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
  combined-stream@1.0.8:
    resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
    engines: {node: '>= 0.8'}
  commander@7.2.0:
    resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
    engines: {node: '>= 10'}
  compare-versions@3.6.0:
    resolution: {integrity: sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==}
  convert-source-map@2.0.0:
    resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
  cross-spawn@7.0.6:
    resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
    engines: {node: '>= 8'}
  crypt@0.0.2:
    resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
  css-select@5.1.0:
    resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
  css-tree@2.2.1:
    resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}
    engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
  css-tree@2.3.1:
    resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
    engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
  css-what@6.1.0:
    resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
    engines: {node: '>= 6'}
  cssesc@3.0.0:
    resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
    engines: {node: '>=4'}
    hasBin: true
  csso@5.0.5:
    resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
    engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
  csstype@3.1.3:
    resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
  dayjs@1.11.13:
    resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
  debug@4.4.0:
    resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
    engines: {node: '>=6.0'}
    peerDependencies:
      supports-color: '*'
    peerDependenciesMeta:
      supports-color:
        optional: true
  delayed-stream@1.0.0:
    resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
    engines: {node: '>=0.4.0'}
  dom-serializer@2.0.0:
    resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
  domelementtype@2.3.0:
    resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
  domhandler@5.0.3:
    resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
    engines: {node: '>= 4'}
  domutils@3.2.2:
    resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
  eastasianwidth@0.2.0:
    resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
  electron-to-chromium@1.5.96:
    resolution: {integrity: sha512-8AJUW6dh75Fm/ny8+kZKJzI1pgoE8bKLZlzDU2W1ENd+DXKJrx7I7l9hb8UWR4ojlnb5OlixMt00QWiYJoVw1w==}
  emoji-regex@8.0.0:
    resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
  emoji-regex@9.2.2:
    resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
  entities@4.5.0:
    resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
    engines: {node: '>=0.12'}
  es-module-lexer@1.6.0:
    resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==}
  esbuild@0.17.19:
    resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==}
    engines: {node: '>=12'}
    hasBin: true
  esbuild@0.21.5:
    resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
    engines: {node: '>=12'}
    hasBin: true
  escalade@3.2.0:
    resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
    engines: {node: '>=6'}
  estree-walker@2.0.2:
    resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
  fast-glob@3.3.3:
    resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
    engines: {node: '>=8.6.0'}
  fastq@1.19.0:
    resolution: {integrity: sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==}
  fill-range@7.1.1:
    resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
    engines: {node: '>=8'}
  follow-redirects@1.15.9:
    resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
    engines: {node: '>=4.0'}
    peerDependencies:
      debug: '*'
    peerDependenciesMeta:
      debug:
        optional: true
  foreground-child@3.3.0:
    resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
    engines: {node: '>=14'}
  form-data@4.0.1:
    resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==}
    engines: {node: '>= 6'}
  fraction.js@4.3.7:
    resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
  fs-extra@10.1.0:
    resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
    engines: {node: '>=12'}
  fsevents@2.3.3:
    resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
    os: [darwin]
  function-bind@1.1.2:
    resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
  generic-names@4.0.0:
    resolution: {integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==}
  gensync@1.0.0-beta.2:
    resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
    engines: {node: '>=6.9.0'}
  glob-parent@5.1.2:
    resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
    engines: {node: '>= 6'}
  glob@10.4.5:
    resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
    hasBin: true
  globals@11.12.0:
    resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
    engines: {node: '>=4'}
  graceful-fs@4.2.11:
    resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
  hash-sum@2.0.0:
    resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==}
  hasown@2.0.2:
    resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
    engines: {node: '>= 0.4'}
  icss-replace-symbols@1.1.0:
    resolution: {integrity: sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==}
  icss-utils@5.1.0:
    resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==}
    engines: {node: ^10 || ^12 || >= 14}
    peerDependencies:
      postcss: ^8.1.0
  invert-kv@3.0.1:
    resolution: {integrity: sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==}
    engines: {node: '>=8'}
  is-binary-path@2.1.0:
    resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
    engines: {node: '>=8'}
  is-buffer@1.1.6:
    resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
  is-core-module@2.16.1:
    resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
    engines: {node: '>= 0.4'}
  is-extglob@2.1.1:
    resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
    engines: {node: '>=0.10.0'}
  is-fullwidth-code-point@3.0.0:
    resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
    engines: {node: '>=8'}
  is-glob@4.0.3:
    resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
    engines: {node: '>=0.10.0'}
  is-number@7.0.0:
    resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
    engines: {node: '>=0.12.0'}
  isexe@2.0.0:
    resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
  jackspeak@3.4.3:
    resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
  js-pinyin@0.2.7:
    resolution: {integrity: sha512-Dbm7qJDwlC2MRHRXQo7GrVrL5w975tEOX6sfc0M8Z3oL9VhR4uUqgkwaETMqh4bAJ14w4zVs3tXsi9pDxWUmNg==}
  js-tokens@4.0.0:
    resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
  jsesc@3.1.0:
    resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
    engines: {node: '>=6'}
    hasBin: true
  json5@2.2.3:
    resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
    engines: {node: '>=6'}
    hasBin: true
  jsonc-parser@3.3.1:
    resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
  jsonfile@6.1.0:
    resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
  lcid@3.1.1:
    resolution: {integrity: sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==}
    engines: {node: '>=8'}
  lilconfig@2.1.0:
    resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
    engines: {node: '>=10'}
  loader-utils@3.3.1:
    resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==}
    engines: {node: '>= 12.13.0'}
  localstorage-polyfill@1.0.1:
    resolution: {integrity: sha512-m4iHVZxFH5734oQcPKU08025gIz2+4bjWR9lulP8ZYxEJR0BpA0w32oJmkzh8y3UI9ci7xCBehQDc3oA1X+VHw==}
    engines: {node: '>=6'}
  lodash-es@4.17.21:
    resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
  lodash.camelcase@4.3.0:
    resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
  lodash@4.17.21:
    resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
  lru-cache@10.4.3:
    resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
  lru-cache@5.1.1:
    resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
  magic-string@0.25.9:
    resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
  magic-string@0.30.17:
    resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
  md5@2.3.0:
    resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
  mdn-data@2.0.28:
    resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
  mdn-data@2.0.30:
    resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
  merge2@1.4.1:
    resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
    engines: {node: '>= 8'}
  merge@2.1.1:
    resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==}
  micromatch@4.0.8:
    resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
    engines: {node: '>=8.6'}
  mime-db@1.52.0:
    resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
    engines: {node: '>= 0.6'}
  mime-types@2.1.35:
    resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
    engines: {node: '>= 0.6'}
  mime@3.0.0:
    resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
    engines: {node: '>=10.0.0'}
    hasBin: true
  minimatch@9.0.5:
    resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
    engines: {node: '>=16 || 14 >=14.17'}
  minipass@7.1.2:
    resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
    engines: {node: '>=16 || 14 >=14.17'}
  module-alias@2.2.3:
    resolution: {integrity: sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==}
  ms@2.1.3:
    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
  nanoid@3.3.8:
    resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
    hasBin: true
  node-releases@2.0.19:
    resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
  normalize-path@3.0.0:
    resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
    engines: {node: '>=0.10.0'}
  normalize-range@0.1.2:
    resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
    engines: {node: '>=0.10.0'}
  nth-check@2.1.1:
    resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
  os-locale-s-fix@1.0.8-fix-1:
    resolution: {integrity: sha512-Sv0OvhPiMutICiwORAUefv02DCPb62IelBmo8ZsSrRHyI3FStqIWZvjqDkvtjU+lcujo7UNir+dCwKSqlEQ/5w==}
    engines: {node: '>=10', yarn: ^1.22.4}
  package-json-from-dist@1.0.1:
    resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
  path-key@3.1.1:
    resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
    engines: {node: '>=8'}
  path-parse@1.0.7:
    resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
  path-scurry@1.11.1:
    resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
    engines: {node: '>=16 || 14 >=14.18'}
  picocolors@1.1.1:
    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
  picomatch@2.3.1:
    resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
    engines: {node: '>=8.6'}
  pify@2.3.0:
    resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
    engines: {node: '>=0.10.0'}
  pinia@2.3.1:
    resolution: {integrity: sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==}
    peerDependencies:
      typescript: '>=4.4.4'
      vue: ^2.7.0 || ^3.5.11
    peerDependenciesMeta:
      typescript:
        optional: true
  postcss-import@14.1.0:
    resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==}
    engines: {node: '>=10.0.0'}
    peerDependencies:
      postcss: ^8.0.0
  postcss-load-config@3.1.4:
    resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
    engines: {node: '>= 10'}
    peerDependencies:
      postcss: '>=8.0.9'
      ts-node: '>=9.0.0'
    peerDependenciesMeta:
      postcss:
        optional: true
      ts-node:
        optional: true
  postcss-modules-extract-imports@3.1.0:
    resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==}
    engines: {node: ^10 || ^12 || >= 14}
    peerDependencies:
      postcss: ^8.1.0
  postcss-modules-local-by-default@4.2.0:
    resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==}
    engines: {node: ^10 || ^12 || >= 14}
    peerDependencies:
      postcss: ^8.1.0
  postcss-modules-scope@3.2.1:
    resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==}
    engines: {node: ^10 || ^12 || >= 14}
    peerDependencies:
      postcss: ^8.1.0
  postcss-modules-values@4.0.0:
    resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==}
    engines: {node: ^10 || ^12 || >= 14}
    peerDependencies:
      postcss: ^8.1.0
  postcss-modules@4.3.1:
    resolution: {integrity: sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==}
    peerDependencies:
      postcss: ^8.0.0
  postcss-selector-parser@6.1.2:
    resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
    engines: {node: '>=4'}
  postcss-selector-parser@7.1.0:
    resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==}
    engines: {node: '>=4'}
  postcss-value-parser@4.2.0:
    resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
  postcss@8.5.1:
    resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==}
    engines: {node: ^10 || ^12 || >=14}
  prettier@3.4.2:
    resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==}
    engines: {node: '>=14'}
    hasBin: true
  proxy-from-env@1.1.0:
    resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
  queue-microtask@1.2.3:
    resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
  read-cache@1.0.0:
    resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
  readdirp@3.6.0:
    resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
    engines: {node: '>=8.10.0'}
  resolve@1.22.10:
    resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
    engines: {node: '>= 0.4'}
    hasBin: true
  reusify@1.0.4:
    resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
    engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
  rollup@4.34.6:
    resolution: {integrity: sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ==}
    engines: {node: '>=18.0.0', npm: '>=8.0.0'}
    hasBin: true
  run-parallel@1.2.0:
    resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
  safe-area-insets@1.4.1:
    resolution: {integrity: sha512-r/nRWTjFGhhm3w1Z6Kd/jY11srN+lHt2mNl1E/emQGW8ic7n3Avu4noibklfSM+Y34peNphHD/BSZecav0sXYQ==}
  semver@6.3.1:
    resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
    hasBin: true
  shebang-command@2.0.0:
    resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
    engines: {node: '>=8'}
  shebang-regex@3.0.0:
    resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
    engines: {node: '>=8'}
  signal-exit@4.1.0:
    resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
    engines: {node: '>=14'}
  source-map-js@1.2.1:
    resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
    engines: {node: '>=0.10.0'}
  source-map@0.6.1:
    resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
    engines: {node: '>=0.10.0'}
  sourcemap-codec@1.4.8:
    resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
    deprecated: Please use @jridgewell/sourcemap-codec instead
  string-hash@1.1.3:
    resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==}
  string-width@4.2.3:
    resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
    engines: {node: '>=8'}
  string-width@5.1.2:
    resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
    engines: {node: '>=12'}
  strip-ansi@6.0.1:
    resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
    engines: {node: '>=8'}
  strip-ansi@7.1.0:
    resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
    engines: {node: '>=12'}
  supports-preserve-symlinks-flag@1.0.0:
    resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
    engines: {node: '>= 0.4'}
  svgo@3.3.2:
    resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==}
    engines: {node: '>=14.0.0'}
    hasBin: true
  tapable@2.2.1:
    resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
    engines: {node: '>=6'}
  to-regex-range@5.0.1:
    resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
    engines: {node: '>=8.0'}
  typescript@5.5.4:
    resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==}
    engines: {node: '>=14.17'}
    hasBin: true
  undici-types@6.19.8:
    resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
  universalify@2.0.1:
    resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
    engines: {node: '>= 10.0.0'}
  update-browserslist-db@1.1.2:
    resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==}
    hasBin: true
    peerDependencies:
      browserslist: '>= 4.21.0'
  util-deprecate@1.0.2:
    resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
  vite@5.4.14:
    resolution: {integrity: sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==}
    engines: {node: ^18.0.0 || >=20.0.0}
    hasBin: true
    peerDependencies:
      '@types/node': ^18.0.0 || >=20.0.0
      less: '*'
      lightningcss: ^1.21.0
      sass: '*'
      sass-embedded: '*'
      stylus: '*'
      sugarss: '*'
      terser: ^5.4.0
    peerDependenciesMeta:
      '@types/node':
        optional: true
      less:
        optional: true
      lightningcss:
        optional: true
      sass:
        optional: true
      sass-embedded:
        optional: true
      stylus:
        optional: true
      sugarss:
        optional: true
      terser:
        optional: true
  vue-demi@0.14.10:
    resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
    engines: {node: '>=12'}
    hasBin: true
    peerDependencies:
      '@vue/composition-api': ^1.0.0-rc.1
      vue: ^3.0.0-0 || ^2.6.0
    peerDependenciesMeta:
      '@vue/composition-api':
        optional: true
  vue-i18n@9.1.9:
    resolution: {integrity: sha512-JeRdNVxS2OGp1E+pye5XB6+M6BBkHwAv9C80Q7+kzoMdUDGRna06tjC0vCB/jDX9aWrl5swxOMFcyAr7or8XTA==}
    engines: {node: '>= 10'}
    peerDependencies:
      vue: ^3.0.0
  vue-router@4.5.0:
    resolution: {integrity: sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==}
    peerDependencies:
      vue: ^3.2.0
  vue@3.5.13:
    resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==}
    peerDependencies:
      typescript: '*'
    peerDependenciesMeta:
      typescript:
        optional: true
  weixin-js-sdk@1.6.5:
    resolution: {integrity: sha512-Gph1WAWB2YN/lMOFB/ymb+hbU/wYazzJgu6PMMktCy9cSCeW5wA6Zwt0dpahJbJ+RJEwtTv2x9iIu0U4enuVSQ==}
  which@2.0.2:
    resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
    engines: {node: '>= 8'}
    hasBin: true
  wrap-ansi@7.0.0:
    resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
    engines: {node: '>=10'}
  wrap-ansi@8.1.0:
    resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
    engines: {node: '>=12'}
  xmlhttprequest@1.8.0:
    resolution: {integrity: sha512-58Im/U0mlVBLM38NdZjHyhuMtCqa61469k2YP/AaPbvCoV9aQGUpbJBj1QRm2ytRiVQBD/fsw7L2bJGDVQswBA==}
    engines: {node: '>=0.4.0'}
  xregexp@3.1.0:
    resolution: {integrity: sha512-4Y1x6DyB8xRoxosooa6PlGWqmmSKatbzhrftZ7Purmm4B8R4qIEJG1A2hZsdz5DhmIqS0msC0I7KEq93GphEVg==}
  yallist@3.1.1:
    resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
  yaml@1.10.2:
    resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
    engines: {node: '>= 6'}
snapshots:
  '@ampproject/remapping@2.3.0':
    dependencies:
      '@jridgewell/gen-mapping': 0.3.8
      '@jridgewell/trace-mapping': 0.3.25
  '@babel/code-frame@7.26.2':
    dependencies:
      '@babel/helper-validator-identifier': 7.25.9
      js-tokens: 4.0.0
      picocolors: 1.1.1
  '@babel/compat-data@7.26.8': {}
  '@babel/core@7.26.8':
    dependencies:
      '@ampproject/remapping': 2.3.0
      '@babel/code-frame': 7.26.2
      '@babel/generator': 7.26.8
      '@babel/helper-compilation-targets': 7.26.5
      '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.8)
      '@babel/helpers': 7.26.7
      '@babel/parser': 7.26.8
      '@babel/template': 7.26.8
      '@babel/traverse': 7.26.8
      '@babel/types': 7.26.8
      '@types/gensync': 1.0.4
      convert-source-map: 2.0.0
      debug: 4.4.0
      gensync: 1.0.0-beta.2
      json5: 2.2.3
      semver: 6.3.1
    transitivePeerDependencies:
      - supports-color
  '@babel/generator@7.26.8':
    dependencies:
      '@babel/parser': 7.26.8
      '@babel/types': 7.26.8
      '@jridgewell/gen-mapping': 0.3.8
      '@jridgewell/trace-mapping': 0.3.25
      jsesc: 3.1.0
  '@babel/helper-compilation-targets@7.26.5':
    dependencies:
      '@babel/compat-data': 7.26.8
      '@babel/helper-validator-option': 7.25.9
      browserslist: 4.24.4
      lru-cache: 5.1.1
      semver: 6.3.1
  '@babel/helper-module-imports@7.25.9':
    dependencies:
      '@babel/traverse': 7.26.8
      '@babel/types': 7.26.8
    transitivePeerDependencies:
      - supports-color
  '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.8)':
    dependencies:
      '@babel/core': 7.26.8
      '@babel/helper-module-imports': 7.25.9
      '@babel/helper-validator-identifier': 7.25.9
      '@babel/traverse': 7.26.8
    transitivePeerDependencies:
      - supports-color
  '@babel/helper-string-parser@7.25.9': {}
  '@babel/helper-validator-identifier@7.25.9': {}
  '@babel/helper-validator-option@7.25.9': {}
  '@babel/helpers@7.26.7':
    dependencies:
      '@babel/template': 7.26.8
      '@babel/types': 7.26.8
  '@babel/parser@7.26.8':
    dependencies:
      '@babel/types': 7.26.8
  '@babel/template@7.26.8':
    dependencies:
      '@babel/code-frame': 7.26.2
      '@babel/parser': 7.26.8
      '@babel/types': 7.26.8
  '@babel/traverse@7.26.8':
    dependencies:
      '@babel/code-frame': 7.26.2
      '@babel/generator': 7.26.8
      '@babel/parser': 7.26.8
      '@babel/template': 7.26.8
      '@babel/types': 7.26.8
      debug: 4.4.0
      globals: 11.12.0
    transitivePeerDependencies:
      - supports-color
  '@babel/types@7.26.8':
    dependencies:
      '@babel/helper-string-parser': 7.25.9
      '@babel/helper-validator-identifier': 7.25.9
  '@cool-vue/vite-plugin@8.0.3':
    dependencies:
      '@vue/compiler-sfc': 3.5.13
      axios: 1.7.9
      glob: 10.4.5
      lodash: 4.17.21
      magic-string: 0.30.17
      prettier: 3.4.2
      svgo: 3.3.2
    transitivePeerDependencies:
      - debug
  '@dcloudio/types@3.4.14': {}
  '@dcloudio/uni-app@3.0.0-3081220230817001(@dcloudio/types@3.4.14)(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))':
    dependencies:
      '@dcloudio/types': 3.4.14
      '@dcloudio/uni-cloud': 3.0.0-3081220230817001(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))
      '@dcloudio/uni-components': 3.0.0-3081220230817001(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))
      '@dcloudio/uni-i18n': 3.0.0-3081220230817001
      '@dcloudio/uni-push': 3.0.0-3081220230817001(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))
      '@dcloudio/uni-shared': 3.0.0-3081220230817001
      '@dcloudio/uni-stat': 3.0.0-3081220230817001(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))
      '@vue/shared': 3.2.47
    transitivePeerDependencies:
      - postcss
      - supports-color
      - ts-node
      - vue
  '@dcloudio/uni-cli-shared@3.0.0-3081220230817001(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))':
    dependencies:
      '@ampproject/remapping': 2.3.0
      '@babel/core': 7.26.8
      '@babel/parser': 7.26.8
      '@babel/types': 7.26.8
      '@dcloudio/uni-i18n': 3.0.0-3081220230817001
      '@dcloudio/uni-shared': 3.0.0-3081220230817001
      '@intlify/core-base': 9.1.9
      '@intlify/shared': 9.1.9
      '@intlify/vue-devtools': 9.1.9
      '@rollup/pluginutils': 4.2.1
      '@vue/compiler-core': 3.2.47
      '@vue/compiler-dom': 3.2.47
      '@vue/compiler-sfc': 3.2.47
      '@vue/server-renderer': 3.2.47(vue@3.5.13(typescript@5.5.4))
      '@vue/shared': 3.2.47
      autoprefixer: 10.4.20(postcss@8.5.1)
      base64url: 3.0.1
      chokidar: 3.6.0
      compare-versions: 3.6.0
      debug: 4.4.0
      es-module-lexer: 1.6.0
      esbuild: 0.17.19
      estree-walker: 2.0.2
      fast-glob: 3.3.3
      fs-extra: 10.1.0
      hash-sum: 2.0.0
      jsonc-parser: 3.3.1
      magic-string: 0.30.17
      merge: 2.1.1
      mime: 3.0.0
      module-alias: 2.2.3
      os-locale-s-fix: 1.0.8-fix-1
      picocolors: 1.1.1
      postcss-import: 14.1.0(postcss@8.5.1)
      postcss-load-config: 3.1.4(postcss@8.5.1)
      postcss-modules: 4.3.1(postcss@8.5.1)
      postcss-selector-parser: 6.1.2
      resolve: 1.22.10
      tapable: 2.2.1
      xregexp: 3.1.0
    transitivePeerDependencies:
      - postcss
      - supports-color
      - ts-node
      - vue
  '@dcloudio/uni-cloud@3.0.0-3081220230817001(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))':
    dependencies:
      '@dcloudio/uni-cli-shared': 3.0.0-3081220230817001(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))
      '@dcloudio/uni-i18n': 3.0.0-3081220230817001
      '@dcloudio/uni-shared': 3.0.0-3081220230817001
      '@vue/shared': 3.2.47
      fast-glob: 3.3.3
    transitivePeerDependencies:
      - postcss
      - supports-color
      - ts-node
      - vue
  '@dcloudio/uni-components@3.0.0-3081220230817001(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))':
    dependencies:
      '@dcloudio/uni-cloud': 3.0.0-3081220230817001(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))
      '@dcloudio/uni-h5': 3.0.0-3081220230817001(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))
      '@dcloudio/uni-i18n': 3.0.0-3081220230817001
    transitivePeerDependencies:
      - postcss
      - supports-color
      - ts-node
      - vue
  '@dcloudio/uni-h5-vite@3.0.0-3081220230817001(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))':
    dependencies:
      '@dcloudio/uni-cli-shared': 3.0.0-3081220230817001(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))
      '@dcloudio/uni-shared': 3.0.0-3081220230817001
      '@rollup/pluginutils': 4.2.1
      '@vue/compiler-dom': 3.2.47
      '@vue/compiler-sfc': 3.2.47
      '@vue/server-renderer': 3.2.47(vue@3.5.13(typescript@5.5.4))
      '@vue/shared': 3.2.47
      debug: 4.4.0
      fs-extra: 10.1.0
      mime: 3.0.0
      module-alias: 2.2.3
    transitivePeerDependencies:
      - postcss
      - supports-color
      - ts-node
      - vue
  '@dcloudio/uni-h5-vue@3.0.0-3081220230817001(vue@3.5.13(typescript@5.5.4))':
    dependencies:
      '@dcloudio/uni-shared': 3.0.0-3081220230817001
      '@vue/server-renderer': 3.2.47(vue@3.5.13(typescript@5.5.4))
    transitivePeerDependencies:
      - vue
  '@dcloudio/uni-h5@3.0.0-3081220230817001(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))':
    dependencies:
      '@dcloudio/uni-h5-vite': 3.0.0-3081220230817001(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))
      '@dcloudio/uni-h5-vue': 3.0.0-3081220230817001(vue@3.5.13(typescript@5.5.4))
      '@dcloudio/uni-i18n': 3.0.0-3081220230817001
      '@dcloudio/uni-shared': 3.0.0-3081220230817001
      '@vue/server-renderer': 3.2.47(vue@3.5.13(typescript@5.5.4))
      '@vue/shared': 3.2.47
      debug: 4.4.0
      localstorage-polyfill: 1.0.1
      postcss-selector-parser: 6.1.2
      safe-area-insets: 1.4.1
      vue-router: 4.5.0(vue@3.5.13(typescript@5.5.4))
      xmlhttprequest: 1.8.0
    transitivePeerDependencies:
      - postcss
      - supports-color
      - ts-node
      - vue
  '@dcloudio/uni-i18n@3.0.0-3081220230817001': {}
  '@dcloudio/uni-push@3.0.0-3081220230817001(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))':
    dependencies:
      '@dcloudio/uni-cli-shared': 3.0.0-3081220230817001(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))
    transitivePeerDependencies:
      - postcss
      - supports-color
      - ts-node
      - vue
  '@dcloudio/uni-shared@3.0.0-3081220230817001':
    dependencies:
      '@vue/shared': 3.2.47
  '@dcloudio/uni-stat@3.0.0-3081220230817001(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))':
    dependencies:
      '@dcloudio/uni-cli-shared': 3.0.0-3081220230817001(postcss@8.5.1)(vue@3.5.13(typescript@5.5.4))
      '@dcloudio/uni-shared': 3.0.0-3081220230817001
      debug: 4.4.0
    transitivePeerDependencies:
      - postcss
      - supports-color
      - ts-node
      - vue
  '@esbuild/aix-ppc64@0.21.5':
    optional: true
  '@esbuild/android-arm64@0.17.19':
    optional: true
  '@esbuild/android-arm64@0.21.5':
    optional: true
  '@esbuild/android-arm@0.17.19':
    optional: true
  '@esbuild/android-arm@0.21.5':
    optional: true
  '@esbuild/android-x64@0.17.19':
    optional: true
  '@esbuild/android-x64@0.21.5':
    optional: true
  '@esbuild/darwin-arm64@0.17.19':
    optional: true
  '@esbuild/darwin-arm64@0.21.5':
    optional: true
  '@esbuild/darwin-x64@0.17.19':
    optional: true
  '@esbuild/darwin-x64@0.21.5':
    optional: true
  '@esbuild/freebsd-arm64@0.17.19':
    optional: true
  '@esbuild/freebsd-arm64@0.21.5':
    optional: true
  '@esbuild/freebsd-x64@0.17.19':
    optional: true
  '@esbuild/freebsd-x64@0.21.5':
    optional: true
  '@esbuild/linux-arm64@0.17.19':
    optional: true
  '@esbuild/linux-arm64@0.21.5':
    optional: true
  '@esbuild/linux-arm@0.17.19':
    optional: true
  '@esbuild/linux-arm@0.21.5':
    optional: true
  '@esbuild/linux-ia32@0.17.19':
    optional: true
  '@esbuild/linux-ia32@0.21.5':
    optional: true
  '@esbuild/linux-loong64@0.17.19':
    optional: true
  '@esbuild/linux-loong64@0.21.5':
    optional: true
  '@esbuild/linux-mips64el@0.17.19':
    optional: true
  '@esbuild/linux-mips64el@0.21.5':
    optional: true
  '@esbuild/linux-ppc64@0.17.19':
    optional: true
  '@esbuild/linux-ppc64@0.21.5':
    optional: true
  '@esbuild/linux-riscv64@0.17.19':
    optional: true
  '@esbuild/linux-riscv64@0.21.5':
    optional: true
  '@esbuild/linux-s390x@0.17.19':
    optional: true
  '@esbuild/linux-s390x@0.21.5':
    optional: true
  '@esbuild/linux-x64@0.17.19':
    optional: true
  '@esbuild/linux-x64@0.21.5':
    optional: true
  '@esbuild/netbsd-x64@0.17.19':
    optional: true
  '@esbuild/netbsd-x64@0.21.5':
    optional: true
  '@esbuild/openbsd-x64@0.17.19':
    optional: true
  '@esbuild/openbsd-x64@0.21.5':
    optional: true
  '@esbuild/sunos-x64@0.17.19':
    optional: true
  '@esbuild/sunos-x64@0.21.5':
    optional: true
  '@esbuild/win32-arm64@0.17.19':
    optional: true
  '@esbuild/win32-arm64@0.21.5':
    optional: true
  '@esbuild/win32-ia32@0.17.19':
    optional: true
  '@esbuild/win32-ia32@0.21.5':
    optional: true
  '@esbuild/win32-x64@0.17.19':
    optional: true
  '@esbuild/win32-x64@0.21.5':
    optional: true
  '@hyoga/uni-socket.io@3.0.4': {}
  '@intlify/core-base@9.1.9':
    dependencies:
      '@intlify/devtools-if': 9.1.9
      '@intlify/message-compiler': 9.1.9
      '@intlify/message-resolver': 9.1.9
      '@intlify/runtime': 9.1.9
      '@intlify/shared': 9.1.9
      '@intlify/vue-devtools': 9.1.9
  '@intlify/devtools-if@9.1.9':
    dependencies:
      '@intlify/shared': 9.1.9
  '@intlify/message-compiler@9.1.9':
    dependencies:
      '@intlify/message-resolver': 9.1.9
      '@intlify/shared': 9.1.9
      source-map: 0.6.1
  '@intlify/message-resolver@9.1.9': {}
  '@intlify/runtime@9.1.9':
    dependencies:
      '@intlify/message-compiler': 9.1.9
      '@intlify/message-resolver': 9.1.9
      '@intlify/shared': 9.1.9
  '@intlify/shared@9.1.9': {}
  '@intlify/vue-devtools@9.1.9':
    dependencies:
      '@intlify/message-resolver': 9.1.9
      '@intlify/runtime': 9.1.9
      '@intlify/shared': 9.1.9
  '@isaacs/cliui@8.0.2':
    dependencies:
      string-width: 5.1.2
      string-width-cjs: string-width@4.2.3
      strip-ansi: 7.1.0
      strip-ansi-cjs: strip-ansi@6.0.1
      wrap-ansi: 8.1.0
      wrap-ansi-cjs: wrap-ansi@7.0.0
  '@jridgewell/gen-mapping@0.3.8':
    dependencies:
      '@jridgewell/set-array': 1.2.1
      '@jridgewell/sourcemap-codec': 1.5.0
      '@jridgewell/trace-mapping': 0.3.25
  '@jridgewell/resolve-uri@3.1.2': {}
  '@jridgewell/set-array@1.2.1': {}
  '@jridgewell/sourcemap-codec@1.5.0': {}
  '@jridgewell/trace-mapping@0.3.25':
    dependencies:
      '@jridgewell/resolve-uri': 3.1.2
      '@jridgewell/sourcemap-codec': 1.5.0
  '@nodelib/fs.scandir@2.1.5':
    dependencies:
      '@nodelib/fs.stat': 2.0.5
      run-parallel: 1.2.0
  '@nodelib/fs.stat@2.0.5': {}
  '@nodelib/fs.walk@1.2.8':
    dependencies:
      '@nodelib/fs.scandir': 2.1.5
      fastq: 1.19.0
  '@pkgjs/parseargs@0.11.0':
    optional: true
  '@rollup/pluginutils@4.2.1':
    dependencies:
      estree-walker: 2.0.2
      picomatch: 2.3.1
  '@rollup/rollup-android-arm-eabi@4.34.6':
    optional: true
  '@rollup/rollup-android-arm64@4.34.6':
    optional: true
  '@rollup/rollup-darwin-arm64@4.34.6':
    optional: true
  '@rollup/rollup-darwin-x64@4.34.6':
    optional: true
  '@rollup/rollup-freebsd-arm64@4.34.6':
    optional: true
  '@rollup/rollup-freebsd-x64@4.34.6':
    optional: true
  '@rollup/rollup-linux-arm-gnueabihf@4.34.6':
    optional: true
  '@rollup/rollup-linux-arm-musleabihf@4.34.6':
    optional: true
  '@rollup/rollup-linux-arm64-gnu@4.34.6':
    optional: true
  '@rollup/rollup-linux-arm64-musl@4.34.6':
    optional: true
  '@rollup/rollup-linux-loongarch64-gnu@4.34.6':
    optional: true
  '@rollup/rollup-linux-powerpc64le-gnu@4.34.6':
    optional: true
  '@rollup/rollup-linux-riscv64-gnu@4.34.6':
    optional: true
  '@rollup/rollup-linux-s390x-gnu@4.34.6':
    optional: true
  '@rollup/rollup-linux-x64-gnu@4.34.6':
    optional: true
  '@rollup/rollup-linux-x64-musl@4.34.6':
    optional: true
  '@rollup/rollup-win32-arm64-msvc@4.34.6':
    optional: true
  '@rollup/rollup-win32-ia32-msvc@4.34.6':
    optional: true
  '@rollup/rollup-win32-x64-msvc@4.34.6':
    optional: true
  '@trysound/sax@0.2.0': {}
  '@types/estree@1.0.6': {}
  '@types/gensync@1.0.4': {}
  '@types/lodash-es@4.17.12':
    dependencies:
      '@types/lodash': 4.17.15
  '@types/lodash@4.17.15': {}
  '@types/md5@2.3.5': {}
  '@types/node@20.17.17':
    dependencies:
      undici-types: 6.19.8
  '@vue/compiler-core@3.2.47':
    dependencies:
      '@babel/parser': 7.26.8
      '@vue/shared': 3.2.47
      estree-walker: 2.0.2
      source-map: 0.6.1
  '@vue/compiler-core@3.5.13':
    dependencies:
      '@babel/parser': 7.26.8
      '@vue/shared': 3.5.13
      entities: 4.5.0
      estree-walker: 2.0.2
      source-map-js: 1.2.1
  '@vue/compiler-dom@3.2.47':
    dependencies:
      '@vue/compiler-core': 3.2.47
      '@vue/shared': 3.2.47
  '@vue/compiler-dom@3.5.13':
    dependencies:
      '@vue/compiler-core': 3.5.13
      '@vue/shared': 3.5.13
  '@vue/compiler-sfc@3.2.47':
    dependencies:
      '@babel/parser': 7.26.8
      '@vue/compiler-core': 3.2.47
      '@vue/compiler-dom': 3.2.47
      '@vue/compiler-ssr': 3.2.47
      '@vue/reactivity-transform': 3.2.47
      '@vue/shared': 3.2.47
      estree-walker: 2.0.2
      magic-string: 0.25.9
      postcss: 8.5.1
      source-map: 0.6.1
  '@vue/compiler-sfc@3.5.13':
    dependencies:
      '@babel/parser': 7.26.8
      '@vue/compiler-core': 3.5.13
      '@vue/compiler-dom': 3.5.13
      '@vue/compiler-ssr': 3.5.13
      '@vue/shared': 3.5.13
      estree-walker: 2.0.2
      magic-string: 0.30.17
      postcss: 8.5.1
      source-map-js: 1.2.1
  '@vue/compiler-ssr@3.2.47':
    dependencies:
      '@vue/compiler-dom': 3.2.47
      '@vue/shared': 3.2.47
  '@vue/compiler-ssr@3.5.13':
    dependencies:
      '@vue/compiler-dom': 3.5.13
      '@vue/shared': 3.5.13
  '@vue/devtools-api@6.6.4': {}
  '@vue/reactivity-transform@3.2.47':
    dependencies:
      '@babel/parser': 7.26.8
      '@vue/compiler-core': 3.2.47
      '@vue/shared': 3.2.47
      estree-walker: 2.0.2
      magic-string: 0.25.9
  '@vue/reactivity@3.5.13':
    dependencies:
      '@vue/shared': 3.5.13
  '@vue/runtime-core@3.5.13':
    dependencies:
      '@vue/reactivity': 3.5.13
      '@vue/shared': 3.5.13
  '@vue/runtime-dom@3.5.13':
    dependencies:
      '@vue/reactivity': 3.5.13
      '@vue/runtime-core': 3.5.13
      '@vue/shared': 3.5.13
      csstype: 3.1.3
  '@vue/server-renderer@3.2.47(vue@3.5.13(typescript@5.5.4))':
    dependencies:
      '@vue/compiler-ssr': 3.2.47
      '@vue/shared': 3.2.47
      vue: 3.5.13(typescript@5.5.4)
  '@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.5.4))':
    dependencies:
      '@vue/compiler-ssr': 3.5.13
      '@vue/shared': 3.5.13
      vue: 3.5.13(typescript@5.5.4)
  '@vue/shared@3.2.47': {}
  '@vue/shared@3.5.13': {}
  '@vue/tsconfig@0.5.1': {}
  ansi-regex@5.0.1: {}
  ansi-regex@6.1.0: {}
  ansi-styles@4.3.0:
    dependencies:
      color-convert: 2.0.1
  ansi-styles@6.2.1: {}
  anymatch@3.1.3:
    dependencies:
      normalize-path: 3.0.0
      picomatch: 2.3.1
  asynckit@0.4.0: {}
  autoprefixer@10.4.20(postcss@8.5.1):
    dependencies:
      browserslist: 4.24.4
      caniuse-lite: 1.0.30001698
      fraction.js: 4.3.7
      normalize-range: 0.1.2
      picocolors: 1.1.1
      postcss: 8.5.1
      postcss-value-parser: 4.2.0
  axios@1.7.9:
    dependencies:
      follow-redirects: 1.15.9
      form-data: 4.0.1
      proxy-from-env: 1.1.0
    transitivePeerDependencies:
      - debug
  balanced-match@1.0.2: {}
  base64url@3.0.1: {}
  binary-extensions@2.3.0: {}
  boolbase@1.0.0: {}
  brace-expansion@2.0.1:
    dependencies:
      balanced-match: 1.0.2
  braces@3.0.3:
    dependencies:
      fill-range: 7.1.1
  browserslist@4.24.4:
    dependencies:
      caniuse-lite: 1.0.30001698
      electron-to-chromium: 1.5.96
      node-releases: 2.0.19
      update-browserslist-db: 1.1.2(browserslist@4.24.4)
  caniuse-lite@1.0.30001698: {}
  charenc@0.0.2: {}
  chokidar@3.6.0:
    dependencies:
      anymatch: 3.1.3
      braces: 3.0.3
      glob-parent: 5.1.2
      is-binary-path: 2.1.0
      is-glob: 4.0.3
      normalize-path: 3.0.0
      readdirp: 3.6.0
    optionalDependencies:
      fsevents: 2.3.3
  color-convert@2.0.1:
    dependencies:
      color-name: 1.1.4
  color-name@1.1.4: {}
  combined-stream@1.0.8:
    dependencies:
      delayed-stream: 1.0.0
  commander@7.2.0: {}
  compare-versions@3.6.0: {}
  convert-source-map@2.0.0: {}
  cross-spawn@7.0.6:
    dependencies:
      path-key: 3.1.1
      shebang-command: 2.0.0
      which: 2.0.2
  crypt@0.0.2: {}
  css-select@5.1.0:
    dependencies:
      boolbase: 1.0.0
      css-what: 6.1.0
      domhandler: 5.0.3
      domutils: 3.2.2
      nth-check: 2.1.1
  css-tree@2.2.1:
    dependencies:
      mdn-data: 2.0.28
      source-map-js: 1.2.1
  css-tree@2.3.1:
    dependencies:
      mdn-data: 2.0.30
      source-map-js: 1.2.1
  css-what@6.1.0: {}
  cssesc@3.0.0: {}
  csso@5.0.5:
    dependencies:
      css-tree: 2.2.1
  csstype@3.1.3: {}
  dayjs@1.11.13: {}
  debug@4.4.0:
    dependencies:
      ms: 2.1.3
  delayed-stream@1.0.0: {}
  dom-serializer@2.0.0:
    dependencies:
      domelementtype: 2.3.0
      domhandler: 5.0.3
      entities: 4.5.0
  domelementtype@2.3.0: {}
  domhandler@5.0.3:
    dependencies:
      domelementtype: 2.3.0
  domutils@3.2.2:
    dependencies:
      dom-serializer: 2.0.0
      domelementtype: 2.3.0
      domhandler: 5.0.3
  eastasianwidth@0.2.0: {}
  electron-to-chromium@1.5.96: {}
  emoji-regex@8.0.0: {}
  emoji-regex@9.2.2: {}
  entities@4.5.0: {}
  es-module-lexer@1.6.0: {}
  esbuild@0.17.19:
    optionalDependencies:
      '@esbuild/android-arm': 0.17.19
      '@esbuild/android-arm64': 0.17.19
      '@esbuild/android-x64': 0.17.19
      '@esbuild/darwin-arm64': 0.17.19
      '@esbuild/darwin-x64': 0.17.19
      '@esbuild/freebsd-arm64': 0.17.19
      '@esbuild/freebsd-x64': 0.17.19
      '@esbuild/linux-arm': 0.17.19
      '@esbuild/linux-arm64': 0.17.19
      '@esbuild/linux-ia32': 0.17.19
      '@esbuild/linux-loong64': 0.17.19
      '@esbuild/linux-mips64el': 0.17.19
      '@esbuild/linux-ppc64': 0.17.19
      '@esbuild/linux-riscv64': 0.17.19
      '@esbuild/linux-s390x': 0.17.19
      '@esbuild/linux-x64': 0.17.19
      '@esbuild/netbsd-x64': 0.17.19
      '@esbuild/openbsd-x64': 0.17.19
      '@esbuild/sunos-x64': 0.17.19
      '@esbuild/win32-arm64': 0.17.19
      '@esbuild/win32-ia32': 0.17.19
      '@esbuild/win32-x64': 0.17.19
  esbuild@0.21.5:
    optionalDependencies:
      '@esbuild/aix-ppc64': 0.21.5
      '@esbuild/android-arm': 0.21.5
      '@esbuild/android-arm64': 0.21.5
      '@esbuild/android-x64': 0.21.5
      '@esbuild/darwin-arm64': 0.21.5
      '@esbuild/darwin-x64': 0.21.5
      '@esbuild/freebsd-arm64': 0.21.5
      '@esbuild/freebsd-x64': 0.21.5
      '@esbuild/linux-arm': 0.21.5
      '@esbuild/linux-arm64': 0.21.5
      '@esbuild/linux-ia32': 0.21.5
      '@esbuild/linux-loong64': 0.21.5
      '@esbuild/linux-mips64el': 0.21.5
      '@esbuild/linux-ppc64': 0.21.5
      '@esbuild/linux-riscv64': 0.21.5
      '@esbuild/linux-s390x': 0.21.5
      '@esbuild/linux-x64': 0.21.5
      '@esbuild/netbsd-x64': 0.21.5
      '@esbuild/openbsd-x64': 0.21.5
      '@esbuild/sunos-x64': 0.21.5
      '@esbuild/win32-arm64': 0.21.5
      '@esbuild/win32-ia32': 0.21.5
      '@esbuild/win32-x64': 0.21.5
  escalade@3.2.0: {}
  estree-walker@2.0.2: {}
  fast-glob@3.3.3:
    dependencies:
      '@nodelib/fs.stat': 2.0.5
      '@nodelib/fs.walk': 1.2.8
      glob-parent: 5.1.2
      merge2: 1.4.1
      micromatch: 4.0.8
  fastq@1.19.0:
    dependencies:
      reusify: 1.0.4
  fill-range@7.1.1:
    dependencies:
      to-regex-range: 5.0.1
  follow-redirects@1.15.9: {}
  foreground-child@3.3.0:
    dependencies:
      cross-spawn: 7.0.6
      signal-exit: 4.1.0
  form-data@4.0.1:
    dependencies:
      asynckit: 0.4.0
      combined-stream: 1.0.8
      mime-types: 2.1.35
  fraction.js@4.3.7: {}
  fs-extra@10.1.0:
    dependencies:
      graceful-fs: 4.2.11
      jsonfile: 6.1.0
      universalify: 2.0.1
  fsevents@2.3.3:
    optional: true
  function-bind@1.1.2: {}
  generic-names@4.0.0:
    dependencies:
      loader-utils: 3.3.1
  gensync@1.0.0-beta.2: {}
  glob-parent@5.1.2:
    dependencies:
      is-glob: 4.0.3
  glob@10.4.5:
    dependencies:
      foreground-child: 3.3.0
      jackspeak: 3.4.3
      minimatch: 9.0.5
      minipass: 7.1.2
      package-json-from-dist: 1.0.1
      path-scurry: 1.11.1
  globals@11.12.0: {}
  graceful-fs@4.2.11: {}
  hash-sum@2.0.0: {}
  hasown@2.0.2:
    dependencies:
      function-bind: 1.1.2
  icss-replace-symbols@1.1.0: {}
  icss-utils@5.1.0(postcss@8.5.1):
    dependencies:
      postcss: 8.5.1
  invert-kv@3.0.1: {}
  is-binary-path@2.1.0:
    dependencies:
      binary-extensions: 2.3.0
  is-buffer@1.1.6: {}
  is-core-module@2.16.1:
    dependencies:
      hasown: 2.0.2
  is-extglob@2.1.1: {}
  is-fullwidth-code-point@3.0.0: {}
  is-glob@4.0.3:
    dependencies:
      is-extglob: 2.1.1
  is-number@7.0.0: {}
  isexe@2.0.0: {}
  jackspeak@3.4.3:
    dependencies:
      '@isaacs/cliui': 8.0.2
    optionalDependencies:
      '@pkgjs/parseargs': 0.11.0
  js-pinyin@0.2.7: {}
  js-tokens@4.0.0: {}
  jsesc@3.1.0: {}
  json5@2.2.3: {}
  jsonc-parser@3.3.1: {}
  jsonfile@6.1.0:
    dependencies:
      universalify: 2.0.1
    optionalDependencies:
      graceful-fs: 4.2.11
  lcid@3.1.1:
    dependencies:
      invert-kv: 3.0.1
  lilconfig@2.1.0: {}
  loader-utils@3.3.1: {}
  localstorage-polyfill@1.0.1: {}
  lodash-es@4.17.21: {}
  lodash.camelcase@4.3.0: {}
  lodash@4.17.21: {}
  lru-cache@10.4.3: {}
  lru-cache@5.1.1:
    dependencies:
      yallist: 3.1.1
  magic-string@0.25.9:
    dependencies:
      sourcemap-codec: 1.4.8
  magic-string@0.30.17:
    dependencies:
      '@jridgewell/sourcemap-codec': 1.5.0
  md5@2.3.0:
    dependencies:
      charenc: 0.0.2
      crypt: 0.0.2
      is-buffer: 1.1.6
  mdn-data@2.0.28: {}
  mdn-data@2.0.30: {}
  merge2@1.4.1: {}
  merge@2.1.1: {}
  micromatch@4.0.8:
    dependencies:
      braces: 3.0.3
      picomatch: 2.3.1
  mime-db@1.52.0: {}
  mime-types@2.1.35:
    dependencies:
      mime-db: 1.52.0
  mime@3.0.0: {}
  minimatch@9.0.5:
    dependencies:
      brace-expansion: 2.0.1
  minipass@7.1.2: {}
  module-alias@2.2.3: {}
  ms@2.1.3: {}
  nanoid@3.3.8: {}
  node-releases@2.0.19: {}
  normalize-path@3.0.0: {}
  normalize-range@0.1.2: {}
  nth-check@2.1.1:
    dependencies:
      boolbase: 1.0.0
  os-locale-s-fix@1.0.8-fix-1:
    dependencies:
      lcid: 3.1.1
  package-json-from-dist@1.0.1: {}
  path-key@3.1.1: {}
  path-parse@1.0.7: {}
  path-scurry@1.11.1:
    dependencies:
      lru-cache: 10.4.3
      minipass: 7.1.2
  picocolors@1.1.1: {}
  picomatch@2.3.1: {}
  pify@2.3.0: {}
  pinia@2.3.1(typescript@5.5.4)(vue@3.5.13(typescript@5.5.4)):
    dependencies:
      '@vue/devtools-api': 6.6.4
      vue: 3.5.13(typescript@5.5.4)
      vue-demi: 0.14.10(vue@3.5.13(typescript@5.5.4))
    optionalDependencies:
      typescript: 5.5.4
    transitivePeerDependencies:
      - '@vue/composition-api'
  postcss-import@14.1.0(postcss@8.5.1):
    dependencies:
      postcss: 8.5.1
      postcss-value-parser: 4.2.0
      read-cache: 1.0.0
      resolve: 1.22.10
  postcss-load-config@3.1.4(postcss@8.5.1):
    dependencies:
      lilconfig: 2.1.0
      yaml: 1.10.2
    optionalDependencies:
      postcss: 8.5.1
  postcss-modules-extract-imports@3.1.0(postcss@8.5.1):
    dependencies:
      postcss: 8.5.1
  postcss-modules-local-by-default@4.2.0(postcss@8.5.1):
    dependencies:
      icss-utils: 5.1.0(postcss@8.5.1)
      postcss: 8.5.1
      postcss-selector-parser: 7.1.0
      postcss-value-parser: 4.2.0
  postcss-modules-scope@3.2.1(postcss@8.5.1):
    dependencies:
      postcss: 8.5.1
      postcss-selector-parser: 7.1.0
  postcss-modules-values@4.0.0(postcss@8.5.1):
    dependencies:
      icss-utils: 5.1.0(postcss@8.5.1)
      postcss: 8.5.1
  postcss-modules@4.3.1(postcss@8.5.1):
    dependencies:
      generic-names: 4.0.0
      icss-replace-symbols: 1.1.0
      lodash.camelcase: 4.3.0
      postcss: 8.5.1
      postcss-modules-extract-imports: 3.1.0(postcss@8.5.1)
      postcss-modules-local-by-default: 4.2.0(postcss@8.5.1)
      postcss-modules-scope: 3.2.1(postcss@8.5.1)
      postcss-modules-values: 4.0.0(postcss@8.5.1)
      string-hash: 1.1.3
  postcss-selector-parser@6.1.2:
    dependencies:
      cssesc: 3.0.0
      util-deprecate: 1.0.2
  postcss-selector-parser@7.1.0:
    dependencies:
      cssesc: 3.0.0
      util-deprecate: 1.0.2
  postcss-value-parser@4.2.0: {}
  postcss@8.5.1:
    dependencies:
      nanoid: 3.3.8
      picocolors: 1.1.1
      source-map-js: 1.2.1
  prettier@3.4.2: {}
  proxy-from-env@1.1.0: {}
  queue-microtask@1.2.3: {}
  read-cache@1.0.0:
    dependencies:
      pify: 2.3.0
  readdirp@3.6.0:
    dependencies:
      picomatch: 2.3.1
  resolve@1.22.10:
    dependencies:
      is-core-module: 2.16.1
      path-parse: 1.0.7
      supports-preserve-symlinks-flag: 1.0.0
  reusify@1.0.4: {}
  rollup@4.34.6:
    dependencies:
      '@types/estree': 1.0.6
    optionalDependencies:
      '@rollup/rollup-android-arm-eabi': 4.34.6
      '@rollup/rollup-android-arm64': 4.34.6
      '@rollup/rollup-darwin-arm64': 4.34.6
      '@rollup/rollup-darwin-x64': 4.34.6
      '@rollup/rollup-freebsd-arm64': 4.34.6
      '@rollup/rollup-freebsd-x64': 4.34.6
      '@rollup/rollup-linux-arm-gnueabihf': 4.34.6
      '@rollup/rollup-linux-arm-musleabihf': 4.34.6
      '@rollup/rollup-linux-arm64-gnu': 4.34.6
      '@rollup/rollup-linux-arm64-musl': 4.34.6
      '@rollup/rollup-linux-loongarch64-gnu': 4.34.6
      '@rollup/rollup-linux-powerpc64le-gnu': 4.34.6
      '@rollup/rollup-linux-riscv64-gnu': 4.34.6
      '@rollup/rollup-linux-s390x-gnu': 4.34.6
      '@rollup/rollup-linux-x64-gnu': 4.34.6
      '@rollup/rollup-linux-x64-musl': 4.34.6
      '@rollup/rollup-win32-arm64-msvc': 4.34.6
      '@rollup/rollup-win32-ia32-msvc': 4.34.6
      '@rollup/rollup-win32-x64-msvc': 4.34.6
      fsevents: 2.3.3
  run-parallel@1.2.0:
    dependencies:
      queue-microtask: 1.2.3
  safe-area-insets@1.4.1: {}
  semver@6.3.1: {}
  shebang-command@2.0.0:
    dependencies:
      shebang-regex: 3.0.0
  shebang-regex@3.0.0: {}
  signal-exit@4.1.0: {}
  source-map-js@1.2.1: {}
  source-map@0.6.1: {}
  sourcemap-codec@1.4.8: {}
  string-hash@1.1.3: {}
  string-width@4.2.3:
    dependencies:
      emoji-regex: 8.0.0
      is-fullwidth-code-point: 3.0.0
      strip-ansi: 6.0.1
  string-width@5.1.2:
    dependencies:
      eastasianwidth: 0.2.0
      emoji-regex: 9.2.2
      strip-ansi: 7.1.0
  strip-ansi@6.0.1:
    dependencies:
      ansi-regex: 5.0.1
  strip-ansi@7.1.0:
    dependencies:
      ansi-regex: 6.1.0
  supports-preserve-symlinks-flag@1.0.0: {}
  svgo@3.3.2:
    dependencies:
      '@trysound/sax': 0.2.0
      commander: 7.2.0
      css-select: 5.1.0
      css-tree: 2.3.1
      css-what: 6.1.0
      csso: 5.0.5
      picocolors: 1.1.1
  tapable@2.2.1: {}
  to-regex-range@5.0.1:
    dependencies:
      is-number: 7.0.0
  typescript@5.5.4: {}
  undici-types@6.19.8: {}
  universalify@2.0.1: {}
  update-browserslist-db@1.1.2(browserslist@4.24.4):
    dependencies:
      browserslist: 4.24.4
      escalade: 3.2.0
      picocolors: 1.1.1
  util-deprecate@1.0.2: {}
  vite@5.4.14(@types/node@20.17.17):
    dependencies:
      esbuild: 0.21.5
      postcss: 8.5.1
      rollup: 4.34.6
    optionalDependencies:
      '@types/node': 20.17.17
      fsevents: 2.3.3
  vue-demi@0.14.10(vue@3.5.13(typescript@5.5.4)):
    dependencies:
      vue: 3.5.13(typescript@5.5.4)
  vue-i18n@9.1.9(vue@3.5.13(typescript@5.5.4)):
    dependencies:
      '@intlify/core-base': 9.1.9
      '@intlify/shared': 9.1.9
      '@intlify/vue-devtools': 9.1.9
      '@vue/devtools-api': 6.6.4
      vue: 3.5.13(typescript@5.5.4)
  vue-router@4.5.0(vue@3.5.13(typescript@5.5.4)):
    dependencies:
      '@vue/devtools-api': 6.6.4
      vue: 3.5.13(typescript@5.5.4)
  vue@3.5.13(typescript@5.5.4):
    dependencies:
      '@vue/compiler-dom': 3.5.13
      '@vue/compiler-sfc': 3.5.13
      '@vue/runtime-dom': 3.5.13
      '@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.5.4))
      '@vue/shared': 3.5.13
    optionalDependencies:
      typescript: 5.5.4
  weixin-js-sdk@1.6.5: {}
  which@2.0.2:
    dependencies:
      isexe: 2.0.0
  wrap-ansi@7.0.0:
    dependencies:
      ansi-styles: 4.3.0
      string-width: 4.2.3
      strip-ansi: 6.0.1
  wrap-ansi@8.1.0:
    dependencies:
      ansi-styles: 6.2.1
      string-width: 5.1.2
      strip-ansi: 7.1.0
  xmlhttprequest@1.8.0: {}
  xregexp@3.1.0: {}
  yallist@3.1.1: {}
  yaml@1.10.2: {}
Diff truncated after the above file
router/index.ts static/css/common.scss static/css/iconfont/font.ttf static/css/iconfont/font.woff static/css/iconfont/font.woff2 static/css/iconfont/index.scss static/css/index.scss static/empty/address.png static/empty/comm.png static/empty/coupon.png static/empty/message.png static/empty/network.png static/empty/order.png static/empty/spopping-cart.png static/icon/alipay.png static/icon/carbonpay.png static/icon/tabbar/admin.png static/icon/tabbar/admin2.png static/icon/tabbar/category.png static/icon/tabbar/category2.png static/icon/tabbar/cool.png static/icon/tabbar/cool2.png static/icon/tabbar/home.png static/icon/tabbar/home2.png static/icon/tabbar/my.png static/icon/tabbar/my2.png static/icon/tabbar/recycle.png static/icon/tabbar/recycle2.png static/icon/tabbar/shopping-cart.png static/icon/tabbar/shopping-cart2.png static/icon/wxpay.png static/logo.png static/recycle/appointment.png static/recycle/qrcode.png static/recycle/record.png static/recycle/transfer.png tsconfig.json types/env.d.ts types/shop.d.ts uni.scss uni_modules/cool-app/components/cl-version-about/cl-version-about.vue uni_modules/cool-app/components/cl-version-upgrade/cl-version-upgrade.vue uni_modules/cool-app/config.ts uni_modules/cool-app/hooks/cache.ts uni_modules/cool-app/hooks/index.ts uni_modules/cool-app/hooks/version.ts uni_modules/cool-app/index.ts uni_modules/cool-app/pages/complain/detail.vue uni_modules/cool-app/pages/complain/list.vue uni_modules/cool-app/pages/complain/submit.vue uni_modules/cool-app/pages/feedback/detail.vue uni_modules/cool-app/pages/feedback/list.vue uni_modules/cool-app/pages/feedback/submit.vue uni_modules/cool-app/pages/version/demo.vue uni_modules/cool-app/pages_init.json uni_modules/cool-app/static/bg.png uni_modules/cool-app/utils/index.ts uni_modules/cool-cs/components/msg-item.vue uni_modules/cool-cs/components/msg-list.vue uni_modules/cool-cs/components/tools/emoji.vue uni_modules/cool-cs/components/tools/fn.vue uni_modules/cool-cs/components/tools/index.vue uni_modules/cool-cs/config.ts uni_modules/cool-cs/hooks/index.ts uni_modules/cool-cs/hooks/message.ts uni_modules/cool-cs/hooks/session.ts uni_modules/cool-cs/hooks/socket.ts uni_modules/cool-cs/hooks/tools.ts uni_modules/cool-cs/pages/chat.vue uni_modules/cool-cs/pages_init.json uni_modules/cool-cs/static/add.png uni_modules/cool-cs/static/emoji.png uni_modules/cool-cs/static/emoji/angry-face.png uni_modules/cool-cs/static/emoji/anguished-face.png uni_modules/cool-cs/static/emoji/astonished-face.png uni_modules/cool-cs/static/emoji/confounded-face.png uni_modules/cool-cs/static/emoji/confused-face.png uni_modules/cool-cs/static/emoji/crying-face.png uni_modules/cool-cs/static/emoji/disappointed-but-relieved-face.png uni_modules/cool-cs/static/emoji/disappointed-face.png uni_modules/cool-cs/static/emoji/dizzy-face.png uni_modules/cool-cs/static/emoji/drooling-face.png uni_modules/cool-cs/static/emoji/expressionless-face.png uni_modules/cool-cs/static/emoji/face-savouring-delicious-food.png uni_modules/cool-cs/static/emoji/face-screaming-in-fear.png uni_modules/cool-cs/static/emoji/face-throwing-a-kiss.png uni_modules/cool-cs/static/emoji/face-with-cold-sweat.png uni_modules/cool-cs/static/emoji/face-with-cowboy-hat.png uni_modules/cool-cs/static/emoji/face-with-finger-covering-closed-lips.png uni_modules/cool-cs/static/emoji/face-with-head-bandage.png uni_modules/cool-cs/static/emoji/face-with-look-of-triumph.png uni_modules/cool-cs/static/emoji/face-with-medical-mask.png uni_modules/cool-cs/static/emoji/face-with-monocle.png uni_modules/cool-cs/static/emoji/face-with-one-eyebrow-raised.png uni_modules/cool-cs/static/emoji/face-with-open-mouth-and-cold-sweat.png uni_modules/cool-cs/static/emoji/face-with-open-mouth-vomiting.png uni_modules/cool-cs/static/emoji/face-with-open-mouth.png uni_modules/cool-cs/static/emoji/face-with-party-horn-and-party-hat.png uni_modules/cool-cs/static/emoji/face-with-pleading-eyes.png uni_modules/cool-cs/static/emoji/face-with-rolling-eyes.png uni_modules/cool-cs/static/emoji/face-with-stuck-out-tongue-and-tightly-closed-eyes.png uni_modules/cool-cs/static/emoji/face-with-stuck-out-tongue-and-winking-eye.png uni_modules/cool-cs/static/emoji/face-with-stuck-out-tongue.png uni_modules/cool-cs/static/emoji/face-with-thermometer.png uni_modules/cool-cs/static/emoji/face-with-uneven-eyes-and-wavy-mouth.png uni_modules/cool-cs/static/emoji/face-without-mouth.png uni_modules/cool-cs/static/emoji/fearful-face.png uni_modules/cool-cs/static/emoji/flushed-face.png uni_modules/cool-cs/static/emoji/freezing-face.png uni_modules/cool-cs/static/emoji/frowning-face-with-open-mouth.png uni_modules/cool-cs/static/emoji/grimacing-face.png uni_modules/cool-cs/static/emoji/grinning-face-with-one-large-and-one-small-eye.png uni_modules/cool-cs/static/emoji/grinning-face-with-smiling-eyes.png uni_modules/cool-cs/static/emoji/grinning-face-with-star-eyes.png uni_modules/cool-cs/static/emoji/grinning-face.png uni_modules/cool-cs/static/emoji/hugging-face.png uni_modules/cool-cs/static/emoji/hushed-face.png uni_modules/cool-cs/static/emoji/imp.png uni_modules/cool-cs/static/emoji/kissing-face-with-closed-eyes.png uni_modules/cool-cs/static/emoji/kissing-face-with-smiling-eyes.png uni_modules/cool-cs/static/emoji/kissing-face.png uni_modules/cool-cs/static/emoji/loudly-crying-face.png uni_modules/cool-cs/static/emoji/lying-face.png uni_modules/cool-cs/static/emoji/money-mouth-face.png uni_modules/cool-cs/static/emoji/nauseated-face.png uni_modules/cool-cs/static/emoji/nerd-face.png uni_modules/cool-cs/static/emoji/neutral-face.png uni_modules/cool-cs/static/emoji/overheated-face.png uni_modules/cool-cs/static/emoji/pensive-face.png uni_modules/cool-cs/static/emoji/persevering-face.png uni_modules/cool-cs/static/emoji/pouting-face.png uni_modules/cool-cs/static/emoji/relieved-face.png uni_modules/cool-cs/static/emoji/rolling-on-the-floor-laughing.png uni_modules/cool-cs/static/emoji/serious-face-with-symbols-covering-mouth.png uni_modules/cool-cs/static/emoji/shocked-face-with-exploding-head.png uni_modules/cool-cs/static/emoji/sleeping-face.png uni_modules/cool-cs/static/emoji/sleepy-face.png uni_modules/cool-cs/static/emoji/slightly-frowning-face.png uni_modules/cool-cs/static/emoji/slightly-smiling-face.png uni_modules/cool-cs/static/emoji/smiling-face-with-halo.png uni_modules/cool-cs/static/emoji/smiling-face-with-heart-shaped-eyes.png uni_modules/cool-cs/static/emoji/smiling-face-with-horns.png uni_modules/cool-cs/static/emoji/smiling-face-with-open-mouth-and-smiling-eyes.png uni_modules/cool-cs/static/emoji/smiling-face-with-open-mouth-and-tightly-closed-eyes.png uni_modules/cool-cs/static/emoji/smiling-face-with-open-mouth.png uni_modules/cool-cs/static/emoji/smiling-face-with-smiling-eyes-and-hand-covering-mouth.png uni_modules/cool-cs/static/emoji/smiling-face-with-smiling-eyes-and-three-hearts.png uni_modules/cool-cs/static/emoji/smiling-face-with-smiling-eyes.png uni_modules/cool-cs/static/emoji/smiling-face-with-sunglasses.png uni_modules/cool-cs/static/emoji/smirking-face.png uni_modules/cool-cs/static/emoji/sneezing-face.png uni_modules/cool-cs/static/emoji/thinking-face.png uni_modules/cool-cs/static/emoji/tired-face.png uni_modules/cool-cs/static/emoji/upside-down-face.png uni_modules/cool-cs/static/emoji/weary-face.png uni_modules/cool-cs/static/emoji/white-frowning-face.png uni_modules/cool-cs/static/emoji/white-smiling-face.png uni_modules/cool-cs/static/emoji/winking-face.png uni_modules/cool-cs/static/emoji/worried-face.png uni_modules/cool-cs/static/emoji/zipper-mouth-face.png uni_modules/cool-cs/static/fn-camera.png uni_modules/cool-cs/static/fn-pic.png uni_modules/cool-cs/static/minus.png uni_modules/cool-cs/static/plus.png uni_modules/cool-cs/types/index.d.ts uni_modules/cool-cs/utils/index.ts uni_modules/cool-ui/changelog.md uni_modules/cool-ui/components/cl-action-sheet/cl-action-sheet.vue uni_modules/cool-ui/components/cl-avatar-group/cl-avatar-group.vue uni_modules/cool-ui/components/cl-avatar/cl-avatar.vue uni_modules/cool-ui/components/cl-badge/cl-badge.vue uni_modules/cool-ui/components/cl-banner/cl-banner.vue uni_modules/cool-ui/components/cl-button/cl-button.vue uni_modules/cool-ui/components/cl-captcha/cl-captcha.vue uni_modules/cool-ui/components/cl-card/cl-card.vue uni_modules/cool-ui/components/cl-checkbox-group/cl-checkbox-group.vue uni_modules/cool-ui/components/cl-checkbox/cl-checkbox.vue uni_modules/cool-ui/components/cl-col/cl-col.vue uni_modules/cool-ui/components/cl-confirm/cl-confirm.vue uni_modules/cool-ui/components/cl-countdown/cl-countdown.vue uni_modules/cool-ui/components/cl-dialog/cl-dialog.vue uni_modules/cool-ui/components/cl-divider/cl-divider.vue uni_modules/cool-ui/components/cl-empty/cl-empty.vue uni_modules/cool-ui/components/cl-filter-bar/cl-filter-bar.vue uni_modules/cool-ui/components/cl-filter-item/cl-filter-item.vue uni_modules/cool-ui/components/cl-footer/cl-footer.vue uni_modules/cool-ui/components/cl-form-item/cl-form-item.vue uni_modules/cool-ui/components/cl-form/cl-form.vue uni_modules/cool-ui/components/cl-grid-item/cl-grid-item.vue uni_modules/cool-ui/components/cl-grid/cl-grid.vue uni_modules/cool-ui/components/cl-guide/cl-guide.vue uni_modules/cool-ui/components/cl-icon/cl-icon.vue uni_modules/cool-ui/components/cl-image/cl-image.vue uni_modules/cool-ui/components/cl-input-number/cl-input-number.vue uni_modules/cool-ui/components/cl-input/cl-input.vue uni_modules/cool-ui/components/cl-list-index/cl-list-index.vue uni_modules/cool-ui/components/cl-list-item/cl-list-item.vue uni_modules/cool-ui/components/cl-list/cl-list.vue uni_modules/cool-ui/components/cl-loading-mask/cl-loading-mask.vue uni_modules/cool-ui/components/cl-loading/cl-loading.vue uni_modules/cool-ui/components/cl-loadmore/cl-loadmore.vue uni_modules/cool-ui/components/cl-noticebar/cl-noticebar.vue uni_modules/cool-ui/components/cl-page/cl-page.vue uni_modules/cool-ui/components/cl-pay/ali.png uni_modules/cool-ui/components/cl-pay/cl-pay.vue uni_modules/cool-ui/components/cl-pay/wx.png uni_modules/cool-ui/components/cl-popup/cl-popup.vue uni_modules/cool-ui/components/cl-progress/cl-progress.vue uni_modules/cool-ui/components/cl-radio-group/cl-radio-group.vue uni_modules/cool-ui/components/cl-radio/cl-radio.vue uni_modules/cool-ui/components/cl-rate/cl-rate.vue uni_modules/cool-ui/components/cl-row/cl-row.vue uni_modules/cool-ui/components/cl-scroller/cl-scroller.vue uni_modules/cool-ui/components/cl-search/cl-search.vue uni_modules/cool-ui/components/cl-select-city/cl-select-city.vue uni_modules/cool-ui/components/cl-select-date/cl-select-date.vue uni_modules/cool-ui/components/cl-select-inner/cl-select-inner.vue uni_modules/cool-ui/components/cl-select-inner/config.ts uni_modules/cool-ui/components/cl-select-popup/cl-select-popup.vue uni_modules/cool-ui/components/cl-select-region/cl-select-region.vue uni_modules/cool-ui/components/cl-select/cl-select.vue uni_modules/cool-ui/components/cl-service/cl-service.vue uni_modules/cool-ui/components/cl-share-popup/cl-share-popup.vue uni_modules/cool-ui/components/cl-share/alipay.png uni_modules/cool-ui/components/cl-share/cl-share.vue uni_modules/cool-ui/components/cl-share/qq.png uni_modules/cool-ui/components/cl-share/wx.png uni_modules/cool-ui/components/cl-share/wx2.png uni_modules/cool-ui/components/cl-skeleton/cl-skeleton.vue uni_modules/cool-ui/components/cl-slider-verify/cl-slider-verify.vue uni_modules/cool-ui/components/cl-slider/cl-slider.vue uni_modules/cool-ui/components/cl-status-bar/cl-status-bar.vue uni_modules/cool-ui/components/cl-sticky/cl-sticky.vue uni_modules/cool-ui/components/cl-switch/cl-switch.vue uni_modules/cool-ui/components/cl-tabs/cl-tabs.vue uni_modules/cool-ui/components/cl-tag/cl-tag.vue uni_modules/cool-ui/components/cl-text/cl-text.vue uni_modules/cool-ui/components/cl-textarea/cl-textarea.vue uni_modules/cool-ui/components/cl-timeline-item/cl-timeline-item.vue uni_modules/cool-ui/components/cl-timeline/cl-timeline.vue uni_modules/cool-ui/components/cl-toast/cl-toast.vue uni_modules/cool-ui/components/cl-topbar/cl-topbar.vue uni_modules/cool-ui/components/cl-tree-item/cl-tree-item.vue uni_modules/cool-ui/components/cl-tree-select/cl-tree-select.vue uni_modules/cool-ui/components/cl-tree/cl-tree.vue uni_modules/cool-ui/components/cl-tree/helper.ts uni_modules/cool-ui/components/cl-upload/cl-upload.vue uni_modules/cool-ui/components/cl-video/cl-video.vue uni_modules/cool-ui/components/cl-waterfall-column/cl-waterfall-column.vue uni_modules/cool-ui/components/cl-waterfall/cl-waterfall.vue uni_modules/cool-ui/config.ts uni_modules/cool-ui/data/city-pca.json uni_modules/cool-ui/hooks/index.ts uni_modules/cool-ui/hooks/style.ts uni_modules/cool-ui/index.scss uni_modules/cool-ui/index.ts uni_modules/cool-ui/readme.md uni_modules/cool-ui/static/css/action-sheet.scss uni_modules/cool-ui/static/css/avatar.scss uni_modules/cool-ui/static/css/badge.scss uni_modules/cool-ui/static/css/banner.scss uni_modules/cool-ui/static/css/button.scss uni_modules/cool-ui/static/css/captcha.scss uni_modules/cool-ui/static/css/card.scss uni_modules/cool-ui/static/css/checkbox.scss uni_modules/cool-ui/static/css/col.scss uni_modules/cool-ui/static/css/column.scss uni_modules/cool-ui/static/css/common.scss uni_modules/cool-ui/static/css/confirm.scss uni_modules/cool-ui/static/css/countdown.scss uni_modules/cool-ui/static/css/dialog.scss uni_modules/cool-ui/static/css/divider.scss uni_modules/cool-ui/static/css/empty.scss uni_modules/cool-ui/static/css/filter-bar.scss uni_modules/cool-ui/static/css/footer.scss uni_modules/cool-ui/static/css/form.scss uni_modules/cool-ui/static/css/grid.scss uni_modules/cool-ui/static/css/guide.scss uni_modules/cool-ui/static/css/icon.scss uni_modules/cool-ui/static/css/image.scss uni_modules/cool-ui/static/css/index.scss uni_modules/cool-ui/static/css/input-number.scss uni_modules/cool-ui/static/css/input.scss uni_modules/cool-ui/static/css/list-index.scss uni_modules/cool-ui/static/css/list.scss uni_modules/cool-ui/static/css/loading-mask.scss uni_modules/cool-ui/static/css/loading.scss uni_modules/cool-ui/static/css/loadmore.scss uni_modules/cool-ui/static/css/noticebar.scss uni_modules/cool-ui/static/css/page.scss uni_modules/cool-ui/static/css/pay.scss uni_modules/cool-ui/static/css/popup.scss uni_modules/cool-ui/static/css/progress.scss uni_modules/cool-ui/static/css/radio.scss uni_modules/cool-ui/static/css/rate.scss uni_modules/cool-ui/static/css/row.scss uni_modules/cool-ui/static/css/scroller.scss uni_modules/cool-ui/static/css/search.scss uni_modules/cool-ui/static/css/select-city.scss uni_modules/cool-ui/static/css/select-date.scss uni_modules/cool-ui/static/css/select-inner.scss uni_modules/cool-ui/static/css/select-popup.scss uni_modules/cool-ui/static/css/select.scss uni_modules/cool-ui/static/css/share.scss uni_modules/cool-ui/static/css/skeleton.scss uni_modules/cool-ui/static/css/slider-verify.scss uni_modules/cool-ui/static/css/status-bar.scss uni_modules/cool-ui/static/css/steps.scss uni_modules/cool-ui/static/css/sticky.scss uni_modules/cool-ui/static/css/switch.scss uni_modules/cool-ui/static/css/tabs.scss uni_modules/cool-ui/static/css/tag.scss uni_modules/cool-ui/static/css/text.scss uni_modules/cool-ui/static/css/textarea.scss uni_modules/cool-ui/static/css/timeline.scss uni_modules/cool-ui/static/css/toast.scss uni_modules/cool-ui/static/css/topbar.scss uni_modules/cool-ui/static/css/tree.scss uni_modules/cool-ui/static/css/upload.scss uni_modules/cool-ui/static/css/video.scss uni_modules/cool-ui/static/css/waterfall.scss uni_modules/cool-ui/static/iconfont/iconfont.scss uni_modules/cool-ui/static/iconfont/iconfont.ttf uni_modules/cool-ui/static/iconfont/iconfont.woff uni_modules/cool-ui/static/iconfont/iconfont.woff2 uni_modules/cool-ui/static/iconfont/index.scss uni_modules/cool-ui/theme.scss uni_modules/cool-ui/types/action-sheet.d.ts uni_modules/cool-ui/types/confirm.d.ts uni_modules/cool-ui/types/form.d.ts uni_modules/cool-ui/types/index.d.ts uni_modules/cool-ui/types/list-index.d.ts uni_modules/cool-ui/types/loading.d.ts uni_modules/cool-ui/types/page.d.ts uni_modules/cool-ui/types/pay.d.ts uni_modules/cool-ui/types/share.d.ts uni_modules/cool-ui/types/timeline.d.ts uni_modules/cool-ui/types/toast.d.ts uni_modules/cool-ui/types/tree.d.ts uni_modules/cool-ui/types/waterfall.d.ts uni_modules/cool-ui/utils/async-validator.ts uni_modules/cool-ui/utils/index.ts uni_modules/mp-html/README.md uni_modules/mp-html/changelog.md uni_modules/mp-html/components/mp-html/mp-html.vue uni_modules/mp-html/components/mp-html/node/node.vue uni_modules/mp-html/components/mp-html/parser.js uni_modules/mp-html/package.json uni_modules/mp-html/static/app-plus/mp-html/js/handler.js uni_modules/mp-html/static/app-plus/mp-html/js/uni.webview.min.js uni_modules/mp-html/static/app-plus/mp-html/local.html vite.config.ts