Compare commits

...

66 Commits

Author SHA1 Message Date
448ee810de bug修复 2024-05-20 00:49:16 +08:00
6cd0361f33 优化 2024-05-17 17:08:30 +08:00
0b1f0b9e26 优化 2024-05-17 13:24:25 +08:00
531a955fe2 登录增加协议选择逻辑
增加审核测试参数处理逻辑
2024-05-17 13:12:03 +08:00
cb40ce643f 优化商品默认颜色尺码配置显示问题 2024-05-16 16:25:24 +08:00
f547216c2b 新增商品其他售价逻辑
修复下单重复创建订单问题
2024-05-16 15:38:34 +08:00
62c9994444 修复生产环境折扣价显示错误问题 2024-05-16 13:07:10 +08:00
100744cfaf 联系商家字段调整 2024-05-10 21:33:10 +08:00
74dba2ef29 充值页面调整
商品折扣显示逻辑调整
增加公众号跳转修改资料逻辑
2024-05-10 17:51:30 +08:00
dccdd43e3a bug修复 2024-05-09 23:51:16 +08:00
eb1abb69fd bug修复 2024-04-26 19:01:31 +08:00
68cce822be 优化 2024-04-26 11:41:26 +08:00
5bbfff9e21 注册有礼 2024-04-24 23:14:34 +08:00
7f6ace8ac6 注册有礼 2024-04-24 23:01:38 +08:00
bb9a32d296 bug修复 2024-04-24 21:53:31 +08:00
bfc04c54aa 优化 2024-04-20 21:58:16 +08:00
5b8c7d4a2d bug修复 2024-04-19 21:45:30 +08:00
634e801eb5 商品增加分类 2024-04-19 13:44:31 +08:00
7b3676eda7 优化占位图显示 2024-04-19 12:51:17 +08:00
6c4c2ac85d bug修复 2024-04-18 00:37:43 +08:00
b87c8e8292 团购已结束,未开始显示处理
订单待支付数量显示
2024-04-16 16:52:20 +08:00
32aa0eb971 优化 2024-04-16 11:41:18 +08:00
178e8ff5cb 问题修复 2024-04-16 01:39:23 +08:00
0562554266 问题修复 2024-04-16 00:53:34 +08:00
877469ab43 优化 2024-04-14 20:18:44 +08:00
13fd77f13b 优化 2024-04-14 17:02:49 +08:00
f2f7fcde48 购物车逻辑调整 2024-04-14 16:43:43 +08:00
16dac8d97f 优化 2024-04-14 01:43:37 +08:00
b676a1acd9 功能完善 2024-04-14 01:05:08 +08:00
a270c9be70 功能完善 2024-04-13 19:44:18 +08:00
d6f3f9c812 功能完善 2024-04-13 19:15:20 +08:00
23af4c25e3 功能完善 2024-04-13 17:20:09 +08:00
be328f9243 功能完善 2024-04-13 00:57:24 +08:00
93e9c5227b 切换公司逻辑完善 2024-04-11 21:13:52 +08:00
222bae69d9 ... 2024-04-10 14:46:29 +08:00
c94ddeed1a 团购支付 2024-04-08 21:33:15 +08:00
935d56227d 处理未登录情况 2024-03-31 19:10:00 +08:00
a06d74934f 优化 2024-03-31 18:24:22 +08:00
60cb832b02 优化 2024-03-31 18:19:26 +08:00
b502385272 购物车逻辑完善
个人信息存储优化
团购支付
2024-03-31 17:22:14 +08:00
1fc0aa432b 购物车逻辑完善
个人信息存储优化
团购下单
2024-03-31 03:19:19 +08:00
074b0057da 团购下单 2024-03-30 14:59:51 +08:00
52b63a757f 详情 2024-03-30 00:05:07 +08:00
51c708f9fa 优化 2024-03-29 14:55:40 +08:00
91af869899 优化 2024-03-25 14:44:38 +08:00
4fb648f003 积分接口联调 2024-03-21 23:22:24 +08:00
792aa4268a 购物车逻辑 2024-03-20 22:20:24 +08:00
8d7f82b07c 优化 2024-03-18 21:52:10 +08:00
7f0f11cf14 ... 2024-03-17 14:50:20 +08:00
cfdc8d088c fix: test 2024-03-17 10:50:26 +08:00
84b6ff15c7 ... 2024-03-16 22:46:10 +08:00
8b0ad91bd4 团购券详情 2024-03-16 22:02:26 +08:00
351e20f0c0 优化 2024-03-15 23:20:20 +08:00
3ce85e2396 登录页面调整 2024-03-15 14:02:39 +08:00
43b5ea640e 用户信息持久化 2024-03-15 11:20:11 +08:00
7a3dd0fae8 团购秒杀 2024-03-14 22:58:38 +08:00
9dda08a1b1 购物车,积分,订单详情 2024-03-14 18:23:33 +08:00
0602dc6c72 登录逻辑+注册有礼 2024-03-13 22:43:59 +08:00
c17b70af2d 订单详情 2024-03-12 22:24:36 +08:00
0883e88df0 确认订单 2024-03-11 23:02:41 +08:00
00013cb46c 商品详情 2024-03-10 15:40:26 +08:00
107783062b 商品详情 2024-03-08 18:06:33 +08:00
2df1a93e6b 新增页面 2024-02-06 21:08:44 +08:00
2610f4e6f1 新增页面 2024-02-05 22:22:37 +08:00
2280c0518b 优化 2024-02-03 20:03:14 +08:00
82b09c68a5 首页页面完善 2024-02-03 19:51:36 +08:00
100 changed files with 11716 additions and 731 deletions

View File

@@ -5,7 +5,7 @@ VITE_APP_TITLE='uniapp-vue3-project'
VITE_APP_ENV='development'
# 接口地址
VITE_APP_BASE_API='/api'
VITE_APP_BASE_API='https://apidev.lakeapp.cn/'
# 删除console
VITE_DROP_CONSOLE=false

View File

@@ -5,7 +5,7 @@ VITE_APP_TITLE='uniapp-vue3-project'
VITE_APP_ENV='production'
# 接口地址
VITE_APP_BASE_API='/'
VITE_APP_BASE_API='http://api.lakeapp.cn/'
# 删除console
VITE_DROP_CONSOLE=true

View File

