init
This commit is contained in:
commit
d2f735d2f6
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
.DS_Store
|
||||
node_modules/
|
||||
unpackage/
|
||||
dist/
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.project
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
||||
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
|
||||
|
||||
|
||||
types/auto-imports.d.ts
|
||||
types/components.d.ts
|
36
.hbuilderx/launch.json
Normal file
36
.hbuilderx/launch.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
8
.hintrc
Normal file
8
.hintrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": [
|
||||
"development"
|
||||
],
|
||||
"hints": {
|
||||
"typescript-config/is-valid": "off"
|
||||
}
|
||||
}
|
8
.prettierrc
Normal file
8
.prettierrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"useTabs": true,
|
||||
"semi": true,
|
||||
"jsxBracketSameLine": true,
|
||||
"singleQuote": false,
|
||||
"printWidth": 100
|
||||
}
|
32
App.vue
Normal file
32
App.vue
Normal 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>
|
67
README.md
Normal file
67
README.md
Normal file
@ -0,0 +1,67 @@
|
||||
# COOL-UNI
|
||||
|
||||
让你不用想太多就能开发完功能,7.0 携带 vue3、vite、ts、pinia 等众多新特性细节曝光!
|
||||
|
||||
## 更快
|
||||
|
||||
- 启动快:基于 `vite`,快速的冷启动,不需要等待打包,即时的热模块更新,真正的按需编译。
|
||||
|
||||
- 开发快:`eps` 模式下,无须手动添加接口请求方法。
|
||||
|
||||
## 更强
|
||||
|
||||
内置请求、路由、文件上传、组件通信、缓存等方法及 ui 库和 hooks
|
||||
|
||||
```html
|
||||
<script lang="ts" setup>
|
||||
import { useCool } from "/@/cool";
|
||||
import { useUi } from "/$/cool-ui";
|
||||
|
||||
const { service, router, mitt, storage, upload } = useCool();
|
||||
const ui = useUi();
|
||||
|
||||
// 请求
|
||||
service.test.page().then((res) => {
|
||||
consoe.log(res);
|
||||
});
|
||||
|
||||
// 跳转
|
||||
router.push({
|
||||
path: "/pages/goods/info",
|
||||
// 方式1
|
||||
query: {
|
||||
id: 1,
|
||||
},
|
||||
// 方式2
|
||||
params: {
|
||||
id: 2,
|
||||
},
|
||||
});
|
||||
|
||||
// ui全局事件
|
||||
ui.showLoading();
|
||||
ui.showToast();
|
||||
|
||||
// 通信
|
||||
mitt.emit("refresh", { page: 1 });
|
||||
mitt.on("refresh", (params) => {});
|
||||
|
||||
// 储存
|
||||
storage.set("token", "a123huis");
|
||||
|
||||
// 文件上传
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sourceType: ["album", "camera"],
|
||||
success(res) {
|
||||
upload(res.tempFiles[0]).then((url) => {
|
||||
console.log(url);
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## 更细
|
||||
|
||||
全面的代码描述
|
3
androidPrivacy.json
Normal file
3
androidPrivacy.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"prompt" : "template"
|
||||
}
|
1687
build/cool/eps.d.ts
vendored
Normal file
1687
build/cool/eps.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
build/cool/eps.json
Normal file
1
build/cool/eps.json
Normal file
@ -0,0 +1 @@
|
||||
[{"prefix":"/app/app/complain","name":"AppComplainEntity","api":[{"method":"post","path":"/submit"},{"method":"post","path":"/page"},{"method":"get","path":"/info"}]},{"prefix":"/app/app/feedback","name":"AppFeedbackEntity","api":[{"method":"post","path":"/submit"},{"method":"post","path":"/page"},{"method":"get","path":"/info"}]},{"prefix":"/app/app/goods","name":"AppGoodsEntity","api":[{"method":"post","path":"/list"}]},{"prefix":"/app/app/version","name":"AppVersionEntity","api":[{"method":"get","path":"/check"}]},{"prefix":"/app/base/comm","name":"","api":[{"method":"get","path":"/uploadMode"},{"method":"post","path":"/upload"},{"method":"get","path":"/param"},{"method":"get","path":"/eps"}]},{"prefix":"/app/cs/msg","name":"CsMsgEntity","api":[{"method":"get","path":"/unreadCount"},{"method":"post","path":"/read"},{"method":"post","path":"/page"}]},{"prefix":"/app/cs/session","name":"","api":[{"method":"get","path":"/detail"},{"method":"post","path":"/create"}]},{"prefix":"/app/dict/info","name":"","api":[{"method":"post","path":"/data"}]},{"prefix":"/app/goods/comment","name":"GoodsCommentEntity","api":[{"method":"post","path":"/submit"},{"method":"post","path":"/page"}]},{"prefix":"/app/goods/info","name":"GoodsInfoEntity","api":[{"method":"post","path":"/page"},{"method":"get","path":"/info"}]},{"prefix":"/app/goods/searchKeyword","name":"GoodsSearchKeywordEntity","api":[{"method":"post","path":"/delete"},{"method":"post","path":"/update"},{"method":"get","path":"/info"},{"method":"post","path":"/list"},{"method":"post","path":"/page"},{"method":"post","path":"/add"}]},{"prefix":"/app/goods/spec","name":"GoodsSpecEntity","api":[{"method":"post","path":"/delete"},{"method":"post","path":"/update"},{"method":"get","path":"/info"},{"method":"post","path":"/list"},{"method":"post","path":"/page"},{"method":"post","path":"/add"}]},{"prefix":"/app/goods/type","name":"GoodsTypeEntity","api":[{"method":"post","path":"/list"}]},{"prefix":"/app/info/banner","name":"InfoBannerEntity","api":[{"method":"post","path":"/list"}]},{"prefix":"/app/market/coupon/info","name":"MarketCouponInfoEntity","api":[{"method":"post","path":"/page"}]},{"prefix":"/app/market/coupon/user","name":"MarketCouponUserEntity","api":[{"method":"post","path":"/receive"},{"method":"post","path":"/delete"},{"method":"post","path":"/update"},{"method":"get","path":"/info"},{"method":"post","path":"/list"},{"method":"post","path":"/page"},{"method":"post","path":"/add"}]},{"prefix":"/app/order/info","name":"OrderInfoEntity","api":[{"method":"get","path":"/logistics"},{"method":"get","path":"/userCount"},{"method":"get","path":"/confirm"},{"method":"post","path":"/create"},{"method":"post","path":"/cancel"},{"method":"post","path":"/refund"},{"method":"post","path":"/update"},{"method":"get","path":"/info"},{"method":"post","path":"/page"}]},{"prefix":"/app/order/pay","name":"","api":[{"method":"post","path":"/wxMiniPay"},{"method":"post","path":"/wxNotify"},{"method":"post","path":"/wxAppPay"},{"method":"post","path":"/wxMpPay"}]},{"prefix":"/app/user/address","name":"UserAddressEntity","api":[{"method":"get","path":"/default"},{"method":"post","path":"/delete"},{"method":"post","path":"/update"},{"method":"get","path":"/info"},{"method":"post","path":"/list"},{"method":"post","path":"/page"},{"method":"post","path":"/add"}]},{"prefix":"/app/user/comm","name":"","api":[{"method":"post","path":"/wxMpConfig"}]},{"prefix":"/app/user/info","name":"UserInfoEntity","api":[{"method":"post","path":"/updatePassword"},{"method":"post","path":"/updatePerson"},{"method":"post","path":"/bindPhone"},{"method":"post","path":"/miniPhone"},{"method":"get","path":"/person"},{"method":"post","path":"/logoff"}]},{"prefix":"/app/user/login","name":"","api":[{"method":"post","path":"/refreshToken"},{"method":"post","path":"/password"},{"method":"get","path":"/captcha"},{"method":"post","path":"/smsCode"},{"method":"post","path":"/wxApp"},{"method":"post","path":"/phone"},{"method":"post","path":"/mini"},{"method":"post","path":"/mp"}]},{"prefix":"/","name":"","api":[{"method":"get","path":"/"}]},{"prefix":"/app/test","name":"TestEntity","api":[{"path":"/page"},{"path":"/list"},{"path":"/info"},{"path":"/delete"},{"path":"/update"},{"path":"/add"}]}]
|
71
components/address/item.vue
Normal file
71
components/address/item.vue
Normal 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>
|
150
components/address/select.vue
Normal file
150
components/address/select.vue
Normal 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>
|
58
components/agree-btn.vue
Normal file
58
components/agree-btn.vue
Normal file
@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<cl-checkbox :size="34" v-model="agree" round>
|
||||
<view class="agree-btn">
|
||||
已阅读并同意
|
||||
<text @tap.stop="toDoc('用户协议', 'userAgreement')">用户协议</text>
|
||||
和
|
||||
<text @tap.stop="toDoc('隐私政策', 'privacyPolicy')">隐私政策</text>
|
||||
</view>
|
||||
</cl-checkbox>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useCool } from "/@/cool";
|
||||
import { useUi } from "/$/cool-ui";
|
||||
|
||||
const { router } = useCool();
|
||||
const ui = useUi();
|
||||
|
||||
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("请先勾选同意后再进行登录");
|
||||
}
|
||||
|
||||
return agree.value;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
check,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.agree-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999999;
|
||||
letter-spacing: 1rpx;
|
||||
|
||||
text {
|
||||
color: $cl-color-primary;
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
166
components/coupon/get.vue
Normal file
166
components/coupon/get.vue
Normal 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>
|
BIN
components/coupon/item/bg.png
Normal file
BIN
components/coupon/item/bg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
168
components/coupon/item/index.vue
Normal file
168
components/coupon/item/index.vue
Normal 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>
|
134
components/coupon/select.vue
Normal file
134
components/coupon/select.vue
Normal 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>
|
96
components/goods/cover.vue
Normal file
96
components/goods/cover.vue
Normal 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>
|
202
components/goods/group.vue
Normal file
202
components/goods/group.vue
Normal 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>
|
68
components/goods/item.vue
Normal file
68
components/goods/item.vue
Normal 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>
|
393
components/goods/spec.vue
Normal file
393
components/goods/spec.vue
Normal 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>
|
229
components/sms-btn.vue
Normal file
229
components/sms-btn.vue
Normal file
@ -0,0 +1,229 @@
|
||||
<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="获取短信验证码"></cl-text>
|
||||
<cl-icon :size="32" name="close" @tap="close"></cl-icon>
|
||||
</view>
|
||||
|
||||
<view class="row">
|
||||
<cl-input
|
||||
type="number"
|
||||
v-model="form.code"
|
||||
placeholder="验证码"
|
||||
: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"
|
||||
>
|
||||
发送短信
|
||||
</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";
|
||||
|
||||
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 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 ? `${countdown.value}s后重新获取` : "获取验证码",
|
||||
);
|
||||
|
||||
// 表单
|
||||
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("短信已发送,请查收");
|
||||
startCountdown();
|
||||
close();
|
||||
emit("success");
|
||||
})
|
||||
.catch((err) => {
|
||||
ui.showToast(err.message);
|
||||
getCaptcha();
|
||||
});
|
||||
|
||||
captcha.sending = false;
|
||||
} else {
|
||||
ui.showToast("请填写验证码");
|
||||
}
|
||||
}
|
||||
|
||||
// 获取图片验证码
|
||||
async function getCaptcha() {
|
||||
clear();
|
||||
captcha.loading = true;
|
||||
|
||||
await service.user.login
|
||||
.captcha({ type: "png", color: "#000000", 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("请填写正确的手机号格式");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭
|
||||
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>
|
17
config/dev.ts
Normal file
17
config/dev.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { proxy } from "./proxy";
|
||||
|
||||
export default {
|
||||
// 根地址
|
||||
host: proxy["/dev/"].target,
|
||||
|
||||
// 请求地址
|
||||
get baseUrl() {
|
||||
// #ifdef H5
|
||||
return "/dev";
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
return this.host + "";
|
||||
// #endif
|
||||
},
|
||||
};
|
40
config/index.ts
Normal file
40
config/index.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import dev from "./dev";
|
||||
import prod from "./prod";
|
||||
|
||||
// 是否开发模式
|
||||
export const isDev = import.meta.env.MODE === "development";
|
||||
|
||||
// 配置
|
||||
export const config = {
|
||||
// 应用信息
|
||||
app: {
|
||||
// 应用名称
|
||||
name: "酷卖",
|
||||
// 应用描述
|
||||
desc: "能用钱解决的事,就不要客气",
|
||||
// 页面配置
|
||||
pages: {
|
||||
login: "/pages/user/login",
|
||||
},
|
||||
wx: {
|
||||
debug: false,
|
||||
},
|
||||
},
|
||||
|
||||
// 调试
|
||||
test: {
|
||||
token: "",
|
||||
mock: false,
|
||||
eps: true,
|
||||
},
|
||||
|
||||
// 忽略
|
||||
ignore: {
|
||||
token: [],
|
||||
},
|
||||
|
||||
// 当前环境
|
||||
...(isDev ? dev : prod),
|
||||
};
|
||||
|
||||
export * from "./proxy";
|
18
config/prod.ts
Normal file
18
config/prod.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { proxy } from "./proxy";
|
||||
|
||||
export default {
|
||||
// 根地址
|
||||
host: proxy["/prod/"].target,
|
||||
|
||||
// 请求地址
|
||||
get baseUrl() {
|
||||
// #ifdef H5
|
||||
return "/api";
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
return "https://cool-mall-dev.cool-js.cloud";
|
||||
// return this.host + "/api";
|
||||
// #endif
|
||||
},
|
||||
};
|
13
config/proxy.ts
Normal file
13
config/proxy.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const proxy = {
|
||||
"/dev/": {
|
||||
target: "http://127.0.0.1:8001",
|
||||
changeOrigin: true,
|
||||
rewrite: (path: string) => path.replace(/^\/dev/, ""),
|
||||
},
|
||||
|
||||
"/prod/": {
|
||||
target: "https://cool-mall-dev.cool-js.cloud",
|
||||
changeOrigin: true,
|
||||
rewrite: (path: string) => path.replace(/^\/prod/, ""),
|
||||
},
|
||||
};
|
138
cool/bootstrap/eps.ts
Normal file
138
cool/bootstrap/eps.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import { merge } from "lodash-es";
|
||||
import { BaseService, service } from "../service";
|
||||
import { path2Obj } from "../utils";
|
||||
import { config, 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新事件
|
||||
function onUpdate() {
|
||||
// 设置 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");
|
||||
}
|
||||
}
|
||||
|
||||
export function createEps() {
|
||||
// 更新 eps
|
||||
onUpdate();
|
||||
|
||||
// #ifdef H5
|
||||
// 开发环境下,生成本地 service 的类型描述文件
|
||||
if (isDev && config.test.eps) {
|
||||
const list = services.map((s) => {
|
||||
const api = Array.from(
|
||||
new Set([
|
||||
...Object.getOwnPropertyNames(s.constructor.prototype),
|
||||
"page",
|
||||
"list",
|
||||
"info",
|
||||
"delete",
|
||||
"update",
|
||||
"add",
|
||||
]),
|
||||
)
|
||||
.filter((e) => !["constructor", "namespace"].includes(e))
|
||||
.map((e) => {
|
||||
return {
|
||||
path: `/${e}`,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
api,
|
||||
module: s.namespace.split("/")[0],
|
||||
name: s.constructor.name + "Entity",
|
||||
prefix: `/app/${s.namespace}`,
|
||||
};
|
||||
});
|
||||
|
||||
service.request({
|
||||
url: "/__cool_eps",
|
||||
method: "POST",
|
||||
proxy: false,
|
||||
data: {
|
||||
list,
|
||||
},
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
// 监听 vite 触发事件
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.on("eps-update", ({ service }) => {
|
||||
if (service) {
|
||||
eps.service = service;
|
||||
}
|
||||
|
||||
onUpdate();
|
||||
});
|
||||
}
|
15
cool/bootstrap/index.ts
Normal file
15
cool/bootstrap/index.ts
Normal 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();
|
||||
}
|
38
cool/bootstrap/modules.ts
Normal file
38
cool/bootstrap/modules.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
28
cool/hooks/app.ts
Normal file
28
cool/hooks/app.ts
Normal 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(),
|
||||
};
|
||||
}
|
11
cool/hooks/comm.ts
Normal file
11
cool/hooks/comm.ts
Normal 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 };
|
||||
}
|
23
cool/hooks/hmr.ts
Normal file
23
cool/hooks/hmr.ts
Normal 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];
|
||||
}
|
||||
};
|
21
cool/hooks/index.ts
Normal file
21
cool/hooks/index.ts
Normal 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";
|
168
cool/hooks/pager.ts
Normal file
168
cool/hooks/pager.ts
Normal file
@ -0,0 +1,168 @@
|
||||
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>() {
|
||||
const { proxy }: any = getCurrentInstance();
|
||||
const ui = useUi();
|
||||
|
||||
// 分页信息
|
||||
const pager = reactive({
|
||||
params: {},
|
||||
pagination: {
|
||||
page: 1,
|
||||
size: 20,
|
||||
total: 0,
|
||||
},
|
||||
list: [] as T[],
|
||||
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();
|
||||
}
|
||||
|
||||
// 是否注册,避免在组件中重复注入事件问题
|
||||
let isReg = true;
|
||||
|
||||
// 上拉加载
|
||||
onReachBottom(() => {
|
||||
if (isReg) {
|
||||
if (!pager.finished) {
|
||||
refresh({ page: pager.pagination.page + 1 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 下拉刷新
|
||||
onPullDownRefresh(async () => {
|
||||
if (isReg) {
|
||||
await refresh({ page: 1 });
|
||||
uni.stopPullDownRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
// 组件销毁
|
||||
onUnmounted(close);
|
||||
|
||||
// 离开页面
|
||||
onUnload(close);
|
||||
|
||||
return {
|
||||
pager,
|
||||
list,
|
||||
onData,
|
||||
onRefresh,
|
||||
onPullDownRefresh,
|
||||
onReachBottom,
|
||||
};
|
||||
}
|
288
cool/hooks/wx.ts
Normal file
288
cool/hooks/wx.ts
Normal file
@ -0,0 +1,288 @@
|
||||
import { ref } from "vue";
|
||||
import { onReady, onShow } from "@dcloudio/uni-app";
|
||||
import { config } from "/@/config";
|
||||
import { getUrlParam, storage } from "../utils";
|
||||
import { service } from "../service";
|
||||
|
||||
// #ifdef H5
|
||||
import wx from "weixin-js-sdk";
|
||||
// #endif
|
||||
|
||||
export function useWx() {
|
||||
const { platform } = uni.getSystemInfoSync();
|
||||
|
||||
// 授权码
|
||||
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: "请在微信浏览器中打开",
|
||||
});
|
||||
}
|
||||
|
||||
wx.chooseWXPay({
|
||||
...params,
|
||||
timestamp: params.timeStamp,
|
||||
success() {
|
||||
resolve();
|
||||
},
|
||||
complete(e: { errMsg: string }) {
|
||||
switch (e.errMsg) {
|
||||
case "chooseWXPay:cancel":
|
||||
reject({ message: "已取消支付" });
|
||||
break;
|
||||
|
||||
default:
|
||||
reject({ message: "支付失败" });
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 微信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: "已取消支付" });
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 微信小程序登录
|
||||
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: "授权信息仅用于用户登录",
|
||||
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: "登录授权失败",
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 微信小程序支付
|
||||
function miniPay(params: any): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.requestPayment({
|
||||
provider: "wxpay",
|
||||
...params,
|
||||
success() {
|
||||
resolve();
|
||||
},
|
||||
fail() {
|
||||
reject({ message: "已取消支付" });
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
getCode();
|
||||
});
|
||||
|
||||
onReady(() => {
|
||||
getMpConfig();
|
||||
});
|
||||
|
||||
return {
|
||||
code,
|
||||
getCode,
|
||||
isWxBrowser,
|
||||
hasApp,
|
||||
downloadApp,
|
||||
mpConfig,
|
||||
mpAuth,
|
||||
mpLogin,
|
||||
mpPay,
|
||||
miniLogin,
|
||||
miniPay,
|
||||
appLogin,
|
||||
appPay,
|
||||
};
|
||||
}
|
9
cool/index.ts
Normal file
9
cool/index.ts
Normal 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";
|
21
cool/module/index.ts
Normal file
21
cool/module/index.ts
Normal 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 };
|
325
cool/router/index.ts
Normal file
325
cool/router/index.ts
Normal 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);
|
||||
},
|
||||
|
||||
// tabbar
|
||||
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("不存在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 };
|
123
cool/service/base.ts
Normal file
123
cool/service/base.ts
Normal file
@ -0,0 +1,123 @@
|
||||
// @ts-nocheck
|
||||
import { has } from "lodash-es";
|
||||
import { isDev, config } from "../../config";
|
||||
import request from "./request";
|
||||
|
||||
export function Service(
|
||||
value:
|
||||
| {
|
||||
namespace?: string;
|
||||
url?: string;
|
||||
mock?: boolean;
|
||||
}
|
||||
| string
|
||||
) {
|
||||
return function (target: any) {
|
||||
// 命名
|
||||
if (typeof value == "string") {
|
||||
target.prototype.namespace = value;
|
||||
}
|
||||
|
||||
// 复杂项
|
||||
if (has(value, "namespace")) {
|
||||
target.prototype.namespace = value.namespace;
|
||||
target.prototype.mock = value.mock;
|
||||
|
||||
if (value.url) {
|
||||
target.prototype.url = value.url;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class BaseService {
|
||||
constructor(
|
||||
options = {} as {
|
||||
namespace?: string;
|
||||
}
|
||||
) {
|
||||
if (options?.namespace) {
|
||||
this.namespace = options.namespace;
|
||||
}
|
||||
}
|
||||
|
||||
request(options: any = {}) {
|
||||
if (!options.params) options.params = {};
|
||||
|
||||
let ns = "";
|
||||
|
||||
// 是否 mock 模式
|
||||
if (this.mock || config.test.mock) {
|
||||
// 测试
|
||||
} else {
|
||||
if (isDev) {
|
||||
ns = this.proxy || config.baseUrl;
|
||||
} else {
|
||||
ns = this.proxy ? this.url : config.baseUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// 拼接前缀
|
||||
if (this.namespace) {
|
||||
ns += "/" + this.namespace;
|
||||
}
|
||||
|
||||
// 处理地址
|
||||
if (options.proxy === undefined || options.proxy) {
|
||||
options.url = ns + options.url;
|
||||
}
|
||||
|
||||
// 处理参数
|
||||
options.data =
|
||||
options.method?.toLocaleUpperCase() == "POST" ? options.data : options.params;
|
||||
|
||||
return request(options);
|
||||
}
|
||||
|
||||
list(data: any) {
|
||||
return this.request({
|
||||
url: "/list",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
page(data: any) {
|
||||
return this.request({
|
||||
url: "/page",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
info(params: any) {
|
||||
return this.request({
|
||||
url: "/info",
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
update(data: any) {
|
||||
return this.request({
|
||||
url: "/update",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
delete(data: any) {
|
||||
return this.request({
|
||||
url: "/delete",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
add(data: any) {
|
||||
return this.request({
|
||||
url: "/add",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
9
cool/service/index.ts
Normal file
9
cool/service/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { BaseService } from "./base";
|
||||
|
||||
// service 数据集合
|
||||
// @ts-ignore
|
||||
export const service: Eps.Service = {
|
||||
request: new BaseService().request,
|
||||
};
|
||||
|
||||
export * from "./base";
|
132
cool/service/request.ts
Normal file
132
cool/service/request.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { useStore } from "../store";
|
||||
import { router } from "../router";
|
||||
import { isDev, config } from "../../config";
|
||||
import { storage } from "../utils";
|
||||
|
||||
// 请求队列
|
||||
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,
|
||||
...options.header,
|
||||
},
|
||||
|
||||
success(res) {
|
||||
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: "服务异常",
|
||||
});
|
||||
}
|
||||
|
||||
// 未找到
|
||||
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: "服务异常" });
|
||||
}
|
||||
},
|
||||
|
||||
fail(err) {
|
||||
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();
|
||||
});
|
||||
}
|
22
cool/service/sign.ts
Normal file
22
cool/service/sign.ts
Normal 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 };
|
66
cool/store/dict.ts
Normal file
66
cool/store/dict.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, reactive, toRaw } from "vue";
|
||||
import { deepTree } 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: string) {
|
||||
return computed(() => data[name]).value || [];
|
||||
}
|
||||
|
||||
// 获取名称
|
||||
function getLabel(name: string | 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?: string[]) {
|
||||
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 = e.value !== undefined ? e.value : e.id;
|
||||
});
|
||||
|
||||
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 };
|
9
cool/store/index.ts
Normal file
9
cool/store/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { useUserStore } from "./user";
|
||||
import { useDictStore } from "./dict";
|
||||
|
||||
export function useStore() {
|
||||
return {
|
||||
user: useUserStore(),
|
||||
dict: useDictStore(),
|
||||
};
|
||||
}
|
94
cool/store/user.ts
Normal file
94
cool/store/user.ts
Normal 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 };
|
43
cool/types/index.d.ts
vendored
Normal file
43
cool/types/index.d.ts
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
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 {
|
||||
interface Item {
|
||||
id: string;
|
||||
label: string;
|
||||
value: any;
|
||||
children?: Item[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface Data {
|
||||
[key: string]: Item[];
|
||||
}
|
||||
}
|
48
cool/upload/comm.ts
Normal file
48
cool/upload/comm.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { isArray, has } from "lodash-es";
|
||||
|
||||
function parse(rules: string[], { url, size }: any) {
|
||||
if (!url) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let h = 0;
|
||||
let w = 0;
|
||||
|
||||
if (isArray(size)) {
|
||||
h = size[0];
|
||||
w = size[1];
|
||||
} else if (has(size, "h")) {
|
||||
h = size.h;
|
||||
w = size.w;
|
||||
|
||||
if (size.m) {
|
||||
rules.push(`m_${size.m}`);
|
||||
}
|
||||
} else {
|
||||
h = w = size;
|
||||
}
|
||||
|
||||
url += url.includes("?") ? "&" : "?";
|
||||
|
||||
if (h) {
|
||||
rules.push(`h_${h}`);
|
||||
}
|
||||
|
||||
if (w) {
|
||||
rules.push(`w_${w}`);
|
||||
}
|
||||
|
||||
return `${url}${rules.join(",")}`;
|
||||
}
|
||||
|
||||
type Size = number | number[] | { h?: number; w?: number; m?: string };
|
||||
|
||||
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 };
|
141
cool/upload/index.ts
Normal file
141
cool/upload/index.ts
Normal 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,
|
||||
};
|
||||
}
|
603
cool/utils/canvas.ts
Normal file
603
cool/utils/canvas.ts
Normal 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 };
|
163
cool/utils/comm.ts
Normal file
163
cool/utils/comm.ts
Normal file
@ -0,0 +1,163 @@
|
||||
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);
|
||||
});
|
||||
}
|
4
cool/utils/index.ts
Normal file
4
cool/utils/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./comm";
|
||||
export * from "./ui";
|
||||
export * from "./canvas";
|
||||
export * from "./storage";
|
75
cool/utils/storage.ts
Normal file
75
cool/utils/storage.ts
Normal 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;
|
||||
},
|
||||
};
|
78
cool/utils/ui.ts
Normal file
78
cool/utils/ui.ts
Normal file
@ -0,0 +1,78 @@
|
||||
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;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
||||
// if (!d.value && n-- > 0) {
|
||||
// setTimeout(() => {
|
||||
// d.value = next();
|
||||
// }, 50);
|
||||
// }
|
||||
|
||||
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);
|
||||
}
|
29
hooks/index.ts
Normal file
29
hooks/index.ts
Normal 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";
|
47
hooks/order.ts
Normal file
47
hooks/order.ts
Normal file
@ -0,0 +1,47 @@
|
||||
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",
|
||||
// },
|
||||
];
|
||||
|
||||
async function toPay(orderId: number, type = "wxpay") {
|
||||
// #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,
|
||||
};
|
||||
}
|
70
hooks/shopping-cart.ts
Normal file
70
hooks/shopping-cart.ts
Normal 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,
|
||||
};
|
||||
});
|
115
hooks/spec.ts
Normal file
115
hooks/spec.ts
Normal 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,
|
||||
};
|
||||
});
|
14
index.html
Normal file
14
index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
|
||||
<title></title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/main.js"></script>
|
||||
</body>
|
||||
</html>
|
18
main.js
Normal file
18
main.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { createSSRApp } from "vue";
|
||||
import { bootstrap } from "/@/cool/bootstrap";
|
||||
import App from "./App.vue";
|
||||
import "./router";
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App);
|
||||
|
||||
// 启动
|
||||
bootstrap(app);
|
||||
|
||||
// 隐藏底部导航栏
|
||||
uni.hideTabBar();
|
||||
|
||||
return {
|
||||
app,
|
||||
};
|
||||
}
|
174
manifest.json
Normal file
174
manifest.json
Normal file
@ -0,0 +1,174 @@
|
||||
{
|
||||
"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" : {}
|
||||
},
|
||||
"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" : "请放心,开启权限不会获取您在其他站点的隐私信息,该权限仅用于标识设备并保障服务安全与提示浏览体验"
|
||||
}
|
||||
},
|
||||
"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" : {}
|
||||
},
|
||||
"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" : false,
|
||||
"es6" : true
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
29
package.json
Normal file
29
package.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "cool-uni",
|
||||
"version": "7.3.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "3.0.0-3081220230817001",
|
||||
"@hyoga/uni-socket.io": "^3.0.4",
|
||||
"dayjs": "^1.11.10",
|
||||
"js-pinyin": "^0.2.5",
|
||||
"lodash-es": "^4.17.21",
|
||||
"md5": "^2.3.0",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.21",
|
||||
"weixin-js-sdk": "^1.6.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cool-vue/vite-plugin": "7.1.3",
|
||||
"@dcloudio/types": "^3.4.8",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/md5": "^2.3.2",
|
||||
"@types/node": "^20.11.26",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.1.6"
|
||||
}
|
||||
}
|
268
pages.json
Normal file
268
pages.json
Normal file
@ -0,0 +1,268 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/home",
|
||||
"style": {
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"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/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/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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
57
pages/goods/comment.vue
Normal file
57
pages/goods/comment.vue
Normal 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>
|
81
pages/goods/components/comment-item.vue
Normal file
81
pages/goods/components/comment-item.vue
Normal 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>
|
350
pages/goods/components/filter-bar.vue
Normal file
350
pages/goods/components/filter-bar.vue
Normal 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>
|
90
pages/goods/components/goods-comment.vue
Normal file
90
pages/goods/components/goods-comment.vue
Normal 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>
|
36
pages/goods/components/goods-detail.vue
Normal file
36
pages/goods/components/goods-detail.vue
Normal 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>
|
158
pages/goods/components/goods-info.vue
Normal file
158
pages/goods/components/goods-info.vue
Normal 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>
|
250
pages/goods/detail.vue
Normal file
250
pages/goods/detail.vue
Normal 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>
|
84
pages/goods/list.vue
Normal file
84
pages/goods/list.vue
Normal 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>
|
164
pages/goods/search.vue
Normal file
164
pages/goods/search.vue
Normal 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>
|
243
pages/index/category.vue
Normal file
243
pages/index/category.vue
Normal 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>
|
57
pages/index/components/banner.vue
Normal file
57
pages/index/components/banner.vue
Normal 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>
|
179
pages/index/components/coupon-activity.vue
Normal file
179
pages/index/components/coupon-activity.vue
Normal file
@ -0,0 +1,179 @@
|
||||
<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];
|
||||
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>
|
93
pages/index/components/hot-category.vue
Normal file
93
pages/index/components/hot-category.vue
Normal 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>
|
146
pages/index/components/tabbar.vue
Normal file
146
pages/index/components/tabbar.vue
Normal 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 == 2 ? 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>
|
147
pages/index/home.vue
Normal file
147
pages/index/home.vue
Normal file
@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<cl-page>
|
||||
<view class="page">
|
||||
<!-- 搜索 -->
|
||||
<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>
|
||||
|
||||
<!-- 轮播图 -->
|
||||
<banner />
|
||||
|
||||
<!-- 热门分类 -->
|
||||
<hot-category />
|
||||
|
||||
<!-- 优惠券活动 -->
|
||||
<coupon-activity />
|
||||
|
||||
<!-- 热门推荐 -->
|
||||
<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>
|
||||
</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";
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
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>
|
298
pages/index/my.vue
Normal file
298
pages/index/my.vue
Normal 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>
|
239
pages/index/shopping-cart.vue
Normal file
239
pages/index/shopping-cart.vue
Normal 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>
|
40
pages/market/coupon.vue
Normal file
40
pages/market/coupon.vue
Normal 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>
|
99
pages/order/comment.vue
Normal file
99
pages/order/comment.vue
Normal 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>
|
113
pages/order/components/cancel.vue
Normal file
113
pages/order/components/cancel.vue
Normal 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>
|
121
pages/order/components/comment.vue
Normal file
121
pages/order/components/comment.vue
Normal 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>
|
41
pages/order/components/op-btns.vue
Normal file
41
pages/order/components/op-btns.vue
Normal 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>
|
80
pages/order/components/op.vue
Normal file
80
pages/order/components/op.vue
Normal 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>
|
110
pages/order/components/pay.vue
Normal file
110
pages/order/components/pay.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<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.key"
|
||||
:label="item.key"
|
||||
: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 = order.payTypes[0].key;
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
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>
|
23
pages/order/components/status-tag.vue
Normal file
23
pages/order/components/status-tag.vue
Normal 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>
|
231
pages/order/detail.vue
Normal file
231
pages/order/detail.vue
Normal 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>
|
92
pages/order/dict/index.ts
Normal file
92
pages/order/dict/index.ts
Normal file
@ -0,0 +1,92 @@
|
||||
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",
|
||||
},
|
||||
];
|
||||
|
||||
export const RefundStatus = [
|
||||
{
|
||||
label: "申请中",
|
||||
value: 0,
|
||||
color: "warning",
|
||||
},
|
||||
{
|
||||
label: "已退款",
|
||||
value: 1,
|
||||
color: "success",
|
||||
},
|
||||
{
|
||||
label: "拒绝退款",
|
||||
value: 2,
|
||||
color: "error",
|
||||
},
|
||||
];
|
240
pages/order/list.vue
Normal file
240
pages/order/list.vue
Normal 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>
|
108
pages/order/logistics.vue
Normal file
108
pages/order/logistics.vue
Normal 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>
|
126
pages/order/refund.vue
Normal file
126
pages/order/refund.vue
Normal 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>
|
209
pages/order/submit.vue
Normal file
209
pages/order/submit.vue
Normal file
@ -0,0 +1,209 @@
|
||||
<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.key"
|
||||
:label="item.key"
|
||||
: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();
|
||||
|
||||
// 表单
|
||||
const form = reactive({
|
||||
payType: "wxpay" as "wxpay" | "alipay",
|
||||
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: "购买商品",
|
||||
},
|
||||
})
|
||||
.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)
|
||||
.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>
|
57
pages/user/about.vue
Normal file
57
pages/user/about.vue
Normal 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>
|
151
pages/user/address-edit.vue
Normal file
151
pages/user/address-edit.vue
Normal 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>
|
116
pages/user/address-list.vue
Normal file
116
pages/user/address-list.vue
Normal 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>
|
132
pages/user/captcha.vue
Normal file
132
pages/user/captcha.vue
Normal 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>
|
37
pages/user/doc.vue
Normal file
37
pages/user/doc.vue
Normal 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>
|
67
pages/user/edit.vue
Normal file
67
pages/user/edit.vue
Normal 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>
|
473
pages/user/login.vue
Normal file
473
pages/user/login.vue
Normal 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>
|
154
pages/user/set.vue
Normal file
154
pages/user/set.vue
Normal 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>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user