@@ -14,6 +14,7 @@ const WX_DESC = envType == 'production' ? '正式环境' : '测试环境'
(async () => {
// @ts-ignore
const manifest = path.resolve(__dirname, './src/manifest.json')
console.log(manifest)
// @ts-ignore
const manifestConfig = JSON.parse(fs.readFileSync(manifest).toString())
const appId = manifestConfig['mp-weixin'].appid

View File

@@ -15,30 +15,30 @@
"postinstall": "simple-git-hooks"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-3090920231225001",
"@dcloudio/uni-app-plus": "3.0.0-3090920231225001",
"@dcloudio/uni-components": "3.0.0-3090920231225001",
"@dcloudio/uni-h5": "3.0.0-3090920231225001",
"@dcloudio/uni-mp-weixin": "3.0.0-3090920231225001",
"@dcloudio/uni-mp-xhs": "3.0.0-3090920231225001",
"@dcloudio/uni-app": "3.0.0-4000820240401001",
"@dcloudio/uni-app-plus": "3.0.0-4000820240401001",
"@dcloudio/uni-components": "3.0.0-4000820240401001",
"@dcloudio/uni-mp-weixin": "3.0.0-4000820240401001",
"dayjs": "^1.11.10",
"pinia": "2.0.36",
"pinia-plugin-persistedstate": "^3.2.1",
"uview-plus": "^3.1.38",
"vue": "3.2.47",
"vue-i18n": "^9.1.9",
"vue": "3.4.21",
"vue-i18n": "^9.9.0",
"z-paging": "^2.6.2"
},
"devDependencies": {
"@antfu/eslint-config": "1.1.0",
"@dcloudio/types": "^3.4.3",
"@dcloudio/uni-automator": "3.0.0-3090920231225001",
"@dcloudio/uni-cli-shared": "3.0.0-3090920231225001",
"@dcloudio/uni-stacktracey": "3.0.0-3090920231225001",
"@dcloudio/vite-plugin-uni": "3.0.0-3090920231225001",
"@dcloudio/uni-automator": "3.0.0-4000820240401001",
"@dcloudio/uni-cli-shared": "3.0.0-4000820240401001",
"@dcloudio/uni-stacktracey": "3.0.0-4000820240401001",
"@dcloudio/vite-plugin-uni": "3.0.0-4000820240401001",
"@types/node": "^20.8.10",
"@typescript-eslint/parser": "^6.10.0",
"@uni-helper/uni-app-types": "^0.5.9",
"@unocss/eslint-plugin": "^0.57.2",
"@vue/runtime-core": "^3.2.45",
"@vue/runtime-core": "^3.4.21",
"@vue/tsconfig": "^0.4.0",
"czg": "^1.7.1",
"eslint": "^8.53.0",

848
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,89 @@
<script setup lang="ts">
import { mpUpdate } from '@/utils';
<script setup lang='ts'>
import { mpUpdate, setCompanyId, setReferrerUserId, setRegisterStoreId } from '@/utils';
onLaunch(async (options) => {
console.log('App Launch options ', options);
const miniProgram = uni.getAccountInfoSync().miniProgram;
const env = miniProgram.envVersion;
//生产
if(env === 'release' || env === 'trial') {
async function getVersionStatus() {
return new Promise((resolve, reject) => {
uni.request({
url: 'https://api.lakeapp.cn/wechat/version_info',
success(res) {
console.log(res);
resolve(res);
},
fail(exception) {
reject(exception);
}
});
}).then((res) => {
const { version, audit } = res.data;
// if(miniProgram.version === version && audit == 1) {
if(audit == 1) {
setCompanyId('1150930317231112193');
setRegisterStoreId('1150930317436633090');
}
});
}
await getVersionStatus();
}
//测试、开发 trial,develop
else {
setCompanyId('1150930317231112193');
setRegisterStoreId('1150930317436633090');
}
if(options?.query) {
// options.query.scene = 'companyId%3D1150930317231112193%26wxOpenId%3D111%26ticketId%3D123456%26storeId%3D1150930317436633090'
//保存登录邀请员工id
if(options.query.referrerUserId) {
setReferrerUserId(options.query.referrerUserId);
}
//保存注册门店id
if(options.query.companyId) {
setCompanyId(options.query.companyId);
}
//保存注册门店id
if(options.query.storeId) {
setRegisterStoreId(options.query.storeId);
}
if(options?.query.scene) {
function getQueryParam(queryParams: string, key: string) {
let regex = new RegExp('(?:[?&]|^)' + key + '=([^&]+)'),
match = queryParams.match(regex);
return match && match[1];
}
//保存注册门店id
const params = decodeURIComponent(options?.query.scene);
if(params.includes('companyId')) {
setCompanyId(getQueryParam(params, 'companyId') || '');
}
if(params.includes('storeId')) {
setRegisterStoreId(getQueryParam(params, 'storeId') || '');
}
if(options?.query.scene === 'edit_avatar_nickname') {
setTimeout(() => {
uni.reLaunch({
url: '/pages/mine/subs/profile/index'
});
}, 500);
}
}
}
onLaunch(() => {
console.log('App Launch');
// #ifdef MP
mpUpdate();
// #endif
@@ -13,9 +94,13 @@ onShow(() => {
onHide(() => {
console.log('App Hide');
});
// globalData : {
// test: '';
// }
</script>
<style lang="scss">
<style lang='scss'>
/* 每个页面公共css */
@import 'uview-plus/index.scss';
@import '@/static/styles/common.scss';

View File

@@ -2,11 +2,13 @@
* 通用接口
*/
import type { SendCodeParams, SendCodeResult, UploadImageResult } from './types';
import { post, upload } from '@/utils/request';
import { get, post, upload } from '@/utils/request';
enum URL {
upload = '/common/upload',
sendCode = '/sendCode',
barCode = '/ext2/qr/barimg',
qrCode = '/ext2/qr/qrimg'
}
// 图片上传
@@ -15,3 +17,12 @@ export const uploadImage = (imagePath: string) =>
// 发送验证码
export const sendCode = (data: SendCodeParams) => post<SendCodeResult>({ url: URL.sendCode, data });
export const generateBarCode = (data: string) => get<ArrayBuffer>({
url: `${URL.barCode}?v=${data}`,
responseType: 'arraybuffer'
});
export const generateQrCode = (data: string) => get<ArrayBuffer>({
url: `${URL.qrCode}?v=${data}`,
responseType: 'arraybuffer'
});

4
src/api/company/index.ts Normal file
View File

@@ -0,0 +1,4 @@
import { get } from '@/utils/request';
export const getCompanyList = (maOpenId: string) => get({ url: `/wc/wechat/company?maOpenId=${maOpenId}` });
export const getCompanyInfo = () => get({ url: `/ext/zconfig/company_find` });

14
src/api/goods/index.ts Normal file
View File

@@ -0,0 +1,14 @@
import { get } from '@/utils';
import { CategoryBean, GoodsBean } from '@/api/goods/types';
enum URL {
categoryList = '/ext/goods/category_list',
goodsList = '/ext/goods/query',
goodsDetail = '/ext/goods/info'
}
export const getCategoryList = () => get<CategoryBean[]>({ url: URL.categoryList });
export const getGoodsList = (params: any) => get<GoodsBean[]>({ url: URL.goodsList, data: params });
export const getGoodsDetail = (goodsId: string) => get<GoodsBean>({ url: URL.goodsDetail + `?goodsId=${goodsId}` });

70
src/api/goods/types.ts Normal file
View File

@@ -0,0 +1,70 @@
export interface CategoryBean {
typeId: string;
typeName: string;
}
export interface GoodsBean {
id: string;
goodsId: string;
goodsName: string;
goodsCode: string,
allow_integral: number;
available: number;
back_factory: number;
brand_id: string;
brand_name: string;
code: string;
company_id: string;
cost_price: number;
create_time: string;
creator_id: string;
discount: number;
images: string;
market_time: string;
material_id: string;
material_name: string;
name: string;
price: number;
send_num: number;
priceExt: number;
price_ext: number;
payPrice: number;
profile: string;
remark: string;
remark1: string;
remark2: string;
remark3: string;
remark4: string;
season_id: string;
season_name: string;
status: number;
stocks: StockBean[],
store_id: string;
suplier_id: string;
suplier_name: string;
type_id: string;
type_name: string;
update_time: string;
years_id: string;
years_name: string;
/*extra params*/
goodsNum: number;
salePrice: number;
originPrice: number;
consumePrice: number;
stockStock: StockBean
checkedStock: StockBean;
checked: boolean | false;
}
export interface StockBean {
colorId: string;
colorName: string;
existingNumber: number;
sizeId: string;
sizeName: string;
stockId: string;
images: string,
count: number;
}

25
src/api/groupbuy/index.ts Normal file
View File

@@ -0,0 +1,25 @@
import { get, post } from '@/utils/request';
import { GroupBuyBean, RecordBean } from '@/api/groupbuy/types';
import { GoodsBean } from '@/api/goods/types';
export const getGroupBuyList = (data: {
pageNum: number,
pageSize: number,
obj: {
timeStatus: number,
}
}) => post({ url: '/group/coupons/wx_query', data });
export const getGroupBuyDetail = (
id: string) => get<GroupBuyBean>({ url: `wechat/coupons/group/get/${id}` });
export const getGroupBuyRecordList = (groupId: string, pageNum: number, pageSize: number) => post<RecordBean[]>({
url: `wechat/coupons/group/order/list?groupId=${groupId}&pageNum=${pageNum}&pageSize=${pageSize}`
});
export const preOrder = (data: any) => post<GoodsBean>({ url: 'wechat/coupons/group/pre_v2', data });
export const progress = (data: any) => post<any>({ url: 'wechat/coupons/group/pay/progress', data });
export const pay = (data: any) => post<any>({ url: 'wechat/coupons/group/pay', data });

37
src/api/groupbuy/types.ts Normal file
View File

@@ -0,0 +1,37 @@
import { GoodsBean } from '@/api/goods/types';
import { CouponBean } from '@/api/user/types';
export interface GroupBuyBean {
companyId: string;
content: string;
couponsList: CouponBean[];
createTime: string;
creatorId: string;
creatorName: string;
endDate: string;
fake: number;
goods: GoodsBean;
id: string;
name: string;
offsetPrice: number;
payPrice: number;
goodsPrice: number;
periodDay: number;
price: number;
publicNum: number;
sendNum: number;
startDate: string;
status: number;
threshold: number;
title: string;
totalNum: number;
}
export interface RecordBean {
bizId: string;
createTime: string;
goodsCode: string;
goodsNum: number;
groupId: string;
memberImage: string;
}

26
src/api/order/index.ts Normal file
View File

@@ -0,0 +1,26 @@
import { get, post } from '@/utils';
enum URL {
orderCreate = '/sales/wx_order_create',
orderList = '/sales/wx_query',
orderDetail = '/order/wx_get/',
paymentList = '/sales/payment/',
overPayment = '/sales/over_payment'
}
export const orderCreate = (data: any) => post({ url: URL.orderCreate, data });
export const getOrderList = (data: {
pageNum: number,
pageSize: number,
obj: {
payStatus: number
}
}) => post({ url: URL.orderList, data });
export const getOrderDetail = (orderId: string) => get<any>({ url: URL.orderDetail + `${orderId}` });
export const getPaymentList = (orderId: string) => get<any>({ url: URL.paymentList + `${orderId}` });
export const overPayment = (data: any) => post({ url: URL.overPayment, data });

78
src/api/order/types.ts Normal file
View File

@@ -0,0 +1,78 @@
import { GoodsBean, StockBean } from '@/api/goods/types';
export interface OrderBean {
allowIntegral: boolean;
bizId: string;
classify: number;
address: any,
companyId: string;
consignTime: string;
consumerId: string;
consumerName: string;
coupon: string;
couponIds: string;
couponsStatus: number;
createTime: string;
creatorId: number;
creatorName: string;
device: string;
discount: number;
discountPrice: number;
finishTime: string;
freePrice: number;
goodsNum: number;
id: string;
integral: number;
itemNum: number;
marketingPrice: number;
marketingText: string;
modifierIds: string;
needPaid: number;
ogList: any[];
orderGoods: GoodsBean[];
changeStockNum: number;
consumePrice: number;
discountOriginPrice: number;
goodsCode: string;
goodsId: string;
goodsName: string;
goodsPriceModify: string;
goodsTypeName: string;
images: string;
offset: string;
orderId: string;
orderNo: string;
originPrice: number;
originStockNum: number;
priceModify: [];
produceIntegral: number;
salePrice: number;
stockId: string;
stockStock: StockBean[];
payStatus: number;//状态 1.未支付 2.已支付
payTypeIds: string;
printed: string;
produceIntegralNumber: number;
profit: string;
reducePrice: number;
relatedId: string;
remark: string;
replacementOrder: string;
saleNum: number;
salers: [];
status: number;
storeId: string;
storeName: string;
storeOrder: string;
totalPrice: number;
transactionPrice: number;
type: number;
typeName: string;
updateTime: string;
updated: boolean;
useGold: number;
wholePrice: number;
//extra field
countdown: number;
}

View File

@@ -1,18 +1,75 @@
/**
* 用户信息相关接口
*/
import type { LoginByCodeParams, LoginParams, LoginResult } from './types';
import type { CouponBean, LoginParams, LoginResult, RegisterParams, TerminalBean } from './types';
import { get, post } from '@/utils/request';
import type { UserState } from '@/store/modules/user/types';
import { UserBean } from '@/store/modules/user/types';
enum URL {
login = '/user/login',
loginByCode = '/user/loginByCode',
// login = '/member/login',
login = '/wc/wechat/LoginByMa',
loginByCode = '/wc/wechat/LoginByMaCode',
register = '/wc/wechat/register',
uploadAvatar = '/wc/wechat/uploadImage',
logout = '/user/logout',
profile = '/user/profile',
// profile = '/user/profile',
profile = 'wc/wechat/member_detail',
updateProfile = '/member/wx_update',
// wc/wechat/member_detail
addressList = '/ext/addr/list',
addressDetail = '/ext/addr/find',
addressCreate = '/ext/addr/create',
addressUpdate = '/ext/addr/update',
addressDelete = '/ext/addr/delete',
dynamicCode = '/member/dynccode',
rechargeList = '/ext/recharge/rule_config',
preRecharge = '/memberIncoming/wx_incoming_pre',
recharge = '/memberIncoming/wx_save',
rechargeVerify = '/memberIncoming/wx_paid',
couponList = '/wechat/coupons/coupons/pageList',
integralList = '/ext/member/integral_query',
tradeList = '/memberIncoming/wx_balance_records',
terminal = 'wechat/coupons/terminal?companyId=',
cardLink = '/wc/wechat/get_card_url',
registerCoupon = '/couponsStrategy/wx_register_coupon'
}
export const getUserProfile = () => get<UserState>({ url: URL.profile });
export const getUserProfile = () => get<UserBean>({ url: URL.profile });
export const updateProfile = (data: any) => post<any>({ url: URL.updateProfile, data });
export const login = (data: LoginParams) => post<LoginResult>({ url: URL.login, data });
export const loginByCode = (data: LoginByCodeParams) => post<any>({ url: URL.loginByCode, data });
export const loginByCode = (code: string, companyId: string) => post<any>({ url: URL.loginByCode + `?code=${code}` });
export const register = (data: RegisterParams) => post<LoginResult>({ url: URL.register, data });
export const logout = () => post<any>({ url: URL.logout });
export const getAddressList = () => get<any>({ url: URL.addressList });
export const getAddressDetail = (id: string) => get<any>({ url: URL.addressDetail + `?id=${id}` });
export const addressCreate = (data: any) => post<any>({ url: URL.addressCreate, data });
export const addressUpdate = (data: any) => post<any>({ url: URL.addressUpdate, data });
export const addressDelete = (id: string) => post<any>({ url: URL.addressDelete + `?addrid=${id}` });
export const getDynamicCode = () => post<any>({ url: URL.dynamicCode });
export const getRechargeList = () => get<any>({ url: URL.rechargeList });
export const preRecharge = (data: any) => post({ url: URL.preRecharge, data });
export const recharge = (data: any) => post({ url: URL.recharge, data });
export const rechargeVerify = (data: any) => post({ url: URL.rechargeVerify, data });
export const getCouponList = (data: any) => post<CouponBean[]>({ url: URL.couponList, data });
export const getIntegralList = (data: any) => get({ url: URL.integralList, data });
export const getTerminal = (companyId: string) => get<TerminalBean>({ url: URL.terminal + companyId });
export const getTradeList = (data: any) => post<any>({ url: URL.tradeList, data });
export const getCardLink = () => get<string>({ url: URL.cardLink });
export const getRegisterCoupon = () => get<any>({ url: URL.registerCoupon });

View File

@@ -1,6 +1,24 @@
import { UserBean } from '@/store/modules/user/types';
export interface LoginParams {
phone: string;
// phone: string;
code: string;
userInfo: any;
storeId: string;
// referrerUserId: string;
}
export interface RegisterParams {
unionId: string,
openId: string,
maOpenId: string,
image: string,
nickName: string,
telephone: string,
birthday: string,
companyId: string,
creatorId: string,
gender: string
}
export interface LoginByCodeParams {
@@ -8,8 +26,94 @@ export interface LoginByCodeParams {
}
export interface LoginResult {
sessionKey: string;
user: UserBean;
userInfo: any;
token: string;
user_id: number;
user_name: string;
avatar: string;
}
export interface IntegralBean {
id: string;
createtime: string;
exsitvalue: number;
remark: string;
type: number;
value: number;
}
export interface CouponBean {
companyId: string;
couponsId: string;
createTime: string;
creatorId: number;
creatorName: string;
dayType: number;
endTime: string;
id: string;
images: string;
imcomingId: string;
memberId: string;
memberName: string;
name: string;
notice: number;
offsetPrice: number;
orderId: null;
periodDay: number;
reduce: number;
remark: string;
startTime: string;
status: number;
strategyType: string;
telephone: string;
threshold: number;
type: number;
userTime: string;
checked: boolean;
}
export interface TerminalBean {
applyId: string;
bindId: string;
bindName: string;
bindStatus: number;
company: string;
companyId: string;
companyName: string;
contact: string;
createTime: string;
creatorId: string;
creatorName: string;
deviceRole: string;
employee: string;
expireTime: string;
id: string;
managerPermission: string;
name: string;
permission: {
deletablePer: boolean,
profitPer: boolean,
exportTypes: any[],
checkoutPer: boolean,
showPhoneNumber: boolean,
storePer: any[]
};
checkoutPer: boolean;
deletablePer: boolean;
exportPer: boolean;
exportTypes: any[];
profitPer: boolean;
showPhoneNumber: boolean;
storePer: any[];
signDate: string;
status: number;
store: string;
storeId: string;
storeName: string;
storeOrder: number;
terminalKey: string;
terminalSn: string;
token: string;
type: number;
}

View File

@@ -0,0 +1,86 @@
<template>
<uni-popup ref='popupRef' type='bottom' :mask-click='false' @touchmove.stop.prevent=''>
<view class='content'>
<text class='title'>切换门店</text>
<scroll-view scroll-y style='max-height: 600rpx;padding: 15rpx'>
<view class='store-item' v-for='(item, index) in companyList' :key='index' @click='() => {
handleClick(index)
}'>
<text>{{ item?.companyName || '' }}</text>
</view>
</scroll-view>
<view class='close-btn primary-button' @click='close'>
<text>取消</text>
</view>
</view>
</uni-popup>
</template>
<script lang='ts' setup>
defineProps({
companyList: {
type: Array,
default: () => []
}
});
const popupRef = ref();
let callback: Function;
const show = (fn: Function) => {
callback = fn;
popupRef.value.open();
};
const handleClick = (index: number) => {
callback(index);
close();
};
const close = () => {
popupRef.value.close();
};
defineExpose({
show, close
});
</script>
<style lang='scss' scoped>
.content {
display: flex;
flex-direction: column;
background: #FFFFFF;
border-radius: 20rpx 20rpx 0 0;
padding: 20rpx 30rpx 30rpx 30rpx;
.title {
display: flex;
align-self: center;
font-size: 30rpx;
font-weight: bold;
padding: 20rpx;
}
.store-item {
display: flex;
flex-direction: column;
font-size: 35rpx;
color: #333333;
//padding: 20rpx 20rpx;
}
.store-item:after {
content: '';
background: #F5F5F5;
height: 0.5rpx;
width: 100%;
margin: 25rpx 0;
}
.close-btn {
background: #F5F5F5;
color: #333333;
}
}
</style>

View File

@@ -0,0 +1,74 @@
<template>
<uni-popup type='bottom' ref='popupRef'>
<view class='popup-content c-flex-column'>
<view class='c-flex-row'>
<text>关注公众号</text>
<image :src='assetsUrl("ic_close.png")' @click.stop='close' />
</view>
<image class='qrcode' :src='assetsUrl("official_account_qrcode.png")' show-menu-by-longpress />
<text class='tips'>长按关注会员中心公众号\n
获得更多优惠和服务信息
</text>
</view>
</uni-popup>
</template>
<script lang='ts' setup>
import { assetsUrl } from '@/utils/assets';
const popupRef = ref();
const show = () => {
popupRef.value.open();
};
const close = () => {
popupRef.value.close();
};
defineExpose({ show });
</script>
<style lang='scss' scoped>
.popup-content {
background: #FFFFFF;
border-radius: 20rpx 20rpx 0 0;
padding: 38rpx;
align-items: center;
position: relative;
view:nth-of-type(1) {
image {
width: 35rpx;
height: 35rpx;
align-self: flex-end;
position: absolute;
right: 38rpx;
}
text {
font-weight: bold;
font-size: 30rpx;
color: #333333;
align-self: center;
}
}
.qrcode {
width: 377rpx;
height: 377rpx;
margin-top: 50rpx;
align-self: center;
}
.tips {
font-weight: bold;
font-size: 30rpx;
color: #333333;
align-self: center;
margin-top: 10rpx;
}
}
</style>

View File

@@ -0,0 +1,92 @@
<template>
<sqb-pay style='width: 100%;' @bindnavigateTo='navigateTo'
:return_url='payParams.return_url'
:total_amount='payParams.total_amount'
:terminal_sn='payParams.terminal_sn'
:client_sn='payParams.client_sn'
:subject='payParams.subject'
:subject_img='payParams.subject_img '
:merchant_name='payParams.merchant_name'
:notify_url='payParams.notify_url'
:sign='payParams.sign'>
<slot name='button'></slot>
</sqb-pay>
</template>
<script lang='ts' setup>
import { PropType } from 'vue';
import { parseParameter, sortASCII } from '@/utils';
import { hexMD5 } from '@/utils/common/md5';
import { useUserStore } from '@/store';
const userState = useUserStore();
const { terminalInfo } = storeToRefs(userState);
export interface SqbPayParams {
return_url: string;
total_amount: number;
client_sn: string;
terminal_sn: string;
subject: string;
subject_img: string;
merchant_name: string;
notify_url: string;
sign: string;
}
const props = defineProps({
payParams: {
type: Object as PropType<SqbPayParams>,
default: () => {
return {
return_url: '',
total_amount: 0,
client_sn: '',
terminal_sn: '',
subject: '',
subject_img: '',
merchant_name: '',
notify_url: '',
sign: ''
};
}
}
});
watch(() => props.payParams, (newValue, prevValue) => {
if(newValue) {
let signParams = buildSignParams.value;
const signStr = parseParameter(signParams) + '&key=' + terminalInfo.value.terminalKey;
props.payParams.sign = hexMD5(signStr).toUpperCase();
}
});
const buildSignParams = computed(() => {
return sortASCII({
client_sn: props.payParams.client_sn || '',
return_url: props.payParams.return_url,
total_amount: Number((props.payParams.total_amount * 100).toFixed(2)),
terminal_sn: props.payParams.terminal_sn,
subject: props.payParams.subject,
subject_img: props.payParams.subject_img,
merchant_name: props.payParams.merchant_name,
notify_url: props.payParams.notify_url
}, true);
});
const navigateTo = (e: any) => {
console.log('--------------_>>>>>>navigateTo ', e);
uni.redirectTo({
url: e.detail.url,
fail(e) {
uni.showToast({
title: '支付失败'
});
}
});
};
</script>
<style lang='scss' scoped>
</style>

View File

@@ -0,0 +1,114 @@
<template>
<uni-popup type='bottom' ref='popupRef' :mask-click='false' @touchmove.stop.prevent=''>
<view class='popup-content c-flex-column'>
<view class='c-flex-row'>
<image :src='assetsUrl("ic_back.png")' @click.stop='close()' />
<text>选择支付方式</text>
</view>
<view class='c-flex-row' @click.stop='doPayment(PAYMENT_TYPE_WECHAT)'>
<image :src='assetsUrl("ic_wechat.png")' />
<text>微信支付</text>
<image :src='assetsUrl(currentType===PAYMENT_TYPE_WECHAT?"ic_checkbox_active.png":"ic_checkbox_normal.png")' />
</view>
<view class='divider' style='margin: 40rpx 0' />
<view class='c-flex-row' @click.stop='doPayment(PAYMENT_TYPE_BALANCE)'>
<image :src='assetsUrl("ic_balance.png")' />
<text>余额剩余{{ userInfo.balance }}</text>
<image :src='assetsUrl(currentType===PAYMENT_TYPE_BALANCE?"ic_checkbox_active.png":"ic_checkbox_normal.png")' />
</view>
</view>
</uni-popup>
</template>
<script lang='ts' setup>
import { assetsUrl } from '@/utils/assets';
import { useUserStore } from '@/store';
const popupRef = ref();
const PAYMENT_TYPE_WECHAT = ref(0);
const PAYMENT_TYPE_BALANCE = ref(1);
const currentType = ref(PAYMENT_TYPE_WECHAT.value);
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const emits = defineEmits(['change']);
const show = () => {
popupRef.value.open();
};
const close = () => {
popupRef.value.close();
};
const doPayment = (paymentType: number) => {
emits('change', paymentType);
currentType.value = paymentType;
switch (paymentType) {
case PAYMENT_TYPE_WECHAT.value:
console.log('PAYMENT_TYPE_WECHAT');
break;
case PAYMENT_TYPE_BALANCE.value:
console.log('PAYMENT_TYPE_BALANCE');
break;
}
close();
};
defineExpose({
show
});
</script>
<style lang='scss' scoped>
.popup-content {
background: #FFFFFF;
border-radius: 20rpx 20rpx 0 0;
padding: 38rpx 45rpx 140rpx 45rpx;
view:nth-of-type(1) {
display: flex;
margin-bottom: 70rpx;
position: relative;
image {
width: 35rpx;
height: 35rpx;
align-self: flex-start;
}
text {
font-weight: bold;
font-size: 30rpx;
color: #333333;
position: absolute;
left: 0;
right: 0;
text-align: center;
}
}
view:nth-of-type(n+2) {
image:nth-of-type(1) {
width: 47rpx;
height: 47rpx;
}
text {
align-self: center;
flex: 1;
font-weight: 400;
font-size: 30rpx;
color: #333333;
margin-left: 23rpx;
}
image:nth-of-type(2) {
width: 35rpx;
height: 35rpx;
}
}
}
</style>

View File

@@ -0,0 +1,296 @@
<template>
<uni-popup ref='popupRef' type='bottom' :mask-click='false' @touchmove.stop.prevent=''>
<view class='content'>
<view class='c-flex-row' style='align-items: flex-start'>
<image class='goods-image' :src='goodsDetailBean?.images||defaultImage' />
<view class='c-flex-column' style='flex: 1;display: inline-grid'>
<text class='goods-name'>{{ goodsDetailBean?.name || '未知' }}</text>
<text class='goods-price'>
{{ flashPrice > 0 ? `${flashPrice}` : `${goodsDetailBean?.consumePrice || 0}` || 0
}}
</text>
</view>
<image class='close-image' :src='assetsUrl("ic_close.png")' @click.stop='close' />
</view>
<view class='sku-view c-flex-column'>
<view class='sku-title'>颜色</view>
<view class='sku-color-list c-flex-row'>
<view class='sku-item'
:class='{"sku-item-active":currentColorIndex==index}'
v-for='(item, index) in skuColorList' :key='index'
@click='colorChange(index)'>
<text>{{ item||'均色' }}</text>
</view>
</view>
<view class='sku-title' style='margin-top: 43rpx'>尺码</view>
<view class='sku-color-list c-flex-row'>
<view class='sku-item'
v-if='(skuSizeList?.length||0)>0'
:class='{"sku-item-active":currentSizeIndex==index,"sku-item-disabled":item.existingNumber<=0}'
v-for='(item, index) in skuSizeList' :key='index'
@click='sizeChange(index)'>
<text>{{ item.sizeName||'均码' }}</text>
</view>
<text v-else style='color: #999999;font-size: 30rpx'>暂无库存</text>
</view>
<view class='c-flex-row' style='margin-top: 52rpx'>
<text class='sku-title'>购买数量</text>
<view class='count-change-view c-flex-row'>
<view class='count-image' @click.stop='countChange(false)'>
<image :src='assetsUrl("ic_reduce.png")' />
</view>
<text>{{ goodsCount }}</text>
<view class='count-image' @click.stop='countChange(true)'>
<image :src='assetsUrl("ic_plus.png")' />
</view>
</view>
</view>
<button class='primary-button' :plain='currentSizeIndex<0' :disabled='currentSizeIndex<0' @click='confirm'>
确定
</button>
</view>
</view>
</uni-popup>
</template>
<script lang='ts' setup>
import { PropType, ref } from 'vue';
import { assetsUrl, defaultImage } from '@/utils/assets';
import { showToast } from '@/utils';
import { getGoodsDetail } from '@/api/goods';
import { GoodsBean, StockBean } from '@/api/goods/types';
import { useUserStore } from '@/store';
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const props = defineProps({
flashPrice: {
type: Number,
default: -1
},
exists: Object as PropType<StockBean>
});
const popupRef = ref();
const goodsDetailBean = ref<GoodsBean>();
const skuColorList = ref<string[]>([]);
const currentColorIndex = ref(0);
const currentSizeIndex = ref(-1);
const goodsCount = ref(1);
let callback: Function;
const show = async (goodsId: string, fn: Function) => {
callback = fn;
goodsDetailBean.value = await getGoodsDetail(goodsId);
goodsDetailBean.value.price = userStore.getRealGoodsPrice(goodsDetailBean.value?.price,goodsDetailBean.value?.priceExt)
goodsDetailBean.value.consumePrice = Number((goodsDetailBean.value?.price * userInfo?.value.userDiscount).toFixed(2));
if((goodsDetailBean.value?.stocks?.length || 0) <= 0) {
showToast('暂无库存');
return;
}
goodsDetailBean.value?.stocks?.map((res: { colorName: string }) => res.colorName).forEach((colorName: string) => {
if(!skuColorList.value.includes(colorName)) {
skuColorList.value.push(colorName);
}
});
setTimeout(() => {
popupRef.value.open();
}, 200);
if(props.exists) {
const colorIndex = skuColorList.value?.findIndex(res => res === props.exists?.colorName);
const sizeIndex = skuSizeList.value?.findIndex(res => res.sizeId === props.exists?.sizeId);
colorChange(colorIndex || 0);
sizeChange(sizeIndex || 0);
goodsCount.value = props.exists?.count;
}
if(skuSizeList) {
// skuSizeList.value![0].existingNumber = 2;
// skuSizeList.value![2].existingNumber = 2;
}
};
const skuSizeList = computed(() => {
const currentColorName = skuColorList.value[currentColorIndex.value];
let sizeList = goodsDetailBean.value?.stocks?.filter((res: { colorName: string }) => {
return res.colorName === currentColorName;
});
currentSizeIndex.value = sizeList?.findIndex(res => res.existingNumber > 0) || 0;
return sizeList;
});
const colorChange = (index: number) => {
currentColorIndex.value = index;
};
const sizeChange = (index: number) => {
if((skuSizeList.value![index].existingNumber || 0) <= 0) {
showToast('库存不足');
} else {
currentSizeIndex.value = index;
}
};
const countChange = (isPlus: boolean) => {
if(isPlus) {
goodsCount.value += 1;
} else {
if(goodsCount.value > 1) {
goodsCount.value -= 1;
}
}
};
const confirm = () => {
if(callback instanceof Function) {
callback({
...goodsDetailBean.value,
checkedStock: {
...skuSizeList.value![currentSizeIndex.value],
count: goodsCount.value
}
});
}
popupRef.value.close();
};
const close = () => {
popupRef.value.close();
};
defineExpose({
show
});
</script>
<style lang='scss' scoped>
.content {
background: #FFFFFF;
border-radius: 20rpx 20rpx 0 0;
padding: 23rpx 30rpx 78rpx 30rpx;
.goods-image {
width: 186rpx;
height: 186rpx;
border-radius: 10rpx;
margin-top: -50rpx;
margin-right: 18rpx;
}
.goods-name {
font-weight: bold;
font-size: 32rpx;
color: #333333;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.goods-price {
font-weight: bold;
font-size: 40rpx;
color: #D95554;
margin-top: 40rpx;
}
.goods-price:before {
content: '¥';
font-size: 28rpx;
margin-right: 2rpx;
}
.close-image {
width: 45rpx;
height: 45rpx;
}
.sku-view {
margin-top: 30rpx;
.sku-title {
font-weight: 400;
font-size: 30rpx;
color: #333333;
flex: 1;
}
.sku-color-list {
display: grid;
grid-template-columns:repeat(4, 1fr);
margin-top: 20rpx;
gap: 20rpx;
}
.sku-item {
display: flex;
align-items: center;
justify-content: center;
background: #F2F2F2;
border-radius: 26rpx;
font-weight: 400;
font-size: 26rpx;
color: #333333;
padding: 7rpx 32rpx;
}
.sku-item-active {
@extend .sku-item;
background: #FFF1F1;
color: #D95554;
}
.sku-item-disabled {
@extend .sku-item;
background: #F2F2F2;
color: #BEBEBE;
}
.count-change-view {
.count-image {
display: flex;
align-items: center;
justify-content: center;
width: 54rpx;
height: 54rpx;
background: #FBFBFB;
image {
width: 15rpx;
height: 2rpx;
}
}
.count-image:nth-of-type(2) image {
width: 17rpx;
height: 17rpx;
}
text {
display: flex;
width: 54rpx;
height: 54rpx;
align-items: center;
justify-content: center;
font-weight: 400;
font-size: 30rpx;
color: #333333;
background: #F2F2F2;
}
}
.primary-button {
width: 100%;
margin-top: 50rpx;
border: none;
}
}
}
</style>

View File

@@ -0,0 +1,88 @@
<template>
<view class='tab-container'>
<view v-for='(item, index) in titles' :key='index' class='tab-item' :class='{"tab-item-active":currentIndex==index}'
:style='{"flex": fixed?1:0,
"min-width":fixed?"150rpx":"auto",
"color": currentIndex==index?itemActiveColor:itemColor}'
@click.stop='onChange(index)'>
<text>{{ item }}</text>
<view class='indicator' v-if='currentIndex==index' :style='{
background: indicatorColor,
}' />
</view>
</view>
</template>
<script lang='ts' setup>
const props = defineProps({
titles: {
type: Object as PropType<string[]>,
default: ''
},
index: {
type: Number,
default: 0
},
fixed: {
type: Boolean,
default: true
}
,
itemColor: {
type: String,
default: '#666666'
},
itemActiveColor: {
type: String,
default: '#333333'
},
indicatorColor: {
type: String,
default: '#333333'
}
});
const emits = defineEmits(['change']);
const currentIndex = ref<number>(0);
watch(() => props.index, (newVal) => {
currentIndex.value = newVal;
});
const onChange = (index: number) => {
currentIndex.value = index;
emits('change', index);
};
</script>
<style lang='scss' scoped>
.tab-container {
display: flex;
flex-direction: row;
background: #FFFFFF;
height: 85rpx;
align-items: center;
.tab-item {
display: flex;
flex-direction: column;
text-align: center;
font-size: 30rpx;
justify-content: center;
align-items: center;
}
.tab-item-active {
@extend .tab-item;
font-size: 34rpx;
.indicator {
width: 68rpx;
height: 6rpx;
margin-top: 13rpx;
}
}
}
</style>

View File

@@ -2,44 +2,42 @@
"name": "SUKE-MP",
"appid": "",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
"versionName": "3.0.0",
"versionCode": "300",
"transformPx": false,
/* 5+App */
"app-plus":
{
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen":
{
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
/* */
"modules": {}
},
/* */
"mp-weixin":
{
"appid": "wx67a750d0ceed4d88",
"setting":
{
"mp-weixin": {
// "appid": "wx92e663dc11d0c0a8",
"appid": "wx67a750d0ceed4d88",
"setting": {
"urlCheck": false
},
"usingComponents": true
"usingComponents": true,
"plugins": {
"sqb-pay": {
"version": "1.3.0",
"provider": "wx55540b288c5ce319"
}
}
},
"uniStatistics":
{
"uniStatistics": {
"enable": false
},
"vueVersion": "3",
"h5":
{
"router":
{
"h5": {
"router": {
"mode": "hash",
"base": "/uniapp-vue3-template/"
}

View File

@@ -11,13 +11,14 @@
{
"path": "pages/home/index",
"style": {
"navigationBarTitleText": "首页"
"navigationBarTitleText": "首页",
"navigationStyle": "custom"
}
},
{
"path": "pages/qrcode/index",
"style": {
"navigationBarTitleText": "支付码"
"navigationBarTitleText": "会员支付码"
}
},
{
@@ -31,6 +32,12 @@
"style": {
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "肃客会员"
}
}
],
"subPackages": [
@@ -41,9 +48,160 @@
"path": "login/index",
"navigationStyle": "custom"
},
{
"path": "register/index",
"style": {
"navigationBarTitleText": "注册有礼"
}
},
{
"path": "groupbuy/index",
"style": {
"navigationBarTitleText": "团购秒杀",
"backgroundColor": "#F32B2B",
"backgroundColorTop": "#F32B2B",
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#F32B2B"
}
},
{
"path": "groupbuy/detail",
"style": {
"navigationBarTitleText": "商品团购券"
}
},
{
"path": "groupbuy/order-confirm",
"style": {
"navigationBarTitleText": "确认订单",
"usingComponents": {
"sqb-pay": "plugin://sqb-pay/sqb-pay"
}
}
},
{
"path": "webview/index",
"navigationBarTitleText": "网页"
},
{
"path": "payresult/index",
"style": {
"navigationBarTitleText": "支付结果"
}
}
]
},
{
"root": "pages/home/subs",
"pages": [
{
"path": "group/join",
"style": {
"navigationBarTitleText": "扫码加群"
}
}
]
},
{
"root": "pages/mall/subs",
"pages": [
{
"path": "goods/detail",
"style": {
"navigationBarTitleText": "商品详情"
}
},
{
"path": "order/order-confirm",
"style": {
"navigationBarTitleText": "确认订单",
"usingComponents": {
"sqb-pay": "plugin://sqb-pay/sqb-pay"
}
}
},
{
"path": "shoppingcart/index",
"style": {
"navigationBarTitleText": "购物车"
}
},
{
"path": "search/index",
"style": {
"navigationBarTitleText": "搜索"
}
}
]
},
{
"root": "pages/mine/subs",
"pages": [
{
"path": "profile/index",
"style": {
"navigationBarTitleText": "会员信息"
}
},
{
"path": "integral/index",
"style": {
"navigationBarTitleText": "我的积分"
}
},
{
"path": "recharge/index",
"style": {
"navigationBarTitleText": "会员充值",
"usingComponents": {
"sqb-pay": "plugin://sqb-pay/sqb-pay"
}
}
},
{
"path": "coupon/index",
"style": {
"navigationBarTitleText": "优惠券"
}
},
{
"path": "order/index",
"style": {
"navigationBarTitleText": "我的订单"
}
},
{
"path": "order/detail",
"style": {
"navigationBarTitleText": "订单详情",
"usingComponents": {
"sqb-pay": "plugin://sqb-pay/sqb-pay"
}
}
},
{
"path": "trade/index",
"style": {
"navigationBarTitleText": "消费记录"
}
},
{
"path": "address/index",
"style": {
"navigationBarTitleText": "收货地址"
}
},
{
"path": "address/create",
"style": {
"navigationBarTitleText": "新增收货地址"
}
},
{
"path": "contact/index",
"style": {
"navigationBarTitleText": "联系商家"
}
}
]
}
@@ -90,8 +248,8 @@
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
"navigationBarTitleText": "VIP顾客中心",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F7F7F7"
}
}

View File

@@ -0,0 +1,120 @@
<template>
<view class='coupon-content'>
<image :src='assetsUrl("bg_coupon.png")' />
<view class='left-content accent-text-color' :class='{"left-content-disabled": item.status==1}'>
<text>{{ item.reduce }}</text>
<text>{{ item.threshold }}元可用</text>
</view>
<view class='right-content accent-text-color' :class='{"right-content-disabled": item.status==1}'>
<text>{{ item?.name }}</text>
<text>有效期至{{ dayjs(item.startTime).format('YYYY-MM-DD') }}</text>
</view>
</view>
</template>
<script lang='ts' setup>
import { assetsUrl } from '@/utils/assets';
import dayjs from 'dayjs';
defineProps({
item: Object
});
</script>
<style lang='scss' scoped>
.coupon-content {
display: flex;
flex-direction: row;
height: 200rpx;
position: relative;
margin: 10rpx 30rpx 20rpx 30rpx;
image {
width: 100%;
height: 100%;
position: absolute;
}
.left-content {
display: flex;
flex-direction: column;
position: relative;
align-items: center;
justify-content: center;
padding-left: 50rpx;
text:nth-of-type(1) {
font-size: 55rpx;
font-weight: bold;
}
text:nth-of-type(1):after {
content: '元';
font-size: 24rpx;
}
text:nth-of-type(2) {
font-size: 26rpx;
font-weight: 400;
}
}
.left-content-disabled {
@extend .left-content;
color: #C2C2C2;
}
.right-content {
display: flex;
flex-direction: column;
position: relative;
flex: 1;
justify-content: center;
margin-left: 60rpx;
text:nth-of-type(1) {
font-size: 30rpx;
}
text:nth-of-type(2) {
font-size: 26rpx;
margin-top: 20rpx;
color: #333333;
}
.btn-text {
display: flex;
width: 146rpx;
height: 55rpx;
align-items: center;
justify-content: center;
border-radius: 28rpx;
border: #F32B2B solid 2rpx;
font-size: 28rpx;
font-weight: bold;
color: #F32B2B;
position: absolute;
right: 20rpx;
}
.btn-text-disabled {
@extend .btn-text;
color: #C2C2C2;
border: none;
}
}
.right-content-disabled {
@extend .right-content;
color: #C2C2C2;
text:nth-of-type(2) {
color: #C2C2C2;
}
}
}
</style>

View File

@@ -0,0 +1,680 @@
<template>
<view class='content'>
<view class='swiper-container'>
<swiper class='swiper' :interval='1500' :duration='1000' @change='swiperChange'>
<swiper-item v-for='(item,index) in bannerList' :key='index'>
<image :src='item||defaultImage' mode='aspectFill'/>
</swiper-item>
</swiper>
<view class='indicator'>
<text>{{ swiperIndex + 1 }}</text>
<text>/{{ bannerList.length }}</text>
</view>
</view>
<view class='countdown c-flex-row'>
<view class='super-second-kill'>
超级\n秒杀
</view>
<view class='price c-flex-column' style='flex: 1'>
<text>{{ groupBuyBean?.payPrice || 0 }}</text>
<text>{{ groupBuyBean?.price || 0 }}</text>
</view>
<view v-if='isPending()' class='countdown-time c-flex-column'>
<view class='c-flex-row'>{{ countdownTime?.days || 0 }}
<text class='time'>{{ countdownTime?.hours || '00' }}</text>
:
<text class='time'>{{ countdownTime?.minutes || '00' }}</text>
:
<text class='time'>{{ countdownTime?.seconds || '00' }}</text>
</view>
<text>距离活动结束仅剩</text>
</view>
<view v-else>
<text style='font-size: 35rpx;color: #FFFFFF'>
{{ isNotStated() ? '未开始' : '已结束' }}
</text>
</view>
</view>
<view class='goods-info-view c-flex-column'>
<view class='c-flex-row'>
<text class='goods-price accent-text-color'>{{ groupBuyBean?.payPrice || 0 }}</text>
<text>销量{{ groupBuyBean?.totalNum || 0 }}</text>
</view>
<view class='c-flex-row'>
<text style='flex: 1'>{{ groupBuyBean?.name || '未知' }}</text>
<view class='share-button c-flex-column'>
<image :src='assetsUrl("ic_share.png")'></image>
<button class='btn' plain open-type='share'>分享</button>
</view>
</view>
</view>
<view class='goods-sku-view c-flex-column'>
<view class='c-flex-row' @click.stop='showSkuDialog'>
<text>选择</text>
<text>规格 颜色/尺码</text>
<image :src='assetsUrl("ic_arrow_right_gray.png")' />
<text>{{ getStockColorCount }}种颜色可选</text>
</view>
</view>
<view class='recommend-view c-flex-column' v-if='(recommendList?.length||0)>0'>
<text>浏览此商品的客户还浏览了</text>
<scroll-view scroll-x>
<view style='display: inline-block' v-for='(item, index) in recommendList'
:key='index'>
<view class='recommend-item c-flex-column'>
<image :src='item.images||defaultImage' />
<text>{{ item.goodsName || '未知' }}</text>
<text class='goods-price'>{{ item.price }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 商品详情图片 -->
<view class='card-view image-container' v-if='groupBuyBean?.content'>
<text class='card-view-title'>商品详情</text>
<image
v-for='(item,index) in JSON.parse(groupBuyBean?.content).filter((a: any) => a.type === 2).map((b: any) => b.images)'
:src='item' mode='aspectFill' :key='index' />
</view>
<!-- 商品详情 -->
<view class='card-view goods-container'>
<view class='c-flex-row'>
<image :src='groupBuyBean?.goods?.images' />
<view class='c-flex-column'>
<text class='goods-name'>{{ groupBuyBean?.goods?.name }}</text>
<text style='color: #a6a6a6'>精选</text>
<view class='tag-view c-flex-row'>
<text>团长推荐</text>
<text>今日必买</text>
</view>
<text style='color: #a6a6a6'>不参与会员折扣</text>
<text style='color: #F32B2B'>¥{{ groupBuyBean?.goods?.payPrice }}</text>
<text class='bought_count'>已团{{ groupBuyBean?.sendNum }}</text>
</view>
</view>
</view>
<!-- 商品详情优惠券 -->
<view class='card-view coupon-container'>
<text class='card-view-title'>赠送优惠券</text>
<view class='c-flex-column'>
<coupon-item v-for='(item,index) in groupBuyBean?.couponsList' :key='index' :item='item' />
</view>
</view>
<!-- 商品详情跟团记录 -->
<view class='card-view record-container'>
<text class='card-view-title'>跟团记录</text>
<u-list :list='recordList' :border='false' @scrolltolower='loadMore'>
<u-list-item v-for='(item,index) in recordList' :key='index'>
<view class='item-view c-flex-row'>
<image :src='item.memberImage' />
<view class='c-flex-column' style='flex: 1'>
<text>{{ item.goodsCode }}</text>
<text>{{ item.createTime }}</text>
</view>
<text>+{{ item.goodsNum }}</text>
</view>
</u-list-item>
<u-loadmore status='loading' />
</u-list>
</view>
<view class='bottom-view c-flex-row'>
<button v-if='isPending()' class='place-order-button' @click.stop='placeOrder'>跟团购买</button>
<button v-else class='place-order-button-disable' :plain='true' :disabled='true'>
{{ isNotStated() ? '即将开始' : '已结束' }}
</button>
</view>
</view>
<sku-dialog ref='skuDialogRef' :flash-price='Number(groupBuyBean?.payPrice) || 0' />
</template>
<script lang='ts' setup>
import { assetsUrl, defaultImage } from '@/utils/assets';
import dayjs from 'dayjs';
import { formatTimeWithZeroPad, goPath, isLogin } from '@/utils';
import SkuDialog from '@/components/sku-dialog.vue';
import CouponItem from './components/coupon-item.vue';
import { getGroupBuyDetail, getGroupBuyRecordList, preOrder } from '@/api/groupbuy';
import { getGoodsList } from '@/api/goods';
import { GoodsBean } from '@/api/goods/types';
import { useUserStore } from '@/store';
import { GroupBuyBean, RecordBean } from '@/api/groupbuy/types';
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const skuDialogRef = ref();
const groupBuyBean = ref<GroupBuyBean>();
const recommendList = ref<GoodsBean[]>();
const bannerList = ref([]);
const swiperIndex = ref(0);
let interval: number;
const countdownTime = ref<{
days: string,
hours: string,
minutes: string,
seconds: string
}>();
const recordList = ref<RecordBean[]>([]);
const currentPageNum = ref(1);
onLoad(async (e: any) => {
if(isLogin()) {
await uni.showLoading();
groupBuyBean.value = await getGroupBuyDetail(e.id);
groupBuyBean.value.goods.price = groupBuyBean.value.price;
bannerList.value = JSON.parse(groupBuyBean.value.content).filter((item: any) => item.type === 2).map((item: any) => item.images);
countdown();
await fetchRecommendList();
await fetchBuyRecordList();
uni.hideLoading();
}
});
onUnload(() => {
if(interval) {
clearInterval(interval);
}
});
const isNotStated = () => {
return dayjs(groupBuyBean.value?.startDate).isAfter(Date.now());
};
const isPending = () => {
return dayjs(groupBuyBean.value?.startDate).isBefore(Date.now()) && dayjs(groupBuyBean.value?.endDate).isAfter(Date.now());
;
};
const isEnded = () => {
return dayjs(groupBuyBean.value?.endDate).isBefore(Date.now());
};
const getStockColorCount = computed(() => {
const list = Array.from(new Set(groupBuyBean.value?.goods?.stocks?.map(item => item.colorName)))
.map(colorName => groupBuyBean.value?.goods?.stocks?.find(item => item.colorName === colorName)!);
return list.length;
});
const fetchRecommendList = async () => {
const { rows } = await getGoodsList({
page: {
page: 1,
pageSize: 20,
bean: {
keyword: '',
typeIds: []
}
}
});
recommendList.value = rows;
};
const fetchBuyRecordList = async (refresh: boolean = true) => {
if(!refresh) {
currentPageNum.value += 1;
}
const { list } = await getGroupBuyRecordList(groupBuyBean?.value?.id || '', currentPageNum.value, 20);
recordList.value = recordList.value.concat(list);
};
const loadMore = () => {
fetchBuyRecordList(false);
};
const swiperChange = (e: any) => {
swiperIndex.value = e.detail.current;
};
const countdown = () => {
interval = setInterval(() => {
if(groupBuyBean.value?.endDate) {
let now = new Date();
let end = dayjs(groupBuyBean.value?.endDate).toDate().getTime();
let remaining = Math.floor((end - now.getTime()) / 1000);
if(remaining > 0) {
countdownTime.value = {
days: formatTimeWithZeroPad(Math.floor(remaining / 60 / 60 / 24)),
hours: formatTimeWithZeroPad(Math.floor(remaining / 60 / 60 % 24)),
minutes: formatTimeWithZeroPad(Math.floor(remaining / 60 % 60)),
seconds: formatTimeWithZeroPad(Math.floor(remaining % 60))
};
} else {
clearInterval(interval);
}
}
}, 1000);
};
const showSkuDialog = (fn: Function) => {
skuDialogRef.value.show(groupBuyBean?.value?.goods?.goodsId, fn);
};
const placeOrder = async () => {
async function create(bean: GoodsBean) {
const params = {
'colorId': bean.checkedStock.colorId,
'sizeId': bean.checkedStock.sizeId,
'goodsId': groupBuyBean?.value?.goods.goodsId,
'groupId': groupBuyBean?.value?.id,
'memberId': userInfo.value.id,
'shareId': '123456'
};
const result = await preOrder(params);
goPath(`/pages/common/groupbuy/order-confirm?orderBean=${encodeURIComponent(JSON.stringify(result))}`);
}
showSkuDialog((e: GoodsBean) => {
create(e);
});
};
</script>
<style lang='scss' scoped>
.content {
padding-bottom: 200rpx;
}
.swiper-container {
position: relative;
.swiper {
display: flex;
width: 100%;
height: 750rpx;
image {
width: 100%;
height: 750rpx;
}
}
.indicator {
background: rgba(1, 1, 1, 0.5);
border-radius: 45rpx;
font-weight: 400;
font-size: 24rpx;
color: #FFFFFF;
position: absolute;
right: 20rpx;
bottom: 20rpx;
padding: 5rpx 25rpx;
text:nth-of-type(1) {
font-size: 30rpx;
}
text:nth-of-type(2) {
font-size: 24rpx;
}
}
}
.countdown {
display: flex;
height: 130rpx;
position: relative;
padding: 15rpx 30rpx;
background-image: url('https://img.lakeapp.cn/wx/images/bg_groupbuy_countdown.png');
background-size: 100% 100%;
.super-second-kill {
display: flex;
width: 100rpx;
height: 100rpx;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.3);
border-radius: 20rpx;
border: 1rpx solid #FFFFFF;
font-weight: 400;
font-size: 36rpx;
text-align: center;
color: #FFFFFF;
margin-right: 23rpx;
}
.price {
color: #FFFFFF;
text:nth-of-type(1) {
font-weight: bold;
font-size: 55rpx;
}
text:nth-of-type(1):before {
content: '¥ ';
font-size: 30rpx;
}
text:nth-of-type(2) {
font-weight: 400;
font-size: 30rpx;
text-decoration-line: line-through;
}
}
.countdown-time {
color: #FFFFFF;
align-items: center;
view:nth-of-type(1) {
margin-bottom: 20rpx;
align-self: flex-end;
}
.time {
display: flex;
align-items: center;
justify-content: center;
width: 40rpx;
height: 40rpx;
margin-left: 12rpx;
margin-right: 12rpx;
background: #FFFFFF;
border-radius: 7rpx;
color: #F32B2B;
}
.time:nth-of-type(1) {
margin-left: 18rpx;
}
.time:nth-of-type(3) {
margin-right: 0;
}
text:not(.time):nth-of-type(1) {
align-self: flex-end;
}
}
}
.goods-info-view {
background: #FFFFFF;
padding: 20rpx 30rpx;
view:nth-of-type(1) {
text:nth-of-type(1) {
display: flex;
font-size: 40rpx;
text-align: center;
align-items: flex-end;
flex: 1;
}
.goods-price:before {
content: "¥";
font-size: 30rpx;
margin-bottom: 5rpx;
margin-right: 5rpx;
}
text:nth-of-type(2) {
font-size: 26rpx;
font-weight: 400;
color: #999999;
}
}
view:nth-of-type(2) {
text {
font-weight: bold;
font-size: 32rpx;
color: #333333;
margin-right: 10rpx;
}
.share-button {
align-items: center;
image {
width: 36rpx;
height: 32rpx;
}
.btn {
background: #00000000;
font-size: 24rpx;
color: #636566;
white-space: nowrap;
padding: 0;
border: none !important;
}
}
}
}
.goods-sku-view {
background: #FFFFFF;
padding: 20rpx 30rpx;
margin-top: 20rpx;
view:nth-of-type(n+1) {
position: relative;
align-items: center;
height: 80rpx;
text:nth-of-type(1) {
width: 100rpx;
font-weight: 400;
font-size: 28rpx;
color: #999999;
}
text:nth-of-type(2) {
font-weight: 400;
font-size: 28rpx;
color: #333333;
flex: 1;
}
image {
width: 13rpx;
height: 24rpx;
}
text:nth-of-type(3) {
font-weight: 400;
font-size: 24rpx;
color: #999999;
position: absolute;
top: 50rpx;
left: 100rpx;
}
}
view:nth-of-type(1) {
align-items: flex-start;
//padding-top: 20rpx;
}
}
.recommend-view {
background: #FFFFFF;
padding: 20rpx 30rpx;
margin-top: 20rpx;
text {
font-weight: bold;
font-size: 28rpx;
color: #333333;
}
scroll-view {
width: 100%;
margin-top: 20rpx;
white-space: nowrap;
height: 300rpx;
}
.recommend-item {
margin-right: 20rpx;
image {
width: 200rpx;
height: 200rpx;
border-radius: 10rpx;
}
text:nth-of-type(1) {
font-weight: bold;
font-size: 28rpx;
color: #333333;
margin-top: 20rpx;
}
text:nth-of-type(2) {
font-weight: bold;
font-size: 28rpx;
color: #F32B2B;
}
.goods-price:before {
content: "¥";
font-size: 28rpx;
}
}
}
.bottom-view {
background: #FFFFFF;
padding: 20rpx 30rpx 78rpx 30rpx;
position: fixed;
left: 0;
right: 0;
bottom: 0;
.place-order-button {
display: flex;
flex: 1;
font-weight: bold;
width: 100%;
font-size: 30rpx;
height: 80rpx;
align-items: center;
justify-content: center;
background: #F32B2B;
color: #FFFFFF;
border-radius: 40rpx;
border: none;
}
.place-order-button-disable {
@extend .place-order-button;
background: #b9b5b5;
color: #FFFFFF;
}
}
.card-view {
display: flex;
flex-direction: column;
background: #FFFFFF;
margin-top: 20rpx;
.card-view-title {
margin: 20rpx 30rpx;
font-weight: bold;
font-size: 30rpx;
color: #333333;
}
}
.image-container {
image {
width: 100%;
}
}
.goods-container {
padding: 30rpx;
position: relative;
image {
width: 200rpx;
height: 200rpx;
margin-right: 15rpx;
border-radius: 8rpx;
}
.c-flex-column {
justify-content: space-between;
position: relative;
flex: 1;
text {
margin-top: 5rpx;
}
.goods-name {
font-size: 30rpx;
color: #333333;
}
.tag-view {
text {
background: #fff0ee;
margin-right: 10rpx;
padding: 5rpx 10rpx;
color: #fc6e51;
border-radius: 8rpx;
}
}
.bought_count {
position: absolute;
bottom: 0;
right: 0;
color: #fc6e51;
font-size: 20rpx;
}
}
}
.record-container {
.item-view {
margin: 10rpx 30rpx;
image {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
}
.c-flex-column {
margin: 0 15rpx;
text:nth-of-type(1) {
font-size: 20rpx;
color: #333333;
}
text:nth-of-type(2) {
font-size: 15rpx;
color: #666666;
margin-top: 10rpx;
}
}
text:nth-of-type(1) {
color: #F32B2B;
}
}
}
</style>

View File

@@ -0,0 +1,208 @@
<template>
<view class='content'>
<view class='tab c-flex-row'>
<text class='tab-item' :class='{"tab-item-active":tabIndex==0}' @click.stop='tabIndex=0'>团购中</text>
<text class='tab-item' :class='{"tab-item-active":tabIndex==1}' @click.stop='tabIndex=1'>即将开始</text>
</view>
<scroll-view class='scroll-view'>
<!-- <u-list class='scroll-view' :border='false' @scrolltolower='loadMore'>-->
<!-- <u-list-item v-for='(item,index) in groupbuyList' :key='index'>-->
<view class='c-flex-column' v-for='(item,index) in groupbuyList' :key='index'>
<view class='item c-flex-row' @click.stop='goPath(`/pages/common/groupbuy/detail?id=${item.id}`)'>
<image class='goods-image' mode='aspectFill' :src='JSON.parse(item.content)[0].images||defaultImage' />
<view class='c-flex-column' style='flex: 1'>
<view class='goods-name'>{{ item.name }}</view>
<view class='middle-view c-flex-row'>
<text>原价{{ item.price }}</text>
<view v-if='(item.price - item.payPrice)>0' class='decline c-flex-row'>
<image :src='assetsUrl("ic_decline.png")' />
<text>直降¥{{ (item.price - item.payPrice).toFixed(2) }}</text>
</view>
<text>{{ tabIndex == 0 ? '即将恢复' : '即将开始' }}</text>
</view>
<view class='bottom-price c-flex-row' :class='{"bottom-price-disabled":tabIndex==1}'>
<text>{{ item.payPrice }}</text>
<text>团购</text>
</view>
</view>
</view>
<view class='divider' style='margin: 0 30rpx' />
</view>
<!-- </u-list-item>-->
<!-- </u-list>-->
<u-empty v-if='groupbuyList.length === 0' text='暂无团购活动' marginTop='100' />
</scroll-view>
</view>
</template>
<script lang='ts' setup>
import { assetsUrl, defaultImage } from '@/utils/assets';
import { getGroupBuyList } from '@/api/groupbuy';
import { goPath } from '@/utils';
const tabIndex = ref(0);
const groupbuyList = ref<any[]>([]);
const currentPageNum = ref(1);
onLoad((e) => {
fetchData();
});
watch(() => tabIndex.value, () => {
fetchData();
});
const fetchData = async (refresh: boolean = true) => {
await uni.showLoading();
if(!refresh) {
currentPageNum.value += 1;
}
const { list } = await getGroupBuyList({
pageNum: currentPageNum.value,
pageSize: 100,
obj: {
timeStatus: tabIndex.value + 1
}
});
groupbuyList.value = list;
uni.hideLoading();
};
const loadMore = () => {
fetchData(false);
};
</script>
<style lang='scss' scoped>
.content {
background: #F32B2B;
padding-top: 30rpx;
}
.tab {
height: 92rpx;
.tab-item:nth-of-type(1) {
border-radius: 30rpx 0 0 0;
}
.tab-item:nth-of-type(2) {
border-radius: 0 30rpx 0 0;
}
.tab-item {
display: flex;
flex: 1;
height: 100%;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 30rpx;
color: #999999;
background: #F2F2F2;
}
.tab-item-active {
@extend .tab-item;
background: #FFFFFF;
color: #333333;
}
}
.scroll-view {
background: #FFFFFF;
height: 100vh;
.item {
padding: 20rpx 30rpx 40rpx 30rpx;
background: #FFFFFF;
position: relative;
.goods-image {
width: 180rpx;
height: 180rpx;
border-radius: 10rpx;
margin-right: 20rpx;
}
.goods-name {
font-weight: bold;
font-size: 30rpx;
color: #333333;
-webkit-line-clamp: 1;
text-overflow: ellipsis;
margin-top: 30rpx;
}
.middle-view {
justify-content: space-between;
margin-top: 16rpx;
text:nth-of-type(1) {
font-weight: bold;
font-size: 26rpx;
color: #333333;
}
.decline {
image {
width: 45rpx;
height: 45rpx;
}
text {
font-weight: bold;
font-size: 24rpx;
color: #AD6021;
}
}
text:nth-of-type(2) {
font-weight: 400;
font-size: 24rpx;
color: #AD6021;
}
}
.bottom-price {
display: flex;
width: 390rpx;
height: 83rpx;
margin-top: 20rpx;
align-self: flex-end;
background-image: url("https://img.lakeapp.cn/wx/images/bg_groupbuy_price.png");
background-size: 100% 100%;
color: #FFFFFF;
justify-content: space-between;
text:nth-of-type(1) {
font-weight: bold;
font-size: 40rpx;
color: #FFFFFF;
margin-left: 30rpx;
margin-bottom: 5rpx;
}
text:nth-of-type(1):before {
content: "秒杀价 ¥ ";
font-size: 24rpx;
margin-bottom: 5rpx;
}
text:nth-of-type(2) {
font-weight: bold;
font-size: 30rpx;
color: #FFFFFF;
margin-right: 50rpx;
}
}
.bottom-price-disabled {
background-image: url("https://img.lakeapp.cn/wx/images/bg_groupbuy_price_disabled.png");
}
}
}
</style>

View File

@@ -0,0 +1,185 @@
<template>
<view class='content'>
<view class='card-view'>
<template class='c-flex-row' v-for='item in orderBean?.orderGoods' :key='item.id'>
<image class='goods-image' :src='item?.images||defaultImage' />
<view class='c-flex-column' style='flex: 1'>
<text class='goods-name'>{{ item?.goodsName || item?.goodsCode }}</text>
<text class='goods-sku'>{{ item?.stockStock?.colorName }} {{ item?.stockStock?.sizeName }}</text>
</view>
<view class='c-flex-column'>
<text class='goods-num'>x{{ item?.goodsNum }}</text>
<view class='c-flex-row'>
<text class='goods-price' style='margin-right: 10rpx;text-decoration: line-through'>¥{{ item?.originPrice }}
</text>
<text class='goods-price'>¥{{ item?.consumePrice }}</text>
</view>
</view>
</template>
</view>
<view class='card-view'>
<text style='flex: 1'>实付</text>
<text style='color: #F32B2B'>¥{{ orderBean?.totalPrice || 0 }}</text>
</view>
<view class='bottom-view c-flex-row'>
<sqb-pay @navigateTo='navigateTo'
:return_url='buildSqbParams.return_url'
:total_amount='buildSqbParams.total_amount'
:terminal_sn='buildSqbParams.terminal_sn'
:client_sn='buildSqbParams.client_sn'
:subject='buildSqbParams.subject'
:subject_img='buildSqbParams.subject_img '
:merchant_name='buildSqbParams.merchant_name'
:notify_url='buildSqbParams.notify_url'
:sign='buildSqbParams.sign'>
<button class='confirm-button' @click='payment'>支付</button>
</sqb-pay>
</view>
</view>
</template>
<script lang='ts' setup>
import { pay, progress } from '@/api/groupbuy';
import { OrderBean } from '@/api/order/types';
import { parseParameter, showToast, sortASCII } from '@/utils';
import { hexMD5 } from '@/utils/common/md5';
import { useUserStore } from '@/store';
import { handlePayResult } from '@/utils/order';
import { defaultImage } from '@/utils/assets';
const userState = useUserStore();
const { terminalInfo } = storeToRefs(userState);
const orderBean = ref<OrderBean>();
onLoad((e: any) => {
orderBean.value = JSON.parse(decodeURIComponent(e?.orderBean));
});
const buildSqbParams = computed(() => {
const params = sortASCII({
client_sn: orderBean.value?.id || '',
return_url: '/pages/common/payresult/index',
total_amount: Number(((orderBean.value?.totalPrice || 0) * 100).toFixed(2)),
terminal_sn: terminalInfo.value.terminalSn,
subject: '商品团购券',
subject_img: orderBean?.value?.orderGoods[0].images || '',
merchant_name: terminalInfo.value.companyName,
notify_url: 'https://www.baidu.com'
}, true);
return {
...params,
sign: hexMD5(parseParameter(params) + '&key=' + terminalInfo.value.terminalKey).toUpperCase()
};
});
const navigateTo = (e: any) => {
handlePayResult(orderBean.value?.id, e, {
onSuccess: () => {
showToast('支付成功', {
icon: 'success',
complete: () => {
pay({
'orderId': orderBean.value?.id,
'result': 'xxx'
}).then(res => {
uni.navigateBack();
});
uni.hideLoading();
}
});
}
});
};
const payment = () => {
const params = {
'id': orderBean.value?.id,
'orderSn': buildSqbParams.value.client_sn,
'terminal_key': terminalInfo.value.terminalKey,
'terminal_sn': terminalInfo.value.terminalSn
};
progress(params);
};
</script>
<style lang='scss' scoped>
.content {
.card-view:nth-of-type(1) {
margin-top: 30rpx;
}
}
.card-view {
display: flex;
flex-direction: row;
margin: 10rpx 30rpx;
background: #FFFFFF;
font-size: 35rpx;
padding: 30rpx;
.goods-image {
width: 130rpx;
height: 130rpx;
margin-right: 15rpx;
border-radius: 8rpx;
}
.goods-name {
color: #333333;
}
.goods-sku {
font-size: 30rpx;
color: #666666;
}
.c-flex-column {
justify-content: space-between;
.goods-num {
display: flex;
font-size: 30rpx;
justify-content: flex-end;
color: #999999;
}
.goods-price {
@extend .goods-num;
}
}
}
.bottom-view {
background: #FFFFFF;
padding: 20rpx 30rpx 78rpx 33rpx;
position: fixed;
left: 0;
right: 0;
bottom: 0;
sqb-pay {
flex: 1;
.confirm-button {
display: flex;
flex: 1;
font-weight: bold;
width: 100%;
font-size: 30rpx;
height: 80rpx;
align-items: center;
justify-content: center;
background: #F32B2B;
color: #FFFFFF;
border-radius: 40rpx;
}
}
}
</style>

View File

@@ -1,167 +1,118 @@
<template>
<view>
<view class="login-form-wrap">
<view class="title">
欢迎登录
</view>
<input v-model="tel" class="u-border-bottom" type="number" placeholder="请输入手机号">
<view class="u-border-bottom my-40rpx flex">
<input v-model="code" class="flex-1" type="number" placeholder="请输入验证码">
<view>
<u-code ref="uCodeRef" @change="codeChange" />
<u-button :text="tips" type="success" size="mini" @click="getCode" />
</view>
</view>
<button :style="[inputStyle]" class="login-btn" @tap="submit">
登录
</button>
<view class='content'>
<view class='title'>
欢迎登录VIP顾客中心
</view>
<view class="alternative">
<view class="password">
密码登录
</view>
<view class="issue">
遇到问题
</view>
</view>
</view>
<view class="login-type-wrap">
<view class="item wechat">
<view class="icon">
<u-icon size="35" name="weixin-fill" color="rgb(83,194,64)" />
</view>
微信
</view>
<view class="item QQ">
<view class="icon">
<u-icon size="35" name="qq-fill" color="rgb(17,183,233)" />
</view>
QQ
</view>
</view>
<view class="hint">
登录代表同意
<text class="link">
用户协议隐私政策
<image class='logo' :src='assetsUrl("ic_logo.jpeg")' />
<button class='login-btn' @tap='wechatLogin'>
微信登录
</button>
<view class='hint'>
<image @click.stop='bindCheck'
:src='isAgreePrivacy?("/static/images/ic_checked_green.png"):("/static/images/ic_checked_gray.png")' />
<text @click.stop='bindCheck'>请先阅读并同意
<text class='link' @click.stop='openPrivacy'>
小程序隐私保护协议
</text>
并授权使用您的账号信息如昵称头像收获地址以便您统一管理
</text>
并授权使用您的账号信息如昵称头像收获地址以便您统一管理
</view>
</view>
</template>
<script setup lang="ts">
import uCode from 'uview-plus/components/u-code/u-code.vue';
import { setToken } from '@/utils/auth';
<script setup lang='ts'>
import { useUserStore } from '@/store';
import { assetsUrl } from '@/utils/assets';
import { showToast } from '@/utils';
const tel = ref<string>('18502811111');
const code = ref<string>('1234');
const tips = ref<string>();
const uCodeRef = ref<InstanceType<typeof uCode> | null>(null);
const userStore = useUserStore();
const isAgreePrivacy = ref(false);
const inputStyle = computed<CSSStyleDeclaration>(() => {
const style = {} as CSSStyleDeclaration;
if (tel.value && code.value) {
style.color = '#fff';
style.backgroundColor = uni.$u.color.warning;
function wechatLogin() {
if(!isAgreePrivacy.value) {
showToast('请先阅读并同意小程序隐私保护协议');
return;
}
return style;
});
function codeChange(text: string) {
tips.value = text;
}
function getCode() {
if (uCodeRef.value?.canGetCode) {
// 模拟向后端请求验证码
uni.showLoading({
title: '正在获取验证码',
});
setTimeout(() => {
uni.hideLoading();
uni.$u.toast('验证码已发送');
// 通知验证码组件内部开始倒计时
uCodeRef.value?.start();
}, 1000);
}
else {
uni.$u.toast('倒计时结束后再发送');
}
}
function submit() {
if (uni.$u.test.mobile(tel.value)) {
setToken('1234567890');
uni.showLoading();
userStore.authLogin().then(() => {
uni.hideLoading();
uni.reLaunch({ url: '/pages/home/index' });
}
});
}
const bindCheck = () => {
isAgreePrivacy.value = !isAgreePrivacy.value;
};
const openPrivacy = () => {
wx.openPrivacyContract({
success: () => {
}, // 打开成功
fail: () => {
}, // 打开失败
complete: () => {
}
});
};
</script>
<style lang="scss" scoped>
.login-form-wrap {
margin: 80rpx auto 0;
width: 600rpx;
.title {
margin-bottom: 100rpx;
font-size: 60rpx;
text-align: left;
font-weight: 500;
}
input {
padding-bottom: 6rpx;
margin-bottom: 10rpx;
text-align: left;
}
.tips {
margin-top: 8rpx;
margin-bottom: 60rpx;
color: $u-info;
}
.login-btn {
padding: 12rpx 0;
font-size: 30rpx;
color: $u-tips-color;
background-color: rgb(253 243 208);
border: none;
&::after {
border: none;
}
}
.alternative {
display: flex;
justify-content: space-between;
margin-top: 30rpx;
color: $u-tips-color;
}
<style lang='scss' scoped>
.content {
background: #FFFFFF;
height: 100vh;
}
.login-type-wrap {
display: flex;
justify-content: space-between;
padding: 350rpx 150rpx 150rpx;
.title {
font-size: 60rpx;
text-align: left;
font-weight: 500;
margin: 50rpx;
}
.item {
display: flex;
align-items: center;
font-size: 28rpx;
color: $u-content-color;
flex-direction: column;
.logo {
width: 300rpx;
height: 300rpx;
align-self: center;
margin: 30rpx;
border-radius: 50%;
}
.login-btn {
padding: 12rpx 0;
font-size: 30rpx;
color: #FFFFFF;
background-color: #1cbf1e;
border: none;
border-radius: 45rpx;
margin: 100rpx 45rpx 0 45rpx;
&::after {
border: none;
}
}
.hint {
padding: 20rpx 40rpx;
font-size: 20rpx;
display: inline-block;
padding: 20rpx 50rpx;
font-size: 25rpx;
color: $u-tips-color;
position: absolute;
align-items: center;
bottom: 50rpx;
.link {
color: $u-warning;
}
image {
width: 30rpx;
height: 30rpx;
margin-right: 10rpx;
margin-bottom: -5rpx;
}
}
</style>

View File

@@ -0,0 +1,11 @@
<template>
<text>支付成功</text>
</template>
<script lang='ts' setup>
</script>
<style lang='scss' scoped>
</style>

View File

@@ -0,0 +1,228 @@
<template>
<view class='content'>
<image class='bg-image' :src='assetsUrl("bg_register.png")' />
<image :src='assetsUrl("ic_register_gift_text.png")'
style='align-self: center;position: relative;width: 356rpx;height: 124rpx;margin-top: 70rpx' />
<view class='card-view'>
<view class='mobile-view c-flex-row' style='display: none'>
<image :src='assetsUrl("ic_register_mobile.png")' />
<input placeholder='请输入手机号' inputmode='number' maxLength='11' />
</view>
<view class='captcha-view c-flex-row' style='display: none'>
<view class='c-flex-row'>
<image :src='assetsUrl("ic_register_captcha.png")' />
<input placeholder='请输入验证码' inputmode='number' maxLength='6' />
</view>
<text @click.stop='startCountdown'>{{ countdown }}</text>
</view>
<text class='btn_register' style='display: none' @click.stop='register'>注册领券</text>
<image :src='companyConfigInfo.regqrcode||defaultImage' :show-menu-by-longpress='true' mode='aspectFill'
style='width: 439rpx;height: 439rpx;align-self: center' />
<text style='font-size: 26rpx;
margin-top: 10rpx;
align-self: center;color: #666666;'>长按扫码注册
</text>
<template v-if='couponBean&&couponBean.status==0'>
<view class='divider' />
<view class='coupon c-flex-column' @click.stop='goPath("/pages/mine/subs/coupon/index")'>
<view class='c-flex-row'>
<text class='coupon-name'>{{ couponBean.title }}</text>
<image :src='assetsUrl("ic_arrow_right_gray.png")' />
</view>
<text class='expired-time'>
有效期至{{ dayjs(Date.now()).add(couponBean.triggerEndDay, 'day').format('YYYY.MM.DD') }}
</text>
</view>
<view class='divider' />
<text style='font-size: 28rpx;
font-weight: bold;
color: #333333;margin-top: 20rpx'>福利详情
</text>
<text style='font-size: 28rpx;
color: #333333;margin-top: 14rpx'>1.优惠金额{{ couponBean.reduce }}\n
2.{{ couponBean.threshold }}{{ couponBean.reduce }}\n
3.赠送{{ couponBean.publicNum }}
</text>
</template>
<template v-else>
<text style='font-size: 28rpx;
font-weight: bold;
color: #333333;margin-top: 20rpx'>
微信扫一扫领取会员卡
</text>
<text style='font-size: 28rpx;
color: #333333;margin-top: 14rpx'>
1及时接受消费小票\n
2随时查询消费记录\n
3及时接收优惠信息
</text>
</template>
</view>
</view>
</template>
<script lang='ts' setup>
import { assetsUrl, defaultImage } from '@/utils/assets';
import { useUserStore } from '@/store';
import { goPath } from '@/utils';
import { getRegisterCoupon } from '@/api/user';
import dayjs from 'dayjs';
const userStore = useUserStore();
const { companyConfigInfo } = storeToRefs(userStore);
const countdown = ref('获取验证码');
const couponBean = ref();
onLoad(async (e) => {
couponBean.value = await getRegisterCoupon();
});
const startCountdown = () => {
if(countdown.value !== '获取验证码') {
return;
}
let time = 120;
let interval = setInterval(() => {
time--;
countdown.value = `${time}s 重新获取`;
if(time == 0) {
clearInterval(interval);
countdown.value = '获取验证码';
}
}, 1000);
};
const register = () => {
};
</script>
<style lang='scss' scoped>
.content {
position: relative;
height: 100vh;
}
.bg-image {
position: fixed;
height: 100vh;
width: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.card-view {
background: #FFFFFF;
border-radius: 10rpx;
margin: 0 40rpx;
padding: 50rpx;
top: 150rpx;
position: relative;
.mobile-view {
border-radius: 10rpx;
border: 1rpx solid #D8D8D8;
padding-left: 28rpx;
image {
width: 36rpx;
height: 36rpx;
}
input {
flex: 1;
height: 80rpx;
font-size: 30rpx;
color: #333333;
margin-left: 26rpx;
}
}
.captcha-view {
margin-top: 20rpx;
.c-flex-row {
border-radius: 10rpx;
border: 1rpx solid #D8D8D8;
padding-left: 28rpx;
flex: 1;
image {
width: 33rpx;
height: 26rpx;
}
input {
height: 80rpx;
font-size: 30rpx;
color: #333333;
margin-left: 26rpx;
}
}
text {
display: flex;
height: 80rpx;
background: #FFD436;
border-radius: 10rpx;
margin-left: 12rpx;
padding: 0 15rpx;
font-size: 28rpx;
align-items: center;
justify-content: center;
color: #FFFFFF;
}
}
.btn_register {
display: flex;
align-items: center;
justify-content: center;
height: 90rpx;
background: #F53636;
border-radius: 10rpx;
font-weight: 400;
font-size: 30rpx;
color: #FFFFFF;
margin-top: 46rpx;
}
.divider {
border-bottom-style: dashed;
border-color: #E98585;
border-width: 0.2rpx;
margin-top: 34rpx
}
.coupon {
margin-top: 30rpx;
.coupon-name {
display: flex;
font-size: 36rpx;
color: #D95554;
flex: 1;
}
image {
width: 13rpx;
height: 24rpx;
}
.expired-time {
font-size: 28rpx;
color: #333333;
margin-top: 2rpx;
}
}
}
</style>

View File

@@ -1,12 +1,12 @@
<template>
<web-view class="h-full" :src="url" />
<web-view class='h-full' :src='url' />
</template>
<script setup lang="ts">
<script setup lang='ts'>
const url = ref<string>('');
onLoad((params: any) => {
if (params.url)
url.value = params.url;
if(params.url)
url.value = decodeURIComponent(params.url);
});
</script>

View File

@@ -1,18 +1,364 @@
<template>
<view class='flex flex-col items-center justify-center'>
<swiper class='swiper'>
<swiper-item v-for='(item, index) in bannerList' :key='index'>
<image :src='item' mode='aspectFill' />
<view class='u-flex-column'>
<swiper class='swiper' :indicator-dots='true' :autoplay='true' :interval='3000' :duration='1000'
@change='swiperChange'>
<swiper-item v-for='(item, index) in companyConfigInfo?.bannerinx' :key='index'>
<image :src='item.src||defaultImage' mode='aspectFill' />
</swiper-item>
</swiper>
<view class='basic-view'>
<view class='indicator-view'>
<view v-for='(item,index) in companyConfigInfo?.bannerinx' :key='item' class='normal'
:class="{'active': index === currentBannerIndex}">
</view>
</view>
<view class='user-info-card' @click.stop='goPath("/pages/mine/index")'>
<image class='user-avatar' :src='userInfo?.image||defaultAvatar' mode='aspectFill' />
<text class='user-name primary-text-color'>{{ userInfo?.nickName || '点击注册会员' }}</text>
<view class='integral-view primary-text-color' @click.stop='goPath("/pages/mine/subs/integral/index")'>
<text>{{ userInfo?.integration || 0 }}</text>
<text>积分</text>
</view>
<view class='divider' style='height: 83rpx' />
<view class='balance-view' @click.stop='goPath("/pages/mine/subs/recharge/index")'>
<text class='accent-text-color'>{{ userInfo?.balance || 0 }}</text>
<text>余额()</text>
</view>
<view class='divider' style='height: 83rpx' />
<view class='switch-view' @click.stop='switchCompany'>
<image :src='assetsUrl("ic_switch_company.png")' />
</view>
</view>
<view class='menu-view'>
<view @click.stop='goPath("/pages/mine/subs/recharge/index")'>
<image :src='assetsUrl("ic_member_card2.png")' style='width: 108rpx;height: 72rpx;padding: 11rpx 0' />
<text>会员充值</text>
</view>
<view class='divider' style='margin: 0;height: 153rpx' />
<view @click.stop='goPath("/pages/mine/subs/coupon/index")'>
<image :src='assetsUrl("ic_coupon2.png")' style='width: 108rpx;height: 95rpx' />
<text>优惠券</text>
</view>
</view>
<view class='second-menu-view'>
<view v-for='(item, index) in submenuList' :key='index'>
<view class='menu-item' @click='goPath(item.path)'>
<image :src='item.icon' />
<text>{{ item.title }}</text>
</view>
</view>
</view>
<text class='hot-recommend'>热门推荐</text>
<view class='bottom-banner-view'>
<swiper :indicator-dots='true' :autoplay='true' :interval='3000' :duration='1000'>
<swiper-item v-for='(item, index) in companyConfigInfo?.bannerhot' :key='index'>
<image :src='item.src' mode='aspectFill' />
</swiper-item>
</swiper>
</view>
</view>
</view>
<CompanyDialog ref='companyDialogRef' :companyList='companyList' />
</template>
<script setup lang='ts'>
import CompanyDialog from '@/components/company-dialog.vue';
import { getCompanyList } from '@/api/company';
import { useUserStore } from '@/store';
import { assetsUrl, defaultAvatar, defaultImage } from '@/utils/assets';
import { getCompanyId, goPath, isLogin } from '@/utils';
import { storeToRefs } from 'pinia';
import { UserBean } from '@/store/modules/user/types';
const store = useUserStore();
const companyDialogRef = ref();
const userStore = useUserStore();
const { userInfo, companyConfigInfo } = storeToRefs(userStore);
const currentBannerIndex = ref(0);
const bannerList = ref<string[]>(['1', '2', '3', '4']);
console.log('store.user_name', store.user_name);
const submenuList = [
{
title: '注册有礼',
icon: assetsUrl('ic_register_gift2.png'),
path: '/pages/common/register/index'
},
{
title: '团购秒杀',
icon: assetsUrl('ic_groupbuy2.png'),
path: '/pages/common/groupbuy/index'
},
{
title: '入群有礼',
icon: assetsUrl('ic_group_gift2.png'),
path: '/pages/home/subs/group/join'
}
];
const companyList = ref([]);
const userList = ref<UserBean[]>([]);
onMounted(() => {
wx.getPrivacySetting({
success: res => {
console.log(res); // 返回结果为: res = { needAuthorization: true/false, privacyContractName: '《xxx隐私保护指引》' }
if(res.needAuthorization) {
// 需要弹出隐私协议
// agreementPopupRef.value.show();
} else {
// 用户已经同意过隐私协议,所以不需要再弹出隐私协议,也能调用隐私接口
}
},
fail: (res) => {
console.log('fail ', res);
},
complete: () => {
}
});
});
onLoad((e) => {
console.log('home load e ', e);
});
onShow(async () => {
if(isLogin()) {
// userStore.getProfile();
if(userInfo.value.maOpenId) {
fetchCompanyList();
} else {
await userStore.getProfile();
}
} else {
// setToken('42ae187265fb4688804fd294cbcf99f1')
}
});
const switchCompany = () => {
fetchCompanyList((companyList: any[], userList: any[]) => {
companyDialogRef.value.show((index: number) => {
userStore.setUserInfo(userList[index]);
userStore.setCompanyInfo(companyList[index]);
});
});
};
const fetchCompanyList = (fn: any = undefined) => {
const maOpenId = userInfo.value?.maOpenId || (userList.value.filter((res: UserBean) => res?.maOpenId !== '')[0]?.maOpenId);
getCompanyList(maOpenId).then(res => {
companyList.value = res.map((res: { company: any }) => res.company);
userList.value = res.map((res: { user: any }) => res.user);
if(fn != undefined && fn instanceof Function) {
fn(companyList.value, userList.value);
} else {
let index = companyList.value.findIndex((res: { id: string }) => res.id === getCompanyId());
if(index < 0) {
index = 0;
}
userStore.setUserInfo(userList.value[index]);
userStore.setCompanyInfo(companyList.value[index]);
}
// uni.showActionSheet({
// itemList: companyList.map((res: { companyName: string }) => res.companyName),
// success: (res) => {
// userStore.setUserInfo(userList[res.tapIndex]);
// userStore.setCompanyInfo(companyList[res.tapIndex]);
// }
// });
});
};
const swiperChange = (e: any) => {
currentBannerIndex.value = e.detail.current;
};
</script>
<style lang='scss'>
.swiper {
display: flex;
position: relative;
width: 100%;
height: 520rpx;
image {
width: 100%;
height: 520rpx;
}
}
.basic-view {
display: flex;
flex-direction: column;
position: relative;
top: -100rpx;
}
.indicator-view {
display: flex;
flex-direction: row;
position: absolute;
justify-content: center;
left: 0;
right: 0;
top: -20rpx;
.normal {
width: 16rpx;
height: 16rpx;
background: #707070;
border-radius: 50%;
margin: 0 8rpx;
}
.active {
@extend .normal;
background: #333333;
}
}
.card {
background: #FFFFFF;
border-radius: 20rpx 20rpx 20rpx 20rpx;
margin: 0 24rpx;
}
.user-info-card {
@extend .card;
display: flex;
flex-direction: row;
padding: 30rpx;
align-items: center;
margin-top: 14rpx;
.user-avatar {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
}
.user-name {
display: flex;
flex: 1;
font-size: 30rpx;
font-weight: bold;
margin-left: 16rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 20rpx;
}
.integral-view {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text:nth-child(1) {
font-size: 36rpx;
font-weight: bold;
}
text:nth-child(2) {
font-size: 26rpx;
font-weight: 400;
margin-top: 10rpx;
}
}
.balance-view {
@extend .integral-view;
text:nth-child(1) {
font-size: 36rpx;
font-weight: bold;
}
}
.switch-view {
image {
width: 50rpx;
height: 50rpx;
}
}
}
.divider {
margin: 0 30rpx;
width: 0.5rpx;
height: 100%;
background: #C7C7C7;
}
.menu-view {
@extend .card;
display: flex;
flex-direction: row;
position: relative;
justify-content: center;
align-items: center;
padding: 40rpx 0 32rpx 0;
margin-top: 20rpx;
view:not(:nth-of-type(2)):nth-of-type(n+1) {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 1;
}
text {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-top: 16rpx;
}
}
.second-menu-view {
@extend .menu-view;
image {
width: 88rpx;
height: 88rpx;
}
text {
font-size: 28rpx;
color: #333333;
margin-top: 23rpx;
}
}
.hot-recommend {
margin-top: 30rpx;
margin-left: 24rpx;
font-size: 36rpx;
font-weight: bold;
color: #333333;
}
.bottom-banner-view {
display: flex;
flex-direction: column;
margin: 20rpx 24rpx 0 24rpx;
border-radius: 20rpx;
height: 285rpx;
swiper {
width: 100%;
height: 100%;
image {
width: 100%;
height: 100%;
border-radius: 20rpx;
}
}
}
</style>

View File

@@ -0,0 +1,61 @@
<template>
<view class='content card-view'>
<text class='title'>入群步骤</text>
<view class='indicator' />
<text class='step-title'>
1.长按识别下方二维码添加店铺福利群
</text>
<view class='divider' style='margin-top: 28rpx' />
<image :src=' companyConfigInfo.groupqrcode||defaultImage' show-menu-by-longpress />
<text class='step-title'>
2.识别店铺福利群发您的二维码即可入群
</text>
</view>
</template>
<script lang='ts' setup>
import { useUserStore } from '@/store';
import { defaultImage } from '@/utils/assets';
const userStore = useUserStore();
const { companyConfigInfo } = storeToRefs(userStore);
</script>
<style lang='scss' scoped>
.content {
display: flex;
margin: 20rpx;
padding: 37rpx 20rpx 30rpx 20rpx;
justify-content: center;
align-items: center;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
}
.indicator {
width: 72rpx;
height: 10rpx;
background: #333333;
border-radius: 50rpx;
margin-top: 10rpx;
}
.step-title {
font-size: 30rpx;
font-weight: 400;
color: #333333;
margin-top: 55rpx;
}
image {
width: 432rpx;
height: 432rpx;
margin-top: 67rpx;
}
</style>

15
src/pages/index/index.vue Normal file
View File

@@ -0,0 +1,15 @@
<template>
</template>
<script lang='ts' setup>
onMounted(() => {
uni.reLaunch({
url: '/pages/home/index'
});
});
</script>
<style lang='scss' scoped>
</style>

View File

@@ -1,25 +1,341 @@
<template>
<view class="flex flex-col items-center justify-center">
<image
class="mb-50rpx mt-200rpx h-200rpx w-200rpx"
src="@/static/images/logo.png"
width="200rpx"
height="200rpx"
/>
<view class="flex justify-center">
<text class="font-size-36rpx color-gray-700">
{{ title }}
</text>
<view class='content' v-if='companyConfigInfo.mallopen==1'>
<view class='search-view'>
<view class='search-input' @click.stop='goPath("/pages/mall/subs/search/index")'>
<image :src=' assetsUrl("ic_search.png")'></image>
<input placeholder='输入名称、款号搜索' :disabled='true' />
</view>
</view>
<view class='container'>
<scroll-view class='category-list' :scroll-y='true'>
<view v-for='(item,index) in categoryList' :key='index' class='category-item'
@click.stop='changeCategory(index)'
:class="{'category-item-active': index === categorySelectedIndex}">
<text>{{ item.typeName }}</text>
</view>
</scroll-view>
<scroll-view class='goods-list' :scroll-y='true' type='custom' @scrolltolower='loadMore'>
<grid-view type='masonry' :cross-axis-count='2'>
<view v-for='(item, index) in goodsList' :key='index' class='goods-item'
@click.stop='goPath(`/pages/mall/subs/goods/detail?goodsId=${item.goodsId}`)'>
<image class='goods-image' :src='item.images||defaultImage' />
<text class='goods-name'>{{ item.goodsName || '未知' }}</text>
<text class='goods-price'>¥{{ (item.price * userInfo.userDiscount).toFixed(2) }}
<text v-if='userInfo.userDiscount>0&&userInfo.userDiscount<1'
style='text-decoration: line-through;color: #999999;font-size: 25rpx'>
¥{{ item.price }}
</text>
</text>
<image class='add-image' :src='assetsUrl("ic_add_goods.png")' @click.stop='addShoppingCart(item)' />
</view>
</grid-view>
<u-empty v-if='goodsList.length === 0' text='暂无商品数据' />
</scroll-view>
</view>
<view class='shopping-cart' @click.stop='goPath("/pages/mall/subs/shoppingcart/index")'>
<image :src='assetsUrl("ic_shopping_cart.png")' />
<text v-if='totalCount>0'>{{ totalCount }}</text>
</view>
</view>
<view class='content' v-else style='align-items: center;justify-content:center;'>
<image :src='assetsUrl("bg_mall_close.png")' style='width: 200rpx; height: 200rpx;' />
</view>
<sku-dialog ref='skuDialogRef' />
</template>
<script setup lang="ts">
<script setup lang='ts'>
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app';
import SkuDialog from '@/components/sku-dialog.vue';
import { assetsUrl, defaultImage } from '@/utils/assets';
import { goLogin, goPath, isLogin, showToast } from '@/utils';
import { getCategoryList, getGoodsList } from '@/api/goods';
import { CategoryBean, GoodsBean } from '@/api/goods/types';
import useShoppingCartStore from '@/store/modules/shoppingcart';
import { useUserStore } from '@/store';
const title = ref<string>();
title.value = import.meta.env.VITE_APP_TITLE;
const userStore = useUserStore();
const { userInfo, companyConfigInfo } = storeToRefs(userStore);
const shoppingCartStore = useShoppingCartStore();
const { totalCount } = storeToRefs(shoppingCartStore);
const store = useUserStore();
console.log('store.user_name', store.user_name);
const skuDialogRef = ref();
const categoryList = ref<CategoryBean[]>([]);
const categorySelectedIndex = ref<number>(0);
const goodsList = ref<GoodsBean[]>([]);
const currentPageNum = ref<number>(1);
onLoad(() => {
if(!isLogin()) {
goLogin();
return;
}
userStore.$onAction(({ name, after }) => {
after(() => {
//更新用户信息
if(name === 'setUserInfo') {
fetchCategoryList();
shoppingCartStore.resetData();
}
});
});
fetchCategoryList();
});
onShareAppMessage((e) => {
return {
title: 'VIP顾客中心',
path: '/pages/mall/index'
};
});
onShareTimeline((e) => {
return {
title: 'VIP顾客中心',
path: '/pages/mall/index'
};
});
const fetchCategoryList = async () => {
if(companyConfigInfo.value.mallopen == 1) {
const list = await getCategoryList();
list.unshift({ typeName: '上新精选', typeId: '2' });
list.unshift({ typeName: '热销商品', typeId: '1' });
list.unshift({ typeName: '全部分类', typeId: '0' });
categoryList.value = list;
await changeCategory(0);
}
};
const changeCategory = async (index: number) => {
categorySelectedIndex.value = index;
await fetchGoodsList();
};
const fetchGoodsList = async (refresh: boolean = true) => {
currentPageNum.value = refresh ? 1 : currentPageNum.value + 1;
let typeId = [categoryList.value[categorySelectedIndex.value].typeId];
let sort = undefined;
if(typeId[0] === '0') {
typeId = [];
sort = undefined;
} else if(typeId[0] === '1') {
typeId = [];
sort = 'good_num DESC';
} else if(typeId[0] === '2') {
typeId = [];
sort = 'update_time DESC';
}
const { rows } = await getGoodsList({
page: {
pageNum: currentPageNum.value,
pageSize: 30,
bean: {
keyword: '',
typeIds: typeId,
sort: sort
}
}
});
if(refresh) {
goodsList.value = rows;
} else {
goodsList.value = goodsList.value.concat(rows);
}
goodsList.value.forEach(e => {
e.price = userStore.getRealGoodsPrice(e.price, e.priceExt);
});
};
const loadMore = () => {
fetchGoodsList(false);
};
const randomImageHeight = () => {
const height = getRandomFloatInRange(1, 2) * 200 + 'rpx';
console.log('--------->>>', height);
return height;
};
const getRandomFloatInRange = (a: number, b: number) => {
return Math.random() * (b - a) + a;
};
const addShoppingCart = (goodsBean: GoodsBean) => {
skuDialogRef.value.show(goodsBean.goodsId, (e: GoodsBean) => {
//重新选择sku后判断当前购物中是否存在相同sku的商品
const existsIndex = shoppingCartStore.getSameGoodsIndex(e?.id || '', e.checkedStock.colorId, e.checkedStock.sizeId);
//不存在则更新当前商品sku
if(existsIndex < 0) {
shoppingCartStore.save(e);
}
//如果已存在,则更新已存在商品数量,并删除当前商品
else {
shoppingCartStore.updateCount(existsIndex, e.checkedStock.count);
}
showToast('添加购物车成功');
});
};
</script>
<style lang='scss' scoped>
.content {
display: flex;
flex-direction: column;
height: 100vh;
}
.search-view {
display: flex;
flex-direction: row;
background: white;
padding: 19rpx 30rpx;
.search-input {
display: flex;
flex-direction: row;
align-items: center;
background: #F7F7F7;
padding: 17rpx 23rpx;
font-size: 26rpx;
width: 100%;
image {
width: 32rpx;
height: 32rpx;
margin-right: 20rpx;
}
}
}
.container {
display: flex;
flex-direction: row;
margin-top: 20rpx;
.category-list {
display: flex;
flex-direction: column;
width: 280rpx;
height: 100vh;
.category-item {
display: flex;
width: 100%;
height: 92rpx;
overflow: hidden;
align-items: center;
justify-content: center;
white-space: nowrap;
text {
width: 60%;
text-align: center;
font-size: 30rpx;
text-overflow: ellipsis;
overflow: hidden;
}
}
.category-item-active {
@extend .category-item;
background: #FFFFFF;
}
.category-item-active:before {
content: '';
position: absolute;
width: 5rpx;
left: 15rpx;
height: 30rpx;
background: #333333;
color: #333333;
}
}
.goods-list {
display: flex;
background: white;
padding: 38rpx 20rpx 0 20rpx;
height: 100vh;
.goods-item {
display: flex;
flex-direction: column;
position: relative;
margin-bottom: 10rpx;
padding: 10rpx 10rpx;
.goods-image {
border-radius: 10rpx;
width: 100%;
max-height: 300rpx;
}
.goods-name {
font-size: 28rpx;
font-weight: 400;
color: #333333;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 10rpx;
}
.goods-price {
font-size: 36rpx;
font-weight: bold;
color: #F32B2B;
}
.add-image {
width: 42rpx;
height: 42rpx;
position: absolute;
bottom: 10rpx;
right: 10rpx;
}
}
}
}
.shopping-cart {
display: flex;
position: fixed;
bottom: 30rpx;
right: 40rpx;
width: 90rpx;
height: 90rpx;
background: #FFFFFF;
border-radius: 50%;
align-items: center;
justify-content: center;
box-shadow: 0 0 20rpx 1rpx rgba(78, 78, 78, 0.16);
image {
width: 40rpx;
height: 40rpx;
}
text {
display: flex;
min-width: 39rpx;
min-height: 39rpx;
background: #F32B2B;
align-items: center;
justify-content: center;
top: -5rpx;
right: -5rpx;
color: white;
font-size: 26rpx;
position: absolute;
border-radius: 50%;
border: 2rpx solid #FFFFFF;
}
}
</style>

View File

@@ -0,0 +1,114 @@
<template>
<uni-popup type='bottom' ref='popupRef' :is-mask-click='false'>
<view class='popup-content c-flex-column'>
<view class='c-flex-row'>
<text>优惠券</text>
<text @click.stop='confirm'>确定</text>
</view>
<scroll-view scroll-y>
<coupon-item v-if='coupons.length>0' v-for='(item,index) in coupons' :item='item' :key='index'
@confirm='confirmItem(index)' />
<u-loadmore v-else status='loading' />
</scroll-view>
</view>
</uni-popup>
</template>
<script lang='ts' setup>
import CouponItem from '../components/coupon-item.vue';
import { getCompanyId } from '@/utils';
import { getCouponList } from '@/api/user';
import { CouponBean } from '@/api/user/types';
import { useUserStore } from '@/store';
const popupRef = ref();
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const props = defineProps({
orderPrice: {
type: Number,
default: 0
}
});
const emits = defineEmits(['confirm']);
const coupons = ref<CouponBean[]>([]);
const fetchData = async (status: number) => {
const { list } = await getCouponList({
companyId: getCompanyId(),
memberId: userInfo.value?.id,
status: status,
pageNum: 1,
pageSize: 100
});
list.filter((item: CouponBean) => item.status = (props.orderPrice >= item.threshold) ? 0 : 1);
coupons.value = list;
};
const show = () => {
popupRef.value.open();
fetchData(0);
};
const confirmItem = (index: number) => {
coupons.value.forEach(item => {
item.checked = false;
});
coupons.value[index].checked = !coupons.value[index].checked;
};
const confirm = () => {
close();
emits('confirm', coupons.value.filter(item => item.checked)[0]);
};
const close = () => {
popupRef.value.close();
};
defineExpose({
show
});
</script>
<style lang='scss' scoped>
.popup-content {
background: #FFFFFF;
border-radius: 20rpx 20rpx 0 0;
padding: 38rpx 45rpx 10rpx 45rpx;
view:nth-of-type(1) {
display: flex;
margin-bottom: 70rpx;
position: relative;
text:nth-of-type(1) {
font-weight: bold;
font-size: 30rpx;
color: #333333;
position: absolute;
left: 0;
right: 0;
text-align: center;
}
text:nth-of-type(2) {
font-weight: bold;
font-size: 30rpx;
color: #F32B2B;
position: absolute;
right: 0;
}
}
scroll-view {
height: 600rpx;
max-height: 600rpx;
}
}
</style>

View File

@@ -0,0 +1,118 @@
<template>
<view class='coupon-content' @click.stop='()=>{emits("confirm")}'>
<image v-if='item?.status==0' :src='assetsUrl("bg_coupon.png")' />
<image v-else-if='item?.status==1' :src='assetsUrl("bg_coupon_expired.png")' />
<view class='left-content accent-text-color' :class='{"left-content-disabled": item?.status==1}'>
<text>{{ item?.reduce }}</text>
<text>满{{ item?.threshold }}元可用</text>
</view>
<view class='right-content accent-text-color' :class='{"right-content-disabled": item?.status==1}'>
<text>{{ item?.name }}</text>
<text>有效期至{{ dayjs(item?.startTime).format('YYYY-MM-DD') }}</text>
<image v-if='item?.status==0' class='checkbox'
:src='assetsUrl(item.checked?"ic_checkbox_active.png":"ic_checkbox_normal.png")'
/>
</view>
</view>
</template>
<script lang='ts' setup>
import { PropType } from 'vue';
import { assetsUrl } from '@/utils/assets';
import { CouponBean } from '@/api/user/types';
import dayjs from 'dayjs';
defineProps({
item: Object as PropType<CouponBean>
});
const emits = defineEmits(['confirm']);
</script>
<style lang='scss' scoped>
.coupon-content {
display: flex;
flex-direction: row;
height: 200rpx;
position: relative;
margin: 10rpx 0 10rpx 0;
image {
width: 100%;
height: 100%;
position: absolute;
}
.left-content {
display: flex;
flex-direction: column;
position: relative;
align-items: center;
justify-content: center;
padding-left: 50rpx;
text:nth-of-type(1) {
font-size: 55rpx;
font-weight: bold;
}
text:nth-of-type(1):after {
content: '';
font-size: 24rpx;
}
text:nth-of-type(2) {
font-size: 26rpx;
font-weight: 400;
}
}
.left-content-disabled {
@extend .left-content;
color: #C2C2C2;
}
.right-content {
display: flex;
flex-direction: column;
position: relative;
flex: 1;
justify-content: center;
margin-left: 60rpx;
text:nth-of-type(1) {
font-size: 30rpx;
}
text:nth-of-type(2) {
font-size: 26rpx;
margin-top: 20rpx;
color: #333333;
}
.checkbox {
display: flex;
width: 42rpx;
height: 42rpx;
align-items: center;
justify-content: center;
font-weight: bold;
position: absolute;
right: 36rpx;
}
}
.right-content-disabled {
@extend .right-content;
color: #C2C2C2;
text:nth-of-type(2) {
color: #C2C2C2;
}
}
}
</style>

View File

@@ -0,0 +1,482 @@
<template>
<view class='content'>
<view class='swiper-container'>
<swiper class='swiper' :interval='1500' :duration='1000' @change='swiperChange'>
<swiper-item v-for='(item,index) in [goodsBean?.images]' :key='index'>
<image :src='item||defaultImage' mode='aspectFill' />
</swiper-item>
</swiper>
<view class='indicator' style='display: none'>
<text>{{ swiperIndex + 1 }}</text>
<text>/{{ bannerList.length }}</text>
</view>
</view>
<view class='goods-info-view c-flex-column' style='margin-right: 10rpx'>
<view class='c-flex-row'>
<text class='goods-price accent-text-color'>
¥{{ ((goodsBean?.price || 0) * userInfo.userDiscount).toFixed(2) }}
<text v-if='userInfo.userDiscount>0&&userInfo.userDiscount<1'
style='display:unset;text-decoration: line-through;font-size: 30rpx;color: #999999'>
¥{{ goodsBean?.price || 0 }}
</text>
</text>
<text>销量{{ goodsBean?.salesCount || 0 }}</text>
</view>
<view class='c-flex-row'>
<text style='flex: 1'>{{ goodsBean?.name || '未知' }}</text>
<view class='share-button'>
<image :src='assetsUrl("ic_share.png")'></image>
<button class='btn' plain open-type='share'>分享</button>
</view>
</view>
</view>
<view class='goods-sku-view c-flex-column'>
<view class='c-flex-row' @click.stop='showSkuDialog'>
<text>选择</text>
<text>规格 颜色/尺码</text>
<image :src='assetsUrl("ic_arrow_right_gray.png")' />
<text>{{ getStockColorCount }}种颜色可选</text>
</view>
<view class='divider' style='width:auto;height: 0.5rpx;margin-left: 100rpx;display: none' />
<view class='c-flex-row' style='display: none' @click.stop='showSkuDialog'>
<text>参数</text>
<text>品牌 风格 季节 款号</text>
<image :src='assetsUrl("ic_arrow_right_gray.png")' />
</view>
</view>
<view class='recommend-view c-flex-column' v-if='(recommendList?.length||0)>0'>
<text>浏览此商品的客户还浏览了</text>
<scroll-view scroll-x>
<view style='display: inline-block' v-for='(item, index) in recommendList'
:key='index'>
<view class='recommend-item c-flex-column'>
<image :src='item.images||defaultImage' />
<text>{{ item.goodsName || '未知' }}</text>
<text class='goods-price'>{{ (item.price * userInfo.userDiscount).toFixed(2) }}
<text v-if='userInfo.userDiscount>0&&userInfo.userDiscount<1'
style='text-decoration: line-through;font-size: 25rpx;color: #999999'>{{ item.price }}
</text>
</text>
</view>
</view>
</scroll-view>
</view>
<view class='goods-detail c-flex-column' style='display: none'>
<text>商品详情</text>
<image :src='assetsUrl("test_bg.png")' mode='aspectFill' />
<image :src='assetsUrl("test_bg.png")' mode='aspectFill' />
<image :src='assetsUrl("test_bg.png")' mode='aspectFill' />
</view>
<view class='bottom-view c-flex-row'>
<view class='small-button-view' @click.stop='goBack'>
<view class='small-button-item'>
<image :src='assetsUrl("ic_goods_store.png")' />
<text>商家</text>
</view>
<view class='small-button-item' @click.stop='goPath("/pages/mine/subs/order/index")'>
<image :src='assetsUrl("ic_goods_order.png")' />
<text>订单</text>
</view>
<view class='small-button-item'>
<view class='shoppingcart-count' @click.stop='goPath("/pages/mall/subs/shoppingcart/index")'>
<image :src='assetsUrl("ic_goods_shoppingcart.png")' />
<text v-if='totalCount>0'>{{ totalCount }}</text>
</view>
<text>购物车</text>
</view>
</view>
<view class='primary-button-view c-flex-row'>
<view class='add-shoppingcart-button' @click.stop='addShoppingCart'>加入购物车</view>
<view class='place-order-button' @click.stop='placeOrder'>立即下单</view>
</view>
</view>
</view>
<sku-dialog ref='skuDialogRef' :bean='goodsBean' />
</template>
<script lang='ts' setup>
import { assetsUrl, defaultImage } from '@/utils/assets';
import SkuDialog from '@/components/sku-dialog.vue';
import { goPath, showToast } from '@/utils';
import { getGoodsDetail, getGoodsList } from '@/api/goods';
import { GoodsBean } from '@/api/goods/types';
import useShoppingCartStore from '@/store/modules/shoppingcart';
import { useUserStore } from '@/store';
const shoppingCartStore = useShoppingCartStore();
const { totalCount } = storeToRefs(shoppingCartStore);
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const goodsBean = ref<GoodsBean>();
const recommendList = ref<GoodsBean[]>();
const skuDialogRef = ref();
const bannerList = ref([]);
const swiperIndex = ref(0);
onLoad(async (e: any) => {
await uni.showLoading();
goodsBean.value = await getGoodsDetail(e.goodsId);
goodsBean.value.price = userStore.getRealGoodsPrice(goodsBean.value?.price, goodsBean.value?.priceExt);
const { rows } = await getGoodsList({
page: {
page: 1,
pageSize: 20,
bean: {
keyword: '',
typeIds: []
}
}
});
recommendList.value = rows;
recommendList.value?.forEach(e => {
e.price = userStore.getRealGoodsPrice(e.price, e.priceExt);
});
uni.hideLoading();
});
const getStockColorCount = computed(() => {
const list = Array.from(new Set(goodsBean.value?.stocks?.map(item => item.colorName)))
.map(colorName => goodsBean.value?.stocks?.find(item => item.colorName === colorName)!);
return list.length;
});
const swiperChange = (e: any) => {
swiperIndex.value = e.detail.current;
};
const goBack = () => {
uni.navigateBack();
};
const showSkuDialog = (fn: Function) => {
skuDialogRef.value.show(goodsBean.value?.id, fn);
};
const addShoppingCart = () => {
showSkuDialog((e: GoodsBean) => {
const index = shoppingCartStore.getSameGoodsIndex(goodsBean.value?.id || '', e.checkedStock?.colorId, e.checkedStock?.sizeId);
if(index >= 0) {
shoppingCartStore.updateCount(index, e.checkedStock?.count);
} else {
shoppingCartStore.save(e);
}
showToast('加入购物车成功');
});
};
const placeOrder = () => {
showSkuDialog((e: GoodsBean) => {
const orderBean = {
totalPrice: (e.consumePrice * e.checkedStock.count).toFixed(2),
orderGoods: [e]
};
goPath(`/pages/mall/subs/order/order-confirm?orderBean=${encodeURIComponent(JSON.stringify(orderBean))}`);
});
};
</script>
<style lang='scss' scoped>
.content {
padding-bottom: 200rpx;
}
.swiper-container {
position: relative;
.swiper {
display: flex;
width: 100%;
height: 750rpx;
image {
width: 100%;
height: 750rpx;
}
}
.indicator {
background: rgba(1, 1, 1, 0.5);
border-radius: 45rpx;
font-weight: 400;
font-size: 24rpx;
color: #FFFFFF;
position: absolute;
right: 20rpx;
bottom: 20rpx;
padding: 5rpx 25rpx;
text:nth-of-type(1) {
font-size: 30rpx;
}
text:nth-of-type(2) {
font-size: 24rpx;
}
}
}
.goods-info-view {
background: #FFFFFF;
padding: 20rpx 30rpx;
view:nth-of-type(1) {
display: flex;
flex-direction: row;
text:nth-of-type(1) {
display: flex;
font-size: 40rpx;
text-align: center;
align-items: flex-end;
flex: 1;
}
.goods-price:before {
//content: "¥";
//font-size: 30rpx;
//margin-bottom: 5rpx;
//margin-right: 5rpx;
}
text:nth-of-type(2) {
font-size: 26rpx;
font-weight: 400;
color: #999999;
}
}
view:nth-of-type(2) {
text {
font-weight: bold;
font-size: 32rpx;
color: #333333;
margin-right: 10rpx;
}
.share-button {
display: flex;
flex-direction: column;
align-items: center;
image {
width: 36rpx;
height: 32rpx;
}
.btn {
background: #00000000;
font-size: 24rpx;
color: #636566;
white-space: nowrap;
padding: 0;
border: none !important;
}
}
}
}
.goods-sku-view {
background: #FFFFFF;
padding: 20rpx 30rpx;
margin-top: 20rpx;
view:nth-of-type(n+1) {
position: relative;
align-items: center;
height: 85rpx;
text:nth-of-type(1) {
width: 100rpx;
font-weight: 400;
font-size: 28rpx;
color: #999999;
}
text:nth-of-type(2) {
font-weight: 400;
font-size: 28rpx;
color: #333333;
flex: 1;
}
image {
width: 13rpx;
height: 24rpx;
}
text:nth-of-type(3) {
font-weight: 400;
font-size: 24rpx;
color: #999999;
position: absolute;
top: 50rpx;
left: 100rpx;
}
}
view:nth-of-type(1) {
align-items: flex-start;
//padding-top: 20rpx;
}
}
.recommend-view {
background: #FFFFFF;
padding: 20rpx 30rpx;
margin-top: 20rpx;
text {
font-weight: bold;
font-size: 28rpx;
color: #333333;
}
scroll-view {
width: 100%;
margin-top: 20rpx;
white-space: nowrap;
height: 300rpx;
}
.recommend-item {
margin-right: 20rpx;
image {
width: 200rpx;
height: 200rpx;
border-radius: 10rpx;
}
text:nth-of-type(1) {
font-weight: bold;
font-size: 28rpx;
color: #333333;
margin-top: 20rpx;
}
text:nth-of-type(2) {
font-weight: bold;
font-size: 28rpx;
color: #F32B2B;
}
.goods-price:before {
content: "¥";
font-size: 28rpx;
}
}
}
.bottom-view {
background: #FFFFFF;
padding: 20rpx 30rpx 78rpx 33rpx;
position: fixed;
left: 0;
right: 0;
bottom: 0;
.small-button-view {
display: flex;
flex-direction: row;
flex: 1;
justify-content: space-between;
margin-right: 35rpx;
.small-button-item {
display: flex;
flex-direction: column;
align-items: center;
image {
width: 34rpx;
height: 34rpx;
margin-bottom: 10rpx;
}
text {
font-weight: 400;
font-size: 20rpx;
color: #333333;
}
.shoppingcart-count {
display: flex;
position: relative;
text {
display: flex;
box-sizing: border-box;
align-items: center;
justify-content: center;
position: absolute;
top: -15rpx;
right: -20rpx;
min-width: 35rpx;
min-height: 30rpx;
background: #F32B2B;
color: #FFFFFF;
padding: 2rpx;
border-radius: 50%;
border: 2rpx solid #FFFFFF;
}
}
}
}
.primary-button-view {
font-weight: bold;
font-size: 30rpx;
.add-shoppingcart-button {
display: flex;
width: 224rpx;
height: 80rpx;
align-items: center;
justify-content: center;
background: #FFE2E2;
color: #F32B2B;
border-radius: 40rpx 0 0 40rpx;
}
.place-order-button {
display: flex;
width: 198rpx;
height: 80rpx;
align-items: center;
justify-content: center;
background: #F32B2B;
color: #FFFFFF;
border-radius: 0 40rpx 40rpx 0;
}
}
}
.goods-detail {
background: #FFFFFF;
margin-top: 20rpx;
text {
margin: 20rpx 30rpx;
font-weight: bold;
font-size: 30rpx;
color: #333333;
}
image {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,546 @@
<template>
<scroll-view scroll-y>
<view class='content'>
<view class='c-flex-row'>
<view class='tab c-flex-row' :class='{"tab-active": tabIndex === 0}' @click.stop='tabIndex=0'>
<image :src='assetsUrl("ic_order_dd.png")' />
<text>到店</text>
</view>
<view class='tab c-flex-row' :class='{"tab-active": tabIndex === 1}' @click.stop='tabIndex=1'>
<image :src='assetsUrl("ic_order_yj.png")' />
<text>邮寄</text>
</view>
</view>
<view v-show='tabIndex==1' class='address-view c-flex-column'
@click.stop='goPath("/pages/mine/subs/address/index")'>
<view v-if='deliveryAddress?.addrid'>
<view class='user-info c-flex-row'>
<text class='status' v-if='deliveryAddress?.defaultstatus==1'>默认</text>
<text>{{ deliveryAddress?.name }}</text>
<text>{{ deliveryAddress?.mobile }}</text>
<view style='flex: 1' />
<image :src='assetsUrl("ic_arrow_right_gray.png")' />
</view>
<view class='addr c-flex-row'>
<image :src='assetsUrl("ic_location.png")' />
<text>{{ deliveryAddress?.addr }}</text>
</view>
</view>
<view v-else class='user-info c-flex-row'>
<text style='flex: 1'>请选择收货地址</text>
<image :src='assetsUrl("ic_arrow_right_gray.png")' />
</view>
<image class='dashed-line' :src='assetsUrl("ic_address_dashed_line.png")' />
</view>
<view class='goods-info-view c-flex-column'>
<view class='c-flex-row' style='margin-bottom: 20rpx' v-for='(item,index) in orderBean?.orderGoods'
:key='index'>
<image class='goods-image' :src='item.images||defaultImage' />
<view class='c-flex-column' style='flex: 1;'>
<view class='c-flex-row'>
<text class='goods-name'>{{ item.name || '未知' }}</text>
</view>
<text style='color: #999999;margin-top: 30rpx'>
{{ item.code }}
</text>
<view class='bottom-sku-view c-flex-row'>
<text>
{{ item.checkedStock.colorName||'均色' }}{{ item.checkedStock.sizeName||'均码' }} x{{ item.checkedStock.count }}
</text>
<text>¥{{ item.price }}</text>
</view>
</view>
</view>
<view class='divider' />
<view class='remark-view c-flex-row'>
<text>备注</text>
<input placeholder='请填写订单备注' @input='orderBean!.remark = $event.detail.value' />
</view>
</view>
<view class='card-view'>
<view class='c-flex-row'>
<text class='card-view-title'>商品总价</text>
<text class='card-view-value'>{{ orderBean?.totalPrice || 0 }}</text>
</view>
<view class='c-flex-row' style='display: none'>
<text class='card-view-title'>运费</text>
<text class='card-view-value'>0</text>
</view>
<view class='c-flex-row' @click.stop='showCouponDialog'>
<text class='card-view-title'>优惠券
<text style='font-size: 22rpx;color: #F32B2B;'>已选最大优惠</text>
</text>
<view class='card-view-value' style='color: #F32B2B;margin: 0'>-{{ checkedCoupon?.reduce || 0 }}
<image :src='assetsUrl("ic_arrow_right_gray.png")' />
</view>
</view>
<view class='divider' />
<view class='c-flex-row'>
<view style='flex: 1' />
<view class='c-flex-row'>
<text class='card-view-value'>共1件商品 合计
<text style='font-size: 34rpx;color:#F32B2B'>¥{{ totalPrice || 0 }}</text>
</text>
</view>
</view>
</view>
<view class='payment-way c-flex-row' @click.stop='changePayment'>
<text>支付方式</text>
<view>{{ paymentType === 0 ? '微信支付' : '余额' }}
<image style='display: none' :src='assetsUrl("ic_arrow_right_gray.png")' />
</view>
</view>
<view class='bottom-view c-flex-row'>
<text>合计</text>
<text>{{ totalPrice || 0 }}</text>
<sqb-pay @navigateTo='navigateTo'
:return_url='buildSqbParams.return_url'
:total_amount='buildSqbParams.total_amount'
:terminal_sn='buildSqbParams.terminal_sn'
:client_sn='buildSqbParams.client_sn'
:subject='buildSqbParams.subject'
:subject_img='buildSqbParams.subject_img '
:merchant_name='buildSqbParams.merchant_name'
:notify_url='buildSqbParams.notify_url'
:sign='buildSqbParams.sign'>
<button class='confirm-order' @click='createOrder'>确认订单</button>
</sqb-pay>
</view>
</view>
</scroll-view>
<payment-dialog ref='paymentDialogRef' @change='args => paymentType=args' />
<coupon-dialog ref='couponDialogRef' @confirm='confirmCoupon' :order-price='orderBean?.totalPrice' />
</template>
<script lang='ts' setup>
import { assetsUrl, defaultImage } from '@/utils/assets';
import PaymentDialog from '@/components/payment-dialog.vue';
import CouponDialog from '@/pages/mall/subs/components/coupon-dialog.vue';
import { goPath, parseParameter, showToast, sortASCII } from '@/utils';
import { hexMD5 } from '@/utils/common/md5';
import { getPaymentList, orderCreate, overPayment } from '@/api/order';
import { CouponBean } from '@/api/user/types';
import { OrderBean } from '@/api/order/types';
import { useUserStore } from '@/store';
import { handlePayResult } from '@/utils/order';
import useShoppingCartStore from '@/store/modules/shoppingcart';
const userStore = useUserStore();
const { userInfo, terminalInfo, deliveryAddress } = storeToRefs(userStore);
const shoppingCartStore = useShoppingCartStore();
const couponDialogRef = ref();
const paymentDialogRef = ref();
const paymentType = ref(0);
const tabIndex = ref(0);
const checkedCoupon = ref<CouponBean>();
const orderBean = ref<OrderBean>();
onLoad((e) => {
orderBean.value = JSON.parse(decodeURIComponent(e?.orderBean));
createOrder();
});
const totalPrice = computed(() => {
return (orderBean?.value?.totalPrice || 0) - (checkedCoupon.value?.reduce || 0);
});
const buildSqbParams = computed(() => {
const params = sortASCII({
client_sn: orderBean.value?.id || '',
return_url: '/pages/common/payresult/index',
total_amount: Number(((totalPrice.value || 0) * 100).toFixed(2)),
terminal_sn: terminalInfo.value.terminalSn,
subject: orderBean?.value?.orderGoods[0].name||'未知',
subject_img: orderBean?.value?.orderGoods[0].images || '',
merchant_name: terminalInfo.value.companyName,
notify_url: 'https://www.baidu.com'
}, true);
return {
...params,
sign: hexMD5(parseParameter(params) + '&key=' + terminalInfo.value.terminalKey).toUpperCase()
};
});
const changePayment = () => {
// paymentDialogRef.value.show();
};
const showCouponDialog = () => {
couponDialogRef.value.show();
};
const confirmCoupon = (item: CouponBean) => {
checkedCoupon.value = item;
createOrder();
};
const navigateTo = (e: any) => {
handlePayResult(orderBean.value?.id, e, {
onSuccess: () => {
console.log('pay success');
payment();
},
onFailure: () => {
console.error('pay onFailure');
if(orderBean.value?.id) {
orderBean.value.id = '';
}
createOrder();
}
});
};
const createOrder = async () => {
//邮寄
if(tabIndex.value == 1 && !deliveryAddress.value.addrid) {
showToast('请选择收货地址');
return;
}
const params = {
// 'discount': 0,
// 'freePrice': 0,
'reducePrice': checkedCoupon.value == undefined ? 0 : checkedCoupon.value?.reduce,
'totalPrice': totalPrice.value,
// 'integral': 0,
// 'allowIntegral': 0,
// 'produceIntegralNumber': 0,
'couponIds': checkedCoupon.value?.id,
'remark': orderBean.value?.remark,
'address': tabIndex.value == 0 ? undefined : JSON.stringify(deliveryAddress.value),
'orderGoods': orderBean?.value?.orderGoods?.map(item => (
{
'goodsId': item.id,
'goodsCode': item.code,
'goodsNum': item.checkedStock.count,
'stockId': item.checkedStock.stockId,
'originPrice': item.price,
// 'consumePrice': item.price * ((userInfo.value?.levelEntity?.discount || 0) / 100),
'consumePrice': item.consumePrice,
'discount': userInfo.value?.levelEntity?.discount || 0
// 'discountOriginPrice': 0,
// 'produceIntegral': 0,
// 'priceModify': [0],
// 'offset': 0
}
))
};
if(orderBean.value?.id == undefined || orderBean.value?.id == '') {
await uni.showLoading();
const result = await orderCreate(params);
orderBean.value!.id = result.id;
uni.hideLoading();
}
//删除购物车已存在商品
orderBean?.value?.orderGoods?.forEach(item => {
const index = shoppingCartStore.getSameGoodsIndex(item.id, item.checkedStock.colorId, item.checkedStock.sizeId);
if(index >= 0) {
shoppingCartStore.delete(index);
}
});
};
const payment = async () => {
await uni.showLoading();
const paymentList = await getPaymentList(orderBean.value?.id || '');
const paymentParams = {
orderId: orderBean?.value?.id,
produceIntegralNumber: Math.round((orderBean?.value?.totalPrice || 0) + (orderBean?.value?.useGold || 0)),
transactionPrice: orderBean?.value?.totalPrice,
useGold: orderBean.value?.useGold || 0,
detailBos: [{
integralNumber: Math.floor(orderBean?.value?.totalPrice || 0),
onlinePayResult: '',
payName: '收钱吧',
payTypeId: paymentList.find((res: any) => res.type === 'ShouQianBa')?.id,
transactionPrice: Number(orderBean?.value?.totalPrice)
}]
};
await overPayment(paymentParams);
showToast('支付成功', {
icon: 'success',
complete: () => {
uni.navigateBack();
uni.hideLoading();
}
});
};
</script>
<style lang='scss' scoped>
.content {
padding: 20rpx 30rpx 200rpx 30rpx;
}
.tab {
height: 63rpx;
background: #E8E8E8;
border-radius: 10rpx 0 10rpx 0;
align-items: center;
justify-content: center;
margin-top: 17rpx;
flex: 1;
text {
font-weight: bold;
font-size: 30rpx;
color: #333333;
margin-left: 10rpx;
}
}
.tab:nth-of-type(1) {
border-radius: 10rpx 0 0 0;
}
.tab:nth-of-type(2) {
border-radius: 0 10rpx 0 0;
}
.tab-active {
background: #FFFFFF;
}
.tab:nth-of-type(1) image {
width: 42rpx;
height: 42rpx;
}
.tab:nth-of-type(2) image {
width: 38rpx;
height: 29rpx;
}
.address-view {
padding-top: 38rpx;
background: #FFFFFF;
.user-info {
margin-left: 20rpx;
margin-right: 20rpx;
.status {
padding: 2rpx 10rpx;
font-size: 24rpx;
color: #F32B2B;
border-radius: 5rpx;
border: 1rpx solid #F32B2B;
margin-right: 12rpx;
}
text:nth-of-type(n+2) {
font-weight: bold;
font-size: 30rpx;
color: #333333;
margin-left: 10rpx;
}
image {
width: 8rpx;
height: 16rpx;
}
}
.addr {
margin-top: 15rpx;
margin-left: 20rpx;
image {
width: 30rpx;
height: 37rpx;
}
text {
font-weight: 400;
font-size: 26rpx;
color: #999999;
margin-left: 16rpx;
}
}
.dashed-line {
width: 100%;
height: 8rpx;
margin-top: 37rpx;
}
}
.goods-info-view {
display: flex;
position: relative;
background: #FFFFFF;
margin-top: 20rpx;
border-radius: 10rpx;
padding: 30rpx 20rpx;
.goods-image {
width: 180rpx;
height: 180rpx;
border-radius: 10rpx;
margin-right: 20rpx;
}
.goods-name {
font-size: 30rpx;
font-weight: 400;
-webkit-line-clamp: 2;
text-overflow: ellipsis;
overflow: hidden;
margin-right: 20rpx;
color: #333333;
flex: 1;
}
.bottom-sku-view {
display: flex;
justify-content: space-between;
margin-top: 10rpx;
text:nth-of-type(1) {
font-size: 26rpx;
font-weight: 400;
flex: 1;
color: #999999;
}
text:nth-of-type(2) {
font-size: 30rpx;
color: #333333;
font-weight: bold;
margin-right: 5rpx;
}
text:nth-of-type(3) {
font-size: 34rpx;
color: #333333;
font-weight: bold;
}
}
.divider {
margin: 30rpx 0;
}
.remark-view {
text {
font-weight: 400;
font-size: 28rpx;
color: #333333;
flex: 1;
}
input {
min-width: 200rpx;
font-weight: 400;
text-align: right;
font-size: 28rpx;
color: #333333;
z-index: 0;
}
}
}
.card-view {
margin-top: 20rpx;
padding: 0 25rpx 0 23rpx;
view:not(.divider):nth-of-type(n+1) {
margin: 20rpx 0;
.card-view-title {
font-weight: 400;
font-size: 30rpx;
color: #333333;
flex: 1;
}
.card-view-value {
font-weight: 400;
font-size: 26rpx;
color: #333333;
image {
width: 8rpx;
height: 16rpx;
margin-left: 16rpx;
}
}
}
}
.payment-way {
background: #FFFFFF;
border-radius: 10rpx;
padding: 22rpx 23rpx;
margin-top: 20rpx;
text:nth-of-type(1) {
font-weight: bold;
font-size: 30rpx;
color: #333333;
flex: 1;
}
view:nth-of-type(1) {
font-weight: 400;
font-size: 28rpx;
color: #333333;
image {
width: 8rpx;
height: 16rpx;
margin-left: 13rpx;
}
}
}
.bottom-view {
background: #FFFFFF;
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20rpx 30rpx 78rpx 30rpx;
text:nth-of-type(1) {
font-weight: 400;
font-size: 30rpx;
color: #333333;
}
text:nth-of-type(2) {
font-weight: bold;
font-size: 36rpx;
color: #F32B2B;
margin-left: 14rpx;
flex: 1;
}
sqb-pay .confirm-order {
display: flex;
padding: 0 45rpx;
height: 80rpx;
border: 2rpx solid #F32B2B;
font-weight: bold;
font-size: 30rpx;
color: #F32B2B;
align-items: center;
justify-content: center;
background: #FFFFFF;
border-radius: 40rpx;
}
}
</style>

View File

@@ -0,0 +1,145 @@
<template>
<view class='content'>
<view class='search-view'>
<view class='search-input'>
<image :src='assetsUrl("ic_search.png")' />
<input placeholder='输入名称、款号搜索' @input='bindInput' />
</view>
</view>
<scroll-view class='goods-list'>
<grid-view :cross-axis-count='2'>
<view v-for='(item, index) in goodsList' :key='index' class='goods-item'
@click.stop='goPath(`/pages/mall/subs/goods/detail?goodsId=${item.goodsId}`)'>
<image class='goods-image' :src='item.images||defaultImage' />
<text class='goods-name'>{{ item.goodsName || '未知' }}</text>
<text class='goods-price'>¥{{ (item.price * userInfo.userDiscount).toFixed(2) }}
<text v-if='userInfo.userDiscount>0&&userInfo.userDiscount<1'
style='text-decoration: line-through;color: #999999;font-size: 25rpx'>¥{{ item.price }}
</text>
</text>
<image class='add-image' :src='assetsUrl("ic_add_goods.png")' @click.stop='addShoppingCart(item)' />
</view>
</grid-view>
</scroll-view>
</view>
<sku-dialog ref='skuDialogRef' />
</template>
<script lang='ts' setup>
import SkuDialog from '@/components/sku-dialog.vue';
import { assetsUrl, defaultImage } from '@/utils/assets';
import { goPath } from '@/utils';
import { getGoodsList } from '@/api/goods';
import useShoppingCartStore from '@/store/modules/shoppingcart';
import { GoodsBean } from '@/api/goods/types';
import { useUserStore } from '@/store';
const skuDialogRef = ref();
const shoppingCartStore = useShoppingCartStore();
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const goodsList = ref<GoodsBean[]>([]);
onLoad((e) => {
});
const bindInput = async (e: any) => {
const { rows } = await getGoodsList({
page: {
pageNum: 1,
pageSize: 100,
bean: {
keyword: e.detail.value,
typeIds: []
}
}
});
goodsList.value = rows;
};
const addShoppingCart = (goodsBean: GoodsBean) => {
skuDialogRef.value.show(goodsBean.goodsId, (e: GoodsBean) => {
shoppingCartStore.save(e);
});
};
</script>
<style lang='scss' scoped>
.content {
display: flex;
flex-direction: column;
height: 100vh;
padding: 0 20rpx;
}
.search-view {
display: flex;
flex-direction: row;
background: white;
padding: 19rpx 30rpx;
.search-input {
display: flex;
flex-direction: row;
align-items: center;
background: #F7F7F7;
padding: 17rpx 23rpx;
font-size: 26rpx;
width: 100%;
image {
width: 32rpx;
height: 32rpx;
margin-right: 20rpx;
}
}
}
.goods-list {
display: flex;
background: white;
.goods-item {
display: flex;
flex-direction: column;
position: relative;
padding: 10rpx 10rpx;
flex: 1;
.goods-image {
border-radius: 10rpx;
width: 100%;
max-height: 400rpx;
}
.goods-name {
font-size: 28rpx;
font-weight: 400;
color: #333333;
-webkit-line-clamp: 1;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 10rpx;
}
.goods-price {
font-size: 36rpx;
font-weight: bold;
color: #F32B2B;
}
.add-image {
width: 42rpx;
height: 42rpx;
position: absolute;
bottom: 10rpx;
right: 10rpx;
}
}
}
</style>

View File

@@ -0,0 +1,333 @@
<template>
<view class='content'>
<view class='c-flex-row'>
<text class='manage' @click.stop='isEditMode = !isEditMode' :style='isEditMode?"color:#F32B2B":"color:#333333"'>
{{ isEditMode ? '退出管理' : '管理' }}
</text>
</view>
<view class='card-view' v-if='totalCount>0'>
<scroll-view>
<view class='c-flex-column' v-for='(item, index) in shoppingCartList' :key='index'>
<view class='c-flex-row'>
<image class='checkbox'
:src='assetsUrl(item.checked?"ic_checkbox_active_red.png":"ic_checkbox_normal.png")'
@click.stop='handleCheck(item)' />
<image class='goods-image' :src='item.images||defaultImage' />
<view class='c-flex-column'>
<text>{{ item.name }}</text>
<view class='c-flex-row'>
<view class='sku-view' @click.stop='showSkuDialog(index)'>
<text>{{ item?.checkedStock?.colorName||'均色' }}{{ item?.checkedStock?.sizeName||'均码' }}
x{{ item?.checkedStock?.count }}
</text>
<image :src='assetsUrl("ic_arrow_down_gray.png")' />
</view>
</view>
<text style='margin-top: 50rpx'>
¥{{ (item?.price * userInfo.userDiscount).toFixed(2) }}
<text v-if='userInfo.userDiscount>0&&userInfo.userDiscount<1'
style='text-decoration: line-through;color: #999999;font-size: 25rpx'>¥{{ item?.price }}
</text>
</text>
<view class='count-change-view c-flex-row'>
<view class='count-image' @click.stop='countChange(index,false)'>
<image :src='assetsUrl("ic_reduce.png")' />
</view>
<text>{{ item?.checkedStock?.count || 0 }}</text>
<view class='count-image' @click.stop='countChange(index,true)'>
<image :src='assetsUrl("ic_plus.png")' />
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<u-empty v-else text='暂无数据' margin-top='100' />
<view class='bottom-view c-flex-row'>
<view class='c-flex-row' @click.stop='handleAllCheck'>
<image :src='assetsUrl(checkedAll?"ic_checkbox_active.png":"ic_checkbox_normal.png")' />
<text>全选</text>
</view>
<view style='flex: 1' />
<text v-if='isEditMode' class='settlement' @click.stop='deleteShoppingCart'>删除</text>
<view v-else class='c-flex-row'>
<text>合计
<text>{{ totalPayPrice.toFixed(2) }}</text>
</text>
<text class='settlement' @click.stop='settlement'>结算</text>
</view>
</view>
</view>
<sku-dialog ref='skuDialogRef' :bean='temporaryGoodsBean' :exists='temporaryStockBean' />
</template>
<script lang='ts' setup>
import { assetsUrl, defaultImage } from '@/utils/assets';
import useShoppingCartStore from '@/store/modules/shoppingcart';
import { goPath, showToast } from '@/utils';
import { GoodsBean, StockBean } from '@/api/goods/types';
import SkuDialog from '@/components/sku-dialog.vue';
import { ref } from 'vue';
import { useUserStore } from '@/store';
const isEditMode = ref(false);
const checkedAll = ref(false);
const shoppingCartStore = useShoppingCartStore();
const { shoppingCartList, totalCount } = storeToRefs(shoppingCartStore);
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const skuDialogRef = ref();
const temporaryGoodsBean = ref<GoodsBean>();
const temporaryStockBean = ref<StockBean>();
const totalPayPrice = computed(() => {
return shoppingCartList.value.filter(res => res.checked).reduce((a, b) => a + b.consumePrice * b.checkedStock.count, 0) || 0;
});
const handleCheck = (item: any) => {
item.checked = !item.checked;
if(!item.checked) {
checkedAll.value = false;
} else {
checkedAll.value = shoppingCartList.value.every(res => res.checked);
}
};
const handleAllCheck = () => {
checkedAll.value = !checkedAll.value;
shoppingCartList.value.forEach(res => {
res.checked = checkedAll.value;
});
};
const deleteShoppingCart = () => {
shoppingCartStore.deleteIfChecked();
checkedAll.value = false;
isEditMode.value = false;
};
const showSkuDialog = (index: number) => {
new Promise((resolve, _) => {
temporaryGoodsBean.value = shoppingCartList.value[index];
temporaryStockBean.value = shoppingCartList.value[index].checkedStock;
resolve(temporaryGoodsBean.value);
}).then((res: any) => {
skuDialogRef.value.show(res.id, (e: GoodsBean) => {
//重新选择sku后判断当前购物中是否存在相同sku的商品
const existsIndex = shoppingCartStore.getSameGoodsIndex(res?.id || '', e.checkedStock.colorId, e.checkedStock.sizeId);
//不存在则更新当前商品sku
if(existsIndex < 0) {
shoppingCartStore.updateStock(index, e.checkedStock);
}
//如果已存在,则更新已存在商品数量,并删除当前商品
else {
shoppingCartStore.updateCount(existsIndex, e.checkedStock.count - (temporaryGoodsBean?.value?.checkedStock?.count || 0));
if(existsIndex != index) {
shoppingCartStore.delete(index);
}
}
});
});
};
const countChange = (index: number, isPlus: Boolean) => {
if(isPlus) {
shoppingCartStore.updateCount(index, 1);
} else {
if(shoppingCartList.value[index].checkedStock.count > 1) {
shoppingCartStore.updateCount(index, -1);
}
}
};
const settlement = () => {
if(shoppingCartList.value.filter(res => res.checked).length == 0) {
showToast('请选择商品');
return;
}
const orderBean = {
totalPrice: totalPayPrice.value,
orderGoods: shoppingCartList.value.filter(res => res.checked)
};
goPath(`/pages/mall/subs/order/order-confirm?orderBean=${encodeURIComponent(JSON.stringify(orderBean))}`);
};
</script>
<style lang='scss' scoped>
.content {
position: relative;
.manage {
width: 100%;
font-weight: 400;
font-size: 30rpx;
color: #333333;
background: #FFFFFF;
align-self: flex-end;
padding: 20rpx 30rpx;
text-align: end;
}
.card-view {
margin: 30rpx 30rpx 180rpx 30rpx;
border-radius: 10rpx;
background: #FFFFFF;
padding: 10rpx 20rpx 10rpx 17rpx;
.c-flex-column:nth-of-type(1) {
margin: 20rpx 0;
}
view:nth-of-type(1) {
.checkbox {
width: 35rpx;
height: 35rpx;
margin-right: 20rpx;
}
.goods-image {
width: 186rpx;
height: 186rpx;
border-radius: 10rpx;
}
.c-flex-column {
flex: 1;
height: 186rpx;
margin-left: 20rpx;
position: relative;
text:nth-of-type(1) {
font-weight: 400;
font-size: 30rpx;
color: #333333;
-webkit-line-clamp: 2;
text-overflow: ellipsis;
overflow: hidden;
}
.sku-view {
display: flex;
flex-direction: row;
position: relative;
background: #F2F2F2;
margin-top: 10rpx;
align-items: center;
border-radius: 22rpx;
padding: 5rpx 20rpx;
text {
font-weight: 400;
font-size: 24rpx;
color: #999999;
}
image {
width: 20rpx;
height: 20rpx;
margin-left: 13rpx;
}
}
text:nth-of-type(2) {
display: flex;
font-weight: bold;
font-size: 32rpx;
color: #333333;
margin-top: 20rpx;
}
.count-change-view {
position: absolute;
right: 0;
bottom: 0;
.count-image {
display: flex;
align-items: center;
justify-content: center;
width: 54rpx;
height: 54rpx;
background: #FBFBFB;
image {
width: 15rpx;
height: 2rpx;
}
}
.count-image:nth-of-type(2) image {
width: 17rpx;
height: 17rpx;
}
text {
display: flex;
width: 54rpx;
height: 54rpx;
align-items: center;
justify-content: center;
font-weight: 400;
font-size: 30rpx;
color: #333333;
background: #F2F2F2;
}
}
}
}
}
.bottom-view {
background: #FFFFFF;
position: fixed;
padding: 12rpx 30rpx 78rpx 30rpx;
bottom: 0;
left: 0;
right: 0;
image {
width: 35rpx;
height: 35rpx;
}
.c-flex-row:nth-of-type(1) {
text {
margin-left: 10rpx;
font-size: 28rpx;
color: #333333;
}
}
text:nth-of-type(1) {
font-size: 30rpx;
text {
font-weight: bold;
font-size: 36rpx;
color: #F32B2B;
}
}
.settlement {
display: flex;
background: #F32B2B;
border-radius: 43rpx;
padding: 17rpx 75rpx;
font-weight: bold;
font-size: 30rpx;
color: #FFFFFF;
margin-left: 37rpx;
align-items: center;
justify-content: center;
}
}
}
</style>

View File

@@ -1,25 +1,382 @@
<template>
<view class="flex flex-col items-center justify-center">
<image
class="mb-50rpx mt-200rpx h-200rpx w-200rpx"
src="@/static/images/logo.png"
width="200rpx"
height="200rpx"
/>
<view class="flex justify-center">
<text class="font-size-36rpx color-gray-700">
{{ title }}
</text>
<view class='content'>
<view class='user-info-view'>
<image
class='image-bg'
:src='companyConfigInfo.userbgcover'
/>
<view class='top-row'>
<image :src='userInfo?.image||defaultAvatar' />
<view class='u-flex-column'>
<text>{{ userInfo?.nickName || '未登录' }}</text>
<text>{{ userInfo?.telephone }}</text>
</view>
<image :src='assetsUrl("ic_qrcode_white.png")' @click.stop='gotoPath("qrcode")' />
</view>
<view class='bottom-row'>
<view @click.stop='gotoPath("/pages/mine/subs/integral/index")'>
<text>{{ userInfo?.integration || 0 }}</text>
<text>积分</text>
</view>
<view @click.stop='gotoPath("/pages/mine/subs/recharge/index")'>
<text>{{ userInfo?.balance || 0 }}</text>
<text>余额</text>
</view>
<view @click.stop='gotoPath("/pages/mine/subs/coupon/index")'>
<text>{{ userInfo?.couponsCount || 0 }}</text>
<text>优惠券</text>
</view>
</view>
</view>
<view class='integral-add-view'>
<text>将会员卡放入微信卡包及时了解积分变动</text>
<text @click.stop='openCard'>去添加</text>
<image :src='assetsUrl("ic_arrow_right_yellow.png")' />
</view>
<text class='title-view'>
我的订单
</text>
<view class='card-order-view'>
<view v-for='(item,index) in orderActionList' :key='index' @click.stop='gotoPath(item.path)'>
<image :src='item.icon' />
<text>{{ item.title }}</text>
<text v-if='index==0&&unPaidOrderCount>0'>{{ unPaidOrderCount }}</text>
</view>
</view>
<text class='title-view'>
会员服务
</text>
<view class='card-service-view'>
<view v-for='(item,index) in serviceList' :key='index' @click.stop='gotoPath(item.path)'>
<image :src='item.icon' />
<text>{{ item.title }}</text>
</view>
</view>
</view>
<official-account-dialog ref='officialAccountDialogRef' />
</template>
<script setup lang="ts">
<script setup lang='ts'>
import { assetsUrl, defaultAvatar } from '@/utils/assets';
import { goLogin, goPath, isLogin } from '@/utils';
import { isPending } from '@/utils/order';
import { getCardLink } from '@/api/user';
import { getOrderList } from '@/api/order';
import OfficialAccountDialog from '@/components/official-account-dialog.vue';
import { useUserStore } from '@/store';
const title = ref<string>();
title.value = import.meta.env.VITE_APP_TITLE;
const userStore = useUserStore();
const officialAccountDialogRef = ref();
const orderActionList = ref([{
title: '未支付',
icon: assetsUrl('ic_order_progressing.png'),
path: '/pages/mine/subs/order/index?index=1'
}, {
title: '已支付',
icon: assetsUrl('ic_order_finish.png'),
path: '/pages/mine/subs/order/index?index=2'
}, {
title: '全部订单',
icon: assetsUrl('ic_order_all.png'),
path: '/pages/mine/subs/order/index?index=0'
}]);
const serviceList = [
{
title: '会员信息',
icon: assetsUrl('ic_member_service_info.png'),
path: '/pages/mine/subs/profile/index'
},
{
title: '消费记录',
icon: assetsUrl('ic_member_service_record.png'),
path: '/pages/mine/subs/trade/index'
},
{
title: '关注公众号',
icon: assetsUrl('ic_member_service_follow.png'),
path: 'follow_official_account'
},
{
title: '收货地址',
icon: assetsUrl('ic_member_service_address.png'),
path: '/pages/mine/subs/address/index'
}, {
title: '联系商家',
icon: assetsUrl('ic_member_service_contact.png'),
path: '/pages/mine/subs/contact/index'
}
// ,{
// title: '推广中心',
// icon: assetsUrl + 'ic_order_progressing.png',
// path: ''
// }
];
const store = useUserStore();
console.log('store.user_name', store.user_name);
const { userInfo, companyInfo, companyConfigInfo } = storeToRefs(store);
const cardLink = ref('');
const unPaidOrderCount = ref(0);
onLoad(() => {
if(!isLogin()) {
goLogin();
return;
}
});
onShow(async () => {
const { list } = await getOrderList({
pageNum: 1,
pageSize: 9999,
obj: { payStatus: 1 }
});
unPaidOrderCount.value = list.filter((item: any) => isPending(item))?.length || 0;
const { cardurl } = await getCardLink();
cardLink.value = cardurl;
await userStore.getProfile();
});
const openCard = () => {
// const url = 'https://mp.weixin.qq.com/bizmall/activatemembercard?action=preshow&&encrypt_card_id=LI%2FNyDp0x3z8XXorvvrHzSR4VUPa2vlioBg2xkDT3HqybiAFNsNgjH7pBpyKGrSA&outer_str=1702887425732870145&biz=MzU2MTg5MjgxMg%3D%3D#wechat_redirect';
goPath(`/pages/common/webview/index?url=${encodeURIComponent(cardLink.value)}`);
// uni.navigateToMiniProgram({ appId: 'wxeb490c6f9b154ef9' });
// uni.addCard({
// cardList: [
// {
// cardId: 'pzCtO1NU8FDcF3v09_Q76-7ZuN8I',
// cardExt: JSON.stringify({ 'code': '12345', openId: 'wxc480826360455685', 'timestamp': Date.now(), 'signature': '' })
// }
// ]
// });
};
const gotoPath = (path: string) => {
if(path === 'follow_official_account') {
showOfficialAccountDialog();
} else if(path === 'qrcode') {
uni.switchTab({
url: '/pages/qrcode/index'
});
} else if(path === 'call') {
uni.makePhoneCall({
phoneNumber: companyInfo.value.telphone
});
} else {
goPath(path);
}
};
const showOfficialAccountDialog = () => {
officialAccountDialogRef.value.show();
};
</script>
<style lang='scss' scoped>
.content {
display: flex;
flex-direction: column;
padding: 30rpx;
}
.user-info-view {
display: flex;
flex-direction: column;
position: relative;
height: 275rpx;
.image-bg {
width: 100%;
height: 100%;
position: absolute;
}
.top-row {
display: flex;
flex-direction: row;
position: relative;
margin-left: 33rpx;
margin-top: 43rpx;
margin-right: 28rpx;
align-items: center;
image:nth-of-type(1) {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
border: #FFFFFF solid 2rpx;
}
view:nth-of-type(1) {
display: flex;
flex-direction: column;
color: white;
flex: 1;
margin-left: 20rpx;
text:nth-of-type(1) {
font-size: 30rpx;
font-weight: 800;
}
text:nth-of-type(2) {
font-size: 24rpx;
font-weight: 400;
margin-top: 5rpx;
}
}
image:nth-of-type(2) {
width: 36rpx;
height: 36rpx;
}
}
.bottom-row {
display: flex;
flex-direction: row;
justify-content: center;
position: relative;
margin-top: 33rpx;
view:nth-of-type(n+1) {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 1;
text:nth-of-type(1) {
font-size: 36rpx;
font-weight: 400;
color: #FFFFFF;
}
text:nth-of-type(2) {
font-size: 26rpx;
font-weight: 400;
margin-top: 8rpx;
color: #FFFFFF;
}
}
}
}
.integral-add-view {
display: flex;
flex-direction: row;
background: #F8ECDE;
align-items: center;
border-radius: 10rpx;
padding: 20rpx 17rpx;
margin-top: 30rpx;
text:nth-of-type(1) {
display: flex;
font-size: 24rpx;
font-weight: 400;
color: #333333;
flex: 1;
}
text:nth-of-type(2) {
font-size: 26rpx;
font-weight: 400;
color: #DB7400;
}
image {
width: 30rpx;
height: 30rpx;
margin-left: 14rpx;
}
}
.title-view {
font-size: 30rpx;
font-weight: 800;
color: #333333;
margin-top: 30rpx;
}
.card-view {
display: flex;
background: #FFFFFF;
border-radius: 20rpx;
margin-top: 20rpx;
}
.card-order-view {
@extend .card-view;
flex-direction: row;
padding: 30rpx 0;
view:nth-of-type(n+1) {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
flex: 1;
image {
width: 58rpx;
height: 58rpx;
}
text:nth-of-type(1) {
font-size: 28rpx;
font-weight: 400;
color: #333333;
margin-top: 10rpx;
}
text:nth-of-type(2) {
display: flex;
position: absolute;
min-width: 40rpx;
min-height: 35rpx;
align-items: center;
font-size: 26rpx;
justify-content: center;
background: #F32B2B;
border-radius: 50%;
color: #FFFFFF;
top: -10rpx;
right: 70rpx;
}
}
}
.card-service-view {
@extend .card-view;
display: grid;
grid-template-columns:repeat(4, 1fr);
view:nth-of-type(n+1) {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 30rpx 0;
font-size: 26rpx;
}
image {
width: 54rpx;
height: 54rpx;
}
}
</style>

View File

@@ -0,0 +1,93 @@
<template>
<view class='card-view' @click.stop='emits("onChecked",item)'>
<view class='c-flex-row'>
<text class='status' v-if='item?.defaultstatus==1'>默认</text>
<text>
{{ item?.name }}
</text>
<text>
{{ item?.mobile }}
</text>
</view>
<text class='address'>
{{ item?.addr }}
</text>
<view class='btn-view c-flex-row'>
<text @click.stop='emits("onEdit",item)'>编辑</text>
<text @click.stop='emits("onDelete",item)'>删除</text>
</view>
</view>
</template>
<script lang='ts' setup>
import { PropType } from 'vue';
defineProps({
item: Object as PropType<{ name: string, mobile: string, addr: string, defaultstatus: number }>
});
const emits = defineEmits(['onEdit', 'onDelete', 'onChecked']);
</script>
<style lang='scss' scoped>
.card-view {
margin: 20rpx 30rpx;
padding: 38rpx 30rpx 30rpx 34rpx;
view:nth-of-type(1) {
.status {
border-radius: 5rpx;
border: 1rpx solid #F32B2B;
color: #F32B2B;
font-size: 24prx;
padding: 1rpx 8rpx;
}
text:nth-of-type(2) {
font-size: 32rpx;
font-weight: bold;
margin-left: 10rpx;
color: #333333;
}
text:nth-of-type(3) {
font-size: 32rpx;
font-weight: bold;
margin-left: 20rpx;
color: #333333;
}
}
.address {
font-size: 26rpx;
font-weight: 400;
margin-top: 10rpx;
color: #999999;
}
.btn-view {
display: flex;
align-self: flex-end;
align-items: center;
text:nth-of-type(n+1) {
display: flex;
width: 84rpx;
height: 48rpx;
border-radius: 24rpx;
border: 1rpx solid #F32B2B;
align-items: center;
justify-content: center;
color: #F32B2B;
margin-left: 20rpx;
}
text:nth-of-type(2) {
border: 1rpx solid #DDDDDD;
color: #666666;
}
}
}
</style>

View File

@@ -0,0 +1,184 @@
<template>
<view class='content'>
<view class='c-flex-row'>
<text>收货人</text>
<input placeholder='请输入收货人姓名' v-model='params.name' @input='bindConsignee' />
</view>
<view class='divider' />
<view class='c-flex-row'>
<text>手机号</text>
<input placeholder='请输入收货人手机号' v-model='params.mobile' @input='bindMobile' :maxlength='11' />
</view>
<view class='divider' />
<view class='c-flex-row detail-row'>
<text>详细地址</text>
<view class='c-flex-column'>
<textarea placeholder='详细地址:如街道、门牌\n号、楼栋号、单元室等'
v-model='params.addr'
@input='bindAddress' />
<view class='tips-view c-flex-row'>
<image :src='assetsUrl("ic_tips.png")' />
<text>记得完善门牌号</text>
</view>
</view>
</view>
<view class='divider' />
<view class='c-flex-column'>
<text style='width: auto'>设为默认地址</text>
<text style='width: auto'>提醒每次下单会默认推荐使用该地址</text>
<!-- @change='ev=>{params.defaultstatus = ev.detail.value}'-->
<switch color='#F32B2B' @change='bindStatus' :checked='params.defaultstatus == 1' />
</view>
<view class='bottom-button-view'>
<button class='primary-button' @click.stop='save'>保存地址</button>
</view>
</view>
</template>
<script lang='ts' setup>
import { assetsUrl } from '@/utils/assets';
import { addressCreate, addressUpdate } from '@/api/user';
const params = ref<{
addrid: string,
name: string,
mobile: string,
zonecode: string,
addr: string,
defaultstatus: number
}>({
addrid: '',
name: '',
mobile: '',
zonecode: '',
addr: '',
defaultstatus: 0
});
onLoad(async (e: any) => {
if(e.bean) {
params.value = JSON.parse(decodeURIComponent(e.bean));
}
});
const bindConsignee = (e: any) => {
params.value.name = e.detail.value;
};
const bindMobile = (e: any) => {
params.value.mobile = e.detail.value;
};
const bindAddress = (e: any) => {
params.value.addr = e.detail.value;
};
const bindStatus = (e: any) => {
params.value.defaultstatus = e.detail.value == true ? 1 : 0;
};
const save = async () => {
let result = false;
if(params.value.addrid) {
result = await addressUpdate(params.value);
} else {
result = await addressCreate(
params.value
);
}
if(result === true) {
uni.showToast({
title: '保存成功',
icon: 'none',
success(result) {
uni.navigateBack();
}
});
}
};
</script>
<style lang='scss' scoped>
.content {
background: #FFFFFF;
padding: 28rpx 28rpx 0 28rpx;
view:not(.divider):nth-of-type(n+1) {
height: 100rpx;
text:nth-of-type(n+1) {
font-size: 32rpx;
font-weight: 400;
color: #333333;
width: 170rpx;
white-space: nowrap;
}
input {
font-size: 30rpx;
}
}
.detail-row {
height: 236rpx !important;
align-items: flex-start;
padding-top: 28rpx;
text:nth-of-type(1) {
width: 170rpx;
}
view:nth-of-type(1) {
flex: 1;
margin: 0 20rpx 0 0;
}
textarea {
height: 200rpx;
width: 100%;
font-size: 30rpx;
}
.tips-view {
image {
width: 32rpx;
height: 32rpx;
}
text {
font-size: 26rpx !important;
font-weight: 400;
color: #999999 !important;
margin-left: 20rpx;
}
}
}
.c-flex-column {
display: flex;
position: relative;
justify-content: center;
height: 170rpx !important;
text:nth-of-type(2) {
font-size: 24rpx !important;
color: #CCCCCC !important;
margin-top: 10rpx;
}
switch {
position: absolute;
align-self: center;
right: 0;
}
}
}
</style>

View File

@@ -0,0 +1,73 @@
<template>
<view class='content'>
<scroll-view :scroll-y='true'>
<item v-for='(item,index) in addressList' :key='index' :item='item'
@on-checked='onChecked(item)'
@on-edit='args => goPath(`create?bean=${encodeURIComponent(JSON.stringify(args))}`)'
@on-delete='args => {handleDelete(args)}' />
<u-empty v-if='addressList.length === 0' text='暂无数据' margin-top='100' />
</scroll-view>
<view class='bottom-button-view'>
<button class='primary-button' @click.stop='goPath("create")'>+新增地址</button>
</view>
</view>
</template>
<script lang='ts' setup>
import Item from './components/item.vue';
import { goPath } from '@/utils';
import { addressDelete, getAddressList } from '@/api/user';
import { useUserStore } from '@/store';
const userStore = useUserStore();
const addressList = ref<{
name: string,
mobile: string,
addr: string,
status: number,
defaultstatus: number,
}[]>([]);
onLoad((e) => {
// fetchAddressList();
});
onShow(() => {
fetchAddressList();
});
const fetchAddressList = async () => {
addressList.value = await getAddressList();
};
const handleDelete = async (item: any) => {
uni.showModal(
{
title: '提示',
content: '确定删除该地址吗',
success: async (res) => {
if(res.confirm) {
const result = await addressDelete(item?.addrid);
if(result) {
await fetchAddressList();
}
}
}
}
);
};
const onChecked = (e: any) => {
const pages = getCurrentPages();
if(pages.length >= 2 && pages[pages.length - 2].route?.includes('pages/mall/subs/order/order-confirm')) {
userStore.setDeliveryAddress(e);
uni.navigateBack();
}
};
</script>
<style lang='scss' scoped>
</style>

View File

@@ -0,0 +1,79 @@
<template>
<uni-popup type='bottom' ref='popupRef' :mask-click='false' @touchmove.stop.prevent=''>
<view class='popup-content c-flex-column'>
<view class='c-flex-row'>
<text>关注公众号</text>
<image :src='assetsUrl("ic_close.png")' @click.stop='close()' />
</view>
<image class='qrcode' :src='assetsUrl("test_bg.png")' />
<text class='tips'>长按关注会员中心公众号
获得更多优惠和服务信息
</text>
</view>
</uni-popup>
</template>
<script lang='ts' setup>
import { assetsUrl } from '@/utils/assets';
const popupRef = ref();
const show = () => {
popupRef.value.open();
};
const close = () => {
popupRef.value.close();
};
defineExpose({ show });
</script>
<style lang='scss' scoped>
.popup-content {
background: #FFFFFF;
border-radius: 20rpx 20rpx 0 0;
padding: 38rpx 45rpx 140rpx 45rpx;
view:nth-of-type(1) {
display: flex;
margin-bottom: 70rpx;
position: relative;
image {
width: 35rpx;
height: 35rpx;
align-self: flex-start;
}
text {
font-weight: bold;
font-size: 30rpx;
color: #333333;
position: absolute;
left: 0;
right: 0;
text-align: center;
}
}
.qrcode {
width: 377rpx;
height: 377rpx;
margin-top: 50rpx;
align-self: center;
}
.tips {
font-weight: bold;
font-size: 30rpx;
color: #333333;
margin-top: 10rpx;
margin-bottom: 104rpx;
}
}
</style>

View File

@@ -0,0 +1,61 @@
<template>
<view class='card-view'>
<view class='c-flex-row'>
<text class='key'>姓名</text>
<text class='value'>{{ companyConfigInfo?.contactname}}</text>
</view>
<view class='divider' />
<view class='c-flex-row'>
<text class='key'>手机号</text>
<text class='value'>{{companyConfigInfo?.contacttelephone}}</text>
<image :src='assetsUrl("ic_contact_merchant.png")' @click.stop='call' />
</view>
</view>
</template>
<script lang='ts' setup>
import { useUserStore } from '@/store';
import { assetsUrl } from '@/utils/assets';
const { companyConfigInfo } = storeToRefs(useUserStore());
const call = () => {
uni.makePhoneCall({
phoneNumber: companyConfigInfo.value.contacttelephone
});
};
</script>
<style lang='scss' scoped>
.card-view {
background-color: #fff;
padding: 30rpx 20rpx;
margin: 24rpx;
border-radius: 10rpx;
.divider {
margin: 30rpx 0;
}
.c-flex-row {
.key {
font-size: 30rpx;
color: #7E7E7E;
width: 120rpx;
}
.value {
display: flex;
font-weight: 400;
font-size: 30rpx;
color: #333333;
flex: 1;
}
image {
width: 39rpx;
height: 41rpx;
margin-right: 10rpx;
}
}
}
</style>

View File

@@ -0,0 +1,127 @@
<template>
<view class='coupon-content'>
<image v-if='item?.status==0' :src='assetsUrl("bg_coupon.png")' />
<image v-else-if='item?.status==1||item?.status==2' :src='assetsUrl("bg_coupon_expired.png")' />
<view class='left-content accent-text-color' :class='{"left-content-disabled": item?.status!=0}'>
<text>{{ item?.reduce }}</text>
<text>{{ item?.threshold }}元可用</text>
</view>
<view class='right-content accent-text-color' :class='{"right-content-disabled": item?.status!=0}'>
<text>{{ item?.name }}</text>
<text>有效期至{{ dayjs(item?.startTime).format('YYYY-MM-DD') }}</text>
<text class='btn-text' :class='{"btn-text-disabled": item?.status!=0}' @click.stop='goPath("/pages/mall/index")'>
立即使用
</text>
</view>
</view>
</template>
<script lang='ts' setup>
import { assetsUrl } from '@/utils/assets';
import dayjs from 'dayjs';
import { PropType } from 'vue';
import { CouponBean } from '@/api/user/types';
import { goPath } from '@/utils';
defineProps({
item: Object as PropType<CouponBean>
});
</script>
<style lang='scss' scoped>
.coupon-content {
display: flex;
flex-direction: row;
height: 200rpx;
position: relative;
margin: 30rpx 30rpx 10rpx 30rpx;
image {
width: 100%;
height: 100%;
position: absolute;
}
.left-content {
display: flex;
flex-direction: column;
position: relative;
align-items: center;
justify-content: center;
padding-left: 50rpx;
text:nth-of-type(1) {
font-size: 55rpx;
font-weight: bold;
}
text:nth-of-type(1):after {
content: '元';
font-size: 24rpx;
}
text:nth-of-type(2) {
font-size: 26rpx;
font-weight: 400;
}
}
.left-content-disabled {
@extend .left-content;
color: #C2C2C2;
}
.right-content {
display: flex;
flex-direction: column;
position: relative;
flex: 1;
justify-content: center;
margin-left: 60rpx;
text:nth-of-type(1) {
font-size: 30rpx;
}
text:nth-of-type(2) {
font-size: 26rpx;
margin-top: 20rpx;
color: #333333;
}
.btn-text {
display: flex;
width: 146rpx;
height: 55rpx;
align-items: center;
justify-content: center;
border-radius: 28rpx;
border: #F32B2B solid 2rpx;
font-size: 28rpx;
font-weight: bold;
color: #F32B2B;
position: absolute;
right: 20rpx;
}
.btn-text-disabled {
@extend .btn-text;
color: #C2C2C2;
border: none;
}
}
.right-content-disabled {
@extend .right-content;
color: #C2C2C2;
text:nth-of-type(2) {
color: #C2C2C2;
}
}
}
</style>

View File

@@ -0,0 +1,75 @@
<template>
<tabbar :titles='["全部","未使用","已使用"]' :indicator-color='"#D95554"' @change='tabChange' />
<u-list @scrolltolower='onLoadMore'>
<coupon-item v-for='item in couponList' :item='item' :key='item.id' />
<u-loadmore v-if='couponList.length>0' :status='loadingStatus' />
<u-empty v-if='couponList.length === 0' text='暂无优惠券' marginTop='100' />
</u-list>
</template>
<script lang='ts' setup>
import { ref } from 'vue';
import CouponItem from './components/coupon-item.vue';
import { getCouponList } from '@/api/user';
import { useUserStore } from '@/store';
import { getCompanyId } from '@/utils';
import { CouponBean } from '@/api/user/types';
const store = useUserStore();
const { userInfo } = storeToRefs(store);
const couponStatus = ref<number | any>(undefined);
const couponList = ref<CouponBean[]>([]);
const currentPageNum = ref(1);
const loadingStatus = ref('loading');
onLoad(async () => {
await fetchData();
});
const tabChange = (index: number) => {
switch (index) {
case 0:
couponStatus.value = undefined;
break;
case 1:
couponStatus.value = 0;
break;
case 2:
couponStatus.value = 1;
break;
}
currentPageNum.value = 1;
fetchData();
};
const fetchData = async (refresh: boolean = true) => {
loadingStatus.value = 'loading';
if(!refresh) {
currentPageNum.value += 1;
}
const { list } = await getCouponList({
companyId: getCompanyId(),
memberId: userInfo.value?.id,
status: couponStatus.value,
pageNum: currentPageNum.value,
pageSize: 20
});
if(refresh) {
couponList.value = list;
} else {
couponList.value = couponList.value.concat(list);
}
loadingStatus.value = 'no';
};
const onLoadMore = () => {
fetchData(false);
};
</script>
<style lang='scss' scoped>
</style>

View File

@@ -0,0 +1,47 @@
<template>
<view class='card-view'>
<!-- <text class='category-title'>2023-02-12</text>-->
<!-- <view class='item c-flex-column' v-for='(item,index) in 3' :key='index'>-->
<view class='c-flex-row'>
<text class='primary-text-color' style='flex: 1'>{{item?.remark}}</text>
<text class='accent-text-color'>{{item?.value}}</text>
</view>
<view class='c-flex-row'>
<text class='secondary-text-color' style='flex: 1'>{{ item?.createtime }}</text>
<text style='color: #999999'>会员积分30.00</text>
</view>
<!-- <view class='divider' style='margin-top: 20rpx'/>-->
<!-- </view>-->
</view>
</template>
<script lang='ts' setup>
import { IntegralBean } from '@/api/user/types';
defineProps({
item: Object as PropType<IntegralBean>
});
</script>
<style lang='scss' scoped>
.card-view {
display: flex;
flex-direction: column;
padding: 16rpx 23rpx 20rpx 30rpx;
margin: 10rpx 0;
}
.category-title {
font-size: 34rpx;
font-weight: bold;
color: #333333;
}
.item {
margin: 10rpx 0;
view:nth-of-type(2) {
margin-top: 10rpx;
}
}
</style>

View File

@@ -0,0 +1,82 @@
<template>
<view class='content'>
<view class='top-view c-flex-column'>
<text>{{ userInfo?.integration }}</text>
<text>会员积分</text>
</view>
<text class='integral-detail-title'>积分明细</text>
<scroll-view style='margin: 10rpx 20rpx'>
<u-list @scrolltolower='loadMore'>
<integral-item v-for='(item,index) in tradeList' :key='index' :item='item' />
<u-empty v-if='tradeList.length === 0' text='暂无数据' margin-top='100' />
</u-list>
</scroll-view>
</view>
</template>
<script lang='ts' setup>
import IntegralItem from './components/integral-item.vue';
import { getIntegralList } from '@/api/user';
import { useUserStore } from '@/store';
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const tradeList = ref([]);
const currentPageNum = ref(1);
onLoad((e) => {
fetchData();
});
const fetchData = async (refresh: boolean = true) => {
currentPageNum.value = refresh ? 1 : currentPageNum.value + 1;
const { rows } = await getIntegralList({
fbean: { 'pageNum': currentPageNum.value, 'pageSize': 20, 'bean': {} }
});
if(refresh) {
tradeList.value = rows;
} else {
tradeList.value = tradeList.value.concat(rows);
}
};
const loadMore = () => {
fetchData(false);
};
</script>
<style lang='scss' scoped>
.top-view {
height: 195rpx;
background: linear-gradient(#F32B2B, #F95D5D);
align-items: center;
justify-content: center;
text:nth-of-type(1) {
font-weight: 400;
font-size: 50rpx;
color: #FFFFFF;
}
text:nth-of-type(2) {
font-weight: 400;
font-size: 26rpx;
color: #FFFFFF;
margin-top: 10rpx;
}
}
.integral-detail-title {
margin: 20rpx 24rpx;
font-weight: bold;
font-size: 30rpx;
color: #333333;
position: relative;
}
scroll-view {
width: auto;
}
</style>

View File

@@ -0,0 +1,170 @@
<template>
<view class='card-view' @click.stop='goPath(`/pages/mine/subs/order/detail?orderId=${item?.id}`)'>
<view class='goods-info-view c-flex-row'>
<image class='goods-image' :src='item?.orderGoods[0].images||defaultImage' />
<view class='c-flex-column' style='flex: 1;'>
<view class='c-flex-row'>
<text class='goods-name'>{{ item?.orderGoods[0].goodsName || '未知' }}</text>
<text class='status'>
<text v-if='item?.payStatus == 2'>已支付</text>
<text v-else-if='isPending(item)'>待支付</text>
<text v-else style='color: #999999'>已过期</text>
</text>
</view>
<view class='bottom-view c-flex-row'>
<text>
{{ item?.orderGoods[0].stockStock.colorName||'均色' }}{{ item?.orderGoods[0].stockStock.sizeName||'均码' }}
</text>
<text>
{{ item?.itemNum }}
</text>
<text>{{ item?.totalPrice }}</text>
</view>
</view>
</view>
<view class='action-view c-flex-row'>
<view class='countdown c-flex-row' v-if='isPending(item)'>
<image :src='assetsUrl("ic_time.png")' />
<text class='primary-text-color'>支付剩余时间
<text class='accent-text-color'>{{ dayjs.unix(item?.countdown || 0).format('mm:ss') || '00:00' }}</text>
</text>
</view>
<view v-else style='flex: 1' />
<view class='c-flex-row'>
<text class='add-shoppingcart secondary-text-color' @click.stop='add(item?.orderGoods[0])'>加入购物车</text>
<text class='payment accent-text-color' v-if='isPending(item)'
@click.stop='goPath(`/pages/mine/subs/order/detail?orderId=${item?.id}`)'>立即支付
</text>
</view>
</view>
</view>
</template>
<script lang='ts' setup>
import { PropType } from 'vue';
import { assetsUrl, defaultImage } from '@/utils/assets';
import { goPath } from '@/utils';
import { isPending } from '@/utils/order';
import { OrderBean } from '@/api/order/types';
import { GoodsBean } from '@/api/goods/types';
import dayjs from 'dayjs';
const emits = defineEmits(['addShoppingCart', 'payment']);
defineProps({
item: Object as PropType<OrderBean>
});
const add = (item: GoodsBean | undefined) => {
emits('addShoppingCart', item);
};
const pay = (orderId: string | undefined) => {
emits('payment', orderId);
};
</script>
<style lang='scss' scoped>
.card-view {
padding: 30rpx 20rpx 20rpx 30rpx;
margin: 20rpx 24rpx;
}
.goods-info-view {
display: flex;
flex-direction: row;
position: relative;
.goods-image {
width: 180rpx;
height: 180rpx;
border-radius: 10rpx;
margin-right: 20rpx;
}
.goods-name {
font-size: 30rpx;
font-weight: 400;
-webkit-line-clamp: 2;
text-overflow: ellipsis;
overflow: hidden;
margin-right: 20rpx;
color: #333333;
flex: 1;
}
.status {
font-size: 26rpx;
font-weight: 400;
color: #F32B2B;
right: 0;
}
.bottom-view {
display: flex;
justify-content: space-between;
margin-top: 40rpx;
text:nth-of-type(1) {
font-size: 26rpx;
font-weight: 400;
flex: 1;
color: #999999;
}
text:nth-of-type(2) {
font-size: 24rpx;
color: #333333;
margin-right: 5rpx;
}
text:nth-of-type(3) {
font-size: 34rpx;
color: #333333;
font-weight: bold;
}
}
}
.action-view {
margin-top: 37rpx;
.countdown {
flex: 1;
image {
width: 37rpx;
height: 37rpx;
margin-right: 5rpx;
}
text {
font-size: 26rpx;
font-weight: 400;
}
}
view:nth-of-type(2) {
text {
display: flex;
align-items: center;
justify-content: center;
padding: 12rpx 28rpx;
font-size: 26rpx;
border-radius: 34rpx;
}
.add-shoppingcart {
border: 2rpx solid #ACACAC;
}
.payment {
border: 2rpx solid #F32B2B;
margin-left: 20rpx;
box-sizing: border-box;
}
}
}
</style>

View File

@@ -0,0 +1,452 @@
<template>
<scroll-view scroll-y>
<view class='content'>
<view class='countdown c-flex-row' v-if='isPending(orderBean?.order)'>
<image :src='assetsUrl("ic_order.png")' />
<view class='c-flex-column'>
<text>
待支付
</text>
<view class='c-flex-row'>
<image :src='assetsUrl("ic_time.png")' />
<text>支付剩余时间
<text style='color: #F32B2B'>{{ dayjs.unix(orderBean?.order?.countdown || 0).format('mm:ss') }}</text>
</text>
</view>
</view>
</view>
<view class='address-view c-flex-column'
v-if='orderBean?.order?.address'>
<view class='user-info c-flex-row'>
<text class='status' v-if='orderBean.order.address.defaultstatus==1'>默认</text>
<text>{{ orderBean?.order?.address.name }}</text>
<text>{{ orderBean?.order?.address.mobile }}</text>
<view style='flex: 1' />
<text style='color: #5B96FB'
@click.stop='copy(orderBean?.order?.address.name+","+orderBean?.order?.address.mobile+","+orderBean?.order?.address.addr)'>
复制
</text>
</view>
<view class='addr c-flex-row'>
<image :src='assetsUrl("ic_location.png")' />
<text>{{ orderBean?.order?.address.addr }}</text>
</view>
<image class='dashed-line' :src='assetsUrl("ic_address_dashed_line.png")' />
</view>
<view class='goods-info-view c-flex-column'>
<view class='c-flex-row' style='margin-bottom: 20rpx' v-for='(item,index) in orderBean?.order.orderGoods'
:key='index'>
<image class='goods-image' :src='item.stockStock.images||defaultImage' />
<view class='c-flex-column' style='flex: 1;'>
<view class='c-flex-row'>
<text class='goods-name'>{{ item.goodsName || '未知' }}</text>
</view>
<text style='color: #999999;margin-top: 30rpx'>
{{ item.goodsCode }}
</text>
<view class='bottom-view c-flex-row'>
<text>
{{ item.stockStock.colorName||'均色' }}{{ item.stockStock.sizeName||'均码' }} x{{ item.goodsNum }}
</text>
<text>{{ item.consumePrice }}</text>
</view>
</view>
</view>
<view class='divider' />
<view class='remark-view c-flex-row'>
<text style='flex: 1'>备注</text>
<text>{{ orderBean?.order?.remark || '无备注' }}</text>
</view>
</view>
<view class='card-view'>
<view class='c-flex-row'>
<text class='card-view-title'>商品总价</text>
<text class='card-view-value'>{{ orderBean?.order?.totalPrice }}</text>
</view>
<view class='c-flex-row'>
<text class='card-view-title'>运费</text>
<text class='card-view-value'>0</text>
</view>
<view class='c-flex-row'>
<text class='card-view-title'>优惠券
<text style='font-size: 22rpx;color: #F32B2B;'>已选最大优惠</text>
</text>
<text class='card-view-value' style='color: #F32B2B;margin: 0'>-{{ orderBean?.order?.reducePrice }}
</text>
</view>
<view class='divider' />
<view class='c-flex-row'>
<view style='flex: 1' />
<view class='c-flex-row'>
<text class='card-view-value'>{{ orderBean?.order?.itemNum }}件商品 合计
<text style='font-size: 34rpx;color:#F32B2B'>¥{{ orderBean?.order?.totalPrice }}</text>
</text>
</view>
</view>
</view>
<view class='order-no c-flex-column'>
<text>订单编号{{ orderBean?.order?.id }}
<text style='color: #5B96FB;margin-left: 10rpx' @click.stop='copy(orderBean?.order?.id||"")'>复制</text>
</text>
<text>下单时间{{ orderBean?.order?.createTime }}</text>
<text>
支付状态
<text v-if='orderBean?.order?.payStatus == 2' style='color: #F32B2B'>已支付</text>
<text v-else-if='isPending(orderBean?.order)' style='color: #F32B2B'>待支付</text>
<text v-else style='color: #999999'>已过期</text>
</text>
</view>
<view class='bottom-action-view c-flex-row' v-if='isPending(orderBean?.order)'>
<text @click.stop='cancel' style='display: none'>取消订单</text>
<sqb-pay @navigateTo='navigateTo'
:return_url='buildSqbParams.return_url'
:total_amount='buildSqbParams.total_amount'
:terminal_sn='buildSqbParams.terminal_sn'
:client_sn='buildSqbParams.client_sn'
:subject='buildSqbParams.subject'
:merchant_name='buildSqbParams.merchant_name'
:notify_url='buildSqbParams.notify_url'
:sign='buildSqbParams.sign'>
<button @click='payment'>立即支付</button>
</sqb-pay>
</view>
</view>
</scroll-view>
</template>
<script lang='ts' setup>
import { assetsUrl, defaultImage } from '@/utils/assets';
import { getOrderDetail } from '@/api/order';
import { OrderBean } from '@/api/order/types';
import { copy, parseParameter, sortASCII } from '@/utils';
import { hexMD5 } from '@/utils/common/md5';
import { useUserStore } from '@/store';
import { getOrderDeadline, handlePayResult, isPending } from '@/utils/order';
import dayjs from 'dayjs';
const userState = useUserStore();
const { terminalInfo } = storeToRefs(userState);
const orderBean = ref<{
order: OrderBean,
stock: any[],
paymentList: []
}>();
let currentInterval = 0;
onLoad(async (e: any) => {
await fetchData(e.orderId);
handleCountdown();
});
const fetchData = async (orderId: string) => {
await uni.showLoading();
orderBean.value = await getOrderDetail(orderId);
if(orderBean.value?.order.address) {
orderBean.value.order.address = JSON.parse(orderBean?.value.order?.address);
}
uni.hideLoading();
};
onUnload(() => {
clearInterval(currentInterval);
});
const buildSqbParams = computed(() => {
const params = sortASCII({
client_sn: orderBean.value?.order?.id || '',
return_url: '/pages/common/payresult/index',
total_amount: Number(((orderBean.value?.order?.totalPrice || 0) * 100).toFixed(2)),
terminal_sn: terminalInfo.value.terminalSn,
subject: '商品支付',
merchant_name: terminalInfo.value.companyName,
notify_url: 'https://www.baidu.com'
}, true);
return {
...params,
sign: hexMD5(parseParameter(params) + '&key=' + terminalInfo.value.terminalKey).toUpperCase()
};
});
const navigateTo = (e: any) => {
handlePayResult(orderBean.value?.order.id, e, {
onSuccess: () => {
fetchData(orderBean.value?.order.id || '');
}
});
};
const handleCountdown = () => {
clearInterval(currentInterval);
currentInterval = setInterval(() => {
if(orderBean.value != undefined) {
if(isPending(orderBean.value?.order)) {
let second = (getOrderDeadline(orderBean.value?.order) - Date.now()) / 1000;
orderBean.value.order.countdown = second;
if(second <= 0) {
fetchData(orderBean.value?.order.id || '');
}
second--;
} else {
clearInterval(currentInterval);
fetchData(orderBean.value?.order.id || '');
}
}
}, 1000);
};
const cancel = () => {
};
const payment = () => {
console.log(buildSqbParams.value);
};
</script>
<style lang='scss' scoped>
.content {
padding: 20rpx 30rpx;
}
.countdown {
padding: 10rpx 30rpx 30rpx 30rpx;
image:nth-of-type(1) {
width: 60rpx;
height: 74rpx;
}
view:not(.c-flex-row):nth-of-type(1) {
margin-left: 23rpx;
text:nth-of-type(1) {
font-weight: bold;
font-size: 36rpx;
color: #333333;
}
view:nth-of-type(1) {
margin-top: 5rpx;
image {
width: 30rpx;
height: 30rpx;
}
text {
font-weight: 400;
font-size: 26rpx;
color: #333333;
margin-left: 8rpx;
}
}
}
}
.address-view {
padding-top: 30rpx;
background: #FFFFFF;
.user-info {
margin-left: 20rpx;
margin-right: 20rpx;
.status {
padding: 2rpx 10rpx;
font-size: 24rpx;
color: #F32B2B;
border-radius: 5rpx;
border: 1rpx solid #F32B2B;
margin-right: 12rpx;
}
text:nth-of-type(n+2) {
font-weight: bold;
font-size: 30rpx;
color: #333333;
margin-left: 10rpx;
}
image {
width: 8rpx;
height: 16rpx;
}
}
.addr {
margin-top: 15rpx;
margin-left: 20rpx;
image {
width: 30rpx;
height: 37rpx;
}
text {
font-weight: 400;
font-size: 26rpx;
color: #999999;
margin-left: 16rpx;
}
}
.dashed-line {
width: 100%;
height: 8rpx;
margin-top: 37rpx;
}
}
.goods-info-view {
display: flex;
position: relative;
background: #FFFFFF;
margin-top: 20rpx;
border-radius: 10rpx;
padding: 30rpx 20rpx;
.goods-image {
width: 180rpx;
height: 180rpx;
border-radius: 10rpx;
margin-right: 20rpx;
}
.goods-name {
font-size: 30rpx;
font-weight: 400;
-webkit-line-clamp: 2;
text-overflow: ellipsis;
overflow: hidden;
margin-right: 20rpx;
color: #333333;
flex: 1;
}
.bottom-view {
display: flex;
justify-content: space-between;
margin-top: 10rpx;
text:nth-of-type(1) {
font-size: 26rpx;
font-weight: 400;
flex: 1;
color: #999999;
}
text:nth-of-type(2) {
font-weight: bold;
font-size: 36rpx;
color: #333333;
}
text:nth-of-type(2):before {
content: '¥';
}
text:nth-of-type(3) {
font-size: 34rpx;
color: #333333;
font-weight: bold;
}
}
.divider {
margin: 30rpx 0;
}
.remark-view {
text {
font-weight: 400;
font-size: 28rpx;
color: #333333;
}
}
}
.card-view {
margin-top: 20rpx;
padding: 0 25rpx 0 23rpx;
view:not(.divider):nth-of-type(n+1) {
margin: 20rpx 0;
.card-view-title {
font-weight: 400;
font-size: 30rpx;
color: #333333;
flex: 1;
}
.card-view-value {
font-weight: 400;
font-size: 26rpx;
color: #333333;
image {
width: 8rpx;
height: 16rpx;
margin-left: 16rpx;
}
}
}
}
.order-no {
background: #FFFFFF;
border-radius: 10rpx;
padding: 22rpx 23rpx;
margin-top: 20rpx;
margin-bottom: 200rpx;
text:nth-of-type(n+1) {
font-weight: 400;
font-size: 26rpx;
color: #333333;
}
text:nth-of-type(3) {
margin-bottom: 0rpx;
}
}
.bottom-action-view {
background: #FFFFFF;
position: fixed;
height: 80rpx;
justify-content: flex-end;
bottom: 0;
left: 0;
right: 0;
padding: 12rpx 30rpx 78rpx 30rpx;
text:nth-of-type(1) {
font-weight: 400;
font-size: 30rpx;
color: #333333;
border-radius: 50rpx;
padding: 17rpx 45rpx;
border: 1rpx solid #707070;
margin-right: 20rpx;
}
sqb-pay button {
padding: 0 45rpx;
border-radius: 50rpx;
border: 2rpx solid #F32B2B;
font-weight: bold;
background: #FFFFFF;
font-size: 30rpx;
color: #F32B2B;
box-sizing: border-box;
}
}
</style>

View File

@@ -0,0 +1,104 @@
<template>
<tabbar :titles="['全部', '未支付', '已支付']" :index='payStatus' @change='tabChange' />
<u-list :list='orderList' :border='false' @scrolltolower='loadMore'>
<u-list-item v-for='(item,index) in orderList' :key='index'>
<order-item :item='item' @addShoppingCart='addShoppingCart' @pay='payment' />
</u-list-item>
<u-loadmore v-if='orderList.length>0' :status='loadingStatus' />
<u-empty v-if='orderList.length === 0' text='暂无数据' margin-top='100' />
</u-list>
<sku-dialog ref='skuDialogRef' :exists='temporaryStockBean' />
</template>
<script lang='ts' setup>
import { ref } from 'vue';
import { showToast } from '@/utils';
import { getOrderDeadline, isPending } from '@/utils/order';
import { getOrderList } from '@/api/order';
import OrderItem from '@/pages/mine/subs/order/components/order-item.vue';
import { OrderBean } from '@/api/order/types';
import SkuDialog from '@/components/sku-dialog.vue';
import { GoodsBean, StockBean } from '@/api/goods/types';
import useShoppingCartStore from '@/store/modules/shoppingcart';
const shoppingCartStore = useShoppingCartStore();
const skuDialogRef = ref();
const temporaryStockBean = ref<StockBean>();
const orderList = ref<OrderBean[]>([]);
const currentPageNum = ref(1);
const payStatus = ref(0);
const loadingStatus = ref('loading');
let currentInterval = 0;
onLoad((e: any) => {
const index = Number(e.index) || 0;
tabChange(index);
});
onUnload(() => {
clearInterval(currentInterval);
});
const tabChange = (index: number) => {
payStatus.value = index;
fetchData(true);
};
const fetchData = async (refresh: boolean = true) => {
loadingStatus.value = 'loading';
if(!refresh) {
currentPageNum.value += 1;
}
const { list } = await getOrderList({
pageNum: currentPageNum.value,
pageSize: 20,
obj: { payStatus: payStatus.value }
});
if(refresh) {
orderList.value = list;
} else {
orderList.value = orderList.value.concat(list);
}
loadingStatus.value = 'no';
handleCountdown(orderList.value.filter(item => item.payStatus == 1));
};
const handleCountdown = (items: OrderBean[]) => {
clearInterval(currentInterval);
currentInterval = setInterval(() => {
items.forEach(item => {
if(isPending(item)) {
let second = (getOrderDeadline(item) - Date.now()) / 1000;
item.countdown = second;
if(item.countdown <= 0) {
item.countdown = 0;
}
second--;
}
});
}, 1000);
};
const loadMore = () => {
fetchData(false);
};
const addShoppingCart = (item: GoodsBean) => {
skuDialogRef.value.show(item.goodsId, (e: GoodsBean) => {
shoppingCartStore.save(e);
showToast('加入购物车成功');
});
};
const payment = (orderId: string) => {
console.log(orderId);
};
</script>
<style lang='scss' scoped>
</style>

View File

@@ -0,0 +1,256 @@
<template>
<view class='content'>
<view class='top-card-view'>
<image class='bg-image' :src='companyConfigInfo.userbgcover' />
<image class='avatar-image' :src='params.image' />
<text>{{ userInfo.nickName }}</text>
<text>{{ userInfo.levelName }}</text>
</view>
<view class='basic-info-view c-flex-column'>
<view class='c-flex-row'>
<text>头像</text>
<view class='avatar-view'>
<image class='avatar-image' :src='params.image' />
<button class='avatar-btn' open-type='chooseAvatar' @chooseavatar='chooseAvatar' />
</view>
</view>
<view class='c-flex-row'>
<text class='nickname'>姓名</text>
<input placeholder='请输入姓名' type='nickname' v-model='userInfo.nickName' @change='bindNickname' />
</view>
<view class='divider' />
<view class='c-flex-row'>
<text>手机号</text>
<input placeholder='请输入手机号' :disabled='(userInfo.telephone?.length||0)!=0'
v-model='params.telephone'
@input='bindTelephone' />
</view>
<view class='divider' />
<view class='c-flex-row'>
<text>性别</text>
<view class='c-flex-row' @click.stop='changeGender(0)'>
<image class='gender-image'
:src='assetsUrl(params?.gender=="男"?"ic_checkbox_active.png":"ic_checkbox_normal.png")' />
<text class='gender-text'></text>
</view>
<view class='c-flex-row' @click.stop='changeGender(1)'>
<image class='gender-image'
:src='assetsUrl(params?.gender=="女"?"ic_checkbox_active.png":"ic_checkbox_normal.png")' />
<text class='gender-text'></text>
</view>
</view>
<view class='divider' />
<view class='c-flex-row'>
<text>生日</text>
<picker mode='date' :disabled='(userInfo.birthday?.length||0)!=0' @change='changeDate'>
<text>{{ params?.birthday || '请选择生日' }}</text>
</picker>
</view>
</view>
<button class='primary-button' @click.stop='save'>保存</button>
</view>
</template>
<script lang='ts' setup>
import { assetsUrl } from '@/utils/assets';
import { useUserStore } from '@/store';
import { showToast } from '@/utils';
import { updateProfile } from '@/api/user';
import dayjs from 'dayjs';
const store = useUserStore();
const { userInfo, companyConfigInfo } = storeToRefs(store);
const params = ref<{
avatarUrl: string | undefined;
image: string;
nickName: string;
telephone: string;
gender: string;
birthday: string;
}>({
avatarUrl: userInfo.value.image,
image: userInfo.value.image,
nickName: userInfo.value.nickName,
telephone: userInfo.value.telephone,
gender: userInfo.value.gender,
birthday: userInfo.value.birthday
});
onLoad(() => {
// store.getProfile();
});
const chooseAvatar = (e: any) => {
uni.showLoading();
uni.uploadFile({
url: import.meta.env.VITE_APP_BASE_API + '/wc/wechat/uploadImage',
filePath: e.detail.avatarUrl,
name: 'fileName',
header: {
'Content-Type': 'multipart/form-data'
},
success: (res: any) => {
params.value!.avatarUrl = JSON.parse(res.data).data;
params.value!.image = JSON.parse(res.data).data;
},
error: (err: any) => {
showToast('上传失败');
},
complete() {
uni.hideLoading();
}
});
};
const bindNickname = (e: any) => {
params.value!.nickName = e.detail.value;
};
const bindTelephone = (e: any) => {
params.value!.telephone = e.detail.value;
};
const changeGender = (index: number) => {
params.value!.gender = index == 0 ? '男' : '女';
};
const changeDate = (e: any) => {
params.value!.birthday = e.detail.value;
};
const save = async () => {
const newUserInfo = {
...userInfo.value,
...params.value
};
delete params.value.avatarUrl;
const result = await updateProfile({
...params.value,
birthday: dayjs(params.value.birthday).format('YYYY-MM-DD HH:mm:ss'),
birthdayType: 0
});
if(result) {
await store.setUserInfo(newUserInfo);
showToast('修改成功', {
icon: 'success', complete: () => {
uni.navigateBack();
}
});
} else {
showToast('服务异常,操作失败', { icon: 'error' });
}
};
</script>
<style lang='scss' scoped>
.top-card-view {
display: flex;
height: 338rpx;
position: relative;
margin: 30rpx;
.bg-image {
width: 100%;
height: 100%;
border-radius: 30rpx;
position: absolute;
}
.avatar-image {
width: 95rpx;
height: 95rpx;
margin-left: 36rpx;
margin-top: 60rpx;
background: #FFFFFF;
border: 1rpx solid #707070;
position: relative;
border-radius: 50%;
}
text:nth-of-type(1) {
font-size: 30rpx;
font-weight: 800;
color: #FFFFFF;
position: absolute;
top: 64rpx;
left: 158rpx;
}
text:nth-of-type(2) {
font-size: 24rpx;
font-weight: 400;
color: #FFFFFF;
position: absolute;
top: 116rpx;
left: 158rpx;
}
}
.basic-info-view {
margin: 0 30rpx 30rpx 30rpx;
.avatar-view {
display: flex;
position: relative;
.avatar-image {
width: 110rpx;
height: 110rpx;
border-radius: 50%;
}
.avatar-btn {
@extend .avatar-image;
position: absolute;
border: none;
background: #00000000;
}
}
input {
font-size: 30rpx;
font-weight: 400;
color: #333333;
}
view:not(.divider):nth-of-type(n+1) {
padding: 0 20rpx;
height: 120rpx;
.gender-image {
width: 37rpx;
height: 37rpx;
}
.gender-text {
width: 80rpx;
color: #333333;
font-size: 30rpx;
margin-left: 10rpx;
}
text:not(.gender-text):nth-of-type(1) {
font-size: 30rpx;
width: 120rpx;
font-weight: 400;
color: #7E7E7E;
}
.nickname:before {
content: '*';
color: #F32B2B;
}
}
}
</style>

View File

@@ -0,0 +1,289 @@
<template>
<view class='content'>
<view class='balance-view'>
<image :src='assetsUrl("bg_member_recharge.png")' />
<view class='balance-content'>
<text>累计余额</text>
<text>{{ userInfo?.totalIncoming }}</text>
</view>
<view class='balance-content'>
<text>当前余额</text>
<text class='accent-text-color'>{{ userInfo?.balance }}</text>
</view>
</view>
<view class='recharge-option-card'>
<view class='top-border'></view>
<text class='title'>充值金额</text>
<scroll-view scroll-y style='flex: 1;padding-bottom: 180rpx'>
<view class='option-item' :class="{'option-item-active': currentIndex==index}"
v-for='(item,index) in rechargeItems' :key='index' @click.stop='change(index)'>
<image v-if='currentIndex==index' class='checked-image' src='/static/images/ic_recharge_checked.png' />
<label class='amount-title'>{{ item.rechargeamount }}</label>
<view class='divider' style='margin-bottom: 36rpx' />
<view class='card-view'>
<view class='title-container'>
<text>充值{{ item?.rechargeamount || 0 }}{{ item?.rewardamount || 0
}}
</text>
<text class='accent-text-color'>
实得{{ item?.rechargeamount + (item?.rewardamount) }}
</text>
</view>
<text class='description'>{{ item.rewardamount || 0 }}{{ item.rewardcouponamount || 0
}}元优惠券共{{ item.rewardcouponnum || 0 }}
返利{{ item.rewardrefundamount || 0 }}
</text>
</view>
</view>
</scroll-view>
</view>
<view class='bottom-button-view'>
<!-- <payment-button style='flex: 1' :payParams='buildSignParams'>-->
<sqb-pay style='width: 100%;' @navigateTo='navigateTo'
:return_url='buildSqbParams.return_url'
:total_amount='buildSqbParams.total_amount'
:terminal_sn='buildSqbParams.terminal_sn'
:client_sn='buildSqbParams.client_sn'
:subject='buildSqbParams.subject'
:merchant_name='buildSqbParams.merchant_name'
:notify_url='buildSqbParams.notify_url'
:sign='buildSqbParams.sign'>
<button class='primary-button'>充值</button>
</sqb-pay>
<!-- </payment-button>-->
</view>
</view>
</template>
<script lang='ts' setup>
import { assetsUrl } from '@/utils/assets';
import { useUserStore } from '@/store';
import { getRechargeList, preRecharge, rechargeVerify } from '@/api/user';
import { parseParameter, sortASCII } from '@/utils';
import { hexMD5 } from '@/utils/common/md5';
import { handlePayResult } from '@/utils/order';
const userState = useUserStore();
const { userInfo, terminalInfo } = storeToRefs(userState);
const rechargeOptionBackgroundUrl = assetsUrl('bg_member_recharge_item.png');
const rechargeItems = ref<{
itemid: string
rechargeamount: number
rewardamount: number
rewardcouponamount: number,
rewardcouponnum: number
rechargetype: number,
rewardrefundamount: number
}[]>([]);
const currentIndex = ref<number>(0);
const preRechargeOrderId = ref();
onLoad(async () => {
const { ruleitems } = await getRechargeList();
rechargeItems.value = ruleitems;
// rechargeItems.value.push(rechargeItems.value[0]);
// rechargeItems.value.push(rechargeItems.value[0]);
// rechargeItems.value.push(rechargeItems.value[0]);
// rechargeItems.value.push(rechargeItems.value[0]);
console.log(rechargeItems.value);
await change(0);
});
const change = async (index: number) => {
currentIndex.value = index;
const { id } = await preRecharge({
itemid: rechargeItems.value[index].itemid
});
preRechargeOrderId.value = id;
};
const buildSqbParams = computed(() => {
const params = sortASCII({
client_sn: preRechargeOrderId.value,
return_url: '/pages/common/payresult/index',
total_amount: Number(((rechargeItems.value[currentIndex.value]?.rechargeamount || 0) * 100).toFixed(2)),
terminal_sn: terminalInfo.value.terminalSn,
subject: '充值',
merchant_name: terminalInfo.value.companyName,
notify_url: 'https://www.baidu.com'
}, true);
return {
...params,
sign: hexMD5(parseParameter(params) + '&key=' + terminalInfo.value.terminalKey).toUpperCase()
};
});
const navigateTo = (e: any) => {
handlePayResult(preRechargeOrderId.value, e, {
onSuccess: async () => {
await goRecharge();
await userState.setUserInfo({
...userInfo.value,
balance: Number(userInfo.value.balance) + Number(rechargeItems.value[currentIndex.value].rechargeamount) + Number(rechargeItems.value[currentIndex.value]?.rewardamount)
});
await uni.navigateBack();
},
onFailure: () => {
}
});
};
const goRecharge = async () => {
// const result = await recharge({
// itemid: rechargeItems.value[currentIndex.value].itemid
// });
// console.log('platform recharge result ', result);
await rechargeVerify({
payid: preRechargeOrderId.value
});
};
</script>
<style lang='scss' scoped>
.balance-view {
display: flex;
flex-direction: row;
height: 280rpx;
position: relative;
image {
width: 100%;
height: 100%;
position: absolute;
}
.balance-content {
display: flex;
flex-direction: column;
position: relative;
flex: 1;
align-items: center;
justify-content: center;
color: white;
text:nth-of-type(1) {
font-size: 30rpx;
}
text:nth-of-type(2) {
font-size: 40rpx;
margin-top: 10rpx;
}
}
}
.recharge-option-card {
display: flex;
flex-direction: column;
background: #F5F5F5;
position: relative;
//margin-top: -25rpx;
.top-border {
background: #FFFFFF;
height: 30rpx;
margin-top: -35rpx;
border-radius: 30rpx 30rpx 0 0;
}
.title {
font-size: 30rpx;
font-weight: bold;
color: #333333;
background: #FFFFFF;
border-radius: 10rpx 10rpx 0 0;
padding: 10rpx 20rpx 20rpx 20rpx;
margin-bottom: 20rpx;
margin-top: -15rpx;
}
.option-item {
display: flex;
flex-direction: column;
margin: 10rpx 20rpx 20rpx 20rpx;
background: #FFFFFF;
border-radius: 20rpx;
padding-top: 20rpx;
box-sizing: border-box;
border: solid #00000000 2rpx;
position: relative;
.amount-title {
font-weight: bold;
font-size: 40rpx;
width: fit-content;
color: #333333;
margin-left: 30rpx;
}
.amount-title:after {
content: "";
display: block;
border: 2rpx solid black;
}
.checked-image {
width: 70rpx;
height: 70rpx;
position: absolute;
right: 0;
top: 0;
border-radius: 0 18rpx 0 0
}
.card-view {
display: flex;
flex-direction: column;
width: 100%;
height: 167rpx;
border-radius: 20rpx;
//background-image: url(v-bind(rechargeOptionBackgroundUrl));
background-image: url('https://img.lakeapp.cn/wx/images/bg_member_recharge_item.png');
background-size: 100% 100%;
justify-content: flex-start;
color: #FFFFFF;
.title-container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-top: 27rpx;
padding: 0 30rpx;
text:nth-of-type(1) {
display: flex;
font-size: 36rpx;
}
text:nth-of-type(2) {
display: flex;
font-size: 30rpx;
align-items: center;
justify-content: center;
width: 185rpx;
height: 50rpx;
background: #FFFFFF;
border-radius: 25rpx;
}
}
.description {
margin-top: 30rpx;
margin-left: 30rpx;
}
}
}
.option-item-active {
@extend .option-item;
border: 2rpx solid #f95e5d;
}
}
</style>

View File

@@ -0,0 +1,48 @@
<template>
<view class='card-view'>
<!-- <text class='category-title'>2023-02-12</text>-->
<!-- <view class='item c-flex-column' v-for='(item,index) in 3' :key='index'>-->
<view class='c-flex-row'>
<text class='primary-text-color' style='flex: 1'>{{ item.name }}</text>
<text class='accent-text-color'>{{item.type===1?'-':'+'}}{{
item.transactionPrice || 0 }}</text>
</view>
<view class='c-flex-row'>
<text class='secondary-text-color' style='flex: 1'>{{ item.createTime }}</text>
<text style='color: #999999'>会员余额{{ item.finalPrice || 0 }}</text>
</view>
<!-- <view class='divider' style='margin-top: 20rpx' />-->
<!-- </view>-->
</view>
</template>
<script lang='ts' setup>
import { PropType } from 'vue';
defineProps({
item: Object as PropType<any>
});
</script>
<style lang='scss' scoped>
.card-view {
display: flex;
flex-direction: column;
padding: 16rpx 23rpx 20rpx 30rpx;
margin: 20rpx 0 10rpx 0;
}
.category-title {
font-size: 34rpx;
font-weight: bold;
color: #333333;
}
.item {
margin: 10rpx 0;
view:nth-of-type(2) {
margin-top: 10rpx;
}
}
</style>

View File

@@ -0,0 +1,103 @@
<template>
<tabbar :titles='["全部","充值","消费"]' @change='changeTab' :item-active-color='"#D95554"'
:indicator-color='"#D95554"' />
<view class='content'>
<!-- <view class='c-flex-row'>-->
<!-- <text>选择日期</text>-->
<!-- <picker mode='date' @change='changeDate'>-->
<!-- <view class='current-date c-flex-row'>-->
<!-- <text>{{ tradeDate }}</text>-->
<!-- <image :src='assetsUrl("ic_triangle_down.png")' />-->
<!-- </view>-->
<!-- </picker>-->
<!-- </view>-->
<u-list @scrolltolower='loadMore'>
<trade-item v-for='(item,index) in tradeList' :key='index' :item='item' />
<u-loadmore v-if='tradeList.length>0' :status='loadingStatus' />
<u-empty v-if='tradeList.length === 0' text='暂无数据' margin-top='100' />
</u-list>
</view>
</template>
<script lang='ts' setup>
import TradeItem from './components/trade-item.vue';
import { getTradeList } from '@/api/user';
import dayjs from 'dayjs';
const tradeList = ref([]);
const tradeType = ref('');
const tradeDate = ref();
const currentPageNum = ref(1);
const loadingStatus = ref('loading');
onLoad((e) => {
tradeDate.value = dayjs(Date.now()).format('YYYY-MM-DD');
fetchData();
});
const changeDate = (e: any) => {
tradeDate.value = e.detail.value;
fetchData();
};
const changeTab = (index: number) => {
switch (index) {
case 0:
tradeType.value = '';
break;
case 1:
tradeType.value = '会员充值';
break;
case 2:
tradeType.value = '余额消费';
break;
}
fetchData();
};
const fetchData = async (refresh: boolean = true) => {
currentPageNum.value = refresh ? 1 : currentPageNum.value + 1;
loadingStatus.value = 'loading';
const { list } = await getTradeList({
// startDate: tradeDate.value,
// endDate: dayjs(tradeDate.value).add(1, 'day').format('YYYY-MM-DD'),
name: tradeType.value,
pageNum: currentPageNum.value,
pageSize: 20
});
if(refresh) {
tradeList.value = list;
} else {
tradeList.value = tradeList.value.concat(list);
}
loadingStatus.value = 'no';
};
const loadMore = () => {
fetchData(false);
};
</script>
<style lang='scss' scoped>
.content {
padding: 20rpx 24rpx;
}
.current-date {
height: 50rpx;
background: #FFFFFF;
border-radius: 5rpx;
font-size: 28rpx;
font-weight: 400;
color: #333333;
padding: 0 20rpx;
margin-left: 10rpx;
image {
width: 19rpx;
height: 11rpx;
margin-left: 15rpx;
}
}
</style>

View File

@@ -1,25 +1,258 @@
<template>
<view class="flex flex-col items-center justify-center">
<image
class="mb-50rpx mt-200rpx h-200rpx w-200rpx"
src="@/static/images/logo.png"
width="200rpx"
height="200rpx"
/>
<view class="flex justify-center">
<text class="font-size-36rpx color-gray-700">
{{ title }}
</text>
<view class='content'>
<view class='member-info-view'>
<image :src='assetsUrl("ic_crown.png")' />
<view class='flex flex-col'>
<text>{{ userInfo?.levelName }}</text>
<view />
</view>
</view>
<view class='qrcode-card'>
<view class='balance-view'>
<text class='balance-text'>账户余额{{ userInfo?.balance || 0 }}</text>
<view class='btn-recharge' @click.stop='goPath("/pages/mine/subs/recharge/index")'>
<text>去充值</text>
<image :src='assetsUrl("ic_arrow_right.png")' />
</view>
</view>
<view class='divider' />
<view class='barcode-view'>
<image class='barcode' :src='code?.barCode'></image>
<text>{{ codeContent }}</text>
</view>
<view class='qrcode-view'>
<image class='qrcode' :src='code?.qrCode'></image>
<text>倒计时{{ codeRefreshInterval }}S后自动更新</text>
</view>
<view class='card-info-view'>
<view>
<image :src='assetsUrl("ic_card.png")' />
<text>卡信息</text>
</view>
<text>店铺名称{{ userInfo?.creatorName }}</text>
<text>{{ userInfo?.storeId }}</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
<script setup lang='ts'>
import { onLoad } from '@dcloudio/uni-app';
import { assetsUrl } from '@/utils/assets';
import { generateBarCode, generateQrCode } from '@/api/common';
import { getDynamicCode } from '@/api/user';
import { useUserStore } from '@/store';
const title = ref<string>();
title.value = import.meta.env.VITE_APP_TITLE;
import { goLogin, goPath, isLogin } from '@/utils';
const store = useUserStore();
console.log('store.user_name', store.user_name);
const { userInfo } = storeToRefs(store);
const code = ref<{ barCode: string, qrCode: string }>();
const codeContent = ref<string>('');
const codeRefreshInterval = ref(30);
onLoad(() => {
if(!isLogin()) {
goLogin();
return;
}
generateCode();
setInterval(() => {
codeRefreshInterval.value -= 1;
if(codeRefreshInterval.value == 0) {
codeRefreshInterval.value = 30;
generateCode();
}
}, 1000);
});
const generateCode = async () => {
const { dynccode } = await getDynamicCode();
codeContent.value = dynccode;
const barCode = await generateBarCode(codeContent.value);
const qrCode = await generateQrCode(codeContent.value);
code.value = {
barCode: convertBase64(barCode),
qrCode: convertBase64(qrCode)
};
};
const convertBase64 = (data: ArrayBuffer) => {
return 'data:image/png;base64,' + uni.arrayBufferToBase64(data);
};
</script>
<style lang='scss' scoped>
.content {
display: flex;
flex-direction: column;
padding: 28rpx 24rpx 0 24rpx;
}
.member-info-view {
display: flex;
flex-direction: row;
align-items: center;
image {
width: 39rpx;
height: 34rpx;
margin-right: 10rpx;
}
view:nth-of-type(1) {
text {
font-size: 32rpx;
font-weight: 800;
color: #333333;
}
view {
width: 100%;
height: 4rpx;
margin-top: 4rpx;
background: #333333
}
}
}
.qrcode-card {
display: flex;
flex-direction: column;
background: #FFFFFF;
border-radius: 20rpx;
padding: 25rpx;
margin-top: 27rpx;
.balance-view {
display: flex;
flex-direction: row;
align-items: center;
.balance-text {
font-size: 36rpx;
font-weight: bold;
color: #333333;
flex: 1;
}
.btn-recharge {
display: flex;
flex-direction: row;
width: 178rpx;
height: 63rpx;
align-items: center;
justify-content: center;
background: #333333;
border-radius: 32rpx;
text {
font-size: 30rpx;
font-weight: 400;
color: #FFFFFF;
white-space: nowrap;
margin-left: 10rpx;
text-align: center;
}
image {
width: 30rpx;
height: 30rpx;
margin-left: 13rpx;
}
}
}
.divider {
width: 100%;
height: 1rpx;
background: #D4D4D4;
margin-top: 35rpx;
margin-bottom: 50rpx;
}
.barcode-view {
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
image {
//width: 100%;
height: 130rpx;
}
text {
font-size: 26rpx;
font-weight: bold;
color: #333333;
margin-top: 10rpx;
}
}
.qrcode-view {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 57rpx;
image {
width: 432rpx;
height: 432rpx;
}
text {
font-size: 26rpx;
font-weight: 400;
color: #666666;
margin-top: 18rpx;
}
}
.card-info-view {
display: flex;
flex-direction: column;
margin-top: 57rpx;
margin-left: 5rpx;
view:nth-of-type(1) {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 20rpx;
image {
width: 35rpx;
height: 32rpx;
}
text {
font-size: 30rpx;
font-weight: 400;
margin-left: 10rpx;
color: #666666;
}
}
text:nth-of-type(1) {
font-size: 30rpx;
font-weight: 400;
color: #333333;
}
text:nth-of-type(2) {
font-size: 32rpx;
font-weight: bold;
margin-top: 18rpx;
color: #333333;
}
}
}
</style>

View File

@@ -3,7 +3,8 @@ import { getToken } from '@/utils/auth';
// 登录页面
const loginPage = '/pages/common/login/index';
// 页面白名单
const whiteList = ['/', '/pages/common/login/index', '/pages/home/index'];
const whiteList = ['/', '/pages/common/login/index', '/pages/home/index','/pages/common/register/index',
'/pages/mine/index','/pages/mine/subs/order/index'];
// 检查地址白名单
function checkWhite(url: string) {

View File

@@ -0,0 +1,7 @@
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "肃客会员",
"setting": {
"compileHotReLoad": true
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,4 +1,79 @@
page {
font-size: 28rpx;
background-color: #f9f9f8;
font-size: 25rpx;
background-color: #F5F5F5;
}
.primary-text-color {
color: #333333;
}
.secondary-text-color {
color: #666666;
}
.accent-text-color {
color: #F32B2B;
}
.primary-bg-color {
background: #FFFFFF;
}
.accent-bg-color {
background: #333333;
}
.c-flex-row {
display: flex;
flex-direction: row;
align-items: center;
}
.c-flex-column {
display: flex;
flex-direction: column;
}
.content {
@extend .c-flex-column;
}
.card-view {
@extend .c-flex-column;
background: #FFFFFF;
border-radius: 20rpx;
justify-content: center;
}
.divider {
width: 100%;
height: 0.3rpx;
background: #E4E4E4;
}
.bottom-button-view {
display: flex;
position: fixed;
background: #FFFFFF;
bottom: 0;
left: 0;
right: 0;
padding: 10rpx 30rpx 78rpx 30rpx;
button {
width: 100% !important;
}
}
.primary-button {
@extend .accent-bg-color;
display: flex;
height: 80rpx;
border-radius: 43rpx;
font-size: 30rpx;
font-weight: bold;
align-items: center;
justify-content: center;
min-width: 80%;
color: #FFFFFF;
}

View File

@@ -5,12 +5,12 @@ import { createPinia } from 'pinia';
import useAppStore from './modules/app';
import useUserStore from './modules/user';
// import piniaPersist from 'pinia-plugin-persist-uni';
import piniaPersist from 'pinia-plugin-persistedstate';
// 安装pinia状态管理插件
function setupStore(app: App) {
const store = createPinia();
// store.use(piniaPersist);
store.use(piniaPersist);
app.use(store);
}

View File

@@ -0,0 +1,71 @@
import { defineStore } from 'pinia';
import { GoodsBean, StockBean } from '@/api/goods/types';
import { getCompanyId } from '@/utils';
const useShoppingCartStore = defineStore('shoppingCart', {
state: (): { shoppingCartList: GoodsBean[] } => ({
shoppingCartList: uni.getStorageSync(`shoppingCart_${getCompanyId()}`) === '' ? [] : uni.getStorageSync(`shoppingCart_${getCompanyId()}`) as GoodsBean[]
}),
persist: {
// 修改存储中使用的键名称,默认为当前 Store的 id
key: 'shoppingCartState',
storage: {
setItem(key, value) {
uni.setStorageSync(key, value || []); // [!code warning]
uni.setStorageSync(`shoppingCart_${getCompanyId()}`, JSON.parse(value).shoppingCartList);
},
getItem(key) {
return uni.getStorageSync(uni.getStorageSync(`shoppingCart_${getCompanyId()}`)) || []; // [!code warning]
}
},
// 部分持久化状态的点符号路径数组,[]意味着没有状态被持久化(默认为undefined持久化整个状态)
paths: undefined
},
getters: {
getShoppingCartList(): GoodsBean[] {
return this.shoppingCartList;
},
totalCount(): number {
return this.shoppingCartList.length;
},
getSameGoodsIndex: (state) => (goodsId: string, colorId: string, sizeId: string) => {
return state.shoppingCartList?.findIndex(res => res.id === goodsId && res.checkedStock.colorId === colorId && res.checkedStock.sizeId === sizeId);
}
},
actions: {
save(partial: Partial<GoodsBean>) {
this.shoppingCartList.push(partial as GoodsBean);
},
updateCount(index: number, count: number) {
this.shoppingCartList[index].checkedStock.count += count;
},
updateStock(index: number, stock: StockBean) {
this.shoppingCartList[index].checkedStock = stock;
},
delete(index: number) {
this.shoppingCartList.splice(index, 1);
},
deleteIfChecked() {
this.shoppingCartList = this.shoppingCartList.filter(res => res.checked == undefined || res.checked === false);
},
resetData() {
this.shoppingCartList = uni.getStorageSync(`shoppingCart_${getCompanyId()}`);
},
clearAll() {
this.shoppingCartList = [];
}
}
});
export default useShoppingCartStore;

View File

@@ -1,69 +1,173 @@
import { defineStore } from 'pinia';
import type { UserState, providerType } from './types';
import {
getUserProfile,
loginByCode,
login as userLogin,
logout as userLogout,
} from '@/api/user/index';
import { clearToken, setToken } from '@/utils/auth';
import type { LoginParams } from '@/api/user/types';
import type { providerType, UserBean } from './types';
import { getTerminal, getUserProfile, login, logout as userLogout, register } from '@/api/user/index';
import { clearToken, getRegisterStoreId, setCompanyId, setToken } from '@/utils/auth';
import type { LoginResult, RegisterParams, TerminalBean } from '@/api/user/types';
import { getCompanyInfo } from '@/api/company';
const useUserStore = defineStore('user', {
state: (): UserState => ({
user_id: '',
user_name: '江阳小道',
avatar: '',
token: '',
}),
getters: {
userInfo(state: UserState): UserState {
return { ...state };
state: () => ({
userInfo: {} as UserBean,
terminalInfo: {} as TerminalBean,
companyInfo: {} as any,
companyConfigInfo: {
bannerhot: [] as any[],
bannerinx: [] as any[],
goodsdisable: 0,
goodsstockzero: 0,
mallopen: 0,
regqrcode: undefined,
userbgcover: undefined,
groupqrcode: undefined,
contactname: '',
contacttelephone: ''
},
deliveryAddress: {
addrid: '',
name: '',
mobile: '',
addr: '',
defaultstatus: 0
}
}),
persist: {
// 修改存储中使用的键名称,默认为当前 Store的 id
key: 'userState',
storage: {
setItem(key, value) {
uni.setStorageSync(key, value); // [!code warning]
},
getItem(key) {
return uni.getStorageSync(key); // [!code warning]
}
},
// 部分持久化状态的点符号路径数组,[]意味着没有状态被持久化(默认为undefined持久化整个状态)
paths: undefined
},
getters: {
// getUserInfo(state: UserBean): UserBean {
// return { state };
// }
getUserDiscount(): number {
if(this.userInfo?.levelEntity?.discount > 0 && this.userInfo?.levelEntity?.discount < 10) {
return this.userInfo?.levelEntity?.discount / 10;
} else if(this.userInfo?.levelEntity?.discount >= 10 && this.userInfo?.levelEntity?.discount <= 100) {
return this.userInfo?.levelEntity?.discount / 100;
}
return 1;
},
getRealGoodsPrice: (state) => (price: number, priceExt: number) => {
return state.userInfo.salePrice == 'price_ext' ? priceExt || 0 : price;
}
},
actions: {
// 设置用户的信息
setInfo(partial: Partial<UserState>) {
this.$patch(partial);
async setUserInfo(partial: Partial<UserBean>) {
this.userInfo = partial as UserBean;
// this.userInfo.levelEntity?.discount = 79;
// this.userInfo.salePrice = 'price_ext';
if(this.userInfo) {
this.userInfo.userDiscount = this.getUserDiscount;
await setCompanyId(this.userInfo.companyId);
} else {
await clearToken();
}
await this.fetchTerminal();
await this.fetchCompanyInfo();
},
setCompanyInfo(partial: Partial<any>) {
this.companyInfo = partial as any;
},
setDeliveryAddress(partial: Partial<any>) {
this.deliveryAddress = partial as any;
},
// 重置用户信息
resetInfo() {
this.$reset();
},
// 获取用户信息
async info() {
async getProfile() {
const result = await getUserProfile();
this.setInfo(result);
await this.setUserInfo(result);
},
// 异步登录并存储token
login(loginForm: LoginParams) {
async fetchCompanyInfo() {
this.companyConfigInfo = await getCompanyInfo();
},
userRegister(registerForm: RegisterParams) {
return new Promise(async (resolve, reject) => {
try {
const result = await userLogin(loginForm);
const token = result?.token;
if (token) {
setToken(token);
}
const result = await register(registerForm);
resolve(result);
} catch (error) {
reject(error)
reject(error);
}
});
},
// Logout
async logout() {
await userLogout();
this.resetInfo();
clearToken();
},
// 小程序授权登录
authLogin(provider: providerType = 'weixin') {
return new Promise((resolve, reject) => {
uni.login({
provider,
success: async (result: UniApp.LoginRes) => {
if (result.code) {
const res = await loginByCode({ code: result.code });
if(result.code) {
const wechatUserInfo = await uni.getUserInfo();
const userInfo = {
...wechatUserInfo.userInfo,
encryptedData: wechatUserInfo?.encryptedData,
rawData: JSON.parse(wechatUserInfo?.rawData),
signature: wechatUserInfo?.signature,
iv: wechatUserInfo.iv
};
const res = await login({
code: result.code,
userInfo: userInfo,
storeId: getRegisterStoreId()
// referrerUserId: '1727303781559697409'
// referrerUserId: getReferrerUserId()
});
if(res.user == undefined || res.user.id === null) {
const registerForm = {
unionId: res.user?.unionId,
openId: res.user?.openId,
maOpenId: res.user?.maOpenId,
image: res.user?.image,
nickName: res.user?.nickName,
telephone: res.user?.telephone,
birthday: res.user?.birthday,
companyId: res.user?.companyId,
creatorId: res.user?.creatorId,
gender: res.user?.gender,
storeId: getRegisterStoreId()
};
const registerResult = await this.userRegister(registerForm);
if(registerResult != null) {
setToken(res.token);
await this.setUserInfo(registerResult as LoginResult);
}
} else {
setToken(res.token);
await this.setUserInfo(res.user);
}
resolve(res);
} else {
reject(new Error(result.errMsg));
@@ -72,11 +176,15 @@ const useUserStore = defineStore('user', {
fail: (err: any) => {
console.error(`login error: ${err}`);
reject(err);
},
}
});
});
},
},
async fetchTerminal() {
this.terminalInfo = await getTerminal(this.userInfo?.companyId);
}
}
});
export default useUserStore;

View File

@@ -1,9 +1,56 @@
export type RoleType = '' | '*' | 'user';
export interface UserState {
user_id?: string;
user_name?: string;
avatar?: string;
export interface UserBean {
token?: string;
address: string;
balance: number;
birthday: string;
birthdayMD: string;
birthdayType: number;
card: string;
companyId: string;
consumption: string;
consumptionDay: string;
couponsCount: number;
createTime: string;
creatorId: string;
creatorName: string;
customerPrice: string;
email: string;
gender: string;
gold: string;
goodsNum: number;
id: string;
image: string | 'https://img.1216.top/lake/def_avatar.png';
integration: number;
lastConsumTime: string;
levelEntity: any;
levelId: string;
levelName: string;
maOpenId: string;
memberLabel: string;
name: string;
nickName: string;
openId: string;
orderNum: number;
ownerId: string;
ownerName: string;
registerDay: number;
relatedRate: number;
remark: string;
salePrice: string;
source: string;
status: string;
storeId: string;
telephone: string;
totalConsumption: string;
totalIncoming: number;
totalIntegral: number;
unionId: string;
updateTime: string;
useCouponsPrice: number;
wirelinedTelephone: string;
userDiscount:number
}
export type providerType =

View File

@@ -0,0 +1,60 @@
## 1.7.92022-04-02
- 修复 弹出层内部无法滚动的bug
## 1.7.82022-03-28
- 修复 小程序中高度错误的bug
## 1.7.72022-03-17
- 修复 快速调用open出现问题的Bug
## 1.7.62022-02-14
- 修复 safeArea 属性不能设置为false的bug
## 1.7.52022-01-19
- 修复 isMaskClick 失效的bug
## 1.7.42022-01-19
- 新增 cancelText \ confirmText 属性 ,可自定义文本
- 新增 maskBackgroundColor 属性 ,可以修改蒙版颜色
- 优化 maskClick属性 更新为 isMaskClick ,解决微信小程序警告的问题
## 1.7.32022-01-13
- 修复 设置 safeArea 属性不生效的bug
## 1.7.22021-11-26
- 优化 组件示例
## 1.7.12021-11-26
- 修复 vuedoc 文字错误
## 1.7.02021-11-19
- 优化 组件UI并提供设计资源详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-popup](https://uniapp.dcloud.io/component/uniui/uni-popup)
## 1.6.22021-08-24
- 新增 支持国际化
## 1.6.12021-07-30
- 优化 vue3下事件警告的问题
## 1.6.02021-07-13
- 组件兼容 vue3如何创建vue3项目详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 1.5.02021-06-23
- 新增 mask-click 遮罩层点击事件
## 1.4.52021-06-22
- 修复 nvue 平台中间弹出后点击内容再点击遮罩无法关闭的Bug
## 1.4.42021-06-18
- 修复 H5平台中间弹出后点击内容再点击遮罩无法关闭的Bug
## 1.4.32021-06-08
- 修复 错误的 watch 字段
- 修复 safeArea 属性不生效的问题
- 修复 点击内容再点击遮罩无法关闭的Bug
## 1.4.22021-05-12
- 新增 组件示例地址
## 1.4.12021-04-29
- 修复 组件内放置 input 、textarea 组件,无法聚焦的问题
## 1.4.0 2021-04-29
- 新增 type 属性的 left\right 值,支持左右弹出
- 新增 open(String:type) 方法参数 ,可以省略 type 属性 ,直接传入类型打开指定弹窗
- 新增 backgroundColor 属性,可定义主窗口背景色,默认不显示背景色
- 新增 safeArea 属性,是否适配底部安全区
- 修复 App\h5\微信小程序底部安全区占位不对的Bug
- 修复 App 端弹出等待的Bug
- 优化 提升低配设备性能,优化动画卡顿问题
- 优化 更简单的组件自定义方式
## 1.2.92021-02-05
- 优化 组件引用关系通过uni_modules引用组件
## 1.2.82021-02-05
- 调整为uni_modules目录规范
## 1.2.72021-02-05
- 调整为uni_modules目录规范
- 新增 支持 PC 端
- 新增 uni-popup-message 、uni-popup-dialog扩展组件支持 PC 端

View File

@@ -0,0 +1,45 @@
// #ifdef H5
export default {
name: 'Keypress',
props: {
disable: {
type: Boolean,
default: false
}
},
mounted () {
const keyNames = {
esc: ['Esc', 'Escape'],
tab: 'Tab',
enter: 'Enter',
space: [' ', 'Spacebar'],
up: ['Up', 'ArrowUp'],
left: ['Left', 'ArrowLeft'],
right: ['Right', 'ArrowRight'],
down: ['Down', 'ArrowDown'],
delete: ['Backspace', 'Delete', 'Del']
}
const listener = ($event) => {
if (this.disable) {
return
}
const keyName = Object.keys(keyNames).find(key => {
const keyName = $event.key
const value = keyNames[key]
return value === keyName || (Array.isArray(value) && value.includes(keyName))
})
if (keyName) {
// 避免和其他按键事件冲突
setTimeout(() => {
this.$emit(keyName, {})
}, 0)
}
}
document.addEventListener('keyup', listener)
this.$once('hook:beforeDestroy', () => {
document.removeEventListener('keyup', listener)
})
},
render: () => {}
}
// #endif

View File

@@ -0,0 +1,271 @@
<template>
<view class="uni-popup-dialog">
<view class="uni-dialog-title">
<text class="uni-dialog-title-text" :class="['uni-popup__'+dialogType]">{{titleText}}</text>
</view>
<view v-if="mode === 'base'" class="uni-dialog-content">
<slot>
<text class="uni-dialog-content-text">{{content}}</text>
</slot>
</view>
<view v-else class="uni-dialog-content">
<slot>
<input class="uni-dialog-input" v-model="val" type="text" :placeholder="placeholderText" :focus="focus" >
</slot>
</view>
<view class="uni-dialog-button-group">
<view class="uni-dialog-button" @click="closeDialog">
<text class="uni-dialog-button-text">{{closeText}}</text>
</view>
<view class="uni-dialog-button uni-border-left" @click="onOk">
<text class="uni-dialog-button-text uni-button-color">{{okText}}</text>
</view>
</view>
</view>
</template>
<script>
import popup from '../uni-popup/popup.js'
import {
initVueI18n
} from '@dcloudio/uni-i18n'
import messages from '../uni-popup/i18n/index.js'
const { t } = initVueI18n(messages)
/**
* PopUp 弹出层-对话框样式
* @description 弹出层-对话框样式
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
* @property {String} value input 模式下的默认值
* @property {String} placeholder input 模式下输入提示
* @property {String} type = [success|warning|info|error] 主题样式
* @value success 成功
* @value warning 提示
* @value info 消息
* @value error 错误
* @property {String} mode = [base|input] 模式、
* @value base 基础对话框
* @value input 可输入对话框
* @property {String} content 对话框内容
* @property {Boolean} beforeClose 是否拦截取消事件
* @event {Function} confirm 点击确认按钮触发
* @event {Function} close 点击取消按钮触发
*/
export default {
name: "uniPopupDialog",
mixins: [popup],
emits:['confirm','close'],
props: {
value: {
type: [String, Number],
default: ''
},
placeholder: {
type: [String, Number],
default: ''
},
type: {
type: String,
default: 'error'
},
mode: {
type: String,
default: 'base'
},
title: {
type: String,
default: ''
},
content: {
type: String,
default: ''
},
beforeClose: {
type: Boolean,
default: false
},
cancelText:{
type: String,
default: ''
},
confirmText:{
type: String,
default: ''
}
},
data() {
return {
dialogType: 'error',
focus: false,
val: ""
}
},
computed: {
okText() {
return this.confirmText || t("uni-popup.ok")
},
closeText() {
return this.cancelText || t("uni-popup.cancel")
},
placeholderText() {
return this.placeholder || t("uni-popup.placeholder")
},
titleText() {
return this.title || t("uni-popup.title")
}
},
watch: {
type(val) {
this.dialogType = val
},
mode(val) {
if (val === 'input') {
this.dialogType = 'info'
}
},
value(val) {
this.val = val
}
},
created() {
// 对话框遮罩不可点击
this.popup.disableMask()
// this.popup.closeMask()
if (this.mode === 'input') {
this.dialogType = 'info'
this.val = this.value
} else {
this.dialogType = this.type
}
},
mounted() {
this.focus = true
},
methods: {
/**
* 点击确认按钮
*/
onOk() {
if (this.mode === 'input'){
this.$emit('confirm', this.val)
}else{
this.$emit('confirm')
}
if(this.beforeClose) return
this.popup.close()
},
/**
* 点击取消按钮
*/
closeDialog() {
this.$emit('close')
if(this.beforeClose) return
this.popup.close()
},
close(){
this.popup.close()
}
}
}
</script>
<style lang="scss" >
.uni-popup-dialog {
width: 300px;
border-radius: 11px;
background-color: #fff;
}
.uni-dialog-title {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
padding-top: 25px;
}
.uni-dialog-title-text {
font-size: 16px;
font-weight: 500;
}
.uni-dialog-content {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
align-items: center;
padding: 20px;
}
.uni-dialog-content-text {
font-size: 14px;
color: #6C6C6C;
}
.uni-dialog-button-group {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
border-top-color: #f5f5f5;
border-top-style: solid;
border-top-width: 1px;
}
.uni-dialog-button {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex: 1;
flex-direction: row;
justify-content: center;
align-items: center;
height: 45px;
}
.uni-border-left {
border-left-color: #f0f0f0;
border-left-style: solid;
border-left-width: 1px;
}
.uni-dialog-button-text {
font-size: 16px;
color: #333;
}
.uni-button-color {
color: #007aff;
}
.uni-dialog-input {
flex: 1;
font-size: 14px;
border: 1px #eee solid;
height: 40px;
padding: 0 10px;
border-radius: 5px;
color: #555;
}
.uni-popup__success {
color: #4cd964;
}
.uni-popup__warn {
color: #f0ad4e;
}
.uni-popup__error {
color: #dd524d;
}
.uni-popup__info {
color: #909399;
}
</style>

View File

@@ -0,0 +1,143 @@
<template>
<view class="uni-popup-message">
<view class="uni-popup-message__box fixforpc-width" :class="'uni-popup__'+type">
<slot>
<text class="uni-popup-message-text" :class="'uni-popup__'+type+'-text'">{{message}}</text>
</slot>
</view>
</view>
</template>
<script>
import popup from '../uni-popup/popup.js'
/**
* PopUp 弹出层-消息提示
* @description 弹出层-消息提示
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
* @property {String} type = [success|warning|info|error] 主题样式
* @value success 成功
* @value warning 提示
* @value info 消息
* @value error 错误
* @property {String} message 消息提示文字
* @property {String} duration 显示时间,设置为 0 则不会自动关闭
*/
export default {
name: 'uniPopupMessage',
mixins:[popup],
props: {
/**
* 主题 success/warning/info/error 默认 success
*/
type: {
type: String,
default: 'success'
},
/**
* 消息文字
*/
message: {
type: String,
default: ''
},
/**
* 显示时间,设置为 0 则不会自动关闭
*/
duration: {
type: Number,
default: 3000
},
maskShow:{
type:Boolean,
default:false
}
},
data() {
return {}
},
created() {
this.popup.maskShow = this.maskShow
this.popup.messageChild = this
},
methods: {
timerClose(){
if(this.duration === 0) return
clearTimeout(this.timer)
this.timer = setTimeout(()=>{
this.popup.close()
},this.duration)
}
}
}
</script>
<style lang="scss" >
.uni-popup-message {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
}
.uni-popup-message__box {
background-color: #e1f3d8;
padding: 10px 15px;
border-color: #eee;
border-style: solid;
border-width: 1px;
flex: 1;
}
@media screen and (min-width: 500px) {
.fixforpc-width {
margin-top: 20px;
border-radius: 4px;
flex: none;
min-width: 380px;
/* #ifndef APP-NVUE */
max-width: 50%;
/* #endif */
/* #ifdef APP-NVUE */
max-width: 500px;
/* #endif */
}
}
.uni-popup-message-text {
font-size: 14px;
padding: 0;
}
.uni-popup__success {
background-color: #e1f3d8;
}
.uni-popup__success-text {
color: #67C23A;
}
.uni-popup__warn {
background-color: #faecd8;
}
.uni-popup__warn-text {
color: #E6A23C;
}
.uni-popup__error {
background-color: #fde2e2;
}
.uni-popup__error-text {
color: #F56C6C;
}
.uni-popup__info {
background-color: #F2F6FC;
}
.uni-popup__info-text {
color: #909399;
}
</style>

View File

@@ -0,0 +1,187 @@
<template>
<view class="uni-popup-share">
<view class="uni-share-title"><text class="uni-share-title-text">{{shareTitleText}}</text></view>
<view class="uni-share-content">
<view class="uni-share-content-box">
<view class="uni-share-content-item" v-for="(item,index) in bottomData" :key="index" @click.stop="select(item,index)">
<image class="uni-share-image" :src="item.icon" mode="aspectFill"></image>
<text class="uni-share-text">{{item.text}}</text>
</view>
</view>
</view>
<view class="uni-share-button-box">
<button class="uni-share-button" @click="close">{{cancelText}}</button>
</view>
</view>
</template>
<script>
import popup from '../uni-popup/popup.js'
import {
initVueI18n
} from '@dcloudio/uni-i18n'
import messages from '../uni-popup/i18n/index.js'
const { t } = initVueI18n(messages)
export default {
name: 'UniPopupShare',
mixins:[popup],
emits:['select'],
props: {
title: {
type: String,
default: ''
},
beforeClose: {
type: Boolean,
default: false
}
},
data() {
return {
bottomData: [{
text: '微信',
icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/c2b17470-50be-11eb-b680-7980c8a877b8.png',
name: 'wx'
},
{
text: '支付宝',
icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/d684ae40-50be-11eb-8ff1-d5dcf8779628.png',
name: 'wx'
},
{
text: 'QQ',
icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/e7a79520-50be-11eb-b997-9918a5dda011.png',
name: 'qq'
},
{
text: '新浪',
icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/0dacdbe0-50bf-11eb-8ff1-d5dcf8779628.png',
name: 'sina'
},
// {
// text: '百度',
// icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/1ec6e920-50bf-11eb-8a36-ebb87efcf8c0.png',
// name: 'copy'
// },
// {
// text: '其他',
// icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/2e0fdfe0-50bf-11eb-b997-9918a5dda011.png',
// name: 'more'
// }
]
}
},
created() {},
computed: {
cancelText() {
return t("uni-popup.cancel")
},
shareTitleText() {
return this.title || t("uni-popup.shareTitle")
}
},
methods: {
/**
* 选择内容
*/
select(item, index) {
this.$emit('select', {
item,
index
})
this.close()
},
/**
* 关闭窗口
*/
close() {
if(this.beforeClose) return
this.popup.close()
}
}
}
</script>
<style lang="scss" >
.uni-popup-share {
background-color: #fff;
border-top-left-radius: 11px;
border-top-right-radius: 11px;
}
.uni-share-title {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: center;
height: 40px;
}
.uni-share-title-text {
font-size: 14px;
color: #666;
}
.uni-share-content {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
padding-top: 10px;
}
.uni-share-content-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
flex-wrap: wrap;
width: 360px;
}
.uni-share-content-item {
width: 90px;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
padding: 10px 0;
align-items: center;
}
.uni-share-content-item:active {
background-color: #f5f5f5;
}
.uni-share-image {
width: 30px;
height: 30px;
}
.uni-share-text {
margin-top: 10px;
font-size: 14px;
color: #3B4144;
}
.uni-share-button-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
padding: 10px 15px;
}
.uni-share-button {
flex: 1;
border-radius: 50px;
color: #666;
font-size: 16px;
}
.uni-share-button::after {
border-radius: 50px;
}
</style>

View File

@@ -0,0 +1,7 @@
{
"uni-popup.cancel": "cancel",
"uni-popup.ok": "ok",
"uni-popup.placeholder": "pleace enter",
"uni-popup.title": "Hint",
"uni-popup.shareTitle": "Share to"
}

View File

@@ -0,0 +1,8 @@
import en from './en.json'
import zhHans from './zh-Hans.json'
import zhHant from './zh-Hant.json'
export default {
en,
'zh-Hans': zhHans,
'zh-Hant': zhHant
}

View File

@@ -0,0 +1,7 @@
{
"uni-popup.cancel": "取消",
"uni-popup.ok": "确定",
"uni-popup.placeholder": "请输入",
"uni-popup.title": "提示",
"uni-popup.shareTitle": "分享到"
}

View File

@@ -0,0 +1,7 @@
{
"uni-popup.cancel": "取消",
"uni-popup.ok": "確定",
"uni-popup.placeholder": "請輸入",
"uni-popup.title": "提示",
"uni-popup.shareTitle": "分享到"
}

View File

@@ -0,0 +1,45 @@
// #ifdef H5
export default {
name: 'Keypress',
props: {
disable: {
type: Boolean,
default: false
}
},
mounted () {
const keyNames = {
esc: ['Esc', 'Escape'],
tab: 'Tab',
enter: 'Enter',
space: [' ', 'Spacebar'],
up: ['Up', 'ArrowUp'],
left: ['Left', 'ArrowLeft'],
right: ['Right', 'ArrowRight'],
down: ['Down', 'ArrowDown'],
delete: ['Backspace', 'Delete', 'Del']
}
const listener = ($event) => {
if (this.disable) {
return
}
const keyName = Object.keys(keyNames).find(key => {
const keyName = $event.key
const value = keyNames[key]
return value === keyName || (Array.isArray(value) && value.includes(keyName))
})
if (keyName) {
// 避免和其他按键事件冲突
setTimeout(() => {
this.$emit(keyName, {})
}, 0)
}
}
document.addEventListener('keyup', listener)
// this.$once('hook:beforeDestroy', () => {
// document.removeEventListener('keyup', listener)
// })
},
render: () => {}
}
// #endif

View File

@@ -0,0 +1,26 @@
export default {
data() {
return {
}
},
created(){
this.popup = this.getParent()
},
methods:{
/**
* 获取父元素实例
*/
getParent(name = 'uniPopup') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false
parentName = parent.$options.name;
}
return parent;
},
}
}

View File

@@ -0,0 +1,474 @@
<template>
<view v-if="showPopup" class="uni-popup" :class="[popupstyle, isDesktop ? 'fixforpc-z-index' : '']">
<view @touchstart="touchstart">
<uni-transition key="1" v-if="maskShow" name="mask" mode-class="fade" :styles="maskClass"
:duration="duration" :show="showTrans" @click="onTap" />
<uni-transition key="2" :mode-class="ani" name="content" :styles="transClass" :duration="duration"
:show="showTrans" @click="onTap">
<view class="uni-popup__wrapper" :style="{ backgroundColor: bg }" :class="[popupstyle]" @click="clear">
<slot />
</view>
</uni-transition>
</view>
<!-- #ifdef H5 -->
<keypress v-if="maskShow" @esc="onTap" />
<!-- #endif -->
</view>
</template>
<script>
// #ifdef H5
import keypress from './keypress.js'
// #endif
/**
* PopUp 弹出层
* @description 弹出层组件,为了解决遮罩弹层的问题
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
* @property {String} type = [top|center|bottom|left|right|message|dialog|share] 弹出方式
* @value top 顶部弹出
* @value center 中间弹出
* @value bottom 底部弹出
* @value left 左侧弹出
* @value right 右侧弹出
* @value message 消息提示
* @value dialog 对话框
* @value share 底部分享示例
* @property {Boolean} animation = [true|false] 是否开启动画
* @property {Boolean} maskClick = [true|false] 蒙版点击是否关闭弹窗(废弃)
* @property {Boolean} isMaskClick = [true|false] 蒙版点击是否关闭弹窗
* @property {String} backgroundColor 主窗口背景色
* @property {String} maskBackgroundColor 蒙版颜色
* @property {Boolean} safeArea 是否适配底部安全区
* @event {Function} change 打开关闭弹窗触发e={show: false}
* @event {Function} maskClick 点击遮罩触发
*/
export default {
name: 'uniPopup',
components: {
// #ifdef H5
keypress
// #endif
},
emits: ['change', 'maskClick'],
props: {
// 开启动画
animation: {
type: Boolean,
default: true
},
// 弹出层类型可选值top: 顶部弹出层bottom底部弹出层center全屏弹出层
// message: 消息提示 ; dialog : 对话框
type: {
type: String,
default: 'center'
},
// maskClick
isMaskClick: {
type: Boolean,
default: null
},
// TODO 2 个版本后废弃属性 ,使用 isMaskClick
maskClick: {
type: Boolean,
default: null
},
backgroundColor: {
type: String,
default: 'none'
},
safeArea: {
type: Boolean,
default: true
},
maskBackgroundColor: {
type: String,
default: 'rgba(0, 0, 0, 0.4)'
},
},
watch: {
/**
* 监听type类型
*/
type: {
handler: function(type) {
if (!this.config[type]) return
this[this.config[type]](true)
},
immediate: true
},
isDesktop: {
handler: function(newVal) {
if (!this.config[newVal]) return
this[this.config[this.type]](true)
},
immediate: true
},
/**
* 监听遮罩是否可点击
* @param {Object} val
*/
maskClick: {
handler: function(val) {
this.mkclick = val
},
immediate: true
},
isMaskClick: {
handler: function(val) {
this.mkclick = val
},
immediate: true
},
// H5 下禁止底部滚动
showPopup(show) {
// #ifdef H5
// fix by mehaotian 处理 h5 滚动穿透的问题
document.getElementsByTagName('body')[0].style.overflow = show ? 'hidden' : 'visible'
// #endif
}
},
data() {
return {
duration: 300,
ani: [],
showPopup: false,
showTrans: false,
popupWidth: 0,
popupHeight: 0,
config: {
top: 'top',
bottom: 'bottom',
center: 'center',
left: 'left',
right: 'right',
message: 'top',
dialog: 'center',
share: 'bottom'
},
maskClass: {
position: 'fixed',
bottom: 0,
top: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(0, 0, 0, 0.4)'
},
transClass: {
position: 'fixed',
left: 0,
right: 0
},
maskShow: true,
mkclick: true,
popupstyle: this.isDesktop ? 'fixforpc-top' : 'top'
}
},
computed: {
isDesktop() {
return this.popupWidth >= 500 && this.popupHeight >= 500
},
bg() {
if (this.backgroundColor === '' || this.backgroundColor === 'none') {
return 'transparent'
}
return this.backgroundColor
}
},
mounted() {
const fixSize = () => {
const {
windowWidth,
windowHeight,
windowTop,
safeArea,
screenHeight,
safeAreaInsets
} = uni.getSystemInfoSync()
this.popupWidth = windowWidth
this.popupHeight = windowHeight + (windowTop || 0)
// TODO fix by mehaotian 是否适配底部安全区 ,目前微信ios 、和 app ios 计算有差异,需要框架修复
if (safeArea && this.safeArea) {
// #ifdef MP-WEIXIN
this.safeAreaInsets = screenHeight - safeArea.bottom
// #endif
// #ifndef MP-WEIXIN
this.safeAreaInsets = safeAreaInsets.bottom
// #endif
} else {
this.safeAreaInsets = 0
}
}
fixSize()
// #ifdef H5
// window.addEventListener('resize', fixSize)
// this.$once('hook:beforeDestroy', () => {
// window.removeEventListener('resize', fixSize)
// })
// #endif
},
// #ifndef VUE3
// TODO vue2
destroyed() {
this.setH5Visible()
},
// #endif
// #ifdef VUE3
// TODO vue3
unmounted() {
this.setH5Visible()
},
// #endif
created() {
// this.mkclick = this.isMaskClick || this.maskClick
if (this.isMaskClick === null && this.maskClick === null) {
this.mkclick = true
} else {
this.mkclick = this.isMaskClick !== null ? this.isMaskClick : this.maskClick
}
if (this.animation) {
this.duration = 300
} else {
this.duration = 0
}
// TODO 处理 message 组件生命周期异常的问题
this.messageChild = null
// TODO 解决头条冒泡的问题
this.clearPropagation = false
this.maskClass.backgroundColor = this.maskBackgroundColor
},
methods: {
setH5Visible() {
// #ifdef H5
// fix by mehaotian 处理 h5 滚动穿透的问题
document.getElementsByTagName('body')[0].style.overflow = 'visible'
// #endif
},
/**
* 公用方法,不显示遮罩层
*/
closeMask() {
this.maskShow = false
},
/**
* 公用方法,遮罩层禁止点击
*/
disableMask() {
this.mkclick = false
},
// TODO nvue 取消冒泡
clear(e) {
// #ifndef APP-NVUE
e.stopPropagation()
// #endif
this.clearPropagation = true
},
open(direction) {
// fix by mehaotian 处理快速打开关闭的情况
if (this.showPopup) {
clearTimeout(this.timer)
this.showPopup = false
}
let innerType = ['top', 'center', 'bottom', 'left', 'right', 'message', 'dialog', 'share']
if (!(direction && innerType.indexOf(direction) !== -1)) {
direction = this.type
}
if (!this.config[direction]) {
console.error('缺少类型:', direction)
return
}
this[this.config[direction]]()
this.$emit('change', {
show: true,
type: direction
})
},
close(type) {
this.showTrans = false
this.$emit('change', {
show: false,
type: this.type
})
clearTimeout(this.timer)
// // 自定义关闭事件
// this.customOpen && this.customClose()
this.timer = setTimeout(() => {
this.showPopup = false
}, 300)
},
// TODO 处理冒泡事件,头条的冒泡事件有问题 ,先这样兼容
touchstart() {
this.clearPropagation = false
},
onTap() {
if (this.clearPropagation) {
// fix by mehaotian 兼容 nvue
this.clearPropagation = false
return
}
this.$emit('maskClick')
if (!this.mkclick) return
this.close()
},
/**
* 顶部弹出样式处理
*/
top(type) {
this.popupstyle = this.isDesktop ? 'fixforpc-top' : 'top'
this.ani = ['slide-top']
this.transClass = {
position: 'fixed',
left: 0,
right: 0,
backgroundColor: this.bg
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
this.$nextTick(() => {
if (this.messageChild && this.type === 'message') {
this.messageChild.timerClose()
}
})
},
/**
* 底部弹出样式处理
*/
bottom(type) {
this.popupstyle = 'bottom'
this.ani = ['slide-bottom']
this.transClass = {
position: 'fixed',
left: 0,
right: 0,
bottom: 0,
// paddingBottom: this.safeAreaInsets + 'px',
backgroundColor: this.bg
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
},
/**
* 中间弹出样式处理
*/
center(type) {
this.popupstyle = 'center'
this.ani = ['zoom-out', 'fade']
this.transClass = {
position: 'fixed',
/* #ifndef APP-NVUE */
display: 'flex',
flexDirection: 'column',
/* #endif */
bottom: 0,
left: 0,
right: 0,
top: 0,
justifyContent: 'center',
alignItems: 'center'
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
},
left(type) {
this.popupstyle = 'left'
this.ani = ['slide-left']
this.transClass = {
position: 'fixed',
left: 0,
bottom: 0,
top: 0,
backgroundColor: this.bg,
/* #ifndef APP-NVUE */
display: 'flex',
flexDirection: 'column'
/* #endif */
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
},
right(type) {
this.popupstyle = 'right'
this.ani = ['slide-right']
this.transClass = {
position: 'fixed',
bottom: 0,
right: 0,
top: 0,
backgroundColor: this.bg,
/* #ifndef APP-NVUE */
display: 'flex',
flexDirection: 'column'
/* #endif */
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
}
}
}
</script>
<style lang="scss">
.uni-popup {
position: fixed;
/* #ifndef APP-NVUE */
z-index: 99;
/* #endif */
&.top,
&.left,
&.right {
/* #ifdef H5 */
top: var(--window-top);
/* #endif */
/* #ifndef H5 */
top: 0;
/* #endif */
}
.uni-popup__wrapper {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: relative;
/* iphonex 等安全区设置,底部安全区适配 */
/* #ifndef APP-NVUE */
// padding-bottom: constant(safe-area-inset-bottom);
// padding-bottom: env(safe-area-inset-bottom);
/* #endif */
&.left,
&.right {
/* #ifdef H5 */
padding-top: var(--window-top);
/* #endif */
/* #ifndef H5 */
padding-top: 0;
/* #endif */
flex: 1;
}
}
}
.fixforpc-z-index {
/* #ifndef APP-NVUE */
z-index: 999;
/* #endif */
}
.fixforpc-top {
top: 0;
}
</style>

View File

@@ -0,0 +1,90 @@
{
"id": "uni-popup",
"displayName": "uni-popup 弹出层",
"version": "1.7.9",
"description": " Popup 组件,提供常用的弹层",
"keywords": [
"uni-ui",
"弹出层",
"弹窗",
"popup",
"弹框"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
},
"uni_modules": {
"dependencies": [
"uni-scss",
"uni-transition"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,17 @@
## Popup 弹出层
> **组件名uni-popup**
> 代码块: `uPopup`
> 关联组件:`uni-transition`
弹出层组件,在应用中弹出一个消息提示窗口、提示框等
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-popup)
#### 如使用过程中有任何问题或者您对uni-ui有一些好的建议欢迎加入 uni-ui 交流群871950839

View File

@@ -0,0 +1,20 @@
## 1.3.12021-11-23
- 修复 init 方法初始化问题
## 1.3.02021-11-19
- 优化 组件UI并提供设计资源详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-transition](https://uniapp.dcloud.io/component/uniui/uni-transition)
## 1.2.12021-09-27
- 修复 init 方法不生效的 Bug
## 1.2.02021-07-30
- 组件兼容 vue3如何创建 vue3 项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 1.1.12021-05-12
- 新增 示例地址
- 修复 示例项目缺少组件的 Bug
## 1.1.02021-04-22
- 新增 通过方法自定义动画
- 新增 custom-class 非 NVUE 平台支持自定义 class 定制样式
- 优化 动画触发逻辑,使动画更流畅
- 优化 支持单独的动画类型
- 优化 文档示例
## 1.0.22021-02-05
- 调整为 uni_modules 目录规范

View File

@@ -0,0 +1,128 @@
// const defaultOption = {
// duration: 300,
// timingFunction: 'linear',
// delay: 0,
// transformOrigin: '50% 50% 0'
// }
// #ifdef APP-NVUE
const nvueAnimation = uni.requireNativePlugin('animation')
// #endif
class MPAnimation {
constructor(options, _this) {
this.options = options
this.animation = uni.createAnimation(options)
this.currentStepAnimates = {}
this.next = 0
this.$ = _this
}
_nvuePushAnimates(type, args) {
let aniObj = this.currentStepAnimates[this.next]
let styles = {}
if (!aniObj) {
styles = {
styles: {},
config: {}
}
} else {
styles = aniObj
}
if (animateTypes1.includes(type)) {
if (!styles.styles.transform) {
styles.styles.transform = ''
}
let unit = ''
if(type === 'rotate'){
unit = 'deg'
}
styles.styles.transform += `${type}(${args+unit}) `
} else {
styles.styles[type] = `${args}`
}
this.currentStepAnimates[this.next] = styles
}
_animateRun(styles = {}, config = {}) {
let ref = this.$.$refs['ani'].ref
if (!ref) return
return new Promise((resolve, reject) => {
nvueAnimation.transition(ref, {
styles,
...config
}, res => {
resolve()
})
})
}
_nvueNextAnimate(animates, step = 0, fn) {
let obj = animates[step]
if (obj) {
let {
styles,
config
} = obj
this._animateRun(styles, config).then(() => {
step += 1
this._nvueNextAnimate(animates, step, fn)
})
} else {
this.currentStepAnimates = {}
typeof fn === 'function' && fn()
this.isEnd = true
}
}
step(config = {}) {
// #ifndef APP-NVUE
this.animation.step(config)
// #endif
// #ifdef APP-NVUE
this.currentStepAnimates[this.next].config = Object.assign({}, this.options, config)
this.currentStepAnimates[this.next].styles.transformOrigin = this.currentStepAnimates[this.next].config.transformOrigin
this.next++
// #endif
return this
}
run(fn) {
// #ifndef APP-NVUE
this.$.animationData = this.animation.export()
this.$.timer = setTimeout(() => {
typeof fn === 'function' && fn()
}, this.$.durationTime)
// #endif
// #ifdef APP-NVUE
this.isEnd = false
let ref = this.$.$refs['ani'] && this.$.$refs['ani'].ref
if(!ref) return
this._nvueNextAnimate(this.currentStepAnimates, 0, fn)
this.next = 0
// #endif
}
}
const animateTypes1 = ['matrix', 'matrix3d', 'rotate', 'rotate3d', 'rotateX', 'rotateY', 'rotateZ', 'scale', 'scale3d',
'scaleX', 'scaleY', 'scaleZ', 'skew', 'skewX', 'skewY', 'translate', 'translate3d', 'translateX', 'translateY',
'translateZ'
]
const animateTypes2 = ['opacity', 'backgroundColor']
const animateTypes3 = ['width', 'height', 'left', 'right', 'top', 'bottom']
animateTypes1.concat(animateTypes2, animateTypes3).forEach(type => {
MPAnimation.prototype[type] = function(...args) {
// #ifndef APP-NVUE
this.animation[type](...args)
// #endif
// #ifdef APP-NVUE
this._nvuePushAnimates(type, args)
// #endif
return this
}
})
export function createAnimation(option, _this) {
if(!_this) return
clearTimeout(_this.timer)
return new MPAnimation(option, _this)
}

View File

@@ -0,0 +1,277 @@
<template>
<view v-if="isShow" ref="ani" :animation="animationData" :class="customClass" :style="transformStyles" @click="onClick"><slot></slot></view>
</template>
<script>
import { createAnimation } from './createAnimation'
/**
* Transition 过渡动画
* @description 简单过渡动画组件
* @tutorial https://ext.dcloud.net.cn/plugin?id=985
* @property {Boolean} show = [false|true] 控制组件显示或隐藏
* @property {Array|String} modeClass = [fade|slide-top|slide-right|slide-bottom|slide-left|zoom-in|zoom-out] 过渡动画类型
* @value fade 渐隐渐出过渡
* @value slide-top 由上至下过渡
* @value slide-right 由右至左过渡
* @value slide-bottom 由下至上过渡
* @value slide-left 由左至右过渡
* @value zoom-in 由小到大过渡
* @value zoom-out 由大到小过渡
* @property {Number} duration 过渡动画持续时间
* @property {Object} styles 组件样式,同 css 样式,注意带’-‘连接符的属性需要使用小驼峰写法如:`backgroundColor:red`
*/
export default {
name: 'uniTransition',
emits:['click','change'],
props: {
show: {
type: Boolean,
default: false
},
modeClass: {
type: [Array, String],
default() {
return 'fade'
}
},
duration: {
type: Number,
default: 300
},
styles: {
type: Object,
default() {
return {}
}
},
customClass:{
type: String,
default: ''
}
},
data() {
return {
isShow: false,
transform: '',
opacity: 1,
animationData: {},
durationTime: 300,
config: {}
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.open()
} else {
// 避免上来就执行 close,导致动画错乱
if (this.isShow) {
this.close()
}
}
},
immediate: true
}
},
computed: {
// 生成样式数据
stylesObject() {
let styles = {
...this.styles,
'transition-duration': this.duration / 1000 + 's'
}
let transform = ''
for (let i in styles) {
let line = this.toLine(i)
transform += line + ':' + styles[i] + ';'
}
return transform
},
// 初始化动画条件
transformStyles() {
return 'transform:' + this.transform + ';' + 'opacity:' + this.opacity + ';' + this.stylesObject
}
},
created() {
// 动画默认配置
this.config = {
duration: this.duration,
timingFunction: 'ease',
transformOrigin: '50% 50%',
delay: 0
}
this.durationTime = this.duration
},
methods: {
/**
* ref 触发 初始化动画
*/
init(obj = {}) {
if (obj.duration) {
this.durationTime = obj.duration
}
this.animation = createAnimation(Object.assign(this.config, obj),this)
},
/**
* 点击组件触发回调
*/
onClick() {
this.$emit('click', {
detail: this.isShow
})
},
/**
* ref 触发 动画分组
* @param {Object} obj
*/
step(obj, config = {}) {
if (!this.animation) return
for (let i in obj) {
try {
if(typeof obj[i] === 'object'){
this.animation[i](...obj[i])
}else{
this.animation[i](obj[i])
}
} catch (e) {
console.error(`方法 ${i} 不存在`)
}
}
this.animation.step(config)
return this
},
/**
* ref 触发 执行动画
*/
run(fn) {
if (!this.animation) return
this.animation.run(fn)
},
// 开始过度动画
open() {
clearTimeout(this.timer)
this.transform = ''
this.isShow = true
let { opacity, transform } = this.styleInit(false)
if (typeof opacity !== 'undefined') {
this.opacity = opacity
}
this.transform = transform
// 确保动态样式已经生效后,执行动画,如果不加 nextTick ,会导致 wx 动画执行异常
this.$nextTick(() => {
// TODO 定时器保证动画完全执行,目前有些问题,后面会取消定时器
this.timer = setTimeout(() => {
this.animation = createAnimation(this.config, this)
this.tranfromInit(false).step()
this.animation.run()
this.$emit('change', {
detail: this.isShow
})
}, 20)
})
},
// 关闭过度动画
close(type) {
if (!this.animation) return
this.tranfromInit(true)
.step()
.run(() => {
this.isShow = false
this.animationData = null
this.animation = null
let { opacity, transform } = this.styleInit(false)
this.opacity = opacity || 1
this.transform = transform
this.$emit('change', {
detail: this.isShow
})
})
},
// 处理动画开始前的默认样式
styleInit(type) {
let styles = {
transform: ''
}
let buildStyle = (type, mode) => {
if (mode === 'fade') {
styles.opacity = this.animationType(type)[mode]
} else {
styles.transform += this.animationType(type)[mode] + ' '
}
}
if (typeof this.modeClass === 'string') {
buildStyle(type, this.modeClass)
} else {
this.modeClass.forEach(mode => {
buildStyle(type, mode)
})
}
return styles
},
// 处理内置组合动画
tranfromInit(type) {
let buildTranfrom = (type, mode) => {
let aniNum = null
if (mode === 'fade') {
aniNum = type ? 0 : 1
} else {
aniNum = type ? '-100%' : '0'
if (mode === 'zoom-in') {
aniNum = type ? 0.8 : 1
}
if (mode === 'zoom-out') {
aniNum = type ? 1.2 : 1
}
if (mode === 'slide-right') {
aniNum = type ? '100%' : '0'
}
if (mode === 'slide-bottom') {
aniNum = type ? '100%' : '0'
}
}
this.animation[this.animationMode()[mode]](aniNum)
}
if (typeof this.modeClass === 'string') {
buildTranfrom(type, this.modeClass)
} else {
this.modeClass.forEach(mode => {
buildTranfrom(type, mode)
})
}
return this.animation
},
animationType(type) {
return {
fade: type ? 1 : 0,
'slide-top': `translateY(${type ? '0' : '-100%'})`,
'slide-right': `translateX(${type ? '0' : '100%'})`,
'slide-bottom': `translateY(${type ? '0' : '100%'})`,
'slide-left': `translateX(${type ? '0' : '-100%'})`,
'zoom-in': `scaleX(${type ? 1 : 0.8}) scaleY(${type ? 1 : 0.8})`,
'zoom-out': `scaleX(${type ? 1 : 1.2}) scaleY(${type ? 1 : 1.2})`
}
},
// 内置动画类型与实际动画对应字典
animationMode() {
return {
fade: 'opacity',
'slide-top': 'translateY',
'slide-right': 'translateX',
'slide-bottom': 'translateY',
'slide-left': 'translateX',
'zoom-in': 'scale',
'zoom-out': 'scale'
}
},
// 驼峰转中横线
toLine(name) {
return name.replace(/([A-Z])/g, '-$1').toLowerCase()
}
}
}
</script>
<style></style>

View File

@@ -0,0 +1,87 @@
{
"id": "uni-transition",
"displayName": "uni-transition 过渡动画",
"version": "1.3.1",
"description": "元素的简单过渡动画",
"keywords": [
"uni-ui",
"uniui",
"动画",
"过渡",
"过渡动画"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
},
"uni_modules": {
"dependencies": ["uni-scss"],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
## Transition 过渡动画
> **组件名uni-transition**
> 代码块: `uTransition`
元素过渡动画
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-transition)
#### 如使用过程中有任何问题或者您对uni-ui有一些好的建议欢迎加入 uni-ui 交流群871950839

4
src/utils/assets.ts Normal file
View File

@@ -0,0 +1,4 @@
export const assetsUrl = (name: string) => `https://img.lakeapp.cn/wx/images/${name}`;
export const defaultAvatar = 'https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4nibH0KlMECNjjGxQUq24ZEaGT4poC6icRiccVGKSyXwibcPq4BWmiaIGuG1icwxaQX6grC9VemZoJ8rg/132';
export const defaultImage = '/static/images/ic_noimage.svg';

View File

@@ -1,15 +1,61 @@
const TokenKey = 'admin-token';
const TokenKey = 'accessToken';
const CompanyIdKey = 'companyId';
const ReferrerUserIdKey = 'referrerUserId';
const RegisterStoreIdKey = 'storeId';
const TokenPrefix = 'Bearer ';
function isLogin() {
return !!uni.getStorageSync(TokenKey);
}
function getToken() {
return uni.getStorageSync(TokenKey);
}
function setToken(token: string) {
uni.setStorageSync(TokenKey, token);
}
function getCompanyId() {
return uni.getStorageSync(CompanyIdKey);
}
function setCompanyId(companyId: string) {
uni.setStorageSync(CompanyIdKey, companyId);
}
function getReferrerUserId() {
return uni.getStorageSync(ReferrerUserIdKey);
}
function setReferrerUserId(referrerUserId: string) {
uni.setStorageSync(ReferrerUserIdKey, referrerUserId);
}
function setRegisterStoreId(storeId: string) {
uni.setStorageSync(RegisterStoreIdKey, storeId);
}
function getRegisterStoreId() {
return uni.getStorageSync(RegisterStoreIdKey);
}
function clearToken() {
uni.removeStorageSync(TokenKey);
uni.removeStorageSync(CompanyIdKey);
}
export { TokenPrefix, isLogin, getToken, setToken, clearToken };
export {
TokenPrefix,
isLogin,
getToken,
setToken,
getCompanyId,
setCompanyId,
getReferrerUserId,
setReferrerUserId,
getRegisterStoreId,
setRegisterStoreId,
clearToken
};

View File

@@ -1,4 +1,5 @@
// 小程序更新检测
export function mpUpdate() {
const updateManager = uni.getUpdateManager();
updateManager.onCheckForUpdate((res) => {
@@ -10,11 +11,11 @@ export function mpUpdate() {
title: '更新提示',
content: '检测到新版本,是否下载新版本并重启小程序?',
success(res) {
if (res.confirm) {
if(res.confirm) {
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate();
}
},
}
});
});
updateManager.onUpdateFailed(() => {
@@ -22,7 +23,97 @@ export function mpUpdate() {
uni.showModal({
title: '已经有新版本了哟~',
content: '新版本已经上线啦~,请您删除当前小程序,重新搜索打开哟~',
showCancel: false,
showCancel: false
});
});
}
interface ToastOption {
icon?: 'none' | 'success' | 'loading' | 'error' | 'fail' | 'exception' | undefined,
duration?: number,
complete?: Function
}
export function showToast(title: string, { icon, duration, complete }: ToastOption = {}) {
const defaultDuration = 1500;
uni.showToast({
title: title,
icon: icon || 'none',
duration: duration || defaultDuration,
complete(result) {
setTimeout(() => {
if(complete != undefined) {
complete();
}
}, duration || defaultDuration);
}
});
}
export function goPath(path: string) {
if(path.includes('home/index') || path.includes('mall/index') || path.includes('qrcode/index') || path.includes('mine/index')) {
uni.switchTab({
url: path
}).then(r => {
});
} else {
uni.navigateTo({
url: path
}).then(r => {
});
}
}
export function goLogin() {
uni.navigateTo({
url: '/pages/common/login/index'
}).then(r => {
});
}
export function formatTimeWithZeroPad(num: number): string {
return num < 10 ? '0' + num : num + '';
}
export function sortASCII(obj: any, isSort = true) {
let arr: any[] = [];
Object.keys(obj).forEach(item => arr.push(item));
let sortArr = isSort ? arr.sort() : arr.sort().reverse();
let sortObj: any = {};
for (let i in sortArr) {
sortObj[sortArr[i]] = obj[sortArr[i]];
}
return sortObj;
}
// Array.prototype.contains = function(obj: any) {
// let i = this.length;
// while (i--) {
// if(this[i] === obj) {
// return true;
// }
// }
// return false;
// };
export function parseParameter(obj: any) {
if(obj === null || obj === undefined) return '';
const arr = [];
const keys: string[] = Object.keys(obj);
const entries: any[] = Object.entries(obj);
for (const [key, value] of entries) {
if(keys.includes(key) && !key.startsWith('function')) {
arr.push(key + '=' + value);
}
}
return arr.join('&');
}
export function copy(content: string) {
uni.setClipboardData({
data: content,
success: function(res) {
showToast('复制成功');
}
});
}

401
src/utils/common/md5.js Normal file
View File

@@ -0,0 +1,401 @@
/*
* JavaScript MD5
* https://github.com/blueimp/JavaScript-MD5
*
* Copyright 2011, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* https://opensource.org/licenses/MIT
*
* Based on
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
* Digest Algorithm, as defined in RFC 1321.
* Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
* Distributed under the BSD License
* See http://pajhome.org.uk/crypt/md5 for more info.
*/
/* global define */
/* eslint-disable strict */
'use strict'
/**
* Add integers, wrapping at 2^32.
* This uses 16-bit operations internally to work around bugs in interpreters.
*
* @param {number} x First integer
* @param {number} y Second integer
* @returns {number} Sum
*/
function safeAdd(x, y) {
var lsw = (x & 0xffff) + (y & 0xffff)
var msw = (x >> 16) + (y >> 16) + (lsw >> 16)
return (msw << 16) | (lsw & 0xffff)
}
/**
* Bitwise rotate a 32-bit number to the left.
*
* @param {number} num 32-bit number
* @param {number} cnt Rotation count
* @returns {number} Rotated number
*/
function bitRotateLeft(num, cnt) {
return (num << cnt) | (num >>> (32 - cnt))
}
/**
* Basic operation the algorithm uses.
*
* @param {number} q q
* @param {number} a a
* @param {number} b b
* @param {number} x x
* @param {number} s s
* @param {number} t t
* @returns {number} Result
*/
function md5cmn(q, a, b, x, s, t) {
return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b)
}
/**
* Basic operation the algorithm uses.
*
* @param {number} a a
* @param {number} b b
* @param {number} c c
* @param {number} d d
* @param {number} x x
* @param {number} s s
* @param {number} t t
* @returns {number} Result
*/
function md5ff(a, b, c, d, x, s, t) {
return md5cmn((b & c) | (~b & d), a, b, x, s, t)
}
/**
* Basic operation the algorithm uses.
*
* @param {number} a a
* @param {number} b b
* @param {number} c c
* @param {number} d d
* @param {number} x x
* @param {number} s s
* @param {number} t t
* @returns {number} Result
*/
function md5gg(a, b, c, d, x, s, t) {
return md5cmn((b & d) | (c & ~d), a, b, x, s, t)
}
/**
* Basic operation the algorithm uses.
*
* @param {number} a a
* @param {number} b b
* @param {number} c c
* @param {number} d d
* @param {number} x x
* @param {number} s s
* @param {number} t t
* @returns {number} Result
*/
function md5hh(a, b, c, d, x, s, t) {
return md5cmn(b ^ c ^ d, a, b, x, s, t)
}
/**
* Basic operation the algorithm uses.
*
* @param {number} a a
* @param {number} b b
* @param {number} c c
* @param {number} d d
* @param {number} x x
* @param {number} s s
* @param {number} t t
* @returns {number} Result
*/
function md5ii(a, b, c, d, x, s, t) {
return md5cmn(c ^ (b | ~d), a, b, x, s, t)
}
/**
* Calculate the MD5 of an array of little-endian words, and a bit length.
*
* @param {Array} x Array of little-endian words
* @param {number} len Bit length
* @returns {Array<number>} MD5 Array
*/
function binlMD5(x, len) {
/* append padding */
x[len >> 5] |= 0x80 << len % 32
x[(((len + 64) >>> 9) << 4) + 14] = len
var i
var olda
var oldb
var oldc
var oldd
var a = 1732584193
var b = -271733879
var c = -1732584194
var d = 271733878
for (i = 0; i < x.length; i += 16) {
olda = a
oldb = b
oldc = c
oldd = d
a = md5ff(a, b, c, d, x[i], 7, -680876936)
d = md5ff(d, a, b, c, x[i + 1], 12, -389564586)
c = md5ff(c, d, a, b, x[i + 2], 17, 606105819)
b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330)
a = md5ff(a, b, c, d, x[i + 4], 7, -176418897)
d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426)
c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341)
b = md5ff(b, c, d, a, x[i + 7], 22, -45705983)
a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416)
d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417)
c = md5ff(c, d, a, b, x[i + 10], 17, -42063)
b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162)
a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682)
d = md5ff(d, a, b, c, x[i + 13], 12, -40341101)
c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290)
b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329)
a = md5gg(a, b, c, d, x[i + 1], 5, -165796510)
d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632)
c = md5gg(c, d, a, b, x[i + 11], 14, 643717713)
b = md5gg(b, c, d, a, x[i], 20, -373897302)
a = md5gg(a, b, c, d, x[i + 5], 5, -701558691)
d = md5gg(d, a, b, c, x[i + 10], 9, 38016083)
c = md5gg(c, d, a, b, x[i + 15], 14, -660478335)
b = md5gg(b, c, d, a, x[i + 4], 20, -405537848)
a = md5gg(a, b, c, d, x[i + 9], 5, 568446438)
d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690)
c = md5gg(c, d, a, b, x[i + 3], 14, -187363961)
b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501)
a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467)
d = md5gg(d, a, b, c, x[i + 2], 9, -51403784)
c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473)
b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734)
a = md5hh(a, b, c, d, x[i + 5], 4, -378558)
d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463)
c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562)
b = md5hh(b, c, d, a, x[i + 14], 23, -35309556)
a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060)
d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353)
c = md5hh(c, d, a, b, x[i + 7], 16, -155497632)
b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640)
a = md5hh(a, b, c, d, x[i + 13], 4, 681279174)
d = md5hh(d, a, b, c, x[i], 11, -358537222)
c = md5hh(c, d, a, b, x[i + 3], 16, -722521979)
b = md5hh(b, c, d, a, x[i + 6], 23, 76029189)
a = md5hh(a, b, c, d, x[i + 9], 4, -640364487)
d = md5hh(d, a, b, c, x[i + 12], 11, -421815835)
c = md5hh(c, d, a, b, x[i + 15], 16, 530742520)
b = md5hh(b, c, d, a, x[i + 2], 23, -995338651)
a = md5ii(a, b, c, d, x[i], 6, -198630844)
d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415)
c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905)
b = md5ii(b, c, d, a, x[i + 5], 21, -57434055)
a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571)
d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606)
c = md5ii(c, d, a, b, x[i + 10], 15, -1051523)
b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799)
a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359)
d = md5ii(d, a, b, c, x[i + 15], 10, -30611744)
c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380)
b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649)
a = md5ii(a, b, c, d, x[i + 4], 6, -145523070)
d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379)
c = md5ii(c, d, a, b, x[i + 2], 15, 718787259)
b = md5ii(b, c, d, a, x[i + 9], 21, -343485551)
a = safeAdd(a, olda)
b = safeAdd(b, oldb)
c = safeAdd(c, oldc)
d = safeAdd(d, oldd)
}
return [a, b, c, d]
}
/**
* Convert an array of little-endian words to a string
*
* @param {Array<number>} input MD5 Array
* @returns {string} MD5 string
*/
function binl2rstr(input) {
var i
var output = ''
var length32 = input.length * 32
for (i = 0; i < length32; i += 8) {
output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff)
}
return output
}
/**
* Convert a raw string to an array of little-endian words
* Characters >255 have their high-byte silently ignored.
*
* @param {string} input Raw input string
* @returns {Array<number>} Array of little-endian words
*/
function rstr2binl(input) {
var i
var output = []
output[(input.length >> 2) - 1] = undefined
for (i = 0; i < output.length; i += 1) {
output[i] = 0
}
var length8 = input.length * 8
for (i = 0; i < length8; i += 8) {
output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32
}
return output
}
/**
* Calculate the MD5 of a raw string
*
* @param {string} s Input string
* @returns {string} Raw MD5 string
*/
function rstrMD5(s) {
return binl2rstr(binlMD5(rstr2binl(s), s.length * 8))
}
/**
* Calculates the HMAC-MD5 of a key and some data (raw strings)
*
* @param {string} key HMAC key
* @param {string} data Raw input string
* @returns {string} Raw MD5 string
*/
function rstrHMACMD5(key, data) {
var i
var bkey = rstr2binl(key)
var ipad = []
var opad = []
var hash
ipad[15] = opad[15] = undefined
if (bkey.length > 16) {
bkey = binlMD5(bkey, key.length * 8)
}
for (i = 0; i < 16; i += 1) {
ipad[i] = bkey[i] ^ 0x36363636
opad[i] = bkey[i] ^ 0x5c5c5c5c
}
hash = binlMD5(ipad.concat(rstr2binl(data)), 512 + data.length * 8)
return binl2rstr(binlMD5(opad.concat(hash), 512 + 128))
}
/**
* Convert a raw string to a hex string
*
* @param {string} input Raw input string
* @returns {string} Hex encoded string
*/
function rstr2hex(input) {
var hexTab = '0123456789abcdef'
var output = ''
var x
var i
for (i = 0; i < input.length; i += 1) {
x = input.charCodeAt(i)
output += hexTab.charAt((x >>> 4) & 0x0f) + hexTab.charAt(x & 0x0f)
}
return output
}
/**
* Encode a string as UTF-8
*
* @param {string} input Input string
* @returns {string} UTF8 string
*/
function str2rstrUTF8(input) {
return unescape(encodeURIComponent(input))
}
/**
* Encodes input string as raw MD5 string
*
* @param {string} s Input string
* @returns {string} Raw MD5 string
*/
function rawMD5(s) {
return rstrMD5(str2rstrUTF8(s))
}
/**
* Encodes input string as Hex encoded string
*
* @param {string} s Input string
* @returns {string} Hex encoded string
*/
export function hexMD5(s) {
return rstr2hex(rawMD5(s))
}
/**
* Calculates the raw HMAC-MD5 for the given key and data
*
* @param {string} k HMAC key
* @param {string} d Input string
* @returns {string} Raw MD5 string
*/
function rawHMACMD5(k, d) {
return rstrHMACMD5(str2rstrUTF8(k), str2rstrUTF8(d))
}
/**
* Calculates the Hex encoded HMAC-MD5 for the given key and data
*
* @param {string} k HMAC key
* @param {string} d Input string
* @returns {string} Raw MD5 string
*/
function hexHMACMD5(k, d) {
return rstr2hex(rawHMACMD5(k, d))
}
/**
* Calculates MD5 value for a given string.
* If a key is provided, calculates the HMAC-MD5 value.
* Returns a Hex encoded string unless the raw argument is given.
*
* @param {string} string Input string
* @param {string} [key] HMAC key
* @param {boolean} [raw] Raw output switch
* @returns {string} MD5 output
*/
function md5(string, key, raw) {
if (!key) {
if (!raw) {
return hexMD5(string)
}
return rawMD5(string)
}
if (!raw) {
return hexHMACMD5(key, string)
}
return rawHMACMD5(key, string)
}
module.exports = {
hexMD5: hexMD5,
}

58
src/utils/order/index.ts Normal file
View File

@@ -0,0 +1,58 @@
import { OrderBean } from '@/api/order/types';
import dayjs from 'dayjs';
import { showToast } from '@/utils';
/**
* 获取订单支付过期时间
* @param order
*/
export function getOrderDeadline(order: OrderBean) {
// return dayjs(order?.createTime).add(30, 'minute').toDate().getTime();
return dayjs(order?.createTime).add(30, 'minute').toDate().getTime();
}
/**
* 订单已过期
* @param item
*/
export const isExpired = (item: OrderBean) => {
return item?.payStatus == 1 && getOrderDeadline(item) < Date.now();
};
/**
* 订单待支付
* @param item
*/
export const isPending = (item: OrderBean | undefined) => {
return item?.payStatus == 1 && getOrderDeadline(item) > Date.now();
};
export const handlePayResult = (orderId: string | undefined, e: any, { onSuccess, onFailure }: any) => {
const payDetail = e.detail;
let url = payDetail.url;
const str = url.split('?')[1];
const resultObj = JSON.parse(str.replaceAll('result=', ''));
if(resultObj?.is_success === true) {
if(onSuccess) {
onSuccess();
} else {
showToast('支付成功', {
icon: 'success',
complete: () => {
uni.navigateBack();
}
});
}
} else {
if(onFailure) {
onFailure();
}
const msg = resultObj?.error_message || '支付失败';
showToast(msg, {
complete: () => {
uni.navigateBack();
}
});
}
};

View File

@@ -1,5 +1,5 @@
// 引入配置
import type { HttpRequestConfig } from 'uview-plus/libs/luch-request/index';
import type { HttpRequestConfig } from 'uview-plus/libs/luch-request';
import { requestInterceptors, responseInterceptors } from './interceptors';
import type { IResponse } from './type';
@@ -14,11 +14,15 @@ export function setupRequest() {
responseInterceptors();
}
export function request<T = any>(config: HttpRequestConfig): Promise<T> {
export function request<T = any>(config: HttpRequestConfig): Promise<T | any> {
return new Promise((resolve) => {
uni.$u.http.request(config).then((res: IResponse) => {
const { result } = res;
resolve(result as T);
if(res instanceof ArrayBuffer) {
resolve(res);
} else {
const { data } = res;
resolve(data as T);
}
});
});
}

View File

@@ -1,10 +1,6 @@
import type {
HttpError,
HttpRequestConfig,
HttpResponse,
} from 'uview-plus/libs/luch-request/index';
import type { HttpError, HttpRequestConfig, HttpResponse } from 'uview-plus/libs/luch-request';
import { showMessage } from './status';
import { getToken } from '@/utils/auth';
import { getCompanyId, getToken } from '@/utils/auth';
import useUserStore from '@/store/modules/user';
// 是否正在刷新token的标记
@@ -24,16 +20,20 @@ function requestInterceptors() {
config.data = config.data || {};
// token设置
const token = getToken();
if (token && config.header) {
if(token && config.header) {
config.header.token = token;
config.header.companyid = getCompanyId();
// config.header.contentType = "x-www-form-urlencoded"
}
return config;
},
(
config: any, // 可使用async await 做异步操作
) => Promise.reject(config),
config: any // 可使用async await 做异步操作
) => Promise.reject(config)
);
}
function responseInterceptors() {
/**
* 响应拦截
@@ -48,15 +48,19 @@ function responseInterceptors() {
// 自定义参数
const custom = config?.custom;
if(config.responseType === 'arraybuffer') {
return data;
}
// 请求成功则返回结果
if (data.code === 200) {
if(data.code === 200 || data?.retcode == 0) {
return data || {};
}
// 登录状态失效,重新登录
if (data.code === 401) {
if(data.code === 401) {
// 是否在获取token中,防止重复获取
if (!isRefreshing) {
if(!isRefreshing) {
// 修改登录状态为true
isRefreshing = true;
await useUserStore().authLogin();
@@ -64,7 +68,7 @@ function responseInterceptors() {
requestQueue.forEach(cb => cb());
// 重试完了清空这个队列
requestQueue = [];
isRefreshing = false;
// isRefreshing = false;
// 重新执行本次请求
return uni.$u.http.request(config);
} else {
@@ -78,26 +82,27 @@ function responseInterceptors() {
}
// 如果没有显式定义custom的toast参数为false的话默认对报错进行toast弹出提示
if (custom?.toast !== false) {
uni.$u.toast(data.message);
if(custom?.toast !== false) {
uni.$u.toast(data.message || data.retinfo);
}
// 如果需要catch返回则进行reject
if (custom?.catch) {
if(custom?.catch) {
return Promise.reject(data);
} else {
// 否则返回一个pending中的promise
return new Promise(() => {});
return new Promise(() => {
});
}
},
(response: HttpError) => {
if (response.statusCode) {
if(response.statusCode) {
// 请求已发出但是不在2xx的范围
showMessage(response.statusCode);
return Promise.reject(response.data);
}
showMessage('网络连接异常,请稍后再试!');
},
}
);
}

View File

@@ -1,7 +1,7 @@
// 返回res.data的interface
export interface IResponse<T = any> {
code: number | string;
result: T;
data: T;
message: string;
status: string | number;
}

View File

@@ -7,8 +7,14 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
CompanyDialog: typeof import('./../src/components/company-dialog.vue')['default']
OfficialAccountDialog: typeof import('./../src/components/official-account-dialog.vue')['default']
PageNav: typeof import('./../src/components/page-nav/page-nav.vue')['default']
PaymentButton: typeof import('./../src/components/payment-button.vue')['default']
PaymentDialog: typeof import('./../src/components/payment-dialog.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SkuDialog: typeof import('./../src/components/sku-dialog.vue')['default']
Tabbar: typeof import('./../src/components/tabbar/tabbar.vue')['default']
}
}